Issues (1686)

sources/ElkArte/Controller/MoveTopic.php (8 issues)

1
<?php
2
3
/**
4
 * Handles the moving of topics from board to board
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\Controller;
18
19
use ElkArte\AbstractController;
20
use ElkArte\Exceptions\Exception;
21
use ElkArte\Helper\Util;
22
use ElkArte\Languages\Loader;
23
24
/**
25
 * Move Topic Controller
26
 */
27
class MoveTopic extends AbstractController
28
{
29
	/** @var int The id of the topic being manipulated */
30
	private $_topic;
31
32
	/** @var array Information about the topic being moved  */
33
	private $_topic_info;
34
35
	/** @var array Information about the board where the topic resides */
36
	private $_board_info;
37
38
	/** @var int Board that will receive the topic */
39
	private $_toboard;
40
41
	/**
42
	 * Pre Dispatch, called before other methods.
43
	 */
44
	public function pre_dispatch()
45
	{
46
		global $topic;
47
48
		// Set the topic from the global, yes yes
49
		$this->_topic = $topic;
50
	}
51
52
	/**
53
	 * Forwards to the action method to handle the action.
54
	 *
55
	 * @see AbstractController::action_index
56
	 */
57
	public function action_index()
58
	{
59
		// move a topic, what else?!
60
		// $this->action_movetopic();
61
	}
62
63
	/**
64
	 * This function allows to move a topic
65
	 *
66
	 * What it does:
67
	 *
68
	 * - It must be called with a topic specified. (that is, global $topic must
69
	 * be set... @todo fix this thing.)
70
	 * - Validates access
71
	 * - Accessed via ?action=movetopic.
72
	 *
73
	 * @uses template_move_topic() sub-template in MoveTopic.template.php
74
	 */
75
	public function action_movetopic()
76
	{
77
		global $context;
78
79
		// Lets make sure they can access the topic being moved and have permissions to move it
80
		$this->_check_access();
81
82
		// Get a list of boards this moderator can move to.
83
		require_once(SUBSDIR . '/Boards.subs.php');
84
		$context += getBoardList(array('not_redirection' => true));
85
86
		// No boards?
87
		if (empty($context['categories']) || $context['num_boards'] == 1)
88
		{
89
			throw new Exception('moveto_noboards', false);
90
		}
91
92
		// Already used the function, let's set the selected board back to the last
93
		$last_moved_to = isset($_SESSION['move_to_topic']['move_to']) && $_SESSION['move_to_topic']['move_to'] != $context['current_board'] ? (int) $_SESSION['move_to_topic']['move_to'] : 0;
94
		if (!empty($last_moved_to))
95
		{
96
			foreach ($context['categories'] as $id => $values)
97
			{
98
				if (isset($values['boards'][$last_moved_to]))
99
				{
100
					$context['categories'][$id]['boards'][$last_moved_to]['selected'] = true;
101
					break;
102
				}
103
			}
104
		}
105
106
		// Set up for the template
107
		theme()->getTemplates()->load('MoveTopic');
108
		$this->_prep_template();
109
	}
110
111
	/**
112
	 * Validates that the member can access the topic
113
	 *
114
	 * What it does:
115
	 *
116
	 * - Checks that a topic is supplied
117
	 * - Validates the topic information can be loaded
118
	 * - If the topic is not approved yet, must have approve permissions to move it
119
	 * - If the member is the topic starter requires the move_own permission, otherwise the move_any permission.
120
	 */
121
	private function _check_access()
122
	{
123
		global $modSettings;
124
125
		if (empty($this->_topic))
126
		{
127
			throw new Exception('no_access', false);
128
		}
129
130
		// Retrieve the basic topic information for whats being moved
131
		require_once(SUBSDIR . '/Topic.subs.php');
132
		$this->_topic_info = getTopicInfo($this->_topic, 'message');
0 ignored issues
show
Documentation Bug introduced by
It seems like getTopicInfo($this->_topic, 'message') can also be of type boolean. However, the property $_topic_info is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
133
134
		if (empty($this->_topic_info))
135
		{
136
			throw new Exception('topic_gone', false);
137
		}
138
139
		// Can they see it - if not approved?
140
		if ($modSettings['postmod_active'] && !$this->_topic_info['approved'])
141
		{
142
			isAllowedTo('approve_posts');
143
		}
144
145
		// Are they allowed to actually move any topics or even their own?
146
		if (allowedTo('move_any'))
147
		{
148
			return;
149
		}
150
151
		if (!($this->_topic_info['id_member_started'] == $this->user->id && !allowedTo('move_own')))
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
152
		{
153
			return;
154
		}
155
156
		throw new Exception('cannot_move_any', false);
157
	}
158
159
	/**
160
	 * Prepares the content for use in the move topic template
161
	 */
162
	private function _prep_template()
163
	{
164
		global $context, $txt, $language, $board;
165
166
		$context['is_approved'] = $this->_topic_info['approved'];
167
		$context['subject'] = $this->_topic_info['subject'];
168
		$context['redirect_topic'] = isset($_SESSION['move_to_topic']['redirect_topic']) ? (int) $_SESSION['move_to_topic']['redirect_topic'] : 0;
169
		$context['redirect_expires'] = isset($_SESSION['move_to_topic']['redirect_expires']) ? (int) $_SESSION['move_to_topic']['redirect_expires'] : 0;
170
		$context['page_title'] = $txt['move_topic'];
171
		$context['sub_template'] = 'move_topic';
172
173
		// Breadcrumbs
174
		$context['breadcrumbs'][] = [
175
			'url' => getUrl('topic', ['topic' => $this->_topic, 'start' => 0, 'subject' => $context['subject']]),
176
			'name' => $context['subject'],
177
		];
178
		$context['breadcrumbs'][] = [
179
			'url' => '#',
180
			'name' => $txt['move_topic'],
181
		];
182
183
		$context['back_to_topic'] = isset($this->_req->post->goback);
184
185
		// Ugly !
186
		if ($this->user->language !== $language)
0 ignored issues
show
Bug Best Practice introduced by
The property language does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
187
		{
188
			$mtxt = [];
189
			$lang = new Loader($language, $mtxt, database());
190
			$lang->load('index');
191
			$txt['movetopic_default'] = $mtxt['movetopic_default'];
192
		}
193
194
		// We will need this
195
		if (isset($this->_req->query->current_board))
196
		{
197
			moveTopicConcurrence((int) $this->_req->query->current_board, $board, $this->_topic);
198
		}
199
200
		// Register this form and get a sequence number in $context.
201
		checkSubmitOnce('register');
202
	}
203
204
	/**
205
	 * Executes the actual move of a topic.
206
	 *
207
	 * What it does:
208
	 *
209
	 * - It is called on the submit of action_movetopic.
210
	 * - This function logs that topics have been moved in the moderation log.
211
	 * - Upon successful completion redirects to message index.
212
	 * - Accessed via ?action=movetopic2.
213
	 *
214
	 * @uses subs/Post.subs.php.
215
	 */
216
	public function action_movetopic2()
217
	{
218
		global $board;
219
220
		$this->_check_access_2();
221
222
		checkSession();
223
		require_once(SUBSDIR . '/Post.subs.php');
224
		require_once(SUBSDIR . '/Boards.subs.php');
225
226
		// The destination board must be numeric.
227
		$this->_toboard = (int) $this->_req->post->toboard;
228
229
		// Make sure they can see the board they are trying to move to (and get whether posts count in the target board).
230
		$this->_board_info = boardInfo($this->_toboard, $this->_topic);
231
		if (empty($this->_board_info))
232
		{
233
			throw new Exception('no_board');
234
		}
235
236
		// Remember this for later.
237
		$_SESSION['move_to_topic'] = array(
238
			'move_to' => $this->_toboard
239
		);
240
241
		// Rename the topic if needed
242
		$this->_rename_topic();
243
244
		// Create a link to this in the old board.
245
		$this->_post_redirect();
246
247
		// Account for boards that count posts and those that don't
248
		$this->_count_update();
249
250
		// Do the move (includes statistics update needed for the redirect topic).
251
		moveTopics($this->_topic, $this->_toboard);
252
253
		// Log that they moved this topic.
254
		if (!allowedTo('move_own') || $this->_topic_info['id_member_started'] != $this->user->id)
255
		{
256
			logAction('move', array('topic' => $this->_topic, 'board_from' => $board, 'board_to' => $this->_toboard));
257
		}
258
259
		// Notify people that this topic has been moved?
260
		require_once(SUBSDIR . '/Notification.subs.php');
261
		sendNotifications($this->_topic, 'move');
262
263
		// Why not go back to the original board in case they want to keep moving?
264
		if (!isset($this->_req->post->goback))
265
		{
266
			redirectexit('board=' . $board . '.0');
267
		}
268
		else
269
		{
270
			redirectexit('topic=' . $this->_topic . '.0');
271
		}
272
	}
273
274
	/**
275
	 * Checks access and input validation before committing the move
276
	 *
277
	 * What it does:
278
	 *
279
	 * - Checks that a topic is supplied
280
	 * - Validates the move location
281
	 * - Checks redirection details if its a redirection is to be posted
282
	 * - If the member is the topic starter requires the move_own permission, otherwise the move_any permission.
283
	 *
284
	 * @return bool
285
	 * @throws \ElkArte\Exceptions\Exception no_access
286
	 */
287
	private function _check_access_2()
288
	{
289
		global $board;
290
291
		if (empty($this->_topic))
292
		{
293
			throw new Exception('no_access', false);
294
		}
295
296
		// You can't choose to have a redirection topic and not provide a reason.
297
		if (isset($this->_req->post->postRedirect) && $this->_req->getPost('reason', 'trim', '') === '')
298
		{
299
			throw new Exception('movetopic_no_reason', false);
300
		}
301
302
		// You have to tell us were you are moving to
303
		if (!isset($this->_req->post->toboard))
304
		{
305
			throw new Exception('movetopic_no_board', false);
306
		}
307
308
		// We will need this
309
		require_once(SUBSDIR . '/Topic.subs.php');
310
		if (isset($this->_req->query->current_board))
311
		{
312
			moveTopicConcurrence((int) $this->_req->query->current_board, $board, $this->_topic);
313
		}
314
315
		// Make sure this form hasn't been submitted before.
316
		checkSubmitOnce('check');
317
318
		// Get the basic details on this topic (again)
319
		$this->_topic_info = getTopicInfo($this->_topic);
0 ignored issues
show
Documentation Bug introduced by
It seems like getTopicInfo($this->_topic) can also be of type boolean. However, the property $_topic_info is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
320
321
		// Not approved then you need approval permissions to move it as well
322
		if (!$this->_topic_info['approved'])
323
		{
324
			isAllowedTo('approve_posts');
325
		}
326
327
		// Can they move topics on this board?
328
		if (!allowedTo('move_any'))
329
		{
330
			if ($this->_topic_info['id_member_started'] == $this->user->id)
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
331
			{
332
				isAllowedTo('move_own');
333
			}
334
			else
335
			{
336
				isAllowedTo('move_any');
337
			}
338
		}
339
340
		return true;
341
	}
342
343
	/**
344
	 * Renames the topic during the move if requested
345
	 *
346
	 * What it does:
347
	 *
348
	 * - Renames the moved topic with a new topic subject
349
	 * - If enforce_subject is set, renames all posts withing the moved topic posts with a new subject
350
	 */
351
	private function _rename_topic()
352
	{
353
		global $context;
354
355
		// Rename the topic...
356
		if (isset($this->_req->post->reset_subject, $this->_req->post->custom_subject) && $this->_req->post->custom_subject != '')
357
		{
358
			$custom_subject = strtr(Util::htmltrim(Util::htmlspecialchars($this->_req->post->custom_subject)), array("\r" => '', "\n" => '', "\t" => ''));
359
360
			// Keep checking the length.
361
			if (Util::strlen($custom_subject) > 100)
362
			{
363
				$custom_subject = Util::substr($custom_subject, 0, 100);
364
			}
365
366
			// If it's still valid move onwards and upwards.
367
			if ($custom_subject !== '')
368
			{
369
				$this->_board_info['subject_new'] = $custom_subject;
370
				$all_messages = isset($this->_req->post->enforce_subject);
371
				if ($all_messages)
372
				{
373
					// Get a response prefix, but in the forum's default language.
374
					$context['response_prefix'] = response_prefix();
375
376
					topicSubject($this->_topic_info, $custom_subject, $context['response_prefix'], $all_messages);
377
				}
378
				else
379
				{
380
					topicSubject($this->_topic_info, $custom_subject);
381
				}
382
383
				// Fix the subject cache.
384
				require_once(SUBSDIR . '/Messages.subs.php');
385
				updateSubjectStats($this->_topic, $custom_subject);
386
			}
387
		}
388
	}
389
390
	/**
391
	 * Posts a redirection topic in the original location of the moved topic
392
	 *
393
	 * What it does:
394
	 *
395
	 * - If leaving a moved "where did it go" topic, validates the needed inputs
396
	 * - Posts a new topic in the originating board of the topic to be moved.
397
	 */
398
	private function _post_redirect()
399
	{
400
		global $board, $language;
401
402
		// @todo Does this make sense if the topic was unapproved before? I'd just about say so.
403
		if (isset($this->_req->post->postRedirect))
404
		{
405
			// Should be in the boardwide language.
406
			if ($this->user->language !== $language)
0 ignored issues
show
Bug Best Practice introduced by
The property language does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
407
			{
408
				$mtxt = [];
409
				$lang = new Loader($language, $mtxt, database());
410
				$lang->load('index');
411
			}
412
413
			$reason = Util::htmlspecialchars($this->_req->post->reason, ENT_QUOTES);
414
			preparsecode($reason);
415
416
			// Add a URL onto the message.
417
			$reason = strtr($reason, array(
418
				$mtxt['movetopic_auto_board'] => '[url=' . getUrl('board', ['board' => $this->_toboard, 'start' => 0, 'name' => $this->_board_info['name']]) . ']' . $this->_board_info['name'] . '[/url]',
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $mtxt does not seem to be defined for all execution paths leading up to this point.
Loading history...
419
				$mtxt['movetopic_auto_topic'] => '[iurl=' . getUrl('topic', ['topic' => $this->_topic, 'start' => 0, 'subject' => $this->_board_info['subject_new'] ?? $this->_board_info['subject']]) . ']' . ($this->_board_info['subject_new'] ?? $this->_board_info['subject']) . '[/iurl]'
420
			));
421
422
			// Auto remove this MOVED redirection topic in the future?
423
			$redirect_expires = empty($this->_req->post->redirect_expires) ? 0 : (int) $this->_req->post->redirect_expires;
424
425
			// Redirect to the MOVED topic from topic list?
426
			$redirect_topic = isset($this->_req->post->redirect_topic) ? $this->_topic : 0;
427
428
			// And remember the last expiry period too.
429
			$_SESSION['move_to_topic']['redirect_topic'] = $redirect_topic;
430
			$_SESSION['move_to_topic']['redirect_expires'] = $redirect_expires;
431
432
			$msgOptions = array(
433
				'subject' => $mtxt['moved'] . ': ' . $this->_board_info['subject'],
434
				'body' => $reason,
435
				'icon' => 'moved',
436
				'smileys_enabled' => 1,
437
			);
438
439
			$topicOptions = array(
440
				'board' => $board,
441
				'lock_mode' => 1,
442
				'mark_as_read' => true,
443
				'redirect_expires' => empty($redirect_expires) ? 0 : ($redirect_expires * 60) + time(),
444
				'redirect_topic' => $redirect_topic,
445
			);
446
447
			$posterOptions = array(
448
				'id' => $this->user->id,
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
449
				'update_post_count' => empty($this->_board_info['count_posts']),
450
			);
451
			createPost($msgOptions, $topicOptions, $posterOptions);
452
		}
453
	}
454
455
	/**
456
	 * Accounts for board / user post counts when a topic is moved.
457
	 *
458
	 * What it does:
459
	 *
460
	 * - Checks if a topic is being moved to/from a board that does/does'nt count posts.
461
	 */
462
	private function _count_update()
463
	{
464
		global $board;
465
466
		$board_from = boardInfo($board);
467
		if ($board_from['count_posts'] != $this->_board_info['count_posts'])
468
		{
469
			require_once(SUBSDIR . '/Members.subs.php');
470
			$posters = postersCount($this->_topic);
471
472
			foreach ($posters as $id_member => $posts)
473
			{
474
				// The board we're moving from counted posts, but not to.
475
				if (empty($board_from['count_posts']))
476
				{
477
					updateMemberData($id_member, array('posts' => 'posts - ' . $posts));
478
				}
479
				// The reverse: from didn't, to did.
480
				else
481
				{
482
					updateMemberData($id_member, array('posts' => 'posts + ' . $posts));
483
				}
484
			}
485
		}
486
	}
487
}
488