esoTalk /controllers/ETConversationsController.class.php

Language PHP Lines 364
MD5 Hash 71a70e5eecc586ca19b1fe7f4ff95c9b
Repository https://github.com/Ramir1/esoTalk.git View Raw File
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
<?php
// Copyright 2011 Toby Zerner, Simon Zerner
// This file is part of esoTalk. Please see the included license file for usage information.

if (!defined("IN_ESOTALK")) exit;

/**
 * The conversations controller displays a list of conversations, and allows filtering by channels
 * and gambits. It also handles marking all conversations as read, and has a method which provides
 * auto-refresh results for the conversations view.
 *
 * @package esoTalk
 */
class ETConversationsController extends ETController {


/**
 * Display a list of conversations, optionally filtered by channel(s) and a search string.
 *
 * @return void
 */
function index($channelSlug = false)
{
	// Add the default gambits to the gambit cloud: gambit text => css class to apply.
	$gambits = array(
		T("gambit.active last ? hours") => "gambit-activeLastHours",
		T("gambit.active last ? days") => "gambit-activeLastDays",
		T("gambit.active today") => "gambit-activeToday",
		T("gambit.author:").T("gambit.member") => "gambit-author",
		T("gambit.contributor:").T("gambit.member") => "gambit-contributor",
		T("gambit.dead") => "gambit-dead",
		T("gambit.has replies") => "gambit-hasReplies",
		T("gambit.has >10 replies") => "gambit-replies",
		T("gambit.locked") => "gambit-locked",
		T("gambit.more results") => "gambit-more",
		T("gambit.order by newest") => "gambit-orderByNewest",
		T("gambit.order by replies") => "gambit-orderByReplies",
		T("gambit.random") => "gambit-random",
		T("gambit.reverse") => "gambit-reverse",
		T("gambit.sticky") => "gambit-sticky",
	);

	// Add some more personal gambits if there is a user logged in.
	if (ET::$session->user) {
		$gambits += array(
			T("gambit.contributor:").T("gambit.myself") => "gambit-contributorMyself",
			T("gambit.author:").T("gambit.myself") => "gambit-authorMyself",
			T("gambit.draft") => "gambit-draft",
			T("gambit.muted") => "gambit-muted",
			T("gambit.private") => "gambit-private",
			T("gambit.starred") => "gambit-starred",
			T("gambit.unread") => "gambit-unread"
		);
	}

	list($channelInfo, $currentChannels, $channelIds, $includeDescendants) = $this->getSelectedChannels($channelSlug);

	// Now we need to construct some arrays to determine which channel "tabs" to show in the view.
	// $channels is a list of channels with the same parent as the current selected channel(s).
	// $path is a breadcrumb trail to the depth of the currently selected channel(s).
	$channels = array();
	$path = array();

	// Work out what channel we will use as the "parent" channel. This will be the last item in $path,
	// and its children will be in $channels.
	$curChannel = false;

	// If channels have been selected, use the first of them.
	if (count($currentChannels)) $curChannel = $channelInfo[$currentChannels[0]];

	// If the currently selected channel has no children, or if we're not including descendants, use
	// its parent as the parent channel.
	if (($curChannel and $curChannel["lft"] >= $curChannel["rgt"] - 1) or !$includeDescendants)
		$curChannel = @$channelInfo[$curChannel["parentId"]];

	// If no channel is selected, make a faux parent channel.
	if (!$curChannel) $curChannel = array("lft" => 0, "rgt" => PHP_INT_MAX, "depth" => -1);

	// Now, finally, go through all the channels and add ancestors of the "parent" channel to the $path,
	// and direct children to the list of $channels. Make sure we don't include any channels which
	// the user has unsubscribed to.
	foreach ($channelInfo as $channel) {
		if ($channel["lft"] > $curChannel["lft"] and $channel["rgt"] < $curChannel["rgt"] and $channel["depth"] == $curChannel["depth"] + 1 and empty($channel["unsubscribed"]))
			$channels[] = $channel;
		elseif ($channel["lft"] <= $curChannel["lft"] and $channel["rgt"] >= $curChannel["rgt"])
			$path[] = $channel;
	}

	// Store the currently selected channel in the session, so that it can be automatically selected
	// if "New conversation" is clicked.
	if (!empty($currentChannels)) ET::$session->store("searchChannelId", $currentChannels[0]);

	// Get the search string request value.
	$searchString = R("search");

	// Last, but definitely not least... perform the search!
	$search = ET::searchModel();
	$conversationIDs = $search->getConversationIDs($channelIds, $searchString, count($currentChannels));
	$results = $search->getResults($conversationIDs);

	// Were there any errors? Show them as messages.
	if ($search->errorCount()) {
		$this->messages($search->errors(), "warning dismissable");
	}

	// Add fulltext keywords to be highlighted. Make sure we keep ones "in quotes" together.
	else {
		$words = array();
		foreach ($search->fulltext as $term) {
			if (preg_match_all('/"(.+?)"/', $term, $matches)) {
				$words[] = $matches[1];
				$term = preg_replace('/".+?"/', '', $term);
			}
			$words = array_unique(array_merge($words, explode(" ", $term)));
		}
		ET::$session->store("highlight", $words);
	}

	// Pass on a bunch of data to the view.
	$this->data("results", $results);
	$this->data("showViewMoreLink", $search->areMoreResults());
	$this->data("channelPath", $path);
	$this->data("channelTabs", $channels);
	$this->data("currentChannels", $currentChannels);
	$this->data("channelInfo", $channelInfo);
	$this->data("channelSlug", $channelSlug ? $channelSlug : "all");
	$this->data("searchString", $searchString);
	$this->data("fulltextString", implode(" ", $search->fulltext));
	$this->data("gambits", $gambits);

	// Construct a canonical URL and add to the breadcrumb stack.
	$slugs = array();
	foreach ($currentChannels as $channel) $slugs[] = $channelInfo[$channel]["slug"];
	$url = "conversations/".urlencode(($k = implode(" ", $slugs)) ? $k : "all").($searchString ? "?search=".urlencode($searchString) : "");
	$this->pushNavigation("conversations", "search", URL($url));
	$this->canonicalURL = URL($url, true);

	// If we're loading the page in full...
	if ($this->responseType === RESPONSE_TYPE_DEFAULT) {

		// Update the user's last action.
		ET::memberModel()->updateLastAction("search");

		// Add a link to the RSS feed in the bar.
		// $this->addToMenu("meta", "feed", "<a href='".URL(str_replace("conversations/", "conversations/index.atom/", $url))."' id='feed'>".T("Feed")."</a>");

		// Construct a list of keywords to use in the meta tags.
		$keywords = array();
		foreach ($channelInfo as $c) {
			if ($c["depth"] == 0) $keywords[] = strtolower($c["title"]);
		}

		// Add meta tags to the header.
		$this->addToHead("<meta name='keywords' content='".sanitizeHTML(($k = C("esoTalk.meta.keywords")) ? $k : implode(",", $keywords))."'>");
		list($lastKeyword) = array_splice($keywords, count($keywords) - 1, 1);
		$this->addToHead("<meta name='description' content='".sanitizeHTML(($d = C("esoTalk.meta.description")) ? $d
			: sprintf(T("forumDescription"), C("esoTalk.forumTitle"), implode(", ", $keywords), $lastKeyword))."'>");

		// If this is not technically the homepage (if it's a search page) the we don't want it to be indexed.
		if ($searchString) $this->addToHead("<meta name='robots' content='noindex, noarchive'>");

		// Add JavaScript language definitions and variables.
		$this->addJSLanguage("Starred", "Unstarred", "gambit.member", "gambit.more results", "Filter conversations", "Jump to last");
		$this->addJSVar("searchUpdateInterval", C("esoTalk.search.updateInterval"));
		$this->addJSVar("currentSearch", $searchString);
		$this->addJSVar("currentChannels", $currentChannels);
		$this->addJSFile("js/lib/jquery.cookie.js");
		$this->addJSFile("js/autocomplete.js");
		$this->addJSFile("js/search.js");

		// Add an array of channels in the form slug => id for the JavaScript to use.
		$channels = array();
		foreach ($channelInfo as $id => $c) $channels[$id] = $c["slug"];
		$this->addJSVar("channels", $channels);

		// Get a bunch of statistics...
		$queries = array(
			"post" => ET::SQL()->select("COUNT(*)")->from("post")->get(),
			"conversation" => ET::SQL()->select("COUNT(*)")->from("conversation")->get(),
			"member" => ET::SQL()->select("COUNT(*)")->from("member")->get()
		);
		$sql = ET::SQL();
		foreach ($queries as $k => $query) $sql->select("($query) AS $k");
		$stats = $sql->exec()->firstRow();

		// ...and show them in the footer.
		foreach ($stats as $k => $v) {
			$stat = Ts("statistic.$k", "statistic.$k.plural", number_format($v));
			if ($k == "member" and (C("esoTalk.members.visibleToGuests") or ET::$session->user)) $stat = "<a href='".URL("members")."'>$stat</a>";
			$this->addToMenu("statistics", "statistic-$k", $stat, array("before" => "statistic-online"));
		}

		$this->render("conversations/index");

	}

	// For a view, just render the results.
	elseif ($this->responseType === RESPONSE_TYPE_VIEW) {
		$this->render("conversations/results");
	}

	// For ajax, render the results, and also pass along the channels view.
	elseif ($this->responseType === RESPONSE_TYPE_AJAX) {
		$this->json("channels", $this->getViewContents("channels/tabs", $this->data));
		$this->render("conversations/results");
	}

	// For json, output the results as a json object.
	elseif ($this->responseType === RESPONSE_TYPE_JSON) {
		$this->json("results", $results);
		$this->render();
	}
}


/**
 * Given the channel slug from a request, work out which channels are selected, whether or not to include
 * descendant channels in the results, and construct a full list of channel IDs to consider when getting the
 * list a conversations.
 *
 * @param string $channelSlug The channel slug from the request.
 * @return array An array containing:
 * 		0 => a full list of channel information.
 * 		1 => the list of currently selected channel IDs.
 * 		2 => the full list of channel IDs to consider (including descendant channels of selected channels.)
 * 		3 => whether or not descendant channels are being included.
 */
protected function getSelectedChannels($channelSlug = "")
{
	// Get a list of all viewable channels.
	$channelInfo = ET::channelModel()->get();

	// Get a list of the currently selected channels.
	$currentChannels = array();
	$includeDescendants = true;

	if (!empty($channelSlug)) {
		$channels = explode(" ", $channelSlug);

		// If the first channel is empty (ie. the URL is conversations/+channel-slug), set a flag
		// to turn off the inclusion of descendant channels when considering conversations.
		if ($channels[0] == "") {
			$includeDescendants = false;
			array_shift($channels);
		}

		// Go through the channels and add their IDs to the list of current channels.
		foreach ($channels as $channel) {
			foreach ($channelInfo as $id => $c) {
				if ($c["slug"] == $channel) {
					$currentChannels[] = $id;
					break;
				}
			}
		}
	}

	// Get an array of channel IDs to consider when getting the list of conversations.
	// If we're not including descendants, this is the same as the list of current channels.
	if (!$includeDescendants) {
		$channelIds = $currentChannels;
	}

	// Otherwise, loop through all the channels and add IDs of descendants. Make sure we don't include
	// any channels which the user has unsubscribed to.
	else {
		$channelIds = array();
		foreach ($currentChannels as $id) {
			$channelIds[] = $id;
			$rootUnsubscribed = !empty($channelInfo[$id]["unsubscribed"]);
			foreach ($channelInfo as $channel) {
				if ($channel["lft"] > $channelInfo[$id]["lft"] and $channel["rgt"] < $channelInfo[$id]["rgt"] and (empty($channel["unsubscribed"]) or $rootUnsubscribed))
					$channelIds[] = $channel["channelId"];
			}
		}
	}

	// If by now we don't have any channel IDs, we must be viewing "all channels." In this case,
	// add all the channels.
	if (empty($channelIds)) {
		foreach ($channelInfo as $id => $channel) {
			if (empty($channel["unsubscribed"])) $channelIds[] = $id;
		}
	}

	return array($channelInfo, $currentChannels, $channelIds, $includeDescendants);
}


/**
 * Mark all conversations as read and return to the index page.
 *
 * @return void
 */
public function markAllAsRead()
{
	// Update the user's preferences.
	ET::$session->setPreferences(array("markedAllConversationsAsRead" => time()));

	// For a normal response, redirect to the conversations page.
	if ($this->responseType === RESPONSE_TYPE_DEFAULT) $this->redirect(URL("conversations"));

	// For an ajax response, just pretend this is a normal search response.
	$this->index();
}


/**
 * Return updated HTML for each row in the conversations table, and indicate if there are new results for the
 * specified channel and search query.
 *
 * @param string $channelSlug The channel slug.
 * @param string $query The search query.
 * @return void
 */
public function update($channelSlug = "", $query = "")
{
	// This must be done as an AJAX request.
	$this->responseType = RESPONSE_TYPE_AJAX;

	list($channelInfo, $currentChannels, $channelIds, $includeDescendants) = $this->getSelectedChannels($channelSlug);

	// Work out which conversations we need to get details for (according to the input value.)
	$conversationIds = explode(",", R("conversationIds"));

	// Make sure they are all integers.
	foreach ($conversationIds as $k => $v) {
		if (!($conversationIds[$k] = (int)$v)) unset($conversationIds[$k]);
	}

	if (!count($conversationIds)) return;
	$conversationIds = array_slice((array)$conversationIds, 0, 20);

	// Get the full result data for these conversations, and construct an array of rendered conversation rows.
	$results = ET::searchModel()->getResults($conversationIds, true);
	$rows = array();
	foreach ($results as $conversation) {
		$rows[$conversation["conversationId"]] = $this->getViewContents("conversations/conversation", array("conversation" => $conversation, "channelInfo" => $channelInfo));
	}

	// Add that to the response.
	$this->json("conversations", $rows);

	// Now we need to work out if there are any new results for this channel/search query.

	// If the "random" gambit is in the search string, then don't go any further (because the results will
	// obviously differ!)
	$terms = $query ? explode("+", strtolower(str_replace("-", "+!", trim($query, " +-")))) : array();
	foreach ($terms as $v) {
		if (trim($v) == T("gambit.random"))	return;
	}

	// Get a list of conversation IDs for the channel/query.
	$newConversationIds = ET::searchModel()->getConversationIDs($channelIds, $query, count($currentChannels));
	$newConversationIds = array_slice((array)$newConversationIds, 0, 20);

	// Get the difference of the two sets of conversationId's.
	$diff = array_diff((array)$newConversationIds, (array)$conversationIds);
	if (count($diff)) $this->message(sprintf(T("message.newSearchResults"), "javascript:ETSearch.showNewActivity();void(0)"), array("id" => "newSearchResults"));

	$this->render();
}

}
Back to Top