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 @@
| ';
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) . "'";
- foreach ($constituencies as $constituency) {
- $MEMBER = new MEMBER(['constituency' => $constituency]);
- $URL = new URL('mp');
- if ($MEMBER->valid) {
- $URL->insert(['m' => $MEMBER->member_id()]);
- }
- print '- ' . htmlentities($MEMBER->first_name()) . ' ' . htmlentities($MEMBER->last_name()) . '';
- print '(' . preg_replace("#$searchterm#i", '$0', $constituency) . ', ' . htmlentities($MEMBER->party()) . ')
';
- }
- print ' ';
- }
-}
-
-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 ' ';
+ }
+}
|