· 7 years ago · Feb 06, 2019, 12:54 AM
1<?php
2
3/**
4 * Simple Machines Forum (SMF)
5 *
6 * @package SMF
7 * @author Simple Machines http://www.simplemachines.org
8 * @copyright 2011 Simple Machines
9 * @license http://www.simplemachines.org/about/smf/license.php BSD
10 *
11 * @version 2.0.14
12 */
13
14if (!defined('SMF'))
15 die('Hacking attempt...');
16
17/* This file contains those functions pertaining to posting, and other such
18 operations, including sending emails, ims, blocking spam, preparsing posts,
19 spell checking, and the post box. This is done with the following:
20
21 void preparsecode(string &message, boolean previewing = false)
22 - takes a message and parses it, returning nothing.
23 - cleans up links (javascript, etc.) and code/quote sections.
24 - won't convert \n's and a few other things if previewing is true.
25
26 string un_preparsecode(string message)
27 // !!!
28
29 void fixTags(string &message)
30 - used by preparsecode, fixes links in message and returns nothing.
31
32 void fixTag(string &message, string myTag, string protocol,
33 bool embeddedUrl = false, bool hasEqualSign = false,
34 bool hasExtra = false)
35 - used by fixTags, fixes a specific tag's links.
36 - myTag is the tag, protocol is http of ftp, embeddedUrl is whether
37 it *can* be set to something, hasEqualSign is whether it *is*
38 set to something, and hasExtra is whether it can have extra
39 cruft after the begin tag.
40
41 bool sendmail(array to, string subject, string message,
42 string message_id = auto, string from = webmaster,
43 bool send_html = false, int priority = 3, bool hotmail_fix = null)
44 - sends an email to the specified recipient.
45 - uses the mail_type setting and the webmaster_email global.
46 - to is he email(s), string or array, to send to.
47 - subject and message are those of the email - expected to have
48 slashes but not be parsed.
49 - subject is expected to have entities, message is not.
50 - from is a string which masks the address for use with replies.
51 - if message_id is specified, uses that as the local-part of the
52 Message-ID header.
53 - send_html indicates whether or not the message is HTML vs. plain
54 text, and does not add any HTML.
55 - returns whether or not the email was sent properly.
56
57 bool AddMailQueue(bool flush = true, array to_array = array(), string subject = '', string message = '',
58 string headers = '', bool send_html = false, int priority = 3)
59 //!!
60
61 array sendpm(array recipients, string subject, string message,
62 bool store_outbox = false, array from = current_member, int pm_head = 0)
63 - sends an personal message from the specified person to the
64 specified people. (from defaults to the user.)
65 - recipients should be an array containing the arrays 'to' and 'bcc',
66 both containing id_member's.
67 - subject and message should have no slashes and no html entities.
68 - pm_head is the ID of the chain being replied to - if any.
69 - from is an array, with the id, name, and username of the member.
70 - returns an array with log entries telling how many recipients were
71 successful and which recipients it failed to send to.
72
73 string mimespecialchars(string text, bool with_charset = true,
74 hotmail_fix = false, string custom_charset = null)
75 - prepare text strings for sending as email.
76 - in case there are higher ASCII characters in the given string, this
77 function will attempt the transport method 'quoted-printable'.
78 Otherwise the transport method '7bit' is used.
79 - with hotmail_fix set all higher ASCII characters are converted to
80 HTML entities to assure proper display of the mail.
81 - uses character set custom_charset if set.
82 - returns an array containing the character set, the converted string
83 and the transport method.
84
85 bool smtp_mail(array mail_to_array, string subject, string message,
86 string headers)
87 - sends mail, like mail() but over SMTP. Used internally.
88 - takes email addresses, a subject and message, and any headers.
89 - expects no slashes or entities.
90 - returns whether it sent or not.
91
92 bool server_parse(string message, resource socket, string response)
93 - sends the specified message to the server, and checks for the
94 expected response. (used internally.)
95 - takes the message to send, socket to send on, and the expected
96 response code.
97 - returns whether it responded as such.
98
99 void SpellCheck()
100 - spell checks the post for typos ;).
101 - uses the pspell library, which MUST be installed.
102 - has problems with internationalization.
103 - is accessed via ?action=spellcheck.
104
105 void sendNotifications(array topics, string type, array exclude = array(), array members_only = array())
106 - sends a notification to members who have elected to receive emails
107 when things happen to a topic, such as replies are posted.
108 - uses the Post langauge file.
109 - topics represents the topics the action is happening to.
110 - the type can be any of reply, sticky, lock, unlock, remove, move,
111 merge, and split. An appropriate message will be sent for each.
112 - automatically finds the subject and its board, and checks permissions
113 for each member who is "signed up" for notifications.
114 - will not send 'reply' notifications more than once in a row.
115 - members in the exclude array will not be processed for the topic with the same key.
116 - members_only are the only ones that will be sent the notification if they have it on.
117
118 bool createPost(&array msgOptions, &array topicOptions, &array posterOptions)
119 // !!!
120
121 bool createAttachment(&array attachmentOptions)
122 // !!!
123
124 bool modifyPost(&array msgOptions, &array topicOptions, &array posterOptions)
125 // !!!
126
127 bool approvePosts(array msgs, bool approve)
128 // !!!
129
130 array approveTopics(array topics, bool approve)
131 // !!!
132
133 void sendApprovalNotifications(array topicData)
134 // !!!
135
136 void updateLastMessages(array id_board's, int id_msg)
137 - takes an array of board IDs and updates their last messages.
138 - if the board has a parent, that parent board is also automatically
139 updated.
140 - columns updated are id_last_msg and lastUpdated.
141 - note that id_last_msg should always be updated using this function,
142 and is not automatically updated upon other changes.
143
144 void adminNotify(string type, int memberID, string member_name = null)
145 - sends all admins an email to let them know a new member has joined.
146 - types supported are 'approval', 'activation', and 'standard'.
147 - called by registerMember() function in Subs-Members.php.
148 - email is sent to all groups that have the moderate_forum permission.
149 - uses the Login language file.
150 - the language set by each member is being used (if available).
151
152 Sending emails from SMF:
153 ---------------------------------------------------------------------------
154 // !!!
155*/
156
157// Parses some bbc before sending into the database...
158function preparsecode(&$message, $previewing = false)
159{
160 global $user_info, $modSettings, $smcFunc, $context;
161
162 // This line makes all languages *theoretically* work even with the wrong charset ;).
163 $message = preg_replace('~&#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $message);
164
165 // Clean up after nobbc ;).
166 $message = preg_replace_callback('~\[nobbc\](.+?)\[/nobbc\]~is', 'nobbc__preg_callback', $message);
167
168 // Remove \r's... they're evil!
169 $message = strtr($message, array("\r" => ''));
170
171 // You won't believe this - but too many periods upsets apache it seems!
172 $message = preg_replace('~\.{100,}~', '...', $message);
173
174 // Trim off trailing quotes - these often happen by accident.
175 while (substr($message, -7) == '[quote]')
176 $message = substr($message, 0, -7);
177 while (substr($message, 0, 8) == '[/quote]')
178 $message = substr($message, 8);
179
180 // Find all code blocks, work out whether we'd be parsing them, then ensure they are all closed.
181 $in_tag = false;
182 $had_tag = false;
183 $codeopen = 0;
184 if (preg_match_all('~(\[(/)*code(?:=[^\]]+)?\])~is', $message, $matches))
185 foreach ($matches[0] as $index => $dummy)
186 {
187 // Closing?
188 if (!empty($matches[2][$index]))
189 {
190 // If it's closing and we're not in a tag we need to open it...
191 if (!$in_tag)
192 $codeopen = true;
193 // Either way we ain't in one any more.
194 $in_tag = false;
195 }
196 // Opening tag...
197 else
198 {
199 $had_tag = true;
200 // If we're in a tag don't do nought!
201 if (!$in_tag)
202 $in_tag = true;
203 }
204 }
205
206 // If we have an open tag, close it.
207 if ($in_tag)
208 $message .= '[/code]';
209 // Open any ones that need to be open, only if we've never had a tag.
210 if ($codeopen && !$had_tag)
211 $message = '[code]' . $message;
212
213 // Now that we've fixed all the code tags, let's fix the img and url tags...
214 $parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
215
216 // The regular expression non breaking space has many versions.
217 $non_breaking_space = $context['utf8'] ? ($context['server']['complex_preg_chars'] ? '\x{A0}' : "\xC2\xA0") : '\xA0';
218
219 // Only mess with stuff outside [code] tags.
220 for ($i = 0, $n = count($parts); $i < $n; $i++)
221 {
222 // It goes 0 = outside, 1 = begin tag, 2 = inside, 3 = close tag, repeat.
223 if ($i % 4 == 0)
224 {
225 fixTags($parts[$i]);
226
227 // Replace /me.+?\n with [me=name]dsf[/me]\n.
228 if (strpos($user_info['name'], '[') !== false || strpos($user_info['name'], ']') !== false || strpos($user_info['name'], '\'') !== false || strpos($user_info['name'], '"') !== false)
229 $parts[$i] = preg_replace('~(\A|\n)/me(?: | )([^\n]*)(?:\z)?~i', '$1[me="' . $user_info['name'] . '"]$2[/me]', $parts[$i]);
230 else
231 $parts[$i] = preg_replace('~(\A|\n)/me(?: | )([^\n]*)(?:\z)?~i', '$1[me=' . $user_info['name'] . ']$2[/me]', $parts[$i]);
232
233 if (!$previewing && strpos($parts[$i], '[html]') !== false)
234 {
235 if (allowedTo('admin_forum'))
236 {
237 static $htmlfunc = null;
238 if ($htmlfunc === null)
239 $htmlfunc = create_function('$m', 'return \'[html]\' . strtr(un_htmlspecialchars("$m[1]"), array("\n" => \' \', \' \' => \'  \', \'[\' => \'[\', \']\' => \']\')) . \'[/html]\';');
240 $parts[$i] = preg_replace_callback('~\[html\](.+?)\[/html\]~is', $htmlfunc, $parts[$i]);
241 }
242
243 // We should edit them out, or else if an admin edits the message they will get shown...
244 else
245 {
246 while (strpos($parts[$i], '[html]') !== false)
247 $parts[$i] = preg_replace('~\[[/]?html\]~i', '', $parts[$i]);
248 }
249 }
250
251 // Let's look at the time tags...
252 $parts[$i] = preg_replace_callback('~\[time(?:=(absolute))*\](.+?)\[/time\]~i', 'time_fix__preg_callback', $parts[$i]);
253
254 // Change the color specific tags to [color=the color].
255 $parts[$i] = preg_replace('~\[(black|blue|green|red|white)\]~', '[color=$1]', $parts[$i]); // First do the opening tags.
256 $parts[$i] = preg_replace('~\[/(black|blue|green|red|white)\]~', '[/color]', $parts[$i]); // And now do the closing tags
257
258 // Make sure all tags are lowercase.
259 $parts[$i] = preg_replace_callback('~\[([/]?)(list|li|table|tr|td)((\s[^\]]+)*)\]~i', 'lowercase_tags__preg_callback', $parts[$i]);
260
261 $list_open = substr_count($parts[$i], '[list]') + substr_count($parts[$i], '[list ');
262 $list_close = substr_count($parts[$i], '[/list]');
263 if ($list_close - $list_open > 0)
264 $parts[$i] = str_repeat('[list]', $list_close - $list_open) . $parts[$i];
265 if ($list_open - $list_close > 0)
266 $parts[$i] = $parts[$i] . str_repeat('[/list]', $list_open - $list_close);
267
268 $mistake_fixes = array(
269 // Find [table]s not followed by [tr].
270 '~\[table\](?![\s' . $non_breaking_space . ']*\[tr\])~s' . ($context['utf8'] ? 'u' : '') => '[table][tr]',
271 // Find [tr]s not followed by [td].
272 '~\[tr\](?![\s' . $non_breaking_space . ']*\[td\])~s' . ($context['utf8'] ? 'u' : '') => '[tr][td]',
273 // Find [/td]s not followed by something valid.
274 '~\[/td\](?![\s' . $non_breaking_space . ']*(?:\[td\]|\[/tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr]',
275 // Find [/tr]s not followed by something valid.
276 '~\[/tr\](?![\s' . $non_breaking_space . ']*(?:\[tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/tr][/table]',
277 // Find [/td]s incorrectly followed by [/table].
278 '~\[/td\][\s' . $non_breaking_space . ']*\[/table\]~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr][/table]',
279 // Find [table]s, [tr]s, and [/td]s (possibly correctly) followed by [td].
280 '~\[(table|tr|/td)\]([\s' . $non_breaking_space . ']*)\[td\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_td_]',
281 // Now, any [td]s left should have a [tr] before them.
282 '~\[td\]~s' => '[tr][td]',
283 // Look for [tr]s which are correctly placed.
284 '~\[(table|/tr)\]([\s' . $non_breaking_space . ']*)\[tr\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_tr_]',
285 // Any remaining [tr]s should have a [table] before them.
286 '~\[tr\]~s' => '[table][tr]',
287 // Look for [/td]s followed by [/tr].
288 '~\[/td\]([\s' . $non_breaking_space . ']*)\[/tr\]~s' . ($context['utf8'] ? 'u' : '') => '[/td]$1[_/tr_]',
289 // Any remaining [/tr]s should have a [/td].
290 '~\[/tr\]~s' => '[/td][/tr]',
291 // Look for properly opened [li]s which aren't closed.
292 '~\[li\]([^\[\]]+?)\[li\]~s' => '[li]$1[_/li_][_li_]',
293 '~\[li\]([^\[\]]+?)\[/list\]~s' => '[_li_]$1[_/li_][/list]',
294 '~\[li\]([^\[\]]+?)$~s' => '[li]$1[/li]',
295 // Lists - find correctly closed items/lists.
296 '~\[/li\]([\s' . $non_breaking_space . ']*)\[/list\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[/list]',
297 // Find list items closed and then opened.
298 '~\[/li\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[_li_]',
299 // Now, find any [list]s or [/li]s followed by [li].
300 '~\[(list(?: [^\]]*?)?|/li)\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_li_]',
301 // Allow for sub lists.
302 '~\[/li\]([\s' . $non_breaking_space . ']*)\[list\]~' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[list]',
303 '~\[/list\]([\s' . $non_breaking_space . ']*)\[li\]~' . ($context['utf8'] ? 'u' : '') => '[/list]$1[_li_]',
304 // Any remaining [li]s weren't inside a [list].
305 '~\[li\]~' => '[list][li]',
306 // Any remaining [/li]s weren't before a [/list].
307 '~\[/li\]~' => '[/li][/list]',
308 // Put the correct ones back how we found them.
309 '~\[_(li|/li|td|tr|/tr)_\]~' => '[$1]',
310 // Images with no real url.
311 '~\[img\]https?://.{0,7}\[/img\]~' => '',
312 );
313
314 // Fix up some use of tables without [tr]s, etc. (it has to be done more than once to catch it all.)
315 for ($j = 0; $j < 3; $j++)
316 $parts[$i] = preg_replace(array_keys($mistake_fixes), $mistake_fixes, $parts[$i]);
317
318 // Now we're going to do full scale table checking...
319 $table_check = $parts[$i];
320 $table_offset = 0;
321 $table_array = array();
322 $table_order = array(
323 'table' => 'td',
324 'tr' => 'table',
325 'td' => 'tr',
326 );
327 while (preg_match('~\[(/)*(table|tr|td)\]~', $table_check, $matches) != false)
328 {
329 // Keep track of where this is.
330 $offset = strpos($table_check, $matches[0]);
331 $remove_tag = false;
332
333 // Is it opening?
334 if ($matches[1] != '/')
335 {
336 // If the previous table tag isn't correct simply remove it.
337 if ((!empty($table_array) && $table_array[0] != $table_order[$matches[2]]) || (empty($table_array) && $matches[2] != 'table'))
338 $remove_tag = true;
339 // Record this was the last tag.
340 else
341 array_unshift($table_array, $matches[2]);
342 }
343 // Otherwise is closed!
344 else
345 {
346 // Only keep the tag if it's closing the right thing.
347 if (empty($table_array) || ($table_array[0] != $matches[2]))
348 $remove_tag = true;
349 else
350 array_shift($table_array);
351 }
352
353 // Removing?
354 if ($remove_tag)
355 {
356 $parts[$i] = substr($parts[$i], 0, $table_offset + $offset) . substr($parts[$i], $table_offset + strlen($matches[0]) + $offset);
357 // We've lost some data.
358 $table_offset -= strlen($matches[0]);
359 }
360
361 // Remove everything up to here.
362 $table_offset += $offset + strlen($matches[0]);
363 $table_check = substr($table_check, $offset + strlen($matches[0]));
364 }
365
366 // Close any remaining table tags.
367 foreach ($table_array as $tag)
368 $parts[$i] .= '[/' . $tag . ']';
369 }
370 }
371
372 // Put it back together!
373 if (!$previewing)
374 $message = strtr(implode('', $parts), array(' ' => ' ', "\n" => '<br />', $context['utf8'] ? "\xC2\xA0" : "\xA0" => ' '));
375 else
376 $message = strtr(implode('', $parts), array(' ' => ' ', $context['utf8'] ? "\xC2\xA0" : "\xA0" => ' '));
377
378 // Now let's quickly clean up things that will slow our parser (which are common in posted code.)
379 $message = strtr($message, array('[]' => '[]', '['' => '[''));
380}
381
382// This is very simple, and just removes things done by preparsecode.
383function un_preparsecode($message)
384{
385 global $smcFunc;
386
387 $parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
388
389 // We're going to unparse only the stuff outside [code]...
390 for ($i = 0, $n = count($parts); $i < $n; $i++)
391 {
392 // If $i is a multiple of four (0, 4, 8, ...) then it's not a code section...
393 if ($i % 4 == 0)
394 {
395 $parts[$i] = preg_replace_callback('~\[html\](.+?)\[/html\]~i', 'htmlspecial_html__preg_callback', $parts[$i]);
396 // $parts[$i] = preg_replace('~\[html\](.+?)\[/html\]~ie', '\'[html]\' . strtr(htmlspecialchars(\'$1\', ENT_QUOTES), array(\'\\"\' => \'"\', \'&#13;\' => \'<br />\', \'&#32;\' => \' \', \'&#38;\' => \'&\', \'&#91;\' => \'[\', \'&#93;\' => \']\')) . \'[/html]\'', $parts[$i]);
397
398 // Attempt to un-parse the time to something less awful.
399 $parts[$i] = preg_replace_callback('~\[time\](\d{0,10})\[/time\]~i', 'time_format__preg_callback', $parts[$i]);
400 }
401 }
402
403 // Change breaks back to \n's and &nsbp; back to spaces.
404 return preg_replace('~<br( /)?' . '>~', "\n", str_replace(' ', ' ', implode('', $parts)));
405}
406
407// Fix any URLs posted - ie. remove 'javascript:'.
408function fixTags(&$message)
409{
410 global $modSettings;
411
412 // WARNING: Editing the below can cause large security holes in your forum.
413 // Edit only if you are sure you know what you are doing.
414
415 $fixArray = array(
416 // [img]http://...[/img] or [img width=1]http://...[/img]
417 array(
418 'tag' => 'img',
419 'protocols' => array('http', 'https'),
420 'embeddedUrl' => false,
421 'hasEqualSign' => false,
422 'hasExtra' => true,
423 ),
424 // [url]http://...[/url]
425 array(
426 'tag' => 'url',
427 'protocols' => array('http', 'https'),
428 'embeddedUrl' => true,
429 'hasEqualSign' => false,
430 ),
431 // [url=http://...]name[/url]
432 array(
433 'tag' => 'url',
434 'protocols' => array('http', 'https'),
435 'embeddedUrl' => true,
436 'hasEqualSign' => true,
437 ),
438 // [iurl]http://...[/iurl]
439 array(
440 'tag' => 'iurl',
441 'protocols' => array('http', 'https'),
442 'embeddedUrl' => true,
443 'hasEqualSign' => false,
444 ),
445 // [iurl=http://...]name[/iurl]
446 array(
447 'tag' => 'iurl',
448 'protocols' => array('http', 'https'),
449 'embeddedUrl' => true,
450 'hasEqualSign' => true,
451 ),
452 // [ftp]ftp://...[/ftp]
453 array(
454 'tag' => 'ftp',
455 'protocols' => array('ftp', 'ftps'),
456 'embeddedUrl' => true,
457 'hasEqualSign' => false,
458 ),
459 // [ftp=ftp://...]name[/ftp]
460 array(
461 'tag' => 'ftp',
462 'protocols' => array('ftp', 'ftps'),
463 'embeddedUrl' => true,
464 'hasEqualSign' => true,
465 ),
466 // [flash]http://...[/flash]
467 array(
468 'tag' => 'flash',
469 'protocols' => array('http', 'https'),
470 'embeddedUrl' => false,
471 'hasEqualSign' => false,
472 'hasExtra' => true,
473 ),
474 );
475
476 // Fix each type of tag.
477 foreach ($fixArray as $param)
478 fixTag($message, $param['tag'], $param['protocols'], $param['embeddedUrl'], $param['hasEqualSign'], !empty($param['hasExtra']));
479
480 // Now fix possible security problems with images loading links automatically...
481 $message = preg_replace_callback('~(\[img.*?\])(.+?)\[/img\]~is', 'action_fix__preg_callback', $message);
482
483 // Limit the size of images posted?
484 if (!empty($modSettings['max_image_width']) || !empty($modSettings['max_image_height']))
485 {
486 // Find all the img tags - with or without width and height.
487 preg_match_all('~\[img(\s+width=\d+)?(\s+height=\d+)?(\s+width=\d+)?\](.+?)\[/img\]~is', $message, $matches, PREG_PATTERN_ORDER);
488
489 $replaces = array();
490 foreach ($matches[0] as $match => $dummy)
491 {
492 // If the width was after the height, handle it.
493 $matches[1][$match] = !empty($matches[3][$match]) ? $matches[3][$match] : $matches[1][$match];
494
495 // Now figure out if they had a desired height or width...
496 $desired_width = !empty($matches[1][$match]) ? (int) substr(trim($matches[1][$match]), 6) : 0;
497 $desired_height = !empty($matches[2][$match]) ? (int) substr(trim($matches[2][$match]), 7) : 0;
498
499 // One was omitted, or both. We'll have to find its real size...
500 if (empty($desired_width) || empty($desired_height))
501 {
502 list ($width, $height) = url_image_size(un_htmlspecialchars($matches[4][$match]));
503
504 // They don't have any desired width or height!
505 if (empty($desired_width) && empty($desired_height))
506 {
507 $desired_width = $width;
508 $desired_height = $height;
509 }
510 // Scale it to the width...
511 elseif (empty($desired_width) && !empty($height))
512 $desired_width = (int) (($desired_height * $width) / $height);
513 // Scale if to the height.
514 elseif (!empty($width))
515 $desired_height = (int) (($desired_width * $height) / $width);
516 }
517
518 // If the width and height are fine, just continue along...
519 if ($desired_width <= $modSettings['max_image_width'] && $desired_height <= $modSettings['max_image_height'])
520 continue;
521
522 // Too bad, it's too wide. Make it as wide as the maximum.
523 if ($desired_width > $modSettings['max_image_width'] && !empty($modSettings['max_image_width']))
524 {
525 $desired_height = (int) (($modSettings['max_image_width'] * $desired_height) / $desired_width);
526 $desired_width = $modSettings['max_image_width'];
527 }
528
529 // Now check the height, as well. Might have to scale twice, even...
530 if ($desired_height > $modSettings['max_image_height'] && !empty($modSettings['max_image_height']))
531 {
532 $desired_width = (int) (($modSettings['max_image_height'] * $desired_width) / $desired_height);
533 $desired_height = $modSettings['max_image_height'];
534 }
535
536 $replaces[$matches[0][$match]] = '[img' . (!empty($desired_width) ? ' width=' . $desired_width : '') . (!empty($desired_height) ? ' height=' . $desired_height : '') . ']' . $matches[4][$match] . '[/img]';
537 }
538
539 // If any img tags were actually changed...
540 if (!empty($replaces))
541 $message = strtr($message, $replaces);
542 }
543}
544
545// Fix a specific class of tag - ie. url with =.
546function fixTag(&$message, $myTag, $protocols, $embeddedUrl = false, $hasEqualSign = false, $hasExtra = false)
547{
548 global $boardurl, $scripturl;
549
550 if (preg_match('~^([^:]+://[^/]+)~', $boardurl, $match) != 0)
551 $domain_url = $match[1];
552 else
553 $domain_url = $boardurl . '/';
554
555 $replaces = array();
556
557 if ($hasEqualSign)
558 preg_match_all('~\[(' . $myTag . ')=([^\]]*?)\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches);
559 else
560 preg_match_all('~\[(' . $myTag . ($hasExtra ? '(?:[^\]]*?)' : '') . ')\](.+?)\[/(' . $myTag . ')\]~is', $message, $matches);
561
562 foreach ($matches[0] as $k => $dummy)
563 {
564 // Remove all leading and trailing whitespace.
565 $replace = trim($matches[2][$k]);
566 $this_tag = $matches[1][$k];
567 $this_close = $hasEqualSign ? (empty($matches[4][$k]) ? '' : $matches[4][$k]) : $matches[3][$k];
568
569 $found = false;
570 foreach ($protocols as $protocol)
571 {
572 $found = strncasecmp($replace, $protocol . '://', strlen($protocol) + 3) === 0;
573 if ($found)
574 break;
575 }
576
577 if (!$found && $protocols[0] == 'http')
578 {
579 if (substr($replace, 0, 1) == '/')
580 $replace = $domain_url . $replace;
581 elseif (substr($replace, 0, 1) == '?')
582 $replace = $scripturl . $replace;
583 elseif (substr($replace, 0, 1) == '#' && $embeddedUrl)
584 {
585 $replace = '#' . preg_replace('~[^A-Za-z0-9_\-#]~', '', substr($replace, 1));
586 $this_tag = 'iurl';
587 $this_close = 'iurl';
588 }
589 else
590 $replace = $protocols[0] . '://' . $replace;
591 }
592 elseif (!$found && $protocols[0] == 'ftp')
593 $replace = $protocols[0] . '://' . preg_replace('~^(?!ftps?)[^:]+://~', '', $replace);
594 elseif (!$found)
595 $replace = $protocols[0] . '://' . $replace;
596
597 if ($hasEqualSign && $embeddedUrl)
598 $replaces[$matches[0][$k]] = '[' . $this_tag . '=' . $replace . ']' . (empty($matches[4][$k]) ? '' : $matches[3][$k] . '[/' . $this_close . ']');
599 elseif ($hasEqualSign)
600 $replaces['[' . $matches[1][$k] . '=' . $matches[2][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']';
601 elseif ($embeddedUrl)
602 $replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']' . $matches[2][$k] . '[/' . $this_close . ']';
603 else
604 $replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . ']' . $replace . '[/' . $this_close . ']';
605 }
606
607 foreach ($replaces as $k => $v)
608 {
609 if ($k == $v)
610 unset($replaces[$k]);
611 }
612
613 if (!empty($replaces))
614 $message = strtr($message, $replaces);
615}
616
617// Send off an email.
618function sendmail($to, $subject, $message, $from = null, $message_id = null, $send_html = false, $priority = 3, $hotmail_fix = null, $is_private = false)
619{
620 global $webmaster_email, $context, $modSettings, $txt, $scripturl;
621 global $smcFunc;
622
623 // Use sendmail if it's set or if no SMTP server is set.
624 $use_sendmail = empty($modSettings['mail_type']) || $modSettings['smtp_host'] == '';
625
626 // Line breaks need to be \r\n only in windows or for SMTP.
627 $line_break = $context['server']['is_windows'] || !$use_sendmail ? "\r\n" : "\n";
628
629 // So far so good.
630 $mail_result = true;
631
632 // If the recipient list isn't an array, make it one.
633 $to_array = is_array($to) ? $to : array($to);
634
635 // Once upon a time, Hotmail could not interpret non-ASCII mails.
636 // In honour of those days, it's still called the 'hotmail fix'.
637 if ($hotmail_fix === null)
638 {
639 $hotmail_to = array();
640 foreach ($to_array as $i => $to_address)
641 {
642 if (preg_match('~@(att|comcast|bellsouth)\.[a-zA-Z\.]{2,6}$~i', $to_address) === 1)
643 {
644 $hotmail_to[] = $to_address;
645 $to_array = array_diff($to_array, array($to_address));
646 }
647 }
648
649 // Call this function recursively for the hotmail addresses.
650 if (!empty($hotmail_to))
651 $mail_result = sendmail($hotmail_to, $subject, $message, $from, $message_id, $send_html, $priority, true);
652
653 // The remaining addresses no longer need the fix.
654 $hotmail_fix = false;
655
656 // No other addresses left? Return instantly.
657 if (empty($to_array))
658 return $mail_result;
659 }
660
661 // Get rid of entities.
662 $subject = un_htmlspecialchars($subject);
663 // Make the message use the proper line breaks.
664 $message = str_replace(array("\r", "\n"), array('', $line_break), $message);
665
666 // Make sure hotmail mails are sent as HTML so that HTML entities work.
667 if ($hotmail_fix && !$send_html)
668 {
669 $send_html = true;
670 $message = strtr($message, array($line_break => '<br />' . $line_break));
671 $message = preg_replace('~(' . preg_quote($scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '<a href="$1">$1</a>', $message);
672 }
673
674 list (, $from_name) = mimespecialchars(addcslashes($from !== null ? $from : $context['forum_name'], '<>()\'\\"'), true, $hotmail_fix, $line_break);
675 list (, $subject) = mimespecialchars($subject, true, $hotmail_fix, $line_break);
676
677 // Construct the mail headers...
678 $headers = 'From: "' . $from_name . '" <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>' . $line_break;
679 $headers .= $from !== null ? 'Reply-To: <' . $from . '>' . $line_break : '';
680 $headers .= 'Return-Path: ' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . $line_break;
681 $headers .= 'Date: ' . gmdate('D, d M Y H:i:s') . ' -0000' . $line_break;
682
683 if ($message_id !== null && empty($modSettings['mail_no_message_id']))
684 $headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . $message_id . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
685 $headers .= 'X-Mailer: SMF' . $line_break;
686
687 // Pass this to the integration before we start modifying the output -- it'll make it easier later.
688 if (in_array(false, call_integration_hook('integrate_outgoing_email', array(&$subject, &$message, &$headers)), true))
689 return false;
690
691 // Save the original message...
692 $orig_message = $message;
693
694 // The mime boundary separates the different alternative versions.
695 $mime_boundary = 'SMF-' . md5($message . time());
696
697 // Using mime, as it allows to send a plain unencoded alternative.
698 $headers .= 'Mime-Version: 1.0' . $line_break;
699 $headers .= 'Content-Type: multipart/alternative; boundary="' . $mime_boundary . '"' . $line_break;
700 $headers .= 'Content-Transfer-Encoding: 7bit' . $line_break;
701
702 // Sending HTML? Let's plop in some basic stuff, then.
703 if ($send_html)
704 {
705 $no_html_message = un_htmlspecialchars(strip_tags(strtr($orig_message, array('</title>' => $line_break))));
706
707 // But, then, dump it and use a plain one for dinosaur clients.
708 list(, $plain_message) = mimespecialchars($no_html_message, false, true, $line_break);
709 $message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
710
711 // This is the plain text version. Even if no one sees it, we need it for spam checkers.
712 list($charset, $plain_charset_message, $encoding) = mimespecialchars($no_html_message, false, false, $line_break);
713 $message .= 'Content-Type: text/plain; charset=' . $charset . $line_break;
714 $message .= 'Content-Transfer-Encoding: ' . $encoding . $line_break . $line_break;
715 $message .= $plain_charset_message . $line_break . '--' . $mime_boundary . $line_break;
716
717 // This is the actual HTML message, prim and proper. If we wanted images, they could be inlined here (with multipart/related, etc.)
718 list($charset, $html_message, $encoding) = mimespecialchars($orig_message, false, $hotmail_fix, $line_break);
719 $message .= 'Content-Type: text/html; charset=' . $charset . $line_break;
720 $message .= 'Content-Transfer-Encoding: ' . ($encoding == '' ? '7bit' : $encoding) . $line_break . $line_break;
721 $message .= $html_message . $line_break . '--' . $mime_boundary . '--';
722 }
723 // Text is good too.
724 else
725 {
726 // Send a plain message first, for the older web clients.
727 list(, $plain_message) = mimespecialchars($orig_message, false, true, $line_break);
728 $message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
729
730 // Now add an encoded message using the forum's character set.
731 list ($charset, $encoded_message, $encoding) = mimespecialchars($orig_message, false, false, $line_break);
732 $message .= 'Content-Type: text/plain; charset=' . $charset . $line_break;
733 $message .= 'Content-Transfer-Encoding: ' . $encoding . $line_break . $line_break;
734 $message .= $encoded_message . $line_break . '--' . $mime_boundary . '--';
735 }
736
737 // Are we using the mail queue, if so this is where we butt in...
738 if (!empty($modSettings['mail_queue']) && $priority != 0)
739 return AddMailQueue(false, $to_array, $subject, $message, $headers, $send_html, $priority, $is_private);
740
741 // If it's a priority mail, send it now - note though that this should NOT be used for sending many at once.
742 elseif (!empty($modSettings['mail_queue']) && !empty($modSettings['mail_limit']))
743 {
744 list ($last_mail_time, $mails_this_minute) = @explode('|', $modSettings['mail_recent']);
745 if (empty($mails_this_minute) || time() > $last_mail_time + 60)
746 $new_queue_stat = time() . '|' . 1;
747 else
748 $new_queue_stat = $last_mail_time . '|' . ((int) $mails_this_minute + 1);
749
750 updateSettings(array('mail_recent' => $new_queue_stat));
751 }
752
753 // SMTP or sendmail?
754 if ($use_sendmail)
755 {
756 $subject = strtr($subject, array("\r" => '', "\n" => ''));
757 if (!empty($modSettings['mail_strip_carriage']))
758 {
759 $message = strtr($message, array("\r" => ''));
760 $headers = strtr($headers, array("\r" => ''));
761 }
762
763 foreach ($to_array as $to)
764 {
765 if (!mail(strtr($to, array("\r" => '', "\n" => '')), $subject, $message, $headers))
766 {
767 log_error(sprintf($txt['mail_send_unable'], $to));
768 $mail_result = false;
769 }
770
771 // Wait, wait, I'm still sending here!
772 @set_time_limit(300);
773 if (function_exists('apache_reset_timeout'))
774 @apache_reset_timeout();
775 }
776 }
777 else
778 $mail_result = $mail_result && smtp_mail($to_array, $subject, $message, $headers);
779
780 // Everything go smoothly?
781 return $mail_result;
782}
783
784// Add an email to the mail queue.
785function AddMailQueue($flush = false, $to_array = array(), $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false)
786{
787 global $context, $modSettings, $smcFunc;
788
789 static $cur_insert = array();
790 static $cur_insert_len = 0;
791
792 if ($cur_insert_len == 0)
793 $cur_insert = array();
794
795 // If we're flushing, make the final inserts - also if we're near the MySQL length limit!
796 if (($flush || $cur_insert_len > 800000) && !empty($cur_insert))
797 {
798 // Only do these once.
799 $cur_insert_len = 0;
800
801 // Dump the data...
802 $smcFunc['db_insert']('',
803 '{db_prefix}mail_queue',
804 array(
805 'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string-65534', 'subject' => 'string-255',
806 'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
807 ),
808 $cur_insert,
809 array('id_mail')
810 );
811
812 $cur_insert = array();
813 $context['flush_mail'] = false;
814 }
815
816 // If we're flushing we're done.
817 if ($flush)
818 {
819 $nextSendTime = time() + 10;
820
821 $smcFunc['db_query']('', '
822 UPDATE {db_prefix}settings
823 SET value = {string:nextSendTime}
824 WHERE variable = {string:mail_next_send}
825 AND value = {string:no_outstanding}',
826 array(
827 'nextSendTime' => $nextSendTime,
828 'mail_next_send' => 'mail_next_send',
829 'no_outstanding' => '0',
830 )
831 );
832
833 return true;
834 }
835
836 // Ensure we tell obExit to flush.
837 $context['flush_mail'] = true;
838
839 foreach ($to_array as $to)
840 {
841 // Will this insert go over MySQL's limit?
842 $this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700;
843
844 // Insert limit of 1M (just under the safety) is reached?
845 if ($this_insert_len + $cur_insert_len > 1000000)
846 {
847 // Flush out what we have so far.
848 $smcFunc['db_insert']('',
849 '{db_prefix}mail_queue',
850 array(
851 'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string-65534', 'subject' => 'string-255',
852 'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
853 ),
854 $cur_insert,
855 array('id_mail')
856 );
857
858 // Clear this out.
859 $cur_insert = array();
860 $cur_insert_len = 0;
861 }
862
863 // Now add the current insert to the array...
864 $cur_insert[] = array(time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private);
865 $cur_insert_len += $this_insert_len;
866 }
867
868 // If they are using SSI there is a good chance obExit will never be called. So lets be nice and flush it for them.
869 if (SMF === 'SSI')
870 return AddMailQueue(true);
871
872 return true;
873}
874
875// Send off a personal message.
876function sendpm($recipients, $subject, $message, $store_outbox = false, $from = null, $pm_head = 0)
877{
878 global $scripturl, $txt, $user_info, $language;
879 global $modSettings, $smcFunc;
880
881 // Make sure the PM language file is loaded, we might need something out of it.
882 loadLanguage('PersonalMessage');
883
884 $onBehalf = $from !== null;
885
886 // Initialize log array.
887 $log = array(
888 'failed' => array(),
889 'sent' => array()
890 );
891
892 if ($from === null)
893 $from = array(
894 'id' => $user_info['id'],
895 'name' => $user_info['name'],
896 'username' => $user_info['username']
897 );
898 // Probably not needed. /me something should be of the typer.
899 else
900 $user_info['name'] = $from['name'];
901
902 // This is the one that will go in their inbox.
903 $htmlmessage = $smcFunc['htmlspecialchars']($message, ENT_QUOTES);
904 $htmlsubject = $smcFunc['htmlspecialchars']($subject);
905 preparsecode($htmlmessage);
906
907 // Integrated PMs
908 call_integration_hook('integrate_personal_message', array($recipients, $from['username'], $subject, $message));
909
910 // Get a list of usernames and convert them to IDs.
911 $usernames = array();
912 foreach ($recipients as $rec_type => $rec)
913 {
914 foreach ($rec as $id => $member)
915 {
916 if (!is_numeric($recipients[$rec_type][$id]))
917 {
918 $recipients[$rec_type][$id] = $smcFunc['strtolower'](trim(preg_replace('/[<>&"\'=\\\]/', '', $recipients[$rec_type][$id])));
919 $usernames[$recipients[$rec_type][$id]] = 0;
920 }
921 }
922 }
923 if (!empty($usernames))
924 {
925 $request = $smcFunc['db_query']('pm_find_username', '
926 SELECT id_member, member_name
927 FROM {db_prefix}members
928 WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name') . ' IN ({array_string:usernames})',
929 array(
930 'usernames' => array_keys($usernames),
931 )
932 );
933 while ($row = $smcFunc['db_fetch_assoc']($request))
934 if (isset($usernames[$smcFunc['strtolower']($row['member_name'])]))
935 $usernames[$smcFunc['strtolower']($row['member_name'])] = $row['id_member'];
936 $smcFunc['db_free_result']($request);
937
938 // Replace the usernames with IDs. Drop usernames that couldn't be found.
939 foreach ($recipients as $rec_type => $rec)
940 foreach ($rec as $id => $member)
941 {
942 if (is_numeric($recipients[$rec_type][$id]))
943 continue;
944
945 if (!empty($usernames[$member]))
946 $recipients[$rec_type][$id] = $usernames[$member];
947 else
948 {
949 $log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]);
950 unset($recipients[$rec_type][$id]);
951 }
952 }
953 }
954
955 // Make sure there are no duplicate 'to' members.
956 $recipients['to'] = array_unique($recipients['to']);
957
958 // Only 'bcc' members that aren't already in 'to'.
959 $recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']);
960
961 // Combine 'to' and 'bcc' recipients.
962 $all_to = array_merge($recipients['to'], $recipients['bcc']);
963
964 // Check no-one will want it deleted right away!
965 $request = $smcFunc['db_query']('', '
966 SELECT
967 id_member, criteria, is_or
968 FROM {db_prefix}pm_rules
969 WHERE id_member IN ({array_int:to_members})
970 AND delete_pm = {int:delete_pm}',
971 array(
972 'to_members' => $all_to,
973 'delete_pm' => 1,
974 )
975 );
976 $deletes = array();
977 // Check whether we have to apply anything...
978 while ($row = $smcFunc['db_fetch_assoc']($request))
979 {
980 $criteria = safe_unserialize($row['criteria']);
981 // Note we don't check the buddy status, cause deletion from buddy = madness!
982 $delete = false;
983 foreach ($criteria as $criterium)
984 {
985 $match = false;
986 if (($criterium['t'] == 'mid' && $criterium['v'] == $from['id']) || ($criterium['t'] == 'gid' && in_array($criterium['v'], $user_info['groups'])) || ($criterium['t'] == 'sub' && strpos($subject, $criterium['v']) !== false) || ($criterium['t'] == 'msg' && strpos($message, $criterium['v']) !== false))
987 $delete = true;
988 // If we're adding and one criteria don't match then we stop!
989 elseif (!$row['is_or'])
990 {
991 $delete = false;
992 break;
993 }
994 }
995 if ($delete)
996 $deletes[$row['id_member']] = 1;
997 }
998 $smcFunc['db_free_result']($request);
999
1000 // Load the membergrounp message limits.
1001 //!!! Consider caching this?
1002 static $message_limit_cache = array();
1003 if (!allowedTo('moderate_forum') && empty($message_limit_cache))
1004 {
1005 $request = $smcFunc['db_query']('', '
1006 SELECT id_group, max_messages
1007 FROM {db_prefix}membergroups',
1008 array(
1009 )
1010 );
1011 while ($row = $smcFunc['db_fetch_assoc']($request))
1012 $message_limit_cache[$row['id_group']] = $row['max_messages'];
1013 $smcFunc['db_free_result']($request);
1014 }
1015
1016 // Load the groups that are allowed to read PMs.
1017 $allowed_groups = array();
1018 $disallowed_groups = array();
1019 $request = $smcFunc['db_query']('', '
1020 SELECT id_group, add_deny
1021 FROM {db_prefix}permissions
1022 WHERE permission = {string:read_permission}',
1023 array(
1024 'read_permission' => 'pm_read',
1025 )
1026 );
1027
1028 while ($row = $smcFunc['db_fetch_assoc']($request))
1029 {
1030 if (empty($row['add_deny']))
1031 $disallowed_groups[] = $row['id_group'];
1032 else
1033 $allowed_groups[] = $row['id_group'];
1034 }
1035
1036 $smcFunc['db_free_result']($request);
1037
1038 if (empty($modSettings['permission_enable_deny']))
1039 $disallowed_groups = array();
1040
1041 $request = $smcFunc['db_query']('', '
1042 SELECT
1043 member_name, real_name, id_member, email_address, lngfile,
1044 pm_email_notify, instant_messages,' . (allowedTo('moderate_forum') ? ' 0' : '
1045 (pm_receive_from = {int:admins_only}' . (empty($modSettings['enable_buddylist']) ? '' : ' OR
1046 (pm_receive_from = {int:buddies_only} AND FIND_IN_SET({string:from_id}, buddy_list) = 0) OR
1047 (pm_receive_from = {int:not_on_ignore_list} AND FIND_IN_SET({string:from_id}, pm_ignore_list) != 0)') . ')') . ' AS ignored,
1048 FIND_IN_SET({string:from_id}, buddy_list) != 0 AS is_buddy, is_activated,
1049 additional_groups, id_group, id_post_group
1050 FROM {db_prefix}members
1051 WHERE id_member IN ({array_int:recipients})
1052 ORDER BY lngfile
1053 LIMIT {int:count_recipients}',
1054 array(
1055 'not_on_ignore_list' => 1,
1056 'buddies_only' => 2,
1057 'admins_only' => 3,
1058 'recipients' => $all_to,
1059 'count_recipients' => count($all_to),
1060 'from_id' => $from['id'],
1061 )
1062 );
1063 $notifications = array();
1064 while ($row = $smcFunc['db_fetch_assoc']($request))
1065 {
1066 // Don't do anything for members to be deleted!
1067 if (isset($deletes[$row['id_member']]))
1068 continue;
1069
1070 // We need to know this members groups.
1071 $groups = explode(',', $row['additional_groups']);
1072 $groups[] = $row['id_group'];
1073 $groups[] = $row['id_post_group'];
1074
1075 $message_limit = -1;
1076 // For each group see whether they've gone over their limit - assuming they're not an admin.
1077 if (!in_array(1, $groups))
1078 {
1079 foreach ($groups as $id)
1080 {
1081 if (isset($message_limit_cache[$id]) && $message_limit != 0 && $message_limit < $message_limit_cache[$id])
1082 $message_limit = $message_limit_cache[$id];
1083 }
1084
1085 if ($message_limit > 0 && $message_limit <= $row['instant_messages'])
1086 {
1087 $log['failed'][$row['id_member']] = sprintf($txt['pm_error_data_limit_reached'], $row['real_name']);
1088 unset($all_to[array_search($row['id_member'], $all_to)]);
1089 continue;
1090 }
1091
1092 // Do they have any of the allowed groups?
1093 if (count(array_intersect($allowed_groups, $groups)) == 0 || count(array_intersect($disallowed_groups, $groups)) != 0)
1094 {
1095 $log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1096 unset($all_to[array_search($row['id_member'], $all_to)]);
1097 continue;
1098 }
1099 }
1100
1101 // Note that PostgreSQL can return a lowercase t/f for FIND_IN_SET
1102 if (!empty($row['ignored']) && $row['ignored'] != 'f' && $row['id_member'] != $from['id'])
1103 {
1104 $log['failed'][$row['id_member']] = sprintf($txt['pm_error_ignored_by_user'], $row['real_name']);
1105 unset($all_to[array_search($row['id_member'], $all_to)]);
1106 continue;
1107 }
1108
1109 // If the receiving account is banned (>=10) or pending deletion (4), refuse to send the PM.
1110 if ($row['is_activated'] >= 10 || ($row['is_activated'] == 4 && !$user_info['is_admin']))
1111 {
1112 $log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
1113 unset($all_to[array_search($row['id_member'], $all_to)]);
1114 continue;
1115 }
1116
1117 // Send a notification, if enabled - taking the buddy list into account.
1118 if (!empty($row['email_address']) && ($row['pm_email_notify'] == 1 || ($row['pm_email_notify'] > 1 && (!empty($modSettings['enable_buddylist']) && $row['is_buddy']))) && $row['is_activated'] == 1)
1119 $notifications[empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']][] = $row['email_address'];
1120
1121 $log['sent'][$row['id_member']] = sprintf(isset($txt['pm_successfully_sent']) ? $txt['pm_successfully_sent'] : '', $row['real_name']);
1122 }
1123 $smcFunc['db_free_result']($request);
1124
1125 // Only 'send' the message if there are any recipients left.
1126 if (empty($all_to))
1127 return $log;
1128
1129 // Insert the message itself and then grab the last insert id.
1130 $smcFunc['db_insert']('',
1131 '{db_prefix}personal_messages',
1132 array(
1133 'id_pm_head' => 'int', 'id_member_from' => 'int', 'deleted_by_sender' => 'int',
1134 'from_name' => 'string-255', 'msgtime' => 'int', 'subject' => 'string-255', 'body' => 'string-65534',
1135 ),
1136 array(
1137 $pm_head, $from['id'], ($store_outbox ? 0 : 1),
1138 $from['username'], time(), $htmlsubject, $htmlmessage,
1139 ),
1140 array('id_pm')
1141 );
1142 $id_pm = $smcFunc['db_insert_id']('{db_prefix}personal_messages', 'id_pm');
1143
1144 // Add the recipients.
1145 if (!empty($id_pm))
1146 {
1147 // If this is new we need to set it part of it's own conversation.
1148 if (empty($pm_head))
1149 $smcFunc['db_query']('', '
1150 UPDATE {db_prefix}personal_messages
1151 SET id_pm_head = {int:id_pm_head}
1152 WHERE id_pm = {int:id_pm_head}',
1153 array(
1154 'id_pm_head' => $id_pm,
1155 )
1156 );
1157
1158 // Some people think manually deleting personal_messages is fun... it's not. We protect against it though :)
1159 $smcFunc['db_query']('', '
1160 DELETE FROM {db_prefix}pm_recipients
1161 WHERE id_pm = {int:id_pm}',
1162 array(
1163 'id_pm' => $id_pm,
1164 )
1165 );
1166
1167 $insertRows = array();
1168 foreach ($all_to as $to)
1169 {
1170 $insertRows[] = array($id_pm, $to, in_array($to, $recipients['bcc']) ? 1 : 0, isset($deletes[$to]) ? 1 : 0, 1);
1171 }
1172
1173 $smcFunc['db_insert']('insert',
1174 '{db_prefix}pm_recipients',
1175 array(
1176 'id_pm' => 'int', 'id_member' => 'int', 'bcc' => 'int', 'deleted' => 'int', 'is_new' => 'int'
1177 ),
1178 $insertRows,
1179 array('id_pm', 'id_member')
1180 );
1181 }
1182
1183 censorText($message);
1184 censorText($subject);
1185 $message = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($smcFunc['htmlspecialchars']($message), false), array('<br />' => "\n", '</div>' => "\n", '</li>' => "\n", '[' => '[', ']' => ']')))));
1186
1187 foreach ($notifications as $lang => $notification_list)
1188 {
1189 // Make sure to use the right language.
1190 loadLanguage('index+PersonalMessage', $lang, false);
1191
1192 // Replace the right things in the message strings.
1193 $mailsubject = str_replace(array('SUBJECT', 'SENDER'), array($subject, un_htmlspecialchars($from['name'])), $txt['new_pm_subject']);
1194 $mailmessage = str_replace(array('SUBJECT', 'MESSAGE', 'SENDER'), array($subject, $message, un_htmlspecialchars($from['name'])), $txt['pm_email']);
1195 $mailmessage .= "\n\n" . $txt['instant_reply'] . ' ' . $scripturl . '?action=pm;sa=send;f=inbox;pmsg=' . $id_pm . ';quote;u=' . $from['id'];
1196
1197 // Off the notification email goes!
1198 sendmail($notification_list, $mailsubject, $mailmessage, null, 'p' . $id_pm, false, 2, null, true);
1199 }
1200
1201 // Back to what we were on before!
1202 loadLanguage('index+PersonalMessage');
1203
1204 // Add one to their unread and read message counts.
1205 foreach ($all_to as $k => $id)
1206 if (isset($deletes[$id]))
1207 unset($all_to[$k]);
1208 if (!empty($all_to))
1209 updateMemberData($all_to, array('instant_messages' => '+', 'unread_messages' => '+', 'new_pm' => 1));
1210
1211 return $log;
1212}
1213
1214// Prepare text strings for sending as email body or header.
1215function mimespecialchars($string, $with_charset = true, $hotmail_fix = false, $line_break = "\r\n", $custom_charset = null)
1216{
1217 global $context;
1218
1219 $charset = $custom_charset !== null ? $custom_charset : $context['character_set'];
1220
1221 // This is the fun part....
1222 if (preg_match_all('~&#(\d{3,8});~', $string, $matches) !== 0 && !$hotmail_fix)
1223 {
1224 // Let's, for now, assume there are only 'ish characters.
1225 $simple = true;
1226
1227 foreach ($matches[1] as $entity)
1228 if ($entity > 128)
1229 $simple = false;
1230 unset($matches);
1231
1232 if ($simple)
1233 $string = preg_replace_callback('~&#(\d{3,8});~', 'return_chr__preg_callback', $string);
1234 else
1235 {
1236 // Try to convert the string to UTF-8.
1237 if (!$context['utf8'] && function_exists('iconv'))
1238 {
1239 $newstring = @iconv($context['character_set'], 'UTF-8', $string);
1240 if ($newstring)
1241 $string = $newstring;
1242 }
1243
1244 $fixchar = create_function('$n', '
1245 if ($n < 128)
1246 return chr($n);
1247 elseif ($n < 2048)
1248 return chr(192 | $n >> 6) . chr(128 | $n & 63);
1249 elseif ($n < 65536)
1250 return chr(224 | $n >> 12) . chr(128 | $n >> 6 & 63) . chr(128 | $n & 63);
1251 else
1252 return chr(240 | $n >> 18) . chr(128 | $n >> 12 & 63) . chr(128 | $n >> 6 & 63) . chr(128 | $n & 63);');
1253
1254 $string = preg_replace_callback('~&#(\d{3,8});~', 'fixchar__callback', $string);
1255
1256 // Unicode, baby.
1257 $charset = 'UTF-8';
1258 }
1259 }
1260
1261 // Convert all special characters to HTML entities...just for Hotmail :-\
1262 if ($hotmail_fix && ($context['utf8'] || function_exists('iconv') || $context['character_set'] === 'ISO-8859-1'))
1263 {
1264 if (!$context['utf8'] && function_exists('iconv'))
1265 {
1266 $newstring = @iconv($context['character_set'], 'UTF-8', $string);
1267 if ($newstring)
1268 $string = $newstring;
1269 }
1270
1271 // Convert all 'special' characters to HTML entities.
1272 return array($charset, preg_replace_callback('~([\x80-\x{10FFFF}])~u', 'mime_convert__preg_callback', $string), '7bit');
1273 }
1274
1275 // We don't need to mess with the subject line if no special characters were in it..
1276 elseif (!$hotmail_fix && preg_match('~([^\x09\x0A\x0D\x20-\x7F])~', $string) === 1)
1277 {
1278 // Base64 encode.
1279 $string = base64_encode($string);
1280
1281 // Show the characterset and the transfer-encoding for header strings.
1282 if ($with_charset)
1283 $string = '=?' . $charset . '?B?' . $string . '?=';
1284
1285 // Break it up in lines (mail body).
1286 else
1287 $string = chunk_split($string, 76, $line_break);
1288
1289 return array($charset, $string, 'base64');
1290 }
1291
1292 else
1293 return array($charset, $string, '7bit');
1294}
1295
1296// Send an email via SMTP.
1297function smtp_mail($mail_to_array, $subject, $message, $headers)
1298{
1299 global $modSettings, $webmaster_email, $txt;
1300
1301 $modSettings['smtp_host'] = trim($modSettings['smtp_host']);
1302
1303 // Try POP3 before SMTP?
1304 // !!! There's no interface for this yet.
1305 if ($modSettings['mail_type'] == 2 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1306 {
1307 $socket = fsockopen($modSettings['smtp_host'], 110, $errno, $errstr, 2);
1308 if (!$socket && (substr($modSettings['smtp_host'], 0, 5) == 'smtp.' || substr($modSettings['smtp_host'], 0, 11) == 'ssl://smtp.'))
1309 $socket = fsockopen(strtr($modSettings['smtp_host'], array('smtp.' => 'pop.')), 110, $errno, $errstr, 2);
1310
1311 if ($socket)
1312 {
1313 fgets($socket, 256);
1314 fputs($socket, 'USER ' . $modSettings['smtp_username'] . "\r\n");
1315 fgets($socket, 256);
1316 fputs($socket, 'PASS ' . base64_decode($modSettings['smtp_password']) . "\r\n");
1317 fgets($socket, 256);
1318 fputs($socket, 'QUIT' . "\r\n");
1319
1320 fclose($socket);
1321 }
1322 }
1323
1324 // Try to connect to the SMTP server... if it doesn't exist, only wait three seconds.
1325 if (!$socket = fsockopen($modSettings['smtp_host'], empty($modSettings['smtp_port']) ? 25 : $modSettings['smtp_port'], $errno, $errstr, 3))
1326 {
1327 // Maybe we can still save this? The port might be wrong.
1328 if (substr($modSettings['smtp_host'], 0, 4) == 'ssl:' && (empty($modSettings['smtp_port']) || $modSettings['smtp_port'] == 25))
1329 {
1330 if ($socket = fsockopen($modSettings['smtp_host'], 465, $errno, $errstr, 3))
1331 log_error($txt['smtp_port_ssl']);
1332 }
1333
1334 // Unable to connect! Don't show any error message, but just log one and try to continue anyway.
1335 if (!$socket)
1336 {
1337 log_error($txt['smtp_no_connect'] . ': ' . $errno . ' : ' . $errstr);
1338 return false;
1339 }
1340 }
1341
1342 // Wait for a response of 220, without "-" continuer.
1343 if (!server_parse(null, $socket, '220'))
1344 return false;
1345
1346 if ($modSettings['mail_type'] == 1 && $modSettings['smtp_username'] != '' && $modSettings['smtp_password'] != '')
1347 {
1348 // !!! These should send the CURRENT server's name, not the mail server's!
1349
1350 // EHLO could be understood to mean encrypted hello...
1351// if (server_parse('EHLO ' . $modSettings['smtp_host'], $socket, null) == '250')
1352 if (server_parse('EHLO ' . 'localhost', $socket, null) == '250')
1353 {
1354 if (!server_parse('AUTH LOGIN', $socket, '334'))
1355 return false;
1356 // Send the username and password, encoded.
1357 if (!server_parse(base64_encode($modSettings['smtp_username']), $socket, '334'))
1358 return false;
1359 // The password is already encoded ;)
1360 if (!server_parse($modSettings['smtp_password'], $socket, '235'))
1361 return false;
1362 }
1363 elseif (!server_parse('HELO ' . $modSettings['smtp_host'], $socket, '250'))
1364 return false;
1365 }
1366 else
1367 {
1368 // Just say "helo".
1369 if (!server_parse('HELO ' . $modSettings['smtp_host'], $socket, '250'))
1370 return false;
1371 }
1372
1373 // Fix the message for any lines beginning with a period! (the first is ignored, you see.)
1374 $message = strtr($message, array("\r\n" . '.' => "\r\n" . '..'));
1375
1376 // !! Theoretically, we should be able to just loop the RCPT TO.
1377 $mail_to_array = array_values($mail_to_array);
1378 foreach ($mail_to_array as $i => $mail_to)
1379 {
1380 // Reset the connection to send another email.
1381 if ($i != 0)
1382 {
1383 if (!server_parse('RSET', $socket, '250'))
1384 return false;
1385 }
1386
1387 // From, to, and then start the data...
1388 if (!server_parse('MAIL FROM: <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>', $socket, '250'))
1389 return false;
1390 if (!server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250'))
1391 return false;
1392 if (!server_parse('DATA', $socket, '354'))
1393 return false;
1394 fputs($socket, 'Subject: ' . $subject . "\r\n");
1395 if (strlen($mail_to) > 0)
1396 fputs($socket, 'To: <' . $mail_to . '>' . "\r\n");
1397 fputs($socket, $headers . "\r\n\r\n");
1398 fputs($socket, $message . "\r\n");
1399
1400 // Send a ., or in other words "end of data".
1401 if (!server_parse('.', $socket, '250'))
1402 return false;
1403
1404 // Almost done, almost done... don't stop me just yet!
1405 @set_time_limit(300);
1406 if (function_exists('apache_reset_timeout'))
1407 @apache_reset_timeout();
1408 }
1409 fputs($socket, 'QUIT' . "\r\n");
1410 fclose($socket);
1411
1412 return true;
1413}
1414
1415// Parse a message to the SMTP server.
1416function server_parse($message, $socket, $response)
1417{
1418 global $txt;
1419
1420 if ($message !== null)
1421 fputs($socket, $message . "\r\n");
1422
1423 // No response yet.
1424 $server_response = '';
1425
1426 while (substr($server_response, 3, 1) != ' ')
1427 if (!($server_response = fgets($socket, 256)))
1428 {
1429 // !!! Change this message to reflect that it may mean bad user/password/server issues/etc.
1430 log_error($txt['smtp_bad_response']);
1431 return false;
1432 }
1433
1434 if ($response === null)
1435 return substr($server_response, 0, 3);
1436
1437 if (substr($server_response, 0, 3) != $response)
1438 {
1439 log_error($txt['smtp_error'] . $server_response);
1440 return false;
1441 }
1442
1443 return true;
1444}
1445
1446function SpellCheck()
1447{
1448 global $txt, $context, $smcFunc;
1449
1450 // A list of "words" we know about but pspell doesn't.
1451 $known_words = array('smf', 'php', 'mysql', 'www', 'gif', 'jpeg', 'png', 'http', 'smfisawesome', 'grandia', 'terranigma', 'rpgs');
1452
1453 loadLanguage('Post');
1454 loadTemplate('Post');
1455
1456 // Okay, this looks funny, but it actually fixes a weird bug.
1457 ob_start();
1458 $old = error_reporting(0);
1459
1460 // See, first, some windows machines don't load pspell properly on the first try. Dumb, but this is a workaround.
1461 pspell_new('en');
1462
1463 // Next, the dictionary in question may not exist. So, we try it... but...
1464 $pspell_link = pspell_new($txt['lang_dictionary'], $txt['lang_spelling'], '', strtr($context['character_set'], array('iso-' => 'iso', 'ISO-' => 'iso')), PSPELL_FAST | PSPELL_RUN_TOGETHER);
1465
1466 // Most people don't have anything but English installed... So we use English as a last resort.
1467 if (!$pspell_link)
1468 $pspell_link = pspell_new('en', '', '', '', PSPELL_FAST | PSPELL_RUN_TOGETHER);
1469
1470 error_reporting($old);
1471 ob_end_clean();
1472
1473 if (!isset($_POST['spellstring']) || !$pspell_link)
1474 die;
1475
1476 // Construct a bit of Javascript code.
1477 $context['spell_js'] = '
1478 var txt = {"done": "' . $txt['spellcheck_done'] . '"};
1479 var mispstr = window.opener.document.forms[spell_formname][spell_fieldname].value;
1480 var misps = Array(';
1481
1482 // Get all the words (Javascript already separated them).
1483 $alphas = explode("\n", strtr($_POST['spellstring'], array("\r" => '')));
1484
1485 $found_words = false;
1486 for ($i = 0, $n = count($alphas); $i < $n; $i++)
1487 {
1488 // Words are sent like 'word|offset_begin|offset_end'.
1489 $check_word = explode('|', $alphas[$i]);
1490
1491 // If the word is a known word, or spelled right...
1492 if (in_array($smcFunc['strtolower']($check_word[0]), $known_words) || pspell_check($pspell_link, $check_word[0]) || !isset($check_word[2]))
1493 continue;
1494
1495 // Find the word, and move up the "last occurance" to here.
1496 $found_words = true;
1497
1498 // Add on the javascript for this misspelling.
1499 $context['spell_js'] .= '
1500 new misp("' . strtr($check_word[0], array('\\' => '\\\\', '"' => '\\"', '<' => '', '>' => '')) . '", ' . (int) $check_word[1] . ', ' . (int) $check_word[2] . ', [';
1501
1502 // If there are suggestions, add them in...
1503 $suggestions = pspell_suggest($pspell_link, $check_word[0]);
1504 if (!empty($suggestions))
1505 {
1506 // But first check they aren't going to be censored - no naughty words!
1507 foreach ($suggestions as $k => $word)
1508 if ($suggestions[$k] != censorText($word))
1509 unset($suggestions[$k]);
1510
1511 if (!empty($suggestions))
1512 $context['spell_js'] .= '"' . implode('", "', $suggestions) . '"';
1513 }
1514
1515 $context['spell_js'] .= ']),';
1516 }
1517
1518 // If words were found, take off the last comma.
1519 if ($found_words)
1520 $context['spell_js'] = substr($context['spell_js'], 0, -1);
1521
1522 $context['spell_js'] .= '
1523 );';
1524
1525 // And instruct the template system to just show the spellcheck sub template.
1526 $context['template_layers'] = array();
1527 $context['sub_template'] = 'spellcheck';
1528}
1529
1530// Notify members that something has happened to a topic they marked!
1531function sendNotifications($topics, $type, $exclude = array(), $members_only = array())
1532{
1533 global $txt, $scripturl, $language, $user_info;
1534 global $modSettings, $sourcedir, $context, $smcFunc;
1535
1536 // Can't do it if there's no topics.
1537 if (empty($topics))
1538 return;
1539 // It must be an array - it must!
1540 if (!is_array($topics))
1541 $topics = array($topics);
1542
1543 // Get the subject and body...
1544 $result = $smcFunc['db_query']('', '
1545 SELECT mf.subject, ml.body, ml.id_member, t.id_last_msg, t.id_topic,
1546 IFNULL(mem.real_name, ml.poster_name) AS poster_name
1547 FROM {db_prefix}topics AS t
1548 INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1549 INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
1550 LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member)
1551 WHERE t.id_topic IN ({array_int:topic_list})
1552 LIMIT 1',
1553 array(
1554 'topic_list' => $topics,
1555 )
1556 );
1557 $topicData = array();
1558 while ($row = $smcFunc['db_fetch_assoc']($result))
1559 {
1560 // Clean it up.
1561 censorText($row['subject']);
1562 censorText($row['body']);
1563 $row['subject'] = un_htmlspecialchars($row['subject']);
1564 $row['body'] = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($row['body'], false, $row['id_last_msg']), array('<br />' => "\n", '</div>' => "\n", '</li>' => "\n", '[' => '[', ']' => ']')))));
1565
1566 $topicData[$row['id_topic']] = array(
1567 'subject' => $row['subject'],
1568 'body' => $row['body'],
1569 'last_id' => $row['id_last_msg'],
1570 'topic' => $row['id_topic'],
1571 'name' => $user_info['name'],
1572 'exclude' => '',
1573 );
1574 }
1575 $smcFunc['db_free_result']($result);
1576
1577 // Work out any exclusions...
1578 foreach ($topics as $key => $id)
1579 if (isset($topicData[$id]) && !empty($exclude[$key]))
1580 $topicData[$id]['exclude'] = (int) $exclude[$key];
1581
1582 // Nada?
1583 if (empty($topicData))
1584 trigger_error('sendNotifications(): topics not found', E_USER_NOTICE);
1585
1586 $topics = array_keys($topicData);
1587 // Just in case they've gone walkies.
1588 if (empty($topics))
1589 return;
1590
1591 // Insert all of these items into the digest log for those who want notifications later.
1592 $digest_insert = array();
1593 foreach ($topicData as $id => $data)
1594 $digest_insert[] = array($data['topic'], $data['last_id'], $type, (int) $data['exclude']);
1595 $smcFunc['db_insert']('',
1596 '{db_prefix}log_digest',
1597 array(
1598 'id_topic' => 'int', 'id_msg' => 'int', 'note_type' => 'string', 'exclude' => 'int',
1599 ),
1600 $digest_insert,
1601 array()
1602 );
1603
1604 // Find the members with notification on for this topic.
1605 $members = $smcFunc['db_query']('', '
1606 SELECT
1607 mem.id_member, mem.email_address, mem.notify_regularity, mem.notify_types, mem.notify_send_body, mem.lngfile,
1608 ln.sent, mem.id_group, mem.additional_groups, b.member_groups, mem.id_post_group, t.id_member_started,
1609 ln.id_topic
1610 FROM {db_prefix}log_notify AS ln
1611 INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ln.id_member)
1612 INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)
1613 INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
1614 WHERE ln.id_topic IN ({array_int:topic_list})
1615 AND mem.notify_types < {int:notify_types}
1616 AND mem.notify_regularity < {int:notify_regularity}
1617 AND mem.is_activated = {int:is_activated}
1618 AND ln.id_member != {int:current_member}' .
1619 (empty($members_only) ? '' : ' AND ln.id_member IN ({array_int:members_only})') . '
1620 ORDER BY mem.lngfile',
1621 array(
1622 'current_member' => $user_info['id'],
1623 'topic_list' => $topics,
1624 'notify_types' => $type == 'reply' ? '4' : '3',
1625 'notify_regularity' => 2,
1626 'is_activated' => 1,
1627 'members_only' => is_array($members_only) ? $members_only : array($members_only),
1628 )
1629 );
1630 $sent = 0;
1631 while ($row = $smcFunc['db_fetch_assoc']($members))
1632 {
1633 // Don't do the excluded...
1634 if ($topicData[$row['id_topic']]['exclude'] == $row['id_member'])
1635 continue;
1636
1637 // Easier to check this here... if they aren't the topic poster do they really want to know?
1638 if ($type != 'reply' && $row['notify_types'] == 2 && $row['id_member'] != $row['id_member_started'])
1639 continue;
1640
1641 if ($row['id_group'] != 1)
1642 {
1643 $allowed = explode(',', $row['member_groups']);
1644 $row['additional_groups'] = explode(',', $row['additional_groups']);
1645 $row['additional_groups'][] = $row['id_group'];
1646 $row['additional_groups'][] = $row['id_post_group'];
1647
1648 if (count(array_intersect($allowed, $row['additional_groups'])) == 0)
1649 continue;
1650 }
1651
1652 $needed_language = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
1653 if (empty($current_language) || $current_language != $needed_language)
1654 $current_language = loadLanguage('Post', $needed_language, false);
1655
1656 $message_type = 'notification_' . $type;
1657 $replacements = array(
1658 'TOPICSUBJECT' => $topicData[$row['id_topic']]['subject'],
1659 'POSTERNAME' => un_htmlspecialchars($topicData[$row['id_topic']]['name']),
1660 'TOPICLINK' => $scripturl . '?topic=' . $row['id_topic'] . '.new;topicseen#new',
1661 'UNSUBSCRIBELINK' => $scripturl . '?action=notify;topic=' . $row['id_topic'] . '.0',
1662 );
1663
1664 if ($type == 'remove')
1665 unset($replacements['TOPICLINK'], $replacements['UNSUBSCRIBELINK']);
1666 // Do they want the body of the message sent too?
1667 if (!empty($row['notify_send_body']) && $type == 'reply' && empty($modSettings['disallow_sendBody']))
1668 {
1669 $message_type .= '_body';
1670 $replacements['MESSAGE'] = $topicData[$row['id_topic']]['body'];
1671 }
1672 if (!empty($row['notify_regularity']) && $type == 'reply')
1673 $message_type .= '_once';
1674
1675 // Send only if once is off or it's on and it hasn't been sent.
1676 if ($type != 'reply' || empty($row['notify_regularity']) || empty($row['sent']))
1677 {
1678 $emaildata = loadEmailTemplate($message_type, $replacements, $needed_language);
1679 sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $topicData[$row['id_topic']]['last_id']);
1680 $sent++;
1681 }
1682 }
1683 $smcFunc['db_free_result']($members);
1684
1685 if (isset($current_language) && $current_language != $user_info['language'])
1686 loadLanguage('Post');
1687
1688 // Sent!
1689 if ($type == 'reply' && !empty($sent))
1690 $smcFunc['db_query']('', '
1691 UPDATE {db_prefix}log_notify
1692 SET sent = {int:is_sent}
1693 WHERE id_topic IN ({array_int:topic_list})
1694 AND id_member != {int:current_member}',
1695 array(
1696 'current_member' => $user_info['id'],
1697 'topic_list' => $topics,
1698 'is_sent' => 1,
1699 )
1700 );
1701
1702 // For approvals we need to unsend the exclusions (This *is* the quickest way!)
1703 if (!empty($sent) && !empty($exclude))
1704 {
1705 foreach ($topicData as $id => $data)
1706 if ($data['exclude'])
1707 $smcFunc['db_query']('', '
1708 UPDATE {db_prefix}log_notify
1709 SET sent = {int:not_sent}
1710 WHERE id_topic = {int:id_topic}
1711 AND id_member = {int:id_member}',
1712 array(
1713 'not_sent' => 0,
1714 'id_topic' => $id,
1715 'id_member' => $data['exclude'],
1716 )
1717 );
1718 }
1719}
1720
1721// Create a post, either as new topic (id_topic = 0) or in an existing one.
1722// The input parameters of this function assume:
1723// - Strings have been escaped.
1724// - Integers have been cast to integer.
1725// - Mandatory parameters are set.
1726function createPost(&$msgOptions, &$topicOptions, &$posterOptions)
1727{
1728 global $user_info, $txt, $modSettings, $smcFunc, $context;
1729
1730 // Set optional parameters to the default value.
1731 $msgOptions['icon'] = empty($msgOptions['icon']) ? 'xx' : $msgOptions['icon'];
1732 $msgOptions['smileys_enabled'] = !empty($msgOptions['smileys_enabled']);
1733 $msgOptions['attachments'] = empty($msgOptions['attachments']) ? array() : $msgOptions['attachments'];
1734 $msgOptions['approved'] = isset($msgOptions['approved']) ? (int) $msgOptions['approved'] : 1;
1735 $topicOptions['id'] = empty($topicOptions['id']) ? 0 : (int) $topicOptions['id'];
1736 $topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
1737 $topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
1738 $topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
1739 $posterOptions['id'] = empty($posterOptions['id']) ? 0 : (int) $posterOptions['id'];
1740 $posterOptions['ip'] = empty($posterOptions['ip']) ? $user_info['ip'] : $posterOptions['ip'];
1741
1742 // We need to know if the topic is approved. If we're told that's great - if not find out.
1743 if (!$modSettings['postmod_active'])
1744 $topicOptions['is_approved'] = true;
1745 elseif (!empty($topicOptions['id']) && !isset($topicOptions['is_approved']))
1746 {
1747 $request = $smcFunc['db_query']('', '
1748 SELECT approved
1749 FROM {db_prefix}topics
1750 WHERE id_topic = {int:id_topic}
1751 LIMIT 1',
1752 array(
1753 'id_topic' => $topicOptions['id'],
1754 )
1755 );
1756 list ($topicOptions['is_approved']) = $smcFunc['db_fetch_row']($request);
1757 $smcFunc['db_free_result']($request);
1758 }
1759
1760 // If nothing was filled in as name/e-mail address, try the member table.
1761 if (!isset($posterOptions['name']) || $posterOptions['name'] == '' || (empty($posterOptions['email']) && !empty($posterOptions['id'])))
1762 {
1763 if (empty($posterOptions['id']))
1764 {
1765 $posterOptions['id'] = 0;
1766 $posterOptions['name'] = $txt['guest_title'];
1767 $posterOptions['email'] = '';
1768 }
1769 elseif ($posterOptions['id'] != $user_info['id'])
1770 {
1771 $request = $smcFunc['db_query']('', '
1772 SELECT member_name, email_address
1773 FROM {db_prefix}members
1774 WHERE id_member = {int:id_member}
1775 LIMIT 1',
1776 array(
1777 'id_member' => $posterOptions['id'],
1778 )
1779 );
1780 // Couldn't find the current poster?
1781 if ($smcFunc['db_num_rows']($request) == 0)
1782 {
1783 trigger_error('createPost(): Invalid member id ' . $posterOptions['id'], E_USER_NOTICE);
1784 $posterOptions['id'] = 0;
1785 $posterOptions['name'] = $txt['guest_title'];
1786 $posterOptions['email'] = '';
1787 }
1788 else
1789 list ($posterOptions['name'], $posterOptions['email']) = $smcFunc['db_fetch_row']($request);
1790 $smcFunc['db_free_result']($request);
1791 }
1792 else
1793 {
1794 $posterOptions['name'] = $user_info['name'];
1795 $posterOptions['email'] = $user_info['email'];
1796 }
1797 }
1798
1799 // It's do or die time: forget any user aborts!
1800 $previous_ignore_user_abort = ignore_user_abort(true);
1801
1802 $new_topic = empty($topicOptions['id']);
1803
1804 // Insert the post.
1805 $smcFunc['db_insert']('',
1806 '{db_prefix}messages',
1807 array(
1808 'id_board' => 'int', 'id_topic' => 'int', 'id_member' => 'int', 'subject' => 'string-255', 'body' => (!empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] > 65534 ? 'string-' . $modSettings['max_messageLength'] : 'string-65534'),
1809 'poster_name' => 'string-255', 'poster_email' => 'string-255', 'poster_time' => 'int', 'poster_ip' => 'string-255',
1810 'smileys_enabled' => 'int', 'modified_name' => 'string', 'icon' => 'string-16', 'approved' => 'int',
1811 ),
1812 array(
1813 $topicOptions['board'], $topicOptions['id'], $posterOptions['id'], $msgOptions['subject'], $msgOptions['body'],
1814 $posterOptions['name'], $posterOptions['email'], time(), $posterOptions['ip'],
1815 $msgOptions['smileys_enabled'] ? 1 : 0, '', $msgOptions['icon'], $msgOptions['approved'],
1816 ),
1817 array('id_msg')
1818 );
1819 $msgOptions['id'] = $smcFunc['db_insert_id']('{db_prefix}messages', 'id_msg');
1820
1821 // Something went wrong creating the message...
1822 if (empty($msgOptions['id']))
1823 return false;
1824
1825 // Fix the attachments.
1826 if (!empty($msgOptions['attachments']))
1827 $smcFunc['db_query']('', '
1828 UPDATE {db_prefix}attachments
1829 SET id_msg = {int:id_msg}
1830 WHERE id_attach IN ({array_int:attachment_list})',
1831 array(
1832 'attachment_list' => $msgOptions['attachments'],
1833 'id_msg' => $msgOptions['id'],
1834 )
1835 );
1836
1837 // Insert a new topic (if the topicID was left empty.)
1838 if ($new_topic)
1839 {
1840 $smcFunc['db_insert']('',
1841 '{db_prefix}topics',
1842 array(
1843 'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int',
1844 'id_last_msg' => 'int', 'locked' => 'int', 'is_sticky' => 'int', 'num_views' => 'int',
1845 'id_poll' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int',
1846 ),
1847 array(
1848 $topicOptions['board'], $posterOptions['id'], $posterOptions['id'], $msgOptions['id'],
1849 $msgOptions['id'], $topicOptions['lock_mode'] === null ? 0 : $topicOptions['lock_mode'], $topicOptions['sticky_mode'] === null ? 0 : $topicOptions['sticky_mode'], 0,
1850 $topicOptions['poll'] === null ? 0 : $topicOptions['poll'], $msgOptions['approved'] ? 0 : 1, $msgOptions['approved'],
1851 ),
1852 array('id_topic')
1853 );
1854 $topicOptions['id'] = $smcFunc['db_insert_id']('{db_prefix}topics', 'id_topic');
1855
1856 // The topic couldn't be created for some reason.
1857 if (empty($topicOptions['id']))
1858 {
1859 // We should delete the post that did work, though...
1860 $smcFunc['db_query']('', '
1861 DELETE FROM {db_prefix}messages
1862 WHERE id_msg = {int:id_msg}',
1863 array(
1864 'id_msg' => $msgOptions['id'],
1865 )
1866 );
1867
1868 return false;
1869 }
1870
1871 // Fix the message with the topic.
1872 $smcFunc['db_query']('', '
1873 UPDATE {db_prefix}messages
1874 SET id_topic = {int:id_topic}
1875 WHERE id_msg = {int:id_msg}',
1876 array(
1877 'id_topic' => $topicOptions['id'],
1878 'id_msg' => $msgOptions['id'],
1879 )
1880 );
1881
1882 // There's been a new topic AND a new post today.
1883 trackStats(array('topics' => '+', 'posts' => '+'));
1884
1885 updateStats('topic', true);
1886 updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
1887
1888 // What if we want to export new topics out to a CMS?
1889 call_integration_hook('integrate_create_topic', array($msgOptions, $topicOptions, $posterOptions));
1890 }
1891 // The topic already exists, it only needs a little updating.
1892 else
1893 {
1894 $countChange = $msgOptions['approved'] ? 'num_replies = num_replies + 1' : 'unapproved_posts = unapproved_posts + 1';
1895
1896 // Update the number of replies and the lock/sticky status.
1897 $smcFunc['db_query']('', '
1898 UPDATE {db_prefix}topics
1899 SET
1900 ' . ($msgOptions['approved'] ? 'id_member_updated = {int:poster_id}, id_last_msg = {int:id_msg},' : '') . '
1901 ' . $countChange . ($topicOptions['lock_mode'] === null ? '' : ',
1902 locked = {int:locked}') . ($topicOptions['sticky_mode'] === null ? '' : ',
1903 is_sticky = {int:is_sticky}') . '
1904 WHERE id_topic = {int:id_topic}',
1905 array(
1906 'poster_id' => $posterOptions['id'],
1907 'id_msg' => $msgOptions['id'],
1908 'locked' => $topicOptions['lock_mode'],
1909 'is_sticky' => $topicOptions['sticky_mode'],
1910 'id_topic' => $topicOptions['id'],
1911 )
1912 );
1913
1914 // One new post has been added today.
1915 trackStats(array('posts' => '+'));
1916 }
1917
1918 // Creating is modifying...in a way.
1919 //!!! Why not set id_msg_modified on the insert?
1920 $smcFunc['db_query']('', '
1921 UPDATE {db_prefix}messages
1922 SET id_msg_modified = {int:id_msg}
1923 WHERE id_msg = {int:id_msg}',
1924 array(
1925 'id_msg' => $msgOptions['id'],
1926 )
1927 );
1928
1929 // Increase the number of posts and topics on the board.
1930 if ($msgOptions['approved'])
1931 $smcFunc['db_query']('', '
1932 UPDATE {db_prefix}boards
1933 SET num_posts = num_posts + 1' . ($new_topic ? ', num_topics = num_topics + 1' : '') . '
1934 WHERE id_board = {int:id_board}',
1935 array(
1936 'id_board' => $topicOptions['board'],
1937 )
1938 );
1939 else
1940 {
1941 $smcFunc['db_query']('', '
1942 UPDATE {db_prefix}boards
1943 SET unapproved_posts = unapproved_posts + 1' . ($new_topic ? ', unapproved_topics = unapproved_topics + 1' : '') . '
1944 WHERE id_board = {int:id_board}',
1945 array(
1946 'id_board' => $topicOptions['board'],
1947 )
1948 );
1949
1950 // Add to the approval queue too.
1951 $smcFunc['db_insert']('',
1952 '{db_prefix}approval_queue',
1953 array(
1954 'id_msg' => 'int',
1955 ),
1956 array(
1957 $msgOptions['id'],
1958 ),
1959 array()
1960 );
1961 }
1962
1963 // Mark inserted topic as read (only for the user calling this function).
1964 if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
1965 {
1966 // Since it's likely they *read* it before replying, let's try an UPDATE first.
1967 if (!$new_topic)
1968 {
1969 $smcFunc['db_query']('', '
1970 UPDATE {db_prefix}log_topics
1971 SET id_msg = {int:id_msg}
1972 WHERE id_member = {int:current_member}
1973 AND id_topic = {int:id_topic}',
1974 array(
1975 'current_member' => $posterOptions['id'],
1976 'id_msg' => $msgOptions['id'],
1977 'id_topic' => $topicOptions['id'],
1978 )
1979 );
1980
1981 $flag = $smcFunc['db_affected_rows']() != 0;
1982 }
1983
1984 if (empty($flag))
1985 {
1986 $smcFunc['db_insert']('ignore',
1987 '{db_prefix}log_topics',
1988 array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
1989 array($topicOptions['id'], $posterOptions['id'], $msgOptions['id']),
1990 array('id_topic', 'id_member')
1991 );
1992 }
1993 }
1994
1995 // If there's a custom search index, it needs updating...
1996 if (!empty($modSettings['search_custom_index_config']))
1997 {
1998 $customIndexSettings = safe_unserialize($modSettings['search_custom_index_config']);
1999
2000 $inserts = array();
2001 foreach (text2words($msgOptions['body'], $customIndexSettings['bytes_per_word'], true) as $word)
2002 $inserts[] = array($word, $msgOptions['id']);
2003
2004 if (!empty($inserts))
2005 $smcFunc['db_insert']('ignore',
2006 '{db_prefix}log_search_words',
2007 array('id_word' => 'int', 'id_msg' => 'int'),
2008 $inserts,
2009 array('id_word', 'id_msg')
2010 );
2011 }
2012
2013 // Increase the post counter for the user that created the post.
2014 if (!empty($posterOptions['update_post_count']) && !empty($posterOptions['id']) && $msgOptions['approved'])
2015 {
2016 // Are you the one that happened to create this post?
2017 if ($user_info['id'] == $posterOptions['id'])
2018 $user_info['posts']++;
2019 updateMemberData($posterOptions['id'], array('posts' => '+'));
2020 }
2021
2022 // They've posted, so they can make the view count go up one if they really want. (this is to keep views >= replies...)
2023 $_SESSION['last_read_topic'] = 0;
2024
2025 // Better safe than sorry.
2026 if (isset($_SESSION['topicseen_cache'][$topicOptions['board']]))
2027 $_SESSION['topicseen_cache'][$topicOptions['board']]--;
2028
2029 // Update all the stats so everyone knows about this new topic and message.
2030 updateStats('message', true, $msgOptions['id']);
2031
2032 // Update the last message on the board assuming it's approved AND the topic is.
2033 if ($msgOptions['approved'])
2034 updateLastMessages($topicOptions['board'], $new_topic || !empty($topicOptions['is_approved']) ? $msgOptions['id'] : 0);
2035
2036 // Alright, done now... we can abort now, I guess... at least this much is done.
2037 ignore_user_abort($previous_ignore_user_abort);
2038
2039 // Success.
2040 return true;
2041}
2042
2043// !!!
2044function createAttachment(&$attachmentOptions)
2045{
2046 global $modSettings, $sourcedir, $smcFunc, $context;
2047
2048 require_once($sourcedir . '/Subs-Graphics.php');
2049
2050 // We need to know where this thing is going.
2051 if (!empty($modSettings['currentAttachmentUploadDir']))
2052 {
2053 if (!is_array($modSettings['attachmentUploadDir']))
2054 $modSettings['attachmentUploadDir'] = safe_unserialize($modSettings['attachmentUploadDir']);
2055
2056 // Just use the current path for temp files.
2057 $attach_dir = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
2058 $id_folder = $modSettings['currentAttachmentUploadDir'];
2059 }
2060 else
2061 {
2062 $attach_dir = $modSettings['attachmentUploadDir'];
2063 $id_folder = 1;
2064 }
2065
2066 $attachmentOptions['errors'] = array();
2067 if (!isset($attachmentOptions['post']))
2068 $attachmentOptions['post'] = 0;
2069 if (!isset($attachmentOptions['approved']))
2070 $attachmentOptions['approved'] = 1;
2071
2072 $already_uploaded = preg_match('~^post_tmp_' . $attachmentOptions['poster'] . '_\d+$~', $attachmentOptions['tmp_name']) != 0;
2073 $file_restricted = @ini_get('open_basedir') != '' && !$already_uploaded;
2074
2075 if ($already_uploaded)
2076 $attachmentOptions['tmp_name'] = $attach_dir . '/' . $attachmentOptions['tmp_name'];
2077
2078 // Make sure the file actually exists... sometimes it doesn't.
2079 if ((!$file_restricted && !file_exists($attachmentOptions['tmp_name'])) || (!$already_uploaded && !is_uploaded_file($attachmentOptions['tmp_name'])))
2080 {
2081 $attachmentOptions['errors'] = array('could_not_upload');
2082 return false;
2083 }
2084
2085 // These are the only valid image types for SMF.
2086 $validImageTypes = array(
2087 1 => 'gif',
2088 2 => 'jpeg',
2089 3 => 'png',
2090 5 => 'psd',
2091 6 => 'bmp',
2092 7 => 'tiff',
2093 8 => 'tiff',
2094 9 => 'jpeg',
2095 14 => 'iff'
2096 );
2097
2098 if (!$file_restricted || $already_uploaded)
2099 {
2100 $size = @getimagesize($attachmentOptions['tmp_name']);
2101 list ($attachmentOptions['width'], $attachmentOptions['height']) = $size;
2102
2103 // If it's an image get the mime type right.
2104 if (empty($attachmentOptions['mime_type']) && $attachmentOptions['width'])
2105 {
2106 // Got a proper mime type?
2107 if (!empty($size['mime']))
2108 $attachmentOptions['mime_type'] = $size['mime'];
2109 // Otherwise a valid one?
2110 elseif (isset($validImageTypes[$size[2]]))
2111 $attachmentOptions['mime_type'] = 'image/' . $validImageTypes[$size[2]];
2112 }
2113 }
2114
2115 // It is possible we might have a MIME type that isn't actually an image but still have a size.
2116 // For example, Shockwave files will be able to return size but be 'application/shockwave' or similar.
2117 if (!empty($attachmentOptions['mime_type']) && strpos($attachmentOptions['mime_type'], 'image/') !== 0)
2118 {
2119 $attachmentOptions['width'] = 0;
2120 $attachmentOptions['height'] = 0;
2121 }
2122
2123 // Get the hash if no hash has been given yet.
2124 if (empty($attachmentOptions['file_hash']))
2125 $attachmentOptions['file_hash'] = getAttachmentFilename($attachmentOptions['name'], false, null, true);
2126
2127 // Is the file too big?
2128 if (!empty($modSettings['attachmentSizeLimit']) && $attachmentOptions['size'] > $modSettings['attachmentSizeLimit'] * 1024)
2129 $attachmentOptions['errors'][] = 'too_large';
2130
2131 if (!empty($modSettings['attachmentCheckExtensions']))
2132 {
2133 $allowed = explode(',', strtolower($modSettings['attachmentExtensions']));
2134 foreach ($allowed as $k => $dummy)
2135 $allowed[$k] = trim($dummy);
2136
2137 if (!in_array(strtolower(substr(strrchr($attachmentOptions['name'], '.'), 1)), $allowed))
2138 $attachmentOptions['errors'][] = 'bad_extension';
2139 }
2140
2141 if (!empty($modSettings['attachmentDirSizeLimit']))
2142 {
2143 // Make sure the directory isn't full.
2144 $dirSize = 0;
2145 $dir = @opendir($attach_dir) or fatal_lang_error('cant_access_upload_path', 'critical');
2146 while ($file = readdir($dir))
2147 {
2148 if ($file == '.' || $file == '..')
2149 continue;
2150
2151 if (preg_match('~^post_tmp_\d+_\d+$~', $file) != 0)
2152 {
2153 // Temp file is more than 5 hours old!
2154 if (filemtime($attach_dir . '/' . $file) < time() - 18000)
2155 @unlink($attach_dir . '/' . $file);
2156 continue;
2157 }
2158
2159 $dirSize += filesize($attach_dir . '/' . $file);
2160 }
2161 closedir($dir);
2162
2163 // Too big! Maybe you could zip it or something...
2164 if ($attachmentOptions['size'] + $dirSize > $modSettings['attachmentDirSizeLimit'] * 1024)
2165 $attachmentOptions['errors'][] = 'directory_full';
2166 // Soon to be too big - warn the admins...
2167 elseif (!isset($modSettings['attachment_full_notified']) && $modSettings['attachmentDirSizeLimit'] > 4000 && $attachmentOptions['size'] + $dirSize > ($modSettings['attachmentDirSizeLimit'] - 2000) * 1024)
2168 {
2169 require_once($sourcedir . '/Subs-Admin.php');
2170 emailAdmins('admin_attachments_full');
2171 updateSettings(array('attachment_full_notified' => 1));
2172 }
2173 }
2174
2175 // Check if the file already exists.... (for those who do not encrypt their filenames...)
2176 if (empty($modSettings['attachmentEncryptFilenames']))
2177 {
2178 // Make sure they aren't trying to upload a nasty file.
2179 $disabledFiles = array('con', 'com1', 'com2', 'com3', 'com4', 'prn', 'aux', 'lpt1', '.htaccess', 'index.php');
2180 if (in_array(strtolower(basename($attachmentOptions['name'])), $disabledFiles))
2181 $attachmentOptions['errors'][] = 'bad_filename';
2182
2183 // Check if there's another file with that name...
2184 $request = $smcFunc['db_query']('', '
2185 SELECT id_attach
2186 FROM {db_prefix}attachments
2187 WHERE filename = {string:filename}
2188 LIMIT 1',
2189 array(
2190 'filename' => strtolower($attachmentOptions['name']),
2191 )
2192 );
2193 if ($smcFunc['db_num_rows']($request) > 0)
2194 $attachmentOptions['errors'][] = 'taken_filename';
2195 $smcFunc['db_free_result']($request);
2196 }
2197
2198 if (!empty($attachmentOptions['errors']))
2199 return false;
2200
2201 if (!is_writable($attach_dir))
2202 fatal_lang_error('attachments_no_write', 'critical');
2203
2204 // Assuming no-one set the extension let's take a look at it.
2205 if (empty($attachmentOptions['fileext']))
2206 {
2207 $attachmentOptions['fileext'] = strtolower(strrpos($attachmentOptions['name'], '.') !== false ? substr($attachmentOptions['name'], strrpos($attachmentOptions['name'], '.') + 1) : '');
2208 if (strlen($attachmentOptions['fileext']) > 8 || '.' . $attachmentOptions['fileext'] == $attachmentOptions['name'])
2209 $attachmentOptions['fileext'] = '';
2210 }
2211
2212 $smcFunc['db_insert']('',
2213 '{db_prefix}attachments',
2214 array(
2215 'id_folder' => 'int', 'id_msg' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
2216 'size' => 'int', 'width' => 'int', 'height' => 'int',
2217 'mime_type' => 'string-20', 'approved' => 'int',
2218 ),
2219 array(
2220 $id_folder, (int) $attachmentOptions['post'], $attachmentOptions['name'], $attachmentOptions['file_hash'], $attachmentOptions['fileext'],
2221 (int) $attachmentOptions['size'], (empty($attachmentOptions['width']) ? 0 : (int) $attachmentOptions['width']), (empty($attachmentOptions['height']) ? '0' : (int) $attachmentOptions['height']),
2222 (!empty($attachmentOptions['mime_type']) ? $attachmentOptions['mime_type'] : ''), (int) $attachmentOptions['approved'],
2223 ),
2224 array('id_attach')
2225 );
2226 $attachmentOptions['id'] = $smcFunc['db_insert_id']('{db_prefix}attachments', 'id_attach');
2227
2228 if (empty($attachmentOptions['id']))
2229 return false;
2230
2231 // If it's not approved add to the approval queue.
2232 if (!$attachmentOptions['approved'])
2233 $smcFunc['db_insert']('',
2234 '{db_prefix}approval_queue',
2235 array(
2236 'id_attach' => 'int', 'id_msg' => 'int',
2237 ),
2238 array(
2239 $attachmentOptions['id'], (int) $attachmentOptions['post'],
2240 ),
2241 array()
2242 );
2243
2244 $attachmentOptions['destination'] = getAttachmentFilename(basename($attachmentOptions['name']), $attachmentOptions['id'], $id_folder, false, $attachmentOptions['file_hash']);
2245
2246 if ($already_uploaded)
2247 rename($attachmentOptions['tmp_name'], $attachmentOptions['destination']);
2248 elseif (!move_uploaded_file($attachmentOptions['tmp_name'], $attachmentOptions['destination']))
2249 fatal_lang_error('attach_timeout', 'critical');
2250
2251 // Attempt to chmod it.
2252 @chmod($attachmentOptions['destination'], 0644);
2253
2254 $size = @getimagesize($attachmentOptions['destination']);
2255 list ($attachmentOptions['width'], $attachmentOptions['height']) = empty($size) ? array(null, null, null) : $size;
2256
2257 // We couldn't access the file before...
2258 if ($file_restricted)
2259 {
2260 // Have a go at getting the right mime type.
2261 if (empty($attachmentOptions['mime_type']) && $attachmentOptions['width'])
2262 {
2263 if (!empty($size['mime']))
2264 $attachmentOptions['mime_type'] = $size['mime'];
2265 elseif (isset($validImageTypes[$size[2]]))
2266 $attachmentOptions['mime_type'] = 'image/' . $validImageTypes[$size[2]];
2267 }
2268
2269 // It is possible we might have a MIME type that isn't actually an image but still have a size.
2270 // For example, Shockwave files will be able to return size but be 'application/shockwave' or similar.
2271 if (!empty($attachmentOptions['mime_type']) && strpos($attachmentOptions['mime_type'], 'image/') !== 0)
2272 {
2273 $attachmentOptions['width'] = 0;
2274 $attachmentOptions['height'] = 0;
2275 }
2276
2277 if (!empty($attachmentOptions['width']) && !empty($attachmentOptions['height']))
2278 $smcFunc['db_query']('', '
2279 UPDATE {db_prefix}attachments
2280 SET
2281 width = {int:width},
2282 height = {int:height},
2283 mime_type = {string:mime_type}
2284 WHERE id_attach = {int:id_attach}',
2285 array(
2286 'width' => (int) $attachmentOptions['width'],
2287 'height' => (int) $attachmentOptions['height'],
2288 'id_attach' => $attachmentOptions['id'],
2289 'mime_type' => empty($attachmentOptions['mime_type']) ? '' : $attachmentOptions['mime_type'],
2290 )
2291 );
2292 }
2293
2294 // Security checks for images
2295 // Do we have an image? If yes, we need to check it out!
2296 if (isset($validImageTypes[$size[2]]))
2297 {
2298 if (!checkImageContents($attachmentOptions['destination'], !empty($modSettings['attachment_image_paranoid'])))
2299 {
2300 // It's bad. Last chance, maybe we can re-encode it?
2301 if (empty($modSettings['attachment_image_reencode']) || (!reencodeImage($attachmentOptions['destination'], $size[2])))
2302 {
2303 // Nothing to do: not allowed or not successful re-encoding it.
2304 require_once($sourcedir . '/ManageAttachments.php');
2305 removeAttachments(array(
2306 'id_attach' => $attachmentOptions['id']
2307 ));
2308 $attachmentOptions['id'] = null;
2309 $attachmentOptions['errors'][] = 'bad_attachment';
2310
2311 return false;
2312 }
2313 // Success! However, successes usually come for a price:
2314 // we might get a new format for our image...
2315 $old_format = $size[2];
2316 $size = @getimagesize($attachmentOptions['destination']);
2317 if (!(empty($size)) && ($size[2] != $old_format))
2318 {
2319 // Let's update the image information
2320 // !!! This is becoming a mess: we keep coming back and update the database,
2321 // instead of getting it right the first time.
2322 if (isset($validImageTypes[$size[2]]))
2323 {
2324 $attachmentOptions['mime_type'] = 'image/' . $validImageTypes[$size[2]];
2325 $smcFunc['db_query']('', '
2326 UPDATE {db_prefix}attachments
2327 SET
2328 mime_type = {string:mime_type}
2329 WHERE id_attach = {int:id_attach}',
2330 array(
2331 'id_attach' => $attachmentOptions['id'],
2332 'mime_type' => $attachmentOptions['mime_type'],
2333 )
2334 );
2335 }
2336 }
2337 }
2338 }
2339
2340 if (!empty($attachmentOptions['skip_thumbnail']) || (empty($attachmentOptions['width']) && empty($attachmentOptions['height'])))
2341 return true;
2342
2343 // Like thumbnails, do we?
2344 if (!empty($modSettings['attachmentThumbnails']) && !empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight']) && ($attachmentOptions['width'] > $modSettings['attachmentThumbWidth'] || $attachmentOptions['height'] > $modSettings['attachmentThumbHeight']))
2345 {
2346 if (createThumbnail($attachmentOptions['destination'], $modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight']))
2347 {
2348 // Figure out how big we actually made it.
2349 $size = @getimagesize($attachmentOptions['destination'] . '_thumb');
2350 list ($thumb_width, $thumb_height) = $size;
2351
2352 if (!empty($size['mime']))
2353 $thumb_mime = $size['mime'];
2354 elseif (isset($validImageTypes[$size[2]]))
2355 $thumb_mime = 'image/' . $validImageTypes[$size[2]];
2356 // Lord only knows how this happened...
2357 else
2358 $thumb_mime = '';
2359
2360 $thumb_filename = $attachmentOptions['name'] . '_thumb';
2361 $thumb_size = filesize($attachmentOptions['destination'] . '_thumb');
2362 $thumb_file_hash = getAttachmentFilename($thumb_filename, false, null, true);
2363
2364 // To the database we go!
2365 $smcFunc['db_insert']('',
2366 '{db_prefix}attachments',
2367 array(
2368 'id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
2369 'size' => 'int', 'width' => 'int', 'height' => 'int', 'mime_type' => 'string-20', 'approved' => 'int',
2370 ),
2371 array(
2372 $id_folder, (int) $attachmentOptions['post'], 3, $thumb_filename, $thumb_file_hash, $attachmentOptions['fileext'],
2373 $thumb_size, $thumb_width, $thumb_height, $thumb_mime, (int) $attachmentOptions['approved'],
2374 ),
2375 array('id_attach')
2376 );
2377 $attachmentOptions['thumb'] = $smcFunc['db_insert_id']('{db_prefix}attachments', 'id_attach');
2378
2379 if (!empty($attachmentOptions['thumb']))
2380 {
2381 $smcFunc['db_query']('', '
2382 UPDATE {db_prefix}attachments
2383 SET id_thumb = {int:id_thumb}
2384 WHERE id_attach = {int:id_attach}',
2385 array(
2386 'id_thumb' => $attachmentOptions['thumb'],
2387 'id_attach' => $attachmentOptions['id'],
2388 )
2389 );
2390
2391 rename($attachmentOptions['destination'] . '_thumb', getAttachmentFilename($thumb_filename, $attachmentOptions['thumb'], $id_folder, false, $thumb_file_hash));
2392 }
2393 }
2394 }
2395
2396 return true;
2397}
2398
2399// !!!
2400function modifyPost(&$msgOptions, &$topicOptions, &$posterOptions)
2401{
2402 global $user_info, $modSettings, $smcFunc, $context;
2403
2404 $topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
2405 $topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
2406 $topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
2407
2408 // This is longer than it has to be, but makes it so we only set/change what we have to.
2409 $messages_columns = array();
2410 if (isset($posterOptions['name']))
2411 $messages_columns['poster_name'] = $posterOptions['name'];
2412 if (isset($posterOptions['email']))
2413 $messages_columns['poster_email'] = $posterOptions['email'];
2414 if (isset($msgOptions['icon']))
2415 $messages_columns['icon'] = $msgOptions['icon'];
2416 if (isset($msgOptions['subject']))
2417 $messages_columns['subject'] = $msgOptions['subject'];
2418 if (isset($msgOptions['body']))
2419 {
2420 $messages_columns['body'] = $msgOptions['body'];
2421
2422 if (!empty($modSettings['search_custom_index_config']))
2423 {
2424 $request = $smcFunc['db_query']('', '
2425 SELECT body
2426 FROM {db_prefix}messages
2427 WHERE id_msg = {int:id_msg}',
2428 array(
2429 'id_msg' => $msgOptions['id'],
2430 )
2431 );
2432 list ($old_body) = $smcFunc['db_fetch_row']($request);
2433 $smcFunc['db_free_result']($request);
2434 }
2435 }
2436 if (!empty($msgOptions['modify_time']))
2437 {
2438 $messages_columns['modified_time'] = $msgOptions['modify_time'];
2439 $messages_columns['modified_name'] = $msgOptions['modify_name'];
2440 $messages_columns['id_msg_modified'] = $modSettings['maxMsgID'];
2441 }
2442 if (isset($msgOptions['smileys_enabled']))
2443 $messages_columns['smileys_enabled'] = empty($msgOptions['smileys_enabled']) ? 0 : 1;
2444
2445 // Which columns need to be ints?
2446 $messageInts = array('modified_time', 'id_msg_modified', 'smileys_enabled');
2447 $update_parameters = array(
2448 'id_msg' => $msgOptions['id'],
2449 );
2450
2451 foreach ($messages_columns as $var => $val)
2452 {
2453 $messages_columns[$var] = $var . ' = {' . (in_array($var, $messageInts) ? 'int' : 'string') . ':var_' . $var . '}';
2454 $update_parameters['var_' . $var] = $val;
2455 }
2456
2457 // Nothing to do?
2458 if (empty($messages_columns))
2459 return true;
2460
2461 // Change the post.
2462 $smcFunc['db_query']('', '
2463 UPDATE {db_prefix}messages
2464 SET ' . implode(', ', $messages_columns) . '
2465 WHERE id_msg = {int:id_msg}',
2466 $update_parameters
2467 );
2468
2469 // Lock and or sticky the post.
2470 if ($topicOptions['sticky_mode'] !== null || $topicOptions['lock_mode'] !== null || $topicOptions['poll'] !== null)
2471 {
2472 $smcFunc['db_query']('', '
2473 UPDATE {db_prefix}topics
2474 SET
2475 is_sticky = {raw:is_sticky},
2476 locked = {raw:locked},
2477 id_poll = {raw:id_poll}
2478 WHERE id_topic = {int:id_topic}',
2479 array(
2480 'is_sticky' => $topicOptions['sticky_mode'] === null ? 'is_sticky' : (int) $topicOptions['sticky_mode'],
2481 'locked' => $topicOptions['lock_mode'] === null ? 'locked' : (int) $topicOptions['lock_mode'],
2482 'id_poll' => $topicOptions['poll'] === null ? 'id_poll' : (int) $topicOptions['poll'],
2483 'id_topic' => $topicOptions['id'],
2484 )
2485 );
2486 }
2487
2488 // Mark the edited post as read.
2489 if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
2490 {
2491 // Since it's likely they *read* it before editing, let's try an UPDATE first.
2492 $smcFunc['db_query']('', '
2493 UPDATE {db_prefix}log_topics
2494 SET id_msg = {int:id_msg}
2495 WHERE id_member = {int:current_member}
2496 AND id_topic = {int:id_topic}',
2497 array(
2498 'current_member' => $user_info['id'],
2499 'id_msg' => $modSettings['maxMsgID'],
2500 'id_topic' => $topicOptions['id'],
2501 )
2502 );
2503
2504 $flag = $smcFunc['db_affected_rows']() != 0;
2505
2506 if (empty($flag))
2507 {
2508 $smcFunc['db_insert']('ignore',
2509 '{db_prefix}log_topics',
2510 array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
2511 array($topicOptions['id'], $user_info['id'], $modSettings['maxMsgID']),
2512 array('id_topic', 'id_member')
2513 );
2514 }
2515 }
2516
2517 // If there's a custom search index, it needs to be modified...
2518 if (isset($msgOptions['body']) && !empty($modSettings['search_custom_index_config']))
2519 {
2520 $customIndexSettings = safe_unserialize($modSettings['search_custom_index_config']);
2521
2522 $stopwords = empty($modSettings['search_stopwords']) ? array() : explode(',', $modSettings['search_stopwords']);
2523 $old_index = text2words($old_body, $customIndexSettings['bytes_per_word'], true);
2524 $new_index = text2words($msgOptions['body'], $customIndexSettings['bytes_per_word'], true);
2525
2526 // Calculate the words to be added and removed from the index.
2527 $removed_words = array_diff(array_diff($old_index, $new_index), $stopwords);
2528 $inserted_words = array_diff(array_diff($new_index, $old_index), $stopwords);
2529 // Delete the removed words AND the added ones to avoid key constraints.
2530 if (!empty($removed_words))
2531 {
2532 $removed_words = array_merge($removed_words, $inserted_words);
2533 $smcFunc['db_query']('', '
2534 DELETE FROM {db_prefix}log_search_words
2535 WHERE id_msg = {int:id_msg}
2536 AND id_word IN ({array_int:removed_words})',
2537 array(
2538 'removed_words' => $removed_words,
2539 'id_msg' => $msgOptions['id'],
2540 )
2541 );
2542 }
2543
2544 // Add the new words to be indexed.
2545 if (!empty($inserted_words))
2546 {
2547 $inserts = array();
2548 foreach ($inserted_words as $word)
2549 $inserts[] = array($word, $msgOptions['id']);
2550 $smcFunc['db_insert']('insert',
2551 '{db_prefix}log_search_words',
2552 array('id_word' => 'string', 'id_msg' => 'int'),
2553 $inserts,
2554 array('id_word', 'id_msg')
2555 );
2556 }
2557 }
2558
2559 if (isset($msgOptions['subject']))
2560 {
2561 // Only update the subject if this was the first message in the topic.
2562 $request = $smcFunc['db_query']('', '
2563 SELECT id_topic
2564 FROM {db_prefix}topics
2565 WHERE id_first_msg = {int:id_first_msg}
2566 LIMIT 1',
2567 array(
2568 'id_first_msg' => $msgOptions['id'],
2569 )
2570 );
2571 if ($smcFunc['db_num_rows']($request) == 1)
2572 updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
2573 $smcFunc['db_free_result']($request);
2574 }
2575
2576 // Finally, if we are setting the approved state we need to do much more work :(
2577 if ($modSettings['postmod_active'] && isset($msgOptions['approved']))
2578 approvePosts($msgOptions['id'], $msgOptions['approved']);
2579
2580 return true;
2581}
2582
2583// Approve (or not) some posts... without permission checks...
2584function approvePosts($msgs, $approve = true)
2585{
2586 global $sourcedir, $smcFunc;
2587
2588 if (!is_array($msgs))
2589 $msgs = array($msgs);
2590
2591 if (empty($msgs))
2592 return false;
2593
2594 // May as well start at the beginning, working out *what* we need to change.
2595 $request = $smcFunc['db_query']('', '
2596 SELECT m.id_msg, m.approved, m.id_topic, m.id_board, t.id_first_msg, t.id_last_msg,
2597 m.body, m.subject, IFNULL(mem.real_name, m.poster_name) AS poster_name, m.id_member,
2598 t.approved AS topic_approved, b.count_posts
2599 FROM {db_prefix}messages AS m
2600 INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
2601 INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
2602 LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
2603 WHERE m.id_msg IN ({array_int:message_list})
2604 AND m.approved = {int:approved_state}',
2605 array(
2606 'message_list' => $msgs,
2607 'approved_state' => $approve ? 0 : 1,
2608 )
2609 );
2610 $msgs = array();
2611 $topics = array();
2612 $topic_changes = array();
2613 $board_changes = array();
2614 $notification_topics = array();
2615 $notification_posts = array();
2616 $member_post_changes = array();
2617 while ($row = $smcFunc['db_fetch_assoc']($request))
2618 {
2619 // Easy...
2620 $msgs[] = $row['id_msg'];
2621 $topics[] = $row['id_topic'];
2622
2623 // Ensure our change array exists already.
2624 if (!isset($topic_changes[$row['id_topic']]))
2625 $topic_changes[$row['id_topic']] = array(
2626 'id_last_msg' => $row['id_last_msg'],
2627 'approved' => $row['topic_approved'],
2628 'replies' => 0,
2629 'unapproved_posts' => 0,
2630 );
2631 if (!isset($board_changes[$row['id_board']]))
2632 $board_changes[$row['id_board']] = array(
2633 'posts' => 0,
2634 'topics' => 0,
2635 'unapproved_posts' => 0,
2636 'unapproved_topics' => 0,
2637 );
2638
2639 // If it's the first message then the topic state changes!
2640 if ($row['id_msg'] == $row['id_first_msg'])
2641 {
2642 $topic_changes[$row['id_topic']]['approved'] = $approve ? 1 : 0;
2643
2644 $board_changes[$row['id_board']]['unapproved_topics'] += $approve ? -1 : 1;
2645 $board_changes[$row['id_board']]['topics'] += $approve ? 1 : -1;
2646
2647 // Note we need to ensure we announce this topic!
2648 $notification_topics[] = array(
2649 'body' => $row['body'],
2650 'subject' => $row['subject'],
2651 'name' => $row['poster_name'],
2652 'board' => $row['id_board'],
2653 'topic' => $row['id_topic'],
2654 'msg' => $row['id_first_msg'],
2655 'poster' => $row['id_member'],
2656 );
2657 }
2658 else
2659 {
2660 $topic_changes[$row['id_topic']]['replies'] += $approve ? 1 : -1;
2661
2662 // This will be a post... but don't notify unless it's not followed by approved ones.
2663 if ($row['id_msg'] > $row['id_last_msg'])
2664 $notification_posts[$row['id_topic']][] = array(
2665 'id' => $row['id_msg'],
2666 'body' => $row['body'],
2667 'subject' => $row['subject'],
2668 'name' => $row['poster_name'],
2669 'topic' => $row['id_topic'],
2670 );
2671 }
2672
2673 // If this is being approved and id_msg is higher than the current id_last_msg then it changes.
2674 if ($approve && $row['id_msg'] > $topic_changes[$row['id_topic']]['id_last_msg'])
2675 $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_msg'];
2676 // If this is being unapproved, and it's equal to the id_last_msg we need to find a new one!
2677 elseif (!$approve)
2678 // Default to the first message and then we'll override in a bit ;)
2679 $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_first_msg'];
2680
2681 $topic_changes[$row['id_topic']]['unapproved_posts'] += $approve ? -1 : 1;
2682 $board_changes[$row['id_board']]['unapproved_posts'] += $approve ? -1 : 1;
2683 $board_changes[$row['id_board']]['posts'] += $approve ? 1 : -1;
2684
2685 // Post count for the user?
2686 if ($row['id_member'] && empty($row['count_posts']))
2687 $member_post_changes[$row['id_member']] = isset($member_post_changes[$row['id_member']]) ? $member_post_changes[$row['id_member']] + 1 : 1;
2688 }
2689 $smcFunc['db_free_result']($request);
2690
2691 if (empty($msgs))
2692 return;
2693
2694 // Now we have the differences make the changes, first the easy one.
2695 $smcFunc['db_query']('', '
2696 UPDATE {db_prefix}messages
2697 SET approved = {int:approved_state}
2698 WHERE id_msg IN ({array_int:message_list})',
2699 array(
2700 'message_list' => $msgs,
2701 'approved_state' => $approve ? 1 : 0,
2702 )
2703 );
2704
2705 // If we were unapproving find the last msg in the topics...
2706 if (!$approve)
2707 {
2708 $request = $smcFunc['db_query']('', '
2709 SELECT id_topic, MAX(id_msg) AS id_last_msg
2710 FROM {db_prefix}messages
2711 WHERE id_topic IN ({array_int:topic_list})
2712 AND approved = {int:approved}
2713 GROUP BY id_topic',
2714 array(
2715 'topic_list' => $topics,
2716 'approved' => 1,
2717 )
2718 );
2719 while ($row = $smcFunc['db_fetch_assoc']($request))
2720 $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_last_msg'];
2721 $smcFunc['db_free_result']($request);
2722 }
2723
2724 // ... next the topics...
2725 foreach ($topic_changes as $id => $changes)
2726 $smcFunc['db_query']('', '
2727 UPDATE {db_prefix}topics
2728 SET approved = {int:approved}, unapproved_posts = unapproved_posts + {int:unapproved_posts},
2729 num_replies = num_replies + {int:num_replies}, id_last_msg = {int:id_last_msg}
2730 WHERE id_topic = {int:id_topic}',
2731 array(
2732 'approved' => $changes['approved'],
2733 'unapproved_posts' => $changes['unapproved_posts'],
2734 'num_replies' => $changes['replies'],
2735 'id_last_msg' => $changes['id_last_msg'],
2736 'id_topic' => $id,
2737 )
2738 );
2739
2740 // ... finally the boards...
2741 foreach ($board_changes as $id => $changes)
2742 $smcFunc['db_query']('', '
2743 UPDATE {db_prefix}boards
2744 SET num_posts = num_posts + {int:num_posts}, unapproved_posts = unapproved_posts + {int:unapproved_posts},
2745 num_topics = num_topics + {int:num_topics}, unapproved_topics = unapproved_topics + {int:unapproved_topics}
2746 WHERE id_board = {int:id_board}',
2747 array(
2748 'num_posts' => $changes['posts'],
2749 'unapproved_posts' => $changes['unapproved_posts'],
2750 'num_topics' => $changes['topics'],
2751 'unapproved_topics' => $changes['unapproved_topics'],
2752 'id_board' => $id,
2753 )
2754 );
2755
2756 // Finally, least importantly, notifications!
2757 if ($approve)
2758 {
2759 if (!empty($notification_topics))
2760 {
2761 require_once($sourcedir . '/Post.php');
2762 notifyMembersBoard($notification_topics);
2763 }
2764 if (!empty($notification_posts))
2765 sendApprovalNotifications($notification_posts);
2766
2767 $smcFunc['db_query']('', '
2768 DELETE FROM {db_prefix}approval_queue
2769 WHERE id_msg IN ({array_int:message_list})
2770 AND id_attach = {int:id_attach}',
2771 array(
2772 'message_list' => $msgs,
2773 'id_attach' => 0,
2774 )
2775 );
2776 }
2777 // If unapproving add to the approval queue!
2778 else
2779 {
2780 $msgInserts = array();
2781 foreach ($msgs as $msg)
2782 $msgInserts[] = array($msg);
2783
2784 $smcFunc['db_insert']('ignore',
2785 '{db_prefix}approval_queue',
2786 array('id_msg' => 'int'),
2787 $msgInserts,
2788 array('id_msg')
2789 );
2790 }
2791
2792 // Update the last messages on the boards...
2793 updateLastMessages(array_keys($board_changes));
2794
2795 // Post count for the members?
2796 if (!empty($member_post_changes))
2797 foreach ($member_post_changes as $id_member => $count_change)
2798 updateMemberData($id_member, array('posts' => 'posts ' . ($approve ? '+' : '-') . ' ' . $count_change));
2799
2800 return true;
2801}
2802
2803// Approve topics?
2804function approveTopics($topics, $approve = true)
2805{
2806 global $smcFunc;
2807
2808 if (!is_array($topics))
2809 $topics = array($topics);
2810
2811 if (empty($topics))
2812 return false;
2813
2814 $approve_type = $approve ? 0 : 1;
2815
2816 // Just get the messages to be approved and pass through...
2817 $request = $smcFunc['db_query']('', '
2818 SELECT id_msg
2819 FROM {db_prefix}messages
2820 WHERE id_topic IN ({array_int:topic_list})
2821 AND approved = {int:approve_type}',
2822 array(
2823 'topic_list' => $topics,
2824 'approve_type' => $approve_type,
2825 )
2826 );
2827 $msgs = array();
2828 while ($row = $smcFunc['db_fetch_assoc']($request))
2829 $msgs[] = $row['id_msg'];
2830 $smcFunc['db_free_result']($request);
2831
2832 return approvePosts($msgs, $approve);
2833}
2834
2835// A special function for handling the hell which is sending approval notifications.
2836function sendApprovalNotifications(&$topicData)
2837{
2838 global $txt, $scripturl, $language, $user_info;
2839 global $modSettings, $sourcedir, $context, $smcFunc;
2840
2841 // Clean up the data...
2842 if (!is_array($topicData) || empty($topicData))
2843 return;
2844
2845 $topics = array();
2846 $digest_insert = array();
2847 foreach ($topicData as $topic => $msgs)
2848 foreach ($msgs as $msgKey => $msg)
2849 {
2850 censorText($topicData[$topic][$msgKey]['subject']);
2851 censorText($topicData[$topic][$msgKey]['body']);
2852 $topicData[$topic][$msgKey]['subject'] = un_htmlspecialchars($topicData[$topic][$msgKey]['subject']);
2853 $topicData[$topic][$msgKey]['body'] = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($topicData[$topic][$msgKey]['body'], false), array('<br />' => "\n", '</div>' => "\n", '</li>' => "\n", '[' => '[', ']' => ']')))));
2854
2855 $topics[] = $msg['id'];
2856 $digest_insert[] = array($msg['topic'], $msg['id'], 'reply', $user_info['id']);
2857 }
2858
2859 // These need to go into the digest too...
2860 $smcFunc['db_insert']('',
2861 '{db_prefix}log_digest',
2862 array(
2863 'id_topic' => 'int', 'id_msg' => 'int', 'note_type' => 'string', 'exclude' => 'int',
2864 ),
2865 $digest_insert,
2866 array()
2867 );
2868
2869 // Find everyone who needs to know about this.
2870 $members = $smcFunc['db_query']('', '
2871 SELECT
2872 mem.id_member, mem.email_address, mem.notify_regularity, mem.notify_types, mem.notify_send_body, mem.lngfile,
2873 ln.sent, mem.id_group, mem.additional_groups, b.member_groups, mem.id_post_group, t.id_member_started,
2874 ln.id_topic
2875 FROM {db_prefix}log_notify AS ln
2876 INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ln.id_member)
2877 INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)
2878 INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
2879 WHERE ln.id_topic IN ({array_int:topic_list})
2880 AND mem.is_activated = {int:is_activated}
2881 AND mem.notify_types < {int:notify_types}
2882 AND mem.notify_regularity < {int:notify_regularity}
2883 GROUP BY mem.id_member, ln.id_topic, mem.email_address, mem.notify_regularity, mem.notify_types, mem.notify_send_body, mem.lngfile, ln.sent, mem.id_group, mem.additional_groups, b.member_groups, mem.id_post_group, t.id_member_started
2884 ORDER BY mem.lngfile',
2885 array(
2886 'topic_list' => $topics,
2887 'is_activated' => 1,
2888 'notify_types' => 4,
2889 'notify_regularity' => 2,
2890 )
2891 );
2892 $sent = 0;
2893 while ($row = $smcFunc['db_fetch_assoc']($members))
2894 {
2895 if ($row['id_group'] != 1)
2896 {
2897 $allowed = explode(',', $row['member_groups']);
2898 $row['additional_groups'] = explode(',', $row['additional_groups']);
2899 $row['additional_groups'][] = $row['id_group'];
2900 $row['additional_groups'][] = $row['id_post_group'];
2901
2902 if (count(array_intersect($allowed, $row['additional_groups'])) == 0)
2903 continue;
2904 }
2905
2906 $needed_language = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
2907 if (empty($current_language) || $current_language != $needed_language)
2908 $current_language = loadLanguage('Post', $needed_language, false);
2909
2910 $sent_this_time = false;
2911 // Now loop through all the messages to send.
2912 foreach ($topicData[$row['id_topic']] as $msg)
2913 {
2914 $replacements = array(
2915 'TOPICSUBJECT' => $topicData[$row['id_topic']]['subject'],
2916 'POSTERNAME' => un_htmlspecialchars($topicData[$row['id_topic']]['name']),
2917 'TOPICLINK' => $scripturl . '?topic=' . $row['id_topic'] . '.new;topicseen#new',
2918 'UNSUBSCRIBELINK' => $scripturl . '?action=notify;topic=' . $row['id_topic'] . '.0',
2919 );
2920
2921 $message_type = 'notification_reply';
2922 // Do they want the body of the message sent too?
2923 if (!empty($row['notify_send_body']) && empty($modSettings['disallow_sendBody']))
2924 {
2925 $message_type .= '_body';
2926 $replacements['BODY'] = $topicData[$row['id_topic']]['body'];
2927 }
2928 if (!empty($row['notify_regularity']))
2929 $message_type .= '_once';
2930
2931 // Send only if once is off or it's on and it hasn't been sent.
2932 if (empty($row['notify_regularity']) || (empty($row['sent']) && !$sent_this_time))
2933 {
2934 $emaildata = loadEmailTemplate($message_type, $replacements, $needed_language);
2935 sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $topicData[$row['id_topic']]['last_id']);
2936 $sent++;
2937 }
2938
2939 $sent_this_time = true;
2940 }
2941 }
2942 $smcFunc['db_free_result']($members);
2943
2944 if (isset($current_language) && $current_language != $user_info['language'])
2945 loadLanguage('Post');
2946
2947 // Sent!
2948 if (!empty($sent))
2949 $smcFunc['db_query']('', '
2950 UPDATE {db_prefix}log_notify
2951 SET sent = {int:is_sent}
2952 WHERE id_topic IN ({array_int:topic_list})
2953 AND id_member != {int:current_member}',
2954 array(
2955 'current_member' => $user_info['id'],
2956 'topic_list' => $topics,
2957 'is_sent' => 1,
2958 )
2959 );
2960}
2961
2962// Update the last message in a board, and its parents.
2963function updateLastMessages($setboards, $id_msg = 0)
2964{
2965 global $board_info, $board, $modSettings, $smcFunc;
2966
2967 // Please - let's be sane.
2968 if (empty($setboards))
2969 return false;
2970
2971 if (!is_array($setboards))
2972 $setboards = array($setboards);
2973
2974 // If we don't know the id_msg we need to find it.
2975 if (!$id_msg)
2976 {
2977 // Find the latest message on this board (highest id_msg.)
2978 $request = $smcFunc['db_query']('', '
2979 SELECT id_board, MAX(id_last_msg) AS id_msg
2980 FROM {db_prefix}topics
2981 WHERE id_board IN ({array_int:board_list})
2982 AND approved = {int:approved}
2983 GROUP BY id_board',
2984 array(
2985 'board_list' => $setboards,
2986 'approved' => 1,
2987 )
2988 );
2989 $lastMsg = array();
2990 while ($row = $smcFunc['db_fetch_assoc']($request))
2991 $lastMsg[$row['id_board']] = $row['id_msg'];
2992 $smcFunc['db_free_result']($request);
2993 }
2994 else
2995 {
2996 // Just to note - there should only be one board passed if we are doing this.
2997 foreach ($setboards as $id_board)
2998 $lastMsg[$id_board] = $id_msg;
2999 }
3000
3001 $parent_boards = array();
3002 // Keep track of last modified dates.
3003 $lastModified = $lastMsg;
3004 // Get all the child boards for the parents, if they have some...
3005 foreach ($setboards as $id_board)
3006 {
3007 if (!isset($lastMsg[$id_board]))
3008 {
3009 $lastMsg[$id_board] = 0;
3010 $lastModified[$id_board] = 0;
3011 }
3012
3013 if (!empty($board) && $id_board == $board)
3014 $parents = $board_info['parent_boards'];
3015 else
3016 $parents = getBoardParents($id_board);
3017
3018 // Ignore any parents on the top child level.
3019 //!!! Why?
3020 foreach ($parents as $id => $parent)
3021 {
3022 if ($parent['level'] != 0)
3023 {
3024 // If we're already doing this one as a board, is this a higher last modified?
3025 if (isset($lastModified[$id]) && $lastModified[$id_board] > $lastModified[$id])
3026 $lastModified[$id] = $lastModified[$id_board];
3027 elseif (!isset($lastModified[$id]) && (!isset($parent_boards[$id]) || $parent_boards[$id] < $lastModified[$id_board]))
3028 $parent_boards[$id] = $lastModified[$id_board];
3029 }
3030 }
3031 }
3032
3033 // Note to help understand what is happening here. For parents we update the timestamp of the last message for determining
3034 // whether there are child boards which have not been read. For the boards themselves we update both this and id_last_msg.
3035
3036 $board_updates = array();
3037 $parent_updates = array();
3038 // Finally, to save on queries make the changes...
3039 foreach ($parent_boards as $id => $msg)
3040 {
3041 if (!isset($parent_updates[$msg]))
3042 $parent_updates[$msg] = array($id);
3043 else
3044 $parent_updates[$msg][] = $id;
3045 }
3046
3047 foreach ($lastMsg as $id => $msg)
3048 {
3049 if (!isset($board_updates[$msg . '-' . $lastModified[$id]]))
3050 $board_updates[$msg . '-' . $lastModified[$id]] = array(
3051 'id' => $msg,
3052 'updated' => $lastModified[$id],
3053 'boards' => array($id)
3054 );
3055
3056 else
3057 $board_updates[$msg . '-' . $lastModified[$id]]['boards'][] = $id;
3058 }
3059
3060 // Now commit the changes!
3061 foreach ($parent_updates as $id_msg => $boards)
3062 {
3063 $smcFunc['db_query']('', '
3064 UPDATE {db_prefix}boards
3065 SET id_msg_updated = {int:id_msg_updated}
3066 WHERE id_board IN ({array_int:board_list})
3067 AND id_msg_updated < {int:id_msg_updated}',
3068 array(
3069 'board_list' => $boards,
3070 'id_msg_updated' => $id_msg,
3071 )
3072 );
3073 }
3074 foreach ($board_updates as $board_data)
3075 {
3076 $smcFunc['db_query']('', '
3077 UPDATE {db_prefix}boards
3078 SET id_last_msg = {int:id_last_msg}, id_msg_updated = {int:id_msg_updated}
3079 WHERE id_board IN ({array_int:board_list})',
3080 array(
3081 'board_list' => $board_data['boards'],
3082 'id_last_msg' => $board_data['id'],
3083 'id_msg_updated' => $board_data['updated'],
3084 )
3085 );
3086 }
3087}
3088
3089// This simple function gets a list of all administrators and sends them an email to let them know a new member has joined.
3090function adminNotify($type, $memberID, $member_name = null)
3091{
3092 global $txt, $modSettings, $language, $scripturl, $user_info, $context, $smcFunc;
3093
3094 // If the setting isn't enabled then just exit.
3095 if (empty($modSettings['notify_new_registration']))
3096 return;
3097
3098 if ($member_name == null)
3099 {
3100 // Get the new user's name....
3101 $request = $smcFunc['db_query']('', '
3102 SELECT real_name
3103 FROM {db_prefix}members
3104 WHERE id_member = {int:id_member}
3105 LIMIT 1',
3106 array(
3107 'id_member' => $memberID,
3108 )
3109 );
3110 list ($member_name) = $smcFunc['db_fetch_row']($request);
3111 $smcFunc['db_free_result']($request);
3112 }
3113
3114 $toNotify = array();
3115 $groups = array();
3116
3117 // All membergroups who can approve members.
3118 $request = $smcFunc['db_query']('', '
3119 SELECT id_group
3120 FROM {db_prefix}permissions
3121 WHERE permission = {string:moderate_forum}
3122 AND add_deny = {int:add_deny}
3123 AND id_group != {int:id_group}',
3124 array(
3125 'add_deny' => 1,
3126 'id_group' => 0,
3127 'moderate_forum' => 'moderate_forum',
3128 )
3129 );
3130 while ($row = $smcFunc['db_fetch_assoc']($request))
3131 $groups[] = $row['id_group'];
3132 $smcFunc['db_free_result']($request);
3133
3134 // Add administrators too...
3135 $groups[] = 1;
3136 $groups = array_unique($groups);
3137
3138 // Get a list of all members who have ability to approve accounts - these are the people who we inform.
3139 $request = $smcFunc['db_query']('', '
3140 SELECT id_member, lngfile, email_address
3141 FROM {db_prefix}members
3142 WHERE (id_group IN ({array_int:group_list}) OR FIND_IN_SET({raw:group_array_implode}, additional_groups) != 0)
3143 AND notify_types != {int:notify_types}
3144 ORDER BY lngfile',
3145 array(
3146 'group_list' => $groups,
3147 'notify_types' => 4,
3148 'group_array_implode' => implode(', additional_groups) != 0 OR FIND_IN_SET(', $groups),
3149 )
3150 );
3151 while ($row = $smcFunc['db_fetch_assoc']($request))
3152 {
3153 $replacements = array(
3154 'USERNAME' => $member_name,
3155 'PROFILELINK' => $scripturl . '?action=profile;u=' . $memberID
3156 );
3157 $emailtype = 'admin_notify';
3158
3159 // If they need to be approved add more info...
3160 if ($type == 'approval')
3161 {
3162 $replacements['APPROVALLINK'] = $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve';
3163 $emailtype .= '_approval';
3164 }
3165
3166 $emaildata = loadEmailTemplate($emailtype, $replacements, empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']);
3167
3168 // And do the actual sending...
3169 sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
3170 }
3171 $smcFunc['db_free_result']($request);
3172
3173 if (isset($current_language) && $current_language != $user_info['language'])
3174 loadLanguage('Login');
3175}
3176
3177function loadEmailTemplate($template, $replacements = array(), $lang = '', $loadLang = true)
3178{
3179 global $txt, $mbname, $scripturl, $settings, $user_info;
3180
3181 // First things first, load up the email templates language file, if we need to.
3182 if ($loadLang)
3183 loadLanguage('EmailTemplates', $lang);
3184
3185 if (!isset($txt['emails'][$template]))
3186 fatal_lang_error('email_no_template', 'template', array($template));
3187
3188 $ret = array(
3189 'subject' => $txt['emails'][$template]['subject'],
3190 'body' => $txt['emails'][$template]['body'],
3191 );
3192
3193 // Add in the default replacements.
3194 $replacements += array(
3195 'FORUMNAME' => $mbname,
3196 'SCRIPTURL' => $scripturl,
3197 'THEMEURL' => $settings['theme_url'],
3198 'IMAGESURL' => $settings['images_url'],
3199 'DEFAULT_THEMEURL' => $settings['default_theme_url'],
3200 'REGARDS' => $txt['regards_team'],
3201 );
3202
3203 // Split the replacements up into two arrays, for use with str_replace
3204 $find = array();
3205 $replace = array();
3206
3207 foreach ($replacements as $f => $r)
3208 {
3209 $find[] = '{' . $f . '}';
3210 $replace[] = $r;
3211 }
3212
3213 // Do the variable replacements.
3214 $ret['subject'] = str_replace($find, $replace, $ret['subject']);
3215 $ret['body'] = str_replace($find, $replace, $ret['body']);
3216
3217 // Now deal with the {USER.variable} items.
3218 $ret['subject'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['subject']);
3219 $ret['body'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['body']);
3220
3221 // Finally return the email to the caller so they can send it out.
3222 return $ret;
3223}
3224
3225function user_info_callback($matches)
3226{
3227 global $user_info;
3228 if (empty($matches[1]))
3229 return '';
3230
3231 $use_ref = true;
3232 $ref = &$user_info;
3233
3234 foreach (explode('.', $matches[1]) as $index)
3235 {
3236 if ($use_ref && isset($ref[$index]))
3237 $ref = &$ref[$index];
3238 else
3239 {
3240 $use_ref = false;
3241 break;
3242 }
3243 }
3244
3245 return $use_ref ? $ref : $matches[0];
3246}
3247
3248function action_fix__preg_callback($matches)
3249{
3250 return $matches[1] . preg_replace('~action(=|%3d)(?!dlattach)~i', 'action-', $matches[2]) . '[/img]';
3251}
3252
3253function mime_convert__preg_callback($matches)
3254{
3255 // I get the feeling we could possibly ditch this and reuse fixchar__callback but handling for < 0x20
3256 // may not be appropriate here.
3257
3258 $c = $matches[1];
3259 if (strlen($c) === 1 && ord($c[0]) <= 0x7F)
3260 return $c;
3261 elseif (strlen($c) === 2 && ord($c[0]) >= 0xC0 && ord($c[0]) <= 0xDF)
3262 return '&#' . (((ord($c[0]) ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ';';
3263 elseif (strlen($c) === 3 && ord($c[0]) >= 0xE0 && ord($c[0]) <= 0xEF)
3264 return '&#' . (((ord($c[0]) ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ';';
3265 elseif (strlen($c) === 4 && ord($c[0]) >= 0xF0 && ord($c[0]) <= 0xF7)
3266 return '&#' . (((ord($c[0]) ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ';';
3267 else
3268 return '';
3269}
3270
3271function time_fix__preg_callback($matches)
3272{
3273 global $modSettings, $user_info;
3274 return '[time]' . (is_numeric($matches[2]) || @strtotime($matches[2]) == 0 ? $matches[2] : strtotime($matches[2]) - ($matches[1] == 'absolute' ? 0 : (($modSettings['time_offset'] + $user_info['time_offset']) * 3600))) . '[/time]';
3275}
3276
3277function nobbc__preg_callback($matches)
3278{
3279 return '[nobbc]' . strtr($matches[1], array('[' => '[', ']' => ']', ':' => ':', '@' => '@')) . '[/nobbc]';
3280}
3281
3282function lowercase_tags__preg_callback($matches)
3283{
3284 return '[' . $matches[1] . strtolower($matches[2]) . $matches[3] . ']';
3285}
3286
3287function htmlspecial_html__preg_callback($matches)
3288{
3289 // Since we're calling htmlspecialchars we probably should know what charset we're using.
3290 global $modSettings, $txt;
3291 static $charset = null;
3292 if ($charset === null)
3293 $charset = empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set'];
3294
3295 return '[html]' . strtr(htmlspecialchars($matches[1], ENT_QUOTES, $charset), array('\\"' => '"', '&#13;' => '<br />', '&#32;' => ' ', '&#91;' => '[', '&#93;' => ']')) . '[/html]';
3296}
3297
3298function time_format__preg_callback($matches)
3299{
3300 return '[time]' . timeformat($matches[1], false) . '[/time]';
3301}
3302?>