1<?php
2/**
3 * MyBB 1.8
4 * Copyright 2014 MyBB Group, All Rights Reserved
5 *
6 * Website: http://www.mybb.com
7 * License: http://www.mybb.com/about/license
8 *
9 */
10
11// Disallow direct access to this file for security reasons
12if(!defined("IN_MYBB"))
13{
14	die("Direct initialization of this file is not allowed.<br /><br />Please make sure IN_MYBB is defined.");
15}
16
17/**
18 * PM handling class, provides common structure to handle private messaging data.
19 *
20 */
21class PMDataHandler extends DataHandler
22{
23	/**
24	* The language file used in the data handler.
25	*
26	* @var string
27	*/
28	public $language_file = 'datahandler_pm';
29
30	/**
31	* The prefix for the language variables used in the data handler.
32	*
33	* @var string
34	*/
35	public $language_prefix = 'pmdata';
36
37	/**
38	 * Array of data inserted in to a private message.
39	 *
40	 * @var array
41	 */
42	public $pm_insert_data = array();
43
44	/**
45	 * Array of data used to update a private message.
46	 *
47	 * @var array
48	 */
49	public $pm_update_data = array();
50
51	/**
52	 * PM ID currently being manipulated by the datahandlers.
53	 *
54	 * @var int
55	 */
56	public $pmid = 0;
57
58	/**
59	 * Values to be returned after inserting a PM.
60	 *
61	 * @var array
62	 */
63	public $return_values = array();
64
65	/**
66	 * Verifies a private message subject.
67	 *
68	 * @return boolean True when valid, false when invalid.
69	 */
70	function verify_subject()
71	{
72		$subject = &$this->data['subject'];
73
74		// Subject is over 85 characters, too long.
75		if(my_strlen($subject) > 85)
76		{
77			$this->set_error("too_long_subject");
78			return false;
79		}
80		// No subject, apply the default [no subject]
81		if(!trim_blank_chrs($subject))
82		{
83			$this->set_error("missing_subject");
84			return false;
85		}
86		return true;
87	}
88
89	/**
90	 * Verifies if a message for a PM is valid.
91	 *
92	 * @return boolean True when valid, false when invalid.
93	 */
94	function verify_message()
95	{
96		$message = &$this->data['message'];
97
98		// No message, return an error.
99		if(trim_blank_chrs($message) == '')
100		{
101			$this->set_error("missing_message");
102			return false;
103		}
104
105		// If the length of message is beyond SQL limitation for 'text' field
106		else if(strlen($message) > 65535)
107		{
108			$this->set_error("message_too_long", array('65535', strlen($message)));
109			return false;
110		}
111
112		return true;
113	}
114
115	/**
116	 * Verifies if the specified sender is valid or not.
117	 *
118	 * @return boolean True when valid, false when invalid.
119	 */
120	function verify_sender()
121	{
122		global $db, $mybb, $lang;
123
124		$pm = &$this->data;
125
126		// Return if we've already validated
127		if(!empty($pm['sender']))
128		{
129			return true;
130		}
131
132		// Fetch the senders profile data.
133		$sender = get_user($pm['fromid']);
134
135		// Collect user permissions for the sender.
136		$sender_permissions = user_permissions($pm['fromid']);
137
138		// Check if the sender is over their quota or not - if they are, disable draft sending
139		if(isset($pm['options']['savecopy']) && $pm['options']['savecopy'] != 0 && empty($pm['saveasdraft']))
140		{
141			if($sender_permissions['pmquota'] != 0 && $sender['totalpms'] >= $sender_permissions['pmquota'] && $this->admin_override != true)
142			{
143				$pm['options']['savecopy'] = 0;
144			}
145		}
146
147		// Assign the sender information to the data.
148		$pm['sender'] = array(
149			"uid" => $sender['uid'],
150			"username" => $sender['username']
151		);
152
153		return true;
154	}
155
156	/**
157	 * Verifies if an array of recipients for a private message are valid
158	 *
159	 * @return boolean True when valid, false when invalid.
160	 */
161	function verify_recipient()
162	{
163		global $cache, $db, $mybb, $lang;
164
165		$pm = &$this->data;
166
167		$recipients = array();
168
169		$invalid_recipients = array();
170		// We have our recipient usernames but need to fetch user IDs
171		if(array_key_exists("to", $pm))
172		{
173			foreach(array("to", "bcc") as $recipient_type)
174			{
175				if(!isset($pm[$recipient_type]))
176				{
177					$pm[$recipient_type] = array();
178				}
179				if(!is_array($pm[$recipient_type]))
180				{
181					$pm[$recipient_type] = array($pm[$recipient_type]);
182				}
183
184				$pm[$recipient_type] = array_map('trim', $pm[$recipient_type]);
185				$pm[$recipient_type] = array_filter($pm[$recipient_type]);
186
187				// No recipients? Skip query
188				if(empty($pm[$recipient_type]))
189				{
190					if($recipient_type == 'to' && empty($pm['saveasdraft']))
191					{
192						$this->set_error("no_recipients");
193						return false;
194					}
195					continue;
196				}
197
198				$recipientUsernames = array_map(array($db, 'escape_string'), $pm[$recipient_type]);
199				$recipientUsernames = "'".implode("','", $recipientUsernames)."'";
200
201				$query = $db->simple_select('users', '*', 'username IN('.$recipientUsernames.')');
202
203				$validUsernames = array();
204
205				while($user = $db->fetch_array($query))
206				{
207					if($recipient_type == "bcc")
208					{
209						$user['bcc'] = 1;
210					}
211
212					$recipients[] = $user;
213					$validUsernames[] = $user['username'];
214				}
215
216				foreach($pm[$recipient_type] as $username)
217				{
218					if(!in_array($username, $validUsernames))
219					{
220						$invalid_recipients[] = $username;
221					}
222				}
223			}
224		}
225		// We have recipient IDs
226		else
227		{
228			foreach(array("toid", "bccid") as $recipient_type)
229			{
230				if(!isset($pm[$recipient_type]))
231				{
232					$pm[$recipient_type] = array();
233				}
234				if(!is_array($pm[$recipient_type]))
235				{
236					$pm[$recipient_type] = array($pm[$recipient_type]);
237				}
238				$pm[$recipient_type] = array_map('intval', $pm[$recipient_type]);
239				$pm[$recipient_type] = array_filter($pm[$recipient_type]);
240
241				// No recipients? Skip query
242				if(empty($pm[$recipient_type]))
243				{
244					if($recipient_type == 'toid' && !$pm['saveasdraft'])
245					{
246						$this->set_error("no_recipients");
247						return false;
248					}
249					continue;
250				}
251
252				$recipientUids = "'".implode("','", $pm[$recipient_type])."'";
253
254				$query = $db->simple_select('users', '*', 'uid IN('.$recipientUids.')');
255
256				$validUids = array();
257
258				while($user = $db->fetch_array($query))
259				{
260					if($recipient_type == "bccid")
261					{
262						$user['bcc'] = 1;
263					}
264
265					$recipients[] = $user;
266					$validUids[] = $user['uid'];
267				}
268
269				foreach($pm[$recipient_type] as $uid)
270				{
271					if(!in_array($uid, $validUids))
272					{
273						$invalid_recipients[] = $uid;
274					}
275				}
276			}
277		}
278
279		// If we have one or more invalid recipients and we're not saving a draft, error
280		if(count($invalid_recipients) > 0)
281		{
282			$invalid_recipients = implode($lang->comma, array_map("htmlspecialchars_uni", $invalid_recipients));
283			$this->set_error("invalid_recipients", array($invalid_recipients));
284			return false;
285		}
286
287		$sender_permissions = user_permissions($pm['fromid']);
288
289		// Are we trying to send this message to more users than the permissions allow?
290		if($sender_permissions['maxpmrecipients'] > 0 && count($recipients) > $sender_permissions['maxpmrecipients'] && $this->admin_override != true)
291		{
292			$this->set_error("too_many_recipients", array($sender_permissions['maxpmrecipients']));
293		}
294
295		// Now we're done with that we loop through each recipient
296		foreach($recipients as $user)
297		{
298			// Collect group permissions for this recipient.
299			$recipient_permissions = user_permissions($user['uid']);
300
301			// See if the sender is on the recipients ignore list and that either
302			// - admin_override is set or
303			// - sender is an administrator
304			if($this->admin_override != true && $sender_permissions['canoverridepm'] != 1)
305			{
306				if(!empty($user['ignorelist']) && strpos(','.$user['ignorelist'].',', ','.$pm['fromid'].',') !== false)
307				{
308					$this->set_error("recipient_is_ignoring", array(htmlspecialchars_uni($user['username'])));
309				}
310
311				// Is the recipient only allowing private messages from their buddy list?
312				if(empty($pm['saveasdraft']) && $mybb->settings['allowbuddyonly'] == 1 && $user['receivefrombuddy'] == 1 && !empty($user['buddylist']) && strpos(','.$user['buddylist'].',', ','.$pm['fromid'].',') === false)
313				{
314					$this->set_error('recipient_has_buddy_only', array(htmlspecialchars_uni($user['username'])));
315				}
316
317				// Can the recipient actually receive private messages based on their permissions or user setting?
318				if(($user['receivepms'] == 0 || $recipient_permissions['canusepms'] == 0) && empty($pm['saveasdraft']))
319				{
320					$this->set_error("recipient_pms_disabled", array(htmlspecialchars_uni($user['username'])));
321					return false;
322				}
323			}
324
325			// Check to see if the user has reached their private message quota - if they have, email them.
326			if($recipient_permissions['pmquota'] != 0 && $user['totalpms'] >= $recipient_permissions['pmquota'] && $sender_permissions['cancp'] != 1 && empty($pm['saveasdraft']) && !$this->admin_override)
327			{
328				if(trim($user['language']) != '' && $lang->language_exists($user['language']))
329				{
330					$uselang = trim($user['language']);
331				}
332				elseif($mybb->settings['bblanguage'])
333				{
334					$uselang = $mybb->settings['bblanguage'];
335				}
336				else
337				{
338					$uselang = "english";
339				}
340				if($uselang == $mybb->settings['bblanguage'] || !$uselang)
341				{
342					$emailsubject = $lang->emailsubject_reachedpmquota;
343					$emailmessage = $lang->email_reachedpmquota;
344				}
345				else
346				{
347					$userlang = new MyLanguage;
348					$userlang->set_path(MYBB_ROOT."inc/languages");
349					$userlang->set_language($uselang);
350					$userlang->load("messages");
351					$emailsubject = $userlang->emailsubject_reachedpmquota;
352					$emailmessage = $userlang->email_reachedpmquota;
353				}
354				$emailmessage = $lang->sprintf($emailmessage, $user['username'], $mybb->settings['bbname'], $mybb->settings['bburl']);
355				$emailsubject = $lang->sprintf($emailsubject, $mybb->settings['bbname'], $pm['subject']);
356
357				$new_email = array(
358					"mailto" => $db->escape_string($user['email']),
359					"mailfrom" => '',
360					"subject" => $db->escape_string($emailsubject),
361					"message" => $db->escape_string($emailmessage),
362					"headers" => ''
363				);
364
365				$db->insert_query("mailqueue", $new_email);
366				$cache->update_mailqueue();
367
368				if($this->admin_override != true)
369				{
370					$this->set_error("recipient_reached_quota", array(htmlspecialchars_uni($user['username'])));
371				}
372			}
373
374			// Everything looks good, assign some specifics about the recipient
375			$pm['recipients'][$user['uid']] = array(
376				"uid" => $user['uid'],
377				"username" => $user['username'],
378				"email" => $user['email'],
379				"lastactive" => $user['lastactive'],
380				"pmnotice" => $user['pmnotice'],
381				"pmnotify" => $user['pmnotify'],
382				"language" => $user['language']
383			);
384
385			// If this recipient is defined as a BCC recipient, save it
386			if(isset($user['bcc']) && $user['bcc'] == 1)
387			{
388				$pm['recipients'][$user['uid']]['bcc'] = 1;
389			}
390		}
391		return true;
392	}
393
394	/**
395	* Verify that the user is not flooding the system.
396	*
397	* @return boolean
398	*/
399	function verify_pm_flooding()
400	{
401		global $mybb, $db;
402
403		$pm = &$this->data;
404
405		// Check if post flooding is enabled within MyBB or if the admin override option is specified.
406		if($mybb->settings['pmfloodsecs'] > 0 && $pm['fromid'] != 0 && $this->admin_override == false && !is_moderator(0, '', $pm['fromid']))
407		{
408			// Fetch the senders profile data.
409			$sender = get_user($pm['fromid']);
410
411			// Calculate last post
412			$query = $db->simple_select("privatemessages", "dateline", "fromid='".$db->escape_string($pm['fromid'])."' AND toid != '0'", array('order_by' => 'dateline', 'order_dir' => 'desc', 'limit' => 1));
413			$sender['lastpm'] = $db->fetch_field($query, "dateline");
414
415			// A little bit of calculation magic and moderator status checking.
416			if(TIME_NOW-$sender['lastpm'] <= $mybb->settings['pmfloodsecs'])
417			{
418				// Oops, user has been flooding - throw back error message.
419				$time_to_wait = ($mybb->settings['pmfloodsecs'] - (TIME_NOW-$sender['lastpm'])) + 1;
420				if($time_to_wait == 1)
421				{
422					$this->set_error("pm_flooding_one_second");
423				}
424				else
425				{
426					$this->set_error("pm_flooding", array($time_to_wait));
427				}
428				return false;
429			}
430		}
431		// All is well that ends well - return true.
432		return true;
433	}
434
435	/**
436	 * Verifies if the various 'options' for sending PMs are valid.
437	 *
438	 * @return boolean True when valid, false when invalid.
439	 */
440	function verify_options()
441	{
442		$options = &$this->data['options'];
443
444		$this->verify_yesno_option($options, 'signature', 1);
445		$this->verify_yesno_option($options, 'savecopy', 1);
446		$this->verify_yesno_option($options, 'disablesmilies', 0);
447
448		// Requesting a read receipt?
449		if(isset($options['readreceipt']) && $options['readreceipt'] == 1)
450		{
451			$options['readreceipt'] = 1;
452		}
453		else
454		{
455			$options['readreceipt'] = 0;
456		}
457		return true;
458	}
459
460	/**
461	 * Validate an entire private message.
462	 *
463	 * @return boolean True when valid, false when invalid.
464	 */
465	function validate_pm()
466	{
467		global $plugins;
468
469		$pm = &$this->data;
470
471		if(empty($pm['savedraft']))
472		{
473			$this->verify_pm_flooding();
474		}
475
476		// Verify all PM assets.
477		$this->verify_subject();
478
479		$this->verify_sender();
480
481		$this->verify_recipient();
482
483		$this->verify_message();
484
485		$this->verify_options();
486
487		$plugins->run_hooks("datahandler_pm_validate", $this);
488
489		// Choose the appropriate folder to save in.
490		if(!empty($pm['saveasdraft']))
491		{
492			$pm['folder'] = 3;
493		}
494		else
495		{
496			$pm['folder'] = 1;
497		}
498
499		// We are done validating, return.
500		$this->set_validated(true);
501		if(count($this->get_errors()) > 0)
502		{
503			return false;
504		}
505		else
506		{
507			return true;
508		}
509	}
510
511	/**
512	 * Insert a new private message.
513	 *
514	 * @return array Array of PM useful data.
515	 */
516	function insert_pm()
517	{
518		global $cache, $db, $mybb, $plugins, $lang;
519
520		// Yes, validating is required.
521		if(!$this->get_validated())
522		{
523			die("The PM needs to be validated before inserting it into the DB.");
524		}
525		if(count($this->get_errors()) > 0)
526		{
527			die("The PM is not valid.");
528		}
529
530		// Assign data to common variable
531		$pm = &$this->data;
532
533		if(empty($pm['pmid']))
534		{
535			$pm['pmid'] = 0;
536		}
537		$pm['pmid'] = (int)$pm['pmid'];
538
539		if(empty($pm['icon']) || $pm['icon'] < 0)
540		{
541			$pm['icon'] = 0;
542		}
543
544		$uid = 0;
545
546		if(!is_array($pm['recipients']))
547		{
548			$recipient_list = array();
549		}
550		else
551		{
552			// Build recipient list
553			foreach($pm['recipients'] as $recipient)
554			{
555				if(!empty($recipient['bcc']))
556				{
557					$recipient_list['bcc'][] = $recipient['uid'];
558				}
559				else
560				{
561					$recipient_list['to'][] = $recipient['uid'];
562					$uid = $recipient['uid'];
563				}
564			}
565		}
566
567		$this->pm_insert_data = array(
568			'fromid' => (int)$pm['sender']['uid'],
569			'folder' => $pm['folder'],
570			'subject' => $db->escape_string($pm['subject']),
571			'icon' => (int)$pm['icon'],
572			'message' => $db->escape_string($pm['message']),
573			'dateline' => TIME_NOW,
574			'status' => 0,
575			'includesig' => $pm['options']['signature'],
576			'smilieoff' => $pm['options']['disablesmilies'],
577			'receipt' => (int)$pm['options']['readreceipt'],
578			'readtime' => 0,
579			'recipients' => $db->escape_string(my_serialize($recipient_list)),
580			'ipaddress' => $db->escape_binary($pm['ipaddress'])
581		);
582
583		// Check if we're updating a draft or not.
584		$query = $db->simple_select("privatemessages", "pmid, deletetime", "folder='3' AND uid='".(int)$pm['sender']['uid']."' AND pmid='{$pm['pmid']}'");
585		$draftcheck = $db->fetch_array($query);
586
587		// This PM was previously a draft
588		if(!empty($draftcheck['pmid']))
589		{
590			if($draftcheck['deletetime'])
591			{
592				// This draft was a reply to a PM
593				$pm['pmid'] = $draftcheck['deletetime'];
594				$pm['do'] = "reply";
595			}
596
597			// Delete the old draft as we no longer need it
598			$db->delete_query("privatemessages", "pmid='{$draftcheck['pmid']}'");
599		}
600
601		// Saving this message as a draft
602		if(!empty($pm['saveasdraft']))
603		{
604			$this->pm_insert_data['uid'] = $pm['sender']['uid'];
605
606			// If this is a reply, then piggyback into the deletetime to let us know in the future
607			if($pm['do'] == "reply" || $pm['do'] == "replyall")
608			{
609				$this->pm_insert_data['deletetime'] = $pm['pmid'];
610			}
611
612			$plugins->run_hooks("datahandler_pm_insert_updatedraft", $this);
613
614			$this->pmid = $db->insert_query("privatemessages", $this->pm_insert_data);
615
616			$plugins->run_hooks("datahandler_pm_insert_updatedraft_commit", $this);
617
618			// If this is a draft, end it here - below deals with complete messages
619			return array(
620				"draftsaved" => 1
621			);
622		}
623
624		$this->pmid = array();
625
626		// Save a copy of the PM for each of our recipients
627		foreach($pm['recipients'] as $recipient)
628		{
629			// Send email notification of new PM if it is enabled for the recipient
630			$query = $db->simple_select("privatemessages", "dateline", "uid='".$recipient['uid']."' AND folder='1'", array('order_by' => 'dateline', 'order_dir' => 'desc', 'limit' => 1));
631			$lastpm = $db->fetch_array($query);
632			if($recipient['pmnotify'] == 1 && (empty($lastpm['dateline']) || $recipient['lastactive'] > $lastpm['dateline']))
633			{
634				if($recipient['language'] != "" && $lang->language_exists($recipient['language']))
635				{
636					$uselang = $recipient['language'];
637				}
638				elseif($mybb->settings['bblanguage'])
639				{
640					$uselang = $mybb->settings['bblanguage'];
641				}
642				else
643				{
644					$uselang = "english";
645				}
646				if($uselang == $mybb->settings['bblanguage'] && !empty($lang->emailsubject_newpm))
647				{
648					$emailsubject = $lang->emailsubject_newpm;
649					$emailmessage = $lang->email_newpm;
650				}
651				else
652				{
653					$userlang = new MyLanguage;
654					$userlang->set_path(MYBB_ROOT."inc/languages");
655					$userlang->set_language($uselang);
656					$userlang->load("messages");
657					$emailsubject = $userlang->emailsubject_newpm;
658					$emailmessage = $userlang->email_newpm;
659				}
660
661				if(!$pm['sender']['username'])
662				{
663					$pm['sender']['username'] = $lang->mybb_engine;
664				}
665
666				require_once MYBB_ROOT.'inc/class_parser.php';
667				$parser = new Postparser;
668
669				$parser_options = array(
670					'me_username'		=> $pm['sender']['username'],
671					'filter_badwords'	=> 1
672				);
673
674				$pm['message'] = $parser->text_parse_message($pm['message'], $parser_options);
675
676				$emailmessage = $lang->sprintf($emailmessage, $recipient['username'], $pm['sender']['username'], $mybb->settings['bbname'], $mybb->settings['bburl'], $pm['message']);
677				$emailsubject = $lang->sprintf($emailsubject, $mybb->settings['bbname'], $pm['subject']);
678
679				$new_email = array(
680					"mailto" => $db->escape_string($recipient['email']),
681					"mailfrom" => '',
682					"subject" => $db->escape_string($emailsubject),
683					"message" => $db->escape_string($emailmessage),
684					"headers" => ''
685				);
686
687				$db->insert_query("mailqueue", $new_email);
688				$cache->update_mailqueue();
689			}
690
691			$this->pm_insert_data['uid'] = $recipient['uid'];
692			$this->pm_insert_data['toid'] = $recipient['uid'];
693
694			$plugins->run_hooks("datahandler_pm_insert", $this);
695
696			$this->pmid[] = $db->insert_query("privatemessages", $this->pm_insert_data);
697
698			$plugins->run_hooks("datahandler_pm_insert_commit", $this);
699
700			// If PM noices/alerts are on, show!
701			if($recipient['pmnotice'] == 1)
702			{
703				$updated_user = array(
704					"pmnotice" => 2
705				);
706				$db->update_query("users", $updated_user, "uid='{$recipient['uid']}'");
707			}
708
709			// Update private message count (total, new and unread) for recipient
710			require_once MYBB_ROOT."/inc/functions_user.php";
711			update_pm_count($recipient['uid'], 7, $recipient['lastactive']);
712		}
713
714		// Are we replying or forwarding an existing PM?
715		if($pm['pmid'])
716		{
717			if($pm['do'] == "reply" || $pm['do'] == "replyall")
718			{
719				$sql_array = array(
720					'status' => 3,
721					'statustime' => TIME_NOW
722				);
723				$db->update_query("privatemessages", $sql_array, "pmid={$pm['pmid']} AND uid={$pm['sender']['uid']}");
724			}
725			elseif($pm['do'] == "forward")
726			{
727				$sql_array = array(
728					'status' => 4,
729					'statustime' => TIME_NOW
730				);
731				$db->update_query("privatemessages", $sql_array, "pmid={$pm['pmid']} AND uid={$pm['sender']['uid']}");
732			}
733		}
734
735		// If we're saving a copy
736		if($pm['options']['savecopy'] != 0)
737		{
738			if(isset($recipient_list['to']) && is_array($recipient_list['to']) && count($recipient_list['to']) == 1)
739			{
740				$this->pm_insert_data['toid'] = $uid;
741			}
742			else
743			{
744				$this->pm_insert_data['toid'] = 0;
745			}
746			$this->pm_insert_data['uid'] = (int)$pm['sender']['uid'];
747			$this->pm_insert_data['folder'] = 2;
748			$this->pm_insert_data['status'] = 1;
749			$this->pm_insert_data['receipt'] = 0;
750
751			$plugins->run_hooks("datahandler_pm_insert_savedcopy", $this);
752
753			$db->insert_query("privatemessages", $this->pm_insert_data);
754
755			$plugins->run_hooks("datahandler_pm_insert_savedcopy_commit", $this);
756
757			// Because the sender saved a copy, update their total pm count
758			require_once MYBB_ROOT."/inc/functions_user.php";
759			update_pm_count($pm['sender']['uid'], 1);
760		}
761
762		// Return back with appropriate data
763		$this->return_values = array(
764			"messagesent" => 1,
765			"pmids" => $this->pmid
766		);
767
768		$plugins->run_hooks("datahandler_pm_insert_end", $this);
769
770		return $this->return_values;
771	}
772}
773