comparison forum/Sources/Subs-Post.php @ 76:e3e11437ecea website

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