Display.php 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727
  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
  11. */
  12. if (!defined('SMF'))
  13. die('Hacking attempt...');
  14. /* This is perhaps the most important and probably most accessed files in all
  15. of SMF. This file controls topic, message, and attachment display. It
  16. does so with the following functions:
  17. void Display()
  18. - loads the posts in a topic up so they can be displayed.
  19. - supports wireless, using wap/wap2/imode and the Wireless templates.
  20. - uses the main sub template of the Display template.
  21. - requires a topic, and can go to the previous or next topic from it.
  22. - jumps to the correct post depending on a number/time/IS_MSG passed.
  23. - depends on the messages_per_page, defaultMaxMessages and enableAllMessages settings.
  24. - is accessed by ?topic=id_topic.START.
  25. array prepareDisplayContext(bool reset = false)
  26. - actually gets and prepares the message context.
  27. - starts over from the beginning if reset is set to true, which is
  28. useful for showing an index before or after the posts.
  29. void Download()
  30. - downloads an attachment or avatar, and increments the downloads.
  31. - requires the view_attachments permission. (not for avatars!)
  32. - disables the session parser, and clears any previous output.
  33. - depends on the attachmentUploadDir setting being correct.
  34. - is accessed via the query string ?action=dlattach.
  35. - views to attachments and avatars do not increase hits and are not
  36. logged in the "Who's Online" log.
  37. array loadAttachmentContext(int id_msg)
  38. - loads an attachment's contextual data including, most importantly,
  39. its size if it is an image.
  40. - expects the $attachments array to have been filled with the proper
  41. attachment data, as Display() does.
  42. - requires the view_attachments permission to calculate image size.
  43. - attempts to keep the "aspect ratio" of the posted image in line,
  44. even if it has to be resized by the max_image_width and
  45. max_image_height settings.
  46. int approved_attach_sort(array a, array b)
  47. - a sort function for putting unapproved attachments first.
  48. void QuickInTopicModeration()
  49. - in-topic quick moderation.
  50. */
  51. // The central part of the board - topic display.
  52. function Display()
  53. {
  54. global $scripturl, $txt, $modSettings, $context, $settings;
  55. global $options, $sourcedir, $user_info, $board_info, $topic, $board;
  56. global $attachments, $messages_request, $topicinfo, $language, $smcFunc;
  57. // What are you gonna display if these are empty?!
  58. if (empty($topic))
  59. fatal_lang_error('no_board', false);
  60. // Load the proper template and/or sub template.
  61. if (WIRELESS)
  62. $context['sub_template'] = WIRELESS_PROTOCOL . '_display';
  63. else
  64. loadTemplate('Display');
  65. // Not only does a prefetch make things slower for the server, but it makes it impossible to know if they read it.
  66. if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
  67. {
  68. ob_end_clean();
  69. header('HTTP/1.1 403 Prefetch Forbidden');
  70. die;
  71. }
  72. // How much are we sticking on each page?
  73. $context['messages_per_page'] = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) && !WIRELESS ? $options['messages_per_page'] : $modSettings['defaultMaxMessages'];
  74. // Let's do some work on what to search index.
  75. if (count($_GET) > 2)
  76. foreach ($_GET as $k => $v)
  77. {
  78. if (!in_array($k, array('topic', 'board', 'start', session_name())))
  79. $context['robot_no_index'] = true;
  80. }
  81. if (!empty($_REQUEST['start']) && (!is_numeric($_REQUEST['start']) || $_REQUEST['start'] % $context['messages_per_page'] != 0))
  82. $context['robot_no_index'] = true;
  83. // Find the previous or next topic. Make a fuss if there are no more.
  84. if (isset($_REQUEST['prev_next']) && ($_REQUEST['prev_next'] == 'prev' || $_REQUEST['prev_next'] == 'next'))
  85. {
  86. // No use in calculating the next topic if there's only one.
  87. if ($board_info['num_topics'] > 1)
  88. {
  89. // Just prepare some variables that are used in the query.
  90. $gt_lt = $_REQUEST['prev_next'] == 'prev' ? '>' : '<';
  91. $order = $_REQUEST['prev_next'] == 'prev' ? '' : ' DESC';
  92. $request = $smcFunc['db_query']('', '
  93. SELECT t2.id_topic
  94. FROM {db_prefix}topics AS t
  95. INNER JOIN {db_prefix}topics AS t2 ON (' . (empty($modSettings['enableStickyTopics']) ? '
  96. t2.id_last_msg ' . $gt_lt . ' t.id_last_msg' : '
  97. (t2.id_last_msg ' . $gt_lt . ' t.id_last_msg AND t2.is_sticky ' . $gt_lt . '= t.is_sticky) OR t2.is_sticky ' . $gt_lt . ' t.is_sticky') . ')
  98. WHERE t.id_topic = {int:current_topic}
  99. AND t2.id_board = {int:current_board}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : '
  100. AND (t2.approved = {int:is_approved} OR (t2.id_member_started != {int:id_member_started} AND t2.id_member_started = {int:current_member}))') . '
  101. ORDER BY' . (empty($modSettings['enableStickyTopics']) ? '' : ' t2.is_sticky' . $order . ',') . ' t2.id_last_msg' . $order . '
  102. LIMIT 1',
  103. array(
  104. 'current_board' => $board,
  105. 'current_member' => $user_info['id'],
  106. 'current_topic' => $topic,
  107. 'is_approved' => 1,
  108. 'id_member_started' => 0,
  109. )
  110. );
  111. // No more left.
  112. if ($smcFunc['db_num_rows']($request) == 0)
  113. {
  114. $smcFunc['db_free_result']($request);
  115. // Roll over - if we're going prev, get the last - otherwise the first.
  116. $request = $smcFunc['db_query']('', '
  117. SELECT id_topic
  118. FROM {db_prefix}topics
  119. WHERE id_board = {int:current_board}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : '
  120. AND (approved = {int:is_approved} OR (id_member_started != {int:id_member_started} AND id_member_started = {int:current_member}))') . '
  121. ORDER BY' . (empty($modSettings['enableStickyTopics']) ? '' : ' is_sticky' . $order . ',') . ' id_last_msg' . $order . '
  122. LIMIT 1',
  123. array(
  124. 'current_board' => $board,
  125. 'current_member' => $user_info['id'],
  126. 'is_approved' => 1,
  127. 'id_member_started' => 0,
  128. )
  129. );
  130. }
  131. // Now you can be sure $topic is the id_topic to view.
  132. list ($topic) = $smcFunc['db_fetch_row']($request);
  133. $smcFunc['db_free_result']($request);
  134. $context['current_topic'] = $topic;
  135. }
  136. // Go to the newest message on this topic.
  137. $_REQUEST['start'] = 'new';
  138. }
  139. // Add 1 to the number of views of this topic.
  140. if (empty($_SESSION['last_read_topic']) || $_SESSION['last_read_topic'] != $topic)
  141. {
  142. $smcFunc['db_query']('', '
  143. UPDATE {db_prefix}topics
  144. SET num_views = num_views + 1
  145. WHERE id_topic = {int:current_topic}',
  146. array(
  147. 'current_topic' => $topic,
  148. )
  149. );
  150. $_SESSION['last_read_topic'] = $topic;
  151. }
  152. // Get all the important topic info.
  153. $request = $smcFunc['db_query']('', '
  154. SELECT
  155. t.num_replies, t.num_views, t.locked, ms.subject, t.is_sticky, t.id_poll,
  156. t.id_member_started, t.id_first_msg, t.id_last_msg, t.approved, t.unapproved_posts,
  157. ' . ($user_info['is_guest'] ? 't.id_last_msg + 1' : 'IFNULL(lt.id_msg, IFNULL(lmr.id_msg, -1)) + 1') . ' AS new_from
  158. ' . (!empty($modSettings['recycle_board']) && $modSettings['recycle_board'] == $board ? ', id_previous_board, id_previous_topic' : '') . '
  159. FROM {db_prefix}topics AS t
  160. INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)' . ($user_info['is_guest'] ? '' : '
  161. LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:current_topic} AND lt.id_member = {int:current_member})
  162. LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:current_board} AND lmr.id_member = {int:current_member})') . '
  163. WHERE t.id_topic = {int:current_topic}
  164. LIMIT 1',
  165. array(
  166. 'current_member' => $user_info['id'],
  167. 'current_topic' => $topic,
  168. 'current_board' => $board,
  169. )
  170. );
  171. if ($smcFunc['db_num_rows']($request) == 0)
  172. fatal_lang_error('not_a_topic', false);
  173. $topicinfo = $smcFunc['db_fetch_assoc']($request);
  174. $smcFunc['db_free_result']($request);
  175. $context['real_num_replies'] = $context['num_replies'] = $topicinfo['num_replies'];
  176. $context['topic_first_message'] = $topicinfo['id_first_msg'];
  177. $context['topic_last_message'] = $topicinfo['id_last_msg'];
  178. // Add up unapproved replies to get real number of replies...
  179. if ($modSettings['postmod_active'] && allowedTo('approve_posts'))
  180. $context['real_num_replies'] += $topicinfo['unapproved_posts'] - ($topicinfo['approved'] ? 0 : 1);
  181. // If this topic has unapproved posts, we need to work out how many posts the user can see, for page indexing.
  182. if ($modSettings['postmod_active'] && $topicinfo['unapproved_posts'] && !$user_info['is_guest'] && !allowedTo('approve_posts'))
  183. {
  184. $request = $smcFunc['db_query']('', '
  185. SELECT COUNT(id_member) AS my_unapproved_posts
  186. FROM {db_prefix}messages
  187. WHERE id_topic = {int:current_topic}
  188. AND id_member = {int:current_member}
  189. AND approved = 0',
  190. array(
  191. 'current_topic' => $topic,
  192. 'current_member' => $user_info['id'],
  193. )
  194. );
  195. list ($myUnapprovedPosts) = $smcFunc['db_fetch_row']($request);
  196. $smcFunc['db_free_result']($request);
  197. $context['total_visible_posts'] = $context['num_replies'] + $myUnapprovedPosts + ($topicinfo['approved'] ? 1 : 0);
  198. }
  199. else
  200. $context['total_visible_posts'] = $context['num_replies'] + $topicinfo['unapproved_posts'] + ($topicinfo['approved'] ? 1 : 0);
  201. // When was the last time this topic was replied to? Should we warn them about it?
  202. $request = $smcFunc['db_query']('', '
  203. SELECT poster_time
  204. FROM {db_prefix}messages
  205. WHERE id_msg = {int:id_last_msg}
  206. LIMIT 1',
  207. array(
  208. 'id_last_msg' => $topicinfo['id_last_msg'],
  209. )
  210. );
  211. list ($lastPostTime) = $smcFunc['db_fetch_row']($request);
  212. $smcFunc['db_free_result']($request);
  213. $context['oldTopicError'] = !empty($modSettings['oldTopicDays']) && $lastPostTime + $modSettings['oldTopicDays'] * 86400 < time() && empty($sticky);
  214. // The start isn't a number; it's information about what to do, where to go.
  215. if (!is_numeric($_REQUEST['start']))
  216. {
  217. // Redirect to the page and post with new messages, originally by Omar Bazavilvazo.
  218. if ($_REQUEST['start'] == 'new')
  219. {
  220. // Guests automatically go to the last post.
  221. if ($user_info['is_guest'])
  222. {
  223. $context['start_from'] = $context['total_visible_posts'] - 1;
  224. $_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : 0;
  225. }
  226. else
  227. {
  228. // Find the earliest unread message in the topic. (the use of topics here is just for both tables.)
  229. $request = $smcFunc['db_query']('', '
  230. SELECT IFNULL(lt.id_msg, IFNULL(lmr.id_msg, -1)) + 1 AS new_from
  231. FROM {db_prefix}topics AS t
  232. LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:current_topic} AND lt.id_member = {int:current_member})
  233. LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:current_board} AND lmr.id_member = {int:current_member})
  234. WHERE t.id_topic = {int:current_topic}
  235. LIMIT 1',
  236. array(
  237. 'current_board' => $board,
  238. 'current_member' => $user_info['id'],
  239. 'current_topic' => $topic,
  240. )
  241. );
  242. list ($new_from) = $smcFunc['db_fetch_row']($request);
  243. $smcFunc['db_free_result']($request);
  244. // Fall through to the next if statement.
  245. $_REQUEST['start'] = 'msg' . $new_from;
  246. }
  247. }
  248. // Start from a certain time index, not a message.
  249. if (substr($_REQUEST['start'], 0, 4) == 'from')
  250. {
  251. $timestamp = (int) substr($_REQUEST['start'], 4);
  252. if ($timestamp === 0)
  253. $_REQUEST['start'] = 0;
  254. else
  255. {
  256. // Find the number of messages posted before said time...
  257. $request = $smcFunc['db_query']('', '
  258. SELECT COUNT(*)
  259. FROM {db_prefix}messages
  260. WHERE poster_time < {int:timestamp}
  261. AND id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && $topicinfo['unapproved_posts'] && !allowedTo('approve_posts') ? '
  262. AND (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')' : ''),
  263. array(
  264. 'current_topic' => $topic,
  265. 'current_member' => $user_info['id'],
  266. 'is_approved' => 1,
  267. 'timestamp' => $timestamp,
  268. )
  269. );
  270. list ($context['start_from']) = $smcFunc['db_fetch_row']($request);
  271. $smcFunc['db_free_result']($request);
  272. // Handle view_newest_first options, and get the correct start value.
  273. $_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : $context['total_visible_posts'] - $context['start_from'] - 1;
  274. }
  275. }
  276. // Link to a message...
  277. elseif (substr($_REQUEST['start'], 0, 3) == 'msg')
  278. {
  279. $virtual_msg = (int) substr($_REQUEST['start'], 3);
  280. if (!$topicinfo['unapproved_posts'] && $virtual_msg >= $topicinfo['id_last_msg'])
  281. $context['start_from'] = $context['total_visible_posts'] - 1;
  282. elseif (!$topicinfo['unapproved_posts'] && $virtual_msg <= $topicinfo['id_first_msg'])
  283. $context['start_from'] = 0;
  284. else
  285. {
  286. // Find the start value for that message......
  287. $request = $smcFunc['db_query']('', '
  288. SELECT COUNT(*)
  289. FROM {db_prefix}messages
  290. WHERE id_msg < {int:virtual_msg}
  291. AND id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && $topicinfo['unapproved_posts'] && !allowedTo('approve_posts') ? '
  292. AND (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')' : ''),
  293. array(
  294. 'current_member' => $user_info['id'],
  295. 'current_topic' => $topic,
  296. 'virtual_msg' => $virtual_msg,
  297. 'is_approved' => 1,
  298. 'no_member' => 0,
  299. )
  300. );
  301. list ($context['start_from']) = $smcFunc['db_fetch_row']($request);
  302. $smcFunc['db_free_result']($request);
  303. }
  304. // We need to reverse the start as well in this case.
  305. $_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : $context['total_visible_posts'] - $context['start_from'] - 1;
  306. }
  307. }
  308. // Create a previous next string if the selected theme has it as a selected option.
  309. $context['previous_next'] = $modSettings['enablePreviousNext'] ? '<a href="' . $scripturl . '?topic=' . $topic . '.0;prev_next=prev#new">' . $txt['previous_next_back'] . '</a> <a href="' . $scripturl . '?topic=' . $topic . '.0;prev_next=next#new">' . $txt['previous_next_forward'] . '</a>' : '';
  310. // Check if spellchecking is both enabled and actually working. (for quick reply.)
  311. $context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && function_exists('pspell_new');
  312. // Do we need to show the visual verification image?
  313. $context['require_verification'] = !$user_info['is_mod'] && !$user_info['is_admin'] && !empty($modSettings['posts_require_captcha']) && ($user_info['posts'] < $modSettings['posts_require_captcha'] || ($user_info['is_guest'] && $modSettings['posts_require_captcha'] == -1));
  314. if ($context['require_verification'])
  315. {
  316. require_once($sourcedir . '/Subs-Editor.php');
  317. $verificationOptions = array(
  318. 'id' => 'post',
  319. );
  320. $context['require_verification'] = create_control_verification($verificationOptions);
  321. $context['visual_verification_id'] = $verificationOptions['id'];
  322. }
  323. // Are we showing signatures - or disabled fields?
  324. $context['signature_enabled'] = substr($modSettings['signature_settings'], 0, 1) == 1;
  325. $context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array();
  326. // Censor the title...
  327. censorText($topicinfo['subject']);
  328. $context['page_title'] = $topicinfo['subject'];
  329. // Is this topic sticky, or can it even be?
  330. $topicinfo['is_sticky'] = empty($modSettings['enableStickyTopics']) ? '0' : $topicinfo['is_sticky'];
  331. // Default this topic to not marked for notifications... of course...
  332. $context['is_marked_notify'] = false;
  333. // Did we report a post to a moderator just now?
  334. $context['report_sent'] = isset($_GET['reportsent']);
  335. // Let's get nosey, who is viewing this topic?
  336. if (!empty($settings['display_who_viewing']))
  337. {
  338. // Start out with no one at all viewing it.
  339. $context['view_members'] = array();
  340. $context['view_members_list'] = array();
  341. $context['view_num_hidden'] = 0;
  342. // Search for members who have this topic set in their GET data.
  343. $request = $smcFunc['db_query']('', '
  344. SELECT
  345. lo.id_member, lo.log_time, mem.real_name, mem.member_name, mem.show_online,
  346. mg.online_color, mg.id_group, mg.group_name
  347. FROM {db_prefix}log_online AS lo
  348. LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lo.id_member)
  349. LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = CASE WHEN mem.id_group = {int:reg_id_group} THEN mem.id_post_group ELSE mem.id_group END)
  350. WHERE INSTR(lo.url, {string:in_url_string}) > 0 OR lo.session = {string:session}',
  351. array(
  352. 'reg_id_group' => 0,
  353. 'in_url_string' => 's:5:"topic";i:' . $topic . ';',
  354. 'session' => $user_info['is_guest'] ? 'ip' . $user_info['ip'] : session_id(),
  355. )
  356. );
  357. while ($row = $smcFunc['db_fetch_assoc']($request))
  358. {
  359. if (empty($row['id_member']))
  360. continue;
  361. if (!empty($row['online_color']))
  362. $link = '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '" style="color: ' . $row['online_color'] . ';">' . $row['real_name'] . '</a>';
  363. else
  364. $link = '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>';
  365. $is_buddy = in_array($row['id_member'], $user_info['buddies']);
  366. if ($is_buddy)
  367. $link = '<strong>' . $link . '</strong>';
  368. // Add them both to the list and to the more detailed list.
  369. if (!empty($row['show_online']) || allowedTo('moderate_forum'))
  370. $context['view_members_list'][$row['log_time'] . $row['member_name']] = empty($row['show_online']) ? '<em>' . $link . '</em>' : $link;
  371. $context['view_members'][$row['log_time'] . $row['member_name']] = array(
  372. 'id' => $row['id_member'],
  373. 'username' => $row['member_name'],
  374. 'name' => $row['real_name'],
  375. 'group' => $row['id_group'],
  376. 'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
  377. 'link' => $link,
  378. 'is_buddy' => $is_buddy,
  379. 'hidden' => empty($row['show_online']),
  380. );
  381. if (empty($row['show_online']))
  382. $context['view_num_hidden']++;
  383. }
  384. // The number of guests is equal to the rows minus the ones we actually used ;).
  385. $context['view_num_guests'] = $smcFunc['db_num_rows']($request) - count($context['view_members']);
  386. $smcFunc['db_free_result']($request);
  387. // Sort the list.
  388. krsort($context['view_members']);
  389. krsort($context['view_members_list']);
  390. }
  391. // If all is set, but not allowed... just unset it.
  392. $can_show_all = !empty($modSettings['enableAllMessages']) && $context['total_visible_posts'] > $context['messages_per_page'] && $context['total_visible_posts'] < $modSettings['enableAllMessages'];
  393. if (isset($_REQUEST['all']) && !$can_show_all)
  394. unset($_REQUEST['all']);
  395. // Otherwise, it must be allowed... so pretend start was -1.
  396. elseif (isset($_REQUEST['all']))
  397. $_REQUEST['start'] = -1;
  398. // Construct the page index, allowing for the .START method...
  399. $context['page_index'] = constructPageIndex($scripturl . '?topic=' . $topic . '.%1$d', $_REQUEST['start'], $context['total_visible_posts'], $context['messages_per_page'], true);
  400. $context['start'] = $_REQUEST['start'];
  401. // This is information about which page is current, and which page we're on - in case you don't like the constructed page index. (again, wireles..)
  402. $context['page_info'] = array(
  403. 'current_page' => $_REQUEST['start'] / $context['messages_per_page'] + 1,
  404. 'num_pages' => floor(($context['total_visible_posts'] - 1) / $context['messages_per_page']) + 1,
  405. );
  406. // Figure out all the link to the next/prev/first/last/etc. for wireless mainly.
  407. $context['links'] = array(
  408. 'first' => $_REQUEST['start'] >= $context['messages_per_page'] ? $scripturl . '?topic=' . $topic . '.0' : '',
  409. 'prev' => $_REQUEST['start'] >= $context['messages_per_page'] ? $scripturl . '?topic=' . $topic . '.' . ($_REQUEST['start'] - $context['messages_per_page']) : '',
  410. 'next' => $_REQUEST['start'] + $context['messages_per_page'] < $context['total_visible_posts'] ? $scripturl . '?topic=' . $topic. '.' . ($_REQUEST['start'] + $context['messages_per_page']) : '',
  411. 'last' => $_REQUEST['start'] + $context['messages_per_page'] < $context['total_visible_posts'] ? $scripturl . '?topic=' . $topic. '.' . (floor($context['total_visible_posts'] / $context['messages_per_page']) * $context['messages_per_page']) : '',
  412. 'up' => $scripturl . '?board=' . $board . '.0'
  413. );
  414. // If they are viewing all the posts, show all the posts, otherwise limit the number.
  415. if ($can_show_all)
  416. {
  417. if (isset($_REQUEST['all']))
  418. {
  419. // No limit! (actually, there is a limit, but...)
  420. $context['messages_per_page'] = -1;
  421. $context['page_index'] .= empty($modSettings['compactTopicPagesEnable']) ? '<strong>' . $txt['all'] . '</strong> ' : '[<strong>' . $txt['all'] . '</strong>] ';
  422. // Set start back to 0...
  423. $_REQUEST['start'] = 0;
  424. }
  425. // They aren't using it, but the *option* is there, at least.
  426. else
  427. $context['page_index'] .= '&nbsp;<a href="' . $scripturl . '?topic=' . $topic . '.0;all">' . $txt['all'] . '</a> ';
  428. }
  429. // Build the link tree.
  430. $context['linktree'][] = array(
  431. 'url' => $scripturl . '?topic=' . $topic . '.0',
  432. 'name' => $topicinfo['subject'],
  433. 'extra_before' => $settings['linktree_inline'] ? $txt['topic'] . ': ' : ''
  434. );
  435. // Build a list of this board's moderators.
  436. $context['moderators'] = &$board_info['moderators'];
  437. $context['link_moderators'] = array();
  438. if (!empty($board_info['moderators']))
  439. {
  440. // Add a link for each moderator...
  441. foreach ($board_info['moderators'] as $mod)
  442. $context['link_moderators'][] = '<a href="' . $scripturl . '?action=profile;u=' . $mod['id'] . '" title="' . $txt['board_moderator'] . '">' . $mod['name'] . '</a>';
  443. // And show it after the board's name.
  444. $context['linktree'][count($context['linktree']) - 2]['extra_after'] = ' (' . (count($context['link_moderators']) == 1 ? $txt['moderator'] : $txt['moderators']) . ': ' . implode(', ', $context['link_moderators']) . ')';
  445. }
  446. // Information about the current topic...
  447. $context['is_locked'] = $topicinfo['locked'];
  448. $context['is_sticky'] = $topicinfo['is_sticky'];
  449. $context['is_very_hot'] = $topicinfo['num_replies'] >= $modSettings['hotTopicVeryPosts'];
  450. $context['is_hot'] = $topicinfo['num_replies'] >= $modSettings['hotTopicPosts'];
  451. $context['is_approved'] = $topicinfo['approved'];
  452. // We don't want to show the poll icon in the topic class here, so pretend it's not one.
  453. $context['is_poll'] = false;
  454. determineTopicClass($context);
  455. $context['is_poll'] = $topicinfo['id_poll'] > 0 && $modSettings['pollMode'] == '1' && allowedTo('poll_view');
  456. // Did this user start the topic or not?
  457. $context['user']['started'] = $user_info['id'] == $topicinfo['id_member_started'] && !$user_info['is_guest'];
  458. $context['topic_starter_id'] = $topicinfo['id_member_started'];
  459. // Set the topic's information for the template.
  460. $context['subject'] = $topicinfo['subject'];
  461. $context['num_views'] = $topicinfo['num_views'];
  462. $context['mark_unread_time'] = $topicinfo['new_from'];
  463. // Set a canonical URL for this page.
  464. $context['canonical_url'] = $scripturl . '?topic=' . $topic . '.' . $context['start'];
  465. // For quick reply we need a response prefix in the default forum language.
  466. if (!isset($context['response_prefix']) && !($context['response_prefix'] = cache_get_data('response_prefix', 600)))
  467. {
  468. if ($language === $user_info['language'])
  469. $context['response_prefix'] = $txt['response_prefix'];
  470. else
  471. {
  472. loadLanguage('index', $language, false);
  473. $context['response_prefix'] = $txt['response_prefix'];
  474. loadLanguage('index');
  475. }
  476. cache_put_data('response_prefix', $context['response_prefix'], 600);
  477. }
  478. // If we want to show event information in the topic, prepare the data.
  479. if (allowedTo('calendar_view') && !empty($modSettings['cal_showInTopic']) && !empty($modSettings['cal_enabled']))
  480. {
  481. // First, try create a better time format, ignoring the "time" elements.
  482. if (preg_match('~%[AaBbCcDdeGghjmuYy](?:[^%]*%[AaBbCcDdeGghjmuYy])*~', $user_info['time_format'], $matches) == 0 || empty($matches[0]))
  483. $date_string = $user_info['time_format'];
  484. else
  485. $date_string = $matches[0];
  486. // Any calendar information for this topic?
  487. $request = $smcFunc['db_query']('', '
  488. SELECT cal.id_event, cal.start_date, cal.end_date, cal.title, cal.id_member, mem.real_name
  489. FROM {db_prefix}calendar AS cal
  490. LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = cal.id_member)
  491. WHERE cal.id_topic = {int:current_topic}
  492. ORDER BY start_date',
  493. array(
  494. 'current_topic' => $topic,
  495. )
  496. );
  497. $context['linked_calendar_events'] = array();
  498. while ($row = $smcFunc['db_fetch_assoc']($request))
  499. {
  500. // Prepare the dates for being formatted.
  501. $start_date = sscanf($row['start_date'], '%04d-%02d-%02d');
  502. $start_date = mktime(12, 0, 0, $start_date[1], $start_date[2], $start_date[0]);
  503. $end_date = sscanf($row['end_date'], '%04d-%02d-%02d');
  504. $end_date = mktime(12, 0, 0, $end_date[1], $end_date[2], $end_date[0]);
  505. $context['linked_calendar_events'][] = array(
  506. 'id' => $row['id_event'],
  507. 'title' => $row['title'],
  508. 'can_edit' => allowedTo('calendar_edit_any') || ($row['id_member'] == $user_info['id'] && allowedTo('calendar_edit_own')),
  509. 'modify_href' => $scripturl . '?action=post;msg=' . $topicinfo['id_first_msg'] . ';topic=' . $topic . '.0;calendar;eventid=' . $row['id_event'] . ';' . $context['session_var'] . '=' . $context['session_id'],
  510. 'start_date' => timeformat($start_date, $date_string, 'none'),
  511. 'start_timestamp' => $start_date,
  512. 'end_date' => timeformat($end_date, $date_string, 'none'),
  513. 'end_timestamp' => $end_date,
  514. 'is_last' => false
  515. );
  516. }
  517. $smcFunc['db_free_result']($request);
  518. if (!empty($context['linked_calendar_events']))
  519. $context['linked_calendar_events'][count($context['linked_calendar_events']) - 1]['is_last'] = true;
  520. }
  521. // Create the poll info if it exists.
  522. if ($context['is_poll'])
  523. {
  524. // Get the question and if it's locked.
  525. $request = $smcFunc['db_query']('', '
  526. SELECT
  527. p.question, p.voting_locked, p.hide_results, p.expire_time, p.max_votes, p.change_vote,
  528. p.guest_vote, p.id_member, IFNULL(mem.real_name, p.poster_name) AS poster_name, p.num_guest_voters, p.reset_poll
  529. FROM {db_prefix}polls AS p
  530. LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = p.id_member)
  531. WHERE p.id_poll = {int:id_poll}
  532. LIMIT 1',
  533. array(
  534. 'id_poll' => $topicinfo['id_poll'],
  535. )
  536. );
  537. $pollinfo = $smcFunc['db_fetch_assoc']($request);
  538. $smcFunc['db_free_result']($request);
  539. $request = $smcFunc['db_query']('', '
  540. SELECT COUNT(DISTINCT id_member) AS total
  541. FROM {db_prefix}log_polls
  542. WHERE id_poll = {int:id_poll}
  543. AND id_member != {int:not_guest}',
  544. array(
  545. 'id_poll' => $topicinfo['id_poll'],
  546. 'not_guest' => 0,
  547. )
  548. );
  549. list ($pollinfo['total']) = $smcFunc['db_fetch_row']($request);
  550. $smcFunc['db_free_result']($request);
  551. // Total voters needs to include guest voters
  552. $pollinfo['total'] += $pollinfo['num_guest_voters'];
  553. // Get all the options, and calculate the total votes.
  554. $request = $smcFunc['db_query']('', '
  555. SELECT pc.id_choice, pc.label, pc.votes, IFNULL(lp.id_choice, -1) AS voted_this
  556. FROM {db_prefix}poll_choices AS pc
  557. LEFT JOIN {db_prefix}log_polls AS lp ON (lp.id_choice = pc.id_choice AND lp.id_poll = {int:id_poll} AND lp.id_member = {int:current_member} AND lp.id_member != {int:not_guest})
  558. WHERE pc.id_poll = {int:id_poll}',
  559. array(
  560. 'current_member' => $user_info['id'],
  561. 'id_poll' => $topicinfo['id_poll'],
  562. 'not_guest' => 0,
  563. )
  564. );
  565. $pollOptions = array();
  566. $realtotal = 0;
  567. $pollinfo['has_voted'] = false;
  568. while ($row = $smcFunc['db_fetch_assoc']($request))
  569. {
  570. censorText($row['label']);
  571. $pollOptions[$row['id_choice']] = $row;
  572. $realtotal += $row['votes'];
  573. $pollinfo['has_voted'] |= $row['voted_this'] != -1;
  574. }
  575. $smcFunc['db_free_result']($request);
  576. // If this is a guest we need to do our best to work out if they have voted, and what they voted for.
  577. if ($user_info['is_guest'] && $pollinfo['guest_vote'] && allowedTo('poll_vote'))
  578. {
  579. if (!empty($_COOKIE['guest_poll_vote']) && preg_match('~^[0-9,;]+$~', $_COOKIE['guest_poll_vote']) && strpos($_COOKIE['guest_poll_vote'], ';' . $topicinfo['id_poll'] . ',') !== false)
  580. {
  581. // ;id,timestamp,[vote,vote...]; etc
  582. $guestinfo = explode(';', $_COOKIE['guest_poll_vote']);
  583. // Find the poll we're after.
  584. foreach ($guestinfo as $i => $guestvoted)
  585. {
  586. $guestvoted = explode(',', $guestvoted);
  587. if ($guestvoted[0] == $topicinfo['id_poll'])
  588. break;
  589. }
  590. // Has the poll been reset since guest voted?
  591. if ($pollinfo['reset_poll'] > $guestvoted[1])
  592. {
  593. // Remove the poll info from the cookie to allow guest to vote again
  594. unset($guestinfo[$i]);
  595. if (!empty($guestinfo))
  596. $_COOKIE['guest_poll_vote'] = ';' . implode(';', $guestinfo);
  597. else
  598. unset($_COOKIE['guest_poll_vote']);
  599. }
  600. else
  601. {
  602. // What did they vote for?
  603. unset($guestvoted[0], $guestvoted[1]);
  604. foreach ($pollOptions as $choice => $details)
  605. {
  606. $pollOptions[$choice]['voted_this'] = in_array($choice, $guestvoted) ? 1 : -1;
  607. $pollinfo['has_voted'] |= $pollOptions[$choice]['voted_this'] != -1;
  608. }
  609. unset($choice, $details, $guestvoted);
  610. }
  611. unset($guestinfo, $guestvoted, $i);
  612. }
  613. }
  614. // Set up the basic poll information.
  615. $context['poll'] = array(
  616. 'id' => $topicinfo['id_poll'],
  617. 'image' => 'normal_' . (empty($pollinfo['voting_locked']) ? 'poll' : 'locked_poll'),
  618. 'question' => parse_bbc($pollinfo['question']),
  619. 'total_votes' => $pollinfo['total'],
  620. 'change_vote' => !empty($pollinfo['change_vote']),
  621. 'is_locked' => !empty($pollinfo['voting_locked']),
  622. 'options' => array(),
  623. 'lock' => allowedTo('poll_lock_any') || ($context['user']['started'] && allowedTo('poll_lock_own')),
  624. 'edit' => allowedTo('poll_edit_any') || ($context['user']['started'] && allowedTo('poll_edit_own')),
  625. 'allowed_warning' => $pollinfo['max_votes'] > 1 ? sprintf($txt['poll_options6'], min(count($pollOptions), $pollinfo['max_votes'])) : '',
  626. 'is_expired' => !empty($pollinfo['expire_time']) && $pollinfo['expire_time'] < time(),
  627. 'expire_time' => !empty($pollinfo['expire_time']) ? timeformat($pollinfo['expire_time']) : 0,
  628. 'has_voted' => !empty($pollinfo['has_voted']),
  629. 'starter' => array(
  630. 'id' => $pollinfo['id_member'],
  631. 'name' => $row['poster_name'],
  632. 'href' => $pollinfo['id_member'] == 0 ? '' : $scripturl . '?action=profile;u=' . $pollinfo['id_member'],
  633. 'link' => $pollinfo['id_member'] == 0 ? $row['poster_name'] : '<a href="' . $scripturl . '?action=profile;u=' . $pollinfo['id_member'] . '">' . $row['poster_name'] . '</a>'
  634. )
  635. );
  636. // Make the lock and edit permissions defined above more directly accessible.
  637. $context['allow_lock_poll'] = $context['poll']['lock'];
  638. $context['allow_edit_poll'] = $context['poll']['edit'];
  639. // You're allowed to vote if:
  640. // 1. the poll did not expire, and
  641. // 2. you're either not a guest OR guest voting is enabled... and
  642. // 3. you're not trying to view the results, and
  643. // 4. the poll is not locked, and
  644. // 5. you have the proper permissions, and
  645. // 6. you haven't already voted before.
  646. $context['allow_vote'] = !$context['poll']['is_expired'] && (!$user_info['is_guest'] || ($pollinfo['guest_vote'] && allowedTo('poll_vote'))) && empty($pollinfo['voting_locked']) && allowedTo('poll_vote') && !$context['poll']['has_voted'];
  647. // You're allowed to view the results if:
  648. // 1. you're just a super-nice-guy, or
  649. // 2. anyone can see them (hide_results == 0), or
  650. // 3. you can see them after you voted (hide_results == 1), or
  651. // 4. you've waited long enough for the poll to expire. (whether hide_results is 1 or 2.)
  652. $context['allow_poll_view'] = allowedTo('moderate_board') || $pollinfo['hide_results'] == 0 || ($pollinfo['hide_results'] == 1 && $context['poll']['has_voted']) || $context['poll']['is_expired'];
  653. $context['poll']['show_results'] = $context['allow_poll_view'] && (isset($_REQUEST['viewresults']) || isset($_REQUEST['viewResults']));
  654. $context['show_view_results_button'] = $context['allow_vote'] && (!$context['allow_poll_view'] || !$context['poll']['show_results'] || !$context['poll']['has_voted']);
  655. // You're allowed to change your vote if:
  656. // 1. the poll did not expire, and
  657. // 2. you're not a guest... and
  658. // 3. the poll is not locked, and
  659. // 4. you have the proper permissions, and
  660. // 5. you have already voted, and
  661. // 6. the poll creator has said you can!
  662. $context['allow_change_vote'] = !$context['poll']['is_expired'] && !$user_info['is_guest'] && empty($pollinfo['voting_locked']) && allowedTo('poll_vote') && $context['poll']['has_voted'] && $context['poll']['change_vote'];
  663. // You're allowed to return to voting options if:
  664. // 1. you are (still) allowed to vote.
  665. // 2. you are currently seeing the results.
  666. $context['allow_return_vote'] = $context['allow_vote'] && $context['poll']['show_results'];
  667. // Calculate the percentages and bar lengths...
  668. $divisor = $realtotal == 0 ? 1 : $realtotal;
  669. // Determine if a decimal point is needed in order for the options to add to 100%.
  670. $precision = $realtotal == 100 ? 0 : 1;
  671. // Now look through each option, and...
  672. foreach ($pollOptions as $i => $option)
  673. {
  674. // First calculate the percentage, and then the width of the bar...
  675. $bar = round(($option['votes'] * 100) / $divisor, $precision);
  676. $barWide = $bar == 0 ? 1 : floor(($bar * 8) / 3);
  677. // Now add it to the poll's contextual theme data.
  678. $context['poll']['options'][$i] = array(
  679. 'id' => 'options-' . $i,
  680. 'percent' => $bar,
  681. 'votes' => $option['votes'],
  682. 'voted_this' => $option['voted_this'] != -1,
  683. 'bar' => '<span style="white-space: nowrap;"><img src="' . $settings['images_url'] . '/poll_' . ($context['right_to_left'] ? 'right' : 'left') . '.gif" alt="" /><img src="' . $settings['images_url'] . '/poll_middle.gif" width="' . $barWide . '" height="12" alt="-" /><img src="' . $settings['images_url'] . '/poll_' . ($context['right_to_left'] ? 'left' : 'right') . '.gif" alt="" /></span>',
  684. // Note: IE < 8 requires us to set a width on the container, too.
  685. 'bar_ndt' => $bar > 0 ? '<div class="bar" style="width: ' . ($bar * 3.5 + 4) . 'px;"><div style="width: ' . $bar * 3.5 . 'px;"></div></div>' : '',
  686. 'bar_width' => $barWide,
  687. 'option' => parse_bbc($option['label']),
  688. 'vote_button' => '<input type="' . ($pollinfo['max_votes'] > 1 ? 'checkbox' : 'radio') . '" name="options[]" id="options-' . $i . '" value="' . $i . '" class="input_' . ($pollinfo['max_votes'] > 1 ? 'check' : 'radio') . '" />'
  689. );
  690. }
  691. }
  692. // Calculate the fastest way to get the messages!
  693. $ascending = empty($options['view_newest_first']);
  694. $start = $_REQUEST['start'];
  695. $limit = $context['messages_per_page'];
  696. $firstIndex = 0;
  697. if ($start >= $context['total_visible_posts'] / 2 && $context['messages_per_page'] != -1)
  698. {
  699. $ascending = !$ascending;
  700. $limit = $context['total_visible_posts'] <= $start + $limit ? $context['total_visible_posts'] - $start : $limit;
  701. $start = $context['total_visible_posts'] <= $start + $limit ? 0 : $context['total_visible_posts'] - $start - $limit;
  702. $firstIndex = $limit - 1;
  703. }
  704. // Get each post and poster in this topic.
  705. $request = $smcFunc['db_query']('display_get_post_poster', '
  706. SELECT id_msg, id_member, approved
  707. FROM {db_prefix}messages
  708. WHERE id_topic = {int:current_topic}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : (!empty($modSettings['db_mysql_group_by_fix']) ? '' : '
  709. GROUP BY id_msg') . '
  710. HAVING (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')') . '
  711. ORDER BY id_msg ' . ($ascending ? '' : 'DESC') . ($context['messages_per_page'] == -1 ? '' : '
  712. LIMIT ' . $start . ', ' . $limit),
  713. array(
  714. 'current_member' => $user_info['id'],
  715. 'current_topic' => $topic,
  716. 'is_approved' => 1,
  717. 'blank_id_member' => 0,
  718. )
  719. );
  720. $messages = array();
  721. $all_posters = array();
  722. while ($row = $smcFunc['db_fetch_assoc']($request))
  723. {
  724. if (!empty($row['id_member']))
  725. $all_posters[$row['id_msg']] = $row['id_member'];
  726. $messages[] = $row['id_msg'];
  727. }
  728. $smcFunc['db_free_result']($request);
  729. $posters = array_unique($all_posters);
  730. // Guests can't mark topics read or for notifications, just can't sorry.
  731. if (!$user_info['is_guest'])
  732. {
  733. $mark_at_msg = max($messages);
  734. if ($mark_at_msg >= $topicinfo['id_last_msg'])
  735. $mark_at_msg = $modSettings['maxMsgID'];
  736. if ($mark_at_msg >= $topicinfo['new_from'])
  737. {
  738. $smcFunc['db_insert']($topicinfo['new_from'] == 0 ? 'ignore' : 'replace',
  739. '{db_prefix}log_topics',
  740. array(
  741. 'id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int',
  742. ),
  743. array(
  744. $user_info['id'], $topic, $mark_at_msg,
  745. ),
  746. array('id_member', 'id_topic')
  747. );
  748. }
  749. // Check for notifications on this topic OR board.
  750. $request = $smcFunc['db_query']('', '
  751. SELECT sent, id_topic
  752. FROM {db_prefix}log_notify
  753. WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
  754. AND id_member = {int:current_member}
  755. LIMIT 2',
  756. array(
  757. 'current_board' => $board,
  758. 'current_member' => $user_info['id'],
  759. 'current_topic' => $topic,
  760. )
  761. );
  762. $do_once = true;
  763. while ($row = $smcFunc['db_fetch_assoc']($request))
  764. {
  765. // Find if this topic is marked for notification...
  766. if (!empty($row['id_topic']))
  767. $context['is_marked_notify'] = true;
  768. // Only do this once, but mark the notifications as "not sent yet" for next time.
  769. if (!empty($row['sent']) && $do_once)
  770. {
  771. $smcFunc['db_query']('', '
  772. UPDATE {db_prefix}log_notify
  773. SET sent = {int:is_not_sent}
  774. WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
  775. AND id_member = {int:current_member}',
  776. array(
  777. 'current_board' => $board,
  778. 'current_member' => $user_info['id'],
  779. 'current_topic' => $topic,
  780. 'is_not_sent' => 0,
  781. )
  782. );
  783. $do_once = false;
  784. }
  785. }
  786. // Have we recently cached the number of new topics in this board, and it's still a lot?
  787. if (isset($_REQUEST['topicseen']) && isset($_SESSION['topicseen_cache'][$board]) && $_SESSION['topicseen_cache'][$board] > 5)
  788. $_SESSION['topicseen_cache'][$board]--;
  789. // Mark board as seen if this is the only new topic.
  790. elseif (isset($_REQUEST['topicseen']))
  791. {
  792. // Use the mark read tables... and the last visit to figure out if this should be read or not.
  793. $request = $smcFunc['db_query']('', '
  794. SELECT COUNT(*)
  795. FROM {db_prefix}topics AS t
  796. LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = {int:current_board} AND lb.id_member = {int:current_member})
  797. LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
  798. WHERE t.id_board = {int:current_board}
  799. AND t.id_last_msg > IFNULL(lb.id_msg, 0)
  800. AND t.id_last_msg > IFNULL(lt.id_msg, 0)' . (empty($_SESSION['id_msg_last_visit']) ? '' : '
  801. AND t.id_last_msg > {int:id_msg_last_visit}'),
  802. array(
  803. 'current_board' => $board,
  804. 'current_member' => $user_info['id'],
  805. 'id_msg_last_visit' => (int) $_SESSION['id_msg_last_visit'],
  806. )
  807. );
  808. list ($numNewTopics) = $smcFunc['db_fetch_row']($request);
  809. $smcFunc['db_free_result']($request);
  810. // If there're no real new topics in this board, mark the board as seen.
  811. if (empty($numNewTopics))
  812. $_REQUEST['boardseen'] = true;
  813. else
  814. $_SESSION['topicseen_cache'][$board] = $numNewTopics;
  815. }
  816. // Probably one less topic - maybe not, but even if we decrease this too fast it will only make us look more often.
  817. elseif (isset($_SESSION['topicseen_cache'][$board]))
  818. $_SESSION['topicseen_cache'][$board]--;
  819. // Mark board as seen if we came using last post link from BoardIndex. (or other places...)
  820. if (isset($_REQUEST['boardseen']))
  821. {
  822. $smcFunc['db_insert']('replace',
  823. '{db_prefix}log_boards',
  824. array('id_msg' => 'int', 'id_member' => 'int', 'id_board' => 'int'),
  825. array($modSettings['maxMsgID'], $user_info['id'], $board),
  826. array('id_member', 'id_board')
  827. );
  828. }
  829. }
  830. $attachments = array();
  831. // If there _are_ messages here... (probably an error otherwise :!)
  832. if (!empty($messages))
  833. {
  834. // Fetch attachments.
  835. if (!empty($modSettings['attachmentEnable']) && allowedTo('view_attachments'))
  836. {
  837. $request = $smcFunc['db_query']('', '
  838. SELECT
  839. a.id_attach, a.id_folder, a.id_msg, a.filename, a.file_hash, IFNULL(a.size, 0) AS filesize, a.downloads, a.approved,
  840. a.width, a.height' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ',
  841. IFNULL(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height') . '
  842. FROM {db_prefix}attachments AS a' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : '
  843. LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)') . '
  844. WHERE a.id_msg IN ({array_int:message_list})
  845. AND a.attachment_type = {int:attachment_type}',
  846. array(
  847. 'message_list' => $messages,
  848. 'attachment_type' => 0,
  849. 'is_approved' => 1,
  850. )
  851. );
  852. $temp = array();
  853. while ($row = $smcFunc['db_fetch_assoc']($request))
  854. {
  855. if (!$row['approved'] && $modSettings['postmod_active'] && !allowedTo('approve_posts') && (!isset($all_posters[$row['id_msg']]) || $all_posters[$row['id_msg']] != $user_info['id']))
  856. continue;
  857. $temp[$row['id_attach']] = $row;
  858. if (!isset($attachments[$row['id_msg']]))
  859. $attachments[$row['id_msg']] = array();
  860. }
  861. $smcFunc['db_free_result']($request);
  862. // This is better than sorting it with the query...
  863. ksort($temp);
  864. foreach ($temp as $row)
  865. $attachments[$row['id_msg']][] = $row;
  866. }
  867. // What? It's not like it *couldn't* be only guests in this topic...
  868. if (!empty($posters))
  869. loadMemberData($posters);
  870. $messages_request = $smcFunc['db_query']('', '
  871. SELECT
  872. id_msg, icon, subject, poster_time, poster_ip, id_member, modified_time, modified_name, body,
  873. smileys_enabled, poster_name, poster_email, approved,
  874. id_msg_modified < {int:new_from} AS is_read
  875. FROM {db_prefix}messages
  876. WHERE id_msg IN ({array_int:message_list})
  877. ORDER BY id_msg' . (empty($options['view_newest_first']) ? '' : ' DESC'),
  878. array(
  879. 'message_list' => $messages,
  880. 'new_from' => $topicinfo['new_from'],
  881. )
  882. );
  883. // Go to the last message if the given time is beyond the time of the last message.
  884. if (isset($context['start_from']) && $context['start_from'] >= $topicinfo['num_replies'])
  885. $context['start_from'] = $topicinfo['num_replies'];
  886. // Since the anchor information is needed on the top of the page we load these variables beforehand.
  887. $context['first_message'] = isset($messages[$firstIndex]) ? $messages[$firstIndex] : $messages[0];
  888. if (empty($options['view_newest_first']))
  889. $context['first_new_message'] = isset($context['start_from']) && $_REQUEST['start'] == $context['start_from'];
  890. else
  891. $context['first_new_message'] = isset($context['start_from']) && $_REQUEST['start'] == $topicinfo['num_replies'] - $context['start_from'];
  892. }
  893. else
  894. {
  895. $messages_request = false;
  896. $context['first_message'] = 0;
  897. $context['first_new_message'] = false;
  898. }
  899. $context['jump_to'] = array(
  900. 'label' => addslashes(un_htmlspecialchars($txt['jump_to'])),
  901. 'board_name' => htmlspecialchars(strtr(strip_tags($board_info['name']), array('&amp;' => '&'))),
  902. 'child_level' => $board_info['child_level'],
  903. );
  904. // Set the callback. (do you REALIZE how much memory all the messages would take?!?)
  905. $context['get_message'] = 'prepareDisplayContext';
  906. // Now set all the wonderful, wonderful permissions... like moderation ones...
  907. $common_permissions = array(
  908. 'can_approve' => 'approve_posts',
  909. 'can_ban' => 'manage_bans',
  910. 'can_sticky' => 'make_sticky',
  911. 'can_merge' => 'merge_any',
  912. 'can_split' => 'split_any',
  913. 'calendar_post' => 'calendar_post',
  914. 'can_mark_notify' => 'mark_any_notify',
  915. 'can_send_topic' => 'send_topic',
  916. 'can_send_pm' => 'pm_send',
  917. 'can_report_moderator' => 'report_any',
  918. 'can_moderate_forum' => 'moderate_forum',
  919. 'can_issue_warning' => 'issue_warning',
  920. 'can_restore_topic' => 'move_any',
  921. 'can_restore_msg' => 'move_any',
  922. );
  923. foreach ($common_permissions as $contextual => $perm)
  924. $context[$contextual] = allowedTo($perm);
  925. // Permissions with _any/_own versions. $context[YYY] => ZZZ_any/_own.
  926. $anyown_permissions = array(
  927. 'can_move' => 'move',
  928. 'can_lock' => 'lock',
  929. 'can_delete' => 'remove',
  930. 'can_add_poll' => 'poll_add',
  931. 'can_remove_poll' => 'poll_remove',
  932. 'can_reply' => 'post_reply',
  933. 'can_reply_unapproved' => 'post_unapproved_replies',
  934. );
  935. foreach ($anyown_permissions as $contextual => $perm)
  936. $context[$contextual] = allowedTo($perm . '_any') || ($context['user']['started'] && allowedTo($perm . '_own'));
  937. // Cleanup all the permissions with extra stuff...
  938. $context['can_mark_notify'] &= !$context['user']['is_guest'];
  939. $context['can_sticky'] &= !empty($modSettings['enableStickyTopics']);
  940. $context['calendar_post'] &= !empty($modSettings['cal_enabled']);
  941. $context['can_add_poll'] &= $modSettings['pollMode'] == '1' && $topicinfo['id_poll'] <= 0;
  942. $context['can_remove_poll'] &= $modSettings['pollMode'] == '1' && $topicinfo['id_poll'] > 0;
  943. $context['can_reply'] &= empty($topicinfo['locked']) || allowedTo('moderate_board');
  944. $context['can_reply_unapproved'] &= $modSettings['postmod_active'] && (empty($topicinfo['locked']) || allowedTo('moderate_board'));
  945. $context['can_issue_warning'] &= in_array('w', $context['admin_features']) && $modSettings['warning_settings'][0] == 1;
  946. // Handle approval flags...
  947. $context['can_reply_approved'] = $context['can_reply'];
  948. $context['can_reply'] |= $context['can_reply_unapproved'];
  949. $context['can_quote'] = $context['can_reply'] && (empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC'])));
  950. $context['can_mark_unread'] = !$user_info['is_guest'] && $settings['show_mark_read'];
  951. $context['can_send_topic'] = (!$modSettings['postmod_active'] || $topicinfo['approved']) && allowedTo('send_topic');
  952. // Start this off for quick moderation - it will be or'd for each post.
  953. $context['can_remove_post'] = allowedTo('delete_any') || (allowedTo('delete_replies') && $context['user']['started']);
  954. // Can restore topic? That's if the topic is in the recycle board and has a previous restore state.
  955. $context['can_restore_topic'] &= !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $board && !empty($topicinfo['id_previous_board']);
  956. $context['can_restore_msg'] &= !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $board && !empty($topicinfo['id_previous_topic']);
  957. // Wireless shows a "more" if you can do anything special.
  958. if (WIRELESS && WIRELESS_PROTOCOL != 'wap')
  959. {
  960. $context['wireless_more'] = $context['can_sticky'] || $context['can_lock'] || allowedTo('modify_any');
  961. $context['wireless_moderate'] = isset($_GET['moderate']) ? ';moderate' : '';
  962. }
  963. // Load up the "double post" sequencing magic.
  964. if (!empty($options['display_quick_reply']))
  965. {
  966. checkSubmitOnce('register');
  967. $context['name'] = isset($_SESSION['guest_name']) ? $_SESSION['guest_name'] : '';
  968. $context['email'] = isset($_SESSION['guest_email']) ? $_SESSION['guest_email'] : '';
  969. }
  970. }
  971. // Callback for the message display.
  972. function prepareDisplayContext($reset = false)
  973. {
  974. global $settings, $txt, $modSettings, $scripturl, $options, $user_info, $smcFunc;
  975. global $memberContext, $context, $messages_request, $topic, $attachments, $topicinfo;
  976. static $counter = null;
  977. // If the query returned false, bail.
  978. if ($messages_request == false)
  979. return false;
  980. // Remember which message this is. (ie. reply #83)
  981. if ($counter === null || $reset)
  982. $counter = empty($options['view_newest_first']) ? $context['start'] : $context['total_visible_posts'] - $context['start'];
  983. // Start from the beginning...
  984. if ($reset)
  985. return @$smcFunc['db_data_seek']($messages_request, 0);
  986. // Attempt to get the next message.
  987. $message = $smcFunc['db_fetch_assoc']($messages_request);
  988. if (!$message)
  989. {
  990. $smcFunc['db_free_result']($messages_request);
  991. return false;
  992. }
  993. // $context['icon_sources'] says where each icon should come from - here we set up the ones which will always exist!
  994. if (empty($context['icon_sources']))
  995. {
  996. $stable_icons = array('xx', 'thumbup', 'thumbdown', 'exclamation', 'question', 'lamp', 'smiley', 'angry', 'cheesy', 'grin', 'sad', 'wink', 'moved', 'recycled', 'wireless', 'clip');
  997. $context['icon_sources'] = array();
  998. foreach ($stable_icons as $icon)
  999. $context['icon_sources'][$icon] = 'images_url';
  1000. }
  1001. // Message Icon Management... check the images exist.
  1002. if (empty($modSettings['messageIconChecks_disable']))
  1003. {
  1004. // If the current icon isn't known, then we need to do something...
  1005. if (!isset($context['icon_sources'][$message['icon']]))
  1006. $context['icon_sources'][$message['icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $message['icon'] . '.gif') ? 'images_url' : 'default_images_url';
  1007. }
  1008. elseif (!isset($context['icon_sources'][$message['icon']]))
  1009. $context['icon_sources'][$message['icon']] = 'images_url';
  1010. // If you're a lazy bum, you probably didn't give a subject...
  1011. $message['subject'] = $message['subject'] != '' ? $message['subject'] : $txt['no_subject'];
  1012. // Are you allowed to remove at least a single reply?
  1013. $context['can_remove_post'] |= allowedTo('delete_own') && (empty($modSettings['edit_disable_time']) || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 >= time()) && $message['id_member'] == $user_info['id'];
  1014. // If it couldn't load, or the user was a guest.... someday may be done with a guest table.
  1015. if (!loadMemberContext($message['id_member'], true))
  1016. {
  1017. // Notice this information isn't used anywhere else....
  1018. $memberContext[$message['id_member']]['name'] = $message['poster_name'];
  1019. $memberContext[$message['id_member']]['id'] = 0;
  1020. $memberContext[$message['id_member']]['group'] = $txt['guest_title'];
  1021. $memberContext[$message['id_member']]['link'] = $message['poster_name'];
  1022. $memberContext[$message['id_member']]['email'] = $message['poster_email'];
  1023. $memberContext[$message['id_member']]['show_email'] = showEmailAddress(true, 0);
  1024. $memberContext[$message['id_member']]['is_guest'] = true;
  1025. }
  1026. else
  1027. {
  1028. $memberContext[$message['id_member']]['can_view_profile'] = allowedTo('profile_view_any') || ($message['id_member'] == $user_info['id'] && allowedTo('profile_view_own'));
  1029. $memberContext[$message['id_member']]['is_topic_starter'] = $message['id_member'] == $context['topic_starter_id'];
  1030. $memberContext[$message['id_member']]['can_see_warning'] = !isset($context['disabled_fields']['warning_status']) && $memberContext[$message['id_member']]['warning_status'] && ($context['user']['can_mod'] || (!$user_info['is_guest'] && !empty($modSettings['warning_show']) && ($modSettings['warning_show'] > 1 || $message['id_member'] == $user_info['id'])));
  1031. }
  1032. $memberContext[$message['id_member']]['ip'] = $message['poster_ip'];
  1033. // Do the censor thang.
  1034. censorText($message['body']);
  1035. censorText($message['subject']);
  1036. // Run BBC interpreter on the message.
  1037. $message['body'] = parse_bbc($message['body'], $message['smileys_enabled'], $message['id_msg']);
  1038. // Compose the memory eat- I mean message array.
  1039. $output = array(
  1040. 'attachment' => loadAttachmentContext($message['id_msg']),
  1041. 'alternate' => $counter % 2,
  1042. 'id' => $message['id_msg'],
  1043. 'href' => $scripturl . '?topic=' . $topic . '.msg' . $message['id_msg'] . '#msg' . $message['id_msg'],
  1044. 'link' => '<a href="' . $scripturl . '?topic=' . $topic . '.msg' . $message['id_msg'] . '#msg' . $message['id_msg'] . '" rel="nofollow">' . $message['subject'] . '</a>',
  1045. 'member' => &$memberContext[$message['id_member']],
  1046. 'icon' => $message['icon'],
  1047. 'icon_url' => $settings[$context['icon_sources'][$message['icon']]] . '/post/' . $message['icon'] . '.gif',
  1048. 'subject' => $message['subject'],
  1049. 'time' => timeformat($message['poster_time']),
  1050. 'timestamp' => forum_time(true, $message['poster_time']),
  1051. 'counter' => $counter,
  1052. 'modified' => array(
  1053. 'time' => timeformat($message['modified_time']),
  1054. 'timestamp' => forum_time(true, $message['modified_time']),
  1055. 'name' => $message['modified_name']
  1056. ),
  1057. 'body' => $message['body'],
  1058. 'new' => empty($message['is_read']),
  1059. 'approved' => $message['approved'],
  1060. 'first_new' => isset($context['start_from']) && $context['start_from'] == $counter,
  1061. 'is_ignored' => !empty($modSettings['enable_buddylist']) && !empty($options['posts_apply_ignore_list']) && in_array($message['id_member'], $context['user']['ignoreusers']),
  1062. 'can_approve' => !$message['approved'] && $context['can_approve'],
  1063. 'can_unapprove' => $message['approved'] && $context['can_approve'],
  1064. 'can_modify' => (!$context['is_locked'] || allowedTo('moderate_board')) && (allowedTo('modify_any') || (allowedTo('modify_replies') && $context['user']['started']) || (allowedTo('modify_own') && $message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || !$message['approved'] || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time()))),
  1065. 'can_remove' => allowedTo('delete_any') || (allowedTo('delete_replies') && $context['user']['started']) || (allowedTo('delete_own') && $message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time())),
  1066. 'can_see_ip' => allowedTo('moderate_forum') || ($message['id_member'] == $user_info['id'] && !empty($user_info['id'])),
  1067. );
  1068. // Is this user the message author?
  1069. $output['is_message_author'] = $message['id_member'] == $user_info['id'];
  1070. if (empty($options['view_newest_first']))
  1071. $counter++;
  1072. else
  1073. $counter--;
  1074. return $output;
  1075. }
  1076. // Download an attachment.
  1077. function Download()
  1078. {
  1079. global $txt, $modSettings, $user_info, $scripturl, $context, $sourcedir, $topic, $smcFunc;
  1080. // Some defaults that we need.
  1081. $context['character_set'] = empty($modSettings['global_character_set']) ? (empty($txt['lang_character_set']) ? 'ISO-8859-1' : $txt['lang_character_set']) : $modSettings['global_character_set'];
  1082. $context['utf8'] = $context['character_set'] === 'UTF-8' && (strpos(strtolower(PHP_OS), 'win') === false || @version_compare(PHP_VERSION, '4.2.3') != -1);
  1083. $context['no_last_modified'] = true;
  1084. // Make sure some attachment was requested!
  1085. if (!isset($_REQUEST['attach']) && !isset($_REQUEST['id']))
  1086. fatal_lang_error('no_access', false);
  1087. $_REQUEST['attach'] = isset($_REQUEST['attach']) ? (int) $_REQUEST['attach'] : (int) $_REQUEST['id'];
  1088. if (isset($_REQUEST['type']) && $_REQUEST['type'] == 'avatar')
  1089. {
  1090. $request = $smcFunc['db_query']('', '
  1091. SELECT id_folder, filename, file_hash, fileext, id_attach, attachment_type, mime_type, approved, id_member
  1092. FROM {db_prefix}attachments
  1093. WHERE id_attach = {int:id_attach}
  1094. AND id_member > {int:blank_id_member}
  1095. LIMIT 1',
  1096. array(
  1097. 'id_attach' => $_REQUEST['attach'],
  1098. 'blank_id_member' => 0,
  1099. )
  1100. );
  1101. $_REQUEST['image'] = true;
  1102. }
  1103. // This is just a regular attachment...
  1104. else
  1105. {
  1106. // This checks only the current board for $board/$topic's permissions.
  1107. isAllowedTo('view_attachments');
  1108. // Make sure this attachment is on this board.
  1109. // NOTE: We must verify that $topic is the attachment's topic, or else the permission check above is broken.
  1110. $request = $smcFunc['db_query']('', '
  1111. SELECT a.id_folder, a.filename, a.file_hash, a.fileext, a.id_attach, a.attachment_type, a.mime_type, a.approved, m.id_member
  1112. FROM {db_prefix}attachments AS a
  1113. INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg AND m.id_topic = {int:current_topic})
  1114. INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})
  1115. WHERE a.id_attach = {int:attach}
  1116. LIMIT 1',
  1117. array(
  1118. 'attach' => $_REQUEST['attach'],
  1119. 'current_topic' => $topic,
  1120. )
  1121. );
  1122. }
  1123. if ($smcFunc['db_num_rows']($request) == 0)
  1124. fatal_lang_error('no_access', false);
  1125. list ($id_folder, $real_filename, $file_hash, $file_ext, $id_attach, $attachment_type, $mime_type, $is_approved, $id_member) = $smcFunc['db_fetch_row']($request);
  1126. $smcFunc['db_free_result']($request);
  1127. // If it isn't yet approved, do they have permission to view it?
  1128. if (!$is_approved && ($id_member == 0 || $user_info['id'] != $id_member) && ($attachment_type == 0 || $attachment_type == 3))
  1129. isAllowedTo('approve_posts');
  1130. // Update the download counter (unless it's a thumbnail).
  1131. if ($attachment_type != 3)
  1132. $smcFunc['db_query']('attach_download_increase', '
  1133. UPDATE LOW_PRIORITY {db_prefix}attachments
  1134. SET downloads = downloads + 1
  1135. WHERE id_attach = {int:id_attach}',
  1136. array(
  1137. 'id_attach' => $id_attach,
  1138. )
  1139. );
  1140. $filename = getAttachmentFilename($real_filename, $_REQUEST['attach'], $id_folder, false, $file_hash);
  1141. // This is done to clear any output that was made before now. (would use ob_clean(), but that's PHP 4.2.0+...)
  1142. ob_end_clean();
  1143. if (!empty($modSettings['enableCompressedOutput']) && @version_compare(PHP_VERSION, '4.2.0') >= 0 && @filesize($filename) <= 4194304 && in_array($file_ext, array('txt', 'html', 'htm', 'js', 'doc', 'pdf', 'docx', 'rtf', 'css', 'php', 'log', 'xml', 'sql', 'c', 'java')))
  1144. @ob_start('ob_gzhandler');
  1145. else
  1146. {
  1147. ob_start();
  1148. header('Content-Encoding: none');
  1149. }
  1150. // No point in a nicer message, because this is supposed to be an attachment anyway...
  1151. if (!file_exists($filename))
  1152. {
  1153. loadLanguage('Errors');
  1154. header('HTTP/1.0 404 ' . $txt['attachment_not_found']);
  1155. header('Content-Type: text/plain; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
  1156. // We need to die like this *before* we send any anti-caching headers as below.
  1157. die('404 - ' . $txt['attachment_not_found']);
  1158. }
  1159. // If it hasn't been modified since the last time this attachement was retrieved, there's no need to display it again.
  1160. if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']))
  1161. {
  1162. list($modified_since) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE']);
  1163. if (strtotime($modified_since) >= filemtime($filename))
  1164. {
  1165. ob_end_clean();
  1166. // Answer the question - no, it hasn't been modified ;).
  1167. header('HTTP/1.1 304 Not Modified');
  1168. exit;
  1169. }
  1170. }
  1171. // Check whether the ETag was sent back, and cache based on that...
  1172. $eTag = '"' . substr($_REQUEST['attach'] . $real_filename . filemtime($filename), 0, 64) . '"';
  1173. if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && strpos($_SERVER['HTTP_IF_NONE_MATCH'], $eTag) !== false)
  1174. {
  1175. ob_end_clean();
  1176. header('HTTP/1.1 304 Not Modified');
  1177. exit;
  1178. }
  1179. // Send the attachment headers.
  1180. header('Pragma: ');
  1181. if (!$context['browser']['is_gecko'])
  1182. header('Content-Transfer-Encoding: binary');
  1183. header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT');
  1184. header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($filename)) . ' GMT');
  1185. header('Accept-Ranges: bytes');
  1186. header('Connection: close');
  1187. header('ETag: ' . $eTag);
  1188. // IE 6 just doesn't play nice. As dirty as this seems, it works.
  1189. if ($context['browser']['is_ie6'] && isset($_REQUEST['image']))
  1190. unset($_REQUEST['image']);
  1191. // Make sure the mime type warrants an inline display.
  1192. elseif (isset($_REQUEST['image']) && !empty($mime_type) && strpos($mime_type, 'image/') !== 0)
  1193. unset($_REQUEST['image']);
  1194. // Does this have a mime type?
  1195. elseif (!empty($mime_type) && (isset($_REQUEST['image']) || !in_array($file_ext, array('jpg', 'gif', 'jpeg', 'x-ms-bmp', 'png', 'psd', 'tiff', 'iff'))))
  1196. header('Content-Type: ' . strtr($mime_type, array('image/bmp' => 'image/x-ms-bmp')));
  1197. else
  1198. {
  1199. header('Content-Type: ' . ($context['browser']['is_ie'] || $context['browser']['is_opera'] ? 'application/octetstream' : 'application/octet-stream'));
  1200. if (isset($_REQUEST['image']))
  1201. unset($_REQUEST['image']);
  1202. }
  1203. // Convert the file to UTF-8, cuz most browsers dig that.
  1204. $utf8name = !$context['utf8'] && function_exists('iconv') ? iconv($context['character_set'], 'UTF-8', $real_filename) : (!$context['utf8'] && function_exists('mb_convert_encoding') ? mb_convert_encoding($real_filename, 'UTF-8', $context['character_set']) : $real_filename);
  1205. $fixchar = create_function('$n', '
  1206. if ($n < 32)
  1207. return \'\';
  1208. elseif ($n < 128)
  1209. return chr($n);
  1210. elseif ($n < 2048)
  1211. return chr(192 | $n >> 6) . chr(128 | $n & 63);
  1212. elseif ($n < 65536)
  1213. return chr(224 | $n >> 12) . chr(128 | $n >> 6 & 63) . chr(128 | $n & 63);
  1214. else
  1215. return chr(240 | $n >> 18) . chr(128 | $n >> 12 & 63) . chr(128 | $n >> 6 & 63) . chr(128 | $n & 63);');
  1216. $disposition = !isset($_REQUEST['image']) ? 'attachment' : 'inline';
  1217. // Different browsers like different standards...
  1218. if ($context['browser']['is_firefox'])
  1219. header('Content-Disposition: ' . $disposition . '; filename*="UTF-8\'\'' . preg_replace('~&#(\d{3,8});~e', '$fixchar(\'$1\')', $utf8name) . '"');
  1220. elseif ($context['browser']['is_opera'])
  1221. header('Content-Disposition: ' . $disposition . '; filename="' . preg_replace('~&#(\d{3,8});~e', '$fixchar(\'$1\')', $utf8name) . '"');
  1222. elseif ($context['browser']['is_ie'])
  1223. header('Content-Disposition: ' . $disposition . '; filename="' . urlencode(preg_replace('~&#(\d{3,8});~e', '$fixchar(\'$1\')', $utf8name)) . '"');
  1224. else
  1225. header('Content-Disposition: ' . $disposition . '; filename="' . $utf8name . '"');
  1226. // If this has an "image extension" - but isn't actually an image - then ensure it isn't cached cause of silly IE.
  1227. if (!isset($_REQUEST['image']) && in_array($file_ext, array('gif', 'jpg', 'bmp', 'png', 'jpeg', 'tiff')))
  1228. header('Cache-Control: no-cache');
  1229. else
  1230. header('Cache-Control: max-age=' . (525600 * 60) . ', private');
  1231. if (empty($modSettings['enableCompressedOutput']) || filesize($filename) > 4194304)
  1232. header('Content-Length: ' . filesize($filename));
  1233. // Try to buy some time...
  1234. @set_time_limit(600);
  1235. // Recode line endings for text files, if enabled.
  1236. if (!empty($modSettings['attachmentRecodeLineEndings']) && !isset($_REQUEST['image']) && in_array($file_ext, array('txt', 'css', 'htm', 'html', 'php', 'xml')))
  1237. {
  1238. if (strpos($_SERVER['HTTP_USER_AGENT'], 'Windows') !== false)
  1239. $callback = create_function('$buffer', 'return preg_replace(\'~[\r]?\n~\', "\r\n", $buffer);');
  1240. elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') !== false)
  1241. $callback = create_function('$buffer', 'return preg_replace(\'~[\r]?\n~\', "\r", $buffer);');
  1242. else
  1243. $callback = create_function('$buffer', 'return preg_replace(\'~[\r]?\n~\', "\n", $buffer);');
  1244. }
  1245. // Since we don't do output compression for files this large...
  1246. if (filesize($filename) > 4194304)
  1247. {
  1248. // Forcibly end any output buffering going on.
  1249. if (function_exists('ob_get_level'))
  1250. {
  1251. while (@ob_get_level() > 0)
  1252. @ob_end_clean();
  1253. }
  1254. else
  1255. {
  1256. @ob_end_clean();
  1257. @ob_end_clean();
  1258. @ob_end_clean();
  1259. }
  1260. $fp = fopen($filename, 'rb');
  1261. while (!feof($fp))
  1262. {
  1263. if (isset($callback))
  1264. echo $callback(fread($fp, 8192));
  1265. else
  1266. echo fread($fp, 8192);
  1267. flush();
  1268. }
  1269. fclose($fp);
  1270. }
  1271. // On some of the less-bright hosts, readfile() is disabled. It's just a faster, more byte safe, version of what's in the if.
  1272. elseif (isset($callback) || @readfile($filename) == null)
  1273. echo isset($callback) ? $callback(file_get_contents($filename)) : file_get_contents($filename);
  1274. obExit(false);
  1275. }
  1276. function loadAttachmentContext($id_msg)
  1277. {
  1278. global $attachments, $modSettings, $txt, $scripturl, $topic, $sourcedir, $smcFunc;
  1279. // Set up the attachment info - based on code by Meriadoc.
  1280. $attachmentData = array();
  1281. $have_unapproved = false;
  1282. if (isset($attachments[$id_msg]) && !empty($modSettings['attachmentEnable']))
  1283. {
  1284. foreach ($attachments[$id_msg] as $i => $attachment)
  1285. {
  1286. $attachmentData[$i] = array(
  1287. 'id' => $attachment['id_attach'],
  1288. 'name' => preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', htmlspecialchars($attachment['filename'])),
  1289. 'downloads' => $attachment['downloads'],
  1290. 'size' => round($attachment['filesize'] / 1024, 2) . ' ' . $txt['kilobyte'],
  1291. 'byte_size' => $attachment['filesize'],
  1292. 'href' => $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_attach'],
  1293. 'link' => '<a href="' . $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_attach'] . '">' . htmlspecialchars($attachment['filename']) . '</a>',
  1294. 'is_image' => !empty($attachment['width']) && !empty($attachment['height']) && !empty($modSettings['attachmentShowImages']),
  1295. 'is_approved' => $attachment['approved'],
  1296. );
  1297. // If something is unapproved we'll note it so we can sort them.
  1298. if (!$attachment['approved'])
  1299. $have_unapproved = true;
  1300. if (!$attachmentData[$i]['is_image'])
  1301. continue;
  1302. $attachmentData[$i]['real_width'] = $attachment['width'];
  1303. $attachmentData[$i]['width'] = $attachment['width'];
  1304. $attachmentData[$i]['real_height'] = $attachment['height'];
  1305. $attachmentData[$i]['height'] = $attachment['height'];
  1306. // Let's see, do we want thumbs?
  1307. if (!empty($modSettings['attachmentThumbnails']) && !empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight']) && ($attachment['width'] > $modSettings['attachmentThumbWidth'] || $attachment['height'] > $modSettings['attachmentThumbHeight']) && strlen($attachment['filename']) < 249)
  1308. {
  1309. // A proper thumb doesn't exist yet? Create one!
  1310. if (empty($attachment['id_thumb']) || $attachment['thumb_width'] > $modSettings['attachmentThumbWidth'] || $attachment['thumb_height'] > $modSettings['attachmentThumbHeight'] || ($attachment['thumb_width'] < $modSettings['attachmentThumbWidth'] && $attachment['thumb_height'] < $modSettings['attachmentThumbHeight']))
  1311. {
  1312. $filename = getAttachmentFilename($attachment['filename'], $attachment['id_attach'], $attachment['id_folder']);
  1313. require_once($sourcedir . '/Subs-Graphics.php');
  1314. if (createThumbnail($filename, $modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight']))
  1315. {
  1316. // So what folder are we putting this image in?
  1317. if (!empty($modSettings['currentAttachmentUploadDir']))
  1318. {
  1319. if (!is_array($modSettings['attachmentUploadDir']))
  1320. $modSettings['attachmentUploadDir'] = @unserialize($modSettings['attachmentUploadDir']);
  1321. $path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
  1322. $id_folder_thumb = $modSettings['currentAttachmentUploadDir'];
  1323. }
  1324. else
  1325. {
  1326. $path = $modSettings['attachmentUploadDir'];
  1327. $id_folder_thumb = 1;
  1328. }
  1329. // Calculate the size of the created thumbnail.
  1330. $size = @getimagesize($filename . '_thumb');
  1331. list ($attachment['thumb_width'], $attachment['thumb_height']) = $size;
  1332. $thumb_size = filesize($filename . '_thumb');
  1333. // These are the only valid image types for SMF.
  1334. $validImageTypes = array(1 => 'gif', 2 => 'jpeg', 3 => 'png', 5 => 'psd', 6 => 'bmp', 7 => 'tiff', 8 => 'tiff', 9 => 'jpeg', 14 => 'iff');
  1335. // What about the extension?
  1336. $thumb_ext = isset($validImageTypes[$size[2]]) ? $validImageTypes[$size[2]] : '';
  1337. // Figure out the mime type.
  1338. if (!empty($size['mime']))
  1339. $thumb_mime = $size['mime'];
  1340. else
  1341. $thumb_mime = 'image/' . $thumb_ext;
  1342. $thumb_filename = $attachment['filename'] . '_thumb';
  1343. $thumb_hash = getAttachmentFilename($thumb_filename, false, null, true);
  1344. // Add this beauty to the database.
  1345. $smcFunc['db_insert']('',
  1346. '{db_prefix}attachments',
  1347. array('id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'size' => 'int', 'width' => 'int', 'height' => 'int', 'fileext' => 'string', 'mime_type' => 'string'),
  1348. array($id_folder_thumb, $id_msg, 3, $thumb_filename, $thumb_hash, (int) $thumb_size, (int) $attachment['thumb_width'], (int) $attachment['thumb_height'], $thumb_ext, $thumb_mime),
  1349. array('id_attach')
  1350. );
  1351. $old_id_thumb = $attachment['id_thumb'];
  1352. $attachment['id_thumb'] = $smcFunc['db_insert_id']('{db_prefix}attachments', 'id_attach');
  1353. if (!empty($attachment['id_thumb']))
  1354. {
  1355. $smcFunc['db_query']('', '
  1356. UPDATE {db_prefix}attachments
  1357. SET id_thumb = {int:id_thumb}
  1358. WHERE id_attach = {int:id_attach}',
  1359. array(
  1360. 'id_thumb' => $attachment['id_thumb'],
  1361. 'id_attach' => $attachment['id_attach'],
  1362. )
  1363. );
  1364. $thumb_realname = getAttachmentFilename($thumb_filename, $attachment['id_thumb'], $id_folder_thumb, false, $thumb_hash);
  1365. rename($filename . '_thumb', $thumb_realname);
  1366. // Do we need to remove an old thumbnail?
  1367. if (!empty($old_id_thumb))
  1368. {
  1369. require_once($sourcedir . '/ManageAttachments.php');
  1370. removeAttachments(array('id_attach' => $old_id_thumb), '', false, false);
  1371. }
  1372. }
  1373. }
  1374. }
  1375. // Only adjust dimensions on successful thumbnail creation.
  1376. if (!empty($attachment['thumb_width']) && !empty($attachment['thumb_height']))
  1377. {
  1378. $attachmentData[$i]['width'] = $attachment['thumb_width'];
  1379. $attachmentData[$i]['height'] = $attachment['thumb_height'];
  1380. }
  1381. }
  1382. if (!empty($attachment['id_thumb']))
  1383. $attachmentData[$i]['thumbnail'] = array(
  1384. 'id' => $attachment['id_thumb'],
  1385. 'href' => $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_thumb'] . ';image',
  1386. );
  1387. $attachmentData[$i]['thumbnail']['has_thumb'] = !empty($attachment['id_thumb']);
  1388. // If thumbnails are disabled, check the maximum size of the image.
  1389. if (!$attachmentData[$i]['thumbnail']['has_thumb'] && ((!empty($modSettings['max_image_width']) && $attachment['width'] > $modSettings['max_image_width']) || (!empty($modSettings['max_image_height']) && $attachment['height'] > $modSettings['max_image_height'])))
  1390. {
  1391. if (!empty($modSettings['max_image_width']) && (empty($modSettings['max_image_height']) || $attachment['height'] * $modSettings['max_image_width'] / $attachment['width'] <= $modSettings['max_image_height']))
  1392. {
  1393. $attachmentData[$i]['width'] = $modSettings['max_image_width'];
  1394. $attachmentData[$i]['height'] = floor($attachment['height'] * $modSettings['max_image_width'] / $attachment['width']);
  1395. }
  1396. elseif (!empty($modSettings['max_image_width']))
  1397. {
  1398. $attachmentData[$i]['width'] = floor($attachment['width'] * $modSettings['max_image_height'] / $attachment['height']);
  1399. $attachmentData[$i]['height'] = $modSettings['max_image_height'];
  1400. }
  1401. }
  1402. elseif ($attachmentData[$i]['thumbnail']['has_thumb'])
  1403. {
  1404. // If the image is too large to show inline, make it a popup.
  1405. if (((!empty($modSettings['max_image_width']) && $attachmentData[$i]['real_width'] > $modSettings['max_image_width']) || (!empty($modSettings['max_image_height']) && $attachmentData[$i]['real_height'] > $modSettings['max_image_height'])))
  1406. $attachmentData[$i]['thumbnail']['javascript'] = 'return reqWin(\'' . $attachmentData[$i]['href'] . ';image\', ' . ($attachment['width'] + 20) . ', ' . ($attachment['height'] + 20) . ', true);';
  1407. else
  1408. $attachmentData[$i]['thumbnail']['javascript'] = 'return expandThumb(' . $attachment['id_attach'] . ');';
  1409. }
  1410. if (!$attachmentData[$i]['thumbnail']['has_thumb'])
  1411. $attachmentData[$i]['downloads']++;
  1412. }
  1413. }
  1414. // Do we need to instigate a sort?
  1415. if ($have_unapproved)
  1416. usort($attachmentData, 'approved_attach_sort');
  1417. return $attachmentData;
  1418. }
  1419. // A sort function for putting unapproved attachments first.
  1420. function approved_attach_sort($a, $b)
  1421. {
  1422. if ($a['is_approved'] == $b['is_approved'])
  1423. return 0;
  1424. return $a['is_approved'] > $b['is_approved'] ? -1 : 1;
  1425. }
  1426. // In-topic quick moderation.
  1427. function QuickInTopicModeration()
  1428. {
  1429. global $sourcedir, $topic, $board, $user_info, $smcFunc, $modSettings, $context;
  1430. // Check the session = get or post.
  1431. checkSession('request');
  1432. require_once($sourcedir . '/RemoveTopic.php');
  1433. if (empty($_REQUEST['msgs']))
  1434. redirectexit('topic=' . $topic . '.' . $_REQUEST['start']);
  1435. $messages = array();
  1436. foreach ($_REQUEST['msgs'] as $dummy)
  1437. $messages[] = (int) $dummy;
  1438. // We are restoring messages. We handle this in another place.
  1439. if (isset($_REQUEST['restore_selected']))
  1440. redirectexit('action=restoretopic;msgs=' . implode(',', $messages) . ';' . $context['session_var'] . '=' . $context['session_id']);
  1441. // Allowed to delete any message?
  1442. if (allowedTo('delete_any'))
  1443. $allowed_all = true;
  1444. // Allowed to delete replies to their messages?
  1445. elseif (allowedTo('delete_replies'))
  1446. {
  1447. $request = $smcFunc['db_query']('', '
  1448. SELECT id_member_started
  1449. FROM {db_prefix}topics
  1450. WHERE id_topic = {int:current_topic}
  1451. LIMIT 1',
  1452. array(
  1453. 'current_topic' => $topic,
  1454. )
  1455. );
  1456. list ($starter) = $smcFunc['db_fetch_row']($request);
  1457. $smcFunc['db_free_result']($request);
  1458. $allowed_all = $starter == $user_info['id'];
  1459. }
  1460. else
  1461. $allowed_all = false;
  1462. // Make sure they're allowed to delete their own messages, if not any.
  1463. if (!$allowed_all)
  1464. isAllowedTo('delete_own');
  1465. // Allowed to remove which messages?
  1466. $request = $smcFunc['db_query']('', '
  1467. SELECT id_msg, subject, id_member, poster_time
  1468. FROM {db_prefix}messages
  1469. WHERE id_msg IN ({array_int:message_list})
  1470. AND id_topic = {int:current_topic}' . (!$allowed_all ? '
  1471. AND id_member = {int:current_member}' : '') . '
  1472. LIMIT ' . count($messages),
  1473. array(
  1474. 'current_member' => $user_info['id'],
  1475. 'current_topic' => $topic,
  1476. 'message_list' => $messages,
  1477. )
  1478. );
  1479. $messages = array();
  1480. while ($row = $smcFunc['db_fetch_assoc']($request))
  1481. {
  1482. if (!$allowed_all && !empty($modSettings['edit_disable_time']) && $row['poster_time'] + $modSettings['edit_disable_time'] * 60 < time())
  1483. continue;
  1484. $messages[$row['id_msg']] = array($row['subject'], $row['id_member']);
  1485. }
  1486. $smcFunc['db_free_result']($request);
  1487. // Get the first message in the topic - because you can't delete that!
  1488. $request = $smcFunc['db_query']('', '
  1489. SELECT id_first_msg, id_last_msg
  1490. FROM {db_prefix}topics
  1491. WHERE id_topic = {int:current_topic}
  1492. LIMIT 1',
  1493. array(
  1494. 'current_topic' => $topic,
  1495. )
  1496. );
  1497. list ($first_message, $last_message) = $smcFunc['db_fetch_row']($request);
  1498. $smcFunc['db_free_result']($request);
  1499. // Delete all the messages we know they can delete. ($messages)
  1500. foreach ($messages as $message => $info)
  1501. {
  1502. // Just skip the first message - if it's not the last.
  1503. if ($message == $first_message && $message != $last_message)
  1504. continue;
  1505. // If the first message is going then don't bother going back to the topic as we're effectively deleting it.
  1506. elseif ($message == $first_message)
  1507. $topicGone = true;
  1508. removeMessage($message);
  1509. // Log this moderation action ;).
  1510. if (allowedTo('delete_any') && (!allowedTo('delete_own') || $info[1] != $user_info['id']))
  1511. logAction('delete', array('topic' => $topic, 'subject' => $info[0], 'member' => $info[1], 'board' => $board));
  1512. }
  1513. redirectexit(!empty($topicGone) ? 'board=' . $board : 'topic=' . $topic . '.' . $_REQUEST['start']);
  1514. }
  1515. ?>