singletonIf('listings.item.comments', static::class); return bff('listings.item.comments'); } protected function initSettings() { $this->tblComments = Listings::TABLE_ITEMS_COMMENTS; $this->tblItems = Listings::TABLE_ITEMS; $this->tblItemsID = 'id'; $this->preModeration = $this->config('listings.comments.premoderation', false, TYPE_BOOL); # премодерация $this->moderationEdit = true; # редактирование на этапе модерации $this->commentsTree = true; # древовидные $this->commentsTree2Levels = true; # не более двух уровней # config-ключ для хранения общего счетчика непромодерированных комментариев в таблице настроек $this->counterKey_UnmoderatedAll = 'listings_comments_mod'; # поле в таблице (TABLE_ITEMS) для хранения кол-ва комментариев объявления(видимых пользователю) $this->counterKey_ItemComments = 'comments_cnt'; $this->commentHideReasons[self::commentDeletedByModerator] = _t('listings', 'Deleted by moderator'); $this->commentHideReasons[self::commentDeletedByCommentOwner] = _t('listings', 'Deleted by commenter'); $this->commentHideReasons[self::commentDeletedByItemOwner] = _t('listings', 'Deleted by listing author'); $this->commentHideReasons[self::commentFromBlockedUser] = _t('listings', 'Comment from a suspended user'); $this->urlListing = Admin::url('listings/listing', ['list_action' => 'edit', 'ftab' => 'comments', 'id' => '']); $this->urlListingAjax = Admin::url('listings/comments_ajax'); $this->minMessageLen = 5; $this->maxMessageLen = 2500; } /** * Получение данных о комментариях к объявлению * @param int $itemID ID объявления * @param int $commentID ID комментария * @param Closure|null $callback обработчик данных о комментарии * @return array */ public function commentsDataFrontend($itemID, $commentID = 0, ?Closure $callback = null): array { $return = ['comments' => [], 'total' => 0]; do { $sql = []; $bind = [':itemID' => $itemID]; if ($commentID > 0) { $sql[] = 'C.id = :commentID'; $bind[':commentID'] = $commentID; } # исключаем непромодерированные if ($this->preModeration) { $sql[] = 'C.moderated = 1'; } # учитываем ID группы if ($this->isGroupUsed()) { $sql[] = 'C.' . $this->tblCommentsGroupID . ' = :groupID'; $bind[':groupID'] = $this->groupID; } $data = $this->db->tag('listings-item-comments-data-frontend') ->select_key('SELECT C.*, U.login, U.name, U.avatar, U.sex, U.blocked as ublocked FROM ' . $this->tblComments . ' C LEFT JOIN ' . Users::TABLE_USERS . ' U ON C.user_id = U.user_id WHERE C.item_id = :itemID' . ( ! empty($sql) ? ' AND ' . join(' AND ', $sql) : ' ' ) . ' ORDER BY C.id ASC', 'id', $bind); if (empty($data)) { break; } foreach ($data as $k => &$v) { if ($v['ublocked'] && !$v['deleted']) { $v['deleted'] = self::commentFromBlockedUser; } if ($v['numlevel'] > 1 && (! isset($data[$v['pid']]) && !$commentID)) { unset($data[$k]); } $v['user_url_profile'] = Users::url('user.profile', ['login' => $v['login']]); $v['user_url_avatar'] = UsersAvatar::url($v['user_id'], $v['avatar'], null, $v['sex']); if ($callback) { $v = $callback($v); } } unset($v); if ($commentID) { $data = $this->db->transformRowsToTree($data, 'id', 'pid', 'sub'); if (isset($data[$commentID])) { $return['total'] = 1; $return['comments'] = [$commentID => $data[$commentID]]; } break; } $return['total'] = sizeof($data); $return['comments'] = $this->db->transformRowsToTree($data, 'id', 'pid', 'sub'); } while (false); return $return; } /** * Валидация текста комментария * @param string $message текст сообщения * @param bool $activateLinks активировать ссылки * @return string */ public function validateMessage($message, $activateLinks = true) { /** * Отключаем активацию ссылок в комментариях отправляемых пользователями */ $message = $this->input->cleanTextPlain($message, $this->maxMessageLen, false); $message = TextParser::antimat($message); $message = $this->app->filter('listings.comments.message.validate', $message); return $message; } /** * Отправка email уведомлений, при добавлении нового комментария * @param int $itemID ID объявления * @param int $commentID ID комментария * @return void */ protected function sendEmailNotify($itemID, $commentID) { $itemData = Listings::model()->itemData($itemID, [ 'id','user_id','link','title', ]); if (empty($itemData)) { return; } if ($commentID) { $commentData = $this->commentData($itemID, $commentID); if (empty($commentData)) { return; } } else { # уведомляем автора объявления $commentData = ['user_id' => 0]; } $enotifyID = Users::ENOTIFY_LISTINGS_COMMENTS; # отправим автору объявления, если это не его комментарий if ($commentData['user_id'] != $itemData['user_id']) { $userData = Users::model()->userDataEnotify($itemData['user_id'], $enotifyID); if ($userData) { $this->app->sendMailTemplate( [ 'name' => $userData['name'], 'email' => $userData['email'], 'user_id' => $itemData['user_id'], 'item_id' => $itemID, 'item_link' => $itemData['link'] . '?alogin=' . Users::loginAutoHash($userData), 'item_title' => $itemData['title'], ], 'listings_item_comment', $userData['email'], false, '', '', $userData['lang'] ); } } # отправим уведомление остальным пользователям, участвовавшим в переписке $usersList = $this->db->select_one_column(' SELECT user_id FROM ' . $this->tblComments . ' WHERE item_id = :item_id AND user_id NOT IN (:owner_item, :owner_comment) GROUP BY user_id', [ ':item_id' => $itemID, ':owner_item' => $itemData['user_id'], ':owner_comment' => $commentData['user_id'], ]); if (! empty($usersList)) { foreach ($usersList as $userID) { if ($userID == $itemData['user_id']) { continue; # автору объявления уже отправили } $userData = Users::model()->userDataEnotify($userID, $enotifyID); if ($userData) { $this->app->sendMailTemplate( [ 'name' => $userData['name'], 'email' => $userData['email'], 'user_id' => $userID, 'item_id' => $itemID, 'item_link' => $itemData['link'] . '?alogin=' . Users::loginAutoHash($userData), 'item_title' => $itemData['title'], ], 'listings_item_comment', $userData['email'], false, '', '', $userData['lang'] ); } } } } /** * Обработчик события модерации комментариев * @param array $commentsID ID промодерированных комментариев * @param string $action тип действия: moderate-one, moderate-many, add, add-admin * @return void */ public function onCommentsModerated(array $commentsID, $action) { switch ($action) { case 'moderate-one': { if ($this->isPreModeration()) { $data = $this->db->select('SELECT id, item_id FROM ' . $this->tblComments . ' WHERE ' . $this->db->prepareIN('id', $commentsID)); foreach ($data as $v) { $this->sendEmailNotify($v['item_id'], $v['id']); } } } break; case 'moderate-many': { if ($this->isPreModeration()) { $data = $this->db->select(' SELECT item_id, COUNT(id) AS cnt, MIN(id) AS id FROM ' . $this->tblComments . ' WHERE ' . $this->db->prepareIN('id', $commentsID) . ' GROUP BY item_id'); $itemsID = []; foreach ($data as $v) { if ($v['cnt'] == 1) { $this->sendEmailNotify($v['item_id'], $v['id']); } else { $itemsID[] = $v['item_id']; } } if (! empty($itemsID)) { foreach ($itemsID as $v) { $this->sendEmailNotify($v, 0); } } } } break; case 'add': case 'add-admin': { $data = $this->db->select( 'SELECT id, item_id FROM ' . $this->tblComments . ' WHERE ' . $this->db->prepareIN('id', $commentsID) ); foreach ($data as $v) { $this->sendEmailNotify($v['item_id'], $v['id']); } } break; } } /** * Блок комментариев объявления * @param array $settings * @return views\item\CommentsBlock */ public function getBlock(array $settings) { return new views\item\CommentsBlock($settings); } /** * Настройки блока комментариев объявления * @param int $itemID ID объявления * @param int $userID ID текущего пользователя * @param array $itemData Данные об объявлении * @param array $companyData Данные о компании * @return array */ public function getBlockSettings($itemID, $userID, array $itemData = [], array $companyData = []) { $settings = [ 'comments' => $this, 'itemID' => $itemID, 'userID' => $userID, ]; # Объявление if (empty($itemData) && $itemID) { $itemData = Listings::model()->itemData($itemID, ['user_id', 'status', 'company_id']); } $settings['itemUserID'] = $itemData['user_id'] ?? 0; $settings['itemCompanyID'] = $itemData['company_id'] ?? 0; $settings['itemStatus'] = $itemData['status'] ?? 0; # Компания if ($settings['itemCompanyID'] && empty($companyData) && bff::businessEnabled()) { $companyData = Business::model()->companiesDataSidebar($settings['itemCompanyID']); } $settings['itemCompanyData'] = $companyData ?: []; return $settings; } /** * Обработчик действий пользователя в блоке комментариев к объявлению * @param int $userID ID пользователя * @param string $action * @return \bff\http\Response */ public function onUserAction($userID, $action) { $response = []; do { if (! $userID) { $this->errors->reloadPage(); break; } if (! $this->isRequestValid(['userId' => $userID])) { $this->errors->reloadPage(); break; } switch ($action) { case 'add': # добавление комментария пользователем $message = $this->input->post('message', TYPE_NOTAGS); $itemID = $this->input->post('item_id', TYPE_UINT); $parentID = $this->input->post('parent', TYPE_UINT); $commentID = $this->addItemComment($itemID, $message, $parentID, $userID, User::data('name'), [ 'throttle' => ['timeout' => 10, 'userId' => $userID], ]); if ($commentID > 0) { $response['premod'] = $this->isPreModeration(); if (! $response['premod']) { $block = $this->getBlock($this->getBlockSettings($itemID, $userID)); $commentsData = $block->getCommentsData($commentID); $response['html'] = $block->getList( $commentsData['comments'] ); } } break; case 'delete': # удаление комментария пользователем $itemID = $this->input->post('item_id', TYPE_UINT); $commentID = $this->input->post('id', TYPE_UINT); $success = $this->deleteItemComment($itemID, $commentID, $userID, [ 'throttle' => ['timeout' => 10, 'userId' => $userID], ]); if ($success) { $response['html'] = ''; $block = $this->getBlock($this->getBlockSettings($itemID, $userID)); $commentsData = $block->getCommentsData($commentID); if ($commentsData['total'] > 0) { $response['html'] = $block->getList( $commentsData['comments'] ); } } break; default: $this->errors->reloadPage(); break; } } while (false); return $this->ajaxResponseForm($response); } /** * Добавление комментария объявления пользователем * @param int $itemID ID объявления * @param string $message текст комментария * @param int $parentID ID комментария на который выполняется ответ * @param int $userID ID пользователя, инициирующего добавление * @param string $userName имя пользователя, инициирующего добавление * @param array $opts [throttle] * @return int ID добавленного комментария или 0 */ public function addItemComment($itemID, $message, $parentID, $userID, $userName = '', array $opts = []) { do { $opts = $this->defaults($opts, [ 'throttle' => [], ]); $message = $this->validateMessage($message, false); if (mb_strlen($message) < $this->minMessageLen) { $this->errors->set(_t('comments', 'Your comment can not be shorter than [length]', [ 'length' => tpl::declension($this->minMessageLen, _t('', 'character;characters;characters')) ]), 'message'); break; } if (! $itemID || ! $userID) { $this->errors->reloadPage(); break; } $itemData = Listings::model()->itemData($itemID, ['status', 'user_id', 'company_id']); if (empty($itemData)) { $this->errors->reloadPage(); break; } if ($itemData['status'] != Listings::STATUS_PUBLICATED) { # Запрещаем добавление комментариев если объявление не опубликовано $this->errors->impossible(); break; } if (! empty($opts['throttle'])) { $throttle = is_array($opts['throttle']) ? $opts['throttle'] : []; if ($this->tooManyRequests($throttle['actionKey'] ?? 'listings-item-comment-add', $throttle)) { break; } } $commentID = $this->commentInsert($itemID, [ 'user_id' => $userID, 'message' => $message, 'name' => $userName, ], $parentID); if ($commentID) { # Сообщаем о событии добавления комментария пользователем $this->app->hook('listings.comments.add', $itemID, [ 'premoderation' => $this->isPreModeration(), 'comment_id' => $commentID, 'parent_id' => $parentID, 'message' => $message, 'user_id' => $userID, 'user_name' => $userName, ]); return $commentID; } } while (false); return 0; } /** * Удаление комментария объявления пользователем * @param int $itemID ID объявления * @param int $commentID ID комментария * @param int $userID ID пользователя, инициирующего удаление * @param array $opts [throttle] * @return bool */ public function deleteItemComment($itemID, $commentID, $userID, array $opts = []) { do { $opts = $this->defaults($opts, [ 'throttle' => [], ]); if (! $itemID || ! $commentID || ! $userID) { $this->errors->reloadPage(); break; } $itemData = Listings::model()->itemData($itemID, ['status', 'user_id', 'company_id']); if (empty($itemData)) { $this->errors->reloadPage(); break; } if ($itemData['status'] != Listings::STATUS_PUBLICATED) { $this->errors->impossible(); break; } $commentData = $this->commentData($itemID, $commentID); if (empty($commentData)) { $this->errors->reloadPage(); break; } if ($commentData['user_id'] != $userID) { # удаление доступно только владельцу комментария $this->errors->reloadPage(); break; } if (! empty($opts['throttle'])) { $throttle = is_array($opts['throttle']) ? $opts['throttle'] : []; if ($this->tooManyRequests($throttle['actionKey'] ?? 'listings-item-comment-delete', $throttle)) { break; } } $this->commentDelete($itemID, $commentID, static::commentDeletedByCommentOwner); return true; } while (false); return false; } }