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