PageRenderTime 55ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/php/Sources/Subs-Post.php

https://github.com/dekoza/openshift-smf-2.0.7
PHP | 3272 lines | 2340 code | 406 blank | 526 comment | 506 complexity | 9821a5e99ae4edb7d0f2e30f1e400dbc MD5 | raw file
Possible License(s): BSD-3-Clause

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

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

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