Issues (1686)

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
	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
The variable $curLevel does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The variable $prevBoard does not seem to be defined for all execution paths leading up to this point.
Loading history...
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
The condition empty($update_query) is always true.
Loading history...
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