Passed
Push — myDevel ( 6b67fa...0991c9 )
by Spuds
04:10
created

ManagePortalArticles_Controller   F

Complexity

Total Complexity 132

Size/Duplication

Total Lines 1084
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 513
dl 0
loc 1084
rs 2
c 8
b 0
f 0
wmc 132

18 Methods

Rating   Name   Duplication   Size   Complexity  
A list_spCountArticles() 0 3 1
A pre_dispatch() 0 6 1
A action_index() 0 41 2
B action_edit() 0 80 10
A list_spLoadArticles() 0 3 1
A editInit() 0 20 4
B action_list() 0 162 1
B action_delete() 0 34 8
A loadProfileContext() 0 20 4
B prepareArticleForm() 0 63 11
A action_status() 0 25 5
A _sportal_admin_article_preview() 0 44 3
C article_attachment_dd() 0 103 16
A prepareEditor() 0 27 3
C processArticleAttachments() 0 59 14
D article_attachment() 0 80 21
C finalizeArticleAttachments() 0 60 12
F _sportal_admin_article_edit_save() 0 111 15

How to fix   Complexity   

Complex Class

Complex classes like ManagePortalArticles_Controller often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ManagePortalArticles_Controller, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @package SimplePortal ElkArte
5
 *
6
 * @author SimplePortal Team
7
 * @copyright 2015-2017 SimplePortal Team
8
 * @license BSD 3-clause
9
 * @version 1.0.0 RC1
10
 */
11
12
use ElkArte\Errors\AttachmentErrorContext;
13
use ElkArte\Errors\ErrorContext;
14
15
/**
16
 * SimplePortal Article Administration controller class.
17
 * This class handles the adding/editing/listing of articles
18
 */
19
class ManagePortalArticles_Controller extends Action_Controller
20
{
21
	/** @var bool|int hold the article id if existing */
22
	protected $_is_aid;
23
24
	/** @var array */
25
	protected $_attachments;
26
27
	/** @var ErrorContext */
28
	protected $article_errors;
29
30
	/** @var AttachmentErrorContext */
31
	protected $attach_errors;
32
33
	/**
34
	 * This method is executed before any action handler.
35
	 * Loads common things for all methods
36
	 */
37
	public function pre_dispatch()
38
	{
39
		// We'll need the utility functions from here.
40
		require_once(SUBSDIR . '/PortalAdmin.subs.php');
41
		require_once(SUBSDIR . '/Portal.subs.php');
42
		require_once(SUBSDIR . '/PortalArticle.subs.php');
43
	}
44
45
	/**
46
	 * Main article dispatcher.
47
	 *
48
	 * This function checks permissions and passes control through.
49
	 */
50
	public function action_index()
51
	{
52
		global $context, $txt;
53
54
		// You need to be an admin or have manage article permissions
55
		if (!allowedTo('sp_admin'))
56
		{
57
			isAllowedTo('sp_manage_articles');
58
		}
59
60
		loadTemplate('PortalAdminArticles');
61
62
		// These are all the actions that we know
63
		$subActions = array(
64
			'list' => array($this, 'action_list'),
65
			'add' => array($this, 'action_edit'),
66
			'edit' => array($this, 'action_edit'),
67
			'status' => array($this, 'action_status'),
68
			'delete' => array($this, 'action_delete'),
69
		);
70
71
		// Start up the controller, provide a hook since we can
72
		$action = new Action('portal_articles');
73
74
		// Set up the tab data
75
		$context[$context['admin_menu_name']]['tab_data'] = array(
76
			'title' => $txt['sp_admin_articles_title'],
77
			'help' => 'sp_ArticlesArea',
78
			'description' => $txt['sp_admin_articles_desc'],
79
			'tabs' => array(
80
				'list' => array(),
81
				'add' => array(),
82
			),
83
		);
84
85
		// By default we want to list articles
86
		$subAction = $action->initialize($subActions, 'list');
87
		$context['sub_action'] = $subAction;
88
89
		// Call the right function for this sub-action
90
		$action->dispatch($subAction);
91
	}
92
93
	/**
94
	 * Show a listing of articles in the system
95
	 */
96
	public function action_list()
97
	{
98
		global $context, $scripturl, $txt, $modSettings;
99
100
		// Build the listoption array to display the categories
101
		$listOptions = array(
102
			'id' => 'portal_articles',
103
			'title' => $txt['sp_admin_articles_list'],
104
			'items_per_page' => $modSettings['defaultMaxMessages'],
105
			'no_items_label' => $txt['error_sp_no_articles'],
106
			'base_href' => $scripturl . '?action=admin;area=portalarticles;sa=list;',
107
			'default_sort_col' => 'title',
108
			'get_items' => array(
109
				'function' => array($this, 'list_spLoadArticles'),
110
			),
111
			'get_count' => array(
112
				'function' => array($this, 'list_spCountArticles'),
113
			),
114
			'columns' => array(
115
				'title' => array(
116
					'header' => array(
117
						'value' => $txt['sp_admin_articles_col_title'],
118
					),
119
					'data' => array(
120
						'db' => 'title',
121
					),
122
					'sort' => array(
123
						'default' => 'title',
124
						'reverse' => 'title DESC',
125
					),
126
				),
127
				'namespace' => array(
128
					'header' => array(
129
						'value' => $txt['sp_admin_articles_col_namespace'],
130
					),
131
					'data' => array(
132
						'db' => 'article_id',
133
					),
134
					'sort' => array(
135
						'default' => 'article_namespace',
136
						'reverse' => 'article_namespace DESC',
137
					),
138
				),
139
				'category' => array(
140
					'header' => array(
141
						'value' => $txt['sp_admin_articles_col_category'],
142
					),
143
					'data' => array(
144
						'db' => 'category_name',
145
					),
146
					'sort' => array(
147
						'default' => 'name',
148
						'reverse' => 'name DESC',
149
					),
150
				),
151
				'author' => array(
152
					'header' => array(
153
						'value' => $txt['sp_admin_articles_col_author'],
154
					),
155
					'data' => array(
156
						'db' => 'author_name',
157
					),
158
					'sort' => array(
159
						'default' => 'author_name',
160
						'reverse' => 'author_name DESC',
161
					),
162
				),
163
				'type' => array(
164
					'header' => array(
165
						'value' => $txt['sp_admin_articles_col_type'],
166
					),
167
					'data' => array(
168
						'db' => 'type',
169
					),
170
					'sort' => array(
171
						'default' => 'type',
172
						'reverse' => 'type DESC',
173
					),
174
				),
175
				'date' => array(
176
					'header' => array(
177
						'value' => $txt['sp_admin_articles_col_date'],
178
					),
179
					'data' => array(
180
						'db' => 'date',
181
					),
182
					'sort' => array(
183
						'default' => 'date',
184
						'reverse' => 'date DESC',
185
					),
186
				),
187
				'status' => array(
188
					'header' => array(
189
						'value' => $txt['sp_admin_articles_col_status'],
190
						'class' => 'centertext',
191
					),
192
					'data' => array(
193
						'db' => 'status_image',
194
						'class' => 'centertext',
195
					),
196
					'sort' => array(
197
						'default' => 'status',
198
						'reverse' => 'status DESC',
199
					),
200
				),
201
				'action' => array(
202
					'header' => array(
203
						'value' => $txt['sp_admin_articles_col_actions'],
204
						'class' => 'centertext',
205
					),
206
					'data' => array(
207
						'sprintf' => array(
208
							'format' => '
209
								<a href="?action=admin;area=portalarticles;sa=edit;article_id=%1$s;' . $context['session_var'] . '=' . $context['session_id'] . '" accesskey="e">' . sp_embed_image('modify') . '</a>&nbsp;
210
								<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('delete') . '</a>',
211
							'params' => array(
212
								'id' => true,
213
							),
214
						),
215
						'class' => 'centertext nowrap',
216
					),
217
				),
218
				'check' => array(
219
					'header' => array(
220
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
221
						'class' => 'centertext',
222
					),
223
					'data' => array(
224
						'function' => function ($row)
225
						{
226
							return '<input type="checkbox" name="remove[]" value="' . $row['id'] . '" class="input_check" />';
227
						},
228
						'class' => 'centertext',
229
					),
230
				),
231
			),
232
			'form' => array(
233
				'href' => $scripturl . '?action=admin;area=portalarticles;sa=delete',
234
				'include_sort' => true,
235
				'include_start' => true,
236
				'hidden_fields' => array(
237
					$context['session_var'] => $context['session_id'],
238
				),
239
			),
240
			'additional_rows' => array(
241
				array(
242
					'class' => 'submitbutton',
243
					'position' => 'below_table_data',
244
					'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>
245
						<input type="submit" name="remove_articles" value="' . $txt['sp_admin_articles_remove'] . '" />',
246
				),
247
			),
248
		);
249
250
		// Set the context values
251
		$context['page_title'] = $txt['sp_admin_articles_title'];
252
		$context['sub_template'] = 'show_list';
253
		$context['default_list'] = 'portal_articles';
254
255
		// Create the list.
256
		require_once(SUBSDIR . '/GenericList.class.php');
257
		createList($listOptions);
258
	}
259
260
	/**
261
	 * Callback for createList(),
262
	 * Returns the number of articles in the system
263
	 */
264
	public function list_spCountArticles()
265
	{
266
		return sp_count_articles();
267
	}
268
269
	/**
270
	 * Callback for createList()
271
	 * Returns an array of articles
272
	 *
273
	 * @param int $start
274
	 * @param int $items_per_page
275
	 * @param string $sort
276
	 *
277
	 * @return array
278
	 */
279
	public function list_spLoadArticles($start, $items_per_page, $sort)
280
	{
281
		return sp_load_articles($start, $items_per_page, $sort);
282
	}
283
284
	/**
285
	 * Edits an existing OR adds a new article to the system
286
	 *
287
	 * - Handles the previewing of an article
288
	 * - Handles article attachments
289
	 */
290
	public function action_edit()
291
	{
292
		global $context, $txt;
293
294
		// Load dependency's, prepare error checking
295
		$this->editInit();
296
297
		// Started with HTML editor and now converting to BBC?
298
		if (!empty($_REQUEST['content_mode']) && $_POST['type'] === 'bbc')
299
		{
300
			require_once(SUBSDIR . '/Html2BBC.class.php');
301
			$convert = $_REQUEST['content'];
302
			$bbc_converter = new Html_2_BBC($convert);
303
			$convert = $bbc_converter->get_bbc();
304
			$convert = un_htmlspecialchars($convert);
305
			$_POST['content'] = $convert;
306
		}
307
308
		// Want to save the work?
309
		if (!empty($_POST['submit']) && !$this->article_errors->hasErrors() && !$this->attach_errors->hasErrors())
310
		{
311
			// If the session has timed out, let the user re-submit their form.
312
			if (checkSession('post', '', false) !== '')
313
			{
314
				$this->article_errors->addError('session_timeout');
315
316
				// Disable the preview and the save
317
				unset($_POST['preview'], $_POST['submit']);
318
319
				return $this->action_edit();
320
			}
321
322
			// Check for errors and if none Save it
323
			$this->_sportal_admin_article_edit_save();
324
		}
325
326
		// Prepare the form fields, preview, errors, etc
327
		$this->prepareArticleForm();
328
329
		// On to the editor
330
		if ($context['article']['type'] === 'bbc')
331
		{
332
			$context['article']['body'] = str_replace(array('"', '<', '>', '&nbsp;'), array('&quot;', '&lt;', '&gt;', ' '), un_preparsecode($context['article']['body']));
333
		}
334
335
		$this->prepareEditor();
336
337
		// Final bits for the template, category's, styles and permission profiles
338
		$this->loadProfileContext();
339
340
		// Attachments
341
		if ($context['attachments']['can']['post'])
342
		{
343
			$this->article_attachment();
344
			$this->article_attachment_dd();
345
		}
346
347
		// Set the editor to the right mode based on type (bbc, html, php)
348
		addInlineJavascript('
349
			$(function() {
350
				diewithfire = window.setTimeout(function() {sp_update_editor("' . $context['article']['type'] . '", "");}, 200);
351
			});
352
		');
353
354
		// Finally the main template
355
		loadTemplate('PortalAdminArticles');
356
		$context['sub_template'] = 'articles';
357
358
		// The article above/below template
359
		$template_layers = Template_Layers::instance();
360
		$template_layers->add('articles_edit');
361
362
		// Page out values
363
		$context[$context['admin_menu_name']]['current_subsection'] = 'add';
364
		$context['article']['style'] = sportal_select_style($context['article']['styles']);
365
		$context['is_new'] = $this->_is_aid;
366
		$context['article']['body'] = sportal_parse_content($context['article']['body'], $context['article']['type'], 'return');
367
		$context['page_title'] = !$this->_is_aid ? $txt['sp_admin_articles_add'] : $txt['sp_admin_articles_edit'];
368
369
		return true;
370
	}
371
372
	/**
373
	 * Loads in dependency's for saving or editing an article
374
	 */
375
	private function editInit()
376
	{
377
		global $modSettings, $context;
378
379
		$this->_is_aid = empty($_REQUEST['article_id']) ? false : (int) $_REQUEST['article_id'];
380
381
		// Going to use editor, attachment and post functions
382
		require_once(SUBSDIR . '/Post.subs.php');
383
		require_once(SUBSDIR . '/Editor.subs.php');
384
		require_once(SUBSDIR . '/Attachments.subs.php');
385
386
		loadLanguage('Post');
387
		loadLanguage('Errors');
388
389
		// Errors are likely
390
		$this->article_errors = ErrorContext::context('article', 0);
391
		$this->attach_errors = AttachmentErrorContext::context();
392
		$this->attach_errors->activate();
393
394
		$context['attachments']['can']['post'] = !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1 && (allowedTo('post_attachment'));
395
	}
396
397
	/**
398
	 * Does the actual saving of the article data
399
	 *
400
	 * - Validates the data is safe to save
401
	 * - Updates existing articles or creates new ones
402
	 */
403
	private function _sportal_admin_article_edit_save()
404
	{
405
		global $context, $txt, $modSettings;
406
407
		// Use our standard validation functions in a few spots
408
		require_once(SUBSDIR . '/DataValidator.class.php');
409
		$validator = new Data_Validator();
410
411
		// If it exists, load the current data
412
		if ($this->_is_aid)
413
		{
414
			$_POST['article_id'] = $this->_is_aid;
415
			$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

415
			$context['article'] = sportal_get_articles(/** @scrutinizer ignore-type */ $this->_is_aid);
Loading history...
416
		}
417
418
		// Clean and Review the post data for compliance
419
		$validator->sanitation_rules(array(
420
			'title' => 'trim|Util::htmlspecialchars',
421
			'namespace' => 'trim|Util::htmlspecialchars',
422
			'article_id' => 'intval',
423
			'category_id' => 'intval',
424
			'permissions' => 'intval',
425
			'styles' => 'intval',
426
			'type' => 'trim',
427
			'content' => 'trim'
428
		));
429
		$validator->validation_rules(array(
430
			'title' => 'required',
431
			'namespace' => 'alpha_numeric|required',
432
			'type' => 'required',
433
			'content' => 'required'
434
		));
435
		$validator->text_replacements(array(
436
			'title' => $txt['sp_admin_articles_col_title'],
437
			'namespace' => $txt['sp_admin_articles_col_namespace'],
438
			'content' => $txt['sp_admin_articles_col_body']
439
		));
440
441
		// If you messed this up, tell them why
442
		if (!$validator->validate($_POST))
443
		{
444
			foreach ($validator->validation_errors() as $id => $error)
445
			{
446
				$this->article_errors->addError($error);
447
			}
448
		}
449
450
		// Lets make sure this namespace (article id) is unique
451
		$has_duplicate = sp_duplicate_articles($validator->article_id, $validator->namespace);
452
		if (!empty($has_duplicate))
453
		{
454
			$this->article_errors->addError('sp_error_article_namespace_duplicate');
455
		}
456
457
		// And we can't have just a numeric namespace (article id)
458
		if (preg_replace('~[0-9]+~', '', $validator->namespace) === '')
459
		{
460
			$this->article_errors->addError('sp_error_article_namespace_numeric');
461
		}
462
463
		// Posting some PHP code, and allowed? Then we need to validate it will run
464
		if ($_POST['type'] === 'php' && !empty($_POST['content']) && empty($modSettings['sp_disable_php_validation']))
465
		{
466
			$validator_php = new Data_Validator();
467
			$validator_php->validation_rules(array('content' => 'php_syntax'));
468
469
			// Bad PHP code
470
			if (!$validator_php->validate(array('content' => $_POST['content'])))
471
			{
472
				$this->article_errors->addError($validator_php->validation_errors());
473
			}
474
		}
475
476
		// Check / Prep attachments
477
		$this->processArticleAttachments();
478
479
		// None shall pass ... with errors
480
		if ($this->article_errors->hasErrors() || $this->attach_errors->hasErrors())
481
		{
482
			unset($_POST['submit']);
483
484
			return false;
485
		}
486
487
		// No errors then, prepare the data for saving
488
		$article_info = array(
489
			'id' => $validator->article_id,
490
			'id_category' => $validator->category_id,
491
			'namespace' => $validator->namespace,
492
			'title' => $validator->title,
493
			'body' => Util::htmlspecialchars($_POST['content'], ENT_QUOTES),
494
			'type' => in_array($validator->type, array('bbc', 'html', 'php')) ? $_POST['type'] : 'bbc',
495
			'permissions' => $validator->permissions,
496
			'styles' => $validator->styles,
497
			'status' => !empty($_POST['status']) ? 1 : 0,
498
		);
499
500
		if ($article_info['type'] === 'bbc')
501
		{
502
			preparsecode($article_info['body']);
503
		}
504
505
		// Bind attachments to the article, create any needed thumbnails, move to sp attachment directory
506
		$this->finalizeArticleAttachments($article_info);
507
508
		// Save away
509
		checkSession();
510
		$this->_is_aid = sp_save_article($article_info, empty($this->_is_aid));
511
512
		// And return to the listing
513
		redirectexit('action=admin;area=portalarticles');
514
	}
515
516
	/**
517
	 * Save attachments based on the form inputs
518
	 *
519
	 * - Remove existing ones that have been "unchecked" in the form
520
	 * - Performs security, size, type, etc checks
521
	 * - Moves files to the current attachment directory, we will move it again to sp attachment in
522
	 * the following steps.
523
	 */
524
	private function processArticleAttachments()
525
	{
526
		global $user_info, $context, $modSettings;
527
528
		// First see if they are trying to delete current attachments.
529
		if (isset($_POST['attach_del']))
530
		{
531
			$keep_temp = array();
532
			$keep_ids = array();
533
			foreach ($_POST['attach_del'] as $idRemove)
534
			{
535
				$attachID = getAttachmentIdFromPublic($idRemove);
536
537
				if (strpos($attachID, 'post_tmp_' . $user_info['id']) !== false)
538
				{
539
					$keep_temp[] = $attachID;
540
				}
541
				else
542
				{
543
					$keep_ids[] = (int) $attachID;
544
				}
545
			}
546
547
			if (isset($_SESSION['temp_attachments']))
548
			{
549
				foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
550
				{
551
					if ((isset($_SESSION['temp_attachments']['post']['files'], $attachment['name'])
552
							&& in_array($attachment['name'], $_SESSION['temp_attachments']['post']['files']))
553
						|| in_array($attachID, $keep_temp)
554
						|| strpos($attachID, 'post_tmp_' . $user_info['id']) === false
555
					)
556
					{
557
						continue;
558
					}
559
560
					unset($_SESSION['temp_attachments'][$attachID]);
561
					@unlink($attachment['tmp_name']);
562
				}
563
			}
564
565
			if (!empty($this->_is_aid))
566
			{
567
				$attachmentQuery = array(
568
					'id_article' => $this->_is_aid,
569
					'not_id_attach' => $keep_ids,
570
					'id_folder' => $modSettings['sp_articles_attachment_dir'],
571
				);
572
				removeArticleAttachments($attachmentQuery);
573
			}
574
		}
575
576
		// Upload any new attachments.
577
		$context['attachments']['can']['post'] = (allowedTo('post_attachment')
578
			|| ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments')));
579
		if ($context['attachments']['can']['post'])
580
		{
581
			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

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

716
			$context['article'] = sportal_get_articles(/** @scrutinizer ignore-type */ $this->_is_aid);
Loading history...
717
			$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

717
			$attach = sportal_get_articles_attachments(/** @scrutinizer ignore-type */ $this->_is_aid, true);
Loading history...
718
			$context['attachments']['current'] = !empty($attach[$this->_is_aid]) ? $attach[$this->_is_aid] : array();
719
		}
720
	}
721
722
	/**
723
	 * Sets up for an article preview
724
	 */
725
	private function _sportal_admin_article_preview()
726
	{
727
		global $scripturl, $user_info;
728
729
		// Existing article will have some data
730
		if ($this->_is_aid)
731
		{
732
			$_POST['article_id'] = $this->_is_aid;
733
			$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

733
			$current = sportal_get_articles(/** @scrutinizer ignore-type */ $this->_is_aid);
Loading history...
734
			$author = $current['author'];
735
			$date = standardTime($current['date']);
736
			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

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