/** * Show a sheet containing a list of groups. Pretty simple, really! * * @return void */ public function action_index() { ET::activityModel()->markNotificationsAsRead('unapproved'); $sql = ET::SQL(); $sql->where("confirmed", 0); $sql->orderBy("m.memberId desc"); $members = ET::memberModel()->getWithSQL($sql); $this->data("members", $members); $this->render("admin/unapproved"); }
/** * Check for updates to the esoTalk software. If there's a new version, and this is the first time we've heard * of it, create a notifcation for the current user. * * @return void */ public function checkForUpdates() { // Save the last update check time so we won't do it again for a while. ET::writeConfig(array("esoTalk.admin.lastUpdateCheckTime" => time())); // If the latest version is different to what it was last time we checked... $info = C("esoTalk.admin.lastUpdateCheckInfo", array("version" => ESOTALK_VERSION)); if ($package = ET::checkForUpdates() and $package["version"] != $info["version"]) { // Create a notification. ET::activityModel()->create("updateAvailable", ET::$session->userId, null, $package); // Write the latest checked version to the config file. ET::writeConfig(array("esoTalk.admin.lastUpdateCheckInfo" => $package)); } }
public function handler_conversationModel_addReplyAfter($sender, $conversation, $postId, $content) { // Only continue if this is the first post. if ($conversation["countPosts"] > 1) { return; } // We get all members who have starred the post author and have no unread posts in the conversation. $sql = ET::SQL()->from("member_member mm2", "mm2.memberId2=:userId AND mm2.memberId1=m.memberId AND mm2.follow=1 AND mm2.memberId1!=:userId", "inner")->from("member_conversation co", "co.conversationId=:conversationId AND co.type='member' AND co.id=m.memberId", "left")->where("co.lastRead IS NULL OR co.lastRead>=:posts")->bind(":conversationId", $conversation["conversationId"])->bind(":posts", $conversation["countPosts"] - 1)->bind(":userId", ET::$session->userId); $members = ET::memberModel()->getWithSQL($sql); $data = array("conversationId" => $conversation["conversationId"], "postId" => $postId, "title" => $conversation["title"]); $emailData = array("content" => $content); foreach ($members as $member) { // Check if this member is allowed to view this conversation before sending them a notification. $sql = ET::SQL()->select("conversationId")->from("conversation c")->where("conversationId", $conversation["conversationId"]); ET::conversationModel()->addAllowedPredicate($sql, $member); if (!$sql->exec()->numRows()) { continue; } ET::activityModel()->create("postMember", $member, ET::$session->user, $data, $emailData); } }
/** * Show a member profile with the activity pane. * * @param string $member The member ID. * @param int $page The activity page number. * @return void */ public function action_activity($member = "", $page = "") { // Set up the member profile page. if (!($member = $this->profile($member, "activity"))) { return; } // Work out the page number we're viewing and fetch the activity. $page = max(0, (int) $page - 1); $activity = ET::activityModel()->getActivity($member, $page * 10, 11); // We fetch 11 items so we can tell if there are more items after this page. $showViewMoreLink = false; if (count($activity) == 11) { array_pop($activity); $showViewMoreLink = true; } // Pass along necessary data to the view. $this->data("activity", $activity); $this->data("page", $page); $this->data("showViewMoreLink", $showViewMoreLink); $this->addJSLanguage("message.confirmDelete"); $this->renderProfile("member/activity"); }
/** * Render the specified view, in the format according to the controller's set response type. * * @param string $view The view to render. This can be left blank if we know the response type is one that * doesn't require a view, such as JSON or ATOM. * @return void */ public function render($view = "") { $this->trigger("renderBefore"); if ($this->responseType == RESPONSE_TYPE_DEFAULT and ET::$session->user) { // Fetch all unread notifications so we have a count for the notifications button. $notifications = ET::activityModel()->getNotifications(-1); $count = count($notifications); $this->addToMenu("user", "notifications", "<a href='" . URL("settings/notifications") . "' id='notifications' class='button popupButton " . ($count ? "new" : "") . "'><span>{$count}</span></a>"); // Show messages with these notifications. $this->notificationMessages($notifications); } // Set up the master view, content type, and other stuff depending on the response type. switch ($this->responseType) { // For an ATOM response, set the master view and the content type. case RESPONSE_TYPE_ATOM: $this->masterView = "atom.master"; $this->contentType = "application/atom+xml"; break; // For an AJAX or JSON response, set the master view and the content type. // If it's an AJAX response, set one of the JSON parameters to the specified view's contents. // For an AJAX or JSON response, set the master view and the content type. // If it's an AJAX response, set one of the JSON parameters to the specified view's contents. case RESPONSE_TYPE_AJAX: if ($view) { $this->json("view", $this->getViewContents($view, $this->data)); } case RESPONSE_TYPE_JSON: $this->masterView = "json.master"; $this->contentType = "application/json"; } // Set a content-type header. header("Content-type: " . $this->contentType . "; charset=" . T("charset", "utf-8")); // If we're just outputting the view on its own, do that now. if ($this->responseType === RESPONSE_TYPE_VIEW) { $this->renderView($view, $this->data); } else { // Make a new data array for the master view. $data = array(); // For any master views but the JSON and ATOM ones, give the view some data that will be useful in // rendering a HTML page. if ($this->masterView != "json.master" and $this->masterView != "atom.master") { // Fetch the content of the view, passing the data collected in the controller. if ($view) { $data["content"] = $this->getViewContents($view, $this->data); } // If config/custom.css contains something, add it to be included in the page. if (file_exists($file = PATH_CONFIG . "/custom.css") and filesize($file) > 0) { $this->addCSSFile("config/custom.css", true); } // Add the <head> contents and the page title. $data["head"] = $this->head(); $titleParts = array(); if ($this->title) { $titleParts[] = $this->title; } if ($t = C("esoTalk.forumTitle")) { $titleParts[] = $t; } $data["pageTitle"] = implode(" - ", $titleParts); // Add the forum title, or logo if the forum has one. $logo = C("esoTalk.forumLogo"); $title = C("esoTalk.forumTitle"); if ($logo) { $size = getimagesize($logo); } $data["forumTitle"] = $logo ? "<img src='" . getWebPath($logo) . "' {$size[3]} alt='{$title}'/>" : $title; // Add the details for the "back" button. $data["backButton"] = ET::$session->getNavigation($this->navigationId); // Get common menu items. foreach ($this->menus as $menu => $items) { $data[$menu . "MenuItems"] = $items->getContents(); } // Add the body class. $data["bodyClass"] = $this->bodyClass; // Get messages. $data["messages"] = $this->getMessages(); } $this->renderView($this->masterView, $data); } $this->trigger("renderAfter"); }
/** * Show a full conversation. * * @param string $conversationId The conversation ID, suffixed with the conversation's slug. * @param mixed $year Can be in one of three formats: * YYYY/MM: start viewing posts from a certain year/month combination * pX: start viewing posts from page X * X: start viewing posts from position X * @param int $month If specified, the YYYY/MM combination will be used. * @return void */ public function action_index($conversationId = false, $year = false, $month = false) { if (!$this->allowed()) { return; } // Get the conversation. $conversation = ET::conversationModel()->getById((int) $conversationId); // Stop here with a 404 header if the conversation wasn't found. if (!$conversation) { $this->render404(T("message.conversationNotFound"), true); return false; } // Are we searching within the conversation? If so, set the searchString and set the number of results as the post count. $searchString = R("search"); if ($searchString) { $conversation["countPosts"] = ET::postModel()->getSearchResultsCount($conversation["conversationId"], $searchString); $conversation["searching"] = true; // Add the keywords in $this->searchString to be highlighted. Make sure we keep ones "in quotes" together. $words = array(); $term = $searchString; 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); } else { ET::$session->remove("highlight"); } // Work out which post we are viewing from. $startFrom = 0; if ($year) { // Redirect to the user's oldest unread post. if ($year == "unread" and ET::$session->user) { // Fetch the post ID of the user's oldest unread post (according to $conversation["lastRead"].) $id = ET::SQL()->select("postId")->from("post")->where("conversationId=:conversationId")->bind(":conversationId", $conversation["conversationId"])->orderBy("time ASC")->offset((int) $conversation["lastRead"])->limit(1)->exec()->result(); // If a post ID was found, redirect to its position within the conversation. $startFrom = max(0, min($conversation["lastRead"], $conversation["countPosts"] - C("esoTalk.conversation.postsPerPage"))); if ($id) { $this->redirect(URL(conversationURL($conversation["conversationId"], $conversation["title"]) . "/{$startFrom}#p{$id}")); } } // Redirect to the last post in the conversation. if ($year == "unread" or $year == "last") { // Fetch the post ID of the last post in the conversation. $id = ET::SQL()->select("postId")->from("post")->where("conversationId=:conversationId")->bind(":conversationId", $conversation["conversationId"])->orderBy("time DESC")->limit(1)->exec()->result(); // Redirect there. $startFrom = max(0, $conversation["countPosts"] - C("esoTalk.conversation.postsPerPage")); $this->redirect(URL(conversationURL($conversation["conversationId"], $conversation["title"]) . "/{$startFrom}#p{$id}")); } elseif ($month and !$searchString) { $year = (int) $year; $month = (int) $month; // Bit of a hacky way of loading posts from the last page. if ($year == 9999 and $month == 99) { $timestamp = PHP_INT_MAX; } else { $timestamp = mktime(0, 0, 0, min($month, 12), 1, min($year, 2038)); } // Find the closest post that's after this timestamp, and find its position within the conversation. $position = ET::SQL()->select("COUNT(postId)", "position")->from("post")->where("time < :time")->bind(":time", $timestamp)->where("conversationId = :conversationId")->bind(":conversationId", $conversation["conversationId"])->exec()->result(); $startFrom = min($conversation["countPosts"] - C("esoTalk.conversation.postsPerPage"), $position); $this->data("month", $month); $this->data("year", $year); } else { if ($year[0] == "p") { $startFrom = ((int) ltrim($year, "p") - 1) * C("esoTalk.conversation.postsPerPage"); } else { $startFrom = (int) $year; } } } // Make sure the startFrom number is within range. $startFrom = max(0, $startFrom); if ($this->responseType === RESPONSE_TYPE_DEFAULT) { $startFrom = min($startFrom, max(0, $conversation["countPosts"] - 1)); } if (ET::$session->userId) { // Update the user's last read. ET::conversationModel()->setLastRead($conversation, ET::$session->userId, $startFrom + C("esoTalk.conversation.postsPerPage")); // If we're on the last page, mark any notifications related to this conversation as read. if ($startFrom + C("esoTalk.conversation.postsPerPage") >= $conversation["countPosts"]) { ET::activityModel()->markNotificationsAsRead(null, $conversation["conversationId"]); } // Update the user's last action. ET::memberModel()->updateLastAction("viewingConversation", $conversation["private"] ? null : array("conversationId" => $conversation["conversationId"], "title" => $conversation["title"])); } // Get the posts in the conversation. $options = array("startFrom" => $startFrom, "limit" => C("esoTalk.conversation.postsPerPage")); if ($searchString) { $options["search"] = $searchString; } if ($startFrom < $conversation["countPosts"]) { $posts = ET::postModel()->getByConversation($conversation["conversationId"], $options); } else { $posts = array(); } $this->trigger("conversationIndex", array(&$conversation, &$posts, &$startFrom, &$searchString)); // Transport some data to the view. $this->data("conversation", $conversation); $this->data("posts", $posts); $this->data("startFrom", $startFrom); $this->data("searchString", $searchString); if ($this->responseType === RESPONSE_TYPE_DEFAULT) { // Construct a canonical URL to this page. $url = conversationURL($conversation["conversationId"], $conversation["title"]) . "/{$startFrom}" . ($searchString ? "?search=" . urlencode($searchString) : ""); $this->canonicalURL = URL($url, true); // If the slug in the URL is not the same as the actual slug, redirect. $slug = slug($conversation["title"]); if ($slug and (strpos($conversationId, "-") === false or substr($conversationId, strpos($conversationId, "-") + 1) != $slug)) { redirect(URL($url), 301); } // Push onto the top of the naviagation stack. $this->pushNavigation("conversation/" . $conversation["conversationId"], "conversation", URL($url)); // Set the title of the page. $this->title = $conversation["title"]; // Get a list of the members allowed in this conversation. $conversation["membersAllowed"] = ET::conversationModel()->getMembersAllowed($conversation); $conversation["membersAllowedSummary"] = ET::conversationModel()->getMembersAllowedSummary($conversation, $conversation["membersAllowed"]); // Get the channel path of this conversation. $conversation["channelPath"] = ET::conversationModel()->getChannelPath($conversation); // Add essential variables and language definitions to be accessible through JavaScript. if ($conversation["canModerate"]) { $this->addJSLanguage("Lock", "Unlock", "Sticky", "Unsticky"); } if (ET::$session->user) { $this->addJSLanguage("Starred", "Unstarred", "message.confirmLeave", "message.confirmDiscardPost", "message.confirmDelete", "Ignore conversation", "Unignore conversation", "Controls", "Follow", "Following"); } $this->addJSVar("postsPerPage", C("esoTalk.conversation.postsPerPage")); $this->addJSVar("conversationUpdateIntervalStart", C("esoTalk.conversation.updateIntervalStart")); $this->addJSVar("conversationUpdateIntervalMultiplier", C("esoTalk.conversation.updateIntervalMultiplier")); $this->addJSVar("conversationUpdateIntervalLimit", C("esoTalk.conversation.updateIntervalLimit")); $this->addJSVar("mentions", C("esoTalk.format.mentions")); $this->addJSVar("time", time()); $this->addJSFile("core/js/lib/jquery.autogrow.js"); $this->addJSFile("core/js/scrubber.js"); $this->addJSFile("core/js/autocomplete.js"); $this->addJSFile("core/js/conversation.js"); // Add the RSS feed button. // $this->addToMenu("meta", "feed", "<a href='".URL("conversation/index.atom/".$url)."' id='feed'>".T("Feed")."</a>"); $controls = ETFactory::make("menu"); // Ignore conversation control if (ET::$session->user) { $controls->add("ignore", "<a href='" . URL("conversation/ignore/" . $conversation["conversationId"] . "/?token=" . ET::$session->token . "&return=" . urlencode($this->selfURL)) . "' id='control-ignore'><i class='icon-eye-close'></i> <span>" . T($conversation["ignored"] ? "Unignore conversation" : "Ignore conversation") . "</span></a>"); } // Mark as unread/read control if (ET::$session->user) { $controls->add("read", "<a href='" . URL("conversation/read/" . $conversation["conversationId"] . "/?token=" . ET::$session->token) . "' id='control-read'><i class='icon-circle'></i> <span>" . T($conversation["lastRead"] >= $conversation["countPosts"] ? "Mark as unread" : "Mark as read") . "</span></a>"); } if ($conversation["canModerate"] or $conversation["startMemberId"] == ET::$session->userId) { $controls->separator(); // Add the change channel control. $controls->add("changeChannel", "<a href='" . URL("conversation/changeChannel/" . $conversation["conversationId"] . "/?return=" . urlencode($this->selfURL)) . "' id='control-changeChannel'><i class='icon-tag'></i> <span>" . T("Change channel") . "</span></a>"); } // If the user has permission to moderate this conversation... if ($conversation["canModerate"]) { // Add the sticky/unsticky control. $controls->add("sticky", "<a href='" . URL("conversation/sticky/" . $conversation["conversationId"] . "/?token=" . ET::$session->token . "&return=" . urlencode($this->selfURL)) . "' id='control-sticky'><i class='icon-pushpin'></i> <span>" . T($conversation["sticky"] ? "Unsticky" : "Sticky") . "</span></a>"); // Add the lock/unlock control. $controls->add("lock", "<a href='" . URL("conversation/lock/" . $conversation["conversationId"] . "/?token=" . ET::$session->token . "&return=" . urlencode($this->selfURL)) . "' id='control-lock'><i class='icon-lock'></i> <span>" . T($conversation["locked"] ? "Unlock" : "Lock") . "</span></a>"); } if ($conversation["canDeleteConversation"]) { // Add the delete conversation control. $controls->separator(); $controls->add("delete", "<a href='" . URL("conversation/delete/" . $conversation["conversationId"] . "/?token=" . ET::$session->token) . "' id='control-delete'><i class='icon-remove'></i> <span>" . T("Delete conversation") . "</span></a>"); } // Add the meta description tag to the head. It will contain an excerpt from the first post's content. if ($conversation["countPosts"] > 0) { $description = ET::SQL()->select("LEFT(content, 156)")->from("post")->where("conversationId=:conversationId")->bind(":conversationId", $conversation["conversationId"])->orderBy("time ASC")->limit(1)->exec()->result(); if (strlen($description) > 155) { $description = substr($description, 0, strrpos($description, " ")) . " ..."; } $description = str_replace(array("\n\n", "\n"), " ", $description); $this->addToHead("<meta name='description' content='" . sanitizeHTML($description) . "'>"); } // Add JavaScript variables which contain conversation information. $this->addJSVar("conversation", array("conversationId" => (int) $conversation["conversationId"], "slug" => conversationURL($conversation["conversationId"], $conversation["title"]), "countPosts" => (int) $conversation["countPosts"], "startFrom" => (int) $startFrom, "searchString" => $searchString, "lastRead" => (ET::$session->user and $conversation["conversationId"]) ? (int) max(0, min($conversation["countPosts"], $conversation["lastRead"])) : (int) $conversation["countPosts"], "updateInterval" => max(C("esoTalk.conversation.updateIntervalStart"), min(round(sqrt(time() - $conversation["lastPostTime"])), C("esoTalk.conversation.updateIntervalLimit"))), "channelId" => (int) $conversation["channelId"])); // Quote a post: get the post details (id, name, content) and then set the value of the reply textarea appropriately. if ($postId = (int) R("quote")) { $post = $this->getPostForQuoting($postId, $conversation["conversationId"]); if ($post) { $conversation["draft"] = "[quote={$postId}:" . $post["username"] . "]" . ET::formatter()->init($post["content"])->removeQuotes()->get() . "[/quote]"; } } // Set up the reply form. $replyForm = ETFactory::make("form"); $replyForm->action = URL("conversation/reply/" . $conversation["conversationId"]); $replyForm->setValue("content", $conversation["draft"]); $this->trigger("conversationIndexDefault", array(&$conversation, &$controls, &$replyForm, &$replyControls)); $this->data("replyForm", $replyForm); $this->data("replyControls", $this->getEditControls("reply")); $this->data("conversation", $conversation); $this->data("controlsMenu", $controls); $this->render("conversation/index"); } elseif ($this->responseType === RESPONSE_TYPE_AJAX) { $this->json("countPosts", $conversation["countPosts"]); $this->json("startFrom", $startFrom); $this->render("conversation/posts"); } elseif ($this->responseType === RESPONSE_TYPE_VIEW) { $this->render("conversation/posts"); } }
/** * Set a member's account and groups. * * @param array $member The details of the member to set the account/groups for. * @param string $account The new account. * @param array $groups The new group IDs. * @return bool true on success, false on error. */ public function setGroups($member, $account, $groups = array()) { // Make sure the account is valid. if (!in_array($account, array(ACCOUNT_MEMBER, ACCOUNT_ADMINISTRATOR, ACCOUNT_SUSPENDED, ACCOUNT_PENDING))) { $this->error("account", "invalidAccount"); } if ($this->errorCount()) { return false; } // Set the member's new account. $this->updateById($member["memberId"], array("account" => $account)); // Delete all of the member's existing group associations. ET::SQL()->delete()->from("member_group")->where("memberId", $member["memberId"])->exec(); // Insert new member-group associations. $inserts = array(); foreach ($groups as $id) { $inserts[] = array($member["memberId"], $id); } if (count($inserts)) { ET::SQL()->insert("member_group")->setMultiple(array("memberId", "groupId"), $inserts)->exec(); } // Now we need to create a new activity item, and to do that we need the names of the member's groups. $groupData = ET::groupModel()->getAll(); $groupNames = array(); foreach ($groups as $id) { $groupNames[$id] = $groupData[$id]["name"]; } ET::activityModel()->create("groupChange", $member, ET::$session->user, array("account" => $account, "groups" => $groupNames)); return true; }
/** * Send private conversation invitation notifications to a list of members. A notification will only * be sent if this is the first time a member has been added to the conversation, to prevent intentional * email spamming. * * @param array $conversation The conversation to that we're sending out notifications for. * @param array $memberIds A list of member IDs to send the notifications to. * @param bool $notifyAll If set to true, all members will be notified regardless of if they have been * added to this conversation before. * @return void */ protected function privateAddNotification($conversation, $memberIds, $notifyAll = false) { $memberIds = (array) $memberIds; // Remove the currently logged in user from the list of member IDs. if (($k = array_search(ET::$session->userId, $memberIds)) !== false) { unset($memberIds[$k]); } if (!count($memberIds)) { return; } // Get the member details for this list of member IDs. $sql = ET::SQL()->from("member_conversation s", "s.conversationId=:conversationId AND s.type='member' AND s.id=m.memberId", "left")->bind(":conversationId", $conversation["conversationId"])->where("m.memberId IN (:memberIds)")->bind(":memberIds", $memberIds); // Only get members where the member_conversation row doesn't exist (implying that this is the first time // they've been added to the conversation.) if (!$notifyAll) { $sql->where("s.id IS NULL"); } $members = ET::memberModel()->getWithSQL($sql); $data = array("conversationId" => $conversation["conversationId"], "title" => $conversation["title"]); foreach ($members as $member) { ET::activityModel()->create("privateAdd", $member, ET::$session->user, $data); } }
/** * Send private conversation invitation notifications to a list of members. A notification will only * be sent if this is the first time a member has been added to the conversation, to prevent intentional * email spamming. * * @param array $conversation The conversation to that we're sending out notifications for. * @param array $memberIds A list of member IDs to send the notifications to. * @param bool $notifyAll If set to true, all members will be notified regardless of if they have been * added to this conversation before. * @return void */ protected function privateAddNotification($conversation, $memberIds, $notifyAll = false, $content = null) { $memberIds = (array) $memberIds; // Remove the currently logged in user from the list of member IDs. if (($k = array_search(ET::$session->userId, $memberIds)) !== false) { unset($memberIds[$k]); } if (!count($memberIds)) { return; } // Get the member details for this list of member IDs. $sql = ET::SQL()->from("member_conversation s", "s.conversationId=:conversationId AND s.type='member' AND s.id=m.memberId", "left")->bind(":conversationId", $conversation["conversationId"])->where("m.memberId IN (:memberIds)")->bind(":memberIds", $memberIds); // Only get members where the member_conversation row doesn't exist (implying that this is the first time // they've been added to the conversation.) if (!$notifyAll) { $sql->where("s.id IS NULL"); } $members = ET::memberModel()->getWithSQL($sql); $data = array("conversationId" => $conversation["conversationId"], "title" => $conversation["title"]); $emailData = array("content" => $content); // Create the "privateAdd" activity which will send out a notification and an email if appropriate. // Also get IDs of members who would like to automatically follow this conversation. $followIds = array(); foreach ($members as $member) { ET::activityModel()->create("privateAdd", $member, ET::$session->user, $data, $emailData); if (!empty($member["preferences"]["starPrivate"])) { $followIds[] = $member["memberId"]; } } // Follow the conversation for the appropriate members. if (!empty($followIds)) { $this->setStatus($conversation["conversationId"], $followIds, array("starred" => true)); } }
/** * */ public function notificationCheck() { $this->responseType = RESPONSE_TYPE_AJAX; $notifications = ET::activityModel()->getNotifications(-1); $this->json("count", count($notifications)); $this->notificationMessages($notifications); $this->render(); }
/** * Create a post in the specified conversation. * * This function will go through the post content and notify any members who are @mentioned. * * @param int $conversationId The ID of the conversation to create the post in. * @param int $memberId The ID of the author of the post. * @param string $content The post content. * @param string $title The title of the conversation (so it can be added alongside the post, for fulltext purposes.) * @return bool|int The new post's ID, or false if there were errors. */ public function create($conversationId, $memberId, $content, $title = "") { // Validate the post content. $this->validate("content", $content, array($this, "validateContent")); if ($this->errorCount()) { return false; } // Prepare the post details for the query. $data = array("conversationId" => $conversationId, "memberId" => $memberId, "time" => time(), "content" => $content, "title" => $title); $id = parent::create($data); // Update the member's post count. ET::SQL()->update("member")->set("countPosts", "countPosts + 1", false)->where("memberId", $memberId)->exec(); // Update the channel's post count. ET::SQL()->update("channel")->set("countPosts", "countPosts + 1", false)->where("channelId", ET::SQL()->select("channelId")->from("conversation")->where("conversationId=:conversationId")->bind(":conversationId", $conversationId)->exec()->result())->exec(); // Parse the post content for @mentions, and notify any members who were mentioned. if (C("esoTalk.format.mentions")) { $names = ET::formatter()->getMentions($content); if (count($names)) { // Get the member details from the database. $sql = ET::SQL()->where("m.username IN (:names)")->bind(":names", $names)->where("m.memberId != :userId")->bind(":userId", $memberId); $members = ET::memberModel()->getWithSQL($sql); $data = array("conversationId" => $conversationId, "postId" => (int) $id, "title" => $title); $emailData = array("content" => $content); $i = 0; foreach ($members as $member) { // Only send notifications to the first 10 members who are mentioned to prevent abuse of the system. if ($i++ > 10) { break; } // Check if this member is allowed to view this conversation before sending them a notification. $sql = ET::SQL()->select("conversationId")->from("conversation c")->where("conversationId", $conversationId); ET::conversationModel()->addAllowedPredicate($sql, $member); if (!$sql->exec()->numRows()) { continue; } ET::activityModel()->create("mention", $member, ET::$session->user, $data, $emailData); } } } return $id; }
/** * Common initialization for all controllers, called on every page load. This will add basic user links to * the "user" menu, and add core JS files and language definitions. * * If this is overridden, parent::init() should be called to maintain consistency between controllers. * * @return void */ public function init() { // Check for updates to the esoTalk software, but only if we're the root admin and we haven't checked in // a while. if (ET::$session->userId == C("esoTalk.rootAdmin") and C("esoTalk.admin.lastUpdateCheckTime") + C("esoTalk.updateCheckInterval") < time()) { ET::upgradeModel()->checkForUpdates(); } if ($this->responseType === RESPONSE_TYPE_DEFAULT) { // If the user IS NOT logged in, add the 'login' and 'sign up' links to the bar. if (!ET::$session->user) { $this->addToMenu("user", "login", "<a href='" . URL("user/login?return=" . urlencode($this->selfURL)) . "' class='link-login'>" . T("Log In") . "</a>"); $this->addToMenu("user", "join", "<a href='" . URL("user/join?return=" . urlencode($this->selfURL)) . "' class='link-join'>" . T("Sign Up") . "</a>"); } else { $this->addToMenu("user", "user", "<a href='" . URL("member/me") . "'>" . avatar(ET::$session->userId, ET::$session->user["avatarFormat"], "thumb") . name(ET::$session->user["username"]) . "</a>"); // Fetch all unread notifications so we have a count for the notifications button. $notifications = ET::activityModel()->getNotifications(-1); $count = count($notifications); $this->addToMenu("user", "notifications", "<a href='" . URL("settings/notifications") . "' id='notifications' class='popupButton " . ($count ? "new" : "") . "'><span>{$count}</span></a>"); // Show messages with these notifications. $this->notificationMessages($notifications); $this->addToMenu("user", "settings", "<a href='" . URL("settings") . "' class='link-settings'>" . T("Settings") . "</a>"); if (ET::$session->isAdmin()) { $this->addToMenu("user", "administration", "<a href='" . URL("admin") . "' class='link-administration'>" . T("Administration") . "</a>"); } $this->addToMenu("user", "logout", "<a href='" . URL("user/logout") . "' class='link-logout'>" . T("Log Out") . "</a>"); } // Get the number of members currently online and add it as a statistic. $online = ET::SQL()->select("COUNT(*)")->from("member")->where("UNIX_TIMESTAMP()-:seconds<lastActionTime")->bind(":seconds", C("esoTalk.userOnlineExpire"))->exec()->result(); $stat = Ts("statistic.online", "statistic.online.plural", number_format($online)); $stat = "<a href='" . URL("members/online") . "' class='link-membersOnline'>{$stat}</a>"; $this->addToMenu("statistics", "statistic-online", $stat); $this->addToMenu("meta", "copyright", "<a href='http://esotalk.com/'>Powered by esoTalk" . (ET::$session->isAdmin() ? " " . ESOTALK_VERSION : "") . "</a>"); // Set up some default JavaScript files and language definitions. $this->addJSFile("js/lib/jquery.js", true); $this->addJSFile("js/lib/jquery.misc.js", true); $this->addJSFile("js/lib/jquery.history.js", true); $this->addJSFile("js/lib/jquery.scrollTo.js", true); $this->addJSFile("js/global.js", true); $this->addJSLanguage("message.ajaxRequestPending", "message.ajaxDisconnected", "Loading...", "Notifications"); $this->addJSVar("notificationCheckInterval", C("esoTalk.notificationCheckInterval")); // If config/custom.css contains something, add it to be included in the page. if (file_exists($file = PATH_CONFIG . "/custom.css") and filesize($file) > 0) { $this->addCSSFile("config/custom.css", true); } } $this->trigger("init"); }
/** * Show the sign up sheet and handle input from its form. * * @return void */ public function action_join() { // If we're already logged in, get out of here. if (ET::$session->user) { $this->redirect(URL("")); } // If registration is closed, show a message. if (!C("esoTalk.registration.open")) { $this->renderMessage(T("Registration Closed"), T("message.registrationClosed")); return; } // Set the title and make sure this page isn't indexed. $this->title = T("Sign Up"); $this->addToHead("<meta name='robots' content='noindex, noarchive'/>"); // Construct a form. $form = ETFactory::make("form"); $form->action = URL("user/join"); // Add the username field to the form structure. $form->addSection("username", T("Username")); $form->addField("username", "username", function ($form) { return $form->input("username"); }, function ($form, $key, &$data) { $data["username"] = $form->getValue($key); }); // Add the email field to the form structure. $form->addSection("email", T("Email")); $form->addField("email", "email", function ($form) { return $form->input("email") . "<br><small>" . T("Used to verify your account and subscribe to conversations") . "</small>"; }, function ($form, $key, &$data) { $data["email"] = $form->getValue($key); }); // Add the password field to the form structure. $form->addSection("password", T("Password")); $form->addField("password", "password", function ($form) { return $form->input("password", "password") . "<br><small>" . sprintf(T("Choose a secure password of at least %s characters"), C("esoTalk.minPasswordLength")) . "</small>"; }, function ($form, $key, &$data) { $data["password"] = $form->getValue($key); }); // Add the confirm password field to the form structure. $form->addSection("confirm", T("Confirm password")); $form->addField("confirm", "confirm", function ($form) { return $form->input("confirm", "password"); }, function ($form, $key, &$data) { // Make sure the passwords match. if ($form->getValue("password") != $form->getValue($key)) { $form->error($key, T("message.passwordsDontMatch")); } }); $this->trigger("initJoin", array($form)); // If the cancel button was pressed, return to where the user was before. if ($form->isPostBack("cancel")) { $this->redirect(URL(R("return"))); } // If the form has been submitted, validate it and add the member into the database. if ($form->validPostBack("submit")) { $data = array(); if ($form->validPostBack()) { $form->runFieldCallbacks($data); } if (!$form->errorCount()) { $data["account"] = ACCOUNT_MEMBER; if (!C("esoTalk.registration.requireConfirmation")) { $data["confirmed"] = true; } else { $data["resetPassword"] = md5(uniqid(rand())); } // Create the member. $model = ET::memberModel(); $memberId = $model->create($data); // If there were validation errors, pass them to the form. if ($model->errorCount()) { $form->errors($model->errors()); } else { // If we require the user to confirm their email, send them an email and show a message. if (C("esoTalk.registration.requireConfirmation") == "email") { $this->sendConfirmationEmail($data["email"], $data["username"], $memberId . $data["resetPassword"]); $this->renderMessage(T("Success!"), T("message.confirmEmail")); } elseif (C("esoTalk.registration.requireConfirmation") == "approval") { $admin = ET::memberModel()->getById(C("esoTalk.rootAdmin")); ET::activityModel()->create("unapproved", $admin, null, array("username" => $data["username"])); $this->renderMessage(T("Success!"), T("message.waitForApproval")); } else { ET::$session->login($form->getValue("username"), $form->getValue("password")); $this->redirect(URL("")); } return; } } } $this->data("form", $form); $this->render("user/join"); }
/** * Unmark a post as deleted. * * @param array $post The post to unmark as deleted. * @return bool true on success, false on error. */ public function restorePost(&$post) { // Update the post. $time = time(); $this->updateById($post["postId"], array("deleteMemberId" => null, "deleteTime" => null)); $post["deleteMemberId"] = null; $post["deleteMemberName"] = null; $post["deleteTime"] = null; //更新缓存 $am = ET::activityModel(); ET::$cache->store($am::CACHE_KEY . '_' . $post['memberId'] . '_' . $am::CACHE_NS_KEY, time()); $sm = ET::searchModel(); ET::$cache->store($sm::CACHE_NS_KEY, time()); return true; }