PageRenderTime 65ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/forum/Sources/Subs-Post.php

https://github.com/leftnode/nooges.com
PHP | 3165 lines | 2250 code | 393 blank | 522 comment | 490 complexity | 936bf510eca0fed7326713bc409515c3 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**********************************************************************************
  3. * Subs-Post.php *
  4. ***********************************************************************************
  5. * SMF: Simple Machines Forum *
  6. * Open-Source Project Inspired by Zef Hemel (zef@zefhemel.com) *
  7. * =============================================================================== *
  8. * Software Version: SMF 2.0 RC2 *
  9. * Software by: Simple Machines (http://www.simplemachines.org) *
  10. * Copyright 2006-2009 by: Simple Machines LLC (http://www.simplemachines.org) *
  11. * 2001-2006 by: Lewis Media (http://www.lewismedia.com) *
  12. * Support, News, Updates at: http://www.simplemachines.org *
  13. ***********************************************************************************
  14. * This program is free software; you may redistribute it and/or modify it under *
  15. * the terms of the provided license as published by Simple Machines LLC. *
  16. * *
  17. * This program is distributed in the hope that it is and will be useful, but *
  18. * WITHOUT ANY WARRANTIES; without even any implied warranty of MERCHANTABILITY *
  19. * or FITNESS FOR A PARTICULAR PURPOSE. *
  20. * *
  21. * See the "license.txt" file for details of the Simple Machines license. *
  22. * The latest version can always be found at http://www.simplemachines.org. *
  23. **********************************************************************************/
  24. if (!defined('SMF'))
  25. die('Hacking attempt...');
  26. /* This file contains those functions pertaining to posting, and other such
  27. operations, including sending emails, ims, blocking spam, preparsing posts,
  28. spell checking, and the post box. This is done with the following:
  29. void preparsecode(string &message, boolean previewing = false)
  30. - takes a message and parses it, returning nothing.
  31. - cleans up links (javascript, etc.) and code/quote sections.
  32. - won't convert \n's and a few other things if previewing is true.
  33. string un_preparsecode(string message)
  34. // !!!
  35. void fixTags(string &message)
  36. - used by preparsecode, fixes links in message and returns nothing.
  37. void fixTag(string &message, string myTag, string protocol,
  38. bool embeddedUrl = false, bool hasEqualSign = false,
  39. bool hasExtra = false)
  40. - used by fixTags, fixes a specific tag's links.
  41. - myTag is the tag, protocol is http of ftp, embeddedUrl is whether
  42. it *can* be set to something, hasEqualSign is whether it *is*
  43. set to something, and hasExtra is whether it can have extra
  44. cruft after the begin tag.
  45. bool sendmail(array to, string subject, string message,
  46. string message_id = auto, string from = webmaster,
  47. bool send_html = false, int priority = 3, bool hotmail_fix = null)
  48. - sends an email to the specified recipient.
  49. - uses the mail_type setting and the webmaster_email global.
  50. - to is he email(s), string or array, to send to.
  51. - subject and message are those of the email - expected to have
  52. slashes but not be parsed.
  53. - subject is expected to have entities, message is not.
  54. - from is a string which masks the address for use with replies.
  55. - if message_id is specified, uses that as the local-part of the
  56. Message-ID header.
  57. - send_html indicates whether or not the message is HTML vs. plain
  58. text, and does not add any HTML.
  59. - returns whether or not the email was sent properly.
  60. bool AddMailQueue(bool flush = true, array to_array = array(), string subject = '', string message = '',
  61. string headers = '', bool send_html = false, int priority = 3)
  62. //!!
  63. array sendpm(array recipients, string subject, string message,
  64. bool store_outbox = false, array from = current_member, int pm_head = 0)
  65. - sends an personal message from the specified person to the
  66. specified people. (from defaults to the user.)
  67. - recipients should be an array containing the arrays 'to' and 'bcc',
  68. both containing id_member's.
  69. - subject and message should have no slashes and no html entities.
  70. - pm_head is the ID of the chain being replied to - if any.
  71. - from is an array, with the id, name, and username of the member.
  72. - returns an array with log entries telling how many recipients were
  73. successful and which recipients it failed to send to.
  74. string mimespecialchars(string text, bool with_charset = true,
  75. hotmail_fix = false, string custom_charset = null)
  76. - prepare text strings for sending as email.
  77. - in case there are higher ASCII characters in the given string, this
  78. function will attempt the transport method 'quoted-printable'.
  79. Otherwise the transport method '7bit' is used.
  80. - with hotmail_fix set all higher ASCII characters are converted to
  81. HTML entities to assure proper display of the mail.
  82. - uses character set custom_charset if set.
  83. - returns an array containing the character set, the converted string
  84. and the transport method.
  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. bool server_parse(string message, resource socket, string response)
  92. - sends the specified message to the server, and checks for the
  93. expected response. (used internally.)
  94. - takes the message to send, socket to send on, and the expected
  95. response code.
  96. - returns whether it responded as such.
  97. void SpellCheck()
  98. - spell checks the post for typos ;).
  99. - uses the pspell library, which MUST be installed.
  100. - has problems with internationalization.
  101. - is accessed via ?action=spellcheck.
  102. void sendNotifications(array topics, string type, array exclude = array(), array members_only = array())
  103. - sends a notification to members who have elected to receive emails
  104. when things happen to a topic, such as replies are posted.
  105. - uses the Post langauge file.
  106. - topics represents the topics the action is happening to.
  107. - the type can be any of reply, sticky, lock, unlock, remove, move,
  108. merge, and split. An appropriate message will be sent for each.
  109. - automatically finds the subject and its board, and checks permissions
  110. for each member who is "signed up" for notifications.
  111. - will not send 'reply' notifications more than once in a row.
  112. - members in the exclude array will not be processed for the topic with the same key.
  113. - members_only are the only ones that will be sent the notification if they have it on.
  114. bool createPost(&array msgOptions, &array topicOptions, &array posterOptions)
  115. // !!!
  116. bool createAttachment(&array attachmentOptions)
  117. // !!!
  118. bool modifyPost(&array msgOptions, &array topicOptions, &array posterOptions)
  119. // !!!
  120. bool approvePosts(array msgs, bool approve)
  121. // !!!
  122. array approveTopics(array topics, bool approve)
  123. // !!!
  124. void sendApprovalNotifications(array topicData)
  125. // !!!
  126. void updateLastMessages(array id_board's, int id_msg)
  127. - takes an array of board IDs and updates their last messages.
  128. - if the board has a parent, that parent board is also automatically
  129. updated.
  130. - columns updated are id_last_msg and lastUpdated.
  131. - note that id_last_msg should always be updated using this function,
  132. and is not automatically updated upon other changes.
  133. void adminNotify(string type, int memberID, string member_name = null)
  134. - sends all admins an email to let them know a new member has joined.
  135. - types supported are 'approval', 'activation', and 'standard'.
  136. - called by registerMember() function in Subs-Members.php.
  137. - email is sent to all groups that have the moderate_forum permission.
  138. - uses the Login language file.
  139. - the language set by each member is being used (if available).
  140. Sending emails from SMF:
  141. ---------------------------------------------------------------------------
  142. // !!!
  143. */
  144. // Parses some bbc before sending into the database...
  145. function preparsecode(&$message, $previewing = false)
  146. {
  147. global $user_info, $modSettings, $smcFunc, $context;
  148. // This line makes all languages *theoretically* work even with the wrong charset ;).
  149. $message = preg_replace('~&amp;#(\d{4,5}|[2-9]\d{2,4}|1[2-9]\d);~', '&#$1;', $message);
  150. // Clean up after nobbc ;).
  151. $message = preg_replace('~\[nobbc\](.+?)\[/nobbc\]~ie', '\'[nobbc]\' . strtr(\'$1\', array(\'[\' => \'&#91;\', \']\' => \'&#93;\', \':\' => \'&#58;\', \'@\' => \'&#64;\')) . \'[/nobbc]\'', $message);
  152. // Remove \r's... they're evil!
  153. $message = strtr($message, array("\r" => ''));
  154. // You won't believe this - but too many periods upsets apache it seems!
  155. $message = preg_replace('~\.{100,}~', '...', $message);
  156. // Trim off trailing quotes - these often happen by accident.
  157. while (substr($message, -7) == '[quote]')
  158. $message = substr($message, 0, -7);
  159. while (substr($message, 0, 8) == '[/quote]')
  160. $message = substr($message, 8);
  161. // Find all code blocks, work out whether we'd be parsing them, then ensure they are all closed.
  162. $in_tag = false;
  163. $had_tag = false;
  164. $codeopen = 0;
  165. if (preg_match_all('~(\[(/)*code(?:=[^\]]+)?\])~is', $message, $matches))
  166. foreach ($matches[0] as $index => $dummy)
  167. {
  168. // Closing?
  169. if (!empty($matches[2][$index]))
  170. {
  171. // If it's closing and we're not in a tag we need to open it...
  172. if (!$in_tag)
  173. $codeopen = true;
  174. // Either way we ain't in one any more.
  175. $in_tag = false;
  176. }
  177. // Opening tag...
  178. else
  179. {
  180. $had_tag = true;
  181. // If we're in a tag don't do nought!
  182. if (!$in_tag)
  183. $in_tag = true;
  184. }
  185. }
  186. // If we have an open tag, close it.
  187. if ($in_tag)
  188. $message .= '[/code]';
  189. // Open any ones that need to be open, only if we've never had a tag.
  190. if ($codeopen && !$had_tag)
  191. $message = '[code]' . $message;
  192. // Now that we've fixed all the code tags, let's fix the img and url tags...
  193. $parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
  194. // The regular expression non breaking space has many versions.
  195. $non_breaking_space = $context['utf8'] ? ($context['server']['complex_preg_chars'] ? '\x{A0}' : "\xC2\xA0") : '\xA0';
  196. // Only mess with stuff outside [code] tags.
  197. for ($i = 0, $n = count($parts); $i < $n; $i++)
  198. {
  199. // It goes 0 = outside, 1 = begin tag, 2 = inside, 3 = close tag, repeat.
  200. if ($i % 4 == 0)
  201. {
  202. fixTags($parts[$i]);
  203. // Replace /me.+?\n with [me=name]dsf[/me]\n.
  204. if (strpos($user_info['name'], '[') !== false || strpos($user_info['name'], ']') !== false || strpos($user_info['name'], '\'') !== false || strpos($user_info['name'], '"') !== false)
  205. $parts[$i] = preg_replace('~(\A|\n)/me(?: |&nbsp;)([^\n]*)(?:\z)?~i', '$1[me=&quot;' . $user_info['name'] . '&quot;]$2[/me]', $parts[$i]);
  206. else
  207. $parts[$i] = preg_replace('~(\A|\n)/me(?: |&nbsp;)([^\n]*)(?:\z)?~i', '$1[me=' . $user_info['name'] . ']$2[/me]', $parts[$i]);
  208. if (!$previewing && strpos($parts[$i], '[html]') !== false)
  209. {
  210. if (allowedTo('admin_forum'))
  211. $parts[$i] = preg_replace('~\[html\](.+?)\[/html\]~ise', '\'[html]\' . strtr(un_htmlspecialchars(\'$1\'), array("\n" => \'&#13;\', \' \' => \' &#32;\')) . \'[/html]\'', $parts[$i]);
  212. // We should edit them out, or else if an admin edits the message they will get shown...
  213. else
  214. {
  215. while (strpos($parts[$i], '[html]') !== false)
  216. $parts[$i] = preg_replace('~\[[/]?html\]~i', '', $parts[$i]);
  217. }
  218. }
  219. // Let's look at the time tags...
  220. $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]);
  221. // Change the color specific tags to [color=the color].
  222. $parts[$i] = preg_replace('~\[(black|blue|green|red|white)\]~', '[color=$1]', $parts[$i]); // First do the opening tags.
  223. $parts[$i] = preg_replace('~\[/(black|blue|green|red|white)\]~', '[/color]', $parts[$i]); // And now do the closing tags
  224. // Make sure all tags are lowercase.
  225. $parts[$i] = preg_replace('~\[([/]?)(list|li|table|tr|td)((\s[^\]]+)*)\]~ie', '\'[$1\' . strtolower(\'$2\') . \'$3]\'', $parts[$i]);
  226. $list_open = substr_count($parts[$i], '[list]') + substr_count($parts[$i], '[list ');
  227. $list_close = substr_count($parts[$i], '[/list]');
  228. if ($list_close - $list_open > 0)
  229. $parts[$i] = str_repeat('[list]', $list_close - $list_open) . $parts[$i];
  230. if ($list_open - $list_close > 0)
  231. $parts[$i] = $parts[$i] . str_repeat('[/list]', $list_open - $list_close);
  232. $mistake_fixes = array(
  233. // Find [table]s not followed by [tr].
  234. '~\[table\](?![\s' . $non_breaking_space . ']*\[tr\])~s' . ($context['utf8'] ? 'u' : '') => '[table][tr]',
  235. // Find [tr]s not followed by [td].
  236. '~\[tr\](?![\s' . $non_breaking_space . ']*\[td\])~s' . ($context['utf8'] ? 'u' : '') => '[tr][td]',
  237. // Find [/td]s not followed by something valid.
  238. '~\[/td\](?![\s' . $non_breaking_space . ']*(?:\[td\]|\[/tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr]',
  239. // Find [/tr]s not followed by something valid.
  240. '~\[/tr\](?![\s' . $non_breaking_space . ']*(?:\[tr\]|\[/table\]))~s' . ($context['utf8'] ? 'u' : '') => '[/tr][/table]',
  241. // Find [/td]s incorrectly followed by [/table].
  242. '~\[/td\][\s' . $non_breaking_space . ']*\[/table\]~s' . ($context['utf8'] ? 'u' : '') => '[/td][/tr][/table]',
  243. // Find [table]s, [tr]s, and [/td]s (possibly correctly) followed by [td].
  244. '~\[(table|tr|/td)\]([\s' . $non_breaking_space . ']*)\[td\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_td_]',
  245. // Now, any [td]s left should have a [tr] before them.
  246. '~\[td\]~s' => '[tr][td]',
  247. // Look for [tr]s which are correctly placed.
  248. '~\[(table|/tr)\]([\s' . $non_breaking_space . ']*)\[tr\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_tr_]',
  249. // Any remaining [tr]s should have a [table] before them.
  250. '~\[tr\]~s' => '[table][tr]',
  251. // Look for [/td]s followed by [/tr].
  252. '~\[/td\]([\s' . $non_breaking_space . ']*)\[/tr\]~s' . ($context['utf8'] ? 'u' : '') => '[/td]$1[_/tr_]',
  253. // Any remaining [/tr]s should have a [/td].
  254. '~\[/tr\]~s' => '[/td][/tr]',
  255. // Look for properly opened [li]s which aren't closed.
  256. '~\[li\]([^\[\]]+?)\[li\]~s' => '[li]$1[_/li_][_li_]',
  257. '~\[li\]([^\[\]]+?)\[/list\]~s' => '[_li_]$1[_/li_][/list]',
  258. '~\[li\]([^\[\]]+?)$~s' => '[li]$1[/li]',
  259. // Lists - find correctly closed items/lists.
  260. '~\[/li\]([\s' . $non_breaking_space . ']*)\[/list\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[/list]',
  261. // Find list items closed and then opened.
  262. '~\[/li\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[_/li_]$1[_li_]',
  263. // Now, find any [list]s or [/li]s followed by [li].
  264. '~\[(list(?: [^\]]*?)?|/li)\]([\s' . $non_breaking_space . ']*)\[li\]~s' . ($context['utf8'] ? 'u' : '') => '[$1]$2[_li_]',
  265. // Allow for sub lists.
  266. '~\[/li\]([\s' . $non_breaking_space . ']*)\[list\]~' => '[_/li_]$1[list]',
  267. '~\[/list\]([\s' . $non_breaking_space . ']*)\[li\]~' => '[/list]$1[_li_]',
  268. // Any remaining [li]s weren't inside a [list].
  269. '~\[li\]~' => '[list][li]',
  270. // Any remaining [/li]s weren't before a [/list].
  271. '~\[/li\]~' => '[/li][/list]',
  272. // Put the correct ones back how we found them.
  273. '~\[_(li|/li|td|tr|/tr)_\]~' => '[$1]',
  274. // Images with no real url.
  275. '~\[img\]https?://.{0,7}\[/img\]~' => '',
  276. );
  277. // Fix up some use of tables without [tr]s, etc. (it has to be done more than once to catch it all.)
  278. for ($j = 0; $j < 3; $j++)
  279. $parts[$i] = preg_replace(array_keys($mistake_fixes), $mistake_fixes, $parts[$i]);
  280. // Now we're going to do full scale table checking...
  281. $table_check = $parts[$i];
  282. $table_offset = 0;
  283. $table_array = array();
  284. $table_order = array(
  285. 'table' => 'td',
  286. 'tr' => 'table',
  287. 'td' => 'tr',
  288. );
  289. while (preg_match('~\[(/)*(table|tr|td)\]~', $table_check, $matches) != false)
  290. {
  291. // Keep track of where this is.
  292. $offset = strpos($table_check, $matches[0]);
  293. $remove_tag = false;
  294. // Is it opening?
  295. if ($matches[1] != '/')
  296. {
  297. // If the previous table tag isn't correct simply remove it.
  298. if ((!empty($table_array) && $table_array[0] != $table_order[$matches[2]]) || (empty($table_array) && $matches[2] != 'table'))
  299. $remove_tag = true;
  300. // Record this was the last tag.
  301. else
  302. array_unshift($table_array, $matches[2]);
  303. }
  304. // Otherwise is closed!
  305. else
  306. {
  307. // Only keep the tag if it's closing the right thing.
  308. if (empty($table_array) || ($table_array[0] != $matches[2]))
  309. $remove_tag = true;
  310. else
  311. array_shift($table_array);
  312. }
  313. // Removing?
  314. if ($remove_tag)
  315. {
  316. $parts[$i] = substr($parts[$i], 0, $table_offset + $offset) . substr($parts[$i], $table_offset + strlen($matches[0]) + $offset);
  317. // We've lost some data.
  318. $table_offset -= strlen($matches[0]);
  319. }
  320. // Remove everything up to here.
  321. $table_offset += $offset + strlen($matches[0]);
  322. $table_check = substr($table_check, $offset + strlen($matches[0]));
  323. }
  324. // Close any remaining table tags.
  325. foreach ($table_array as $tag)
  326. $parts[$i] .= '[/' . $tag . ']';
  327. }
  328. }
  329. // Put it back together!
  330. if (!$previewing)
  331. $message = strtr(implode('', $parts), array(' ' => '&nbsp; ', "\n" => '<br />', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
  332. else
  333. $message = strtr(implode('', $parts), array(' ' => '&nbsp; ', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;'));
  334. // Now let's quickly clean up things that will slow our parser (which are common in posted code.)
  335. $message = strtr($message, array('[]' => '&#91;]', '[&#039;' => '&#91;&#039;'));
  336. }
  337. // This is very simple, and just removes things done by preparsecode.
  338. function un_preparsecode($message)
  339. {
  340. global $smcFunc;
  341. $parts = preg_split('~(\[/code\]|\[code(?:=[^\]]+)?\])~i', $message, -1, PREG_SPLIT_DELIM_CAPTURE);
  342. // We're going to unparse only the stuff outside [code]...
  343. for ($i = 0, $n = count($parts); $i < $n; $i++)
  344. {
  345. // If $i is a multiple of four (0, 4, 8, ...) then it's not a code section...
  346. if ($i % 4 == 0)
  347. {
  348. $parts[$i] = preg_replace('~\[html\](.+?)\[/html\]~ie', '\'[html]\' . strtr(htmlspecialchars(\'$1\', ENT_QUOTES), array(\'\\&quot;\' => \'&quot;\', \'&amp;#13;\' => \'<br />\', \'&amp;#32;\' => \' \')) . \'[/html]\'', $parts[$i]);
  349. // Attempt to un-parse the time to something less awful.
  350. $parts[$i] = preg_replace('~\[time\](\d{0,10})\[/time\]~ie', '\'[time]\' . timeformat(\'$1\', false) . \'[/time]\'', $parts[$i]);
  351. }
  352. }
  353. // Change breaks back to \n's and &nsbp; back to spaces.
  354. return preg_replace('~<br( /)?' . '>~', "\n", str_replace('&nbsp;', ' ', implode('', $parts)));
  355. }
  356. // Fix any URLs posted - ie. remove 'javascript:'.
  357. function fixTags(&$message)
  358. {
  359. global $modSettings;
  360. // WARNING: Editing the below can cause large security holes in your forum.
  361. // Edit only if you are sure you know what you are doing.
  362. $fixArray = array(
  363. // [img]http://...[/img] or [img width=1]http://...[/img]
  364. array(
  365. 'tag' => 'img',
  366. 'protocols' => array('http', 'https'),
  367. 'embeddedUrl' => false,
  368. 'hasEqualSign' => false,
  369. 'hasExtra' => true,
  370. ),
  371. // [url]http://...[/url]
  372. array(
  373. 'tag' => 'url',
  374. 'protocols' => array('http', 'https'),
  375. 'embeddedUrl' => true,
  376. 'hasEqualSign' => false,
  377. ),
  378. // [url=http://...]name[/url]
  379. array(
  380. 'tag' => 'url',
  381. 'protocols' => array('http', 'https'),
  382. 'embeddedUrl' => true,
  383. 'hasEqualSign' => true,
  384. ),
  385. // [iurl]http://...[/iurl]
  386. array(
  387. 'tag' => 'iurl',
  388. 'protocols' => array('http', 'https'),
  389. 'embeddedUrl' => true,
  390. 'hasEqualSign' => false,
  391. ),
  392. // [iurl=http://...]name[/iurl]
  393. array(
  394. 'tag' => 'iurl',
  395. 'protocols' => array('http', 'https'),
  396. 'embeddedUrl' => true,
  397. 'hasEqualSign' => true,
  398. ),
  399. // [ftp]ftp://...[/ftp]
  400. array(
  401. 'tag' => 'ftp',
  402. 'protocols' => array('ftp', 'ftps'),
  403. 'embeddedUrl' => true,
  404. 'hasEqualSign' => false,
  405. ),
  406. // [ftp=ftp://...]name[/ftp]
  407. array(
  408. 'tag' => 'ftp',
  409. 'protocols' => array('ftp', 'ftps'),
  410. 'embeddedUrl' => true,
  411. 'hasEqualSign' => true,
  412. ),
  413. // [flash]http://...[/flash]
  414. array(
  415. 'tag' => 'flash',
  416. 'protocols' => array('http', 'https'),
  417. 'embeddedUrl' => false,
  418. 'hasEqualSign' => false,
  419. 'hasExtra' => true,
  420. ),
  421. );
  422. // Fix each type of tag.
  423. foreach ($fixArray as $param)
  424. fixTag($message, $param['tag'], $param['protocols'], $param['embeddedUrl'], $param['hasEqualSign'], !empty($param['hasExtra']));
  425. // Now fix possible security problems with images loading links automatically...
  426. $message = preg_replace('~(\[img.*?\])(.+?)\[/img\]~eis', '\'$1\' . preg_replace(\'~action(=|%3d)(?!dlattach)~i\', \'action-\', \'$2\') . \'[/img]\'', $message);
  427. // Limit the size of images posted?
  428. if (!empty($modSettings['max_image_width']) || !empty($modSettings['max_image_height']))
  429. {
  430. // Find all the img tags - with or without width and height.
  431. preg_match_all('~\[img(\s+width=\d+)?(\s+height=\d+)?(\s+width=\d+)?\](.+?)\[/img\]~is', $message, $matches, PREG_PATTERN_ORDER);
  432. $replaces = array();
  433. foreach ($matches[0] as $match => $dummy)
  434. {
  435. // If the width was after the height, handle it.
  436. $matches[1][$match] = !empty($matches[3][$match]) ? $matches[3][$match] : $matches[1][$match];
  437. // Now figure out if they had a desired height or width...
  438. $desired_width = !empty($matches[1][$match]) ? (int) substr(trim($matches[1][$match]), 6) : 0;
  439. $desired_height = !empty($matches[2][$match]) ? (int) substr(trim($matches[2][$match]), 7) : 0;
  440. // One was omitted, or both. We'll have to find its real size...
  441. if (empty($desired_width) || empty($desired_height))
  442. {
  443. list ($width, $height) = url_image_size(un_htmlspecialchars($matches[4][$match]));
  444. // They don't have any desired width or height!
  445. if (empty($desired_width) && empty($desired_height))
  446. {
  447. $desired_width = $width;
  448. $desired_height = $height;
  449. }
  450. // Scale it to the width...
  451. elseif (empty($desired_width) && !empty($height))
  452. $desired_width = (int) (($desired_height * $width) / $height);
  453. // Scale if to the height.
  454. elseif (!empty($width))
  455. $desired_height = (int) (($desired_width * $height) / $width);
  456. }
  457. // If the width and height are fine, just continue along...
  458. if ($desired_width <= $modSettings['max_image_width'] && $desired_height <= $modSettings['max_image_height'])
  459. continue;
  460. // Too bad, it's too wide. Make it as wide as the maximum.
  461. if ($desired_width > $modSettings['max_image_width'] && !empty($modSettings['max_image_width']))
  462. {
  463. $desired_height = (int) (($modSettings['max_image_width'] * $desired_height) / $desired_width);
  464. $desired_width = $modSettings['max_image_width'];
  465. }
  466. // Now check the height, as well. Might have to scale twice, even...
  467. if ($desired_height > $modSettings['max_image_height'] && !empty($modSettings['max_image_height']))
  468. {
  469. $desired_width = (int) (($modSettings['max_image_height'] * $desired_width) / $desired_height);
  470. $desired_height = $modSettings['max_image_height'];
  471. }
  472. $replaces[$matches[0][$match]] = '[img' . (!empty($desired_width) ? ' width=' . $desired_width : '') . (!empty($desired_height) ? ' height=' . $desired_height : '') . ']' . $matches[4][$match] . '[/img]';
  473. }
  474. // If any img tags were actually changed...
  475. if (!empty($replaces))
  476. $message = strtr($message, $replaces);
  477. }
  478. }
  479. // Fix a specific class of tag - ie. url with =.
  480. function fixTag(&$message, $myTag, $protocols, $embeddedUrl = false, $hasEqualSign = false, $hasExtra = false)
  481. {
  482. global $boardurl, $scripturl;
  483. if (preg_match('~^([^:]+://[^/]+)~', $boardurl, $match) != 0)
  484. $domain_url = $match[1];
  485. else
  486. $domain_url = $boardurl . '/';
  487. $replaces = array();
  488. if ($hasEqualSign)
  489. preg_match_all('~\[(' . $myTag . ')=([^\]]*?)\](?:(.+?)\[/(' . $myTag . ')\])?~is', $message, $matches);
  490. else
  491. preg_match_all('~\[(' . $myTag . ($hasExtra ? '(?:[^\]]*?)' : '') . ')\](.+?)\[/(' . $myTag . ')\]~is', $message, $matches);
  492. foreach ($matches[0] as $k => $dummy)
  493. {
  494. // Remove all leading and trailing whitespace.
  495. $replace = trim($matches[2][$k]);
  496. $this_tag = $matches[1][$k];
  497. $this_close = $hasEqualSign ? (empty($matches[4][$k]) ? '' : $matches[4][$k]) : $matches[3][$k];
  498. $found = false;
  499. foreach ($protocols as $protocol)
  500. {
  501. $found = strncasecmp($replace, $protocol . '://', strlen($protocol) + 3) === 0;
  502. if ($found)
  503. break;
  504. }
  505. if (!$found && $protocols[0] == 'http')
  506. {
  507. if (substr($replace, 0, 1) == '/')
  508. $replace = $domain_url . $replace;
  509. elseif (substr($replace, 0, 1) == '?')
  510. $replace = $scripturl . $replace;
  511. elseif (substr($replace, 0, 1) == '#' && $embeddedUrl)
  512. {
  513. $replace = '#' . preg_replace('~[^A-Za-z0-9_\-#]~', '', substr($replace, 1));
  514. $this_tag = 'iurl';
  515. $this_close = 'iurl';
  516. }
  517. else
  518. $replace = $protocols[0] . '://' . $replace;
  519. }
  520. elseif (!$found && $protocols[0] == 'ftp')
  521. $replace = $protocols[0] . '://' . preg_replace('~^(?!ftps?)[^:]+://~', '', $replace);
  522. elseif (!$found)
  523. $replace = $protocols[0] . '://' . $replace;
  524. if ($hasEqualSign && $embeddedUrl)
  525. $replaces[$matches[0][$k]] = '[' . $this_tag . '=' . $replace . ']' . (empty($matches[4][$k]) ? '' : $matches[3][$k] . '[/' . $this_close . ']');
  526. elseif ($hasEqualSign)
  527. $replaces['[' . $matches[1][$k] . '=' . $matches[2][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']';
  528. elseif ($embeddedUrl)
  529. $replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . '=' . $replace . ']' . $matches[2][$k] . '[/' . $this_close . ']';
  530. else
  531. $replaces['[' . $matches[1][$k] . ']' . $matches[2][$k] . '[/' . $matches[3][$k] . ']'] = '[' . $this_tag . ']' . $replace . '[/' . $this_close . ']';
  532. }
  533. foreach ($replaces as $k => $v)
  534. {
  535. if ($k == $v)
  536. unset($replaces[$k]);
  537. }
  538. if (!empty($replaces))
  539. $message = strtr($message, $replaces);
  540. }
  541. // Send off an email.
  542. function sendmail($to, $subject, $message, $from = null, $message_id = null, $send_html = false, $priority = 3, $hotmail_fix = null, $is_private = true)
  543. {
  544. global $webmaster_email, $context, $modSettings, $txt, $scripturl;
  545. global $smcFunc;
  546. // Use sendmail if it's set or if no SMTP server is set.
  547. $use_sendmail = empty($modSettings['mail_type']) || $modSettings['smtp_host'] == '';
  548. // Line breaks need to be \r\n only in windows or for SMTP.
  549. $line_break = $context['server']['is_windows'] || !$use_sendmail ? "\r\n" : "\n";
  550. // So far so good.
  551. $mail_result = true;
  552. // If the recipient list isn't an array, make it one.
  553. $to_array = is_array($to) ? $to : array($to);
  554. // Once upon a time, Hotmail could not interpret non-ASCII mails.
  555. // In honour of those days, it's still called the 'hotmail fix'.
  556. if ($hotmail_fix === null)
  557. {
  558. $hotmail_to = array();
  559. foreach ($to_array as $i => $to_address)
  560. {
  561. if (preg_match('~@(att|comcast|bellsouth)\.[a-zA-Z\.]{2,6}$~i', $to_address) === 1)
  562. {
  563. $hotmail_to[] = $to_address;
  564. $to_array = array_diff($to_array, array($to_address));
  565. }
  566. }
  567. // Call this function recursively for the hotmail addresses.
  568. if (!empty($hotmail_to))
  569. $mail_result = sendmail($hotmail_to, $subject, $message, $from, $message_id, $send_html, $priority, true);
  570. // The remaining addresses no longer need the fix.
  571. $hotmail_fix = false;
  572. // No other addresses left? Return instantly.
  573. if (empty($to_array))
  574. return $mail_result;
  575. }
  576. // Get rid of entities.
  577. $subject = un_htmlspecialchars($subject);
  578. // Make the message use the proper line breaks.
  579. $message = str_replace(array("\r", "\n"), array('', $line_break), $message);
  580. // Make sure hotmail mails are sent as HTML so that HTML entities work.
  581. if ($hotmail_fix && !$send_html)
  582. {
  583. $send_html = true;
  584. $message = strtr($message, array($line_break => '<br />' . $line_break));
  585. $message = preg_replace('~(' . preg_quote($scripturl, '~') . '(?:[?/][\w\-_%\.,\?&;=#]+)?)~', '<a href="$1">$1</a>', $message);
  586. }
  587. list (, $from_name) = mimespecialchars(addcslashes($from !== null ? $from : $context['forum_name'], '<>()\'\\"'), true, $hotmail_fix, $line_break);
  588. list (, $subject) = mimespecialchars($subject, true, $hotmail_fix, $line_break);
  589. // Construct the mail headers...
  590. $headers = 'From: "' . $from_name . '" <' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . '>' . $line_break;
  591. $headers .= $from !== null ? 'Reply-To: <' . $from . '>' . $line_break : '';
  592. $headers .= 'Return-Path: ' . (empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from']) . $line_break;
  593. $headers .= 'Date: ' . gmdate('D, d M Y H:i:s') . ' -0000' . $line_break;
  594. if ($message_id !== null && empty($modSettings['mail_no_message_id']))
  595. $headers .= 'Message-ID: <' . md5($scripturl . microtime()) . '-' . $message_id . strstr(empty($modSettings['mail_from']) ? $webmaster_email : $modSettings['mail_from'], '@') . '>' . $line_break;
  596. $headers .= 'X-Mailer: SMF' . $line_break;
  597. // pass this to the integration before we start modifying the output -- it'll make it easier later
  598. if (isset($modSettings['integrate_outgoing_email']) && function_exists($modSettings['integrate_outgoing_email']))
  599. {
  600. if ($modSettings['integrate_outgoing_email']($subject, $message, $headers) === false)
  601. return false;
  602. }
  603. // Save the original message...
  604. $orig_message = $message;
  605. // The mime boundary separates the different alternative versions.
  606. $mime_boundary = 'SMF-' . md5($message . time());
  607. // Using mime, as it allows to send a plain unencoded alternative.
  608. $headers .= 'Mime-Version: 1.0' . $line_break;
  609. $headers .= 'Content-Type: multipart/alternative; boundary="' . $mime_boundary . '"' . $line_break;
  610. $headers .= 'Content-Transfer-Encoding: 7bit' . $line_break;
  611. // Sending HTML? Let's plop in some basic stuff, then.
  612. if ($send_html)
  613. {
  614. $no_html_message = un_htmlspecialchars(strip_tags(strtr($orig_message, array('</title>' => $line_break))));
  615. // But, then, dump it and use a plain one for dinosaur clients.
  616. list(, $plain_message) = mimespecialchars($no_html_message, false, true, $line_break);
  617. $message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
  618. // This is the plain text version. Even if no one sees it, we need it for spam checkers.
  619. list($charset, $plain_charset_message, $encoding) = mimespecialchars($no_html_message, false, false, $line_break);
  620. $message .= 'Content-Type: text/plain; charset=' . $charset . $line_break;
  621. $message .= 'Content-Transfer-Encoding: ' . $encoding . $line_break . $line_break;
  622. $message .= $plain_charset_message . $line_break . '--' . $mime_boundary . $line_break;
  623. // This is the actual HTML message, prim and proper. If we wanted images, they could be inlined here (with multipart/related, etc.)
  624. list($charset, $html_message, $encoding) = mimespecialchars($orig_message, false, $hotmail_fix, $line_break);
  625. $message .= 'Content-Type: text/html; charset=' . $charset . $line_break;
  626. $message .= 'Content-Transfer-Encoding: ' . ($encoding == '' ? '7bit' : $encoding) . $line_break . $line_break;
  627. $message .= $html_message . $line_break . '--' . $mime_boundary . '--';
  628. }
  629. // Text is good too.
  630. else
  631. {
  632. // Send a plain message first, for the older web clients.
  633. list(, $plain_message) = mimespecialchars($orig_message, false, true, $line_break);
  634. $message = $plain_message . $line_break . '--' . $mime_boundary . $line_break;
  635. // Now add an encoded message using the forum's character set.
  636. list ($charset, $encoded_message, $encoding) = mimespecialchars($orig_message, false, false, $line_break);
  637. $message .= 'Content-Type: text/plain; charset=' . $charset . $line_break;
  638. $message .= 'Content-Transfer-Encoding: ' . $encoding . $line_break . $line_break;
  639. $message .= $encoded_message . $line_break . '--' . $mime_boundary . '--';
  640. }
  641. // Are we using the mail queue, if so this is where we butt in...
  642. if (!empty($modSettings['mail_queue']) && $priority != 0)
  643. return AddMailQueue(false, $to_array, $subject, $message, $headers, $send_html, $priority, $is_private);
  644. // If it's a priority mail, send it now - note though that this should NOT be used for sending many at once.
  645. elseif (!empty($modSettings['mail_queue']) && !empty($modSettings['mail_limit']))
  646. {
  647. list ($last_mail_time, $mails_this_minute) = @explode('|', $modSettings['mail_recent']);
  648. if (empty($mails_this_minute) || time() > $last_mail_time + 60)
  649. $new_queue_stat = time() . '|' . 1;
  650. else
  651. $new_queue_stat = $last_mail_time . '|' . ((int) $mails_this_minute + 1);
  652. updateSettings(array('mail_recent' => $new_queue_stat));
  653. }
  654. // SMTP or sendmail?
  655. if ($use_sendmail)
  656. {
  657. $subject = strtr($subject, array("\r" => '', "\n" => ''));
  658. if (!empty($modSettings['mail_strip_carriage']))
  659. {
  660. $message = strtr($message, array("\r" => ''));
  661. $headers = strtr($headers, array("\r" => ''));
  662. }
  663. foreach ($to_array as $to)
  664. {
  665. if (!mail(strtr($to, array("\r" => '', "\n" => '')), $subject, $message, $headers))
  666. {
  667. log_error(sprintf($txt['mail_send_unable'], $to));
  668. $mail_result = false;
  669. }
  670. // Wait, wait, I'm still sending here!
  671. @set_time_limit(300);
  672. if (function_exists('apache_reset_timeout'))
  673. @apache_reset_timeout();
  674. }
  675. }
  676. else
  677. $mail_result = $mail_result && smtp_mail($to_array, $subject, $message, $headers);
  678. // Everything go smoothly?
  679. return $mail_result;
  680. }
  681. // Add an email to the mail queue.
  682. function AddMailQueue($flush = false, $to_array = array(), $subject = '', $message = '', $headers = '', $send_html = false, $priority = 3, $is_private = false)
  683. {
  684. global $context, $modSettings, $smcFunc;
  685. static $cur_insert = array();
  686. static $cur_insert_len = 0;
  687. if ($cur_insert_len == 0)
  688. $cur_insert = array();
  689. // If we're flushing, make the final inserts - also if we're near the MySQL length limit!
  690. if (($flush || $cur_insert_len > 800000) && !empty($cur_insert))
  691. {
  692. // Only do these once.
  693. $cur_insert_len = 0;
  694. // Dump the data...
  695. $smcFunc['db_insert']('',
  696. '{db_prefix}mail_queue',
  697. array(
  698. 'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string-65534', 'subject' => 'string-255',
  699. 'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
  700. ),
  701. $cur_insert,
  702. array('id_mail')
  703. );
  704. $cur_insert = array();
  705. $context['flush_mail'] = false;
  706. }
  707. // If we're flushing we're done.
  708. if ($flush)
  709. {
  710. $nextSendTime = time() + 10;
  711. $smcFunc['db_query']('', '
  712. UPDATE {db_prefix}settings
  713. SET value = {string:nextSendTime}
  714. WHERE variable = {string:mail_next_send}
  715. AND value = {string:no_outstanding}',
  716. array(
  717. 'nextSendTime' => $nextSendTime,
  718. 'mail_next_send' => 'mail_next_send',
  719. 'no_outstanding' => '0',
  720. )
  721. );
  722. return true;
  723. }
  724. // Ensure we tell obExit to flush.
  725. $context['flush_mail'] = true;
  726. foreach ($to_array as $to)
  727. {
  728. // Will this insert go over MySQL's limit?
  729. $this_insert_len = strlen($to) + strlen($message) + strlen($headers) + 700;
  730. // Insert limit of 1M (just under the safety) is reached?
  731. if ($this_insert_len + $cur_insert_len > 1000000)
  732. {
  733. // Flush out what we have so far.
  734. $smcFunc['db_insert']('',
  735. '{db_prefix}mail_queue',
  736. array(
  737. 'time_sent' => 'int', 'recipient' => 'string-255', 'body' => 'string-65534', 'subject' => 'string-255',
  738. 'headers' => 'string-65534', 'send_html' => 'int', 'priority' => 'int', 'private' => 'int',
  739. ),
  740. $cur_insert,
  741. array('id_mail')
  742. );
  743. // Clear this out.
  744. $cur_insert = array();
  745. $cur_insert_len = 0;
  746. }
  747. // Now add the current insert to the array...
  748. $cur_insert[] = array(time(), (string) $to, (string) $message, (string) $subject, (string) $headers, ($send_html ? 1 : 0), $priority, (int) $is_private);
  749. $cur_insert_len += $this_insert_len;
  750. }
  751. // If they are using SSI there is a good chance obExit will never be called. So lets be nice and flush it for them.
  752. if (SMF === 'SSI')
  753. return AddMailQueue(true);
  754. return true;
  755. }
  756. // Send off a personal message.
  757. function sendpm($recipients, $subject, $message, $store_outbox = false, $from = null, $pm_head = 0)
  758. {
  759. global $scripturl, $txt, $user_info, $language;
  760. global $modSettings, $smcFunc;
  761. // Make sure the PM language file is loaded, we might need something out of it.
  762. loadLanguage('PersonalMessage');
  763. $onBehalf = $from !== null;
  764. // Initialize log array.
  765. $log = array(
  766. 'failed' => array(),
  767. 'sent' => array()
  768. );
  769. if ($from === null)
  770. $from = array(
  771. 'id' => $user_info['id'],
  772. 'name' => $user_info['name'],
  773. 'username' => $user_info['username']
  774. );
  775. // Probably not needed. /me something should be of the typer.
  776. else
  777. $user_info['name'] = $from['name'];
  778. // This is the one that will go in their inbox.
  779. $htmlmessage = $smcFunc['htmlspecialchars']($message, ENT_QUOTES);
  780. $htmlsubject = $smcFunc['htmlspecialchars']($subject);
  781. preparsecode($htmlmessage);
  782. // Integrated PMs
  783. if (isset($modSettings['integrate_personal_message']) && function_exists($modSettings['integrate_personal_message']))
  784. $modSettings['integrate_personal_message']($recipients, $from['username'], $subject, $message);
  785. // Get a list of usernames and convert them to IDs.
  786. $usernames = array();
  787. foreach ($recipients as $rec_type => $rec)
  788. {
  789. foreach ($rec as $id => $member)
  790. {
  791. if (!is_numeric($recipients[$rec_type][$id]))
  792. {
  793. $recipients[$rec_type][$id] = $smcFunc['strtolower'](trim(preg_replace('/[<>&"\'=\\\]/', '', $recipients[$rec_type][$id])));
  794. $usernames[$recipients[$rec_type][$id]] = 0;
  795. }
  796. }
  797. }
  798. if (!empty($usernames))
  799. {
  800. $request = $smcFunc['db_query']('pm_find_username', '
  801. SELECT id_member, member_name
  802. FROM {db_prefix}members
  803. WHERE ' . ($smcFunc['db_case_sensitive'] ? 'LOWER(member_name)' : 'member_name') . ' IN ({array_string:usernames})',
  804. array(
  805. 'usernames' => array_keys($usernames),
  806. )
  807. );
  808. while ($row = $smcFunc['db_fetch_assoc']($request))
  809. if (isset($usernames[$smcFunc['strtolower']($row['member_name'])]))
  810. $usernames[$smcFunc['strtolower']($row['member_name'])] = $row['id_member'];
  811. $smcFunc['db_free_result']($request);
  812. // Replace the usernames with IDs. Drop usernames that couldn't be found.
  813. foreach ($recipients as $rec_type => $rec)
  814. foreach ($rec as $id => $member)
  815. {
  816. if (is_numeric($recipients[$rec_type][$id]))
  817. continue;
  818. if (!empty($usernames[$member]))
  819. $recipients[$rec_type][$id] = $usernames[$member];
  820. else
  821. {
  822. $log['failed'][$id] = sprintf($txt['pm_error_user_not_found'], $recipients[$rec_type][$id]);
  823. unset($recipients[$rec_type][$id]);
  824. }
  825. }
  826. }
  827. // Make sure there are no duplicate 'to' members.
  828. $recipients['to'] = array_unique($recipients['to']);
  829. // Only 'bcc' members that aren't already in 'to'.
  830. $recipients['bcc'] = array_diff(array_unique($recipients['bcc']), $recipients['to']);
  831. // Combine 'to' and 'bcc' recipients.
  832. $all_to = array_merge($recipients['to'], $recipients['bcc']);
  833. // Check no-one will want it deleted right away!
  834. $request = $smcFunc['db_query']('', '
  835. SELECT
  836. id_member, criteria, is_or
  837. FROM {db_prefix}pm_rules
  838. WHERE id_member IN ({array_int:to_members})
  839. AND delete_pm = {int:delete_pm}',
  840. array(
  841. 'to_members' => $all_to,
  842. 'delete_pm' => 1,
  843. )
  844. );
  845. $deletes = array();
  846. // Check whether we have to apply anything...
  847. while ($row = $smcFunc['db_fetch_assoc']($request))
  848. {
  849. $criteria = unserialize($row['criteria']);
  850. // Note we don't check the buddy status, cause deletion from buddy = madness!
  851. $delete = false;
  852. foreach ($criteria as $criterium)
  853. {
  854. $match = false;
  855. 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))
  856. $delete = true;
  857. // If we're adding and one criteria don't match then we stop!
  858. elseif (!$row['is_or'])
  859. {
  860. $delete = false;
  861. break;
  862. }
  863. }
  864. if ($delete)
  865. $deletes[$row['id_member']] = 1;
  866. }
  867. $smcFunc['db_free_result']($request);
  868. // Load the membergrounp message limits.
  869. //!!! Consider caching this?
  870. static $message_limit_cache = array();
  871. if (!allowedTo('moderate_forum') && empty($message_limit_cache))
  872. {
  873. $request = $smcFunc['db_query']('', '
  874. SELECT id_group, max_messages
  875. FROM {db_prefix}membergroups',
  876. array(
  877. )
  878. );
  879. while ($row = $smcFunc['db_fetch_assoc']($request))
  880. $message_limit_cache[$row['id_group']] = $row['max_messages'];
  881. $smcFunc['db_free_result']($request);
  882. }
  883. // Load the groups that are allowed to read PMs.
  884. $allowed_groups = array();
  885. $disallowed_groups = array();
  886. $request = $smcFunc['db_query']('', '
  887. SELECT id_group, add_deny
  888. FROM {db_prefix}permissions
  889. WHERE permission = {string:read_permission}',
  890. array(
  891. 'read_permission' => 'pm_read',
  892. )
  893. );
  894. while ($row = $smcFunc['db_fetch_assoc']($request))
  895. {
  896. if (empty($row['add_deny']))
  897. $disallowed_groups[] = $row['id_group'];
  898. else
  899. $allowed_groups[] = $row['id_group'];
  900. }
  901. $smcFunc['db_free_result']($request);
  902. if (empty($modSettings['permission_enable_deny']))
  903. $disallowed_groups = array();
  904. $request = $smcFunc['db_query']('', '
  905. SELECT
  906. member_name, real_name, id_member, email_address, lngfile,
  907. pm_email_notify, instant_messages,' . (allowedTo('moderate_forum') ? ' 0' : '
  908. (pm_receive_from = {int:admins_only}' . (!empty($modSettings['buddies_enable']) ? '' : ' OR
  909. (pm_receive_from = {int:buddies_only} AND NOT FIND_IN_SET({string:from_id}, buddy_list)) OR
  910. (pm_receive_from = {int:not_on_ignore_list} AND FIND_IN_SET({string:from_id}, pm_ignore_list))') . ')') . ' AS ignored,
  911. FIND_IN_SET({string:from_id}, buddy_list) AS is_buddy, is_activated,
  912. additional_groups, id_group, id_post_group
  913. FROM {db_prefix}members
  914. WHERE id_member IN ({array_int:recipients})
  915. ORDER BY lngfile
  916. LIMIT {int:count_recipients}',
  917. array(
  918. 'not_on_ignore_list' => 1,
  919. 'buddies_only' => 2,
  920. 'admins_only' => 3,
  921. 'recipients' => $all_to,
  922. 'count_recipients' => count($all_to),
  923. 'from_id' => $from['id'],
  924. )
  925. );
  926. $notifications = array();
  927. while ($row = $smcFunc['db_fetch_assoc']($request))
  928. {
  929. // Don't do anything for members to be deleted!
  930. if (isset($deletes[$row['id_member']]))
  931. continue;
  932. // We need to know this members groups.
  933. $groups = explode(',', $row['additional_groups']);
  934. $groups[] = $row['id_group'];
  935. $groups[] = $row['id_post_group'];
  936. $message_limit = -1;
  937. // For each group see whether they've gone over their limit - assuming they're not an admin.
  938. if (!in_array(1, $groups))
  939. {
  940. foreach ($groups as $id)
  941. {
  942. if (isset($message_limit_cache[$id]) && $message_limit != 0 && $message_limit < $message_limit_cache[$id])
  943. $message_limit = $message_limit_cache[$id];
  944. }
  945. if ($message_limit > 0 && $message_limit <= $row['instant_messages'])
  946. {
  947. $log['failed'][$row['id_member']] = sprintf($txt['pm_error_data_limit_reached'], $row['real_name']);
  948. unset($all_to[array_search($row['id_member'], $all_to)]);
  949. continue;
  950. }
  951. // Do they have any of the allowed groups?
  952. if (count(array_intersect($allowed_groups, $groups)) == 0 || count(array_intersect($disallowed_groups, $groups)) != 0)
  953. {
  954. $log['failed'][$row['id_member']] = sprintf($txt['pm_error_user_cannot_read'], $row['real_name']);
  955. unset($all_to[array_search($row['id_member'], $all_to)]);
  956. continue;
  957. }
  958. }
  959. // Note that PostgreSQL can return a lowercase t/f for FIND_IN_SET
  960. if (!empty($row['ignored']) && $row['ignored'] != 'f')
  961. {
  962. $log['failed'][$row['id_member']] = sprintf($txt['pm_error_ignored_by_user'], $row['real_name']);
  963. unset($all_to[array_search($row['id_member'], $all_to)]);
  964. continue;
  965. }
  966. // Send a notification, if enabled - taking into account buddy list!.
  967. 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)
  968. $notifications[empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile']][] = $row['email_address'];
  969. $log['sent'][$row['id_member']] = sprintf(isset($txt['pm_successfully_sent']) ? $txt['pm_successfully_sent'] : '', $row['real_name']);
  970. }
  971. $smcFunc['db_free_result']($request);
  972. // Only 'send' the message if there are any recipients left.
  973. if (empty($all_to))
  974. return $log;
  975. // Insert the message itself and then grab the last insert id.
  976. $smcFunc['db_insert']('',
  977. '{db_prefix}personal_messages',
  978. array(
  979. 'id_pm_head' => 'int', 'id_member_from' => 'int', 'deleted_by_sender' => 'int',
  980. 'from_name' => 'string-255', 'msgtime' => 'int', 'subject' => 'string-255', 'body' => 'string-65534',
  981. ),
  982. array(
  983. $pm_head, $from['id'], ($store_outbox ? 0 : 1),
  984. $from['username'], time(), $htmlsubject, $htmlmessage,
  985. ),
  986. array('id_pm')
  987. );
  988. $id_pm = $smcFunc['db_insert_id']('{db_prefix}personal_messages', 'id_pm');
  989. // Add the recipients.
  990. if (!empty($id_pm))
  991. {
  992. // If this is new we need to set it part of it's own conversation.
  993. if (empty($pm_head))
  994. $smcFunc['db_query']('', '
  995. UPDATE {db_prefix}personal_messages
  996. SET id_pm_head = {int:id_pm_head}
  997. WHERE id_pm = {int:id_pm_head}',
  998. array(
  999. 'id_pm_head' => $id_pm,
  1000. )
  1001. );
  1002. // Some people think manually deleting personal_messages is fun... it's not. We protect against it though :)
  1003. $smcFunc['db_query']('', '
  1004. DELETE FROM {db_prefix}pm_recipients
  1005. WHERE id_pm = {int:id_pm}',
  1006. array(
  1007. 'id_pm' => $id_pm,
  1008. )
  1009. );
  1010. $insertRows = array();
  1011. foreach ($all_to as $to)
  1012. {
  1013. $insertRows[] = array($id_pm, $to, in_array($to, $recipients['bcc']) ? 1 : 0, isset($deletes[$to]) ? 1 : 0, 1);
  1014. }
  1015. $smcFunc['db_insert']('insert',
  1016. '{db_prefix}pm_recipients',
  1017. array(
  1018. 'id_pm' => 'int', 'id_member' => 'int', 'bcc' => 'int', 'deleted' => 'int', 'is_new' => 'int'
  1019. ),
  1020. $insertRows,
  1021. array('id_pm', 'id_member')
  1022. );
  1023. }
  1024. censorText($message);
  1025. censorText($subject);
  1026. $message = trim(un_htmlspecialchars(strip_tags(strtr(parse_bbc(htmlspecialchars($message), false), array('<br />' => "\n", '</div>' => "\n", '</li>' => "\n", '&#91;' => '[', '&#93;' => ']')))));
  1027. foreach ($notifications as $lang => $notification_list)
  1028. {
  1029. // Make sure to use the right language.
  1030. loadLanguage('index+PersonalMessage', $lang, false);
  1031. // Replace the right things in the message strings.
  1032. $mailsubject = str_replace(array('SUBJECT', 'SENDER'), array($subject, un_htmlspecialchars($from['name'])), $txt['new_pm_subject']);
  1033. $mailmessage = str_replace(array('SUBJECT', 'MESSAGE', 'SENDER'), array($subject, $message, un_htmlspecialchars($from['name'])), $txt['pm_email']);
  1034. $mailmessage .= "\n\n" . $txt['instant_reply'] . ' ' . $scripturl . '?action=pm;sa=send;f=inbox;pmsg=' . $id_pm . ';quote;u=' . $from['id'];
  1035. // Off the notification email goes!
  1036. sendmail($notification_list, $mailsubject, $mailmessage, null, 'p' . $id_pm, false, 2, null, true);
  1037. }
  1038. // Back to what we were on before!
  1039. loadLanguage('index+PersonalMessage');
  1040. // Add one to their unread and read message counts.
  1041. foreach ($all_to as $k => $id)
  1042. if (isset($deletes[$id]))
  1043. unset($all_to[$k]);
  1044. if (!empty($all_to))
  1045. updateMemberData($all_to, array('instant_messages' => '+', 'unread_messages' => '+', 'new_pm' => 1));
  1046. return $log;
  1047. }
  1048. // Prepare text strings for sending as email body or header.
  1049. function mimespecialchars($string, $with_charset = true, $hotmail_fix = false, $line_break = "\r\n", $custom_charset = null)
  1050. {
  1051. global $context;
  1052. $charset = $custom_charset !== null ? $custom_charset : $context['character_set'];
  1053. // This is the fun part....
  1054. if (preg_match_all('~&#(\d{3,8});~', $string, $matches) !== 0 && !$hotmail_fix)
  1055. {
  1056. // Let's, for now, assume there are only &#021;'ish characters.
  1057. $simple = true;
  1058. foreach ($matches[1] as $entity)
  1059. if ($entity > 128)
  1060. $simple = false;
  1061. unset($matches);
  1062. if ($simple)
  1063. $string = preg_replace('~&#(\d{3,8});~e', 'chr(\'$1\')', $string);
  1064. else
  1065. {
  1066. // Try to convert the string to UTF-8.
  1067. if (!$context['utf8'] && function_exists('iconv'))
  1068. {
  1069. $newstring = @iconv($context['character_set'], 'UTF-8', $string);
  1070. if ($newstring)
  1071. $string = $newstring;
  1072. }
  1073. $fixchar = create_function('$n', '
  1074. if ($n < 128)
  1075. return chr($n);
  1076. elseif ($n < 2048)
  1077. return chr(192 | $n >> 6) . chr(128 | $n & 63);
  1078. elseif ($n < 65536)
  1079. return chr(224 | $n >> 12) . chr(128 | $n >> 6 & 63) . chr(128 | $n & 63);
  1080. else
  1081. return chr(240 | $n >> 18) . chr(128 | $n >> 12 & 63) . chr(128 | $n >> 6 & 63) . chr(128 | $n & 63);');
  1082. $string = preg_replace('~&#(\d{3,8});~e', '$fixchar(\'$1\')', $string);
  1083. // Unicode, baby.
  1084. $charset = 'UTF-8';
  1085. }
  1086. }
  1087. // Convert all special characters to HTML entities...just for Hotmail :-\
  1088. if ($hotmail_fix && ($context['utf8'] || function_exists('iconv') || $context['character_set'] === 'ISO-8859-1'))
  1089. {
  1090. if (!$context['utf8'] && function_exists('iconv'))
  1091. {
  1092. $newstring = @iconv($context['character_set'], 'UTF-8', $string);
  1093. if ($newstring)
  1094. $string = $newstring;
  1095. }
  1096. $entityConvert = create_function('$c', '
  1097. if (strlen($c) === 1 && ord($c{0}) <= 0x7F)
  1098. return $c;
  1099. elseif (strlen($c) === 2 && ord($c{0}) >= 0xC0 && ord($c{0}) <= 0xDF)

Large files files are truncated, but you can click here to view the full file