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