diff --git a/tests/SearchHighlightTest.php b/tests/SearchHighlightTest.php new file mode 100644 index 00000000..8def7bf0 --- /dev/null +++ b/tests/SearchHighlightTest.php @@ -0,0 +1,40 @@ +alert("x") Test'; + $result = highlighted_html($input, ''); + + $this->assertSame('<script>alert("x")</script> Test', $result); + } + + public function test_matches_are_highlighted_case_insensitively(): void { + $input = 'Canberra CANBERRA'; + $result = highlighted_html($input, 'canberra'); + + $this->assertSame('Canberra CANBERRA', $result); + } + + public function test_regex_characters_in_searchterm_are_treated_literally(): void { + $input = 'A+B?'; + $result = highlighted_html($input, 'A+B?'); + + $this->assertSame('<b>A+B?</b>', $result); + } + + public function test_no_match_returns_escaped_text_without_highlight(): void { + $input = 'Parliament House'; + $result = highlighted_html($input, 'senate'); + + $this->assertSame('Parliament House', $result); + } + +} diff --git a/www/docs/search/index.php b/www/docs/search/index.php index e0f4b4c4..a872012f 100644 --- a/www/docs/search/index.php +++ b/www/docs/search/index.php @@ -3,6 +3,7 @@ include_once __DIR__ . '/../../includes/easyparliament/init.php'; include_once __DIR__ . '/../../includes/easyparliament/member.php'; include_once __DIR__ . '/../../includes/easyparliament/glossary.php'; +include_once __DIR__ . '/../../includes/easyparliament/search.php'; // From http://cvs.sourceforge.net/viewcvs.py/publicwhip/publicwhip/website/ include_once __DIR__ . '/../../includes/postcode.php'; @@ -100,7 +101,7 @@ Date range - $speaker) { print ''; print $speaker['count'] . ''; @@ -198,230 +199,3 @@ ] ]); $PAGE->page_end(); - -function find_comments($args){ - $commentlist = new COMMENTLIST; - $commentlist->display('search', $args); -} - -function find_constituency($args){ - // We see if the user is searching for a postcode or constituency. - global $PAGE; - $db = getParlDB(); - - if ($args['s'] != '') { - $searchterm = $args['s']; - } else { - $PAGE->error_message('No search string'); - return false; - } - - $constituencies = []; - $constituency = ''; - $validpostcode = false; - - if (validate_postcode($searchterm)) { - // Looks like a postcode - can we find the constituency? - $constituencies = postcode_to_constituency($searchterm); - if ($constituencies == '') { - $constituencies = []; - } else { - $validpostcode = true; - } - if (!is_array($constituencies)) { - $constituencies = [$constituencies]; - } - } - - if ($constituencies == [] && $searchterm) { - // No luck so far - let's see if they're searching for a constituency. - $try = strtolower($searchterm); - if (normalise_constituency_name($try)) { - $constituency = normalise_constituency_name($try); - } else { - $q = $db->query("SELECT DISTINCT - (SELECT name FROM constituency WHERE cons_id = o.cons_id AND main_name) AS name - FROM constituency AS o WHERE name LIKE ? - AND from_date <= DATE(NOW()) AND DATE(NOW()) <= to_date", - "%$try%" - ); - for ($n = 0; $n < $q->rows(); $n++) { - $constituencies[] = $q->field($n, 'name'); - } - } - } - - if (count($constituencies) == 1) { - $constituency = $constituencies[0]; - } - - if ($constituency != '') { - // Got a match, display.... - - $MEMBER = new MEMBER(['constituency' => $constituency]); - $URL = new URL('mp'); - if ($MEMBER->valid) { - $URL->insert(['m' => $MEMBER->member_id()]); - print '

MP for ' . preg_replace("#$searchterm#i", '$0', $constituency); - if ($validpostcode) { - // Display the postcode the user searched for. - print ' (' . htmlentities(strtoupper($args['s'])) . ')'; - } - print "

"; - print "

generate() . "\">" . htmlentities($MEMBER->first_name()) . ' ' . htmlentities($MEMBER->last_name()) . " - (" . htmlentities($MEMBER->party()) . ")

"; - } - - } elseif (count($constituencies)) { - print "

MPs in constituencies matching '" . htmlentities($searchterm) . "'

'; - } -} - -function find_members($args){ - // Maybe there'll be a better place to put this at some point... - global $PAGE, $parties; - $db = getParlDB(); - - - - if ($args['s'] != '') { - // $args['s'] should have been tidied up by the time we get here. - // eg, by doing filter_user_input($s, 'strict'); - $searchstring = $args['s']; - } else { - $PAGE->error_message("No search string"); - return false; - } - - $searchwords = explode(' ', preg_replace('#[^a-z ]#i', '', $searchstring)); - foreach ($searchwords as $i => $searchword) { - $searchwords[$i] = htmlentities($searchword); - if (!strcasecmp($searchword, 'Opik')) - $searchwords[$i] = 'Öpik'; - } - - $params = []; - if (count($searchwords) == 1) { - $where = "first_name LIKE ? OR last_name LIKE ?"; - $params = ["%$searchwords[0]%", "%$searchwords[0]%"]; - } elseif (count($searchwords) == 2) { - // We don't do anything special if there are more than two search words. - // And here we're assuming the user's put the names in the right order. - $where = "(first_name LIKE ? AND last_name LIKE ?)"; - $where .= " OR (first_name LIKE ? AND last_name LIKE ?)"; - $params = [ - "%$searchwords[0]%", - "%$searchwords[1]%", - "%$searchwords[1]%", - "%$searchwords[0]%", - ]; - } else { - $where = "(first_name LIKE ? AND last_name LIKE ?)"; - $where .= " OR (first_name LIKE ? AND last_name LIKE ?)"; - $params = [ - "%$searchwords[0] $searchwords[1]%", - "%$searchwords[2]%", - "%$searchwords[0]%", - "%$searchwords[1] $searchwords[2]%", - ]; - } - $q = $db->query("SELECT person_id, - title, first_name, last_name, - constituency, party, - left_house, house - FROM member - WHERE ($where) - ORDER BY last_name, first_name, person_id, entered_house desc - ", ...$params); - - if ($q->rows() > 0) { - - $URL1 = new URL('mp'); - $URL2 = new URL('peer'); - $members = []; - - $last_pid = -1; - for ($n = 0; $n < $q->rows(); $n++) { - if ($q->field($n, 'person_id') != $last_pid) { - $last_pid = $q->field($n, 'person_id'); - if ($q->field($n, 'left_house') != '9999-12-31') { - $former = 'formerly '; - } else { - $former = ''; - } - if ($q->field($n, 'house') == 1) { - $URL1->insert(array('pid' => $last_pid)); - $s = ''; - $s .= $q->field($n, 'first_name') . ' ' . $q->field($n, 'last_name') . ' (' . $former . $q->field($n, 'constituency') . ', '; - } else { - $URL2->insert(array('pid' => $last_pid)); - $s = ''; - $s .= member_full_name($q->field($n, 'house'), $q->field($n, 'title'), $q->field($n, 'first_name'), $q->field($n, 'last_name'), $q->field($n, 'constituency')); - $s .= ' ('; - } - $party = $q->field($n, 'party'); - if (isset($parties[$party])) - $party = $parties[$party]; - $s .= $party . ')'; - $MOREURL = new URL('search'); - $MOREURL->insert(array('pid' => $last_pid, 'pop' => 1, 's' => null)); - $s .= ' - View recent appearances'; - $members[] = $s; - } - } - ?> -
-

Representatives matching ''

- -
- num_search_matches) && $GLOSSARY->num_search_matches >= 1) { - - // Got a match(es), display.... - $URL = new URL('glossary'); - $URL->insert(['gl' => ""]); - ?> -

Matching glossary terms:

-

- search_matches as $glossary_id => $term) { - $URL->update(['gl' => $glossary_id]); - ?> - num_search_matches) { - print ", "; - } - $n++; - } - ?> -

- page_end_mobile(); -function find_comments($args){ - $commentlist = new COMMENTLIST; - $commentlist->display('search', $args); -} - -function find_constituency($args){ - // We see if the user is searching for a postcode or constituency. - global $PAGE; - $db = getParlDB(); - - if ($args['s'] != '') { - $searchterm = $args['s']; - } else { - $PAGE->error_message('No search string'); - return false; - } - - $constituencies = []; - $constituency = ''; - $validpostcode = false; - - if (validate_postcode($searchterm)) { - // Looks like a postcode - can we find the constituency? - $constituencies = postcode_to_constituency($searchterm); - if ($constituencies == '') { - $constituencies = []; - } else { - $validpostcode = true; - } - if (!is_array($constituencies)) { - $constituencies = [$constituencies]; - } - } - - if ($constituencies == [] && $searchterm) { - // No luck so far - let's see if they're searching for a constituency. - $try = strtolower($searchterm); - if (normalise_constituency_name($try)) { - $constituency = normalise_constituency_name($try); - } else { - $q = $db->query("select distinct - (select name from constituency where cons_id = o.cons_id and main_name) as name - from constituency AS o where name like ? - and from_date <= date(now()) and date(now()) <= to_date", "%" . $try . "%"); - for ($n = 0; $n < $q->rows(); $n++) { - $constituencies[] = $q->field($n, 'name'); - } - } - } - - if (count($constituencies) == 1) { - $constituency = $constituencies[0]; - } - - if ($constituency != '') { - // Got a match, display.... - - $MEMBER = new MEMBER(array('constituency' => $constituency)); - $URL = new URL('mp'); - if ($MEMBER->valid) { - $URL->insert(['m' => $MEMBER->member_id()]); - print '

MP for ' . htmlentities(preg_replace("#$searchterm#i", '$0', $constituency)); - if ($validpostcode) { - // Display the postcode the user searched for. - print ' (' . htmlentities(strtoupper($args['s'])) . ')'; - } - - print "

"; - print "

generate() . "\">" . htmlentities($MEMBER->first_name()) . ' ' . htmlentities($MEMBER->last_name()) . ""; - print " (" . htmlentities($MEMBER->party()) . ")

"; - } - - } elseif (count($constituencies)) { - print "

MPs in constituencies matching '" . htmlentities($searchterm) . "'

'; - } -} - -function find_members($args){ - // Maybe there'll be a better place to put this at some point... - global $PAGE, $parties; - $db = getParlDB(); - - if ($args['s'] != '') { - // $args['s'] should have been tidied up by the time we get here. - // eg, by doing filter_user_input($s, 'strict'); - $searchstring = $args['s']; - } else { - $PAGE->error_message("No search string"); - return false; - } - - $searchwords = explode(' ', preg_replace('#[^a-z ]#i', '', $searchstring)); - $params = []; - - // Clean up searchwords and handle special cases - $cleaned_words = []; - foreach ($searchwords as $searchword) { - $word = htmlentities($searchword); - if (!strcasecmp($searchword, 'Opik')) { - $word = 'Öpik'; - } - if (!empty($word)) { - $cleaned_words[] = $word; - } - } - $searchwords = $cleaned_words; - - if (count($searchwords) == 1) { - $where = "first_name LIKE ? OR last_name LIKE ?"; - $params = ['%' . $searchwords[0] . '%', '%' . $searchwords[0] . '%']; - } elseif (count($searchwords) == 2) { - // We don't do anything special if there are more than two search words. - // And here we're assuming the user's put the names in the right order. - $where = "(first_name LIKE ? AND last_name LIKE ?) OR (first_name LIKE ? AND last_name LIKE ?)"; - $params = [ - "%$searchwords[0]%", - "%$searchwords[1]%", - "%$searchwords[1]%", - "%$searchwords[0]%" - ]; - } else { - $where = "(first_name LIKE ? AND last_name LIKE ?) OR (first_name LIKE ? AND last_name LIKE ?)"; - $params = [ - "%$searchwords[0] $searchwords[1]%", - "%$searchwords[2]%", - "%$searchwords[0]%", - "%$searchwords[1] $searchwords[2]%" - ]; - } - - $q = $db->query("SELECT person_id, - title, first_name, last_name, - constituency, party, - left_house, house - FROM member - WHERE ($where) - ORDER BY last_name, first_name, person_id, entered_house desc - ", ...$params); - - if ($q->rows() > 0) { - - $URL1 = new URL('mp'); - $URL2 = new URL('peer'); - $members = []; - - $last_pid = -1; - for ($n = 0; $n < $q->rows(); $n++) { - if ($q->field($n, 'person_id') != $last_pid) { - $last_pid = $q->field($n, 'person_id'); - if ($q->field($n, 'left_house') != '9999-12-31') { - $former = 'formerly '; - } else { - $former = ''; - } - if ($q->field($n, 'house') == 1) { - $URL1->insert(array('pid' => $last_pid)); - $s = ''; - $s .= $q->field($n, 'first_name') . ' ' . $q->field($n, 'last_name') . ' (' . $former . $q->field($n, 'constituency') . ', '; - } else { - $URL2->insert(array('pid' => $last_pid)); - $s = ''; - $s .= member_full_name($q->field($n, 'house'), $q->field($n, 'title'), $q->field($n, 'first_name'), $q->field($n, 'last_name'), $q->field($n, 'constituency')); - $s .= ' ('; - } - $party = $q->field($n, 'party'); - if (isset($parties[$party])) { - $party = $parties[$party]; - } - $s .= $party . ')'; - $MOREURL = new URL('search'); - $MOREURL->insert(array('pid' => $last_pid, 'pop' => 1, 's' => null)); - $s .= ' - View recent appearances'; - $members[] = $s; - } - } - ?> -
-

Representatives matching ''

- -
- num_search_matches) && $GLOSSARY->num_search_matches >= 1) { - - // Got a match(es), display.... - $URL = new URL('glossary'); - $URL->insert(['gl' => ""]); - - ?> -

Matching glossary terms:

-

- search_matches as $glossary_id => $term) { - $URL->update(['gl' => $glossary_id]); - ?> - num_search_matches) { - print ", "; - } - $n++; - } - ?> -

- $0', $escaped_text); +} + +/** + * Render comment search results. + * + * @param array $args + * Search arguments. + */ +function find_comments(array $args): void { + $commentlist = new COMMENTLIST(); + $commentlist->display('search', $args); +} + +/** + * Render constituency/postcode matches. + * + * @param array $args + * Search arguments. + * + * @return false|null + * Returns false on missing search input; otherwise null. + */ +function find_constituency(array $args) { + // We see if the user is searching for a postcode or constituency. + global $PAGE; + $db = getParlDB(); + + if ($args['s'] != '') { + $searchterm = $args['s']; + } else { + $PAGE->error_message('No search string'); + return false; + } + + $constituencies = []; + $constituency = ''; + $validpostcode = false; + + if (validate_postcode($searchterm)) { + // Looks like a postcode - can we find the constituency? + $constituencies = postcode_to_constituency($searchterm); + if ($constituencies == '') { + $constituencies = []; + } else { + $validpostcode = true; + } + if (!is_array($constituencies)) { + $constituencies = [$constituencies]; + } + } + + if ($constituencies == [] && $searchterm) { + // No luck so far - let's see if they're searching for a constituency. + $try = strtolower($searchterm); + if (normalise_constituency_name($try)) { + $constituency = normalise_constituency_name($try); + } else { + $q = $db->query( + "SELECT DISTINCT + (SELECT name FROM constituency WHERE cons_id = o.cons_id AND main_name) AS name + FROM constituency AS o WHERE name LIKE ? + AND from_date <= DATE(NOW()) AND DATE(NOW()) <= to_date", + "%$try%" + ); + for ($n = 0; $n < $q->rows(); $n++) { + $constituencies[] = $q->field($n, 'name'); + } + } + } + + if (count($constituencies) == 1) { + $constituency = $constituencies[0]; + } + + if ($constituency != '') { + // Got a match, display.... + $MEMBER = new MEMBER(['constituency' => $constituency]); + $URL = new URL('mp'); + if ($MEMBER->valid) { + $URL->insert(['m' => $MEMBER->member_id()]); + print '

MP for ' . highlighted_html($constituency, $searchterm); + if ($validpostcode) { + // Display the postcode the user searched for. + print ' (' . htmlentities(strtoupper($args['s'])) . ')'; + } + print '

'; + print "

generate()) . "\">" . htmlentities($MEMBER->first_name()) . ' ' . htmlentities($MEMBER->last_name()) . "\n (" . htmlentities($MEMBER->party()) . ")

"; + } + + } elseif (count($constituencies)) { + print "

MPs in constituencies matching '" . htmlentities($searchterm) . "'

'; + } +} + +/** + * Render member matches for the search term. + * + * @param array $args + * Search arguments. + * + * @return false|null + * Returns false on missing search input; otherwise null. + */ +function find_members(array $args) { + // Maybe there'll be a better place to put this at some point... + global $PAGE, $parties; + $db = getParlDB(); + + if ($args['s'] != '') { + // $args['s'] should have been tidied up by the time we get here. + // eg, by doing filter_user_input($s, 'strict'); + $searchstring = $args['s']; + } else { + $PAGE->error_message("No search string"); + return false; + } + + $searchwords = explode(' ', preg_replace('#[^a-z ]#i', '', $searchstring)); + $params = []; + + // Clean up searchwords and handle special cases. + $cleaned_words = []; + foreach ($searchwords as $searchword) { + $word = htmlentities($searchword); + if (!strcasecmp($searchword, 'Opik')) { + $word = 'Öpik'; + } + if (!empty($word)) { + $cleaned_words[] = $word; + } + } + $searchwords = $cleaned_words; + + if (count($searchwords) == 1) { + $where = "first_name LIKE ? OR last_name LIKE ?"; + $params = ['%' . $searchwords[0] . '%', '%' . $searchwords[0] . '%']; + } elseif (count($searchwords) == 2) { + // We don't do anything special if there are more than two search words. + // And here we're assuming the user's put the names in the right order. + $where = "(first_name LIKE ? AND last_name LIKE ?) OR (first_name LIKE ? AND last_name LIKE ?)"; + $params = [ + "%$searchwords[0]%", + "%$searchwords[1]%", + "%$searchwords[1]%", + "%$searchwords[0]%" + ]; + } else { + $where = "(first_name LIKE ? AND last_name LIKE ?) OR (first_name LIKE ? AND last_name LIKE ?)"; + $params = [ + "%$searchwords[0] $searchwords[1]%", + "%$searchwords[2]%", + "%$searchwords[0]%", + "%$searchwords[1] $searchwords[2]%" + ]; + } + + $q = $db->query("SELECT person_id, + title, first_name, last_name, + constituency, party, + left_house, house + FROM member + WHERE ($where) + ORDER BY last_name, first_name, person_id, entered_house desc + ", ...$params); + + if ($q->rows() > 0) { + + $URL1 = new URL('mp'); + $URL2 = new URL('peer'); + $members = []; + + $last_pid = -1; + for ($n = 0; $n < $q->rows(); $n++) { + if ($q->field($n, 'person_id') != $last_pid) { + $last_pid = $q->field($n, 'person_id'); + if ($q->field($n, 'left_house') != '9999-12-31') { + $former = 'formerly '; + } else { + $former = ''; + } + if ($q->field($n, 'house') == 1) { + $URL1->insert(['pid' => $last_pid]); + $s = ''; + $s .= htmlentities($q->field($n, 'first_name')) . ' ' . htmlentities($q->field($n, 'last_name')) . ' (' . $former . htmlentities($q->field($n, 'constituency')) . ', '; + } else { + $URL2->insert(['pid' => $last_pid]); + $s = ''; + $s .= htmlentities(member_full_name($q->field($n, 'house'), $q->field($n, 'title'), $q->field($n, 'first_name'), $q->field($n, 'last_name'), $q->field($n, 'constituency'))); + $s .= ' ('; + } + $party = $q->field($n, 'party'); + if (isset($parties[$party])) { + $party = $parties[$party]; + } + $s .= htmlentities($party) . ')'; + $MOREURL = new URL('search'); + $MOREURL->insert(['pid' => $last_pid, 'pop' => 1, 's' => null]); + $s .= ' - View recent appearances'; + $members[] = $s; + } + } + echo '
'; + echo "

Representatives matching '" . htmlentities($searchstring) . "'

"; + echo '
    '; + echo '
  • ' . implode("
  • \n\t
  • ", $members) . '
  • '; + echo '
'; + echo '
'; + } + + // We don't display anything if there were no matches. + +} + +/** + * Render links to glossary entries that match the search term. + * + * @param array $args + * Search arguments. + */ +function find_glossary_items(array $args): void { + + $searchterm = $args['s']; + $GLOSSARY = new GLOSSARY($args); + + if (isset($GLOSSARY->num_search_matches) && $GLOSSARY->num_search_matches >= 1) { + + // Got a match(es), display.... + $URL = new URL('glossary'); + $URL->insert(['gl' => ""]); + + echo '

Matching glossary terms:

'; + echo '

'; + $n = 1; + foreach ($GLOSSARY->search_matches as $glossary_id => $term) { + $URL->update(['gl' => $glossary_id]); + echo '' . htmlentities($term['title']) . ''; + if ($n < $GLOSSARY->num_search_matches) { + echo ', '; + } + $n++; + } + echo '

'; + } +}