1 | <?php |
||||
2 | |||||
3 | /** |
||||
4 | * This file has functions in it to handle merging of two or more topics |
||||
5 | * |
||||
6 | * @package ElkArte Forum |
||||
7 | * @copyright ElkArte Forum contributors |
||||
8 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||||
9 | * |
||||
10 | * This file contains code covered by: |
||||
11 | * copyright: 2011 Simple Machines (http://www.simplemachines.org) |
||||
12 | * |
||||
13 | * @version 2.0 dev |
||||
14 | * |
||||
15 | */ |
||||
16 | |||||
17 | namespace ElkArte; |
||||
18 | |||||
19 | use ElkArte\Helper\Util; |
||||
20 | |||||
21 | /** |
||||
22 | * This class has functions to handle merging of two or more topics |
||||
23 | * in to a single new or existing topic. |
||||
24 | * |
||||
25 | * Class TopicsMerge |
||||
26 | */ |
||||
27 | class TopicsMerge |
||||
28 | { |
||||
29 | /** @var array For each topic a set of information (id, board, subject, poll, etc.) */ |
||||
30 | public $topic_data = []; |
||||
31 | |||||
32 | /** @var int[] All the boards the topics are in */ |
||||
33 | public $boards = []; |
||||
34 | |||||
35 | /** @var int The id_topic with the lowest id_first_msg */ |
||||
36 | public $firstTopic = 0; |
||||
37 | |||||
38 | /** @var int The id_board of the topic TopicsMerge::$firstTopic */ |
||||
39 | public $firstBoard = 0; |
||||
40 | |||||
41 | /** @var int[] Just the array of topics to merge. */ |
||||
42 | private $_topics; |
||||
43 | |||||
44 | /** @var int Sum of the number of views of each topic. */ |
||||
45 | private $_num_views = 0; |
||||
46 | |||||
47 | /** @var int If at least one of the topics is sticky */ |
||||
48 | private $_is_sticky = 0; |
||||
49 | |||||
50 | /** @var array An array of "totals" (number of topics/messages, unapproved, etc.) for each board involved */ |
||||
51 | private $_boardTotals = []; |
||||
52 | |||||
53 | /** @var int[] If any topic has a poll, the array of poll id */ |
||||
54 | private $_polls = []; |
||||
55 | |||||
56 | /** @var string[] List of errors occurred */ |
||||
57 | private $_errors = []; |
||||
58 | |||||
59 | /** @var object The database object */ |
||||
60 | private $_db; |
||||
61 | |||||
62 | /** |
||||
63 | * Initialize the class with a list of topics to merge |
||||
64 | * |
||||
65 | * @param int[] $topics array of topics to merge into one |
||||
66 | */ |
||||
67 | public function __construct($topics) |
||||
68 | { |
||||
69 | // Prepare the vars |
||||
70 | $this->_db = database(); |
||||
71 | |||||
72 | // Ensure all the id's are integers |
||||
73 | $topics = array_map('intval', $topics); |
||||
74 | $this->_topics = array_filter($topics); |
||||
75 | |||||
76 | // Find out some preliminary information |
||||
77 | $this->_loadTopicDetails(); |
||||
78 | } |
||||
79 | |||||
80 | /** |
||||
81 | * Grabs all the details of the topics involved in the merge process and loads |
||||
82 | * then in $this->topic_data |
||||
83 | */ |
||||
84 | protected function _loadTopicDetails() |
||||
85 | { |
||||
86 | global $modSettings; |
||||
87 | |||||
88 | // Joy of all joys, make sure they're not pi**ing about with unapproved topics they can't see :P |
||||
89 | $can_approve_boards = false; |
||||
90 | if ($modSettings['postmod_active']) |
||||
91 | { |
||||
92 | $can_approve_boards = empty(User::$info->mod_cache['ap']) ? boardsAllowedTo('approve_posts') : User::$info->mod_cache['ap']; |
||||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||||
93 | } |
||||
94 | |||||
95 | // Get info about the topics and polls that will be merged. |
||||
96 | $request = $this->_db->query('', ' |
||||
97 | SELECT |
||||
98 | t.id_topic, t.id_board, b.id_cat, t.id_poll, t.num_views, t.is_sticky, t.approved, t.num_replies, t.unapproved_posts, |
||||
99 | m1.subject, m1.poster_time AS time_started, COALESCE(mem1.id_member, 0) AS id_member_started, COALESCE(mem1.real_name, m1.poster_name) AS name_started, |
||||
100 | m2.poster_time AS time_updated, COALESCE(mem2.id_member, 0) AS id_member_updated, COALESCE(mem2.real_name, m2.poster_name) AS name_updated |
||||
101 | FROM {db_prefix}topics AS t |
||||
102 | INNER JOIN {db_prefix}messages AS m1 ON (m1.id_msg = t.id_first_msg) |
||||
103 | INNER JOIN {db_prefix}messages AS m2 ON (m2.id_msg = t.id_last_msg) |
||||
104 | LEFT JOIN {db_prefix}members AS mem1 ON (mem1.id_member = m1.id_member) |
||||
105 | LEFT JOIN {db_prefix}members AS mem2 ON (mem2.id_member = m2.id_member) |
||||
106 | LEFT JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board) |
||||
107 | WHERE t.id_topic IN ({array_int:topic_list}) |
||||
108 | ORDER BY t.id_first_msg |
||||
109 | LIMIT {int:limit}', |
||||
110 | array( |
||||
111 | 'topic_list' => $this->_topics, |
||||
112 | 'limit' => count($this->_topics), |
||||
113 | ) |
||||
114 | ); |
||||
115 | if ($request->num_rows() < 2) |
||||
116 | { |
||||
117 | $request->free_result(); |
||||
118 | |||||
119 | $this->_errors[] = array('no_topic_id', true); |
||||
120 | |||||
121 | return false; |
||||
122 | } |
||||
123 | |||||
124 | while (($row = $request->fetch_assoc())) |
||||
125 | { |
||||
126 | // Make a note for the board counts... |
||||
127 | if (!isset($this->_boardTotals[$row['id_board']])) |
||||
128 | { |
||||
129 | $this->_boardTotals[$row['id_board']] = array( |
||||
130 | 'num_posts' => 0, |
||||
131 | 'num_topics' => 0, |
||||
132 | 'unapproved_posts' => 0, |
||||
133 | 'unapproved_topics' => 0 |
||||
134 | ); |
||||
135 | } |
||||
136 | |||||
137 | // We can't see unapproved topics here? |
||||
138 | if ($modSettings['postmod_active'] && !$row['approved'] && $can_approve_boards != array(0) && !in_array($row['id_board'], $can_approve_boards)) |
||||
139 | { |
||||
140 | unset($this->_topics[$row['id_topic']]); |
||||
141 | continue; |
||||
142 | } |
||||
143 | |||||
144 | if (!$row['approved']) |
||||
145 | { |
||||
146 | $this->_boardTotals[$row['id_board']]['unapproved_topics']++; |
||||
147 | } |
||||
148 | else |
||||
149 | { |
||||
150 | $this->_boardTotals[$row['id_board']]['num_topics']++; |
||||
151 | } |
||||
152 | |||||
153 | $this->_boardTotals[$row['id_board']]['unapproved_posts'] += $row['unapproved_posts']; |
||||
154 | $this->_boardTotals[$row['id_board']]['num_posts'] += $row['num_replies'] + ($row['approved'] ? 1 : 0); |
||||
155 | |||||
156 | $this->topic_data[$row['id_topic']] = array( |
||||
157 | 'id' => $row['id_topic'], |
||||
158 | 'board' => $row['id_board'], |
||||
159 | 'poll' => $row['id_poll'], |
||||
160 | 'num_views' => $row['num_views'], |
||||
161 | 'subject' => $row['subject'], |
||||
162 | 'started' => array( |
||||
163 | 'time' => standardTime($row['time_started']), |
||||
164 | 'html_time' => htmlTime($row['time_started']), |
||||
165 | 'timestamp' => forum_time(true, $row['time_started']), |
||||
166 | 'href' => empty($row['id_member_started']) ? '' : getUrl('profile', ['action' => 'profile', 'u' => $row['id_member_started'], 'name' => $row['name_started']]), |
||||
167 | 'link' => empty($row['id_member_started']) ? $row['name_started'] : '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row['id_member_started'], 'name' => $row['name_started']]) . '">' . $row['name_started'] . '</a>' |
||||
168 | ), |
||||
169 | 'updated' => array( |
||||
170 | 'time' => standardTime($row['time_updated']), |
||||
171 | 'html_time' => htmlTime($row['time_updated']), |
||||
172 | 'timestamp' => forum_time(true, $row['time_updated']), |
||||
173 | 'href' => empty($row['id_member_updated']) ? '' : getUrl('profile', ['action' => 'profile', 'u' => $row['id_member_updated'], 'name' => $row['name_updated']]), |
||||
174 | 'link' => empty($row['id_member_updated']) ? $row['name_updated'] : '<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row['id_member_updated'], 'name' => $row['name_updated']]) . '">' . $row['name_updated'] . '</a>' |
||||
175 | ) |
||||
176 | ); |
||||
177 | $this->_num_views += $row['num_views']; |
||||
178 | $this->boards[] = $row['id_board']; |
||||
179 | |||||
180 | // If there's no poll, id_poll == 0... |
||||
181 | if ($row['id_poll'] > 0) |
||||
182 | { |
||||
183 | $this->_polls[] = $row['id_poll']; |
||||
184 | } |
||||
185 | |||||
186 | // Store the id_topic with the lowest id_first_msg. |
||||
187 | if (empty($this->firstTopic)) |
||||
188 | { |
||||
189 | $this->firstTopic = $row['id_topic']; |
||||
190 | $this->firstBoard = $row['id_board']; |
||||
191 | } |
||||
192 | |||||
193 | $this->_is_sticky = max($this->_is_sticky, $row['is_sticky']); |
||||
194 | } |
||||
195 | |||||
196 | $request->free_result(); |
||||
197 | |||||
198 | $this->boards = array_map('intval', array_values(array_unique($this->boards))); |
||||
199 | |||||
200 | // If we didn't get any topics then they've been messing with unapproved stuff. |
||||
201 | if (empty($this->topic_data)) |
||||
202 | { |
||||
203 | $this->_errors[] = array('no_topic_id', true); |
||||
204 | } |
||||
205 | |||||
206 | return true; |
||||
207 | } |
||||
208 | |||||
209 | /** |
||||
210 | * If errors occurred while working |
||||
211 | * |
||||
212 | * @return bool |
||||
213 | */ |
||||
214 | public function hasErrors() |
||||
215 | { |
||||
216 | return !empty($this->_errors); |
||||
217 | } |
||||
218 | |||||
219 | /** |
||||
220 | * The first error occurred |
||||
221 | * |
||||
222 | * @return string |
||||
223 | */ |
||||
224 | public function firstError() |
||||
225 | { |
||||
226 | if (!empty($this->_errors)) |
||||
227 | { |
||||
228 | $errors = array_values($this->_errors); |
||||
229 | |||||
230 | return array_shift($errors); |
||||
231 | } |
||||
232 | |||||
233 | return ''; |
||||
234 | } |
||||
235 | |||||
236 | /** |
||||
237 | * Returns the polls information if any of the topics has a poll. |
||||
238 | * |
||||
239 | * @return array |
||||
240 | */ |
||||
241 | public function getPolls() |
||||
242 | { |
||||
243 | $polls = []; |
||||
244 | |||||
245 | if (count($this->_polls) > 1) |
||||
246 | { |
||||
247 | $this->_db->fetchQuery(' |
||||
248 | SELECT |
||||
249 | t.id_topic, t.id_poll, m.subject, p.question |
||||
250 | FROM {db_prefix}polls AS p |
||||
251 | INNER JOIN {db_prefix}topics AS t ON (t.id_poll = p.id_poll) |
||||
252 | INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg) |
||||
253 | WHERE p.id_poll IN ({array_int:polls}) |
||||
254 | LIMIT {int:limit}', |
||||
255 | array( |
||||
256 | 'polls' => $this->_polls, |
||||
257 | 'limit' => count($this->_polls), |
||||
258 | ) |
||||
259 | )->fetch_callback( |
||||
260 | function ($row) use (&$polls) { |
||||
261 | $polls[] = array( |
||||
262 | 'id' => $row['id_poll'], |
||||
263 | 'topic' => array( |
||||
264 | 'id' => $row['id_topic'], |
||||
265 | 'subject' => $row['subject'] |
||||
266 | ), |
||||
267 | 'question' => $row['question'], |
||||
268 | 'selected' => $row['id_topic'] == $this->firstTopic |
||||
269 | ); |
||||
270 | } |
||||
271 | ); |
||||
272 | } |
||||
273 | |||||
274 | return $polls; |
||||
275 | } |
||||
276 | |||||
277 | /** |
||||
278 | * Performs the merge operations |
||||
279 | * |
||||
280 | * @param array $details |
||||
281 | * @return bool|int[] |
||||
282 | */ |
||||
283 | public function doMerge($details = array()) |
||||
284 | { |
||||
285 | // Just to be sure, here we should not have any error around |
||||
286 | $this->_errors = []; |
||||
287 | |||||
288 | // Determine target board. |
||||
289 | $target_board = count($this->boards) > 1 ? (int) $details['board'] : $this->boards[0]; |
||||
290 | if (!in_array($target_board, $details['accessible_boards'])) |
||||
291 | { |
||||
292 | $this->_errors[] = array('no_board', true); |
||||
293 | |||||
294 | return false; |
||||
295 | } |
||||
296 | |||||
297 | // Determine which poll will survive and which polls won't. |
||||
298 | $target_poll = count($this->_polls) > 1 ? (int) $details['poll'] : (count($this->_polls) === 1 ? $this->_polls[0] : 0); |
||||
299 | if ($target_poll > 0 && !in_array($target_poll, $this->_polls)) |
||||
300 | { |
||||
301 | $this->_errors[] = array('no_access', false); |
||||
302 | |||||
303 | return false; |
||||
304 | } |
||||
305 | |||||
306 | $deleted_polls = empty($target_poll) ? $this->_polls : array_diff($this->_polls, array($target_poll)); |
||||
307 | |||||
308 | // Determine the subject of the newly merged topic - was a custom subject specified? |
||||
309 | if (empty($details['subject']) && $details['custom_subject'] != '') |
||||
310 | { |
||||
311 | $target_subject = strtr(Util::htmltrim(Util::htmlspecialchars($details['custom_subject'])), array("\r" => '', "\n" => '', "\t" => '')); |
||||
312 | |||||
313 | // Keep checking the length. |
||||
314 | if (Util::strlen($target_subject) > 100) |
||||
315 | { |
||||
316 | $target_subject = Util::substr($target_subject, 0, 100); |
||||
317 | } |
||||
318 | |||||
319 | // Nothing left - odd but pick the first topics subject. |
||||
320 | if ($target_subject === '') |
||||
321 | { |
||||
322 | $target_subject = $this->topic_data[$this->firstTopic]['subject']; |
||||
323 | } |
||||
324 | } |
||||
325 | // A subject was selected from the list. |
||||
326 | elseif (!empty($this->topic_data[(int) $details['subject']]['subject'])) |
||||
327 | { |
||||
328 | $target_subject = $this->topic_data[(int) $details['subject']]['subject']; |
||||
329 | } |
||||
330 | // Nothing worked? Just take the subject of the first message. |
||||
331 | else |
||||
332 | { |
||||
333 | $target_subject = $this->topic_data[$this->firstTopic]['subject']; |
||||
334 | } |
||||
335 | |||||
336 | // Get the first and last message and the number of messages.... |
||||
337 | $topic_approved = 1; |
||||
338 | $first_msg = 0; |
||||
339 | $num_replies = 0; |
||||
340 | $this->_db->fetchQuery(' |
||||
341 | SELECT |
||||
342 | approved, MIN(id_msg) AS first_msg, MAX(id_msg) AS last_msg, COUNT(*) AS message_count |
||||
343 | FROM {db_prefix}messages |
||||
344 | WHERE id_topic IN ({array_int:topics}) |
||||
345 | GROUP BY approved |
||||
346 | ORDER BY approved DESC', |
||||
347 | array( |
||||
348 | 'topics' => $this->_topics, |
||||
349 | ) |
||||
350 | )->fetch_callback( |
||||
351 | static function ($row) use (&$topic_approved, &$first_msg, &$num_replies, &$last_msg, &$num_unapproved) { |
||||
352 | // If this is approved, or is fully unapproved. |
||||
353 | if ($row['approved'] || !isset($first_msg)) |
||||
354 | { |
||||
355 | $first_msg = $row['first_msg']; |
||||
356 | $last_msg = $row['last_msg']; |
||||
357 | if ($row['approved']) |
||||
358 | { |
||||
359 | $num_replies = $row['message_count'] - 1; |
||||
360 | $num_unapproved = 0; |
||||
361 | } |
||||
362 | else |
||||
363 | { |
||||
364 | $topic_approved = 0; |
||||
365 | $num_replies = 0; |
||||
366 | $num_unapproved = $row['message_count']; |
||||
367 | } |
||||
368 | } |
||||
369 | else |
||||
370 | { |
||||
371 | // If this has a lower first_msg then the first post is not approved and hence the number of replies was wrong! |
||||
372 | if ($first_msg > $row['first_msg']) |
||||
373 | { |
||||
374 | $first_msg = $row['first_msg']; |
||||
375 | $num_replies++; |
||||
376 | $topic_approved = 0; |
||||
377 | } |
||||
378 | |||||
379 | $num_unapproved = $row['message_count']; |
||||
380 | } |
||||
381 | } |
||||
382 | ); |
||||
383 | |||||
384 | // Ensure we have a board stat for the target board. |
||||
385 | if (!isset($this->_boardTotals[$target_board])) |
||||
386 | { |
||||
387 | $this->_boardTotals[$target_board] = array( |
||||
388 | 'num_posts' => 0, |
||||
389 | 'num_topics' => 0, |
||||
390 | 'unapproved_posts' => 0, |
||||
391 | 'unapproved_topics' => 0 |
||||
392 | ); |
||||
393 | } |
||||
394 | |||||
395 | // Fix the topic count stuff depending on what the new one counts as. |
||||
396 | if ($topic_approved !== 0) |
||||
397 | { |
||||
398 | $this->_boardTotals[$target_board]['num_topics']--; |
||||
399 | } |
||||
400 | else |
||||
401 | { |
||||
402 | $this->_boardTotals[$target_board]['unapproved_topics']--; |
||||
403 | } |
||||
404 | |||||
405 | $this->_boardTotals[$target_board]['unapproved_posts'] -= $num_unapproved; |
||||
406 | $this->_boardTotals[$target_board]['num_posts'] -= $topic_approved !== 0 ? $num_replies + 1 : $num_replies; |
||||
407 | |||||
408 | // Get the member ID of the first and last message. |
||||
409 | $request = $this->_db->fetchQuery(' |
||||
410 | SELECT |
||||
411 | id_member |
||||
412 | FROM {db_prefix}messages |
||||
413 | WHERE id_msg IN ({int:first_msg}, {int:last_msg}) |
||||
414 | ORDER BY id_msg |
||||
415 | LIMIT 2', |
||||
416 | array( |
||||
417 | 'first_msg' => $first_msg, |
||||
418 | 'last_msg' => $last_msg, |
||||
419 | ) |
||||
420 | ); |
||||
421 | [$member_started] = $request->fetch_row(); |
||||
422 | [$member_updated] = $request->fetch_row(); |
||||
423 | |||||
424 | // First and last message are the same, so only row was returned. |
||||
425 | if ($member_updated === null) |
||||
426 | { |
||||
427 | $member_updated = $member_started; |
||||
428 | } |
||||
429 | |||||
430 | $request->free_result(); |
||||
431 | |||||
432 | // Obtain all the message ids we are going to affect. |
||||
433 | $affected_msgs = messagesInTopics($this->_topics); |
||||
434 | |||||
435 | // Assign the first topic ID to be the merged topic. |
||||
436 | $id_topic = min($this->_topics); |
||||
437 | |||||
438 | $enforce_subject = Util::htmlspecialchars(trim($details['enforce_subject'])); |
||||
439 | |||||
440 | // Merge topic notifications. |
||||
441 | $notifications = is_array($details['notifications']) ? array_intersect($this->_topics, $details['notifications']) : array(); |
||||
442 | fixMergedTopics($first_msg, $this->_topics, $id_topic, $target_board, $target_subject, $enforce_subject, $notifications); |
||||
443 | |||||
444 | // Assign the properties of the newly merged topic. |
||||
445 | setTopicAttribute($id_topic, array( |
||||
446 | 'id_board' => $target_board, |
||||
447 | 'is_sticky' => $this->_is_sticky, |
||||
448 | 'approved' => $topic_approved, |
||||
449 | 'id_member_started' => $member_started, |
||||
450 | 'id_member_updated' => $member_updated, |
||||
451 | 'id_first_msg' => $first_msg, |
||||
452 | 'id_last_msg' => $last_msg, |
||||
453 | 'id_poll' => $target_poll, |
||||
454 | 'num_replies' => $num_replies, |
||||
455 | 'unapproved_posts' => $num_unapproved, |
||||
456 | 'num_views' => $this->_num_views, |
||||
457 | )); |
||||
458 | |||||
459 | // Get rid of the redundant polls. |
||||
460 | if (!empty($deleted_polls)) |
||||
461 | { |
||||
462 | require_once(SUBSDIR . '/Poll.subs.php'); |
||||
463 | removePoll($deleted_polls); |
||||
464 | } |
||||
465 | |||||
466 | $this->_updateStats($affected_msgs, $id_topic, $target_subject, $enforce_subject); |
||||
0 ignored issues
–
show
$enforce_subject of type string is incompatible with the type boolean expected by parameter $enforce_subject of ElkArte\TopicsMerge::_updateStats() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
467 | |||||
468 | return array($id_topic, $target_board); |
||||
469 | } |
||||
470 | |||||
471 | /** |
||||
472 | * Takes care of updating all the relevant statistics |
||||
473 | * |
||||
474 | * @param int[] $affected_msgs |
||||
475 | * @param int $id_topic |
||||
476 | * @param string $target_subject |
||||
477 | * @param bool $enforce_subject |
||||
478 | * @throws \ElkArte\Exceptions\Exception |
||||
479 | */ |
||||
480 | protected function _updateStats($affected_msgs, $id_topic, $target_subject, $enforce_subject) |
||||
481 | { |
||||
482 | global $modSettings; |
||||
483 | |||||
484 | // Cycle through each board... |
||||
485 | foreach ($this->_boardTotals as $id_board => $stats) |
||||
486 | { |
||||
487 | decrementBoard($id_board, $stats); |
||||
488 | } |
||||
489 | |||||
490 | // Determine the board the final topic resides in |
||||
491 | $topic_info = getTopicInfo($id_topic); |
||||
492 | $id_board = $topic_info['id_board']; |
||||
493 | |||||
494 | // Update all the statistics. |
||||
495 | require_once(SUBSDIR . '/Topic.subs.php'); |
||||
496 | updateTopicStats(); |
||||
497 | |||||
498 | require_once(SUBSDIR . '/Messages.subs.php'); |
||||
499 | updateSubjectStats($id_topic, $target_subject); |
||||
500 | updateLastMessages($this->boards); |
||||
501 | |||||
502 | logAction('merge', array('topic' => $id_topic, 'board' => $id_board)); |
||||
503 | |||||
504 | // Notify people that these topics have been merged? |
||||
505 | require_once(SUBSDIR . '/Notification.subs.php'); |
||||
506 | sendNotifications($id_topic, 'merge'); |
||||
507 | |||||
508 | // Grab the response prefix (like 'Re: ') in the default forum language. |
||||
509 | $response_prefix = response_prefix(); |
||||
510 | |||||
511 | // If there's a search index that needs updating, update it... |
||||
512 | $searchAPI = new Search\SearchApiWrapper(empty($modSettings['search_index']) ? '' : $modSettings['search_index']); |
||||
513 | $searchAPI->topicMerge($id_topic, $this->_topics, $affected_msgs, empty($enforce_subject) ? null : array($response_prefix, $target_subject)); |
||||
514 | } |
||||
515 | } |
||||
516 |