1 | <?php |
||
2 | |||
3 | /** |
||
4 | * This file is mainly concerned with minor tasks relating to boards, such as |
||
5 | * marking them read, collapsing categories, or quick moderation. |
||
6 | * |
||
7 | * @package ElkArte Forum |
||
8 | * @copyright ElkArte Forum contributors |
||
9 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||
10 | * |
||
11 | * This file contains code covered by: |
||
12 | * copyright: 2011 Simple Machines (http://www.simplemachines.org) |
||
13 | * |
||
14 | * @version 2.0 dev |
||
15 | * |
||
16 | */ |
||
17 | |||
18 | namespace ElkArte; |
||
19 | |||
20 | use ElkArte\Cache\Cache; |
||
21 | use ElkArte\Database\QueryInterface; |
||
22 | use ElkArte\Exceptions\Exception; |
||
23 | |||
24 | /** |
||
25 | * Class BoardsTree |
||
26 | * |
||
27 | * @package ElkArte |
||
28 | */ |
||
29 | class BoardsTree |
||
30 | { |
||
31 | protected $cat_tree = []; |
||
32 | |||
33 | protected $boards = []; |
||
34 | |||
35 | protected $boardList = []; |
||
36 | 2 | ||
37 | protected $db; |
||
38 | 2 | ||
39 | public function __construct(QueryInterface $db) |
||
40 | 2 | { |
|
41 | 2 | $this->db = $db; |
|
42 | |||
43 | $this->loadBoardTree(); |
||
44 | } |
||
45 | |||
46 | /** |
||
47 | * Load a lot of useful information regarding the boards and categories. |
||
48 | * |
||
49 | * - The information retrieved is stored in globals: |
||
50 | * $this->boards: properties of each board. |
||
51 | * $this->boardList: a list of boards grouped by category ID. |
||
52 | * $this->cat_tree: properties of each category. |
||
53 | * |
||
54 | * @param array $query |
||
55 | 2 | * |
|
56 | * @throws Exception no_valid_parent |
||
57 | */ |
||
58 | 2 | protected function loadBoardTree($query = array()) |
|
59 | { |
||
60 | // Addons may want to add their own information to the board table. |
||
61 | 2 | call_integration_hook('integrate_board_tree_query', array(&$query)); |
|
62 | |||
63 | // Getting all the board and category information you'd ever wanted. |
||
64 | $request = $this->db->query('', ' |
||
65 | 2 | SELECT |
|
66 | 2 | COALESCE(b.id_board, 0) AS id_board, b.id_parent, b.name AS board_name, b.description, b.child_level, |
|
67 | b.board_order, b.count_posts, b.old_posts, b.member_groups, b.id_theme, b.override_theme, b.id_profile, b.redirect, |
||
68 | 2 | b.num_posts, b.num_topics, b.deny_member_groups, c.id_cat, c.name AS cat_name, c.cat_order, c.can_collapse' . (empty($query['select']) ? |
|
69 | 2 | '' : $query['select']) . ' |
|
70 | FROM {db_prefix}categories AS c |
||
71 | 2 | LEFT JOIN {db_prefix}boards AS b ON (b.id_cat = c.id_cat)' . (empty($query['join']) ? |
|
72 | '' : $query['join']) . ' |
||
73 | 2 | ORDER BY c.cat_order, b.child_level, b.board_order', |
|
74 | 2 | array() |
|
75 | 2 | ); |
|
76 | 2 | $this->cat_tree = array(); |
|
77 | $this->boards = array(); |
||
78 | 2 | $last_board_order = 0; |
|
79 | while (($row = $request->fetch_assoc())) |
||
80 | 2 | { |
|
81 | 1 | $row['id_cat'] = (int) $row['id_cat']; |
|
82 | 2 | if (!isset($this->cat_tree[$row['id_cat']])) |
|
83 | 2 | { |
|
84 | 2 | $this->cat_tree[$row['id_cat']] = [ |
|
85 | 2 | 'node' => [ |
|
86 | 'id' => $row['id_cat'], |
||
87 | 2 | 'name' => $row['cat_name'], |
|
88 | 2 | 'order' => (int) $row['cat_order'], |
|
89 | 'can_collapse' => $row['can_collapse'] |
||
90 | ], |
||
91 | 2 | 'is_first' => empty($this->cat_tree), |
|
92 | 2 | 'last_board_order' => $last_board_order, |
|
93 | 'children' => [] |
||
94 | ]; |
||
95 | 2 | $prevBoard = 0; |
|
96 | $curLevel = 0; |
||
97 | 2 | } |
|
98 | |||
99 | if (!empty($row['id_board'])) |
||
100 | { |
||
101 | $row['id_board'] = (int) $row['id_board']; |
||
102 | 2 | $row['child_level'] = (int) $row['child_level']; |
|
103 | 2 | if ($row['child_level'] !== $curLevel) |
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
104 | 2 | { |
|
105 | 2 | $prevBoard = 0; |
|
106 | 2 | } |
|
107 | 2 | ||
108 | 2 | $this->boards[$row['id_board']] = array( |
|
109 | 2 | 'id' => $row['id_board'], |
|
110 | 2 | 'category' => $row['id_cat'], |
|
111 | 2 | 'parent' => (int) $row['id_parent'], |
|
112 | 2 | 'level' => $row['child_level'], |
|
113 | 2 | 'order' => (int) $row['board_order'], |
|
114 | 2 | 'name' => $row['board_name'], |
|
115 | 2 | 'member_groups' => array_map('intval', explode(',', $row['member_groups'])), |
|
116 | 2 | 'deny_groups' => array_map('intval', explode(',', $row['deny_member_groups'])), |
|
117 | 2 | 'description' => $row['description'], |
|
118 | 2 | 'count_posts' => empty($row['count_posts']), |
|
119 | 2 | 'old_posts' => empty($row['old_posts']), |
|
120 | 'posts' => $row['num_posts'], |
||
121 | 2 | 'topics' => $row['num_topics'], |
|
122 | 2 | 'theme' => $row['id_theme'], |
|
123 | 'override_theme' => $row['override_theme'], |
||
124 | 2 | 'profile' => $row['id_profile'], |
|
125 | 'redirect' => $row['redirect'], |
||
126 | 2 | 'prev_board' => $prevBoard |
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
127 | 2 | ); |
|
128 | 2 | $prevBoard = $row['id_board']; |
|
129 | $last_board_order = $row['board_order']; |
||
130 | |||
131 | 2 | if (empty($row['child_level'])) |
|
132 | { |
||
133 | $this->cat_tree[$row['id_cat']]['children'][$row['id_board']] = array( |
||
134 | 'node' => &$this->boards[$row['id_board']], |
||
135 | 'is_first' => empty($this->cat_tree[$row['id_cat']]['children']), |
||
136 | 'children' => [] |
||
137 | ); |
||
138 | $this->boards[$row['id_board']]['tree'] = &$this->cat_tree[$row['id_cat']]['children'][$row['id_board']]; |
||
139 | } |
||
140 | else |
||
141 | { |
||
142 | // Parent doesn't exist! |
||
143 | if (!isset($this->boards[$row['id_parent']]['tree'])) |
||
144 | { |
||
145 | throw new Exception('no_valid_parent', false, array($row['board_name'])); |
||
146 | } |
||
147 | |||
148 | // Wrong childlevel...we can silently fix this... |
||
149 | if ($this->boards[$row['id_parent']]['tree']['node']['level'] != $row['child_level'] - 1) |
||
150 | { |
||
151 | $this->db->query('', ' |
||
152 | UPDATE {db_prefix}boards |
||
153 | SET |
||
154 | child_level = {int:new_child_level} |
||
155 | WHERE id_board = {int:selected_board}', |
||
156 | array( |
||
157 | 'new_child_level' => $this->boards[$row['id_parent']]['tree']['node']['level'] + 1, |
||
158 | 'selected_board' => $row['id_board'], |
||
159 | ) |
||
160 | ); |
||
161 | } |
||
162 | |||
163 | $this->boards[$row['id_parent']]['tree']['children'][$row['id_board']] = array( |
||
164 | 'node' => &$this->boards[$row['id_board']], |
||
165 | 2 | 'is_first' => empty($this->boards[$row['id_parent']]['tree']['children']), |
|
166 | 'children' => [] |
||
167 | 2 | ); |
|
168 | $this->boards[$row['id_board']]['tree'] = &$this->boards[$row['id_parent']]['tree']['children'][$row['id_board']]; |
||
169 | } |
||
170 | 2 | } |
|
171 | 2 | ||
172 | // Let integration easily add data to $this->boards and $this->cat_tree |
||
173 | 2 | call_integration_hook('integrate_board_tree', array($row)); |
|
174 | } |
||
175 | 2 | ||
176 | $request->free_result(); |
||
177 | |||
178 | // Get a list of all the boards in each category (using recursion). |
||
179 | $this->boardList = []; |
||
180 | foreach (array_keys($this->cat_tree) as $catID) |
||
181 | { |
||
182 | $this->boardsInCategory($catID); |
||
183 | } |
||
184 | 2 | } |
|
185 | |||
186 | 2 | /** |
|
187 | * Recursively get a list of boards. |
||
188 | 2 | * |
|
189 | * - Used by loadBoardTree |
||
190 | * |
||
191 | * @param int $catID The category id |
||
192 | */ |
||
193 | 2 | public function boardsInCategory($catID) |
|
194 | { |
||
195 | 2 | $this->boardList[$catID] = []; |
|
196 | 2 | ||
197 | if (empty($this->cat_tree[$catID]['children'])) |
||
198 | 2 | { |
|
199 | return; |
||
200 | 2 | } |
|
201 | |||
202 | 2 | foreach ($this->cat_tree[$catID]['children'] as $id => $node) |
|
203 | { |
||
204 | 2 | $this->boardList[$catID][] = $id; |
|
205 | $this->boardList[$catID] = array_merge($this->boardList[$catID], $this->allChildsOf($id)); |
||
206 | } |
||
207 | } |
||
208 | |||
209 | /** |
||
210 | * Retrieves all the child boards of a given board. |
||
211 | * |
||
212 | * @param int $board_id The ID of the parent board. |
||
213 | * @return array An array of all the child board IDs. |
||
214 | */ |
||
215 | public function allChildsOf($board_id) |
||
216 | { |
||
217 | if (empty($this->boards[$board_id]['tree']['children'])) |
||
218 | { |
||
219 | return []; |
||
220 | } |
||
221 | |||
222 | 2 | $boardsList = []; |
|
223 | foreach ($this->boards[$board_id]['tree']['children'] as $id => $node) |
||
224 | 2 | { |
|
225 | $boardsList[] = $id; |
||
226 | $boardsList = array_merge($boardsList, $this->allChildsOf($id)); |
||
227 | 2 | } |
|
228 | |||
229 | 2 | return $boardsList; |
|
230 | } |
||
231 | |||
232 | public function getBoardList() |
||
233 | { |
||
234 | return $this->boardList; |
||
235 | } |
||
236 | |||
237 | public function getCategories() |
||
238 | { |
||
239 | return $this->cat_tree; |
||
240 | } |
||
241 | |||
242 | public function getBoards() |
||
243 | { |
||
244 | return $this->boards; |
||
245 | } |
||
246 | |||
247 | public function getCategoryNodeById($id) |
||
248 | { |
||
249 | if (isset($this->cat_tree[$id])) |
||
250 | { |
||
251 | return $this->cat_tree[$id]; |
||
252 | } |
||
253 | |||
254 | throw new \Exception("Category id doesn't exist: " . $id); |
||
255 | } |
||
256 | |||
257 | public function getBoardsInCat($id) |
||
258 | { |
||
259 | if (isset($this->boardList[$id])) |
||
260 | { |
||
261 | return $this->boardList[$id]; |
||
262 | } |
||
263 | |||
264 | throw new \Exception("Category id doesn't exist: " . $id); |
||
265 | } |
||
266 | |||
267 | public function categoryExists($id) |
||
268 | { |
||
269 | return isset($this->boardList[$id]); |
||
270 | } |
||
271 | |||
272 | public function boardExists($id) |
||
273 | { |
||
274 | return isset($this->boards[$id]); |
||
275 | } |
||
276 | |||
277 | /** |
||
278 | * Retrieves a board object by its id. |
||
279 | * |
||
280 | * @param int $id The id of the board. |
||
281 | * |
||
282 | * @return Board The board object with the specified id. |
||
283 | * |
||
284 | * @throws \Exception When the board id doesn't exist. |
||
285 | */ |
||
286 | public function getBoardById($id) |
||
287 | { |
||
288 | if (isset($this->boards[$id])) |
||
289 | { |
||
290 | return $this->boards[$id]; |
||
291 | } |
||
292 | |||
293 | throw new \Exception("Board id doesn't exist: " . $id); |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * Returns whether the sub-board id is actually a child of the parent (recursive). |
||
298 | * |
||
299 | * @param int $child The ID of the child board |
||
300 | * @param int $parent The ID of a parent board |
||
301 | * |
||
302 | * @return bool if the specified child board is a child of the specified parent board. |
||
303 | */ |
||
304 | public function isChildOf($child, $parent) |
||
305 | { |
||
306 | if (empty($this->boards[$child]['parent'])) |
||
307 | { |
||
308 | return false; |
||
309 | } |
||
310 | |||
311 | if ($this->boards[$child]['parent'] == $parent) |
||
312 | { |
||
313 | return true; |
||
314 | } |
||
315 | |||
316 | return $this->isChildOf($this->boards[$child]['parent'], $parent); |
||
317 | } |
||
318 | |||
319 | /** |
||
320 | * Remove one or more boards. |
||
321 | * |
||
322 | * - Allows to move the children of the board before deleting it |
||
323 | * - if moveChildrenTo is set to null, the sub-boards will be deleted. |
||
324 | * - Deletes: |
||
325 | * - all topics that are on the given boards; |
||
326 | * - all information that's associated with the given boards; |
||
327 | * - updates the statistics to reflect the new situation. |
||
328 | * |
||
329 | * @param int[] $boards_to_remove |
||
330 | * @param int|null $moveChildrenTo = null |
||
331 | * @throws \ElkArte\Exceptions\Exception |
||
332 | */ |
||
333 | public function deleteBoards($boards_to_remove, $moveChildrenTo = null) |
||
334 | { |
||
335 | // No boards to delete? Return! |
||
336 | if (empty($boards_to_remove)) |
||
337 | { |
||
338 | return; |
||
339 | } |
||
340 | |||
341 | call_integration_hook('integrate_delete_board', array($boards_to_remove, &$moveChildrenTo)); |
||
342 | |||
343 | // If $moveChildrenTo is set to null, include the children in the removal. |
||
344 | if ($moveChildrenTo === null) |
||
345 | { |
||
346 | // Get a list of the sub-boards that will also be removed. |
||
347 | $child_boards_to_remove = array(); |
||
348 | foreach ($boards_to_remove as $board_to_remove) |
||
349 | { |
||
350 | $child_boards_to_remove = array_merge($child_boards_to_remove, $this->allChildsOf($board_to_remove)); |
||
351 | } |
||
352 | |||
353 | // Merge the children with their parents. |
||
354 | if (!empty($child_boards_to_remove)) |
||
355 | { |
||
356 | $boards_to_remove = array_unique(array_merge($boards_to_remove, $child_boards_to_remove)); |
||
357 | } |
||
358 | } |
||
359 | // Move the children to a safe home. |
||
360 | else |
||
361 | { |
||
362 | foreach ($boards_to_remove as $id_board) |
||
363 | { |
||
364 | // @todo Separate category? |
||
365 | if ($moveChildrenTo === 0) |
||
366 | { |
||
367 | $this->fixChildren($id_board, 0, 0); |
||
368 | } |
||
369 | else |
||
370 | { |
||
371 | $this->fixChildren($id_board, $this->boards[$moveChildrenTo]['level'] + 1, $moveChildrenTo); |
||
372 | } |
||
373 | } |
||
374 | } |
||
375 | |||
376 | // Delete ALL topics in the selected boards (done first so topics can't be marooned.) |
||
377 | $topics = $this->db->fetchQuery(' |
||
378 | SELECT |
||
379 | id_topic |
||
380 | FROM {db_prefix}topics |
||
381 | WHERE id_board IN ({array_int:boards_to_remove})', |
||
382 | array( |
||
383 | 'boards_to_remove' => $boards_to_remove, |
||
384 | ) |
||
385 | )->fetch_all(); |
||
386 | |||
387 | require_once(SUBSDIR . '/Topic.subs.php'); |
||
388 | removeTopics($topics, false); |
||
389 | |||
390 | // Delete the board's logs. |
||
391 | $this->db->query('', ' |
||
392 | DELETE FROM {db_prefix}log_mark_read |
||
393 | WHERE id_board IN ({array_int:boards_to_remove})', |
||
394 | array( |
||
395 | 'boards_to_remove' => $boards_to_remove, |
||
396 | ) |
||
397 | ); |
||
398 | $this->db->query('', ' |
||
399 | DELETE FROM {db_prefix}log_boards |
||
400 | WHERE id_board IN ({array_int:boards_to_remove})', |
||
401 | array( |
||
402 | 'boards_to_remove' => $boards_to_remove, |
||
403 | ) |
||
404 | ); |
||
405 | $this->db->query('', ' |
||
406 | DELETE FROM {db_prefix}log_notify |
||
407 | WHERE id_board IN ({array_int:boards_to_remove})', |
||
408 | array( |
||
409 | 'boards_to_remove' => $boards_to_remove, |
||
410 | ) |
||
411 | ); |
||
412 | |||
413 | // Delete this board's moderators. |
||
414 | $this->db->query('', ' |
||
415 | DELETE FROM {db_prefix}moderators |
||
416 | WHERE id_board IN ({array_int:boards_to_remove})', |
||
417 | array( |
||
418 | 'boards_to_remove' => $boards_to_remove, |
||
419 | ) |
||
420 | ); |
||
421 | |||
422 | // Delete any extra events in the calendar. |
||
423 | $this->db->query('', ' |
||
424 | DELETE FROM {db_prefix}calendar |
||
425 | WHERE id_board IN ({array_int:boards_to_remove})', |
||
426 | array( |
||
427 | 'boards_to_remove' => $boards_to_remove, |
||
428 | ) |
||
429 | ); |
||
430 | |||
431 | // Delete any message icons that only appear on these boards. |
||
432 | $this->db->query('', ' |
||
433 | DELETE FROM {db_prefix}message_icons |
||
434 | WHERE id_board IN ({array_int:boards_to_remove})', |
||
435 | array( |
||
436 | 'boards_to_remove' => $boards_to_remove, |
||
437 | ) |
||
438 | ); |
||
439 | |||
440 | // Delete the boards. |
||
441 | $this->db->query('', ' |
||
442 | DELETE FROM {db_prefix}boards |
||
443 | WHERE id_board IN ({array_int:boards_to_remove})', |
||
444 | array( |
||
445 | 'boards_to_remove' => $boards_to_remove, |
||
446 | ) |
||
447 | ); |
||
448 | |||
449 | // Latest message/topic might not be there anymore. |
||
450 | require_once(SUBSDIR . '/Messages.subs.php'); |
||
451 | updateMessageStats(); |
||
452 | |||
453 | require_once(SUBSDIR . '/Topic.subs.php'); |
||
454 | updateTopicStats(); |
||
455 | updateSettings(array('calendar_updated' => time())); |
||
456 | |||
457 | // Plus reset the cache to stop people getting odd results. |
||
458 | updateSettings(array('settings_updated' => time())); |
||
459 | |||
460 | // Clean the cache as well. |
||
461 | Cache::instance()->clean('data'); |
||
462 | |||
463 | // Let's do some serious logging. |
||
464 | foreach ($boards_to_remove as $id_board) |
||
465 | { |
||
466 | logAction('delete_board', array('boardname' => $this->boards[$id_board]['name']), 'admin'); |
||
467 | } |
||
468 | |||
469 | $this->reorderBoards(); |
||
470 | } |
||
471 | |||
472 | /** |
||
473 | * Fixes the children of a board by setting their child_levels to new values. |
||
474 | * |
||
475 | * - Used when a board is deleted or moved, to affect its children. |
||
476 | * |
||
477 | * @param int $parent |
||
478 | * @param int $newLevel |
||
479 | * @param int $newParent |
||
480 | */ |
||
481 | private function fixChildren($parent, $newLevel, $newParent) |
||
482 | { |
||
483 | // Grab all children of $parent... |
||
484 | $children = $this->db->fetchQuery(' |
||
485 | SELECT |
||
486 | id_board |
||
487 | FROM {db_prefix}boards |
||
488 | WHERE id_parent = {int:parent_board}', |
||
489 | array( |
||
490 | 'parent_board' => $parent, |
||
491 | ) |
||
492 | )->fetch_callback( |
||
493 | static fn($row) => (int) $row['id_board'] |
||
494 | ); |
||
495 | |||
496 | // ...and set it to a new parent and child_level. |
||
497 | $this->db->query('', ' |
||
498 | UPDATE {db_prefix}boards |
||
499 | SET |
||
500 | id_parent = {int:new_parent}, child_level = {int:new_child_level} |
||
501 | 2 | WHERE id_parent = {int:parent_board}', |
|
502 | array( |
||
503 | 2 | 'new_parent' => $newParent, |
|
504 | 2 | 'new_child_level' => $newLevel, |
|
505 | 'parent_board' => $parent, |
||
506 | ) |
||
507 | 2 | ); |
|
508 | 2 | ||
509 | // Recursively fix the children of the children. |
||
510 | 2 | foreach ($children as $child) |
|
511 | { |
||
512 | 2 | $this->fixChildren($child, $newLevel + 1, $child); |
|
513 | } |
||
514 | 2 | } |
|
515 | 2 | ||
516 | /** |
||
517 | 1 | * Put all boards in the right order and sorts the records of the boards table. |
|
518 | * |
||
519 | * - Used by modifyBoard(), deleteBoards(), modifyCategory(), and deleteCategories() functions |
||
520 | 2 | */ |
|
521 | 2 | public function reorderBoards() |
|
522 | { |
||
523 | 2 | $update_query = ''; |
|
524 | 2 | $update_params = []; |
|
525 | $this->loadBoardTree(); |
||
526 | |||
527 | // Set the board order for each category. |
||
528 | $board_order = 0; |
||
529 | foreach ($this->cat_tree as $catID => $dummy) |
||
530 | { |
||
531 | 2 | foreach ($this->boardList[$catID] as $boardID) |
|
532 | { |
||
533 | if ($this->boards[$boardID]['order'] != ++$board_order) |
||
534 | { |
||
535 | $update_query .= sprintf( |
||
536 | 2 | ' |
|
537 | WHEN {int:selected_board%1$d} THEN {int:new_order%1$d}', |
||
538 | $boardID |
||
539 | 2 | ); |
|
540 | |||
541 | 1 | $update_params = array_merge( |
|
542 | $update_params, |
||
543 | 2 | [ |
|
544 | 'new_order' . $boardID => $board_order, |
||
545 | 'selected_board' . $boardID => $boardID, |
||
546 | ] |
||
547 | ); |
||
548 | } |
||
549 | } |
||
550 | } |
||
551 | |||
552 | if (empty($update_query)) |
||
0 ignored issues
–
show
|
|||
553 | { |
||
554 | return; |
||
555 | } |
||
556 | |||
557 | $this->db->query('', |
||
558 | 'UPDATE {db_prefix}boards |
||
559 | SET |
||
560 | board_order = CASE id_board ' . $update_query . ' ELSE board_order END', |
||
561 | $update_params |
||
562 | ); |
||
563 | } |
||
564 | } |
||
565 |