|
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
|
|
|
$data['error'] = "Sorry, you are not allowed to add Glossary terms."; |
|
193
|
|
|
return $data; |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
if ($data['title'] == '') { |
|
197
|
|
|
$data['error'] = "Sorry, you can't define a term without a title"; |
|
198
|
|
|
return $data; |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
if ($data['body'] == '') { |
|
202
|
|
|
$data['error'] = "You haven't entered a definition!"; |
|
203
|
|
|
return $data; |
|
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 update(&$data) { |
|
249
|
|
|
global $THEUSER; |
|
250
|
8 |
|
|
|
251
|
|
|
if (!$THEUSER->is_able_to('addterm')) { |
|
252
|
|
|
$data['error'] = "Sorry, you are not allowed to add Glossary terms."; |
|
253
|
8 |
|
return $data; |
|
254
|
|
|
} |
|
255
|
8 |
|
|
|
256
|
8 |
|
if ($data['body'] == '') { |
|
257
|
8 |
|
$data['error'] = "You haven't entered a definition!"; |
|
258
|
8 |
|
return $data; |
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
if ($data['glossary_id'] == '') { |
|
262
|
|
|
$data['error'] = "You haven't specified an entry!"; |
|
263
|
8 |
|
return $data; |
|
264
|
|
|
} |
|
265
|
|
|
|
|
266
|
|
|
$q = $this->db->query( |
|
267
|
|
|
"UPDATE glossary |
|
268
|
|
|
SET body = :body |
|
269
|
|
|
WHERE glossary_id = :id", |
|
270
|
|
|
[ |
|
271
|
|
|
':id' => $data['glossary_id'], |
|
272
|
8 |
|
':body' => $data['body'], |
|
273
|
|
|
] |
|
274
|
|
|
); |
|
275
|
|
|
|
|
276
|
|
|
if ($q->success()) { |
|
277
|
8 |
|
$data['success'] = true; |
|
278
|
8 |
|
} else { |
|
279
|
|
|
$data['error'] = "There was a problem updating the entry"; |
|
280
|
|
|
} |
|
281
|
|
|
|
|
282
|
8 |
|
return $data; |
|
283
|
8 |
|
} |
|
284
|
|
|
|
|
285
|
8 |
|
public function delete($glossary_id) { |
|
286
|
|
|
$q = $this->db->query("DELETE from glossary where glossary_id=$glossary_id LIMIT 1;"); |
|
287
|
8 |
|
// if that worked, we need to update the editqueue, |
|
288
|
|
|
// and remove the term from the already generated object list. |
|
289
|
8 |
|
if ($q->affected_rows() >= 1) { |
|
290
|
|
|
unset($this->replace_order[$glossary_id]); |
|
291
|
|
|
unset($this->terms[$glossary_id]); |
|
292
|
8 |
|
} |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
8 |
|
public function glossarise($body, $tokenize = 0, $urlize = 0, $return_expansions = 0) { |
|
|
|
|
|
|
296
|
|
|
// Turn a body of text into a link-up wonderland of glossary joy |
|
297
|
8 |
|
|
|
298
|
8 |
|
global $this_page; |
|
299
|
|
|
|
|
300
|
|
|
$findwords = []; |
|
301
|
|
|
$replacewords = []; |
|
302
|
8 |
|
global $replacemap, $titlemap; |
|
303
|
8 |
|
$replacemap = []; |
|
304
|
|
|
$titlemap = []; |
|
305
|
|
|
$URL = new \MySociety\TheyWorkForYou\Url("glossary"); |
|
306
|
|
|
$URL->insert(["gl" => ""]); |
|
307
|
|
|
|
|
308
|
|
|
// External links shown within their own definition |
|
309
|
|
|
// should be the complete and linked url. |
|
310
|
8 |
|
// NB. This should only match when $body is a definition beginning with "https:" |
|
311
|
|
|
if (is_string($body) && preg_match("/^(https?:*[^\s]*)$/i", $body)) { |
|
312
|
8 |
|
$body = "<a href=\"" . $body . "\" rel=\"nofollow\" title=\"External link to " . $body . "\">" . $body . "</a>"; |
|
313
|
|
|
return ($body); |
|
314
|
|
|
} |
|
315
|
|
|
|
|
316
|
|
|
// otherwise, just replace everything. |
|
317
|
|
|
|
|
318
|
|
|
// generate links from URL when wanted |
|
319
|
|
|
// NB WRANS is already doing this |
|
320
|
|
|
if ($urlize == 1) { |
|
321
|
|
|
$body = preg_replace("~(http(s)?:\/\/[^\s\n]*)\b(\/)?~i", "<a href=\"\\0\">\\0</a>", $body); |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
$pd = new \Parsedown(); |
|
325
|
|
|
$pd->setSafeMode(true); |
|
326
|
|
|
// check for any glossary terms to replace |
|
327
|
|
|
foreach ($this->replace_order as $glossary_id => $count) { |
|
328
|
|
|
if ($glossary_id == $this->glossary_id) { |
|
329
|
|
|
continue; |
|
330
|
|
|
} |
|
331
|
|
|
|
|
332
|
|
|
$term_body = $this->terms[$glossary_id]['body']; |
|
333
|
|
|
$term_title = $this->terms[$glossary_id]['title']; |
|
334
|
|
|
|
|
335
|
|
|
$URL->update(["gl" => $glossary_id]); |
|
336
|
|
|
# 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 |
|
337
|
|
|
$findwords[$glossary_id] = "/\b(" . $term_title . ")\b(?!(?>[^<]*(?:<(?!\/?a\b)[^<]*)*)<\/a>)/i"; |
|
338
|
|
|
// catch glossary terms within their own definitions |
|
339
|
|
|
if ($glossary_id == $this->glossary_id) { |
|
340
|
|
|
$replacewords[] = "<strong>\\1</strong>"; |
|
341
|
|
|
} else { |
|
342
|
|
|
if ($this_page == "admin_glossary") { |
|
343
|
|
|
$link_url = "#gl" . $glossary_id; |
|
344
|
|
|
} else { |
|
345
|
|
|
$link_url = $URL->generate('url'); |
|
346
|
|
|
} |
|
347
|
|
|
$title = _htmlentities(trim_characters($term_body, 0, 80)); |
|
348
|
|
|
$class_extra = ''; |
|
349
|
|
|
$nofollow = ''; |
|
350
|
|
|
$map_replacement = "<a popovertarget=\"def-" . $glossary_id . "\" href=\"$link_url\" title=\"Display definition of " . $term_title . "\"$nofollow class=\"glossary-term-button" . $class_extra . "\">" . $term_title . "</a>"; |
|
351
|
|
|
if (preg_match("/^(https?:*[^\s]*)$/i", $term_body)) { |
|
352
|
|
|
$link_url = $term_body; |
|
353
|
|
|
$title = "External link to " . $term_body; |
|
354
|
|
|
$class_extra = ' glossary_external'; |
|
355
|
|
|
$nofollow = ' rel="nofollow"'; |
|
356
|
|
|
$map_replacement = "<a href=\"$link_url\" title=\"$title\"$nofollow class=\"glossary" . $class_extra . "\">" . $term_title . "</a>"; |
|
357
|
|
|
} |
|
358
|
|
|
$replacewords[] = "<a href=\"$link_url\" title=\"$title\"$nofollow class=\"glossary" . $class_extra . "\">\\1</a>"; |
|
359
|
|
|
$lc_title = strtolower($term_title); |
|
360
|
|
|
$replacemap[$lc_title] = $map_replacement; |
|
361
|
|
|
$titlemap[$lc_title] = ['id' => $glossary_id, 'body' => $pd->text($term_body)]; |
|
362
|
|
|
} |
|
363
|
|
|
} |
|
364
|
|
|
// Highlight all occurrences of another glossary term in the definition. |
|
365
|
|
|
if ($return_expansions) { |
|
366
|
|
|
global $expansions; |
|
367
|
|
|
$expansions = []; |
|
368
|
|
|
$body = preg_replace_callback($findwords, function ($matches) { |
|
369
|
|
|
global $expansions, $replacemap, $titlemap; |
|
370
|
|
|
$lc_match = strtolower($matches[0]); |
|
371
|
|
|
$expansions = $expansions + [$matches[0] => $titlemap[$lc_match]]; |
|
372
|
|
|
return $replacemap[$lc_match]; |
|
373
|
|
|
}, $body, 1); |
|
374
|
|
|
} else { |
|
375
|
|
|
$body = preg_replace($findwords, $replacewords, $body, 1); |
|
376
|
|
|
} |
|
377
|
|
|
if (isset($this->glossary_id)) { |
|
378
|
|
|
$body = preg_replace("/(?<![>\.\'\/])\b(" . $this->terms[$this->glossary_id]['title'] . ")\b(?![<\'])/i", '<strong>\\1</strong>', $body, 1); |
|
379
|
|
|
} |
|
380
|
|
|
|
|
381
|
|
|
// Replace any phrases in wikipedia |
|
382
|
|
|
// TODO: Merge this code into above, so our gloss and wikipedia |
|
383
|
|
|
// don't clash (e.g. URLs getting doubly munged etc.) |
|
384
|
|
|
$body = \MySociety\TheyWorkForYou\Utility\Wikipedia::wikipedize($body); |
|
385
|
|
|
|
|
386
|
|
|
if ($return_expansions) { |
|
387
|
|
|
return [($body), $expansions]; |
|
|
|
|
|
|
388
|
|
|
} |
|
389
|
|
|
return ($body); |
|
390
|
|
|
} |
|
391
|
|
|
|
|
392
|
|
|
} |
|
393
|
|
|
|