ManagePortalArticles_Controller::pre_dispatch()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @package SimplePortal ElkArte
5
 *
6
 * @author SimplePortal Team
7
 * @copyright 2015-2021 SimplePortal Team
8
 * @license BSD 3-clause
9
 * @version 1.0.0
10
 */
11
12
use BBC\PreparseCode;
13
use ElkArte\Errors\AttachmentErrorContext;
14
use ElkArte\Errors\ErrorContext;
15
16
/**
17
 * SimplePortal Article Administration controller class.
18
 * This class handles the adding/editing/listing of articles
19
 */
20
class ManagePortalArticles_Controller extends Action_Controller
21
{
22
	/** @var bool|int hold the article id if existing */
23
	protected $_is_aid;
24
25
	/** @var array */
26
	protected $_attachments;
27
28
	/** @var ErrorContext */
29
	protected $article_errors;
30
31
	/** @var AttachmentErrorContext */
32
	protected $attach_errors;
33
34
	/**
35
	 * This method is executed before any action handler.
36
	 * Loads common things for all methods
37
	 */
38
	public function pre_dispatch()
39
	{
40
		// We'll need the utility functions from here.
41
		require_once(SUBSDIR . '/PortalAdmin.subs.php');
42
		require_once(SUBSDIR . '/Portal.subs.php');
43
		require_once(SUBSDIR . '/PortalArticle.subs.php');
44
	}
45
46
	/**
47
	 * Main article dispatcher.
48
	 *
49
	 * This function checks permissions and passes control through.
50
	 */
51
	public function action_index()
52
	{
53
		global $context, $txt;
54
55
		// You need to be an admin or have manage article permissions
56
		if (!allowedTo('sp_admin'))
57
		{
58
			isAllowedTo('sp_manage_articles');
59
		}
60
61
		loadTemplate('PortalAdminArticles');
62
63
		// These are all the actions that we know
64
		$subActions = array(
65
			'list' => array($this, 'action_list'),
66
			'add' => array($this, 'action_edit'),
67
			'edit' => array($this, 'action_edit'),
68
			'status' => array($this, 'action_status'),
69
			'delete' => array($this, 'action_delete'),
70
		);
71
72
		// Start up the controller, provide a hook since we can
73
		$action = new Action('portal_articles');
74
75
		// Set up the tab data
76
		$context[$context['admin_menu_name']]['tab_data'] = array(
77
			'title' => $txt['sp_admin_articles_title'],
78
			'help' => 'sp_ArticlesArea',
79
			'description' => $txt['sp_admin_articles_desc'],
80
			'tabs' => array(
81
				'list' => array(),
82
				'add' => array(),
83
			),
84
		);
85
86
		// By default we want to list articles
87
		$subAction = $action->initialize($subActions, 'list');
88
		$context['sub_action'] = $subAction;
89
90
		// Call the right function for this sub-action
91
		$action->dispatch($subAction);
92
	}
93
94
	/**
95
	 * Show a listing of articles in the system
96
	 */
97
	public function action_list()
98
	{
99
		global $context, $scripturl, $txt, $modSettings;
100
101
		// Build the listoption array to display the categories
102
		$listOptions = array(
103
			'id' => 'portal_articles',
104
			'title' => $txt['sp_admin_articles_list'],
105
			'items_per_page' => $modSettings['defaultMaxMessages'],
106
			'no_items_label' => $txt['error_sp_no_articles'],
107
			'base_href' => $scripturl . '?action=admin;area=portalarticles;sa=list;',
108
			'default_sort_col' => 'title',
109
			'get_items' => array(
110
				'function' => array($this, 'list_spLoadArticles'),
111
			),
112
			'get_count' => array(
113
				'function' => array($this, 'list_spCountArticles'),
114
			),
115
			'columns' => array(
116
				'title' => array(
117
					'header' => array(
118
						'value' => $txt['sp_admin_articles_col_title'],
119
					),
120
					'data' => array(
121
						'db' => 'title',
122
					),
123
					'sort' => array(
124
						'default' => 'title',
125
						'reverse' => 'title DESC',
126
					),
127
				),
128
				'namespace' => array(
129
					'header' => array(
130
						'value' => $txt['sp_admin_articles_col_namespace'],
131
					),
132
					'data' => array(
133
						'db' => 'article_id',
134
					),
135
					'sort' => array(
136
						'default' => 'article_namespace',
137
						'reverse' => 'article_namespace DESC',
138
					),
139
				),
140
				'category' => array(
141
					'header' => array(
142
						'value' => $txt['sp_admin_articles_col_category'],
143
					),
144
					'data' => array(
145
						'db' => 'category_name',
146
					),
147
					'sort' => array(
148
						'default' => 'name',
149
						'reverse' => 'name DESC',
150
					),
151
				),
152
				'author' => array(
153
					'header' => array(
154
						'value' => $txt['sp_admin_articles_col_author'],
155
					),
156
					'data' => array(
157
						'db' => 'author_name',
158
					),
159
					'sort' => array(
160
						'default' => 'author_name',
161
						'reverse' => 'author_name DESC',
162
					),
163
				),
164
				'type' => array(
165
					'header' => array(
166
						'value' => $txt['sp_admin_articles_col_type'],
167
					),
168
					'data' => array(
169
						'db' => 'type',
170
					),
171
					'sort' => array(
172
						'default' => 'type',
173
						'reverse' => 'type DESC',
174
					),
175
				),
176
				'date' => array(
177
					'header' => array(
178
						'value' => $txt['sp_admin_articles_col_date'],
179
					),
180
					'data' => array(
181
						'db' => 'date',
182
					),
183
					'sort' => array(
184
						'default' => 'date',
185
						'reverse' => 'date DESC',
186
					),
187
				),
188
				'status' => array(
189
					'header' => array(
190
						'value' => $txt['sp_admin_articles_col_status'],
191
						'class' => 'centertext',
192
					),
193
					'data' => array(
194
						'db' => 'status_image',
195
						'class' => 'centertext',
196
					),
197
					'sort' => array(
198
						'default' => 'status',
199
						'reverse' => 'status DESC',
200
					),
201
				),
202
				'action' => array(
203
					'header' => array(
204
						'value' => $txt['sp_admin_articles_col_actions'],
205
						'class' => 'centertext',
206
					),
207
					'data' => array(
208
						'sprintf' => array(
209
							'format' => '
210
								<a href="?action=admin;area=portalarticles;sa=edit;article_id=%1$s;' . $context['session_var'] . '=' . $context['session_id'] . '" accesskey="e">' . sp_embed_image('edit') . '</a>&nbsp;
211
								<a href="?action=admin;area=portalarticles;sa=delete;article_id=%1$s;' . $context['session_var'] . '=' . $context['session_id'] . '" onclick="return confirm(' . JavaScriptEscape($txt['sp_admin_articles_delete_confirm']) . ') && submitThisOnce(this);" accesskey="d">' . sp_embed_image('trash') . '</a>',
212
							'params' => array(
213
								'id' => true,
214
							),
215
						),
216
						'class' => 'centertext nowrap',
217
					),
218
				),
219
				'check' => array(
220
					'header' => array(
221
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
222
						'class' => 'centertext',
223
					),
224
					'data' => array(
225
						'function' => function ($row)
226
						{
227
							return '<input type="checkbox" name="remove[]" value="' . $row['id'] . '" class="input_check" />';
228
						},
229
						'class' => 'centertext',
230
					),
231
				),
232
			),
233
			'form' => array(
234
				'href' => $scripturl . '?action=admin;area=portalarticles;sa=delete',
235
				'include_sort' => true,
236
				'include_start' => true,
237
				'hidden_fields' => array(
238
					$context['session_var'] => $context['session_id'],
239
				),
240
			),
241
			'additional_rows' => array(
242
				array(
243
					'class' => 'submitbutton',
244
					'position' => 'below_table_data',
245
					'value' => '<a class="linkbutton" href="?action=admin;area=portalarticles;sa=add;' . $context['session_var'] . '=' . $context['session_id'] . '" accesskey="a">' . $txt['sp_admin_articles_add'] . '</a>
246
						<input type="submit" name="remove_articles" value="' . $txt['sp_admin_articles_remove'] . '" />',
247
				),
248
			),
249
		);
250
251
		// Set the context values
252
		$context['page_title'] = $txt['sp_admin_articles_title'];
253
		$context['sub_template'] = 'show_list';
254
		$context['default_list'] = 'portal_articles';
255
256
		// Create the list.
257
		require_once(SUBSDIR . '/GenericList.class.php');
258
		createList($listOptions);
259
	}
260
261
	/**
262
	 * Callback for createList(),
263
	 * Returns the number of articles in the system
264
	 */
265
	public function list_spCountArticles()
266
	{
267
		return sp_count_articles();
268
	}
269
270
	/**
271
	 * Callback for createList()
272
	 * Returns an array of articles
273
	 *
274
	 * @param int $start
275
	 * @param int $items_per_page
276
	 * @param string $sort
277
	 *
278
	 * @return array
279
	 */
280
	public function list_spLoadArticles($start, $items_per_page, $sort)
281
	{
282
		return sp_load_articles($start, $items_per_page, $sort);
283
	}
284
285
	/**
286
	 * Edits an existing OR adds a new article to the system
287
	 *
288
	 * - Handles the previewing of an article
289
	 * - Handles article attachments
290
	 */
291
	public function action_edit()
292
	{
293
		global $context, $txt;
294
295
		// Load dependency's, prepare error checking
296
		$this->editInit();
297
298
		// Want to save the work?
299
		if (!empty($_POST['submit']) && !$this->article_errors->hasErrors() && !$this->attach_errors->hasErrors())
300
		{
301
			// If the session has timed out, let the user re-submit their form.
302
			if (checkSession('post', '', false) !== '')
303
			{
304
				$this->article_errors->addError('session_timeout');
305
306
				// Disable the preview and the save
307
				unset($_POST['preview'], $_POST['submit']);
308
309
				return $this->action_edit();
310
			}
311
312
			// Check for errors and if none Save it
313
			$this->_sportal_admin_article_edit_save();
314
		}
315
316
		// Prepare the form fields, preview, errors, etc
317
		$this->prepareArticleForm();
318
319
		// On to the editor
320
		if ($context['article']['type'] === 'bbc')
321
		{
322
			$context['article']['body'] = PreparseCode::instance()->un_preparsecode($context['article']['body']);
323
			$context['article']['body'] = str_replace(array('"', '<', '>', '&nbsp;'), array('&quot;', '&lt;', '&gt;', ' '), $context['article']['body']);
324
		}
325
326
		$this->prepareEditor();
327
328
		// Final bits for the template, category's, styles and permission profiles
329
		$this->loadProfileContext();
330
331
		// Attachments
332
		if ($context['attachments']['can']['post'])
333
		{
334
			$this->article_attachment();
335
			$this->article_attachment_dd();
336
		}
337
338
		// Set the globals, spplugin will be called with editor init to set mode
339
		addConversionJS($context['article']['type']);
340
341
		// Finally the main template
342
		loadTemplate('PortalAdminArticles');
343
		$context['sub_template'] = 'articles';
344
345
		// The article above/below template
346
		$template_layers = Template_Layers::instance();
347
		$template_layers->add('articles_edit');
348
349
		// Page out values
350
		$context[$context['admin_menu_name']]['current_subsection'] = 'add';
351
		$context['article']['style'] = sportal_select_style($context['article']['styles']);
352
		$context['is_new'] = $this->_is_aid;
353
		$context['article']['body'] = sportal_parse_content($context['article']['body'], $context['article']['type'], 'return');
354
		$context['page_title'] = !$this->_is_aid ? $txt['sp_admin_articles_add'] : $txt['sp_admin_articles_edit'];
355
356
		return true;
357
	}
358
359
	/**
360
	 * Loads in dependency's for saving or editing an article
361
	 */
362
	private function editInit()
363
	{
364
		global $modSettings, $context;
365
366
		$this->_is_aid = empty($_REQUEST['article_id']) ? false : (int) $_REQUEST['article_id'];
367
368
		// Going to use editor, attachment and post functions
369
		require_once(SUBSDIR . '/Post.subs.php');
370
		require_once(SUBSDIR . '/Editor.subs.php');
371
		require_once(SUBSDIR . '/Attachments.subs.php');
372
373
		loadLanguage('Post');
374
		loadLanguage('Errors');
375
376
		// Errors are likely
377
		$this->article_errors = ErrorContext::context('article', 0);
378
		$this->attach_errors = AttachmentErrorContext::context();
379
		$this->attach_errors->activate();
380
381
		$context['attachments']['can']['post'] = !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1 && (allowedTo('post_attachment'));
382
	}
383
384
	/**
385
	 * Does the actual saving of the article data
386
	 *
387
	 * - Validates the data is safe to save
388
	 * - Updates existing articles or creates new ones
389
	 */
390
	private function _sportal_admin_article_edit_save()
391
	{
392
		global $context, $txt, $modSettings;
393
394
		checkSession();
395
396
		// Use our standard validation functions in a few spots
397
		require_once(SUBSDIR . '/DataValidator.class.php');
398
		$validator = new Data_Validator();
399
400
		// If it exists, load the current data
401
		if ($this->_is_aid)
402
		{
403
			$_POST['article_id'] = $this->_is_aid;
404
			$context['article'] = sportal_get_articles($this->_is_aid);
405
		}
406
407
		// Clean and Review the post data for compliance
408
		$validator->sanitation_rules(array(
409
			'title' => 'trim|Util::htmlspecialchars',
410
			'namespace' => 'trim|Util::htmlspecialchars',
411
			'article_id' => 'intval',
412
			'category_id' => 'intval',
413
			'permissions' => 'intval',
414
			'styles' => 'intval',
415
			'type' => 'trim',
416
			'content' => 'trim'
417
		));
418
		$validator->validation_rules(array(
419
			'title' => 'required',
420
			'namespace' => 'alpha_numeric|required',
421
			'type' => 'required',
422
			'content' => 'required'
423
		));
424
		$validator->text_replacements(array(
425
			'title' => $txt['sp_admin_articles_col_title'],
426
			'namespace' => $txt['sp_admin_articles_col_namespace'],
427
			'content' => $txt['sp_admin_articles_col_body']
428
		));
429
430
		// If you messed this up, tell them why
431
		if (!$validator->validate($_POST))
432
		{
433
			foreach ($validator->validation_errors() as $id => $error)
434
			{
435
				$this->article_errors->addError($error);
436
			}
437
		}
438
439
		// Lets make sure this namespace (article id) is unique
440
		$has_duplicate = sp_duplicate_articles($validator->article_id, $validator->namespace);
441
		if (!empty($has_duplicate))
442
		{
443
			$this->article_errors->addError('sp_error_article_namespace_duplicate');
444
		}
445
446
		// And we can't have just a numeric namespace (article id)
447
		if (preg_replace('~[0-9]+~', '', $validator->namespace) === '')
448
		{
449
			$this->article_errors->addError('sp_error_article_namespace_numeric');
450
		}
451
452
		// Posting some PHP code, and allowed? Then we need to validate it will run
453
		if ($_POST['type'] === 'php' && !empty($_POST['content']) && empty($modSettings['sp_disable_php_validation']))
454
		{
455
			$validator_php = new Data_Validator();
456
			$validator_php->validation_rules(array('content' => 'php_syntax'));
457
458
			// Bad PHP code
459
			if (!$validator_php->validate(array('content' => $_POST['content'])))
460
			{
461
				$this->article_errors->addError($validator_php->validation_errors());
462
			}
463
		}
464
465
		// Check / Prep attachments
466
		$this->processArticleAttachments();
467
468
		// None shall pass ... with errors
469
		if ($this->article_errors->hasErrors() || $this->attach_errors->hasErrors())
470
		{
471
			unset($_POST['submit']);
472
473
			return false;
474
		}
475
476
		// No errors then, prepare the data for saving
477
		$article_info = array(
478
			'id' => $validator->article_id,
479
			'id_category' => $validator->category_id,
480
			'namespace' => $validator->namespace,
481
			'title' => $validator->title,
482
			'body' => Util::htmlspecialchars($_POST['content'], ENT_QUOTES),
483
			'type' => in_array($validator->type, array('bbc', 'html', 'php', 'markdown')) ? $_POST['type'] : 'bbc',
484
			'permissions' => $validator->permissions,
485
			'styles' => $validator->styles,
486
			'status' => !empty($_POST['status']) ? 1 : 0,
487
		);
488
489
		if ($article_info['type'] === 'bbc')
490
		{
491
			PreparseCode::instance()->preparsecode($article_info['body'], false);
492
		}
493
494
		// Bind attachments to the article if existing, create any needed thumbnails,
495
		// move them to the sp attachment directory
496
		$attachIDs = $this->finalizeArticleAttachments($article_info);
497
498
		// Save the article
499
		$this->_is_aid = sp_save_article($article_info, empty($this->_is_aid));
500
501
		// If this was a new article, with attachments, bind them to one another.
502
		if (empty($validator->article_id) && !empty($attachIDs))
503
		{
504
			bindArticleAttachments($this->_is_aid, $attachIDs);
505
		}
506
507
		// And return to the listing
508
		redirectexit('action=admin;area=portalarticles');
509
	}
510
511
	/**
512
	 * Save attachments based on the form inputs
513
	 *
514
	 * - Remove existing ones that have been "unchecked" in the form
515
	 * - Performs security, size, type, etc checks
516
	 * - Moves files to the current attachment directory, we will move it again to sp attachment in
517
	 * the following steps.
518
	 */
519
	private function processArticleAttachments()
520
	{
521
		global $user_info, $context, $modSettings;
522
523
		// First see if they are trying to delete current attachments.
524
		if (isset($_POST['attach_del']))
525
		{
526
			$keep_temp = array();
527
			$keep_ids = array();
528
			foreach ($_POST['attach_del'] as $idRemove)
529
			{
530
				$attachID = getAttachmentIdFromPublic($idRemove);
531
532
				if (strpos($attachID, 'post_tmp_' . $user_info['id']) !== false)
533
				{
534
					$keep_temp[] = $attachID;
535
				}
536
				else
537
				{
538
					$keep_ids[] = (int) $attachID;
539
				}
540
			}
541
542
			if (isset($_SESSION['temp_attachments']))
543
			{
544
				foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
545
				{
546
					if ((isset($_SESSION['temp_attachments']['post']['files'], $attachment['name'])
547
							&& in_array($attachment['name'], $_SESSION['temp_attachments']['post']['files']))
548
						|| in_array($attachID, $keep_temp)
549
						|| strpos($attachID, 'post_tmp_' . $user_info['id']) === false
550
					)
551
					{
552
						continue;
553
					}
554
555
					unset($_SESSION['temp_attachments'][$attachID]);
556
					@unlink($attachment['tmp_name']);
557
				}
558
			}
559
560
			if (!empty($this->_is_aid))
561
			{
562
				$attachmentQuery = array(
563
					'id_article' => $this->_is_aid,
564
					'not_id_attach' => $keep_ids,
565
					'id_folder' => $modSettings['sp_articles_attachment_dir'],
566
				);
567
				removeArticleAttachments($attachmentQuery);
568
			}
569
		}
570
571
		// Upload any new attachments.
572
		$context['attachments']['can']['post'] = (allowedTo('post_attachment')
573
			|| ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments')));
574
		if ($context['attachments']['can']['post'])
575
		{
576
			list($context['attachments']['quantity'], $context['attachments']['total_size']) = attachmentsSizeForArticle($this->_is_aid);
0 ignored issues
show
Bug introduced by
It seems like $this->_is_aid can also be of type boolean; however, parameter $id_article of attachmentsSizeForArticle() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

576
			list($context['attachments']['quantity'], $context['attachments']['total_size']) = attachmentsSizeForArticle(/** @scrutinizer ignore-type */ $this->_is_aid);
Loading history...
577
			processAttachments();
578
		}
579
	}
580
581
	/**
582
	 * Handle the final processing of attachments, including any thumbnail generation
583
	 * and linking attachments to the specific article.  Saves the articles in the SP
584
	 * attachment directory.
585
	 */
586
	private function finalizeArticleAttachments(&$article_info)
587
	{
588
		global $context, $user_info, $modSettings, $ignore_temp;
589
590
		$attachIDs = array();
591
		if (empty($ignore_temp) && $context['attachments']['can']['post'] && !empty($_SESSION['temp_attachments']))
592
		{
593
			foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
594
			{
595
				if ($attachID !== 'initial_error' && strpos($attachID, 'post_tmp_' . $user_info['id']) === false)
596
				{
597
					continue;
598
				}
599
600
				// If there was an initial error just show that message.
601
				if ($attachID === 'initial_error')
602
				{
603
					unset($_SESSION['temp_attachments']);
604
					break;
605
				}
606
607
				// No errors, then try to create the attachment
608
				if (empty($attachment['errors']))
609
				{
610
					// Load the attachmentOptions array with the data needed to create an attachment
611
					$attachmentOptions = array(
612
						'article' => !empty($this->_is_aid) ? $this->_is_aid : 0,
613
						'poster' => $user_info['id'],
614
						'name' => $attachment['name'],
615
						'tmp_name' => $attachment['tmp_name'],
616
						'size' => $attachment['size'] ?? 0,
617
						'mime_type' => $attachment['type'] ?? '',
618
						'id_folder' => $modSettings['sp_articles_attachment_dir'],
619
						'approved' => true,
620
						'errors' => array(),
621
					);
622
623
					if (createArticleAttachment($attachmentOptions))
624
					{
625
						$attachIDs[] = $attachmentOptions['id'];
626
						if (!empty($attachmentOptions['thumb']))
627
						{
628
							$attachIDs[] = $attachmentOptions['thumb'];
629
						}
630
631
						// Replace ila attach tags with the new valid attachment id and [spattach] tag
632
						$article_info['body'] = preg_replace('~\[attach(.*?)\]' . $attachment['public_attachid'] . '\[\/attach\]~', '[spattach$1]' . $attachmentOptions['id'] . '[/spattach]', $article_info['body']);
633
					}
634
				}
635
				// We have errors on this file, build out the issues for display to the user
636
				else
637
				{
638
					@unlink($attachment['tmp_name']);
639
				}
640
			}
641
642
			unset($_SESSION['temp_attachments']);
643
		}
644
645
		return $attachIDs;
646
	}
647
648
	/**
649
	 * Setup the add/edit article template values
650
	 */
651
	private function prepareArticleForm()
652
	{
653
		global $txt, $context;
654
655
		$context['attachments']['current'] = array();
656
657
		// Just taking a look before you save, or tried to save with errors?
658
		if (!empty($_POST['preview']) || $this->article_errors->hasErrors() || $this->attach_errors->hasErrors())
659
		{
660
			$context['article'] = $this->_sportal_admin_article_preview();
661
662
			// If there are attachment errors. Let's show a list to the user.
663
			if ($this->attach_errors->hasErrors())
664
			{
665
				loadTemplate('Errors');
666
				$errors = $this->attach_errors->prepareErrors();
667
				foreach ($errors as $key => $error)
668
				{
669
					$context['attachment_error_keys'][] = $key . '_error';
670
					$context[$key . '_error'] = $error;
671
				}
672
			}
673
674
			// Showing errors or a preview?
675
			if ($this->article_errors->hasErrors())
676
			{
677
				$context['article_errors'] = array(
678
					'errors' => $this->article_errors->prepareErrors(),
679
					'type' => $this->article_errors->getErrorType() == 0 ? 'minor' : 'serious',
680
					'title' => $txt['sp_form_errors_detected'],
681
				);
682
			}
683
684
			// Preview needs a flag
685
			if (!empty($_POST['preview']))
686
			{
687
				// We reuse this template for the preview
688
				loadTemplate('PortalArticles');
689
				$context['preview'] = true;
690
691
				// The editor will steal focus so we have to delay
692
				addInlineJavascript('setTimeout(() => $("html, body").animate({scrollTop: $("#preview_section").offset().top}, 250), 750);', true);
693
			}
694
		}
695
		// Something new?
696
		elseif (!$this->_is_aid)
697
		{
698
			$context['article'] = array(
699
				'id' => 0,
700
				'article_id' => 'article' . random_int(1, 5000),
701
				'category' => array('id' => 0),
702
				'title' => $txt['sp_articles_default_title'],
703
				'body' => '',
704
				'type' => 'bbc',
705
				'permissions' => 3,
706
				'styles' => 4,
707
				'status' => 1,
708
			);
709
		}
710
		// Something used
711
		else
712
		{
713
			$_REQUEST['article_id'] = $this->_is_aid;
714
			$context['article'] = sportal_get_articles($this->_is_aid);
0 ignored issues
show
Bug introduced by
It seems like $this->_is_aid can also be of type true; however, parameter $article_id of sportal_get_articles() does only seem to accept integer|null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

714
			$context['article'] = sportal_get_articles(/** @scrutinizer ignore-type */ $this->_is_aid);
Loading history...
715
			$attach = sportal_get_articles_attachments($this->_is_aid, true);
0 ignored issues
show
Bug introduced by
It seems like $this->_is_aid can also be of type true; however, parameter $articles of sportal_get_articles_attachments() does only seem to accept integer|integer[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

715
			$attach = sportal_get_articles_attachments(/** @scrutinizer ignore-type */ $this->_is_aid, true);
Loading history...
716
			$context['attachments']['current'] = !empty($attach[$this->_is_aid]) ? $attach[$this->_is_aid] : array();
717
		}
718
	}
719
720
	/**
721
	 * Sets up for an article preview
722
	 */
723
	private function _sportal_admin_article_preview()
724
	{
725
		global $scripturl, $user_info;
726
727
		// Existing article will have some data
728
		if ($this->_is_aid)
729
		{
730
			$_POST['article_id'] = $this->_is_aid;
731
			$current = sportal_get_articles($this->_is_aid);
0 ignored issues
show
Bug introduced by
It seems like $this->_is_aid can also be of type true; however, parameter $article_id of sportal_get_articles() does only seem to accept integer|null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

731
			$current = sportal_get_articles(/** @scrutinizer ignore-type */ $this->_is_aid);
Loading history...
732
			$author = $current['author'];
733
			$date = standardTime($current['date']);
734
			list($views, $comments) = sportal_get_article_views_comments($this->_is_aid);
0 ignored issues
show
Bug introduced by
It seems like $this->_is_aid can also be of type true; however, parameter $id of sportal_get_article_views_comments() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

734
			list($views, $comments) = sportal_get_article_views_comments(/** @scrutinizer ignore-type */ $this->_is_aid);
Loading history...
735
		}
736
		// New ones we set defaults
737
		else
738
		{
739
			$author = array('link' => '<a href="' . $scripturl . '?action=profile;u=' . $user_info['id'] . '">' . $user_info['name'] . '</a>');
740
			$date = standardTime(time());
741
			$views = 0;
742
			$comments = 0;
743
		}
744
745
		$article = array(
746
			'id' => $_POST['article_id'],
747
			'article_id' => $_POST['namespace'],
748
			'category' => sportal_get_categories((int) $_POST['category_id']),
749
			'author' => $author,
750
			'title' => Util::htmlspecialchars($_POST['title'], ENT_QUOTES),
751
			'body' => Util::htmlspecialchars($_POST['content'], ENT_QUOTES),
752
			'type' => $_POST['type'],
753
			'permissions' => $_POST['permissions'],
754
			'styles' => $_POST['styles'],
755
			'date' => $date,
756
			'status' => !empty($_POST['status']),
757
			'view_count' => $views,
758
			'comment_count' => $comments,
759
		);
760
761
		if ($article['type'] === 'bbc')
762
		{
763
			PreparseCode::instance()->preparsecode($article['body'], false);
764
		}
765
766
		return $article;
767
	}
768
769
	/**
770
	 * Sets up editor options as needed for SP, temporarily ignores any user options
771
	 * so we can enable it in the proper mode bbc/php/html
772
	 */
773
	private function prepareEditor()
774
	{
775
		global $context, $options;
776
777
		if ($context['article']['type'] !== 'bbc')
778
		{
779
			// Override user preferences for wizzy mode if they don't need it
780
			$temp_editor = !empty($options['wysiwyg_default']);
781
			$options['wysiwyg_default'] = false;
782
		}
783
784
		// Fire up the editor with the values
785
		$editorOptions = array(
786
			'id' => 'content',
787
			'value' => $context['article']['body'],
788
			'width' => '100%',
789
			'height' => '275px',
790
			'preview_type' => 1,
791
		);
792
		$editorOptions['plugin_addons'] = array();
793
		$editorOptions['plugin_addons'][] = 'spplugin';
794
		create_control_richedit($editorOptions);
795
		$context['post_box_name'] = $editorOptions['id'];
796
		$context['post_box_class'] = $context['article']['type'] !== 'bbc' ? 'sceditor-container' : 'sp-sceditor-container';
797
		$context['attached'] = '';
798
799
		// Restore their settings
800
		if (isset($temp_editor))
801
		{
802
			$options['wysiwyg_default'] = $temp_editor;
803
		}
804
	}
805
806
	/**
807
	 * Loads in permission, visibility and style profiles for use in the template
808
	 * If unable to load profiles, simply dies as "somethings broke"tm
809
	 */
810
	private function loadProfileContext()
811
	{
812
		global $context;
813
814
		$context['article']['permission_profiles'] = sportal_get_profiles(null, 1, 'name');
815
		if (empty($context['article']['permission_profiles']))
816
		{
817
			throw new Elk_Exception('error_sp_no_permission_profiles', false);
818
		}
819
820
		$context['article']['style_profiles'] = sportal_get_profiles(null, 2, 'name');
821
		if (empty($context['article']['permission_profiles']))
822
		{
823
			throw new Elk_Exception('error_sp_no_style_profiles', false);
824
		}
825
826
		$context['article']['categories'] = sportal_get_categories();
827
		if (empty($context['article']['categories']))
828
		{
829
			throw new Elk_Exception('error_sp_no_category', false);
830
		}
831
	}
832
833
	/**
834
	 * Handles the checking of uploaded attachments and loads valid ones
835
	 * into context
836
	 */
837
	private function article_attachment()
838
	{
839
		global $context, $txt;
840
841
		// If there are attachments, calculate the total size and how many.
842
		$this->_attachments = array();
843
		$this->_attachments['total_size'] = 0;
844
		$this->_attachments['quantity'] = 0;
845
846
		// If this isn't a new article, account for any current attachments.
847
		if ($this->_is_aid && !empty($context['attachments']))
848
		{
849
			$this->_attachments['quantity'] = count($context['attachments']['current']);
850
			foreach ($context['attachments']['current'] as $attachment)
851
			{
852
				$this->_attachments['total_size'] += $attachment['size'];
853
			}
854
		}
855
856
		// Any failed/aborted attachments left in session that we should clear
857
		if (!empty($_SESSION['temp_attachments']) && empty($_POST['preview']) && empty($_POST['submit']) && !$this->_is_aid)
858
		{
859
			foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
860
			{
861
				unset($_SESSION['temp_attachments'][$attachID]);
862
				@unlink($attachment['tmp_name']);
863
			}
864
		}
865
		// New attachments to add
866
		elseif (!empty($_SESSION['temp_attachments']))
867
		{
868
			foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
869
			{
870
				// PHP upload error means we drop the file
871
				if ($attachID === 'initial_error')
872
				{
873
					$txt['error_attach_initial_error'] = $txt['attach_no_upload'] . '<div class="attachmenterrors">' . (is_array($attachment) ? vsprintf($txt[$attachment[0]], $attachment[1]) : $txt[$attachment]) . '</div>';
874
					$this->attach_errors->addError('attach_initial_error');
875
					unset($_SESSION['temp_attachments']);
876
					break;
877
				}
878
879
				// Show any errors which might have occurred.
880
				if (!empty($attachment['errors']))
881
				{
882
					$txt['error_attach_errors'] = empty($txt['error_attach_errors']) ? '<br />' : '';
883
					$txt['error_attach_errors'] .= vsprintf($txt['attach_warning'], $attachment['name']) . '<div class="attachmenterrors">';
884
					foreach ($attachment['errors'] as $error)
885
					{
886
						$txt['error_attach_errors'] .= (is_array($error) ? vsprintf($txt[$error[0]], $error[1]) : $txt[$error]) . '<br  />';
887
					}
888
					$txt['error_attach_errors'] .= '</div>';
889
890
					$this->attach_errors->addError('attach_errors');
891
892
					// Take out the trash.
893
					unset($_SESSION['temp_attachments'][$attachID]);
894
					@unlink($attachment['tmp_name']);
895
896
					continue;
897
				}
898
899
				// In session but the file is missing, then some house cleaning
900
				if (isset($attachment['tmp_name']) && !file_exists($attachment['tmp_name']))
901
				{
902
					unset($_SESSION['temp_attachments'][$attachID]);
903
					continue;
904
				}
905
906
				$this->_attachments['name'] = !empty($this->_attachments['name']) ? $this->_attachments['name'] : '';
907
				$this->_attachments['size'] = !empty($this->_attachments['size']) ? $this->_attachments['size'] : 0;
908
				$this->_attachments['quantity']++;
909
				$this->_attachments['total_size'] += $this->_attachments['size'];
910
911
				$context['attachments']['current'][] = array(
912
					'name' => '<span class="underline">' . htmlspecialchars($this->_attachments['name'], ENT_COMPAT) . '</span>',
913
					'size' => $this->_attachments['size'],
914
					'id' => $attachID,
915
					'unchecked' => false,
916
					'approved' => 1,
917
				);
918
			}
919
		}
920
	}
921
922
	/**
923
	 * Prepares template values for the attachment area such as space left,
924
	 * types allowed, etc.
925
	 *
926
	 * Prepares the D&D JS initialization values
927
	 */
928
	private function article_attachment_dd()
929
	{
930
		global $context, $modSettings, $txt;
931
932
		// If they've unchecked an attachment, they may still want to attach that many more files, but don't allow more than num_allowed_attachments.
933
		$context['attachments']['num_allowed'] = empty($modSettings['attachmentNumPerPostLimit']) ? 50 : min($modSettings['attachmentNumPerPostLimit'] - count($context['attachments']['current']), $modSettings['attachmentNumPerPostLimit']);
934
		$context['attachments']['can']['post_unapproved'] = allowedTo('post_attachment');
935
		$context['attachments']['restrictions'] = array();
936
		$context['attachments']['ila_enabled'] = true;
937
938
		if (!empty($modSettings['attachmentCheckExtensions']))
939
		{
940
			$context['attachments']['allowed_extensions'] = strtr(strtolower($modSettings['attachmentExtensions']), array(',' => ', '));
941
		}
942
		else
943
		{
944
			$context['attachments']['allowed_extensions'] = '';
945
		}
946
947
		$context['attachments']['templates'] = array(
948
			'existing' => 'template_article_existing_attachments',
949
			'add_new' => 'template_article_new_attachments',
950
		);
951
952
		$attachmentRestrictionTypes = array('attachmentNumPerPostLimit', 'attachmentPostLimit', 'attachmentSizeLimit');
953
		foreach ($attachmentRestrictionTypes as $type)
954
		{
955
			if (!empty($modSettings[$type]))
956
			{
957
				$context['attachments']['restrictions'][] = sprintf($txt['attach_restrict_' . $type], comma_format($modSettings[$type], 0));
958
959
				// Show some numbers. If they exist.
960
				if ($type === 'attachmentNumPerPostLimit' && $this->_attachments['quantity'] > 0)
961
				{
962
					$context['attachments']['restrictions'][] = sprintf($txt['attach_remaining'], $modSettings['attachmentNumPerPostLimit'] - $this->_attachments['quantity']);
963
				}
964
				elseif ($type === 'attachmentPostLimit' && $this->_attachments['total_size'] > 0)
965
				{
966
					$context['attachments']['restrictions'][] = sprintf($txt['attach_available'], comma_format(round(max($modSettings['attachmentPostLimit'] - ($this->_attachments['total_size'] / 1028), 0)), 0));
967
				}
968
			}
969
		}
970
971
		// The portal articles always allow "ila" style attachments, so show that insert interface
972
		addInlineJavascript('
973
		let IlaDropEvents = {
974
			UploadSuccess: function($button, data) {
975
				let inlineAttach = ElkInlineAttachments(\'#postAttachment2,#postAttachment\', \'' . $context['post_box_name'] . '\', {
976
					trigger: $(\'<div class="share icon i-share" />\'),
977
					template: ' . JavaScriptEscape('<div class="insertoverlay">
978
						<input type="button" class="button" value="' . $txt['insert'] . '">
979
						<ul data-group="tabs" class="tabs">
980
							<li data-tab="size">' . $txt['ila_opt_size'] . '</li><li data-tab="align">' . $txt['ila_opt_align'] . '</li>
981
						</ul>
982
						<div class="container" data-visual="size">
983
							<label><input data-size="thumb" type="radio" name="imgmode">' . $txt['ila_opt_size_thumb'] . '</label>
984
							<label><input data-size="full" type="radio" name="imgmode">' . $txt['ila_opt_size_full'] . '</label>
985
							<label><input data-size="cust" type="radio" name="imgmode">' . $txt['ila_opt_size_cust'] . '</label>
986
							<div class="customsize">
987
								<input type="range" class="range" min="100" max="500"><input type="text" class="visualizesize" disabled="disabled">
988
							</div>
989
						</div>
990
						<div class="container" data-visual="align">
991
							<label><input data-align="none" type="radio" name="align">' . $txt['ila_opt_align_none'] . '</label>
992
							<label><input data-align="left" type="radio" name="align">' . $txt['ila_opt_align_left'] . '</label>
993
							<label><input data-align="center" type="radio" name="align">' . $txt['ila_opt_align_center'] . '</label>
994
							<label><input data-align="right" type="radio" name="align">' . $txt['ila_opt_align_right'] . '</label>
995
						</div>
996
					</div>') . '
997
				});
998
				inlineAttach.addInterface($button, data.attachid);
999
			},
1000
			RemoveSuccess: function(attachid) {
1001
				var inlineAttach = ElkInlineAttachments(\'#postAttachment2,#postAttachment\', \'' . $context['post_box_name'] . '\', {
1002
					trigger: $(\'<div class="share icon i-share" />\')
1003
				});
1004
				inlineAttach.removeAttach(attachid);
1005
			}
1006
		};', true);
1007
1008
		// Load up the drag and drop attachment magic
1009
		addInlineJavascript('
1010
		var dropAttach = dragDropAttachment({
1011
			board: 0,
1012
			allowedExtensions: ' . JavaScriptEscape($context['attachments']['allowed_extensions']) . ',
1013
			totalSizeAllowed: ' . JavaScriptEscape(empty($modSettings['attachmentPostLimit']) ? '' : $modSettings['attachmentPostLimit']) . ',
1014
			individualSizeAllowed: ' . JavaScriptEscape(empty($modSettings['attachmentSizeLimit']) ? '' : $modSettings['attachmentSizeLimit']) . ',
1015
			numOfAttachmentAllowed: ' . $context['attachments']['num_allowed'] . ',
1016
			totalAttachSizeUploaded: ' . (isset($context['attachments']['total_size']) && !empty($context['attachments']['total_size']) ? $context['attachments']['total_size'] : 0) . ',
1017
			numAttachUploaded: ' . (isset($context['attachments']['quantity']) && !empty($context['attachments']['quantity']) ? $context['attachments']['quantity'] : 0) . ',
1018
			fileDisplayTemplate: \'<div class="statusbar"><div class="info"></div><div class="progressBar"><div></div></div><div class="control icon i-close"></div></div>\',
1019
			oTxt: ({
1020
				allowedExtensions : ' . JavaScriptEscape(sprintf($txt['cant_upload_type'], $context['attachments']['allowed_extensions'])) . ',
1021
				totalSizeAllowed : ' . JavaScriptEscape($txt['attach_max_total_file_size']) . ',
1022
				individualSizeAllowed : ' . JavaScriptEscape(sprintf($txt['file_too_big'], comma_format($modSettings['attachmentSizeLimit'], 0))) . ',
1023
				numOfAttachmentAllowed : ' . JavaScriptEscape(sprintf($txt['attachments_limit_per_post'], $modSettings['attachmentNumPerPostLimit'])) . ',
1024
				postUploadError : ' . JavaScriptEscape($txt['post_upload_error']) . ',
1025
				areYouSure: ' . JavaScriptEscape($txt['ila_confirm_removal']) . ',
1026
			}),
1027
			existingSelector: ".inline_insert",
1028
			events: IlaDropEvents' . (!empty($this->_is_aid) ? ',
1029
			article: ' . $this->_is_aid : '') . '
1030
		});', true);
1031
	}
1032
1033
	/**
1034
	 * Toggle an articles active status on/off
1035
	 */
1036
	public function action_status()
1037
	{
1038
		global $context;
1039
1040
		checkSession(isset($_REQUEST['xml']) ? '' : 'get');
1041
1042
		$article_id = !empty($_REQUEST['article_id']) ? (int) $_REQUEST['article_id'] : 0;
1043
		$state = sp_changeState('article', $article_id);
1044
1045
		// Doing this the ajax way?
1046
		if (isset($_REQUEST['xml']))
1047
		{
1048
			$context['item_id'] = $article_id;
1049
			$context['status'] = !empty($state) ? 'active' : 'deactive';
1050
1051
			// Clear out any template layers, add the xml response
1052
			loadTemplate('PortalAdmin');
1053
			$template_layers = Template_Layers::instance();
1054
			$template_layers->removeAll();
1055
			$context['sub_template'] = 'change_status';
1056
1057
			obExit();
1058
		}
1059
1060
		redirectexit('action=admin;area=portalarticles');
1061
	}
1062
1063
	/**
1064
	 * Remove an article from the system
1065
	 *
1066
	 * - Removes the article
1067
	 * - Removes attachments associated with an article
1068
	 * - Updates category totals to reflect removed items
1069
	 */
1070
	public function action_delete()
1071
	{
1072
		$article_ids = array();
1073
1074
		// Get the article id's to remove
1075
		if (!empty($_POST['remove_articles']) && !empty($_POST['remove']) && is_array($_POST['remove']))
1076
		{
1077
			checkSession();
1078
1079
			foreach ($_POST['remove'] as $index => $article_id)
1080
			{
1081
				$article_ids[(int) $index] = (int) $article_id;
1082
			}
1083
		}
1084
		elseif (!empty($_REQUEST['article_id']))
1085
		{
1086
			checkSession('get');
1087
			$article_ids[] = (int) $_REQUEST['article_id'];
1088
		}
1089
1090
		// If we have some to remove ....
1091
		if (!empty($article_ids))
1092
		{
1093
			// Update the category counts as we are about to remove some articles
1094
			foreach ($article_ids as $article_id)
0 ignored issues
show
Comprehensibility Bug introduced by
$article_id is overwriting a variable from outer foreach loop.
Loading history...
1095
			{
1096
				$article_info = sportal_get_articles($article_id);
1097
				sp_category_update_total($article_info['category']['id']);
1098
			}
1099
1100
			sp_delete_articles($article_ids);
1101
		}
1102
1103
		redirectexit('action=admin;area=portalarticles');
1104
	}
1105
}
1106