ManageNews::action_mailingsend()   F
last analyzed

Complexity

Conditions 66
Paths > 20000

Size

Total Lines 386
Code Lines 182

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 4422

Importance

Changes 0
Metric Value
cc 66
eloc 182
nc 37421282
nop 1
dl 0
loc 386
ccs 0
cts 159
cp 0
crap 4422
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Handles all news and newsletter functions for the site
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\AdminController;
18
19
use ElkArte\AbstractController;
20
use ElkArte\Action;
21
use ElkArte\Helper\Util;
22
use ElkArte\Languages\Txt;
23
use ElkArte\SettingsForm\SettingsForm;
24
25
/**
26
 * ManageNews controller, for news administration screens.
27
 *
28
 * @package News
29
 */
30
class ManageNews extends AbstractController
31
{
32
	/** @var array Members specifically being included in a newsletter */
33
	protected array $_members = [];
34
35
	/** @var array Members specifically being excluded from a newsletter */
36
	protected array $_exclude_members = [];
37
38
	/**
39
	 * The news dispatcher / delegator
40
	 *
41
	 * What it does:
42
	 *
43
	 * - This is the entrance point for all News and Newsletter screens.
44
	 * - Called by ?action=admin;area=news.
45
	 * - It does the permission checks, and calls the appropriate function
46
	 * based on the requested sub-action.
47
	 *
48
	 * @event integrate_sa_manage_news used to add new subactions
49
	 * @see AbstractController::action_index
50
	 */
51
	public function action_index()
52
	{
53
		global $context, $txt;
54
55
		theme()->getTemplates()->load('ManageNews');
56
57
		// Format: 'sub-action' => array('function', 'permission')
58
		$subActions = [
59
			'editnews' => [
60
				'controller' => $this,
61
				'function' => 'action_editnews',
62
				'permission' => 'edit_news'],
63
			'mailingmembers' => [
64
				'controller' => $this,
65
				'function' => 'action_mailingmembers',
66
				'permission' => 'send_mail'],
67
			'mailingcompose' => [
68
				'controller' => $this,
69
				'function' => 'action_mailingcompose',
70
				'permission' => 'send_mail'],
71
			'mailingsend' => [
72
				'controller' => $this,
73
				'function' => 'action_mailingsend',
74
				'permission' => 'send_mail'],
75
			'settings' => [
76
				'controller' => $this,
77
				'function' => 'action_newsSettings_display',
78
				'permission' => 'admin_forum'],
79
		];
80
81
		// Action control
82
		$action = new Action('manage_news');
83
84
		// Give integration its shot via integrate_sa_manage_news
85
		$subAction = $action->initialize($subActions, (allowedTo('edit_news') ? 'editnews' : (allowedTo('send_mail') ? 'mailingmembers' : 'settings')));
86
87
		// Some bits for the template
88
		$context['page_title'] = $txt['news_title'];
89
		$context['sub_action'] = $subAction;
90
91
		// Create the tabs for the template.
92
		$context[$context['admin_menu_name']]['object']->prepareTabData([
93
			'title' => 'news_title',
94
			'help' => 'edit_news',
95
			'description' => 'admin_news_desc',
96
			'tabs' => [
97
				'editnews' => [],
98
				'mailingmembers' => [
99
					'description' => $txt['news_mailing_desc'],
100
				],
101
				'settings' => [
102
					'description' => $txt['news_settings_desc'],
103
				],
104
			],
105
		]);
106
107
		// Force the right area...
108
		if (strpos($subAction, 'mailing') === 0)
109
		{
110
			$context[$context['admin_menu_name']]['current_subsection'] = 'mailingmembers';
111
		}
112
113
		// Call the right function for this sub-action.
114
		$action->dispatch($subAction);
115
	}
116
117
	/**
118
	 * Let the administrator(s) edit the news items for the forum.
119
	 *
120
	 * What it does:
121
	 *
122
	 * - It writes an entry into the moderation log.
123
	 * - This function uses the edit_news administration area.
124
	 * - Called by ?action=admin;area=news.
125
	 * - Requires the edit_news permission.
126
	 * - Can be accessed with ?action=admin;sa=editnews.
127
	 *
128
	 * @event integrate_list_news_lists
129
	 */
130
	public function action_editnews(): void
131
	{
132
		global $txt, $modSettings, $context;
133
134
		require_once(SUBSDIR . '/Post.subs.php');
135
136
		// The 'remove selected' button was pressed.
137
		if (!empty($this->_req->post->delete_selection) && !empty($this->_req->post->remove))
138
		{
139
			checkSession();
140
141
			// Store the news temporarily in this array.
142
			$temp_news = explode("\n", $modSettings['news']);
143
144
			// Remove the items that were selected.
145
			foreach (array_keys($temp_news) as $i)
146
			{
147
				if (in_array($i, $this->_req->post->remove))
148
				{
149
					unset($temp_news[$i]);
150
				}
151
			}
152
153
			// Update the database.
154
			updateSettings(['news' => implode("\n", $temp_news)]);
155
156
			logAction('news');
157
		}
158
		// The 'Save' button was pressed.
159
		elseif (!empty($this->_req->post->save_items))
160
		{
161
			checkSession();
162
163
			foreach ($this->_req->post->news as $i => $news)
164
			{
165
				if (trim($news) === '')
166
				{
167
					unset($this->_req->post->news[$i]);
168
				}
169
				else
170
				{
171
					$this->_req->post->news[$i] = Util::htmlspecialchars($this->_req->post->news[$i], ENT_QUOTES);
172
					preparsecode($this->_req->post->news[$i]);
173
				}
174
			}
175
176
			// Send the new news to the database.
177
			updateSettings(['news' => implode("\n", $this->_req->post->news)]);
178
179
			// Log this into the moderation log.
180
			logAction('news');
181
		}
182
183
		// We're going to want this for making our list.
184
		require_once(SUBSDIR . '/News.subs.php');
185
186
		$context['page_title'] = $txt['admin_edit_news'];
187
188
		// Use the standard templates for showing this.
189
		$listOptions = [
190
			'id' => 'news_lists',
191
			'get_items' => [
192
				'function' => 'getNews',
193
			],
194
			'columns' => [
195
				'news' => [
196
					'header' => [
197
						'value' => $txt['admin_edit_news'],
198
					],
199
					'data' => [
200
						'function' => static fn($news) => '<textarea class="" id="data_' . $news['id'] . '" rows="3" name="news[]">' . $news['unparsed'] . '</textarea>
201
							<br />
202
							<div id="preview_' . $news['id'] . '"></div>',
203
						'class' => 'newsarea',
204
					],
205
				],
206
				'preview' => [
207
					'header' => [
208
						'value' => $txt['preview'],
209
					],
210
					'data' => [
211
						'function' => static fn($news) => '<div id="box_preview_' . $news['id'] . '">' . $news['parsed'] . '</div>',
212
						'class' => 'newspreview',
213
					],
214
				],
215
				'check' => [
216
					'header' => [
217
						'value' => '<input type="checkbox" onclick="invertAll(this, this.form);" class="input_check" />',
218
						'class' => 'centertext',
219
					],
220
					'data' => [
221
						'function' => static function ($news) {
222
							if (is_numeric($news['id']))
223
							{
224
								return '<input type="checkbox" name="remove[]" value="' . $news['id'] . '" class="input_check" />';
225
							}
226
							return '';
227
						},
228
						'style' => 'vertical-align: top',
229
					],
230
				],
231
			],
232
			'form' => [
233
				'href' => getUrl('admin', ['action' => 'admin', 'area' => 'news', 'sa' => 'editnews']),
234
				'hidden_fields' => [
235
					$context['session_var'] => $context['session_id'],
236
				],
237
			],
238
			'additional_rows' => [
239
				[
240
					'position' => 'bottom_of_list',
241
					'class' => 'submitbutton',
242
					'value' => '
243
					<input type="submit" name="save_items" value="' . $txt['save'] . '" />
244
					<input type="submit" name="delete_selection" value="' . $txt['editnews_remove_selected'] . '" onclick="return confirm(\'' . $txt['editnews_remove_confirm'] . '\');" />
245
					<span id="moreNewsItems_link" class="hide">
246
						<a class="linkbutton" href="javascript:void(0);" onclick="addAnotherNews(); return false;">' . $txt['editnews_clickadd'] . '</a>
247
					</span>',
248
				],
249
			],
250
			'javascript' => '
251
				document.getElementById("list_news_lists_last").style.display = "none";
252
				document.getElementById("moreNewsItems_link").style.display = "inline";
253
				document.addEventListener("DOMContentLoaded", function() {
254
				    let divs = document.querySelectorAll(\'div[id^="preview_"]\');
255
				    divs.forEach(function (div) {
256
				        let preview_id = div.id.split("_")[1];
257
				        if (last_preview < preview_id && preview_id !== "last")
258
				            last_preview = preview_id;
259
				        make_preview_btn(preview_id);
260
				    });
261
				});',
262
		];
263
264
		theme()->addJavascriptVar([
265
			'last_preview' => 0,
266
		    'txt_preview' => JavaScriptEscape($txt['preview']),
267
		    'txt_news_error_no_news' => JavaScriptEscape($txt['news_error_no_news'])]);
268
269
		// Create the request list.
270
		createList($listOptions);
271
272
		$context['sub_template'] = 'show_list';
273
		$context['default_list'] = 'news_lists';
274
	}
275
276
	/**
277
	 * This function allows a user to select the membergroups to send their mailing to.
278
	 *
279
	 * What it does:
280
	 *
281
	 * - Called by ?action=admin;area=news;sa=mailingmembers.
282
	 * - Requires the send_mail permission.
283
	 * - Form is submitted to ?action=admin;area=news;mailingcompose.
284
	 *
285
	 * @uses the ManageNews template and email_members sub template.
286
	 */
287
	public function action_mailingmembers(): void
288
	{
289
		global $txt, $context;
290
291
		require_once(SUBSDIR . '/Membergroups.subs.php');
292
		require_once(SUBSDIR . '/News.subs.php');
293
294
		// Setup the template
295
		$context['page_title'] = $txt['admin_newsletters'];
296
		$context['sub_template'] = 'email_members';
297
		loadJavascriptFile('suggest.js', ['defer' => true]);
298
299
		// We need group data, including which groups we have and who is in them
300
		$allgroups = getBasicMembergroupData(['all'], [], null, true);
301
		$groups = $allgroups['groups'];
302
303
		// All of the members in post based and member based groups
304
		$pg = [];
305
		foreach ($allgroups['postgroups'] as $postgroup)
306
		{
307
			$pg[] = $postgroup['id'];
308
		}
309
310
		$mg = [];
311
		foreach ($allgroups['membergroups'] as $membergroup)
312
		{
313
			$mg[] = $membergroup['id'];
314
		}
315
316
		// How many are in each group
317
		$mem_groups = membersInGroups($pg, $mg, true, true);
318
		foreach ($mem_groups as $id_group => $member_count)
319
		{
320
			if (isset($groups[$id_group]['member_count']))
321
			{
322
				$groups[$id_group]['member_count'] += $member_count;
323
			}
324
			else
325
			{
326
				$groups[$id_group]['member_count'] = $member_count;
327
			}
328
		}
329
330
		// Generate the include and exclude group select lists for the template
331
		foreach ($groups as $group)
332
		{
333
			$groups[$group['id']]['status'] = 'on';
334
			$groups[$group['id']]['is_postgroup'] = in_array($group['id'], $pg);
335
		}
336
337
		$context['groups'] = [
338
			'select_group' => $txt['admin_newsletters_select_groups'],
339
			'member_groups' => $groups,
340
		];
341
342
		foreach ($groups as $group)
343
		{
344
			$groups[$group['id']]['status'] = 'off';
345
		}
346
347
		$context['exclude_groups'] = [
348
			'select_group' => $txt['admin_newsletters_exclude_groups'],
349
			'member_groups' => $groups,
350
		];
351
352
		// Needed if for the PM option in the mail to all
353
		$context['can_send_pm'] = allowedTo('pm_send');
354
	}
355
356
	/**
357
	 * Shows a form to edit a forum mailing and its recipients.
358
	 *
359
	 * What it does:
360
	 *
361
	 * - Called by ?action=admin;area=news;sa=mailingcompose.
362
	 * - Requires the send_mail permission.
363
	 * - Form is submitted to ?action=admin;area=news;sa=mailingsend.
364
	 *
365
	 * @uses ManageNews template, email_members_compose sub-template.
366
	 */
367
	public function action_mailingcompose(): ?bool
368
	{
369
		global $txt, $context;
370
371
		// Setup the template!
372
		$context['page_title'] = $txt['admin_newsletters'];
373
		$context['sub_template'] = 'email_members_compose';
374
		$context['subject'] = empty($this->_req->post->subject) ? $context['forum_name'] . ': ' . htmlspecialchars($txt['subject'], ENT_COMPAT, 'UTF-8') : $this->_req->post->subject;
375
		$context['message'] = empty($this->_req->post->message) ? htmlspecialchars($txt['message'] . "\n\n" . replaceBasicActionUrl($txt['regards_team']) . "\n\n" . '{$board_url}', ENT_COMPAT, 'UTF-8') : $this->_req->post->message;
376
377
		// Needed for the WYSIWYG editor.
378
		require_once(SUBSDIR . '/Editor.subs.php');
379
380
		// Now create the editor.
381
		$editorOptions = [
382
			'id' => 'message',
383
			'value' => $context['message'],
384
			'height' => '250px',
385
			'width' => '100%',
386
			'labels' => [
387
				'post_button' => $txt['sendtopic_send'],
388
			],
389
			'smiley_container' => 'smileyBox_message',
390
			'bbc_container' => 'bbcBox_message',
391
			'preview_type' => 2,
392
		];
393
		create_control_richedit($editorOptions);
394
395
		if (isset($context['preview']))
396
		{
397
			require_once(SUBSDIR . '/Mail.subs.php');
398
			$context['recipients']['members'] = empty($this->_req->post->members) ? [] : explode(',', $this->_req->post->members);
399
			$context['recipients']['exclude_members'] = empty($this->_req->post->exclude_members) ? [] : explode(',', $this->_req->post->exclude_members);
400
			$context['recipients']['groups'] = empty($this->_req->post->groups) ? [] : explode(',', $this->_req->post->groups);
401
			$context['recipients']['exclude_groups'] = empty($this->_req->post->exclude_groups) ? [] : explode(',', $this->_req->post->exclude_groups);
402
			$context['recipients']['emails'] = empty($this->_req->post->emails) ? [] : explode(';', $this->_req->post->emails);
403
			$context['email_force'] = $this->_req->getPost('email_force', 'isset', false);
404
			$context['total_emails'] = $this->_req->getPost('total_emails', 'intval', 0);
405
			$context['max_id_member'] = $this->_req->getPost('max_id_member', 'intval', 0);
406
			$context['send_pm'] = $this->_req->getPost('send_pm', 'isset', false);
407
			$context['send_html'] = $this->_req->getPost('send_html', 'isset', false);
408
409
			prepareMailingForPreview();
410
411
			return null;
412
		}
413
414
		// Start by finding any manually entered members!
415
		$this->_toClean();
416
417
		// Add in any members chosen from the auto select dropdown.
418
		$this->_toAddOrExclude();
419
420
		// Clean the other vars.
421
		$this->action_mailingsend(true);
422
423
		// We need a couple strings from the email template file
424
		Txt::load('EmailTemplates');
425
		require_once(SUBSDIR . '/News.subs.php');
426
427
		// Get a list of all full banned users.  Use their Username and email to find them.
428
		// Only get the ones that can't login to turn off notification.
429
		$context['recipients']['exclude_members'] = excludeBannedMembers();
430
431
		// Did they select moderators - if so add them as specific members...
432
		if ((!empty($context['recipients']['groups']) && in_array(3, $context['recipients']['groups'])) || (!empty($context['recipients']['exclude_groups']) && in_array(3, $context['recipients']['exclude_groups'])))
433
		{
434
			$mods = getModerators();
435
436
			foreach ($mods as $row)
437
			{
438
				if (in_array(3, $context['recipients']))
439
				{
440
					$context['recipients']['exclude_members'][] = $row;
441
				}
442
				else
443
				{
444
					$context['recipients']['members'][] = $row;
445
				}
446
			}
447
		}
448
449
		require_once(SUBSDIR . '/Members.subs.php');
450
451
		// For progress bar!
452
		$context['total_emails'] = count($context['recipients']['emails']);
453
		$context['max_id_member'] = maxMemberID();
454
455
		// Make sure to fully load the array with the form choices
456
		$context['recipients']['members'] = array_merge($this->_members, $context['recipients']['members']);
457
		$context['recipients']['exclude_members'] = array_merge($this->_exclude_members, $context['recipients']['exclude_members']);
458
459
		// Clean up the arrays.
460
		$context['recipients']['members'] = array_unique($context['recipients']['members']);
461
		$context['recipients']['exclude_members'] = array_unique($context['recipients']['exclude_members']);
462
463
		return true;
464
	}
465
466
	/**
467
	 * If they did not use auto select function on the include/exclude members then
468
	 * we need to look them up from the supplied "one","two" string
469
	 */
470
	private function _toClean(): void
471
	{
472
		$toClean = [];
473
		if (!empty($this->_req->post->members))
474
		{
475
			$toClean['_members'] = 'members';
476
		}
477
478
		if (!empty($this->_req->post->exclude_members))
479
		{
480
			$toClean['_exclude_members'] = 'exclude_members';
481
		}
482
483
		// Manual entries found?
484
		if (!empty($toClean))
485
		{
486
			require_once(SUBSDIR . '/Auth.subs.php');
487
			foreach ($toClean as $key => $type)
488
			{
489
				// Remove the quotes.
490
				$temp = strtr((string) $this->_req->post->{$type}, ['\\"' => '"']);
491
492
				// Break it up in to an array for processing
493
				preg_match_all('~"([^"]+)"~', $this->_req->post->{$type}, $matches);
494
				$temp = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $temp))));
495
496
				// Clean the valid ones, drop the mangled ones
497
				foreach ($temp as $index => $member)
498
				{
499
					if (trim($member) !== '')
500
					{
501
						$temp[$index] = Util::htmlspecialchars(Util::strtolower(trim($member)));
502
					}
503
					else
504
					{
505
						unset($temp[$index]);
506
					}
507
				}
508
509
				// Find the members
510
				$this->{$key} = array_keys(findMembers($temp));
511
			}
512
		}
513
	}
514
515
	/**
516
	 * Members may have been chosen via autoselection pulldown for both Add or Exclude
517
	 * this will process them and combine them to any manually added ones.
518
	 */
519
	private function _toAddOrExclude(): void
520
	{
521
		// Members selected (via auto select) to specifically get the newsletter
522
		if (is_array($this->_req->getPost('member_list')))
523
		{
524
			$members = [];
525
			foreach ($this->_req->post->member_list as $member_id)
526
			{
527
				$members[] = (int) $member_id;
528
			}
529
530
			$this->_members = array_unique(array_merge($this->_members, $members));
531
		}
532
533
		// Members selected (via auto select) to specifically not get the newsletter
534
		if (is_array($this->_req->getPost('exclude_member_list')))
535
		{
536
			$members = [];
537
			foreach ($this->_req->post->exclude_member_list as $member_id)
538
			{
539
				$members[] = (int) $member_id;
540
			}
541
542
			$this->_exclude_members = array_unique(array_merge($this->_exclude_members, $members));
543
		}
544
	}
545
546
	/**
547
	 * Handles the sending of the forum mailing in batches.
548
	 *
549
	 * What it does:
550
	 *
551
	 * - Called by ?action=admin;area=news;sa=mailingsend
552
	 * - Requires the send_mail permission.
553
	 * - Redirects to itself when more batches need to be sent.
554
	 * - Redirects to ?action=admin after everything has been sent.
555
	 *
556
	 * @param bool $clean_only = false; if set, it will only clean the variables, put them in context, then return.
557
	 *
558
	 * @uses ManageNews template and email_members_send sub template.
559
	 *
560
	 */
561
	public function action_mailingsend(bool $clean_only = false): void
562
	{
563
		global $txt, $context, $scripturl, $modSettings;
564
565
		// A nice successful screen if you did it
566
		if (isset($this->_req->query->success))
567
		{
568
			$context['sub_template'] = 'email_members_succeeded';
569
			theme()->getTemplates()->load('ManageNews');
570
571
			return;
572
		}
573
574
		// If just previewing we prepare a message and return it for viewing
575
		if (isset($this->_req->post->preview))
576
		{
577
			$context['preview'] = true;
578
579
			$this->action_mailingcompose();
580
			return;
581
		}
582
583
		// How many to send at once? Quantity depends on whether we are queueing or not.
584
		// @todo Might need an interface? (used in Post.controller.php too with different limits)
585
		$num_at_once = empty($modSettings['mail_queue']) ? 60 : 1000;
586
587
		// If by PM's I suggest we half the above number.
588
		if (!empty($this->_req->post->send_pm))
589
		{
590
			$num_at_once /= 2;
591
		}
592
593
		checkSession();
594
595
		// Where are we actually to?
596
		$context['start'] = $this->_req->getPost('start', 'intval', 0);
597
		$context['email_force'] = $this->_req->getPost('email_force', 'isset', false);
598
		$context['total_emails'] = $this->_req->getPost('total_emails', 'intval', 0);
599
		$context['max_id_member'] = $this->_req->getPost('max_id_member', 'intval', 0);
600
		$context['send_pm'] = $this->_req->getPost('send_pm', 'isset', false);
601
		$context['send_html'] = $this->_req->getPost('send_html', 'isset', false);
602
		$context['parse_html'] = $this->_req->getPost('parse_html', 'isset', false);
603
604
		// Create our main context.
605
		$context['recipients'] = [
606
			'groups' => [],
607
			'exclude_groups' => [],
608
			'members' => [],
609
			'exclude_members' => [],
610
			'emails' => [],
611
		];
612
613
		// Have we any excluded members?
614
		if (!empty($this->_req->post->exclude_members))
615
		{
616
			$members = explode(',', $this->_req->post->exclude_members);
617
			foreach ($members as $member)
618
			{
619
				if ($member >= $context['start'])
620
				{
621
					$context['recipients']['exclude_members'][] = (int) $member;
622
				}
623
			}
624
		}
625
626
		// What about members we *must* do?
627
		if (!empty($this->_req->post->members))
628
		{
629
			$members = explode(',', $this->_req->post->members);
630
			foreach ($members as $member)
631
			{
632
				if ($member >= $context['start'])
633
				{
634
					$context['recipients']['members'][] = (int) $member;
635
				}
636
			}
637
		}
638
639
		// Cleaning groups is simple - although deal with both checkbox and commas.
640
		if (is_array($this->_req->getPost('groups')))
641
		{
642
			foreach ($this->_req->post->groups as $group => $dummy)
643
			{
644
				$context['recipients']['groups'][] = (int) $group;
645
			}
646
		}
647
		elseif ($this->_req->getPost('groups', 'trim', '') !== '')
648
		{
649
			$groups = explode(',', $this->_req->post->groups);
650
			foreach ($groups as $group)
651
			{
652
				$context['recipients']['groups'][] = (int) $group;
653
			}
654
		}
655
656
		// Same for excluded groups
657
		if (is_array($this->_req->getPost('exclude_groups')))
658
		{
659
			foreach ($this->_req->post->exclude_groups as $group => $dummy)
660
			{
661
				$context['recipients']['exclude_groups'][] = (int) $group;
662
			}
663
		}
664
		elseif ($this->_req->getPost('exclude_groups', 'trim', '') !== '')
665
		{
666
			$groups = explode(',', $this->_req->post->exclude_groups);
667
			foreach ($groups as $group)
668
			{
669
				$context['recipients']['exclude_groups'][] = (int) $group;
670
			}
671
		}
672
673
		// Finally - emails!
674
		if (!empty($this->_req->post->emails))
675
		{
676
			$addressed = array_unique(explode(';', strtr($this->_req->post->emails, ["\n" => ';', "\r" => ';', ',' => ';'])));
677
			foreach ($addressed as $curmem)
678
			{
679
				$curmem = trim($curmem);
680
				if ($curmem !== '')
681
				{
682
					$context['recipients']['emails'][$curmem] = $curmem;
683
				}
684
			}
685
		}
686
687
		// If we're only cleaning drop out here.
688
		if ($clean_only)
689
		{
690
			return;
691
		}
692
693
		// Some functions we will need
694
		require_once(SUBSDIR . '/Mail.subs.php');
695
		if ($context['send_pm'])
696
		{
697
			require_once(SUBSDIR . '/PersonalMessage.subs.php');
698
		}
699
700
		$base_subject = $this->_req->getPost('subject', 'trim|strval', '');
701
		$base_message = $this->_req->getPost('message', 'strval', '');
702
703
		// Save the message and its subject in $context
704
		$context['subject'] = htmlspecialchars($base_subject, ENT_COMPAT, 'UTF-8');
705
		$context['message'] = htmlspecialchars($base_message, ENT_COMPAT, 'UTF-8');
706
707
		// Prepare the message for sending it as HTML
708
		if (!$context['send_pm'] && !empty($context['send_html']))
709
		{
710
			// Prepare the message for HTML.
711
			if (!empty($context['parse_html']))
712
			{
713
				$base_message = str_replace(["\n", '  '], ['<br />' . "\n", '&nbsp; '], $base_message);
714
			}
715
716
			// This is here to prevent spam filters from tagging this as spam.
717
			if (preg_match('~<html~i', $base_message) == 0)
718
			{
719
				if (preg_match('~<body~i', $base_message) == 0)
720
				{
721
					$base_message = '<html><head><title>' . $base_subject . '</title></head>' . "\n" . '<body>' . $base_message . '</body></html>';
722
				}
723
				else
724
				{
725
					$base_message = '<html>' . $base_message . '</html>';
726
				}
727
			}
728
		}
729
730
		if (empty($base_message) || empty($base_subject))
731
		{
732
			$context['preview'] = true;
733
734
			$this->action_mailingcompose();
735
			return;
736
		}
737
738
		// Use the default time format.
739
		$this->user->time_format = $modSettings['time_format'];
740
741
		$variables = [
742
			'{$board_url}',
743
			'{$current_time}',
744
			'{$latest_member.link}',
745
			'{$latest_member.id}',
746
			'{$latest_member.name}'
747
		];
748
749
		// We might need this in a bit
750
		$cleanLatestMember = empty($context['send_html']) || $context['send_pm'] ? un_htmlspecialchars($modSettings['latestRealName']) : $modSettings['latestRealName'];
751
752
		// Replace in all the standard things.
753
		$base_message = str_replace($variables,
754
			[
755
				empty($context['send_html']) ? $scripturl : '<a href="' . $scripturl . '">' . $scripturl . '</a>',
756
				standardTime(forum_time(), false),
757
				empty($context['send_html']) ? ($context['send_pm'] ? '[url=' . getUrl('profile', ['action' => 'profile', 'u' => $modSettings['latestMember'], 'name' => $cleanLatestMember]) . ']' . $cleanLatestMember . '[/url]' : $cleanLatestMember) : ('<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $modSettings['latestMember'], 'name' => $cleanLatestMember]) . '">' . $cleanLatestMember . '</a>'),
758
				$modSettings['latestMember'],
759
				$cleanLatestMember
760
			], $base_message);
761
762
		$base_subject = str_replace($variables,
763
			[
764
				$scripturl,
765
				standardTime(forum_time(), false),
766
				$modSettings['latestRealName'],
767
				$modSettings['latestMember'],
768
				$modSettings['latestRealName']
769
			], $base_subject);
770
771
		$from_member = [
772
			'{$member.email}',
773
			'{$member.link}',
774
			'{$member.id}',
775
			'{$member.name}'
776
		];
777
778
		// If we still have emails, do them first!
779
		$i = 0;
780
		foreach ($context['recipients']['emails'] as $k => $email)
781
		{
782
			// Done as many as we can?
783
			if ($i >= $num_at_once)
784
			{
785
				break;
786
			}
787
788
			// Don't sent it twice!
789
			unset($context['recipients']['emails'][$k]);
790
791
			// Dammit - can't PM emails!
792
			if ($context['send_pm'])
793
			{
794
				continue;
795
			}
796
797
			$to_member = [
798
				$email,
799
				empty($context['send_html']) ? $email : '<a href="mailto:' . $email . '">' . $email . '</a>',
800
				'??',
801
				$email
802
			];
803
804
			sendmail($email, str_replace($from_member, $to_member, $base_subject), str_replace($from_member, $to_member, $base_message), null, null, !empty($context['send_html']), 5);
805
806
			// Done another...
807
			$i++;
808
		}
809
810
		// Got some more to send this batch?
811
		$last_id_member = 0;
812
		if ($i < $num_at_once)
813
		{
814
			// Need to build quite a query!
815
			$sendQuery = '(';
816
			$sendParams = [];
817
			if (!empty($context['recipients']['groups']))
818
			{
819
				// Take the long route...
820
				$queryBuild = [];
821
				foreach ($context['recipients']['groups'] as $group)
822
				{
823
					$sendParams['group_' . $group] = $group;
824
					$queryBuild[] = 'mem.id_group = {int:group_' . $group . '}';
825
					if (!empty($group))
826
					{
827
						$queryBuild[] = 'FIND_IN_SET({int:group_' . $group . '}, mem.additional_groups) != 0';
828
						$queryBuild[] = 'mem.id_post_group = {int:group_' . $group . '}';
829
					}
830
				}
831
832
				if (!empty($queryBuild))
833
				{
834
					$sendQuery .= implode(' OR ', $queryBuild);
835
				}
836
			}
837
838
			if (!empty($context['recipients']['members']))
839
			{
840
				$sendQuery .= ($sendQuery === '(' ? '' : ' OR ') . 'mem.id_member IN ({array_int:members})';
841
				$sendParams['members'] = $context['recipients']['members'];
842
			}
843
844
			$sendQuery .= ')';
845
846
			// If we've not got a query then we must be done!
847
			if ($sendQuery === '()')
848
			{
849
				redirectexit('action=admin');
850
			}
851
852
			// Anything to exclude?
853
			if (!empty($context['recipients']['exclude_groups']) && in_array(0, $context['recipients']['exclude_groups']))
854
			{
855
				$sendQuery .= ' AND mem.id_group != {int:regular_group}';
856
			}
857
858
			if (!empty($context['recipients']['exclude_members']))
859
			{
860
				$sendQuery .= ' AND mem.id_member NOT IN ({array_int:exclude_members})';
861
				$sendParams['exclude_members'] = $context['recipients']['exclude_members'];
862
			}
863
864
			// Force them to have it?
865
			if (empty($context['email_force']))
866
			{
867
				$sendQuery .= ' AND mem.notify_announcements = {int:notify_announcements}';
868
			}
869
870
			require_once(SUBSDIR . '/News.subs.php');
871
872
			// Get the smelly people - note we respect the id_member range as it gives us a quicker query.
873
			$recipients = getNewsletterRecipients($sendQuery, $sendParams, $context['start'], $num_at_once, $i);
874
875
			foreach ($recipients as $row)
876
			{
877
				$last_id_member = $row['id_member'];
878
879
				// What groups are we looking at here?
880
				$groups = array_merge([$row['id_group'], $row['id_post_group']], (empty($row['additional_groups']) ? [] : explode(',', $row['additional_groups'])));
881
882
				// Excluded groups?
883
				if (array_intersect($groups, $context['recipients']['exclude_groups']) !== [])
884
				{
885
					continue;
886
				}
887
888
				// We might need this
889
				$cleanMemberName = empty($context['send_html']) || $context['send_pm'] ? un_htmlspecialchars($row['real_name']) : $row['real_name'];
890
891
				// Replace the member-dependant variables
892
				$message = str_replace($from_member,
893
					[
894
						$row['email_address'],
895
						empty($context['send_html']) ? ($context['send_pm'] ? '[url=' . getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $cleanMemberName]) . ']' . $cleanMemberName . '[/url]' : $cleanMemberName) : ('<a href="' . getUrl('profile', ['action' => 'profile', 'u' => $row['id_member'], 'name' => $cleanMemberName]) . '">' . $cleanMemberName . '</a>'),
896
						$row['id_member'],
897
						$cleanMemberName,
898
					], $base_message);
899
900
				$subject = str_replace($from_member,
901
					[
902
						$row['email_address'],
903
						$row['real_name'],
904
						$row['id_member'],
905
						$row['real_name'],
906
					], $base_subject);
907
908
				// Send the actual email - or a PM!
909
				if (!$context['send_pm'])
910
				{
911
					sendmail($row['email_address'], $subject, $message, null, null, !empty($context['send_html']), 5);
912
				}
913
				else
914
				{
915
					sendpm(['to' => [$row['id_member']], 'bcc' => []], $subject, $message);
916
				}
917
			}
918
		}
919
920
		// If used our batch assume we still have a member.
921
		if ($i >= $num_at_once)
922
		{
923
			$last_id_member = $context['start'];
924
		}
925
		// Or we didn't have one in range?
926
		elseif (empty($last_id_member) && $context['start'] + $num_at_once < $context['max_id_member'])
927
		{
928
			$last_id_member = $context['start'] + $num_at_once;
929
		}
930
		// If we have no id_member then we're done.
931
		elseif (empty($last_id_member) && empty($context['recipients']['emails']))
932
		{
933
			// Log this into the admin log.
934
			logAction('newsletter', [], 'admin');
935
			redirectexit('action=admin;area=news;sa=mailingsend;success');
936
		}
937
938
		$context['start'] = $last_id_member;
939
940
		// Working out progress is a black art of sorts.
941
		$percentEmails = $context['total_emails'] == 0 ? 0 : ((count($context['recipients']['emails']) / $context['total_emails']) * ($context['total_emails'] / ($context['total_emails'] + $context['max_id_member'])));
942
		$percentMembers = ($context['start'] / $context['max_id_member']) * ($context['max_id_member'] / ($context['total_emails'] + $context['max_id_member']));
943
		$context['percentage_done'] = round(($percentEmails + $percentMembers) * 100, 2);
944
945
		$context['page_title'] = $txt['admin_newsletters'];
946
		$context['sub_template'] = 'email_members_send';
947
	}
948
949
	/**
950
	 * Set general news and newsletter settings and permissions.
951
	 *
952
	 * What it does:
953
	 *
954
	 * - Called by ?action=admin;area=news;sa=settings.
955
	 * - Requires the forum_admin permission.
956
	 *
957
	 * @event integrate_save_news_settings save new news settings
958
	 * @uses ManageNews template, news_settings sub-template.
959
	 */
960
	public function action_newsSettings_display(): void
961
	{
962
		global $context, $txt;
963
964
		// Initialize the form
965
		$settingsForm = new SettingsForm(SettingsForm::DB_ADAPTER);
966
967
		// Initialize it with our settings
968
		$settingsForm->setConfigVars($this->_settings());
969
970
		// Add some javascript at the bottom...
971
		theme()->addInlineJavascript('
972
			document.getElementById("xmlnews_maxle").disabled = !document.getElementById("xmlnews_enable").checked;
973
			document.getElementById("xmlnews_limit").disabled = !document.getElementById("xmlnews_enable").checked;', true);
974
975
		// Wrap it all up nice and warm...
976
		$context['page_title'] = $txt['admin_edit_news'] . ' - ' . $txt['settings'];
977
		$context['sub_template'] = 'show_settings';
978
		$context['post_url'] = getUrl('admin', ['action' => 'admin', 'area' => 'news', 'save', 'sa' => 'settings']);
979
980
		// Saving the settings?
981
		if (isset($this->_req->query->save))
982
		{
983
			checkSession();
984
985
			call_integration_hook('integrate_save_news_settings');
986
987
			$settingsForm->setConfigValues((array) $this->_req->post);
988
			$settingsForm->save();
989
			redirectexit('action=admin;area=news;sa=settings');
990
		}
991
992
		$settingsForm->prepare();
993
	}
994
995
	/**
996
	 * Get the settings of the forum related to news.
997
	 *
998
	 * @event integrate_modify_news_settings add new news settings
999
	 */
1000
	private function _settings(): array
1001
	{
1002
		global $txt;
1003
1004
		$config_vars = [
1005
			['title', 'settings'],
1006
			// Inline permissions.
1007
			['permissions', 'edit_news', 'help' => '', 'collapsed' => true],
1008
			['permissions', 'send_mail', 'collapsed' => true],
1009
			'',
1010
			// Just the remaining settings.
1011
			['check', 'xmlnews_enable', 'onclick' => "document.getElementById('xmlnews_maxlen').disabled = !this.checked;document.getElementById('xmlnews_limit').disabled = !this.checked;"],
1012
			['int', 'xmlnews_maxlen', 'subtext' => $txt['xmlnews_maxlen_note'], 10],
1013
			['int', 'xmlnews_limit', 'subtext' => $txt['xmlnews_limit_note'], 10],
1014
		];
1015
1016
		// Add new settings with a nice hook, makes them available for admin settings search as well
1017
		call_integration_hook('integrate_modify_news_settings');
1018
1019
		return $config_vars;
1020
	}
1021
1022
	/**
1023 2
	 * Return the form settings for use in admin search
1024
	 */
1025 2
	public function settings_search(): array
1026
	{
1027
		return $this->_settings();
1028 2
	}
1029
}
1030