1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
|
5
|
|
|
The Glossary item handles: |
6
|
|
|
1. Search matching for particular items. |
7
|
|
|
2. Addition of glossary items |
8
|
|
|
3. Removal of glossary items |
9
|
|
|
4. Notification of pending glossary additions |
10
|
|
|
|
11
|
|
|
Glossary items can only (at present) be added on the Search page, |
12
|
|
|
and only in the event that the term has not already been defined. |
13
|
|
|
|
14
|
|
|
[?] will it be possible to amend the term? |
15
|
|
|
|
16
|
|
|
It should not be possible to add a term if no results are found during the search. |
17
|
|
|
|
18
|
|
|
All Glossary items need to be confirmed by a moderator (unless posted by a moderator). |
19
|
|
|
As they are being approved/declined they can be modified (spelling etc...). |
20
|
|
|
|
21
|
|
|
*/ |
22
|
|
|
|
23
|
|
|
// This handles basic insertion and approval functions for all epobjects |
24
|
|
|
include_once INCLUDESPATH . "easyparliament/searchengine.php"; |
25
|
|
|
|
26
|
|
|
class GLOSSARY { |
27
|
|
|
public $num_terms; // how many glossary entries do we have |
28
|
|
|
// (changes depending on how GLOSSARY is called |
29
|
|
|
public $hansard_count; // how many times does the phrase appear in hansard? |
30
|
|
|
public $query; // search term |
31
|
|
|
public $glossary_id; // if this is set then we only have 1 glossary term |
32
|
|
|
public $current_term; // will only be set if we have a valid epobject_id |
33
|
|
|
public $current_letter; |
34
|
|
|
|
35
|
|
|
// constructor... |
36
|
|
|
public function __construct($args = []) { |
37
|
8 |
|
// We can optionally start the glossary with one of several arguments |
38
|
|
|
// 1. glossary_id - treat the glossary as a single term |
39
|
|
|
// 2. glossary_term - search within glossary for a term |
40
|
|
|
// With no argument it will pick up all items. |
41
|
|
|
|
42
|
|
|
$this->db = new ParlDB(); |
|
|
|
|
43
|
8 |
|
|
44
|
|
|
$this->replace_order = []; |
|
|
|
|
45
|
8 |
|
if (isset($args['s']) && ($args['s'] != "")) { |
46
|
8 |
|
$args['s'] = urldecode($args['s']); |
47
|
|
|
$this->search_glossary($args); |
48
|
|
|
} |
49
|
|
|
$got = $this->get_glossary_item($args); |
50
|
8 |
|
if ($got && isset($args['sort']) && ($args['sort'] == 'regexp_replace')) { |
51
|
8 |
|
// We need to sort the terms in the array by "number of words in term". |
52
|
|
|
// This way, "prime minister" gets dealt with before "minister" when generating glossary links. |
53
|
|
|
|
54
|
|
|
// sort by number of words |
55
|
|
|
foreach ($this->terms as $glossary_id => $term) { |
56
|
8 |
|
$this->replace_order[$glossary_id] = count(explode(" ", $term['title'])); |
57
|
8 |
|
} |
58
|
|
|
arsort($this->replace_order); |
59
|
8 |
|
|
60
|
|
|
// secondary sort for number of letters? |
61
|
|
|
// pending functionality... |
62
|
|
|
|
63
|
|
|
// We can either turn off the "current term" completely - |
64
|
|
|
// so that it never links to its own page, |
65
|
|
|
// Or we can handle it in $this->glossarise below |
66
|
|
|
/* |
67
|
|
|
if (isset($this->epobject_id)) { |
68
|
|
|
unset ($this->replace_order[$this->epobject_id]); |
69
|
|
|
} |
70
|
|
|
*/ |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
// These stop stupid submissions. |
74
|
|
|
// everything should be lowercase. |
75
|
|
|
$this->stopwords = [ "the", "of", "to", "and", "for", "in", "a", "on", "is", "that", "will", "secretary", "are", "ask", "state", "have", "be", "has", "by", "with", "i", "not", "what", "as", "it", "hon", "he", "which", "from", "if", "been", "this", "s", "we", "at", "government", "was", "my", "an", "department", "there", "make", "or", "made", "their", "all", "but", "they", "how", "debate" ]; |
|
|
|
|
76
|
8 |
|
|
77
|
|
|
} |
78
|
8 |
|
|
79
|
|
|
public function get_glossary_item($args = []) { |
80
|
8 |
|
// Search for and fetch glossary item with title or glossary_id |
81
|
|
|
// We could also search glossary text that contains the title text, for cross references |
82
|
|
|
|
83
|
|
|
$this->alphabet = []; |
|
|
|
|
84
|
8 |
|
foreach (range("A", "Z") as $letter) { |
85
|
8 |
|
$this->alphabet[$letter] = []; |
86
|
8 |
|
} |
87
|
|
|
|
88
|
|
|
$q = $this->db->query("SELECT g.glossary_id, g.title, g.body, u.user_id, u.firstname, u.lastname |
89
|
8 |
|
FROM editqueue AS eq, glossary AS g, users AS u |
90
|
|
|
WHERE g.glossary_id=eq.glossary_id AND u.user_id=eq.user_id AND g.visible=1 AND eq.approved=1 |
91
|
|
|
ORDER by g.title"); |
92
|
|
|
if ($q->success() && $q->rows()) { |
93
|
8 |
|
foreach ($q as $row) { |
94
|
8 |
|
$this->terms[$row["glossary_id"]] = $row; |
|
|
|
|
95
|
8 |
|
// Now add the epobject to the alphabet navigation. |
96
|
|
|
$first_letter = strtoupper(substr($row["title"], 0, 1)); |
97
|
8 |
|
$this->alphabet[$first_letter][] = $row["glossary_id"]; |
98
|
8 |
|
} |
99
|
|
|
|
100
|
|
|
$this->num_terms = $q->rows(); |
101
|
8 |
|
|
102
|
|
|
// If we were given a glossary_id, then we need one term in particular, |
103
|
|
|
// as well as knowing the next and previous terms for the navigation |
104
|
|
|
if (isset($args['glossary_id']) && ($args['glossary_id'] != "")) { |
105
|
8 |
|
$next = 0; |
106
|
|
|
$first_term = null; |
107
|
|
|
foreach ($this->terms as $term) { |
108
|
|
|
if (!$first_term) { |
109
|
|
|
$first_term = $term; |
110
|
|
|
} |
111
|
|
|
$last_term = $term; |
112
|
|
|
if ($next == 1) { |
113
|
|
|
$this->next_term = $term; |
|
|
|
|
114
|
|
|
break; |
115
|
|
|
} elseif ($term['glossary_id'] == $args['glossary_id']) { |
116
|
|
|
$this->glossary_id = $args['glossary_id']; |
117
|
|
|
$this->current_term = $term; |
118
|
|
|
$next = 1; |
119
|
|
|
|
120
|
|
|
} else { |
121
|
|
|
$this->previous_term = $term; |
|
|
|
|
122
|
|
|
} |
123
|
|
|
} |
124
|
|
|
// The first term in the list has no previous, so we'll make it the last term |
125
|
|
|
if (!isset($this->previous_term)) { |
126
|
|
|
$this->previous_term = $last_term; |
|
|
|
|
127
|
|
|
} |
128
|
|
|
// and the last has no next, so we'll make it the first |
129
|
|
|
if (!isset($this->next_term)) { |
130
|
|
|
$this->next_term = $first_term; |
131
|
|
|
} |
132
|
|
|
} |
133
|
|
|
|
134
|
8 |
|
return ($this->num_terms); |
135
|
|
|
} else { |
136
|
|
|
return false; |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
public function search_glossary($args = []) { |
141
|
|
|
// Search for and fetch glossary item with a title |
142
|
|
|
// Useful for the search page, and nowhere else (so far) |
143
|
|
|
|
144
|
|
|
$this->query = addslashes($args['s']); |
145
|
|
|
$this->search_matches = []; |
|
|
|
|
146
|
|
|
$this->num_search_matches = 0; |
|
|
|
|
147
|
|
|
|
148
|
|
|
$query = "SELECT g.glossary_id, g.title, g.body, u.user_id, u.firstname, u.lastname |
149
|
|
|
FROM editqueue AS eq, glossary AS g, users AS u |
150
|
|
|
WHERE g.glossary_id=eq.glossary_id AND u.user_id=eq.user_id AND g.visible=1 |
151
|
|
|
AND g.title LIKE '%" . $this->query . "%' |
152
|
|
|
ORDER by g.title"; |
153
|
|
|
$q = $this->db->query($query); |
154
|
|
|
if ($q->success() && $q->rows()) { |
155
|
|
|
foreach ($q as $row) { |
156
|
|
|
$this->search_matches[$row["glossary_id"]] = $row; |
157
|
|
|
} |
158
|
|
|
$this->num_search_matches = $q->rows(); |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
public function create(&$data) { |
163
|
|
|
// Add a Glossary definition. |
164
|
|
|
// Sets visiblity to 0, and awaits moderator intervention. |
165
|
|
|
// For this we need to start up an epobject of type 2 and then an editqueue item |
166
|
|
|
// where editqueue.epobject_id_l = epobject.epobject_id |
167
|
|
|
|
168
|
|
|
$EDITQUEUE = new \MySociety\TheyWorkForYou\GlossaryEditQueue(); |
169
|
|
|
|
170
|
|
|
// Assuming that everything is ok, we will need: |
171
|
|
|
// For epobject: |
172
|
|
|
// title VARCHAR(255), |
173
|
|
|
// body TEXT, |
174
|
|
|
// type INTEGER, |
175
|
|
|
// created DATETIME, |
176
|
|
|
// modified DATETIME, |
177
|
|
|
// and for editqueue: |
178
|
|
|
// edit_id INTEGER PRIMARY KEY NOT NULL, |
179
|
|
|
// user_id INTEGER, |
180
|
|
|
// edit_type INTEGER, |
181
|
|
|
// epobject_id_l INTEGER, |
182
|
|
|
// title VARCHAR(255), |
183
|
|
|
// body TEXT, |
184
|
|
|
// submitted DATETIME, |
185
|
|
|
// editor_id INTEGER, |
186
|
|
|
// approved BOOLEAN, |
187
|
|
|
// decided DATETIME |
188
|
|
|
|
189
|
|
|
global $THEUSER; |
190
|
|
|
|
191
|
|
|
if (!$THEUSER->is_able_to('addterm')) { |
192
|
|
|
error("Sorry, you are not allowed to add Glossary terms."); |
193
|
|
|
return false; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
if ($data['title'] == '') { |
197
|
|
|
error("Sorry, you can't define a term without a title"); |
198
|
|
|
return false; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
if ($data['body'] == '') { |
202
|
|
|
error("You haven't entered a definition!"); |
203
|
|
|
return false; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
if (is_numeric($THEUSER->user_id()) && !$THEUSER->status == 'Superuser') { |
207
|
|
|
// Flood check - make sure the user hasn't just posted a term recently. |
208
|
|
|
// To help prevent accidental duplicates, among other nasty things. |
209
|
|
|
|
210
|
|
|
$flood_time_limit = 20; // How many seconds until a user can post again? |
211
|
|
|
|
212
|
|
|
$q = $this->db->query("SELECT glossary_id, submitted, submitted + 0 as s, NOW() as n, NOW() - $flood_time_limit as f |
213
|
|
|
FROM editqueue |
214
|
|
|
WHERE user_id = '" . $THEUSER->user_id() . "' |
215
|
|
|
AND submitted + 0 > NOW() - $flood_time_limit"); |
216
|
|
|
|
217
|
|
|
if ($q->rows() > 0) { |
218
|
|
|
$data['error'] = "Sorry, we limit people to posting one term per $flood_time_limit seconds to help prevent duplicate postings. Please go back and try again, thanks."; |
219
|
|
|
return $data; |
220
|
|
|
} |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
// OK, let's get on with it... |
224
|
|
|
|
225
|
|
|
// Tidy up the HTML tags |
226
|
|
|
// (but we don't make URLs into links; only when displaying the comment). |
227
|
|
|
// We can display Glossary terms the same as the comments |
228
|
|
|
$data['title'] = filter_user_input($data['title'], 'comment_title'); // In utility.php |
229
|
|
|
$data['body'] = filter_user_input($data['body'], 'comment'); // In utility.php |
230
|
|
|
// Add the time and the edit type for the editqueue |
231
|
|
|
$data['posted'] = date('Y-m-d H:i:s', time()); |
232
|
|
|
$data['edit_type'] = 2; |
233
|
|
|
|
234
|
|
|
// Add the item to the edit queue |
235
|
|
|
$success = $EDITQUEUE->add($data); |
236
|
|
|
|
237
|
|
|
if ($success) { |
238
|
|
|
// if the user is a super user then just add the entry without confirmation |
239
|
|
|
if ($THEUSER->status == 'Superuser') { |
240
|
|
|
$EDITQUEUE->approve(['approvals' => [$success], 'epobject_type' => 2]); |
241
|
|
|
} |
242
|
|
|
return ($success); |
243
|
|
|
} else { |
244
|
|
|
return false; |
245
|
|
|
} |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
public function delete($glossary_id) { |
249
|
|
|
$q = $this->db->query("DELETE from glossary where glossary_id=$glossary_id LIMIT 1;"); |
250
|
8 |
|
// if that worked, we need to update the editqueue, |
251
|
|
|
// and remove the term from the already generated object list. |
252
|
|
|
if ($q->affected_rows() >= 1) { |
253
|
8 |
|
unset($this->replace_order[$glossary_id]); |
254
|
|
|
unset($this->terms[$glossary_id]); |
255
|
8 |
|
} |
256
|
8 |
|
} |
257
|
8 |
|
|
258
|
8 |
|
public function glossarise($body, $tokenize = 0, $urlize = 0) { |
|
|
|
|
259
|
|
|
// Turn a body of text into a link-up wonderland of glossary joy |
260
|
|
|
|
261
|
|
|
global $this_page; |
262
|
|
|
|
263
|
8 |
|
$findwords = []; |
264
|
|
|
$replacewords = []; |
265
|
|
|
$URL = new \MySociety\TheyWorkForYou\Url("glossary"); |
266
|
|
|
$URL->insert(["gl" => ""]); |
267
|
|
|
|
268
|
|
|
// External links shown within their own definition |
269
|
|
|
// should be the complete and linked url. |
270
|
|
|
// NB. This should only match when $body is a definition beginning with "https:" |
271
|
|
|
if (is_string($body) && preg_match("/^(https?:*[^\s])$/i", $body)) { |
272
|
8 |
|
$body = "<a href=\"" . $body . "\" title=\"External link to " . $body . "\">" . $body . "</a>"; |
273
|
|
|
return ($body); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
// otherwise, just replace everything. |
277
|
8 |
|
|
278
|
8 |
|
// generate links from URL when wanted |
279
|
|
|
// NB WRANS is already doing this |
280
|
|
|
if ($urlize == 1) { |
281
|
|
|
$body = preg_replace("~(http(s)?:\/\/[^\s\n]*)\b(\/)?~i", "<a href=\"\\0\">\\0</a>", $body); |
282
|
8 |
|
} |
283
|
8 |
|
|
284
|
|
|
// check for any glossary terms to replace |
285
|
8 |
|
foreach ($this->replace_order as $glossary_id => $count) { |
286
|
|
|
if ($glossary_id == $this->glossary_id) { |
287
|
8 |
|
continue; |
288
|
|
|
} |
289
|
8 |
|
|
290
|
|
|
$term_body = $this->terms[$glossary_id]['body']; |
291
|
|
|
$term_title = $this->terms[$glossary_id]['title']; |
292
|
8 |
|
|
293
|
|
|
$URL->update(["gl" => $glossary_id]); |
294
|
|
|
# The regex here ensures that the phrase is only matched if it's not already within <a> tags, preventing double-linking. Kudos to http://stackoverflow.com/questions/7798829/php-regular-expression-to-match-keyword-outside-html-tag-a |
295
|
8 |
|
$findwords[$glossary_id] = "/\b(" . $term_title . ")\b(?!(?>[^<]*(?:<(?!\/?a\b)[^<]*)*)<\/a>)/i"; |
296
|
|
|
// catch glossary terms within their own definitions |
297
|
8 |
|
if ($glossary_id == $this->glossary_id) { |
298
|
8 |
|
$replacewords[] = "<strong>\\1</strong>"; |
299
|
|
|
} else { |
300
|
|
|
if ($this_page == "admin_glossary") { |
301
|
|
|
$link_url = "#gl" . $glossary_id; |
302
|
8 |
|
} else { |
303
|
8 |
|
$link_url = $URL->generate('url'); |
304
|
|
|
} |
305
|
|
|
$title = _htmlentities(trim_characters($term_body, 0, 80)); |
306
|
|
|
# strip markdown |
307
|
|
|
$title = preg_replace("/[*~_]/", "", $title); |
308
|
|
|
$replacewords[] = "<a href=\"$link_url\" title=\"$title\" class=\"glossary\">\\1</a>"; |
309
|
|
|
} |
310
|
8 |
|
} |
311
|
|
|
// Highlight all occurrences of another glossary term in the definition. |
312
|
8 |
|
$body = preg_replace($findwords, $replacewords, $body, 1); |
313
|
|
|
if (isset($this->glossary_id)) { |
314
|
|
|
$body = preg_replace("/(?<![>\.\'\/])\b(" . $this->terms[$this->glossary_id]['title'] . ")\b(?![<\'])/i", '<strong>\\1</strong>', $body, 1); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
// Replace any phrases in wikipedia |
318
|
|
|
// TODO: Merge this code into above, so our gloss and wikipedia |
319
|
|
|
// don't clash (e.g. URLs getting doubly munged etc.) |
320
|
|
|
$body = \MySociety\TheyWorkForYou\Utility\Wikipedia::wikipedize($body); |
321
|
|
|
|
322
|
|
|
return ($body); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
} |
326
|
|
|
|