Issues (1696)

sources/ElkArte/BoardsTree.php (3 issues)

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