· 7 years ago · Feb 06, 2019, 12:48 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 {
1353 if (!server_parse('AUTH LOGIN', $socket, '334'))
1354 return false;
1355 // Send the username and password, encoded.
1356 if (!server_parse(base64_encode($modSettings['smtp_username']), $socket, '334'))
1357 return false;
1358 // The password is already encoded ;)
1359 if (!server_parse($modSettings['smtp_password'], $socket, '235'))
1360 return false;
1361 }
1362 elseif (!server_parse('HELO ' . $modSettings['smtp_host'], $socket, '250'))
1363 return false;
1364 }
1365 else
1366 {
1367 // Just say "helo".
1368 if (!server_parse('HELO ' . $modSettings['smtp_host'], $socket, '250'))
1369 return false;
1370 }
1371
1372 // Fix the message for any lines beginning with a period! (the first is ignored, you see.)
1373 $message = strtr($message, array("\r\n" . '.' => "\r\n" . '..'));
1374
1375 // !! Theoretically, we should be able to just loop the RCPT TO.
1376 $mail_to_array = array_values($mail_to_array);
1377 foreach ($mail_to_array as $i => $mail_to)
1378 {
1379 // Reset the connection to send another email.
1380 if ($i != 0)
1381 {
1382 if (!server_parse('RSET', $socket, '250'))
1383 return false;
1384 }
1385
1386 // From, to, and then start the data...
1387 if (!server_parse('MAIL FROM: <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>', $socket, '250'))
1388 return false;
1389 if (!server_parse('RCPT TO: <' . $mail_to . '>', $socket, '250'))
1390 return false;
1391 if (!server_parse('DATA', $socket, '354'))
1392 return false;
1393 fputs($socket, 'Subject: ' . $subject . "\r\n");
1394 if (strlen($mail_to) > 0)
1395 fputs($socket, 'To: <' . $mail_to . '>' . "\r\n");
1396 fputs($socket, $headers . "\r\n\r\n");
1397 fputs($socket, $message . "\r\n");
1398
1399 // Send a ., or in other words "end of data".
1400 if (!server_parse('.', $socket, '250'))
1401 return false;
1402
1403 // Almost done, almost done... don't stop me just yet!
1404 @set_time_limit(300);
1405 if (function_exists('apache_reset_timeout'))
1406 @apache_reset_timeout();
1407 }
1408 fputs($socket, 'QUIT' . "\r\n");
1409 fclose($socket);
1410
1411 return true;
1412}
1413
1414// Parse a message to the SMTP server.
1415function server_parse($message, $socket, $response)
1416{
1417 global $txt;
1418
1419 if ($message !== null)
1420 fputs($socket, $message . "\r\n");
1421
1422 // No response yet.
1423 $server_response = '';
1424
1425 while (substr($server_response, 3, 1) != ' ')
1426 if (!($server_response = fgets($socket, 256)))
1427 {
1428 // !!! Change this message to reflect that it may mean bad user/password/server issues/etc.
1429 log_error($txt['smtp_bad_response']);
1430 return false;
1431 }
1432
1433 if ($response === null)
1434 return substr($server_response, 0, 3);
1435
1436 if (substr($server_response, 0, 3) != $response)
1437 {
1438 log_error($txt['smtp_error'] . $server_response);
1439 return false;
1440 }
1441
1442 return true;
1443}
1444
1445function SpellCheck()
1446{
1447 global $txt, $context, $smcFunc;
1448
1449 // A list of "words" we know about but pspell doesn't.
1450 $known_words = array('smf', 'php', 'mysql', 'www', 'gif', 'jpeg', 'png', 'http', 'smfisawesome', 'grandia', 'terranigma', 'rpgs');
1451
1452 loadLanguage('Post');
1453 loadTemplate('Post');
1454
1455 // Okay, this looks funny, but it actually fixes a weird bug.
1456 ob_start();
1457 $old = error_reporting(0);
1458
1459 // See, first, some windows machines don't load pspell properly on the first try. Dumb, but this is a workaround.
1460 pspell_new('en');
1461
1462 // Next, the dictionary in question may not exist. So, we try it... but...
1463 $pspell_link = pspell_new($txt['lang_dictionary'], $txt['lang_spelling'], '', strtr($context['character_set'], array('iso-' => 'iso', 'ISO-' => 'iso')), PSPELL_FAST | PSPELL_RUN_TOGETHER);
1464
1465 // Most people don't have anything but English installed... So we use English as a last resort.
1466 if (!$pspell_link)
1467 $pspell_link = pspell_new('en', '', '', '', PSPELL_FAST | PSPELL_RUN_TOGETHER);
1468
1469 error_reporting($old);
1470 ob_end_clean();
1471
1472 if (!isset($_POST['spellstring']) || !$pspell_link)
1473 die;
1474
1475 // Construct a bit of Javascript code.
1476 $context['spell_js'] = '
1477 var txt = {"done": "' . $txt['spellcheck_done'] . '"};
1478 var mispstr = window.opener.document.forms[spell_formname][spell_fieldname].value;
1479 var misps = Array(';
1480
1481 // Get all the words (Javascript already separated them).
1482 $alphas = explode("\n", strtr($_POST['spellstring'], array("\r" => '')));
1483
1484 $found_words = false;
1485 for ($i = 0, $n = count($alphas); $i < $n; $i++)
1486 {
1487 // Words are sent like 'word|offset_begin|offset_end'.
1488 $check_word = explode('|', $alphas[$i]);
1489
1490 // If the word is a known word, or spelled right...
1491 if (in_array($smcFunc['strtolower']($check_word[0]), $known_words) || pspell_check($pspell_link, $check_word[0]) || !isset($check_word[2]))
1492 continue;
1493
1494 // Find the word, and move up the "last occurance" to here.
1495 $found_words = true;
1496
1497 // Add on the javascript for this misspelling.
1498 $context['spell_js'] .= '
1499 new misp("' . strtr($check_word[0], array('\\' => '\\\\', '"' => '\\"', '<' => '', '>' => '')) . '", ' . (int) $check_word[1] . ', ' . (int) $check_word[2] . ', [';
1500
1501 // If there are suggestions, add them in...
1502 $suggestions = pspell_suggest($pspell_link, $check_word[0]);
1503 if (!empty($suggestions))
1504 {
1505 // But first check they aren't going to be censored - no naughty words!
1506 foreach ($suggestions as $k => $word)
1507 if ($suggestions[$k] != censorText($word))
1508 unset($suggestions[$k]);
1509
1510 if (!empty($suggestions))
1511 $context['spell_js'] .= '"' . implode('", "', $suggestions) . '"';
1512 }
1513
1514 $context['spell_js'] .= ']),';
1515 }
1516
1517 // If words were found, take off the last comma.
1518 if ($found_words)
1519 $context['spell_js'] = substr($context['spell_js'], 0, -1);
1520
1521 $context['spell_js'] .= '
1522 );';
1523
1524 // And instruct the template system to just show the spellcheck sub template.
1525 $context['template_layers'] = array();
1526 $context['sub_template'] = 'spellcheck';
1527}
1528
1529// Notify members that something has happened to a topic they marked!
1530function sendNotifications($topics, $type, $exclude = array(), $members_only = array())
1531{
1532 global $txt, $scripturl, $language, $user_info;
1533 global $modSettings, $sourcedir, $context, $smcFunc;
1534
1535 // Can't do it if there's no topics.
1536 if (empty($topics))
1537 return;
1538 // It must be an array - it must!
1539 if (!is_array($topics))
1540 $topics = array($topics);
1541
1542 // Get the subject and body...
1543 $result = $smcFunc['db_query']('', '
1544 SELECT mf.subject, ml.body, ml.id_member, t.id_last_msg, t.id_topic,
1545 IFNULL(mem.real_name, ml.poster_name) AS poster_name
1546 FROM {db_prefix}topics AS t
1547 INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1548 INNER JOIN {db_prefix}messages AS ml ON (ml.id_msg = t.id_last_msg)
1549 LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = ml.id_member)
1550 WHERE t.id_topic IN ({array_int:topic_list})
1551 LIMIT 1',
1552 array(
1553 'topic_list' => $topics,
1554 )
1555 );
1556 $topicData = array();
1557 while ($row = $smcFunc['db_fetch_assoc']($result))
1558 {
1559 // Clean it up.
1560 censorText($row['subject']);
1561 censorText($row['body']);
1562 $row['subject'] = un_htmlspecialchars($row['subject']);
1563 $row['body'] = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($row['body'], false, $row['id_last_msg']), array('<br />' => "\n", '</div>' => "\n", '</li>' => "\n", '[' => '[', ']' => ']')))));
1564
1565 $topicData[$row['id_topic']] = array(
1566 'subject' => $row['subject'],
1567 'body' => $row['body'],
1568 'last_id' => $row['id_last_msg'],
1569 'topic' => $row['id_topic'],
1570 'name' => $user_info['name'],
1571 'exclude' => '',
1572 );
1573 }
1574 $smcFunc['db_free_result']($result);
1575
1576 // Work out any exclusions...
1577 foreach ($topics as $key => $id)
1578 if (isset($topicData[$id]) && !empty($exclude[$key]))
1579 $topicData[$id]['exclude'] = (int) $exclude[$key];
1580
1581 // Nada?
1582 if (empty($topicData))
1583 trigger_error('sendNotifications(): topics not found', E_USER_NOTICE);
1584
1585 $topics = array_keys($topicData);
1586 // Just in case they've gone walkies.
1587 if (empty($topics))
1588 return;
1589
1590 // Insert all of these items into the digest log for those who want notifications later.
1591 $digest_insert = array();
1592 foreach ($topicData as $id => $data)
1593 $digest_insert[] = array($data['topic'], $data['last_id'], $type, (int) $data['exclude']);
1594 $smcFunc['db_insert']('',
1595 '{db_prefix}log_digest',
1596 array(
1597 'id_topic' => 'int', 'id_msg' => 'int', 'note_type' => 'string', 'exclude' => 'int',
1598 ),
1599 $digest_insert,
1600 array()
1601 );
1602
1603 // Find the members with notification on for this topic.
1604 $members = $smcFunc['db_query']('', '
1605 SELECT
1606 mem.id_member, mem.email_address, mem.notify_regularity, mem.notify_types, mem.notify_send_body, mem.lngfile,
1607 ln.sent, mem.id_group, mem.additional_groups, b.member_groups, mem.id_post_group, t.id_member_started,
1608 ln.id_topic
1609 FROM {db_prefix}log_notify AS ln
1610 INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ln.id_member)
1611 INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)
1612 INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
1613 WHERE ln.id_topic IN ({array_int:topic_list})
1614 AND mem.notify_types < {int:notify_types}
1615 AND mem.notify_regularity < {int:notify_regularity}
1616 AND mem.is_activated = {int:is_activated}
1617 AND ln.id_member != {int:current_member}' .
1618 (empty($members_only) ? '' : ' AND ln.id_member IN ({array_int:members_only})') . '
1619 ORDER BY mem.lngfile',
1620 array(
1621 'current_member' => $user_info['id'],
1622 'topic_list' => $topics,
1623 'notify_types' => $type == 'reply' ? '4' : '3',
1624 'notify_regularity' => 2,
1625 'is_activated' => 1,
1626 'members_only' => is_array($members_only) ? $members_only : array($members_only),
1627 )
1628 );
1629 $sent = 0;
1630 while ($row = $smcFunc['db_fetch_assoc']($members))
1631 {
1632 // Don't do the excluded...
1633 if ($topicData[$row['id_topic']]['exclude'] == $row['id_member'])
1634 continue;
1635
1636 // Easier to check this here... if they aren't the topic poster do they really want to know?
1637 if ($type != 'reply' && $row['notify_types'] == 2 && $row['id_member'] != $row['id_member_started'])
1638 continue;
1639
1640 if ($row['id_group'] != 1)
1641 {
1642 $allowed = explode(',', $row['member_groups']);
1643 $row['additional_groups'] = explode(',', $row['additional_groups']);
1644 $row['additional_groups'][] = $row['id_group'];
1645 $row['additional_groups'][] = $row['id_post_group'];
1646
1647 if (count(array_intersect($allowed, $row['additional_groups'])) == 0)
1648 continue;
1649 }
1650
1651 $needed_language = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
1652 if (empty($current_language) || $current_language != $needed_language)
1653 $current_language = loadLanguage('Post', $needed_language, false);
1654
1655 $message_type = 'notification_' . $type;
1656 $replacements = array(
1657 'TOPICSUBJECT' => $topicData[$row['id_topic']]['subject'],
1658 'POSTERNAME' => un_htmlspecialchars($topicData[$row['id_topic']]['name']),
1659 'TOPICLINK' => $scripturl . '?topic=' . $row['id_topic'] . '.new;topicseen#new',
1660 'UNSUBSCRIBELINK' => $scripturl . '?action=notify;topic=' . $row['id_topic'] . '.0',
1661 );
1662
1663 if ($type == 'remove')
1664 unset($replacements['TOPICLINK'], $replacements['UNSUBSCRIBELINK']);
1665 // Do they want the body of the message sent too?
1666 if (!empty($row['notify_send_body']) && $type == 'reply' && empty($modSettings['disallow_sendBody']))
1667 {
1668 $message_type .= '_body';
1669 $replacements['MESSAGE'] = $topicData[$row['id_topic']]['body'];
1670 }
1671 if (!empty($row['notify_regularity']) && $type == 'reply')
1672 $message_type .= '_once';
1673
1674 // Send only if once is off or it's on and it hasn't been sent.
1675 if ($type != 'reply' || empty($row['notify_regularity']) || empty($row['sent']))
1676 {
1677 $emaildata = loadEmailTemplate($message_type, $replacements, $needed_language);
1678 sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $topicData[$row['id_topic']]['last_id']);
1679 $sent++;
1680 }
1681 }
1682 $smcFunc['db_free_result']($members);
1683
1684 if (isset($current_language) && $current_language != $user_info['language'])
1685 loadLanguage('Post');
1686
1687 // Sent!
1688 if ($type == 'reply' && !empty($sent))
1689 $smcFunc['db_query']('', '
1690 UPDATE {db_prefix}log_notify
1691 SET sent = {int:is_sent}
1692 WHERE id_topic IN ({array_int:topic_list})
1693 AND id_member != {int:current_member}',
1694 array(
1695 'current_member' => $user_info['id'],
1696 'topic_list' => $topics,
1697 'is_sent' => 1,
1698 )
1699 );
1700
1701 // For approvals we need to unsend the exclusions (This *is* the quickest way!)
1702 if (!empty($sent) && !empty($exclude))
1703 {
1704 foreach ($topicData as $id => $data)
1705 if ($data['exclude'])
1706 $smcFunc['db_query']('', '
1707 UPDATE {db_prefix}log_notify
1708 SET sent = {int:not_sent}
1709 WHERE id_topic = {int:id_topic}
1710 AND id_member = {int:id_member}',
1711 array(
1712 'not_sent' => 0,
1713 'id_topic' => $id,
1714 'id_member' => $data['exclude'],
1715 )
1716 );
1717 }
1718}
1719
1720// Create a post, either as new topic (id_topic = 0) or in an existing one.
1721// The input parameters of this function assume:
1722// - Strings have been escaped.
1723// - Integers have been cast to integer.
1724// - Mandatory parameters are set.
1725function createPost(&$msgOptions, &$topicOptions, &$posterOptions)
1726{
1727 global $user_info, $txt, $modSettings, $smcFunc, $context;
1728
1729 // Set optional parameters to the default value.
1730 $msgOptions['icon'] = empty($msgOptions['icon']) ? 'xx' : $msgOptions['icon'];
1731 $msgOptions['smileys_enabled'] = !empty($msgOptions['smileys_enabled']);
1732 $msgOptions['attachments'] = empty($msgOptions['attachments']) ? array() : $msgOptions['attachments'];
1733 $msgOptions['approved'] = isset($msgOptions['approved']) ? (int) $msgOptions['approved'] : 1;
1734 $topicOptions['id'] = empty($topicOptions['id']) ? 0 : (int) $topicOptions['id'];
1735 $topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
1736 $topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
1737 $topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
1738 $posterOptions['id'] = empty($posterOptions['id']) ? 0 : (int) $posterOptions['id'];
1739 $posterOptions['ip'] = empty($posterOptions['ip']) ? $user_info['ip'] : $posterOptions['ip'];
1740
1741 // We need to know if the topic is approved. If we're told that's great - if not find out.
1742 if (!$modSettings['postmod_active'])
1743 $topicOptions['is_approved'] = true;
1744 elseif (!empty($topicOptions['id']) && !isset($topicOptions['is_approved']))
1745 {
1746 $request = $smcFunc['db_query']('', '
1747 SELECT approved
1748 FROM {db_prefix}topics
1749 WHERE id_topic = {int:id_topic}
1750 LIMIT 1',
1751 array(
1752 'id_topic' => $topicOptions['id'],
1753 )
1754 );
1755 list ($topicOptions['is_approved']) = $smcFunc['db_fetch_row']($request);
1756 $smcFunc['db_free_result']($request);
1757 }
1758
1759 // If nothing was filled in as name/e-mail address, try the member table.
1760 if (!isset($posterOptions['name']) || $posterOptions['name'] == '' || (empty($posterOptions['email']) && !empty($posterOptions['id'])))
1761 {
1762 if (empty($posterOptions['id']))
1763 {
1764 $posterOptions['id'] = 0;
1765 $posterOptions['name'] = $txt['guest_title'];
1766 $posterOptions['email'] = '';
1767 }
1768 elseif ($posterOptions['id'] != $user_info['id'])
1769 {
1770 $request = $smcFunc['db_query']('', '
1771 SELECT member_name, email_address
1772 FROM {db_prefix}members
1773 WHERE id_member = {int:id_member}
1774 LIMIT 1',
1775 array(
1776 'id_member' => $posterOptions['id'],
1777 )
1778 );
1779 // Couldn't find the current poster?
1780 if ($smcFunc['db_num_rows']($request) == 0)
1781 {
1782 trigger_error('createPost(): Invalid member id ' . $posterOptions['id'], E_USER_NOTICE);
1783 $posterOptions['id'] = 0;
1784 $posterOptions['name'] = $txt['guest_title'];
1785 $posterOptions['email'] = '';
1786 }
1787 else
1788 list ($posterOptions['name'], $posterOptions['email']) = $smcFunc['db_fetch_row']($request);
1789 $smcFunc['db_free_result']($request);
1790 }
1791 else
1792 {
1793 $posterOptions['name'] = $user_info['name'];
1794 $posterOptions['email'] = $user_info['email'];
1795 }
1796 }
1797
1798 // It's do or die time: forget any user aborts!
1799 $previous_ignore_user_abort = ignore_user_abort(true);
1800
1801 $new_topic = empty($topicOptions['id']);
1802
1803 // Insert the post.
1804 $smcFunc['db_insert']('',
1805 '{db_prefix}messages',
1806 array(
1807 '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'),
1808 'poster_name' => 'string-255', 'poster_email' => 'string-255', 'poster_time' => 'int', 'poster_ip' => 'string-255',
1809 'smileys_enabled' => 'int', 'modified_name' => 'string', 'icon' => 'string-16', 'approved' => 'int',
1810 ),
1811 array(
1812 $topicOptions['board'], $topicOptions['id'], $posterOptions['id'], $msgOptions['subject'], $msgOptions['body'],
1813 $posterOptions['name'], $posterOptions['email'], time(), $posterOptions['ip'],
1814 $msgOptions['smileys_enabled'] ? 1 : 0, '', $msgOptions['icon'], $msgOptions['approved'],
1815 ),
1816 array('id_msg')
1817 );
1818 $msgOptions['id'] = $smcFunc['db_insert_id']('{db_prefix}messages', 'id_msg');
1819
1820 // Something went wrong creating the message...
1821 if (empty($msgOptions['id']))
1822 return false;
1823
1824 // Fix the attachments.
1825 if (!empty($msgOptions['attachments']))
1826 $smcFunc['db_query']('', '
1827 UPDATE {db_prefix}attachments
1828 SET id_msg = {int:id_msg}
1829 WHERE id_attach IN ({array_int:attachment_list})',
1830 array(
1831 'attachment_list' => $msgOptions['attachments'],
1832 'id_msg' => $msgOptions['id'],
1833 )
1834 );
1835
1836 // Insert a new topic (if the topicID was left empty.)
1837 if ($new_topic)
1838 {
1839 $smcFunc['db_insert']('',
1840 '{db_prefix}topics',
1841 array(
1842 'id_board' => 'int', 'id_member_started' => 'int', 'id_member_updated' => 'int', 'id_first_msg' => 'int',
1843 'id_last_msg' => 'int', 'locked' => 'int', 'is_sticky' => 'int', 'num_views' => 'int',
1844 'id_poll' => 'int', 'unapproved_posts' => 'int', 'approved' => 'int',
1845 ),
1846 array(
1847 $topicOptions['board'], $posterOptions['id'], $posterOptions['id'], $msgOptions['id'],
1848 $msgOptions['id'], $topicOptions['lock_mode'] === null ? 0 : $topicOptions['lock_mode'], $topicOptions['sticky_mode'] === null ? 0 : $topicOptions['sticky_mode'], 0,
1849 $topicOptions['poll'] === null ? 0 : $topicOptions['poll'], $msgOptions['approved'] ? 0 : 1, $msgOptions['approved'],
1850 ),
1851 array('id_topic')
1852 );
1853 $topicOptions['id'] = $smcFunc['db_insert_id']('{db_prefix}topics', 'id_topic');
1854
1855 // The topic couldn't be created for some reason.
1856 if (empty($topicOptions['id']))
1857 {
1858 // We should delete the post that did work, though...
1859 $smcFunc['db_query']('', '
1860 DELETE FROM {db_prefix}messages
1861 WHERE id_msg = {int:id_msg}',
1862 array(
1863 'id_msg' => $msgOptions['id'],
1864 )
1865 );
1866
1867 return false;
1868 }
1869
1870 // Fix the message with the topic.
1871 $smcFunc['db_query']('', '
1872 UPDATE {db_prefix}messages
1873 SET id_topic = {int:id_topic}
1874 WHERE id_msg = {int:id_msg}',
1875 array(
1876 'id_topic' => $topicOptions['id'],
1877 'id_msg' => $msgOptions['id'],
1878 )
1879 );
1880
1881 // There's been a new topic AND a new post today.
1882 trackStats(array('topics' => '+', 'posts' => '+'));
1883
1884 updateStats('topic', true);
1885 updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
1886
1887 // What if we want to export new topics out to a CMS?
1888 call_integration_hook('integrate_create_topic', array($msgOptions, $topicOptions, $posterOptions));
1889 }
1890 // The topic already exists, it only needs a little updating.
1891 else
1892 {
1893 $countChange = $msgOptions['approved'] ? 'num_replies = num_replies + 1' : 'unapproved_posts = unapproved_posts + 1';
1894
1895 // Update the number of replies and the lock/sticky status.
1896 $smcFunc['db_query']('', '
1897 UPDATE {db_prefix}topics
1898 SET
1899 ' . ($msgOptions['approved'] ? 'id_member_updated = {int:poster_id}, id_last_msg = {int:id_msg},' : '') . '
1900 ' . $countChange . ($topicOptions['lock_mode'] === null ? '' : ',
1901 locked = {int:locked}') . ($topicOptions['sticky_mode'] === null ? '' : ',
1902 is_sticky = {int:is_sticky}') . '
1903 WHERE id_topic = {int:id_topic}',
1904 array(
1905 'poster_id' => $posterOptions['id'],
1906 'id_msg' => $msgOptions['id'],
1907 'locked' => $topicOptions['lock_mode'],
1908 'is_sticky' => $topicOptions['sticky_mode'],
1909 'id_topic' => $topicOptions['id'],
1910 )
1911 );
1912
1913 // One new post has been added today.
1914 trackStats(array('posts' => '+'));
1915 }
1916
1917 // Creating is modifying...in a way.
1918 //!!! Why not set id_msg_modified on the insert?
1919 $smcFunc['db_query']('', '
1920 UPDATE {db_prefix}messages
1921 SET id_msg_modified = {int:id_msg}
1922 WHERE id_msg = {int:id_msg}',
1923 array(
1924 'id_msg' => $msgOptions['id'],
1925 )
1926 );
1927
1928 // Increase the number of posts and topics on the board.
1929 if ($msgOptions['approved'])
1930 $smcFunc['db_query']('', '
1931 UPDATE {db_prefix}boards
1932 SET num_posts = num_posts + 1' . ($new_topic ? ', num_topics = num_topics + 1' : '') . '
1933 WHERE id_board = {int:id_board}',
1934 array(
1935 'id_board' => $topicOptions['board'],
1936 )
1937 );
1938 else
1939 {
1940 $smcFunc['db_query']('', '
1941 UPDATE {db_prefix}boards
1942 SET unapproved_posts = unapproved_posts + 1' . ($new_topic ? ', unapproved_topics = unapproved_topics + 1' : '') . '
1943 WHERE id_board = {int:id_board}',
1944 array(
1945 'id_board' => $topicOptions['board'],
1946 )
1947 );
1948
1949 // Add to the approval queue too.
1950 $smcFunc['db_insert']('',
1951 '{db_prefix}approval_queue',
1952 array(
1953 'id_msg' => 'int',
1954 ),
1955 array(
1956 $msgOptions['id'],
1957 ),
1958 array()
1959 );
1960 }
1961
1962 // Mark inserted topic as read (only for the user calling this function).
1963 if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
1964 {
1965 // Since it's likely they *read* it before replying, let's try an UPDATE first.
1966 if (!$new_topic)
1967 {
1968 $smcFunc['db_query']('', '
1969 UPDATE {db_prefix}log_topics
1970 SET id_msg = {int:id_msg}
1971 WHERE id_member = {int:current_member}
1972 AND id_topic = {int:id_topic}',
1973 array(
1974 'current_member' => $posterOptions['id'],
1975 'id_msg' => $msgOptions['id'],
1976 'id_topic' => $topicOptions['id'],
1977 )
1978 );
1979
1980 $flag = $smcFunc['db_affected_rows']() != 0;
1981 }
1982
1983 if (empty($flag))
1984 {
1985 $smcFunc['db_insert']('ignore',
1986 '{db_prefix}log_topics',
1987 array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
1988 array($topicOptions['id'], $posterOptions['id'], $msgOptions['id']),
1989 array('id_topic', 'id_member')
1990 );
1991 }
1992 }
1993
1994 // If there's a custom search index, it needs updating...
1995 if (!empty($modSettings['search_custom_index_config']))
1996 {
1997 $customIndexSettings = safe_unserialize($modSettings['search_custom_index_config']);
1998
1999 $inserts = array();
2000 foreach (text2words($msgOptions['body'], $customIndexSettings['bytes_per_word'], true) as $word)
2001 $inserts[] = array($word, $msgOptions['id']);
2002
2003 if (!empty($inserts))
2004 $smcFunc['db_insert']('ignore',
2005 '{db_prefix}log_search_words',
2006 array('id_word' => 'int', 'id_msg' => 'int'),
2007 $inserts,
2008 array('id_word', 'id_msg')
2009 );
2010 }
2011
2012 // Increase the post counter for the user that created the post.
2013 if (!empty($posterOptions['update_post_count']) && !empty($posterOptions['id']) && $msgOptions['approved'])
2014 {
2015 // Are you the one that happened to create this post?
2016 if ($user_info['id'] == $posterOptions['id'])
2017 $user_info['posts']++;
2018 updateMemberData($posterOptions['id'], array('posts' => '+'));
2019 }
2020
2021 // They've posted, so they can make the view count go up one if they really want. (this is to keep views >= replies...)
2022 $_SESSION['last_read_topic'] = 0;
2023
2024 // Better safe than sorry.
2025 if (isset($_SESSION['topicseen_cache'][$topicOptions['board']]))
2026 $_SESSION['topicseen_cache'][$topicOptions['board']]--;
2027
2028 // Update all the stats so everyone knows about this new topic and message.
2029 updateStats('message', true, $msgOptions['id']);
2030
2031 // Update the last message on the board assuming it's approved AND the topic is.
2032 if ($msgOptions['approved'])
2033 updateLastMessages($topicOptions['board'], $new_topic || !empty($topicOptions['is_approved']) ? $msgOptions['id'] : 0);
2034
2035 // Alright, done now... we can abort now, I guess... at least this much is done.
2036 ignore_user_abort($previous_ignore_user_abort);
2037
2038 // Success.
2039 return true;
2040}
2041
2042// !!!
2043function createAttachment(&$attachmentOptions)
2044{
2045 global $modSettings, $sourcedir, $smcFunc, $context;
2046
2047 require_once($sourcedir . '/Subs-Graphics.php');
2048
2049 // We need to know where this thing is going.
2050 if (!empty($modSettings['currentAttachmentUploadDir']))
2051 {
2052 if (!is_array($modSettings['attachmentUploadDir']))
2053 $modSettings['attachmentUploadDir'] = safe_unserialize($modSettings['attachmentUploadDir']);
2054
2055 // Just use the current path for temp files.
2056 $attach_dir = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
2057 $id_folder = $modSettings['currentAttachmentUploadDir'];
2058 }
2059 else
2060 {
2061 $attach_dir = $modSettings['attachmentUploadDir'];
2062 $id_folder = 1;
2063 }
2064
2065 $attachmentOptions['errors'] = array();
2066 if (!isset($attachmentOptions['post']))
2067 $attachmentOptions['post'] = 0;
2068 if (!isset($attachmentOptions['approved']))
2069 $attachmentOptions['approved'] = 1;
2070
2071 $already_uploaded = preg_match('~^post_tmp_' . $attachmentOptions['poster'] . '_\d+$~', $attachmentOptions['tmp_name']) != 0;
2072 $file_restricted = @ini_get('open_basedir') != '' && !$already_uploaded;
2073
2074 if ($already_uploaded)
2075 $attachmentOptions['tmp_name'] = $attach_dir . '/' . $attachmentOptions['tmp_name'];
2076
2077 // Make sure the file actually exists... sometimes it doesn't.
2078 if ((!$file_restricted && !file_exists($attachmentOptions['tmp_name'])) || (!$already_uploaded && !is_uploaded_file($attachmentOptions['tmp_name'])))
2079 {
2080 $attachmentOptions['errors'] = array('could_not_upload');
2081 return false;
2082 }
2083
2084 // These are the only valid image types for SMF.
2085 $validImageTypes = array(
2086 1 => 'gif',
2087 2 => 'jpeg',
2088 3 => 'png',
2089 5 => 'psd',
2090 6 => 'bmp',
2091 7 => 'tiff',
2092 8 => 'tiff',
2093 9 => 'jpeg',
2094 14 => 'iff'
2095 );
2096
2097 if (!$file_restricted || $already_uploaded)
2098 {
2099 $size = @getimagesize($attachmentOptions['tmp_name']);
2100 list ($attachmentOptions['width'], $attachmentOptions['height']) = $size;
2101
2102 // If it's an image get the mime type right.
2103 if (empty($attachmentOptions['mime_type']) && $attachmentOptions['width'])
2104 {
2105 // Got a proper mime type?
2106 if (!empty($size['mime']))
2107 $attachmentOptions['mime_type'] = $size['mime'];
2108 // Otherwise a valid one?
2109 elseif (isset($validImageTypes[$size[2]]))
2110 $attachmentOptions['mime_type'] = 'image/' . $validImageTypes[$size[2]];
2111 }
2112 }
2113
2114 // It is possible we might have a MIME type that isn't actually an image but still have a size.
2115 // For example, Shockwave files will be able to return size but be 'application/shockwave' or similar.
2116 if (!empty($attachmentOptions['mime_type']) && strpos($attachmentOptions['mime_type'], 'image/') !== 0)
2117 {
2118 $attachmentOptions['width'] = 0;
2119 $attachmentOptions['height'] = 0;
2120 }
2121
2122 // Get the hash if no hash has been given yet.
2123 if (empty($attachmentOptions['file_hash']))
2124 $attachmentOptions['file_hash'] = getAttachmentFilename($attachmentOptions['name'], false, null, true);
2125
2126 // Is the file too big?
2127 if (!empty($modSettings['attachmentSizeLimit']) && $attachmentOptions['size'] > $modSettings['attachmentSizeLimit'] * 1024)
2128 $attachmentOptions['errors'][] = 'too_large';
2129
2130 if (!empty($modSettings['attachmentCheckExtensions']))
2131 {
2132 $allowed = explode(',', strtolower($modSettings['attachmentExtensions']));
2133 foreach ($allowed as $k => $dummy)
2134 $allowed[$k] = trim($dummy);
2135
2136 if (!in_array(strtolower(substr(strrchr($attachmentOptions['name'], '.'), 1)), $allowed))
2137 $attachmentOptions['errors'][] = 'bad_extension';
2138 }
2139
2140 if (!empty($modSettings['attachmentDirSizeLimit']))
2141 {
2142 // Make sure the directory isn't full.
2143 $dirSize = 0;
2144 $dir = @opendir($attach_dir) or fatal_lang_error('cant_access_upload_path', 'critical');
2145 while ($file = readdir($dir))
2146 {
2147 if ($file == '.' || $file == '..')
2148 continue;
2149
2150 if (preg_match('~^post_tmp_\d+_\d+$~', $file) != 0)
2151 {
2152 // Temp file is more than 5 hours old!
2153 if (filemtime($attach_dir . '/' . $file) < time() - 18000)
2154 @unlink($attach_dir . '/' . $file);
2155 continue;
2156 }
2157
2158 $dirSize += filesize($attach_dir . '/' . $file);
2159 }
2160 closedir($dir);
2161
2162 // Too big! Maybe you could zip it or something...
2163 if ($attachmentOptions['size'] + $dirSize > $modSettings['attachmentDirSizeLimit'] * 1024)
2164 $attachmentOptions['errors'][] = 'directory_full';
2165 // Soon to be too big - warn the admins...
2166 elseif (!isset($modSettings['attachment_full_notified']) && $modSettings['attachmentDirSizeLimit'] > 4000 && $attachmentOptions['size'] + $dirSize > ($modSettings['attachmentDirSizeLimit'] - 2000) * 1024)
2167 {
2168 require_once($sourcedir . '/Subs-Admin.php');
2169 emailAdmins('admin_attachments_full');
2170 updateSettings(array('attachment_full_notified' => 1));
2171 }
2172 }
2173
2174 // Check if the file already exists.... (for those who do not encrypt their filenames...)
2175 if (empty($modSettings['attachmentEncryptFilenames']))
2176 {
2177 // Make sure they aren't trying to upload a nasty file.
2178 $disabledFiles = array('con', 'com1', 'com2', 'com3', 'com4', 'prn', 'aux', 'lpt1', '.htaccess', 'index.php');
2179 if (in_array(strtolower(basename($attachmentOptions['name'])), $disabledFiles))
2180 $attachmentOptions['errors'][] = 'bad_filename';
2181
2182 // Check if there's another file with that name...
2183 $request = $smcFunc['db_query']('', '
2184 SELECT id_attach
2185 FROM {db_prefix}attachments
2186 WHERE filename = {string:filename}
2187 LIMIT 1',
2188 array(
2189 'filename' => strtolower($attachmentOptions['name']),
2190 )
2191 );
2192 if ($smcFunc['db_num_rows']($request) > 0)
2193 $attachmentOptions['errors'][] = 'taken_filename';
2194 $smcFunc['db_free_result']($request);
2195 }
2196
2197 if (!empty($attachmentOptions['errors']))
2198 return false;
2199
2200 if (!is_writable($attach_dir))
2201 fatal_lang_error('attachments_no_write', 'critical');
2202
2203 // Assuming no-one set the extension let's take a look at it.
2204 if (empty($attachmentOptions['fileext']))
2205 {
2206 $attachmentOptions['fileext'] = strtolower(strrpos($attachmentOptions['name'], '.') !== false ? substr($attachmentOptions['name'], strrpos($attachmentOptions['name'], '.') + 1) : '');
2207 if (strlen($attachmentOptions['fileext']) > 8 || '.' . $attachmentOptions['fileext'] == $attachmentOptions['name'])
2208 $attachmentOptions['fileext'] = '';
2209 }
2210
2211 $smcFunc['db_insert']('',
2212 '{db_prefix}attachments',
2213 array(
2214 'id_folder' => 'int', 'id_msg' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
2215 'size' => 'int', 'width' => 'int', 'height' => 'int',
2216 'mime_type' => 'string-20', 'approved' => 'int',
2217 ),
2218 array(
2219 $id_folder, (int) $attachmentOptions['post'], $attachmentOptions['name'], $attachmentOptions['file_hash'], $attachmentOptions['fileext'],
2220 (int) $attachmentOptions['size'], (empty($attachmentOptions['width']) ? 0 : (int) $attachmentOptions['width']), (empty($attachmentOptions['height']) ? '0' : (int) $attachmentOptions['height']),
2221 (!empty($attachmentOptions['mime_type']) ? $attachmentOptions['mime_type'] : ''), (int) $attachmentOptions['approved'],
2222 ),
2223 array('id_attach')
2224 );
2225 $attachmentOptions['id'] = $smcFunc['db_insert_id']('{db_prefix}attachments', 'id_attach');
2226
2227 if (empty($attachmentOptions['id']))
2228 return false;
2229
2230 // If it's not approved add to the approval queue.
2231 if (!$attachmentOptions['approved'])
2232 $smcFunc['db_insert']('',
2233 '{db_prefix}approval_queue',
2234 array(
2235 'id_attach' => 'int', 'id_msg' => 'int',
2236 ),
2237 array(
2238 $attachmentOptions['id'], (int) $attachmentOptions['post'],
2239 ),
2240 array()
2241 );
2242
2243 $attachmentOptions['destination'] = getAttachmentFilename(basename($attachmentOptions['name']), $attachmentOptions['id'], $id_folder, false, $attachmentOptions['file_hash']);
2244
2245 if ($already_uploaded)
2246 rename($attachmentOptions['tmp_name'], $attachmentOptions['destination']);
2247 elseif (!move_uploaded_file($attachmentOptions['tmp_name'], $attachmentOptions['destination']))
2248 fatal_lang_error('attach_timeout', 'critical');
2249
2250 // Attempt to chmod it.
2251 @chmod($attachmentOptions['destination'], 0644);
2252
2253 $size = @getimagesize($attachmentOptions['destination']);
2254 list ($attachmentOptions['width'], $attachmentOptions['height']) = empty($size) ? array(null, null, null) : $size;
2255
2256 // We couldn't access the file before...
2257 if ($file_restricted)
2258 {
2259 // Have a go at getting the right mime type.
2260 if (empty($attachmentOptions['mime_type']) && $attachmentOptions['width'])
2261 {
2262 if (!empty($size['mime']))
2263 $attachmentOptions['mime_type'] = $size['mime'];
2264 elseif (isset($validImageTypes[$size[2]]))
2265 $attachmentOptions['mime_type'] = 'image/' . $validImageTypes[$size[2]];
2266 }
2267
2268 // It is possible we might have a MIME type that isn't actually an image but still have a size.
2269 // For example, Shockwave files will be able to return size but be 'application/shockwave' or similar.
2270 if (!empty($attachmentOptions['mime_type']) && strpos($attachmentOptions['mime_type'], 'image/') !== 0)
2271 {
2272 $attachmentOptions['width'] = 0;
2273 $attachmentOptions['height'] = 0;
2274 }
2275
2276 if (!empty($attachmentOptions['width']) && !empty($attachmentOptions['height']))
2277 $smcFunc['db_query']('', '
2278 UPDATE {db_prefix}attachments
2279 SET
2280 width = {int:width},
2281 height = {int:height},
2282 mime_type = {string:mime_type}
2283 WHERE id_attach = {int:id_attach}',
2284 array(
2285 'width' => (int) $attachmentOptions['width'],
2286 'height' => (int) $attachmentOptions['height'],
2287 'id_attach' => $attachmentOptions['id'],
2288 'mime_type' => empty($attachmentOptions['mime_type']) ? '' : $attachmentOptions['mime_type'],
2289 )
2290 );
2291 }
2292
2293 // Security checks for images
2294 // Do we have an image? If yes, we need to check it out!
2295 if (isset($validImageTypes[$size[2]]))
2296 {
2297 if (!checkImageContents($attachmentOptions['destination'], !empty($modSettings['attachment_image_paranoid'])))
2298 {
2299 // It's bad. Last chance, maybe we can re-encode it?
2300 if (empty($modSettings['attachment_image_reencode']) || (!reencodeImage($attachmentOptions['destination'], $size[2])))
2301 {
2302 // Nothing to do: not allowed or not successful re-encoding it.
2303 require_once($sourcedir . '/ManageAttachments.php');
2304 removeAttachments(array(
2305 'id_attach' => $attachmentOptions['id']
2306 ));
2307 $attachmentOptions['id'] = null;
2308 $attachmentOptions['errors'][] = 'bad_attachment';
2309
2310 return false;
2311 }
2312 // Success! However, successes usually come for a price:
2313 // we might get a new format for our image...
2314 $old_format = $size[2];
2315 $size = @getimagesize($attachmentOptions['destination']);
2316 if (!(empty($size)) && ($size[2] != $old_format))
2317 {
2318 // Let's update the image information
2319 // !!! This is becoming a mess: we keep coming back and update the database,
2320 // instead of getting it right the first time.
2321 if (isset($validImageTypes[$size[2]]))
2322 {
2323 $attachmentOptions['mime_type'] = 'image/' . $validImageTypes[$size[2]];
2324 $smcFunc['db_query']('', '
2325 UPDATE {db_prefix}attachments
2326 SET
2327 mime_type = {string:mime_type}
2328 WHERE id_attach = {int:id_attach}',
2329 array(
2330 'id_attach' => $attachmentOptions['id'],
2331 'mime_type' => $attachmentOptions['mime_type'],
2332 )
2333 );
2334 }
2335 }
2336 }
2337 }
2338
2339 if (!empty($attachmentOptions['skip_thumbnail']) || (empty($attachmentOptions['width']) && empty($attachmentOptions['height'])))
2340 return true;
2341
2342 // Like thumbnails, do we?
2343 if (!empty($modSettings['attachmentThumbnails']) && !empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight']) && ($attachmentOptions['width'] > $modSettings['attachmentThumbWidth'] || $attachmentOptions['height'] > $modSettings['attachmentThumbHeight']))
2344 {
2345 if (createThumbnail($attachmentOptions['destination'], $modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight']))
2346 {
2347 // Figure out how big we actually made it.
2348 $size = @getimagesize($attachmentOptions['destination'] . '_thumb');
2349 list ($thumb_width, $thumb_height) = $size;
2350
2351 if (!empty($size['mime']))
2352 $thumb_mime = $size['mime'];
2353 elseif (isset($validImageTypes[$size[2]]))
2354 $thumb_mime = 'image/' . $validImageTypes[$size[2]];
2355 // Lord only knows how this happened...
2356 else
2357 $thumb_mime = '';
2358
2359 $thumb_filename = $attachmentOptions['name'] . '_thumb';
2360 $thumb_size = filesize($attachmentOptions['destination'] . '_thumb');
2361 $thumb_file_hash = getAttachmentFilename($thumb_filename, false, null, true);
2362
2363 // To the database we go!
2364 $smcFunc['db_insert']('',
2365 '{db_prefix}attachments',
2366 array(
2367 'id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
2368 'size' => 'int', 'width' => 'int', 'height' => 'int', 'mime_type' => 'string-20', 'approved' => 'int',
2369 ),
2370 array(
2371 $id_folder, (int) $attachmentOptions['post'], 3, $thumb_filename, $thumb_file_hash, $attachmentOptions['fileext'],
2372 $thumb_size, $thumb_width, $thumb_height, $thumb_mime, (int) $attachmentOptions['approved'],
2373 ),
2374 array('id_attach')
2375 );
2376 $attachmentOptions['thumb'] = $smcFunc['db_insert_id']('{db_prefix}attachments', 'id_attach');
2377
2378 if (!empty($attachmentOptions['thumb']))
2379 {
2380 $smcFunc['db_query']('', '
2381 UPDATE {db_prefix}attachments
2382 SET id_thumb = {int:id_thumb}
2383 WHERE id_attach = {int:id_attach}',
2384 array(
2385 'id_thumb' => $attachmentOptions['thumb'],
2386 'id_attach' => $attachmentOptions['id'],
2387 )
2388 );
2389
2390 rename($attachmentOptions['destination'] . '_thumb', getAttachmentFilename($thumb_filename, $attachmentOptions['thumb'], $id_folder, false, $thumb_file_hash));
2391 }
2392 }
2393 }
2394
2395 return true;
2396}
2397
2398// !!!
2399function modifyPost(&$msgOptions, &$topicOptions, &$posterOptions)
2400{
2401 global $user_info, $modSettings, $smcFunc, $context;
2402
2403 $topicOptions['poll'] = isset($topicOptions['poll']) ? (int) $topicOptions['poll'] : null;
2404 $topicOptions['lock_mode'] = isset($topicOptions['lock_mode']) ? $topicOptions['lock_mode'] : null;
2405 $topicOptions['sticky_mode'] = isset($topicOptions['sticky_mode']) ? $topicOptions['sticky_mode'] : null;
2406
2407 // This is longer than it has to be, but makes it so we only set/change what we have to.
2408 $messages_columns = array();
2409 if (isset($posterOptions['name']))
2410 $messages_columns['poster_name'] = $posterOptions['name'];
2411 if (isset($posterOptions['email']))
2412 $messages_columns['poster_email'] = $posterOptions['email'];
2413 if (isset($msgOptions['icon']))
2414 $messages_columns['icon'] = $msgOptions['icon'];
2415 if (isset($msgOptions['subject']))
2416 $messages_columns['subject'] = $msgOptions['subject'];
2417 if (isset($msgOptions['body']))
2418 {
2419 $messages_columns['body'] = $msgOptions['body'];
2420
2421 if (!empty($modSettings['search_custom_index_config']))
2422 {
2423 $request = $smcFunc['db_query']('', '
2424 SELECT body
2425 FROM {db_prefix}messages
2426 WHERE id_msg = {int:id_msg}',
2427 array(
2428 'id_msg' => $msgOptions['id'],
2429 )
2430 );
2431 list ($old_body) = $smcFunc['db_fetch_row']($request);
2432 $smcFunc['db_free_result']($request);
2433 }
2434 }
2435 if (!empty($msgOptions['modify_time']))
2436 {
2437 $messages_columns['modified_time'] = $msgOptions['modify_time'];
2438 $messages_columns['modified_name'] = $msgOptions['modify_name'];
2439 $messages_columns['id_msg_modified'] = $modSettings['maxMsgID'];
2440 }
2441 if (isset($msgOptions['smileys_enabled']))
2442 $messages_columns['smileys_enabled'] = empty($msgOptions['smileys_enabled']) ? 0 : 1;
2443
2444 // Which columns need to be ints?
2445 $messageInts = array('modified_time', 'id_msg_modified', 'smileys_enabled');
2446 $update_parameters = array(
2447 'id_msg' => $msgOptions['id'],
2448 );
2449
2450 foreach ($messages_columns as $var => $val)
2451 {
2452 $messages_columns[$var] = $var . ' = {' . (in_array($var, $messageInts) ? 'int' : 'string') . ':var_' . $var . '}';
2453 $update_parameters['var_' . $var] = $val;
2454 }
2455
2456 // Nothing to do?
2457 if (empty($messages_columns))
2458 return true;
2459
2460 // Change the post.
2461 $smcFunc['db_query']('', '
2462 UPDATE {db_prefix}messages
2463 SET ' . implode(', ', $messages_columns) . '
2464 WHERE id_msg = {int:id_msg}',
2465 $update_parameters
2466 );
2467
2468 // Lock and or sticky the post.
2469 if ($topicOptions['sticky_mode'] !== null || $topicOptions['lock_mode'] !== null || $topicOptions['poll'] !== null)
2470 {
2471 $smcFunc['db_query']('', '
2472 UPDATE {db_prefix}topics
2473 SET
2474 is_sticky = {raw:is_sticky},
2475 locked = {raw:locked},
2476 id_poll = {raw:id_poll}
2477 WHERE id_topic = {int:id_topic}',
2478 array(
2479 'is_sticky' => $topicOptions['sticky_mode'] === null ? 'is_sticky' : (int) $topicOptions['sticky_mode'],
2480 'locked' => $topicOptions['lock_mode'] === null ? 'locked' : (int) $topicOptions['lock_mode'],
2481 'id_poll' => $topicOptions['poll'] === null ? 'id_poll' : (int) $topicOptions['poll'],
2482 'id_topic' => $topicOptions['id'],
2483 )
2484 );
2485 }
2486
2487 // Mark the edited post as read.
2488 if (!empty($topicOptions['mark_as_read']) && !$user_info['is_guest'])
2489 {
2490 // Since it's likely they *read* it before editing, let's try an UPDATE first.
2491 $smcFunc['db_query']('', '
2492 UPDATE {db_prefix}log_topics
2493 SET id_msg = {int:id_msg}
2494 WHERE id_member = {int:current_member}
2495 AND id_topic = {int:id_topic}',
2496 array(
2497 'current_member' => $user_info['id'],
2498 'id_msg' => $modSettings['maxMsgID'],
2499 'id_topic' => $topicOptions['id'],
2500 )
2501 );
2502
2503 $flag = $smcFunc['db_affected_rows']() != 0;
2504
2505 if (empty($flag))
2506 {
2507 $smcFunc['db_insert']('ignore',
2508 '{db_prefix}log_topics',
2509 array('id_topic' => 'int', 'id_member' => 'int', 'id_msg' => 'int'),
2510 array($topicOptions['id'], $user_info['id'], $modSettings['maxMsgID']),
2511 array('id_topic', 'id_member')
2512 );
2513 }
2514 }
2515
2516 // If there's a custom search index, it needs to be modified...
2517 if (isset($msgOptions['body']) && !empty($modSettings['search_custom_index_config']))
2518 {
2519 $customIndexSettings = safe_unserialize($modSettings['search_custom_index_config']);
2520
2521 $stopwords = empty($modSettings['search_stopwords']) ? array() : explode(',', $modSettings['search_stopwords']);
2522 $old_index = text2words($old_body, $customIndexSettings['bytes_per_word'], true);
2523 $new_index = text2words($msgOptions['body'], $customIndexSettings['bytes_per_word'], true);
2524
2525 // Calculate the words to be added and removed from the index.
2526 $removed_words = array_diff(array_diff($old_index, $new_index), $stopwords);
2527 $inserted_words = array_diff(array_diff($new_index, $old_index), $stopwords);
2528 // Delete the removed words AND the added ones to avoid key constraints.
2529 if (!empty($removed_words))
2530 {
2531 $removed_words = array_merge($removed_words, $inserted_words);
2532 $smcFunc['db_query']('', '
2533 DELETE FROM {db_prefix}log_search_words
2534 WHERE id_msg = {int:id_msg}
2535 AND id_word IN ({array_int:removed_words})',
2536 array(
2537 'removed_words' => $removed_words,
2538 'id_msg' => $msgOptions['id'],
2539 )
2540 );
2541 }
2542
2543 // Add the new words to be indexed.
2544 if (!empty($inserted_words))
2545 {
2546 $inserts = array();
2547 foreach ($inserted_words as $word)
2548 $inserts[] = array($word, $msgOptions['id']);
2549 $smcFunc['db_insert']('insert',
2550 '{db_prefix}log_search_words',
2551 array('id_word' => 'string', 'id_msg' => 'int'),
2552 $inserts,
2553 array('id_word', 'id_msg')
2554 );
2555 }
2556 }
2557
2558 if (isset($msgOptions['subject']))
2559 {
2560 // Only update the subject if this was the first message in the topic.
2561 $request = $smcFunc['db_query']('', '
2562 SELECT id_topic
2563 FROM {db_prefix}topics
2564 WHERE id_first_msg = {int:id_first_msg}
2565 LIMIT 1',
2566 array(
2567 'id_first_msg' => $msgOptions['id'],
2568 )
2569 );
2570 if ($smcFunc['db_num_rows']($request) == 1)
2571 updateStats('subject', $topicOptions['id'], $msgOptions['subject']);
2572 $smcFunc['db_free_result']($request);
2573 }
2574
2575 // Finally, if we are setting the approved state we need to do much more work :(
2576 if ($modSettings['postmod_active'] && isset($msgOptions['approved']))
2577 approvePosts($msgOptions['id'], $msgOptions['approved']);
2578
2579 return true;
2580}
2581
2582// Approve (or not) some posts... without permission checks...
2583function approvePosts($msgs, $approve = true)
2584{
2585 global $sourcedir, $smcFunc;
2586
2587 if (!is_array($msgs))
2588 $msgs = array($msgs);
2589
2590 if (empty($msgs))
2591 return false;
2592
2593 // May as well start at the beginning, working out *what* we need to change.
2594 $request = $smcFunc['db_query']('', '
2595 SELECT m.id_msg, m.approved, m.id_topic, m.id_board, t.id_first_msg, t.id_last_msg,
2596 m.body, m.subject, IFNULL(mem.real_name, m.poster_name) AS poster_name, m.id_member,
2597 t.approved AS topic_approved, b.count_posts
2598 FROM {db_prefix}messages AS m
2599 INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
2600 INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
2601 LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
2602 WHERE m.id_msg IN ({array_int:message_list})
2603 AND m.approved = {int:approved_state}',
2604 array(
2605 'message_list' => $msgs,
2606 'approved_state' => $approve ? 0 : 1,
2607 )
2608 );
2609 $msgs = array();
2610 $topics = array();
2611 $topic_changes = array();
2612 $board_changes = array();
2613 $notification_topics = array();
2614 $notification_posts = array();
2615 $member_post_changes = array();
2616 while ($row = $smcFunc['db_fetch_assoc']($request))
2617 {
2618 // Easy...
2619 $msgs[] = $row['id_msg'];
2620 $topics[] = $row['id_topic'];
2621
2622 // Ensure our change array exists already.
2623 if (!isset($topic_changes[$row['id_topic']]))
2624 $topic_changes[$row['id_topic']] = array(
2625 'id_last_msg' => $row['id_last_msg'],
2626 'approved' => $row['topic_approved'],
2627 'replies' => 0,
2628 'unapproved_posts' => 0,
2629 );
2630 if (!isset($board_changes[$row['id_board']]))
2631 $board_changes[$row['id_board']] = array(
2632 'posts' => 0,
2633 'topics' => 0,
2634 'unapproved_posts' => 0,
2635 'unapproved_topics' => 0,
2636 );
2637
2638 // If it's the first message then the topic state changes!
2639 if ($row['id_msg'] == $row['id_first_msg'])
2640 {
2641 $topic_changes[$row['id_topic']]['approved'] = $approve ? 1 : 0;
2642
2643 $board_changes[$row['id_board']]['unapproved_topics'] += $approve ? -1 : 1;
2644 $board_changes[$row['id_board']]['topics'] += $approve ? 1 : -1;
2645
2646 // Note we need to ensure we announce this topic!
2647 $notification_topics[] = array(
2648 'body' => $row['body'],
2649 'subject' => $row['subject'],
2650 'name' => $row['poster_name'],
2651 'board' => $row['id_board'],
2652 'topic' => $row['id_topic'],
2653 'msg' => $row['id_first_msg'],
2654 'poster' => $row['id_member'],
2655 );
2656 }
2657 else
2658 {
2659 $topic_changes[$row['id_topic']]['replies'] += $approve ? 1 : -1;
2660
2661 // This will be a post... but don't notify unless it's not followed by approved ones.
2662 if ($row['id_msg'] > $row['id_last_msg'])
2663 $notification_posts[$row['id_topic']][] = array(
2664 'id' => $row['id_msg'],
2665 'body' => $row['body'],
2666 'subject' => $row['subject'],
2667 'name' => $row['poster_name'],
2668 'topic' => $row['id_topic'],
2669 );
2670 }
2671
2672 // If this is being approved and id_msg is higher than the current id_last_msg then it changes.
2673 if ($approve && $row['id_msg'] > $topic_changes[$row['id_topic']]['id_last_msg'])
2674 $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_msg'];
2675 // If this is being unapproved, and it's equal to the id_last_msg we need to find a new one!
2676 elseif (!$approve)
2677 // Default to the first message and then we'll override in a bit ;)
2678 $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_first_msg'];
2679
2680 $topic_changes[$row['id_topic']]['unapproved_posts'] += $approve ? -1 : 1;
2681 $board_changes[$row['id_board']]['unapproved_posts'] += $approve ? -1 : 1;
2682 $board_changes[$row['id_board']]['posts'] += $approve ? 1 : -1;
2683
2684 // Post count for the user?
2685 if ($row['id_member'] && empty($row['count_posts']))
2686 $member_post_changes[$row['id_member']] = isset($member_post_changes[$row['id_member']]) ? $member_post_changes[$row['id_member']] + 1 : 1;
2687 }
2688 $smcFunc['db_free_result']($request);
2689
2690 if (empty($msgs))
2691 return;
2692
2693 // Now we have the differences make the changes, first the easy one.
2694 $smcFunc['db_query']('', '
2695 UPDATE {db_prefix}messages
2696 SET approved = {int:approved_state}
2697 WHERE id_msg IN ({array_int:message_list})',
2698 array(
2699 'message_list' => $msgs,
2700 'approved_state' => $approve ? 1 : 0,
2701 )
2702 );
2703
2704 // If we were unapproving find the last msg in the topics...
2705 if (!$approve)
2706 {
2707 $request = $smcFunc['db_query']('', '
2708 SELECT id_topic, MAX(id_msg) AS id_last_msg
2709 FROM {db_prefix}messages
2710 WHERE id_topic IN ({array_int:topic_list})
2711 AND approved = {int:approved}
2712 GROUP BY id_topic',
2713 array(
2714 'topic_list' => $topics,
2715 'approved' => 1,
2716 )
2717 );
2718 while ($row = $smcFunc['db_fetch_assoc']($request))
2719 $topic_changes[$row['id_topic']]['id_last_msg'] = $row['id_last_msg'];
2720 $smcFunc['db_free_result']($request);
2721 }
2722
2723 // ... next the topics...
2724 foreach ($topic_changes as $id => $changes)
2725 $smcFunc['db_query']('', '
2726 UPDATE {db_prefix}topics
2727 SET approved = {int:approved}, unapproved_posts = unapproved_posts + {int:unapproved_posts},
2728 num_replies = num_replies + {int:num_replies}, id_last_msg = {int:id_last_msg}
2729 WHERE id_topic = {int:id_topic}',
2730 array(
2731 'approved' => $changes['approved'],
2732 'unapproved_posts' => $changes['unapproved_posts'],
2733 'num_replies' => $changes['replies'],
2734 'id_last_msg' => $changes['id_last_msg'],
2735 'id_topic' => $id,
2736 )
2737 );
2738
2739 // ... finally the boards...
2740 foreach ($board_changes as $id => $changes)
2741 $smcFunc['db_query']('', '
2742 UPDATE {db_prefix}boards
2743 SET num_posts = num_posts + {int:num_posts}, unapproved_posts = unapproved_posts + {int:unapproved_posts},
2744 num_topics = num_topics + {int:num_topics}, unapproved_topics = unapproved_topics + {int:unapproved_topics}
2745 WHERE id_board = {int:id_board}',
2746 array(
2747 'num_posts' => $changes['posts'],
2748 'unapproved_posts' => $changes['unapproved_posts'],
2749 'num_topics' => $changes['topics'],
2750 'unapproved_topics' => $changes['unapproved_topics'],
2751 'id_board' => $id,
2752 )
2753 );
2754
2755 // Finally, least importantly, notifications!
2756 if ($approve)
2757 {
2758 if (!empty($notification_topics))
2759 {
2760 require_once($sourcedir . '/Post.php');
2761 notifyMembersBoard($notification_topics);
2762 }
2763 if (!empty($notification_posts))
2764 sendApprovalNotifications($notification_posts);
2765
2766 $smcFunc['db_query']('', '
2767 DELETE FROM {db_prefix}approval_queue
2768 WHERE id_msg IN ({array_int:message_list})
2769 AND id_attach = {int:id_attach}',
2770 array(
2771 'message_list' => $msgs,
2772 'id_attach' => 0,
2773 )
2774 );
2775 }
2776 // If unapproving add to the approval queue!
2777 else
2778 {
2779 $msgInserts = array();
2780 foreach ($msgs as $msg)
2781 $msgInserts[] = array($msg);
2782
2783 $smcFunc['db_insert']('ignore',
2784 '{db_prefix}approval_queue',
2785 array('id_msg' => 'int'),
2786 $msgInserts,
2787 array('id_msg')
2788 );
2789 }
2790
2791 // Update the last messages on the boards...
2792 updateLastMessages(array_keys($board_changes));
2793
2794 // Post count for the members?
2795 if (!empty($member_post_changes))
2796 foreach ($member_post_changes as $id_member => $count_change)
2797 updateMemberData($id_member, array('posts' => 'posts ' . ($approve ? '+' : '-') . ' ' . $count_change));
2798
2799 return true;
2800}
2801
2802// Approve topics?
2803function approveTopics($topics, $approve = true)
2804{
2805 global $smcFunc;
2806
2807 if (!is_array($topics))
2808 $topics = array($topics);
2809
2810 if (empty($topics))
2811 return false;
2812
2813 $approve_type = $approve ? 0 : 1;
2814
2815 // Just get the messages to be approved and pass through...
2816 $request = $smcFunc['db_query']('', '
2817 SELECT id_msg
2818 FROM {db_prefix}messages
2819 WHERE id_topic IN ({array_int:topic_list})
2820 AND approved = {int:approve_type}',
2821 array(
2822 'topic_list' => $topics,
2823 'approve_type' => $approve_type,
2824 )
2825 );
2826 $msgs = array();
2827 while ($row = $smcFunc['db_fetch_assoc']($request))
2828 $msgs[] = $row['id_msg'];
2829 $smcFunc['db_free_result']($request);
2830
2831 return approvePosts($msgs, $approve);
2832}
2833
2834// A special function for handling the hell which is sending approval notifications.
2835function sendApprovalNotifications(&$topicData)
2836{
2837 global $txt, $scripturl, $language, $user_info;
2838 global $modSettings, $sourcedir, $context, $smcFunc;
2839
2840 // Clean up the data...
2841 if (!is_array($topicData) || empty($topicData))
2842 return;
2843
2844 $topics = array();
2845 $digest_insert = array();
2846 foreach ($topicData as $topic => $msgs)
2847 foreach ($msgs as $msgKey => $msg)
2848 {
2849 censorText($topicData[$topic][$msgKey]['subject']);
2850 censorText($topicData[$topic][$msgKey]['body']);
2851 $topicData[$topic][$msgKey]['subject'] = un_htmlspecialchars($topicData[$topic][$msgKey]['subject']);
2852 $topicData[$topic][$msgKey]['body'] = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc($topicData[$topic][$msgKey]['body'], false), array('<br />' => "\n", '</div>' => "\n", '</li>' => "\n", '[' => '[', ']' => ']')))));
2853
2854 $topics[] = $msg['id'];
2855 $digest_insert[] = array($msg['topic'], $msg['id'], 'reply', $user_info['id']);
2856 }
2857
2858 // These need to go into the digest too...
2859 $smcFunc['db_insert']('',
2860 '{db_prefix}log_digest',
2861 array(
2862 'id_topic' => 'int', 'id_msg' => 'int', 'note_type' => 'string', 'exclude' => 'int',
2863 ),
2864 $digest_insert,
2865 array()
2866 );
2867
2868 // Find everyone who needs to know about this.
2869 $members = $smcFunc['db_query']('', '
2870 SELECT
2871 mem.id_member, mem.email_address, mem.notify_regularity, mem.notify_types, mem.notify_send_body, mem.lngfile,
2872 ln.sent, mem.id_group, mem.additional_groups, b.member_groups, mem.id_post_group, t.id_member_started,
2873 ln.id_topic
2874 FROM {db_prefix}log_notify AS ln
2875 INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ln.id_member)
2876 INNER JOIN {db_prefix}topics AS t ON (t.id_topic = ln.id_topic)
2877 INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
2878 WHERE ln.id_topic IN ({array_int:topic_list})
2879 AND mem.is_activated = {int:is_activated}
2880 AND mem.notify_types < {int:notify_types}
2881 AND mem.notify_regularity < {int:notify_regularity}
2882 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
2883 ORDER BY mem.lngfile',
2884 array(
2885 'topic_list' => $topics,
2886 'is_activated' => 1,
2887 'notify_types' => 4,
2888 'notify_regularity' => 2,
2889 )
2890 );
2891 $sent = 0;
2892 while ($row = $smcFunc['db_fetch_assoc']($members))
2893 {
2894 if ($row['id_group'] != 1)
2895 {
2896 $allowed = explode(',', $row['member_groups']);
2897 $row['additional_groups'] = explode(',', $row['additional_groups']);
2898 $row['additional_groups'][] = $row['id_group'];
2899 $row['additional_groups'][] = $row['id_post_group'];
2900
2901 if (count(array_intersect($allowed, $row['additional_groups'])) == 0)
2902 continue;
2903 }
2904
2905 $needed_language = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
2906 if (empty($current_language) || $current_language != $needed_language)
2907 $current_language = loadLanguage('Post', $needed_language, false);
2908
2909 $sent_this_time = false;
2910 // Now loop through all the messages to send.
2911 foreach ($topicData[$row['id_topic']] as $msg)
2912 {
2913 $replacements = array(
2914 'TOPICSUBJECT' => $topicData[$row['id_topic']]['subject'],
2915 'POSTERNAME' => un_htmlspecialchars($topicData[$row['id_topic']]['name']),
2916 'TOPICLINK' => $scripturl . '?topic=' . $row['id_topic'] . '.new;topicseen#new',
2917 'UNSUBSCRIBELINK' => $scripturl . '?action=notify;topic=' . $row['id_topic'] . '.0',
2918 );
2919
2920 $message_type = 'notification_reply';
2921 // Do they want the body of the message sent too?
2922 if (!empty($row['notify_send_body']) && empty($modSettings['disallow_sendBody']))
2923 {
2924 $message_type .= '_body';
2925 $replacements['BODY'] = $topicData[$row['id_topic']]['body'];
2926 }
2927 if (!empty($row['notify_regularity']))
2928 $message_type .= '_once';
2929
2930 // Send only if once is off or it's on and it hasn't been sent.
2931 if (empty($row['notify_regularity']) || (empty($row['sent']) && !$sent_this_time))
2932 {
2933 $emaildata = loadEmailTemplate($message_type, $replacements, $needed_language);
2934 sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, 'm' . $topicData[$row['id_topic']]['last_id']);
2935 $sent++;
2936 }
2937
2938 $sent_this_time = true;
2939 }
2940 }
2941 $smcFunc['db_free_result']($members);
2942
2943 if (isset($current_language) && $current_language != $user_info['language'])
2944 loadLanguage('Post');
2945
2946 // Sent!
2947 if (!empty($sent))
2948 $smcFunc['db_query']('', '
2949 UPDATE {db_prefix}log_notify
2950 SET sent = {int:is_sent}
2951 WHERE id_topic IN ({array_int:topic_list})
2952 AND id_member != {int:current_member}',
2953 array(
2954 'current_member' => $user_info['id'],
2955 'topic_list' => $topics,
2956 'is_sent' => 1,
2957 )
2958 );
2959}
2960
2961// Update the last message in a board, and its parents.
2962function updateLastMessages($setboards, $id_msg = 0)
2963{
2964 global $board_info, $board, $modSettings, $smcFunc;
2965
2966 // Please - let's be sane.
2967 if (empty($setboards))
2968 return false;
2969
2970 if (!is_array($setboards))
2971 $setboards = array($setboards);
2972
2973 // If we don't know the id_msg we need to find it.
2974 if (!$id_msg)
2975 {
2976 // Find the latest message on this board (highest id_msg.)
2977 $request = $smcFunc['db_query']('', '
2978 SELECT id_board, MAX(id_last_msg) AS id_msg
2979 FROM {db_prefix}topics
2980 WHERE id_board IN ({array_int:board_list})
2981 AND approved = {int:approved}
2982 GROUP BY id_board',
2983 array(
2984 'board_list' => $setboards,
2985 'approved' => 1,
2986 )
2987 );
2988 $lastMsg = array();
2989 while ($row = $smcFunc['db_fetch_assoc']($request))
2990 $lastMsg[$row['id_board']] = $row['id_msg'];
2991 $smcFunc['db_free_result']($request);
2992 }
2993 else
2994 {
2995 // Just to note - there should only be one board passed if we are doing this.
2996 foreach ($setboards as $id_board)
2997 $lastMsg[$id_board] = $id_msg;
2998 }
2999
3000 $parent_boards = array();
3001 // Keep track of last modified dates.
3002 $lastModified = $lastMsg;
3003 // Get all the child boards for the parents, if they have some...
3004 foreach ($setboards as $id_board)
3005 {
3006 if (!isset($lastMsg[$id_board]))
3007 {
3008 $lastMsg[$id_board] = 0;
3009 $lastModified[$id_board] = 0;
3010 }
3011
3012 if (!empty($board) && $id_board == $board)
3013 $parents = $board_info['parent_boards'];
3014 else
3015 $parents = getBoardParents($id_board);
3016
3017 // Ignore any parents on the top child level.
3018 //!!! Why?
3019 foreach ($parents as $id => $parent)
3020 {
3021 if ($parent['level'] != 0)
3022 {
3023 // If we're already doing this one as a board, is this a higher last modified?
3024 if (isset($lastModified[$id]) && $lastModified[$id_board] > $lastModified[$id])
3025 $lastModified[$id] = $lastModified[$id_board];
3026 elseif (!isset($lastModified[$id]) && (!isset($parent_boards[$id]) || $parent_boards[$id] < $lastModified[$id_board]))
3027 $parent_boards[$id] = $lastModified[$id_board];
3028 }
3029 }
3030 }
3031
3032 // Note to help understand what is happening here. For parents we update the timestamp of the last message for determining
3033 // whether there are child boards which have not been read. For the boards themselves we update both this and id_last_msg.
3034
3035 $board_updates = array();
3036 $parent_updates = array();
3037 // Finally, to save on queries make the changes...
3038 foreach ($parent_boards as $id => $msg)
3039 {
3040 if (!isset($parent_updates[$msg]))
3041 $parent_updates[$msg] = array($id);
3042 else
3043 $parent_updates[$msg][] = $id;
3044 }
3045
3046 foreach ($lastMsg as $id => $msg)
3047 {
3048 if (!isset($board_updates[$msg . '-' . $lastModified[$id]]))
3049 $board_updates[$msg . '-' . $lastModified[$id]] = array(
3050 'id' => $msg,
3051 'updated' => $lastModified[$id],
3052 'boards' => array($id)
3053 );
3054
3055 else
3056 $board_updates[$msg . '-' . $lastModified[$id]]['boards'][] = $id;
3057 }
3058
3059 // Now commit the changes!
3060 foreach ($parent_updates as $id_msg => $boards)
3061 {
3062 $smcFunc['db_query']('', '
3063 UPDATE {db_prefix}boards
3064 SET id_msg_updated = {int:id_msg_updated}
3065 WHERE id_board IN ({array_int:board_list})
3066 AND id_msg_updated < {int:id_msg_updated}',
3067 array(
3068 'board_list' => $boards,
3069 'id_msg_updated' => $id_msg,
3070 )
3071 );
3072 }
3073 foreach ($board_updates as $board_data)
3074 {
3075 $smcFunc['db_query']('', '
3076 UPDATE {db_prefix}boards
3077 SET id_last_msg = {int:id_last_msg}, id_msg_updated = {int:id_msg_updated}
3078 WHERE id_board IN ({array_int:board_list})',
3079 array(
3080 'board_list' => $board_data['boards'],
3081 'id_last_msg' => $board_data['id'],
3082 'id_msg_updated' => $board_data['updated'],
3083 )
3084 );
3085 }
3086}
3087
3088// This simple function gets a list of all administrators and sends them an email to let them know a new member has joined.
3089function adminNotify($type, $memberID, $member_name = null)
3090{
3091 global $txt, $modSettings, $language, $scripturl, $user_info, $context, $smcFunc;
3092
3093 // If the setting isn't enabled then just exit.
3094 if (empty($modSettings['notify_new_registration']))
3095 return;
3096
3097 if ($member_name == null)
3098 {
3099 // Get the new user's name....
3100 $request = $smcFunc['db_query']('', '
3101 SELECT real_name
3102 FROM {db_prefix}members
3103 WHERE id_member = {int:id_member}
3104 LIMIT 1',
3105 array(
3106 'id_member' => $memberID,
3107 )
3108 );
3109 list ($member_name) = $smcFunc['db_fetch_row']($request);
3110 $smcFunc['db_free_result']($request);
3111 }
3112
3113 $toNotify = array();
3114 $groups = array();
3115
3116 // All membergroups who can approve members.
3117 $request = $smcFunc['db_query']('', '
3118 SELECT id_group
3119 FROM {db_prefix}permissions
3120 WHERE permission = {string:moderate_forum}
3121 AND add_deny = {int:add_deny}
3122 AND id_group != {int:id_group}',
3123 array(
3124 'add_deny' => 1,
3125 'id_group' => 0,
3126 'moderate_forum' => 'moderate_forum',
3127 )
3128 );
3129 while ($row = $smcFunc['db_fetch_assoc']($request))
3130 $groups[] = $row['id_group'];
3131 $smcFunc['db_free_result']($request);
3132
3133 // Add administrators too...
3134 $groups[] = 1;
3135 $groups = array_unique($groups);
3136
3137 // Get a list of all members who have ability to approve accounts - these are the people who we inform.
3138 $request = $smcFunc['db_query']('', '
3139 SELECT id_member, lngfile, email_address
3140 FROM {db_prefix}members
3141 WHERE (id_group IN ({array_int:group_list}) OR FIND_IN_SET({raw:group_array_implode}, additional_groups) != 0)
3142 AND notify_types != {int:notify_types}
3143 ORDER BY lngfile',
3144 array(
3145 'group_list' => $groups,
3146 'notify_types' => 4,
3147 'group_array_implode' => implode(', additional_groups) != 0 OR FIND_IN_SET(', $groups),
3148 )
3149 );
3150 while ($row = $smcFunc['db_fetch_assoc']($request))
3151 {
3152 $replacements = array(
3153 'USERNAME' => $member_name,
3154 'PROFILELINK' => $scripturl . '?action=profile;u=' . $memberID
3155 );
3156 $emailtype = 'admin_notify';
3157
3158 // If they need to be approved add more info...
3159 if ($type == 'approval')
3160 {
3161 $replacements['APPROVALLINK'] = $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve';
3162 $emailtype .= '_approval';
3163 }
3164
3165 $emaildata = loadEmailTemplate($emailtype, $replacements, empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']);
3166
3167 // And do the actual sending...
3168 sendmail($row['email_address'], $emaildata['subject'], $emaildata['body'], null, null, false, 0);
3169 }
3170 $smcFunc['db_free_result']($request);
3171
3172 if (isset($current_language) && $current_language != $user_info['language'])
3173 loadLanguage('Login');
3174}
3175
3176function loadEmailTemplate($template, $replacements = array(), $lang = '', $loadLang = true)
3177{
3178 global $txt, $mbname, $scripturl, $settings, $user_info;
3179
3180 // First things first, load up the email templates language file, if we need to.
3181 if ($loadLang)
3182 loadLanguage('EmailTemplates', $lang);
3183
3184 if (!isset($txt['emails'][$template]))
3185 fatal_lang_error('email_no_template', 'template', array($template));
3186
3187 $ret = array(
3188 'subject' => $txt['emails'][$template]['subject'],
3189 'body' => $txt['emails'][$template]['body'],
3190 );
3191
3192 // Add in the default replacements.
3193 $replacements += array(
3194 'FORUMNAME' => $mbname,
3195 'SCRIPTURL' => $scripturl,
3196 'THEMEURL' => $settings['theme_url'],
3197 'IMAGESURL' => $settings['images_url'],
3198 'DEFAULT_THEMEURL' => $settings['default_theme_url'],
3199 'REGARDS' => $txt['regards_team'],
3200 );
3201
3202 // Split the replacements up into two arrays, for use with str_replace
3203 $find = array();
3204 $replace = array();
3205
3206 foreach ($replacements as $f => $r)
3207 {
3208 $find[] = '{' . $f . '}';
3209 $replace[] = $r;
3210 }
3211
3212 // Do the variable replacements.
3213 $ret['subject'] = str_replace($find, $replace, $ret['subject']);
3214 $ret['body'] = str_replace($find, $replace, $ret['body']);
3215
3216 // Now deal with the {USER.variable} items.
3217 $ret['subject'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['subject']);
3218 $ret['body'] = preg_replace_callback('~{USER.([^}]+)}~', 'user_info_callback', $ret['body']);
3219
3220 // Finally return the email to the caller so they can send it out.
3221 return $ret;
3222}
3223
3224function user_info_callback($matches)
3225{
3226 global $user_info;
3227 if (empty($matches[1]))
3228 return '';
3229
3230 $use_ref = true;
3231 $ref = &$user_info;
3232
3233 foreach (explode('.', $matches[1]) as $index)
3234 {
3235 if ($use_ref && isset($ref[$index]))
3236 $ref = &$ref[$index];
3237 else
3238 {
3239 $use_ref = false;
3240 break;
3241 }
3242 }
3243
3244 return $use_ref ? $ref : $matches[0];
3245}
3246
3247function action_fix__preg_callback($matches)
3248{
3249 return $matches[1] . preg_replace('~action(=|%3d)(?!dlattach)~i', 'action-', $matches[2]) . '[/img]';
3250}
3251
3252function mime_convert__preg_callback($matches)
3253{
3254 // I get the feeling we could possibly ditch this and reuse fixchar__callback but handling for < 0x20
3255 // may not be appropriate here.
3256
3257 $c = $matches[1];
3258 if (strlen($c) === 1 && ord($c[0]) <= 0x7F)
3259 return $c;
3260 elseif (strlen($c) === 2 && ord($c[0]) >= 0xC0 && ord($c[0]) <= 0xDF)
3261 return '&#' . (((ord($c[0]) ^ 0xC0) << 6) + (ord($c[1]) ^ 0x80)) . ';';
3262 elseif (strlen($c) === 3 && ord($c[0]) >= 0xE0 && ord($c[0]) <= 0xEF)
3263 return '&#' . (((ord($c[0]) ^ 0xE0) << 12) + ((ord($c[1]) ^ 0x80) << 6) + (ord($c[2]) ^ 0x80)) . ';';
3264 elseif (strlen($c) === 4 && ord($c[0]) >= 0xF0 && ord($c[0]) <= 0xF7)
3265 return '&#' . (((ord($c[0]) ^ 0xF0) << 18) + ((ord($c[1]) ^ 0x80) << 12) + ((ord($c[2]) ^ 0x80) << 6) + (ord($c[3]) ^ 0x80)) . ';';
3266 else
3267 return '';
3268}
3269
3270function time_fix__preg_callback($matches)
3271{
3272 global $modSettings, $user_info;
3273 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]';
3274}
3275
3276function nobbc__preg_callback($matches)
3277{
3278 return '[nobbc]' . strtr($matches[1], array('[' => '[', ']' => ']', ':' => ':', '@' => '@')) . '[/nobbc]';
3279}
3280
3281function lowercase_tags__preg_callback($matches)
3282{
3283 return '[' . $matches[1] . strtolower($matches[2]) . $matches[3] . ']';
3284}
3285
3286function htmlspecial_html__preg_callback($matches)
3287{
3288 // Since we're calling htmlspecialchars we probably should know what charset we're using.
3289 global $modSettings, $txt;
3290 static $charset = null;
3291 if ($charset === null)
3292 $charset = empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set'];
3293
3294 return '[html]' . strtr(htmlspecialchars($matches[1], ENT_QUOTES, $charset), array('\\"' => '"', '&#13;' => '<br />', '&#32;' => ' ', '&#91;' => '[', '&#93;' => ']')) . '[/html]';
3295}
3296
3297function time_format__preg_callback($matches)
3298{
3299 return '[time]' . timeformat($matches[1], false) . '[/time]';
3300}
3301?>