question.php 16.3 KB
Newer Older
Scott committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php
/*
	Question2Answer by Gideon Greenspan and contributors
	http://www.question2answer.org/

	Description: Controller for question page (only viewing functionality here)


	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	More about this license: http://www.question2answer.org/license.php
*/

Scott committed
22
if (!defined('QA_VERSION')) { // don't allow this page to be requested directly from browser
23
	header('Location: ../../');
Scott committed
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
	exit;
}

require_once QA_INCLUDE_DIR . 'app/cookies.php';
require_once QA_INCLUDE_DIR . 'app/format.php';
require_once QA_INCLUDE_DIR . 'db/selects.php';
require_once QA_INCLUDE_DIR . 'util/sort.php';
require_once QA_INCLUDE_DIR . 'util/string.php';
require_once QA_INCLUDE_DIR . 'app/captcha.php';
require_once QA_INCLUDE_DIR . 'pages/question-view.php';
require_once QA_INCLUDE_DIR . 'app/updates.php';

$questionid = qa_request_part(0);
$userid = qa_get_logged_in_userid();
$cookieid = qa_cookie_get();
$pagestate = qa_get_state();
Scott committed
40 41


Scott committed
42
// Get information about this question
Scott committed
43

44
$cacheDriver = \Q2A\Storage\CacheFactory::getCacheDriver();
45
$cacheKey = "question:$questionid";
Scott committed
46
$useCache = $userid === null && $cacheDriver->isEnabled() && !qa_is_http_post() && empty($pagestate);
Scott committed
47 48 49
$saveCache = false;

if ($useCache) {
Scott committed
50
	$questionData = $cacheDriver->get($cacheKey);
Scott committed
51 52 53 54 55 56 57 58 59 60 61 62 63 64
}

if (!isset($questionData)) {
	$questionData = qa_db_select_with_pending(
		qa_db_full_post_selectspec($userid, $questionid),
		qa_db_full_child_posts_selectspec($userid, $questionid),
		qa_db_full_a_child_posts_selectspec($userid, $questionid),
		qa_db_post_parent_q_selectspec($questionid),
		qa_db_post_close_post_selectspec($questionid),
		qa_db_post_duplicates_selectspec($questionid),
		qa_db_post_meta_selectspec($questionid, 'qa_q_extra'),
		qa_db_category_nav_selectspec($questionid, true, true, true),
		isset($userid) ? qa_db_is_favorite_selectspec($userid, QA_ENTITY_QUESTION, $questionid) : null
	);
Scott committed
65

Scott committed
66 67 68
	// whether to save the cache (actioned below, after basic checks)
	$saveCache = $useCache;
}
Scott committed
69

Scott committed
70
list($question, $childposts, $achildposts, $parentquestion, $closepost, $duplicateposts, $extravalue, $categories, $favorite) = $questionData;
Scott committed
71

Scott committed
72

Scott committed
73 74
if ($question['basetype'] != 'Q') // don't allow direct viewing of other types of post
	$question = null;
Scott committed
75

Scott committed
76 77
if (isset($question)) {
	$q_request = qa_q_request($questionid, $question['title']);
Amiya committed
78

Scott committed
79 80 81 82
	if (trim($q_request, '/') !== trim(qa_request(), '/')) {
		// redirect if the current URL is incorrect
		qa_redirect($q_request);
	}
Amiya committed
83

Scott committed
84
	$question['extra'] = $extravalue;
Scott committed
85

Scott committed
86 87
	$answers = qa_page_q_load_as($question, $childposts);
	$commentsfollows = qa_page_q_load_c_follows($question, $childposts, $achildposts, $duplicateposts);
Scott committed
88

Scott committed
89
	$question = $question + qa_page_q_post_rules($question, null, null, $childposts + $duplicateposts); // array union
Scott committed
90

Scott committed
91 92
	if ($question['selchildid'] && (@$answers[$question['selchildid']]['type'] != 'A'))
		$question['selchildid'] = null; // if selected answer is hidden or somehow not there, consider it not selected
Scott committed
93

Scott committed
94 95 96 97
	foreach ($answers as $key => $answer) {
		$answers[$key] = $answer + qa_page_q_post_rules($answer, $question, $answers, $achildposts);
		$answers[$key]['isselected'] = ($answer['postid'] == $question['selchildid']);
	}
Scott committed
98

Scott committed
99 100 101
	foreach ($commentsfollows as $key => $commentfollow) {
		$parent = ($commentfollow['parentid'] == $questionid) ? $question : @$answers[$commentfollow['parentid']];
		$commentsfollows[$key] = $commentfollow + qa_page_q_post_rules($commentfollow, $parent, $commentsfollows, null);
Scott committed
102
	}
Scott committed
103
}
Scott committed
104

Scott committed
105
// Deal with question not found or not viewable, otherwise report the view event
Scott committed
106

Scott committed
107 108
if (!isset($question))
	return include QA_INCLUDE_DIR . 'qa-page-not-found.php';
Scott committed
109

Scott committed
110 111
if (!$question['viewable']) {
	$qa_content = qa_content_prepare();
Scott committed
112

Scott committed
113 114 115 116 117 118 119 120
	if ($question['queued'])
		$qa_content['error'] = qa_lang_html('question/q_waiting_approval');
	elseif ($question['flagcount'] && !isset($question['lastuserid']))
		$qa_content['error'] = qa_lang_html('question/q_hidden_flagged');
	elseif ($question['authorlast'])
		$qa_content['error'] = qa_lang_html('question/q_hidden_author');
	else
		$qa_content['error'] = qa_lang_html('question/q_hidden_other');
Scott committed
121

Scott committed
122
	$qa_content['suggest_next'] = qa_html_suggest_qs_tags(qa_using_tags());
Scott committed
123

Scott committed
124 125
	return $qa_content;
}
Scott committed
126

Scott committed
127
$permiterror = qa_user_post_permit_error('permit_view_q_page', $question, null, false);
Scott committed
128

Scott committed
129 130 131
if ($permiterror && (qa_is_human_probably() || !qa_opt('allow_view_q_bots'))) {
	$qa_content = qa_content_prepare();
	$topage = qa_q_request($questionid, $question['title']);
Scott committed
132

Scott committed
133 134 135 136
	switch ($permiterror) {
		case 'login':
			$qa_content['error'] = qa_insert_login_links(qa_lang_html('main/view_q_must_login'), $topage);
			break;
Scott committed
137

Scott committed
138 139 140
		case 'confirm':
			$qa_content['error'] = qa_insert_login_links(qa_lang_html('main/view_q_must_confirm'), $topage);
			break;
Scott committed
141

Scott committed
142
		case 'approve':
143 144 145 146
			$qa_content['error'] = strtr(qa_lang_html('main/view_q_must_be_approved'), array(
				'^1' => '<a href="' . qa_path_html('account') . '">',
				'^2' => '</a>',
			));
Scott committed
147
			break;
Scott committed
148

Scott committed
149 150 151
		default:
			$qa_content['error'] = qa_lang_html('users/no_permission');
			break;
Scott committed
152 153
	}

Scott committed
154 155 156
	return $qa_content;
}

Scott committed
157

Scott committed
158
// Save question data to cache (if older than configured limit)
Scott committed
159

Scott committed
160 161 162
if ($saveCache) {
	$questionAge = qa_opt('db_time') - $question['created'];
	if ($questionAge > 86400 * qa_opt('caching_q_start')) {
Scott committed
163
		$cacheDriver->set($cacheKey, $questionData, qa_opt('caching_q_time'));
Scott committed
164
	}
Scott committed
165
}
Scott committed
166 167


Scott committed
168
// Determine if captchas will be required
Scott committed
169

Scott committed
170 171
$captchareason = qa_user_captcha_reason(qa_user_level_for_post($question));
$usecaptcha = ($captchareason != false);
Scott committed
172 173


Scott committed
174 175
// If we're responding to an HTTP POST, include file that handles all posting/editing/etc... logic
// This is in a separate file because it's a *lot* of logic, and will slow down ordinary page views
Scott committed
176

Scott committed
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
$pagestart = qa_get_start();
$showid = qa_get('show');
$pageerror = null;
$formtype = null;
$formpostid = null;
$jumptoanchor = null;
$commentsall = null;

if (substr($pagestate, 0, 13) == 'showcomments-') {
	$commentsall = substr($pagestate, 13);
	$pagestate = null;

} elseif (isset($showid)) {
	foreach ($commentsfollows as $comment) {
		if ($comment['postid'] == $showid) {
			$commentsall = $comment['parentid'];
			break;
		}
Scott committed
195
	}
Scott committed
196
}
Scott committed
197

Scott committed
198 199
if (qa_is_http_post() || strlen($pagestate))
	require QA_INCLUDE_DIR . 'pages/question-post.php';
Scott committed
200

Scott committed
201
$formrequested = isset($formtype);
Scott committed
202

Scott committed
203
if (!$formrequested && $question['answerbutton']) {
Scott committed
204
	$immedoption = qa_opt('show_a_form_immediate');
Scott committed
205

Scott committed
206
	if ($immedoption == 'always' || ($immedoption == 'if_no_as' && !$question['isbyuser'] && !$question['acount']))
Scott committed
207 208
		$formtype = 'a_add'; // show answer form by default
}
Scott committed
209 210


Scott committed
211
// Get information on the users referenced
Scott committed
212

Scott committed
213
$usershtml = qa_userids_handles_html(array_merge(array($question), $answers, $commentsfollows), true);
Scott committed
214 215


Scott committed
216
// Prepare content for theme
Scott committed
217

Scott committed
218
$qa_content = qa_content_prepare(true, array_keys(qa_category_path($categories, $question['categoryid'])));
Scott committed
219

Scott committed
220 221 222
if (isset($userid) && !$formrequested)
	$qa_content['favorite'] = qa_favorite_form(QA_ENTITY_QUESTION, $questionid, $favorite,
		qa_lang($favorite ? 'question/remove_q_favorites' : 'question/add_q_favorites'));
Scott committed
223

Scott committed
224 225
if (isset($pageerror))
	$qa_content['error'] = $pageerror; // might also show voting error set in qa-index.php
Scott committed
226

Scott committed
227 228
elseif ($question['queued'])
	$qa_content['error'] = $question['isbyuser'] ? qa_lang_html('question/q_your_waiting_approval') : qa_lang_html('question/q_waiting_your_approval');
Scott committed
229

Scott committed
230 231
if ($question['hidden'])
	$qa_content['hidden'] = true;
Scott committed
232

Scott committed
233
qa_sort_by($commentsfollows, 'created');
Scott committed
234 235


Scott committed
236
// Prepare content for the question...
Scott committed
237

Scott committed
238 239 240 241 242
if ($formtype == 'q_edit') { // ...in edit mode
	$qa_content['title'] = qa_lang_html($question['editable'] ? 'question/edit_q_title' :
		(qa_using_categories() ? 'question/recat_q_title' : 'question/retag_q_title'));
	$qa_content['form_q_edit'] = qa_page_q_edit_q_form($qa_content, $question, @$qin, @$qerrors, $completetags, $categories);
	$qa_content['q_view']['raw'] = $question;
Scott committed
243

Scott committed
244 245
} else { // ...in view mode
	$qa_content['q_view'] = qa_page_q_question_view($question, $parentquestion, $closepost, $usershtml, $formrequested);
Scott committed
246

Scott committed
247
	$qa_content['title'] = $qa_content['q_view']['title'];
Scott committed
248

Scott committed
249
	$qa_content['description'] = qa_html(qa_shorten_string_line(qa_viewer_text($question['content'], $question['format']), 150));
Scott committed
250

Scott committed
251
	$categorykeyword = @$categories[$question['categoryid']]['title'];
Scott committed
252

Scott committed
253 254 255 256 257
	$qa_content['keywords'] = qa_html(implode(',', array_merge(
		(qa_using_categories() && strlen($categorykeyword)) ? array($categorykeyword) : array(),
		qa_tagstring_to_tags($question['tags'])
	))); // as far as I know, META keywords have zero effect on search rankings or listings, but many people have asked for this
}
Scott committed
258

Scott committed
259 260 261
$microdata = qa_opt('use_microdata');
if ($microdata) {
	$qa_content['head_lines'][] = '<meta itemprop="name" content="' . qa_html($qa_content['q_view']['raw']['title']) . '">';
Scott committed
262 263
	$qa_content['html_tags'] .= ' itemscope itemtype="https://schema.org/QAPage"';
	$qa_content['wrapper_tags'] = ' itemprop="mainEntity" itemscope itemtype="https://schema.org/Question"';
Scott committed
264
}
Scott committed
265

Scott committed
266

Scott committed
267
// Prepare content for an answer being edited (if any) or to be added
Scott committed
268

Scott committed
269 270 271
if ($formtype == 'a_edit') {
	$qa_content['a_form'] = qa_page_q_edit_a_form($qa_content, 'a' . $formpostid, $answers[$formpostid],
		$question, $answers, $commentsfollows, @$aeditin[$formpostid], @$aediterrors[$formpostid]);
Scott committed
272

Scott committed
273 274
	$qa_content['a_form']['c_list'] = qa_page_q_comment_follow_list($question, $answers[$formpostid],
		$commentsfollows, true, $usershtml, $formrequested, $formpostid);
Scott committed
275

Scott committed
276
	$jumptoanchor = 'a' . $formpostid;
Scott committed
277

Scott committed
278
} elseif ($formtype == 'a_add' || ($question['answerbutton'] && !$formrequested)) {
Scott committed
279
	$qa_content['a_form'] = qa_page_q_add_a_form($qa_content, 'anew', $captchareason, $question, @$anewin, @$anewerrors, $formtype == 'a_add', $formrequested);
Scott committed
280

Scott committed
281 282 283 284 285 286
	if ($formrequested) {
		$jumptoanchor = 'anew';
	} elseif ($formtype == 'a_add') {
		$qa_content['script_onloads'][] = array(
			"qa_element_revealed=document.getElementById('anew');"
		);
Scott committed
287
	}
Scott committed
288
}
Scott committed
289 290


Scott committed
291
// Prepare content for comments on the question, plus add or edit comment forms
Scott committed
292

Scott committed
293 294 295
if ($formtype == 'q_close') {
	$qa_content['q_view']['c_form'] = qa_page_q_close_q_form($qa_content, $question, 'close', @$closein, @$closeerrors);
	$jumptoanchor = 'close';
Scott committed
296

Scott committed
297
} elseif (($formtype == 'c_add' && $formpostid == $questionid) || ($question['commentbutton'] && !$formrequested)) { // ...to be added
Scott committed
298 299
	$qa_content['q_view']['c_form'] = qa_page_q_add_c_form($qa_content, $question, $question, 'c' . $questionid,
		$captchareason, @$cnewin[$questionid], @$cnewerrors[$questionid], $formtype == 'c_add');
Scott committed
300

Scott committed
301
	if ($formtype == 'c_add' && $formpostid == $questionid) {
Scott committed
302 303 304
		$jumptoanchor = 'c' . $questionid;
		$commentsall = $questionid;
	}
Scott committed
305

Scott committed
306
} elseif ($formtype == 'c_edit' && @$commentsfollows[$formpostid]['parentid'] == $questionid) { // ...being edited
Scott committed
307 308
	$qa_content['q_view']['c_form'] = qa_page_q_edit_c_form($qa_content, 'c' . $formpostid, $commentsfollows[$formpostid],
		@$ceditin[$formpostid], @$cediterrors[$formpostid]);
Scott committed
309

Scott committed
310 311 312
	$jumptoanchor = 'c' . $formpostid;
	$commentsall = $questionid;
}
Scott committed
313

Scott committed
314 315
$qa_content['q_view']['c_list'] = qa_page_q_comment_follow_list($question, $question, $commentsfollows,
	$commentsall == $questionid, $usershtml, $formrequested, $formpostid); // ...for viewing
Scott committed
316 317


Scott committed
318
// Prepare content for existing answers (could be added to by Ajax)
Scott committed
319

Scott committed
320 321 322 323
$qa_content['a_list'] = array(
	'tags' => 'id="a_list"',
	'as' => array(),
);
Scott committed
324

Scott committed
325
// sort according to the site preferences
Scott committed
326

Scott committed
327 328 329
if (qa_opt('sort_answers_by') == 'votes') {
	foreach ($answers as $answerid => $answer)
		$answers[$answerid]['sortvotes'] = $answer['downvotes'] - $answer['upvotes'];
Scott committed
330

Scott committed
331
	qa_sort_by($answers, 'sortvotes', 'created');
Scott committed
332

Scott committed
333 334 335
} else {
	qa_sort_by($answers, 'created');
}
Scott committed
336

Scott committed
337
// further changes to ordering to deal with queued, hidden and selected answers
Scott committed
338

339
$countfortitle = (int) $question['acount'];
Scott committed
340 341
$nextposition = 10000;
$answerposition = array();
Scott committed
342

Scott committed
343 344 345
foreach ($answers as $answerid => $answer) {
	if ($answer['viewable']) {
		$position = $nextposition++;
Scott committed
346

Scott committed
347 348
		if ($answer['hidden'])
			$position += 10000;
Scott committed
349

Scott committed
350 351 352
		elseif ($answer['queued']) {
			$position -= 10000;
			$countfortitle++; // include these in displayed count
Scott committed
353

Scott committed
354 355
		} elseif ($answer['isselected'] && qa_opt('show_selected_first'))
			$position -= 5000;
Scott committed
356

Scott committed
357 358 359
		$answerposition[$answerid] = $position;
	}
}
Scott committed
360

Scott committed
361
asort($answerposition, SORT_NUMERIC);
Scott committed
362

Scott committed
363
// extract IDs and prepare for pagination
Scott committed
364

Scott committed
365 366 367
$answerids = array_keys($answerposition);
$countforpages = count($answerids);
$pagesize = qa_opt('page_size_q_as');
Scott committed
368

Scott committed
369
// see if we need to display a particular answer
Scott committed
370

Scott committed
371 372 373
if (isset($showid)) {
	if (isset($commentsfollows[$showid]))
		$showid = $commentsfollows[$showid]['parentid'];
Scott committed
374

Scott committed
375
	$position = array_search($showid, $answerids);
Scott committed
376

Scott committed
377 378 379
	if (is_numeric($position))
		$pagestart = floor($position / $pagesize) * $pagesize;
}
Scott committed
380

Scott committed
381
// set the canonical url based on possible pagination
Scott committed
382

Scott committed
383 384
$qa_content['canonical'] = qa_path_html(qa_q_request($question['postid'], $question['title']),
	($pagestart > 0) ? array('start' => $pagestart) : null, qa_opt('site_url'));
Scott committed
385

Scott committed
386
// build the actual answer list
Scott committed
387

Scott committed
388
$answerids = array_slice($answerids, $pagestart, $pagesize);
Scott committed
389

Scott committed
390 391
foreach ($answerids as $answerid) {
	$answer = $answers[$answerid];
Scott committed
392

Scott committed
393 394
	if (!($formtype == 'a_edit' && $formpostid == $answerid)) {
		$a_view = qa_page_q_answer_view($question, $answer, $answer['isselected'], $usershtml, $formrequested);
Scott committed
395

Scott committed
396
		// Prepare content for comments on this answer, plus add or edit comment forms
Scott committed
397

Scott committed
398 399 400
		if (($formtype == 'c_add' && $formpostid == $answerid) || ($answer['commentbutton'] && !$formrequested)) { // ...to be added
			$a_view['c_form'] = qa_page_q_add_c_form($qa_content, $question, $answer, 'c' . $answerid,
				$captchareason, @$cnewin[$answerid], @$cnewerrors[$answerid], $formtype == 'c_add');
Scott committed
401

Scott committed
402 403 404 405
			if ($formtype == 'c_add' && $formpostid == $answerid) {
				$jumptoanchor = 'c' . $answerid;
				$commentsall = $answerid;
			}
Scott committed
406

Scott committed
407 408 409
		} elseif ($formtype == 'c_edit' && @$commentsfollows[$formpostid]['parentid'] == $answerid) { // ...being edited
			$a_view['c_form'] = qa_page_q_edit_c_form($qa_content, 'c' . $formpostid, $commentsfollows[$formpostid],
				@$ceditin[$formpostid], @$cediterrors[$formpostid]);
Scott committed
410

Scott committed
411 412 413
			$jumptoanchor = 'c' . $formpostid;
			$commentsall = $answerid;
		}
Scott committed
414

Scott committed
415 416
		$a_view['c_list'] = qa_page_q_comment_follow_list($question, $answer, $commentsfollows,
			$commentsall == $answerid, $usershtml, $formrequested, $formpostid); // ...for viewing
Scott committed
417

Scott committed
418
		// Add the answer to the list
Scott committed
419

Scott committed
420
		$qa_content['a_list']['as'][] = $a_view;
Scott committed
421
	}
Scott committed
422
}
Scott committed
423

Scott committed
424 425
if ($question['basetype'] == 'Q') {
	$qa_content['a_list']['title_tags'] = 'id="a_list_title"';
Scott committed
426

427 428 429
	$split = $countfortitle == 1
		? qa_lang_html_sub_split('question/1_answer_title', '1', '1')
		: qa_lang_html_sub_split('question/x_answers_title', $countfortitle);
Scott committed
430

431 432 433 434 435 436 437
	if ($microdata) {
		$split['data'] = '<span itemprop="answerCount">' . $split['data'] . '</span>';
	}

	$qa_content['a_list']['title'] = $split['prefix'] . $split['data'] . $split['suffix'];

	if ($countfortitle == 0) {
Scott committed
438
		$qa_content['a_list']['title_tags'] .= ' style="display:none;" ';
439
	}
Scott committed
440
}
Scott committed
441

Scott committed
442
if (!$formrequested) {
Scott committed
443
	$qa_content['page_links'] = qa_html_page_links(qa_request(), $pagestart, $pagesize, $countforpages, qa_opt('pages_prev_next'), array(), false, 'a_list_title');
Scott committed
444
}
Scott committed
445 446


Scott committed
447
// Some generally useful stuff
Scott committed
448

Scott committed
449
if (qa_using_categories() && count($categories)) {
Scott committed
450
	$qa_content['navigation']['cat'] = qa_category_navigation($categories, $question['categoryid']);
Scott committed
451
}
Scott committed
452

Scott committed
453
if (isset($jumptoanchor)) {
Scott committed
454 455 456
	$qa_content['script_onloads'][] = array(
		'qa_scroll_page_to($("#"+' . qa_js($jumptoanchor) . ').offset().top);'
	);
Scott committed
457
}
Scott committed
458 459


Scott committed
460
// Determine whether this request should be counted for page view statistics.
461
// The lastviewip check is now part of the hotness query in order to bypass caching.
Scott committed
462

463
if (qa_opt('do_count_q_views') && !$formrequested && !qa_is_http_post() && qa_is_human_probably() &&
464 465 466 467 468
	(!$question['views'] || (
		// if it has more than zero views, then it must be different IP & user & cookieid from the creator
		(@inet_ntop($question['createip']) != qa_remote_ip_address() || !isset($question['createip'])) &&
		($question['userid'] != $userid || !isset($question['userid'])) &&
		($question['cookieid'] != $cookieid || !isset($question['cookieid']))
469 470
	))
) {
Scott committed
471
	$qa_content['inc_views_postid'] = $questionid;
472
}
Scott committed
473 474 475


return $qa_content;