security->publicMethod($this, [ 'catsList', 'searchSuggest', 'img', 'comments', ]); } /** * Список выбора категорий * @param string $listType тип списка * @param array $opt параметры [ * 'parentID' - ID parent-категории * 'query' - текст для поиска * 'inputName' - [cat_id] * ] * @return mixed|void */ public function catsList($listType = '', array $opt = []) { $opt = $this->defaults($opt, [ 'init' => false, 'parentID' => 0, 'query' => '', ]); $parentID = $opt['parentID']; $query = $opt['query']; $showAll = false; if ($this->isAJAX()) { $listType = $opt['listType'] ?? $this->input->getpost('act', TYPE_STR); $parentID = $this->input->post('parent', TYPE_UINT); $query = $this->input->post('query', TYPE_STR); $showAll = $this->input->post('showAll', TYPE_BOOL); } $catData = function ($id, $iconSize) { return $this->model->catData($id, [ 'id','pid','title','keyword','landing_url', 'numlevel','numleft','numright', 'icon_' . $iconSize . ' as icon', 'items', 'subs', 'settings', ]); }; $url = function ($keyword, $landingUrl) use (&$query) { return $this->url('items.search', ['keyword' => $keyword, 'landing_url' => $landingUrl, 'q' => $query]); }; $rootID = static::CATS_ROOTID; $icon = $this->categoryIcon(); $iconLarge = $icon::BIG; $iconSmall = $icon::SMALL; switch ($listType) { case 'search': # фильтр категории $selectedID = 0; $showAll = Request::isPhone(); if ($parentID > $rootID) { $parentData = $catData($parentID, $iconLarge); if (!empty($parentData)) { if (!$parentData['subs']) { # в данной категории нет подкатегорий # формируем список подкатегорий ее parent-категории $parentData = $catData($parentData['pid'], $iconLarge); if (!empty($parentData)) { $selectedID = $parentID; $parentID = $parentData['id']; } } } } $data = $this->model->catsList($listType, $parentID, $iconLarge); if (!empty($data)) { $searchShowEmptyCats = $this->seoSearchFilterShowEmptyCats(); foreach ($data as $k => &$v) { if (! $v['items'] && ! $searchShowEmptyCats && $v['pid'] > $rootID) { unset($data[$k]); } $v['l'] = $url($v['k'], $v['lpu']); $v['i'] = $icon->url($v['id'], $v['i'], $iconLarge); $v['active'] = ($v['id'] == $selectedID); } unset($v); } $tplData = [ 'cats' => $data, 'query' => $query, 'showAll' => $showAll, 'langViewAll' => _t('filter', 'view all listings'), 'urlViewAll' => Listings::url('items.search', ['q' => $query]), 'langAll' => _t('filter', 'All listings'), ]; if ($parentID > $rootID) { if (!empty($parentData)) { $parentData['link'] = $url($parentData['keyword'], $parentData['landing_url']); $parentData['main'] = ($parentData['pid'] == $rootID); if ($parentData['main']) { $parentData['icon'] = $icon->url($parentData['id'], $parentData['icon'], $iconLarge); } else { # глубже второго уровня, получаем настройки основной категории $parentsID = $this->model->catParentsID($parentData, false); if (!empty($parentsID[1])) { $main = $this->model->catData($parentsID[1], [ 'id', 'icon_' . $iconLarge . ' as icon', ]); $parentData['icon'] = $icon->url($parentsID[1], $main['icon'], $iconLarge); } } $tplData['step'] = 2; $tplData['parent'] = $parentData; $data = $this->template('search/cats.desktop', $tplData); if ($this->isAJAX()) { return $this->ajaxResponseForm(['html' => $data]); } else { return $data; } } else { $this->errors->impossible(); return $this->ajaxResponseForm(['html' => '']); } } else { $geo = Geo::filter(); if (!empty($geo['id'])) { $total = $this->model->itemsCountByFilter(['cat_id' => 0, 'region_id' => $geo['id'], 'delivery' => 0, 'no_region' => 0], false, true, 60); $total += $this->model->itemsCountByFilter(['cat_id' => 0, 'region_id' => '0', 'delivery' => 0, 'no_region' => 1], false, true, 60); $countryID = Geo::regionCountry($geo['id']); if ($countryID) { $total += $this->model->itemsCountByFilter(['cat_id' => 0, 'region_id' => $countryID, 'delivery' => 1, 'no_region' => 0], false, true, 60); } } else { $total = $this->model->itemsCountByFilter(['cat_id' => 0, 'region_id' => 0, 'delivery' => 0, 'no_region' => 0], false, true, 60); } $tplData['step'] = 1; $tplData['total'] = $total; $tplData['showAll'] = false; return $this->template('search/cats.desktop', $tplData); } case 'form': # форма объявления: выбор категории if ($opt['init']) { /** * Формирование данных об основных категориях * для jListingsItemForm.init({catsMain:DATA}); */ $data = $this->model->catsList('form', $parentID, $iconSmall); if (! empty($data)) { foreach ($data as $k => $v) { $data[$k]['i'] = $icon->url($v['id'], $v['i'], $iconSmall); } } return $data; } $selectedID = 0; if ($parentID > $rootID) { $parentData = $catData($parentID, $iconSmall); if (!empty($parentData)) { if (!$parentData['subs']) { # в данной категории нет подкатегорий # формируем список подкатегорий ее parent-категории $parentData = $catData($parentData['pid'], $iconSmall); if (!empty($parentData)) { $selectedID = $parentID; $parentID = $parentData['id']; } } } } $catsList = $this->model->catsList($listType, $parentID, $iconSmall); if (!empty($catsList)) { foreach ($catsList as $k => $v) { $catsList[$k]['i'] = $icon->url($v['id'], $v['i'], $iconSmall); $catsList[$k]['active'] = ($v['id'] == $selectedID); } } if ($parentID > $rootID) { if (!empty($parentData)) { $parentData['main'] = ($parentData['pid'] == $rootID); if ($parentData['main']) { $parentData['icon'] = $icon->url($parentData['id'], $parentData['icon'], $iconSmall); } else { # глубже второго уровня, получаем иконку основной категории $parentsID = $this->model->catParentsID($parentData, false); if (!empty($parentsID[1])) { $main = $this->model->catData($parentsID[1], [ 'id', 'icon_' . $iconSmall . ' as icon', ]); $parentData['icon'] = $icon->url($parentsID[1], $main['icon'], $iconSmall); } } $data = ['cats' => $catsList, 'parent' => $parentData, 'step' => 2, 'showAll' => $showAll]; $data = $this->template('item/form.cat.select', $data); if ($this->isAJAX()) { return $this->ajaxResponseForm([ 'html' => $data, 'cats' => $catsList, 'pid' => $parentData['pid'], ]); } else { return $data; } } else { $this->errors->impossible(); return $this->ajaxResponseForm(['html' => '']); } } else { $opt['cats'] = $catsList; $opt['cat_id'] = $opt['cat_id'] ?? 0; $opt['step'] = 1; $opt['showAll'] = $showAll; if ($opt['cat_id']) { if (empty($opt['cat_data'])) { $opt['cat_data'] = $this->itemFormByCategory($opt['cat_id']); } if (empty($opt['cat_path'])) { $opt['cat_path'] = []; $catPath = $this->model->catParentsData($opt['cat_id'], ['id','title','icon_s']); foreach ($catPath as $v) { $opt['cat_path'][$v['id']] = $v['title']; } $catParent = reset($catPath); $opt['cat_data']['icon'] = $icon->url($catParent['id'], $catParent['icon_s'], $iconSmall); } } return $this->template('item/form.cat.select', $opt); } case 'suggest': # форма объявления: выбор категории if (empty($query)) { return $this->ajaxResponseForm([]); } $pre = $this->input->post('pre', TYPE_UINT); $fields = ['id', 'pid', 'title', 'numlevel', 'keyword', 'landing_url', 'subs', 'icon_' . $iconSmall . ' as i']; $cats = $this->app->filter('listings.category.select.suggest', [], [ 'query' => $query, 'pre' => $pre, 'fields' => $fields, 'parentID' => $parentID, ]); if (empty($cats)) { if ($pre) { return $this->ajaxResponseForm([]); } $filter = [ 'enabled' => 1, ':sub' => 'C.numright - C.numleft = 1', ':ft_q' => $query, ]; if ($parentID != static::CATS_ROOTID) { $cat = $this->model->catData($parentID, ['numleft', 'numright']); if (! empty($cat['numleft']) && ! empty($cat['numright'])) { $filter[':pid'] = [ 'C.numleft >= :left AND C.numright <= :right', ':left' => $cat['numleft'], ':right' => $cat['numright'] ]; } } $cats = $this->model->catsDataByFilter($filter, $fields, ['limit' => 15]); } $parents = func::array_transparent($cats, 'id', true); $pids = $parents; while (! empty($pids)) { $ids = []; foreach ($pids as $v) { $ids[ $v['pid'] ] = 1; } if (empty($ids)) { break; } $pids = $this->model->catsDataByFilter([ 'id' => array_keys($ids), 'numlevel' => ['>', 0], ], $fields); $pids = func::array_transparent($pids, 'id', true); $parents = $parents + $pids; } foreach ($cats as & $v) { $pid = $v['pid']; $p = []; $v['mid'] = static::CATS_ROOTID; $v['cats'] = [['id' => $v['id'], 't' => $v['title']]]; while (isset($parents[$pid])) { $a = $parents[$pid]; $pid = $a['pid']; $p[$pid] = $a['title']; if (! empty($a['i']) && empty($v['i'])) { $v['i'] = $icon->url($a['id'], $a['i'], $iconSmall); } $v['cats'][] = ['id' => $a['id'], 't' => $a['title']]; $v['mid'] = $a['id']; } $v['parents'] = array_reverse($p, true); $v['cats'] = array_reverse($v['cats']); } unset($v); $data = [ 'step' => 3, 'cats' => $cats, 'pre' => $pre, ]; return $this->ajaxResponseForm(['html' => $this->template('item/form.cat.select', $data)]); } } /** * Быстрый поиск объявлений по строке */ public function searchSuggest() { $limit = $this->config('listings.search.suggest.items.limit', 3, TYPE_UINT); if (! $limit) { return $this->ajaxResponseForm(['items' => [], 'cnt' => 0]); } $query = $this->input->post('q', [TYPE_NOTAGS, 'len' => 80]); $query = $this->input->cleanSearchString($query, 80); $f = $this->input->postm([ 'c' => TYPE_UINT, # категория 'ct' => TYPE_UINT, # тип категории ]); $data = []; $filter = [ 'is_publicated' => 1, 'status' => static::STATUS_PUBLICATED, ]; if ($f['c'] > 0 && $this->config('listings.search.suggest.items.category', false, TYPE_BOOL)) { $filter[':cat-filter'] = $f['c']; } $regionID = Geo::filter('id'); # user if ($regionID > 0) { $filter[':region-filter'] = $regionID; } $filter[':query'] = $query; $data['items'] = $this->model->itemsQuickSearch($filter, [ 'limit' => $limit, 'orderBy' => 'publicated_order DESC', ]); $data['cnt'] = sizeof($data['items']); foreach ($data['items'] as &$v) { if (sizeof($v['img']) > 4) { $v['img'] = array_slice($v['img'], 0, 4); } } unset($v); $data['items'] = $this->template('search/suggest', $data); $data = $this->app->filter('listings.search.suggest', $data, ['query' => $query, 'f' => $f]); return $this->ajaxResponseForm($data); } /** * Страница результата добавления / редактирование / управления ОБ * @return string */ public function status() { $action = $this->input->get('action', TYPE_STR); $id = $this->input->get('id', TYPE_STR); return $this->itemStatus($action, $id); } /** * Страница результата добавления / редактирование / управления ОБ * @param string $state ключ результата * @param int $itemID ID объявления * @param array $data дополнительные данные * @return string|mixed HTML */ protected function itemStatus($state, $itemID, array $data = []) { $title = ''; do { # получаем данные об объявлении if (!$itemID) { $state = false; break; } $itemData = $this->model->itemData($itemID, [ 'id','user_id','company_id','title','link', 'status','status_prev','activate_key', 'svc', 'cat_id1','cat_id','cat_id_virtual', ]); if (!empty($itemData['cat_id_virtual'])) { $itemData['cat_id'] = $itemData['cat_id_virtual']; } if (empty($itemData)) { $state = false; break; }; $params = $this->input->getm([ 'ak' => TYPE_STR, 'activated' => TYPE_STR, 'pub' => TYPE_BOOL, 'from' => TYPE_STR ]); $redirect = $this->app->filter('listings.item.status.redirect', false, [ 'params' => & $params, 'state' => $state, 'itemID' => $itemID, 'data' => $data, 'itemData' => $itemData, ]); if ($redirect) { return $this->redirect($redirect); } if ($state == 'new' || $state == 'edit') { # проверяем владельца $userID = User::id(); $itemUserID = $itemData['user_id']; if ($userID && $itemUserID != $userID) { break; } if ($itemData['status'] == static::STATUS_NOTACTIVATED) { # проверка корректности перехода, по совпадению части подстроки ключа активации $activateCodePart = $params['ak']; if (stripos($itemData['activate_key'], $activateCodePart) === false) { return $this->redirect()->route('index'); # не совпадают } # Шаг активации объявления + телефона $registerPhone = Users::registerPhone([Users::REGISTER_TYPE_BOTH, Users::REGISTER_TYPE_PHONE]); $data['new_user'] = false; if (!$userID) { # Запрещаем изменение номера телефона в случае если объявление добавляет # неавторизованный пользователь от имени зарегистрированного с наличием одного и более активированных ОБ $nUserItemsCounter = $this->model->itemsCount(['user_id' => $itemUserID, 'status' => ['!=',static::STATUS_NOTACTIVATED]]); if ($nUserItemsCounter > 1) { $data['new_user'] = true; } } if ($registerPhone && $this->isAJAX()) { $userData = Users::model()->userData($itemUserID, [ 'email', 'name', 'activated', 'activate_key', 'phone_number', 'phone_number_verified', ]); if (!$this->security->validateReferer() || empty($userData)) { $this->errors->reloadPage(); return $this->ajaxResponseForm(); } $response = []; $userPhone = $userData['phone_number']; $confirmation = new \modules\users\views\register\PhoneConfirmation(); switch ($this->input->postget('act')) { # Проверка кода подтверждения case 'code-validate': { $code = $this->input->postget('code', TYPE_NOTAGS); if ($code != $userData['activate_key'] && mb_strtolower($code) !== $userData['activate_key']) { $this->errors->set(_t('users', 'Incorrect verification code'), 'phone'); break; } # Активируем аккаунт + объявления if (empty($userData['activated'])) { $activated = Users::userActivate($itemUserID, [ 'verifyPhone' => true, # Generate password because no password input in item add form 'password' => ($password = $this->security->generatePassword()), ]); if ($activated) { # Авторизуем if (! $userID) { Users::authById($itemUserID); } # Отправляем письмо об успешной регистрации Users::sendPhoneRegistrationNotification($itemUserID, $userData['email'], $userData['phone_number'], $password); } else { $this->errors->set(_t('users', 'Registration error, contact the administrator')); break; } } else { # Активируем объявления пользователя $this->onUserRegistered($itemUserID); # Авторизуем if (! $userID) { Users::authById($itemUserID); } # Помечаем успешное подтверждение номера телефона if (! $userData['phone_number_verified']) { Users::model()->userSave($itemUserID, ['phone_number_verified' => 1]); } } $response['redirect'] = static::url('item.status', [ 'action' => 'new', 'id' => $itemID, 'activated' => 1, ]); } break; # Повторная отправка кода подтверждения case 'code-resend': { $confirmation->codeResend($itemUserID, $userPhone); } break; # Смена номера телефона case 'phone-change': { if ($data['new_user']) { $this->errors->reloadPage(); break; } $phone = $this->input->postget('phone', [TYPE_NOTAGS, 'len' => 30]); if ($confirmation->phoneChange($itemUserID, $phone, $userPhone)) { $response['phone'] = '+' . $phone; } } break; } return $this->ajaxResponseForm($response); } $state = 'new.notactivated' . ($registerPhone ? '.phone' : ''); $title = _t('listings', 'Thank you! It remains only to activate the listing!'); } elseif ($itemData['status'] == static::STATUS_BLOCKED) { $state = 'edit.blocked.wait'; $title = _t('listings', 'You have successfully edited the listing!'); } else { if ($state == 'new') { $state = 'new.publicated'; if (!empty($data['activated']) || $params['activated']) { # активировали $title = _t('listings', 'You have successfully activated the listing!'); $this->app->hook('listings.item.status.new.activated', $itemID, $itemData); } else { $title = _t('listings', 'You have successfully created the listing!'); $this->app->hook('listings.item.status.new.created', $itemID, $itemData); } } else { # изменился статус публикации if ($params['pub']) { if ( $itemData['status'] == static::STATUS_PUBLICATED && $itemData['status_prev'] == static::STATUS_PUBLICATED_OUT ) { # опубликовали $state = 'edit.publicated'; $title = _t('listings', 'You have successfully published the listing!'); } elseif ( $itemData['status'] == static::STATUS_PUBLICATED_OUT && $itemData['status_prev'] == static::STATUS_PUBLICATED ) { # сняли с публикации $state = 'edit.publicated.out'; $title = _t('listings', 'You have successfully removed the listing from the published!'); } else { $state = 'edit.normal'; $title = _t('listings', 'You have successfully edited the listing!'); } } else { # отредактировали без изменения статуса $state = 'edit.normal'; $title = _t('listings', 'You have successfully edited the listing!'); } } } } elseif ($state == 'promote.success') { $title = _t('listings', 'Promotion of the listing'); } else { return $this->errors->error404(); } } while (false); if ($state === false) { return $this->errors->error404(); } $data['user'] = Users::model()->userData($itemData['user_id'], ['email','phone_number','phone_number_verified','activated','name']); $data['state'] = $state; $data['item'] = & $itemData; $data['back'] = Request::referer(Url::to()); $data['from'] = $params['from']; $data['link_mylist'] = Listings::url('my.items', ['from' => 'add']); if (! empty($itemData['company_id']) && bff::moduleExists('business')) { $data['link_mylist'] = Business::url('my.listings', ['from' => 'add']); } $this->app->setMeta($title); $this->seo()->robotsIndex(false); return $this->showShortPage($title, $this->template('item/status', $data)); } /** * Продвижение объявления */ public function promote() { if (! bff::servicesEnabled('listings')) { return $this->errors->error404(); } $data = []; $pageTitle = _t('listings', 'Promotion of the listing'); $from = $this->input->postget('from', TYPE_NOTAGS); $userID = User::id(); $svc = $this->input->postget('svc', TYPE_STR); $itemID = $this->input->getpost('id', TYPE_UINT); # ID объявления if ($this->input->get('success')) { return $this->itemStatus('promote.success', $itemID); } $itemData = $this->model->itemsList(['id' => $itemID], false, ['fields' => ['user_id','company_id','status','blocked_reason','cat_id','geo_city']]); $itemData = reset($itemData); if (empty($itemData['imgs'])) { $images = $this->itemImages(); $itemData['img_s'] = $images->urlDefault($images::szSmall); $itemData['img_m'] = $images->urlDefault($images::szMedium); } $payWaysList = Bills::getPayWaysList(!empty($userID), ['promotePage' => true]); $manager = $this->itemServices(); $userBalance = User::balance(); if ($this->isPOST()) { $response = []; do { $ps = $this->input->getpost('ps', TYPE_STR); if (! $ps || !array_key_exists($ps, $payWaysList)) { $this->errors->reloadPage(); break; } if ( ! $itemID || empty($itemData) || in_array($itemData['status'], [ static::STATUS_BLOCKED, static::STATUS_DELETED, static::STATUS_PUBLICATED_OUT, ]) ) { $this->errors->reloadPage(); break; } if (empty($svc)) { $this->errors->set(_t('listings', 'Select Service')); break; } $response = $manager->activateOrPay($svc, $itemID, $userID, $ps, [ 'redirect' => $this->url('item.promote', [ 'id' => $itemID, 'success' => 1, 'from' => $from, ]), ]); } while (false); return $this->ajaxResponseForm($response); } # SEO $this->seo()->robotsIndex(false); $this->app->setMeta($pageTitle); if (!$itemID || empty($itemData)) { return $this->showForbidden($pageTitle, _t('listings', 'Listing not found, or the link is incorrect')); } # Проверка доступности возможности продвижения if ($from !== 'new' && !$this->itemViewPromoteAvailable($this->isItemOwner($itemID, $itemData['user_id']))) { return $this->showForbidden($pageTitle, _t('listings', 'Listing not found, or the link is incorrect')); } # проверяем статус ОБ if ($itemData['status'] == static::STATUS_DELETED) { return $this->showForbidden($pageTitle, _t('listings', 'Listing has been removed')); } elseif ($itemData['status'] == static::STATUS_BLOCKED) { return $this->showForbidden( $pageTitle, _t('listings', 'Listing was blocked by moderator, reason: [reason]', [ 'reason' => $itemData['blocked_reason'], ]) ); } elseif ($itemData['status'] == static::STATUS_PUBLICATED_OUT) { return $this->showForbidden( $pageTitle, _t('listings', 'You need to publish a listing for further promotion.') ); } $data['item'] = & $itemData; # SEO: корректируем ссылку $this->urlCorrection($this->url('item.promote')); # способы оплаты $data['curr'] = Currency::default(); $data['ps'] = &$payWaysList; reset($payWaysList); $data['ps_active_key'] = key($payWaysList); foreach ($payWaysList as $k => &$v) { $v['active'] = ($k == $data['ps_active_key']); } unset($v); # список услуг $services = $manager->getItemPromoteServices($itemID, $userID, $itemData); foreach ($services as & $v) { $v['active'] = $v['service_key'] == $svc; if (! empty($v['disabled'])) { $v['active'] = false; } } unset($v); $data['services'] = & $services; $data['user_balance'] = & $userBalance; $data['svc'] = $svc; $data['from'] = $from; View::setPageData([ 'listings_promote_id' => $itemID, 'listings_promote_data' => &$itemData, ]); return $this->template('item/promote', $data); } /** * Активация объявления * @return string HTML */ public function activate() { $pageTitle = _t('listings', 'Listing activation'); $activateKey = $this->input->get('c', TYPE_STR); # ключ активации + ID объявления list($activateKey, $itemID) = explode('_', (!empty($activateKey) && (strpos($activateKey, '_') !== false) ? $activateKey : '_'), 2); $itemID = $this->input->clean($itemID, TYPE_UINT); # SEO $this->seo()->robotsIndex(false); $this->app->setMeta($pageTitle); $data = $this->model->itemData($itemID, [ 'user_id','cat_id','company_id','status','activate_key', 'activate_expire','blocked_reason','publicated_period', ]); if (empty($data)) { # не нашли такого объявления return $this->showForbidden( $pageTitle, _t('listings', 'Listing not found. Your activation link may have expired.', [ 'link_add' => 'href="' . $this->url('item.add') . '"', ]) ); } if ($data['activate_key'] != $activateKey || strtotime($data['activate_expire']) < microtime(true)) { # ключ активации неверный или срок его действия истек return $this->showForbidden( $pageTitle, _t('listings', 'The activation link has expired or it is not valid. Please add a new listing.', [ 'link_add' => 'href="' . $this->url('item.add') . '"', ]) ); } if ($data['status'] == static::STATUS_DELETED) { # объявление было удалено return $this->showForbidden($pageTitle, _t('listings', 'Listing has been removed')); } if ($data['status'] == static::STATUS_BLOCKED) { # объявление было заблокировано return $this->showForbidden( $pageTitle, _t('listings', 'Listing was blocked by moderator, reason: [reason]', [ 'reason' => $data['blocked_reason'], ]) ); } # 2. Получаем данные о пользователе: $userData = Users::model()->userData($data['user_id'], [ 'user_id','email','name','activated','blocked','blocked_reason', ]); if (empty($userData)) { # попытка активации объявления при отсутствующем профиле пользователя (публиковавшего объявление) return $this->showForbidden($pageTitle, _t('listings', 'Activation error, contact support.')); } $userID = $userData['user_id']; # аккаунт заблокирован if ($userData['blocked']) { return $this->showForbidden( $pageTitle, _t('listings', 'Your account is blocked. For detailed information, please contact support.') ); } # активируем аккаунт if (! $userData['activated']) { $userActivated = Users::userActivate($userID, [ 'verifyEmail' => true, 'password' => ($password = $this->security->generatePassword()), ]); if ($userActivated) { Users::sendAutoRegistrationNotification($userID, $userData['name'], $userData['email'], $password); } } else { $this->activateUserItems($userID, $itemID); } # авторизуем, если текущий пользователь неавторизован if (User::guest()) { Users::authById($userID); } return $this->itemStatus('new', $itemID, ['activated' => true]); } /** * Список категорий для страницы "Карта сайта" */ public function catsListSitemap() { $iconSize = CategoryIcon::SMALL; $data = $this->model->catsListSitemap($iconSize); if (! empty($data)) { foreach ($data as &$v) { $v['link'] = $this->url('items.search', ['keyword' => $v['keyword'], 'landing_url' => $v['landing_url']]); } unset($v); $data = $this->db->transformRowsToTree($data, 'id', 'pid', 'subs'); $icon = $this->categoryIcon(0); foreach ($data as &$v) { $v['icon'] = $icon->url($v['id'], $v['icon'], $iconSize); } unset($v); } return $data; } public function ajax() { $userID = User::id(); $response = []; switch ($this->input->getpost('act', TYPE_STR)) { # форма добавления/редактирования (в зависимости от настроек категорий) case 'item-form-cat': { $categoryID = $this->input->post('id', TYPE_UINT); $response['id'] = $categoryID; $data = $this->itemFormByCategory($categoryID); if (empty($data)) { $this->errors->unknownRecord(); break; } else { if ($data['types'] === false) { $data['types'] = []; } $data['types'] = HTML::selectOptions($data['types'], 0, false, 'id', 'title'); } $response = array_merge($data, $response); } break; # стоимость услуг для формы добавления/редактирования case 'item-form-svc-block': { if (! bff::servicesEnabled()) { break; } $response['html'] = $this->app->filter('listings.item.form.svc.block', ''); } break; # дин. свойства: child-свойства case 'dp-child': { $p = $this->input->postm([ 'dp_id' => TYPE_UINT, # ID parent-дин.свойства 'dp_value' => TYPE_UINT, # ID выбранного значения parent-дин.свойства 'name_prefix' => TYPE_NOTAGS, # Префикс для name 'search' => TYPE_BOOL, # true - форма поиска ОБ, false - форма доб/ред ОБ 'format' => TYPE_STR, # требуемый формат: 'f-desktop', 'f-phone', '' ]); if (empty($p['dp_id']) && empty($p['dp_value'])) { $this->errors->impossible(); } else { $isFilter = (!empty($p['format'])); $data = $this->dp()->formChildByParentIDValue($p['dp_id'], $p['dp_value'], ['name' => $p['name_prefix'], 'class' => 'form-control'], $p['search'], ($isFilter ? false : 'form.child')); if ($isFilter && !empty($data)) { switch ($p['format']) { case 'f-desktop': case 'f-phone': { $response = [ 'id' => $data['id'], 'df' => $data['data_field'], 'multi' => $data['multi'], ]; } break; } } else { $response['form'] = $data; } } } break; # избранные объявления (добавление / удаление) - для авторизованных # для неавторизованных - процесс выполняется средствами javascript+cookie case 'item-fav': { $itemID = $this->input->post('id', TYPE_UINT); if (!$itemID || !$userID || !$this->isRequestValid()) { $this->errors->reloadPage(); break; } $result = $this->favorites()->add($itemID, $userID); if ($result !== false) { $response['added'] = $result['favorite']; $response['cnt'] = $result['total']; } } break; # смена статуса объявления case 'item-status': { $response['alerts'] = []; $itemID = $this->input->postget('id', TYPE_UINT); if (!$itemID || !$userID || !$this->isRequestValid()) { $this->errors->reloadPage(); break; } $data = $this->model->itemData($itemID, [ 'user_id','company_id','cat_id','status', 'publicated_to','publicated_order','publicated_period', ]); if (empty($data)) { $this->errors->reloadPage(); break; } if (!$this->isItemOwner($itemID, $data['user_id'])) { $this->errors->set(_t('listings', 'You are not the owner of this listing.')); break; } $action = $this->input->getpost('status', TYPE_STR); if (! $this->errors->no('listings.ajax.item-status', ['action' => $action, 'id' => $itemID, 'data' => &$data])) { break; } switch ($action) { case 'unpublicate': # снятие с публикации { if ($data['status'] != static::STATUS_PUBLICATED) { $this->errors->reloadPage(); break; } $res = $this->model->itemSave($itemID, [ 'status' => static::STATUS_PUBLICATED_OUT, 'status_prev' => $data['status'], 'publicated_to' => $this->db->now(), ]); if (empty($res)) { $this->errors->reloadPage(); } else { $response['message'] = _t('listings', 'Listing was successfully removed from the published'); } } break; case 'publicate': # публикация { $res = $this->itemPublicate($itemID); if (empty($res)) { if ($this->errors->no()) { $this->errors->reloadPage(); } } else { $response['message'] = _t('listings', 'Listing was successfully published'); } } break; case 'refresh': # продление публикации { if ($data['status'] != static::STATUS_PUBLICATED) { $this->errors->reloadPage(); break; } if ( $this->limitsExceed([ 'user_id' => $userID, 'company_id' => $data['company_id'], 'cat_id' => $data['cat_id'], ]) ) { break; } # от даты завершения публикации $res = $this->model->itemSave($itemID, [ 'publicated_to' => $this->itemPublicationPeriod()->refreshTo($data['publicated_to']), ]); if (empty($res)) { if ($this->errors->no()) { $this->errors->reloadPage(); } } else { $response['message'] = _t('listings', 'The time of the listing was successfully extended'); } } break; case 'delete': # удаление { $is_soon_left = (strtotime($data['publicated_to']) - time()) < static::PUBLICATION_SOON_LEFT; if ($data['status'] == static::STATUS_PUBLICATED && ! $is_soon_left) { $this->errors->set(_t('listings', 'To remove a listing, you must remove it from the published')); break; } $response['message'] = _t('listings', 'Listing was successfully deleted'); $response['redirect'] = $this->url('my.items'); if ($data['status'] == static::STATUS_DELETED) { break; } $res = $this->model->itemSave($itemID, [ # помечаем как удаленное + снимаем с публикации 'deleted' => 1, 'status' => static::STATUS_DELETED, 'status_prev' => $data['status'], 'publicated_to' => $this->db->now(), ]); if (empty($res)) { $this->errors->set(_t('listings', 'Could not remove listing, maybe this listing has already been deleted.')); } } break; } # проверим превышение лимита, для вывода алерта в кабинете $this->limitsExceed([ 'user_id' => $userID, 'company_id' => $data['company_id'], ], [ 'silent' => true, 'getAlert' => function ($html) use (&$response) { $response['alerts'][] = $html; } ]); $response['alerts'] = join('', $response['alerts']); } break; case 'item-svc-up-free': # бесплатное понятие объявления { $itemID = $this->input->post('id', TYPE_UINT); $up = $this->itemServices('up'); if ( ! $up || ! $itemID || ! $userID || ! $this->isRequestValid() || ! $this->isItemOwner($itemID) ) { $this->errors->reloadPage(); break; } $response['message'] = $up->upFree($itemID, false); } break; case 'item-svc-up-auto-form': { $itemID = $this->input->post('id', TYPE_UINT); $up = $this->itemServices('up'); if (! $up || ! $itemID || ! $userID || !$this->isRequestValid()) { $this->errors->reloadPage(); break; } $data = []; $up->setActivationSettings($itemID, $userID); $up->onActivationForm($data, ['form' => true]); if (empty($data['form'])) { $this->errors->impossible(); break; } $response['html'] = $data['form']; } break; default: { $this->errors->impossible(); } break; } return $this->ajaxResponseForm($response); } /** * RSS Лента * @return \bff\http\Response */ public function rss() { $category = $this->input->get('cat', TYPE_UINT); $region = $this->input->get('region', TYPE_UINT); $lang = $this->locale->current(); # SEO: проверка на обязательный завершающий слеш $this->seo()->urlCorrectionEndSlash(); if (! $this->rssEnabled() || ! $category) { return $this->errors->error404(); } $catData = $this->model->catData($category, ['enabled']); if (empty($catData['enabled'])) { return $this->errors->error404(); } # Формируем + кешируем $minutes = $this->config('listings.rss.cache', 15, TYPE_UINT); if ($minutes <= 0) { $minutes = 15; } $data = Cache::remember(join('-', ['rss', $category, $region, $lang]), ($minutes * 60), function () use ($category, $region, $lang) { return $this->rssGenerator($category, $region, $lang); }); return Response::text($data, 200, [ 'Content-Type' => 'application/rss+xml; charset=UTF-8', 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT', ]); } /** * Формируем RSS ленту * @param int $categoryID ID Категории * @param int $regionID ID региона * @param string $lng Ключ языка * @return string */ protected function rssGenerator($categoryID, $regionID, $lng) { $sql = [ 'is_publicated' => 1, 'status' => static::STATUS_PUBLICATED, ]; if ($categoryID > 0) { $sql[':cat-filter'] = $categoryID; } if ($regionID) { $sql[':region-filter'] = $regionID; } $limit = $this->config('listings.rss.limit', 30, TYPE_UINT); if ($limit <= 0) { $limit = 30; } $fields = ['I.descr']; for ($i = $this->catsDepthLimit(); $i > 0; $i--) { $fields[] = 'I.cat_id' . $i; } $data = $this->model->itemsList($sql, false, [ 'context' => 'rss', 'orderBy' => 'publicated_order DESC, id DESC', 'limit' => $limit, 'favs' => false, 'lang' => $lng, 'fields' => $fields, ]); $items = []; if (! empty($data)) { $cats = []; foreach ($data as $v) { for ($i = $this->catsDepthLimit(); $i > 0; $i--) { if ($v['cat_id' . $i] && !in_array($v['cat_id' . $i], $cats)) { $cats[] = $v['cat_id' . $i]; } } } $cats = $this->model->catsDataByFilter(['id' => $cats], ['id','pid','title'], ['lang' => $lng]); $cats = func::array_transparent($cats, 'id', true); foreach ($data as $v) { $catTitles = []; for ($i = 1; $i <= $this->catsDepthLimit(); $i++) { if ($v['cat_id' . $i] && isset($cats[ $v['cat_id' . $i] ])) { $catTitles[] = $cats[ $v['cat_id' . $i] ]['title']; } } $items[] = ' ' . strip_tags($v['title']) . ' ' . $v['link'] . ' ' . htmlspecialchars($v['descr']) . ' ' . $v['link'] . ' ' . strip_tags(join(' / ', $catTitles)) . ' ' . date('D, j M Y G:i:s O', strtotime($v['publicated'])) . ' '; } } $title = _t('listings', 'Listings on [site]', ['site' => Site::title('listings.rss')]); if ($categoryID > 0) { $catTitles = []; $parents = $this->model->catParentsData($categoryID); foreach ($parents as $v) { $catTitles[] = $v['title']; } $title .= ' (' . join(' / ', $catTitles) . ')'; } $lang = $this->locale->getLanguageSettings($lng, 'locale'); $lang = str_replace('_', '-', mb_strtolower($lang)); $url = Url::to('/', ['lang' => $lng]); $description = '' . $title . ' ' . $url . ' ' . $title . ' ' . $lang . ' ' . date('D, j M Y G:i:s O') . ' ' . date('D, j M Y G:i:s O') . ' ' . Site::logoURL('header') . ' ' . $title . ' ' . $url . ' '; return strtr('{description}{items}', [ '{description}' => $description, '{items}' => join('', $items), ]); } /** * Обработчик действий пользователя в блоке комментариев к объявлению * @return \bff\http\Response */ public function comments() { if (! $this->isAJAX()) { return $this->errors->error404(); } return $this->itemComments()->onUserAction( User::id(), $this->input->getpost('act', TYPE_STR) ); } /** * Сбор метрик для мониторинга * @param $metrics */ public function _metrics($metrics) { $sphinx = $this->itemsSearchSphinx(); $enabled = $sphinx::enabled(); $metrics->add('sphinx_enabled') ->help('Sphinx Enabled') ->value((int)$enabled) ; if (! $enabled) { return; } $sphinx->metrics($metrics); $isRunning = $sphinx->isRunning(); $metrics->add('sphinx_listings_indexing_up') ->help('Listings Sphinx Indexing') ->value((int)$isRunning) ; } }