scope('items-listing', _t('@listings', 'Listings List View')) ->method('listing') ->method('ajax')->actions(['item-info', 'item-user']) ->scope('items-edit', _t('@listings', 'Listings Management (Add / Edit / Delete)')) ->method('listing') ->action('delete') ->action('mass', function () { return in_array($this->input->post('action', TYPE_STR), [ 'delete', 'delete1', ], true); }) ->method('ajax')->actions(['item-form-cat', 'category-autocomplete']) ->scope('items-moderate', _t('@listings', 'Listings Moderation (Blocking / Approval / Renewal / Activation)')) ->method('ajax')->actions(['item-block', 'item-activate', 'item-approve', 'item-refresh', 'item-unpublicate']) ->method('listing') ->action('mass', function () { return in_array($this->input->post('action', TYPE_STR), [ 'refresh', 'approve', 'unpublicate', 'block', ], true); }) ->scope('items-comments', _t('@listings', 'Comments Management')) ->methods(['comments_ajax', 'comments_mod']) ->scope('claims-listing', _t('@listings', 'Claims List View')) ->method('claims') ->scope('claims-edit', _t('@listings', 'Claims Management (Moderate / Delete)')) ->method('claims')->actions(['delete', 'viewed']) ->scope('categories', _t('@listings', 'Category Management')) ->methods(['categories_listing', 'categories_packetActions', 'categories_add', 'categories_edit']) ->methods(['dynprops_listing', 'dynprops_action']) # dynprops ->method('ajax')->action(['category-options', 'category-autocomplete']) ->scope('types', _t('@listings', 'Category Types Management')) ->methods(['types_listing', 'types']) ->scope('svc', _t('@listings', 'Services Management')) ->methods([]) ->scope('settings', _t('@listings', 'Additional Settings')) ->method('settingsSystemWordforms') ->seoTemplates() ; } # ------------------------------------------------------------------------------------------------------------------------------- # объявления public function listing() { if (!$this->haveAccessTo('items-listing')) { return $this->showAccessDenied(); } $form = AdminHelpers::form($this, 'admin/form', ['id' => 'listings.items']) ->onSubmit(function ($id, $p) { if (!$this->haveAccessTo('items-edit')) { $this->showAccessDenied(); return false; } if ($id) { $itemData = $this->model->itemData($id); } else { if ($p['data']['user_id']) { $userData = Users::model()->userData($p['data']['user_id'], array('name', 'phones', 'contacts')); foreach ($userData as $k => $v) { if (isset($p['data'][$k])) { continue; } $p['data'][$k] = $v; } } } # виртуальная категория => реальная $catID = $this->model->catToReal($p['data']['cat_id']); $p['data']['cat_id_virtual'] = ($catID != $p['data']['cat_id'] ? $p['data']['cat_id'] : null); # ? $p['data']['cat_id'] = $catID; # ? $p['data'] = $this->validateItemData($p['data'], [ 'adminPanel' => true, 'id' => $id, 'afterValidation' => function (&$data, $validationForm) use ($id) { /** @var \modules\listings\views\ItemForm $validationForm */ if (! $id) { $validationForm->submitAddPublicated($data); } }, ]); if ($id) { if ($itemData['moderated'] == 1) { $p['data']['moderated'] = 1; # Если сохраняем промодерированное объявления, то обновим промодерированные данные } } else { $p['data']['status'] = static::STATUS_PUBLICATED; $p['data']['moderated'] = 1; } }); $this->app->hook('listings.admin.listing.form', $form); $form ->onLoad(function ($id) { $data = $this->model->itemData($id, [], true); $langData = $this->model->itemLangData($id); $lang = $data['lang'] ?? $this->locale->current(); foreach ($langData as $f => $value) { # use main language data for translatable fields if (array_key_exists($f, $this->model->langItem)) { if (! static::translate()) { continue; } $langData[$f][$lang] = $data[$f] ?? ''; } $data[$f] = $langData[$f]; } return $data; }) ->onSave(function ($id, $data) { return $this->model->itemSave($id, $data); }, false) ; $list = AdminHelpers::list($this, 'admin/listing', ['id' => 'listings.items']) ->perPageDefault($this->config('listings.admin.items.list.limit', 20, TYPE_UINT)) ->formAdd($form); $this->app->hook('listings.admin.listing.list', $list); $list->onDelete(function ($id) { if (!$this->haveAccessTo('items-edit')) { $this->errors->accessDenied(); return; } $this->itemDelete($id); }); $list->onFordev(_t('@listings', 'Rebuild All Listings Links'), function () { if (!$this->isAdminFordev()) { return $this->showAccessDenied(); } $data = []; $cronManager = $this->app->cronManager(); $error = 0; if ($cronManager->isEnabled()) { $cronManager->executeOnce('listings', 'itemsLinksRebuild'); $data['cronManager'] = 1; $msg = _t('@listings', 'The task is processing and will be completed in a few minutes automatically'); } else { $error = 1; $msg = _t('@listings', 'Cron Manager was not started'); } $form = AdminHelpers::form($this) ->tab('', '', ['stretch' => true, 'hidden' => true]) ->alert($msg, $error ? 'error' : 'info', false); return $form->view(); }, true, 'icon-check'); $list->onFordev(_t('@listings', 'Convert All Prices to Default Currency'), function () { if (!$this->isAdminFordev()) { return $this->showAccessDenied(); } $this->model->itemsDefaultCurrency(); return $this->adminRedirect(Errors::SUCCESS); }, true, 'icon-share-alt'); $filter = $this->listingFilter($list); $orderBy = $list->order('id-desc', true); $this->app->hook('listings.admin.listing.filter', [ 'list' => $list, 'filter' => & $filter, 'orderBy' => & $orderBy, ]); $list->massActionsHandler(function ($list, $opts = []) use (&$filter) { if (empty($opts['action'])) { $this->errors->impossible(); return []; } if ($list !== 'all') { $filter['id'] = $list; } $mass = $opts['form'] ?? []; $mass['action'] = $opts['action']; $mass['sql'] = $filter; $count = $this->model->itemsListing($filter, true); if ($count > 100) { $mass['count'] = $count; $this->app->cronManager()->executeOnce('listings', 'cronAdminMassAction', $mass, md5(json_encode($mass))); $response = [ 'message' => _t('@listings', 'The bulk operation is scheduled and will be completed within a few minutes.'), ]; } else { $response = $this->cronAdminMassAction($mass); } return $response; }); $list->total($this->model->itemsListing($filter, true)); $opts = [ 'limit' => $list->getLimit(), 'offset' => $list->getOffset(), 'orderBy' => $orderBy, ]; $list->rows($this->model->itemsListing($filter, false, $opts)); return $list->view(); } /** * @param BlockList $list * @return array */ protected function listingFilter($list) { $filter = []; switch ($list->tab()) { case '0': # Опубликованные $filter['is_publicated'] = 1; $filter['status'] = static::STATUS_PUBLICATED; break; case 2: # Снятые с публикации $filter['is_publicated'] = 0; $filter['status'] = static::STATUS_PUBLICATED_OUT; break; case 3: # На модерации $filter['is_moderating'] = 1; $list->perPageDefault($this->config('listings.admin.items.list.moderate.limit', $list->getLimit(), TYPE_UINT)); if ($list->filter('moderate_list') == 1) { # отредактированные $filter['moderated'] = ['>', 1]; } elseif ($list->filter('moderate_list') == 2) { # импортированные $filter['import'] = ['>', 0]; } break; case 4: # Неактивированные $filter['is_publicated'] = 0; $filter['status'] = static::STATUS_NOTACTIVATED; break; case 5: # Заблокированные $filter['is_publicated'] = 0; $filter['status'] = static::STATUS_BLOCKED; break; case 6: # Удаленные $filter['is_publicated'] = 0; $filter['status'] = static::STATUS_DELETED; break; case 7: # Все break; } if (($cat = $list->filter('cat')) > 0) { $filter[':cat-filter'] = $cat; } $userID = 0; if ($uid = $list->filter('uid')) { $userFilter = []; if ($this->input->isEmail($uid)) { $userFilter['email'] = $uid; } elseif (mb_substr($uid, 0, 1) == '+') { if ($this->input->isPhoneNumber($uid, ['codeFilter' => false])) { $userFilter['phone_number'] = $uid; } } elseif (is_numeric($uid)) { $userFilter['user_id'] = $uid; } if (!empty($userFilter)) { $userData = Users::model()->userDataByFilter($userFilter, ['user_id','company_id']); if (! empty($userData['user_id'])) { $userID = intval($userData['user_id']); $filter['user_id'] = $userID; if ($userData['company_id'] > 0) { $filter['company_id'] = [0, intval($userData['company_id'])]; } else { $filter['company_id'] = 0; } } else { # unknown user $filter['user_id'] = -1; $filter['company_id'] = 0; } } } if (bff::businessEnabled() && ($companyId = $list->filter('companyid'))) { if (! $userID) { $companyData = Business::model()->companyData($companyId, ['user_id']); if (! empty($companyData['user_id'])) { $filter['user_id'] = intval($companyData['user_id']); } } $filter['company_id'] = $companyId; } if ($title = $list->filter('title')) { if (is_numeric($title)) { if (mb_strlen($title) >= 8 || mb_substr($title, 0, 1) === '+') { # ищем еще и номер телефона if ($this->getItemContactsFromProfile()) { if (empty($f['uid'])) { $users = Users::model()->usersList([ ['(phone LIKE :phone OR phones LIKE :phone)', ':phone' => '%' . $title . '%'], ], ['user_id'], false, $this->db->prepareLimit(0, 50)); if (!empty($users)) { $filter['user_id'] = array_keys($users); } } } else { $filter[':query'] = $title; } } else { $filter['id'] = intval($title); } } elseif (mb_strpos($title, 'import:#') === 0) { $filter['import'] = (int)mb_substr($title, 8); } else { $filter[':query'] = $title; } } if ($region = $list->filter('region')) { $filter[':region-filter'] = $region; } return $filter; } public function comments_ajax() { if (!$this->haveAccessTo('items-comments')) { return $this->showAccessDenied(); } return $this->itemComments()->admAjax(); } public function comments_mod() { if (!$this->haveAccessTo('items-comments')) { return $this->showAccessDenied(); } return $this->itemComments()->admListingModerate(15, true); } public function claims() { if (!$this->haveAccessTo('claims-listing')) { return $this->showAccessDenied(); } if ($this->isAJAX()) { switch ($this->input->get('act', TYPE_STR)) { case 'delete': # удаляем жалобу { if (!$this->haveAccessTo('claims-edit')) { return $this->ajaxResponse(Errors::ACCESSDENIED); } $claimID = $this->input->post('claim_id', TYPE_UINT); if ($claimID) { $data = $this->model->claimData($claimID, ['id','viewed']); if (empty($data)) { return $this->ajaxResponse(Errors::IMPOSSIBLE); } $response = ['counter_update' => false]; $res = $this->model->claimDelete($claimID); if ($res && !$data['viewed']) { $this->claimsCounterUpdate(-1); $response['counter_update'] = true; } $response['res'] = $res; return $this->ajaxResponse($response); } } break; case 'viewed': # отмечаем жалобу как прочитанную { if (!$this->haveAccessTo('claims-edit')) { return $this->ajaxResponse(Errors::ACCESSDENIED); } $claimID = $this->input->post('claim_id', TYPE_UINT); if ($claimID) { $res = $this->model->claimSave($claimID, ['viewed' => 1]); if ($res) { $this->claimsCounterUpdate(-1); } return $this->ajaxResponse(Errors::SUCCESS); } } break; } return $this->ajaxResponse(Errors::IMPOSSIBLE); } $data = $this->input->getm([ 'item' => TYPE_UINT, 'page' => TYPE_UINT, 'perpage' => TYPE_UINT, 'status' => TYPE_UINT, ]); $filter = []; if ($data['item']) { $filter['item_id'] = $data['item']; } switch ($data['status']) { case 1: { /* все */ } break; default: { $filter['viewed'] = 0; } break; } $count = $this->model->claimsListing($filter, true); $perPage = $this->preparePerpage($data['perpage'], [20, 40, 60]); $filterQuery = http_build_query($data); unset($data['page']); $pages = new Pagination($count, $data['perpage'], $this->adminLink("claims&$filterQuery&page=" . Pagination::PAGE_ID)); $data['pgn'] = $pages->view(); $data['claims'] = ($count > 0 ? $this->model->claimsListing($filter, false, $pages->getLimitOffset()) : []); foreach ($data['claims'] as &$v) { $v['message'] = $this->getItemClaimText($v['reason'], $v['message']); } unset($v); $data['perpage'] = $perPage; return $this->template('admin/items.claims.listing', $data); } public function ajax() { $response = []; $action = $this->input->get('act', TYPE_STR); $statusBlock = function ($itemID) use (&$response) { $data = $this->model->itemData($itemID); $data['user'] = ['blocked' => $data['user_blocked']]; $data['is_refresh'] = true; $data['is_popup'] = $this->input->post('popup', TYPE_BOOL); $response['buttons'] = ''; $data['status_buttons'] = & $response['buttons']; $response['html'] = $this->template('admin/form.status', $data); }; switch ($action) { case 'item-info': /** * Краткая информация об ОБ (popup) * @param integer 'id' ID объявления */ if (!$this->haveAccessTo('items-listing')) { $this->errors->accessDenied(); break; } $itemID = $this->input->get('id', TYPE_UINT); if (!$itemID) { $this->errors->unknownRecord(); break; } $data = $this->model->itemData($itemID, [ 'id','user_id','company_id','cat_id','created', 'title','descr','link','status','status_prev','status_changed', 'claims_cnt','blocked_id','blocked_num','blocked_reason', 'moderated','publicated','publicated_to','publicated_order','publicated_period', 'price','price_curr','price_ex','price_ex_mod','imgcnt', ]); if (empty($data)) { $this->errors->unknownRecord(); break; } $data['user'] = Users::model()->userData($data['user_id'], [ 'email','phone_number','name','blocked','company_id', ]); if ($data['company_id'] && $data['company_id'] == $data['user']['company_id'] && bff::businessEnabled()) { $data['company'] = Business::model()->companyData($data['company_id'], ['id','link','title']); } $data['cats_path'] = $this->model->catParentsData($data['cat_id'], ['id','title','settings']); $data['img'] = $this->itemImages($itemID); $data['images'] = $data['img']->getData($data['imgcnt']); $this->itemPrice()->format(['data' => &$data]); return $this->adminModal($data, 'admin/items.info'); case 'item-form-cat': /** * Форма ОБ, дополнительные поля в зависимости от категории * @param integer 'cat_id' ID категории */ if (!$this->haveAccessTo('items-edit')) { $this->errors->accessDenied(); break; } $categoryID = $this->input->post('cat_id', TYPE_UINT); $response['id'] = $categoryID; do { $data = $this->itemFormByCategory($categoryID); if (empty($data)) { $this->errors->unknownRecord(); break; } $response = array_merge($data, $response); } while (false); break; case 'item-block': /** * Блокировка объявления (если уже заблокирован => изменение причины блокировки) * @param string 'blocked_reason' причина блокировки * @param integer 'id' ID объявления */ if (!$this->haveAccessTo('items-moderate')) { $this->errors->accessDenied(); break; } $itemID = $this->input->post('id', TYPE_UINT); $opts = [ 'blocked_id' => $this->input->postget('blocked_id', TYPE_UINT), 'blocked_reason' => $this->input->postget('blocked_reason', TYPE_STR), 'isBlocked' => false, ]; $this->itemBlock($itemID, $opts); $response['blocked'] = $opts['isBlocked']; $response['reason'] = $opts['blocked_reason']; $statusBlock($itemID); break; case 'item-activate': if (!$this->haveAccessTo('items-moderate')) { $this->errors->accessDenied(); break; } $itemID = $this->input->post('id', TYPE_UINT); if (!$itemID) { $this->errors->unknownRecord(); break; } $itemData = $this->model->itemData($itemID, ['status','user_id','publicated_period','cat_id']); if (empty($itemData) || $itemData['status'] != static::STATUS_NOTACTIVATED) { $this->errors->impossible(); break; } $userData = Users::model()->userData($itemData['user_id'], ['activated','blocked']); if (empty($userData)) { $this->errors->impossible(); break; } if (!$userData['activated']) { $this->errors->set(_t('@listings', 'It is not allowed to activate an listing for a non-activated user')); break; } if ($userData['blocked']) { $this->errors->set(_t('@listings', 'It is not allowed to activate an listing for a banned user')); break; } $period = $this->itemPublicationPeriod(); $publicated_period = $period->publishPeriod($itemData['publicated_period'], $itemData['cat_id'] ?? 0); $res = $this->model->itemSave($itemID, [ 'activate_key' => '', # чистим ключ активации 'publicated' => $this->db->now(), 'publicated_order' => $this->db->now(), 'publicated_to' => $period->publishTo(['period' => $publicated_period]), 'publicated_period' => $publicated_period, 'status_prev' => static::STATUS_NOTACTIVATED, 'status' => static::STATUS_PUBLICATED, 'moderated' => 1, ]); if (empty($res)) { $this->errors->impossible(); } else { # обновляем счетчик "на модерации" $this->moderationCounterUpdate(); $statusBlock($itemID); } break; case 'item-approve': if (!$this->haveAccessTo('items-moderate')) { $this->errors->accessDenied(); break; } $itemID = $this->input->post('id', TYPE_UINT); if ($this->itemApprove($itemID)) { $statusBlock($itemID); } break; case 'item-refresh': /** * Продление публикации ОБ * @param integer 'id' ID объявления */ if (!$this->haveAccessTo('items-moderate')) { $this->errors->accessDenied(); break; } $itemID = $this->input->post('id', TYPE_UINT); $opts = [ # поднять вверх списка 'topUp' => $this->input->post('topup', TYPE_BOOL), ]; if ($this->itemRefresh($itemID, $opts)) { $statusBlock($itemID); } break; case 'item-unpublicate': /** * Снимаем ОБ с публикации * @param integer 'id' ID объявления */ if (!$this->haveAccessTo('items-moderate')) { $this->errors->accessDenied(); break; } $itemID = $this->input->post('id', TYPE_UINT); if ($this->itemUnpublicate($itemID)) { $statusBlock($itemID); } break; case 'item-user': if (!$this->haveAccessTo('items-listing')) { $this->errors->accessDenied(); return $this->ajaxResponse($response); } $q = $this->input->post('q', TYPE_NOTAGS); $response = $this->ajaxHandlerItemUser(['q' => $q]); return $this->ajaxResponse($response); case 'item-auto-title': if (!$this->haveAccessTo('items-edit')) { $this->errors->accessDenied(); break; } $catID = $this->input->post('cat_id', TYPE_UINT); $field = 'tpl_title_view'; if ($this->translate()) { $catData = []; $cat = $this->catNearestParent($catID, [$field], $catData); $catData = $this->model->catDataLang($cat, [$field]); foreach ($catData as $v) { $response['title'][$v['lang']] = $this->dp()->buildItemTemplate($catID, $v[$field], $this->input->post(), $v['lang']); } } else { $catData = []; $this->catNearestParent($catID, [$field], $catData); $response['title'] = $this->dp()->buildItemTemplate($catID, $catData[$field], $this->input->post()); } break; case 'category-options': if (!$this->haveAccessTo('categories')) { # todo $this->errors->accessDenied(); break; } $type = $this->input->post('type', TYPE_NOTAGS); $selectedID = $this->input->post('selected', TYPE_UINT); $response['options'] = $this->model->catsOptions($type, $selectedID); break; case 'category-autocomplete': if (!$this->haveAccessTo('categories')) { # todo return $this->errors->accessDenied(); } $q = $this->input->post('q', TYPE_NOTAGS); $type = $this->input->post('type', TYPE_NOTAGS); $result = $this->ajaxHandlerCategoryAutocomplete(['q' => $q, 'type' => $type]); return $this->ajaxResponse($result); default: $this->app->hook('listings.admin.ajax.default.action', $action, $this); $this->errors->impossible(); break; } return $this->ajaxResponseForm($response); } public function ajaxHandlerCategoryAutocomplete($opts) { $type = $opts['type'] ?? ''; $filter = []; if (! empty($opts['q'])) { $q = $opts['q'] ?? ''; $q = $this->input->cleanSearchString($q, 100); $filter['q'] = $q; } elseif (! empty($opts['id'])) { $filter['id'] = (int)$opts['id']; } if (empty($filter)) { return []; } $cats = $this->model->catsOptions($type, 0, false, $filter, ['array' => true, 'limit' => 20]); $cats = $this->model->catsParents($cats); $result = []; foreach ($cats as $v) { $p = []; foreach ($v['parents'] ?? [] as $vv) { $p[] = $vv['title']; } $result[] = [$v['id'], $v['title'] . '
' . join(' / ', $p) . '', [ 'title' => $v['title'], 'parents' => join(' / ', $p), 'settings' => $v['settings'] ?? 0, ],]; } return $result; } public function ajaxHandlerItemUser($opts) { $query = $opts['q'] ?? ''; if (empty($query)) { return []; } $query = $this->input->cleanSearchString($query); $forcePhone = Users::isForcePhone($query); $filter = [ 'blocked' => 0, 'activated' => 1, ]; if (mb_substr($query, 0, 1) === '#') { $filter['user_id'] = mb_substr($query, 1); } else { $filter[':email'] = [ ' ( ' . (Users::model()->userEmailCrypted() ? 'BFF_DECRYPT(email)' : 'email') . ' LIKE :email OR ' . (Users::model()->userPhoneCrypted() ? 'BFF_DECRYPT(phone_number)' : 'phone_number') . ' LIKE :email ' . ' ) ', ':email' => $query . '%' ]; } if ($this->publisher(static::PUBLISHER_COMPANY)) { $filter[':company'] = 'company_id > 0'; } elseif ($this->publisher(static::PUBLISHER_USER_TO_COMPANY)) { $filter['company_id'] = 0; } $usersList = Users::model()->usersList($filter, ['user_id','email','phone_number','company_id'], false, 10); $response = []; foreach ($usersList as $v) { $response[] = [$v['user_id'], Users::autocompleteTitleEmail($v, $forcePhone), $v['company_id']]; } return $response; } # ------------------------------------------------------------------------------------------------------------------------------- # категории public function categories_listing() { if (!$this->haveAccessTo('categories')) { return $this->showAccessDenied(); } $data = []; $action = $this->input->get('act', TYPE_STR); if (!empty($action)) { switch ($action) { case 'subs-list': { $categoryID = $this->input->postget('category', TYPE_UINT); if (!$categoryID) { return $this->ajaxResponse(Errors::UNKNOWNRECORD); } $data['cats'] = $this->model->catsListing(['pid' => $categoryID]); $data['deep'] = $this->catsDepthLimit(); return $this->ajaxResponse([ 'list' => $this->template('admin/categories.listing.ajax', $data), 'cnt' => sizeof($data['cats']), ]); } case 'toggle': { $categoryID = $this->input->get('rec', TYPE_UINT); if ($this->model->catToggle($categoryID, 'enabled')) { return $this->ajaxResponseForm(['reload' => true]); } } break; case 'rotate': { if ($this->model->catsRotate()) { return $this->ajaxResponse(Errors::SUCCESS); } } break; case 'delete': { $categoryID = $this->input->post('rec', TYPE_UINT); if ($this->isAdminFordev()) { if ($this->model->catDelete($categoryID, true)) { return $this->ajaxResponse(Errors::SUCCESS); } } elseif ($this->model->catDelete($categoryID)) { return $this->ajaxResponse(Errors::SUCCESS); } } break; case 'dev-landing-pages-auto': { $this->model->catsLandingPagesAuto(); return $this->adminRedirect(Errors::SUCCESS, 'categories_listing'); } case 'dev-export': { if (!$this->isAdminFordev()) { return $this->showAccessDenied(); } $type = $this->input->getpost('type', TYPE_STR); $content = ''; switch ($type) { case 'txt': default: { $data = $this->model->catsExport('txt'); foreach ($data as &$v) { $content .= str_repeat("\t", $v['numlevel'] - 1) . ($v['subs'] ? '-' : '+') . ' ' . $v['id'] . ' ' . $v['title'] . "\n"; } unset($v); } break; } return Response::attachment($content, 'categories_export.txt') ->withHeader('Content-Type', 'text/plain; charset=utf-8'); } case 'dev-treevalidate': { if (!$this->isAdminFordev()) { return $this->showAccessDenied(); } set_time_limit(0); ignore_user_abort(true); return $this->model->treeCategories()->validate(true); } case 'dev-delete-all': { if (!$this->isAdminFordev()) { return $this->showAccessDenied(); } if ($this->model->catDeleteAll()) { return $this->adminRedirect(Errors::SUCCESS, 'categories_listing'); } } break; } return $this->ajaxResponse(Errors::IMPOSSIBLE); } $filter = []; $catState = $this->input->cookie($this->app->cookieKey('listings_cats_state')); $catExpandedID = (!empty($catState) ? explode('.', $catState) : []); $catExpandedID = array_map('intval', $catExpandedID); $catExpandedID[] = static::CATS_ROOTID; $filter['pid'] = $catExpandedID; $data['cats'] = $this->model->catsListing($filter); $data['deep'] = $this->catsDepthLimit(); foreach ($data['cats'] as &$v) { $v['expanded'] = in_array($v['id'], $catExpandedID); } unset($v); $data['cats'] = $this->template('admin/categories.listing.ajax', $data); return $this->template('admin/categories.listing', $data); } public function categories_packetActions() { if (!$this->isAdminFordev()) { return $this->showAccessDenied(); } $data = []; if ($this->isAJAX()) { $updated = 0; do { $actions = $this->input->post('actions', TYPE_ARRAY_BOOL); $actions = $this->input->clean_array($actions, [ 'currency_default' => TYPE_BOOL, 'photos_max' => TYPE_BOOL, 'list_type' => TYPE_BOOL, 'video' => TYPE_BOOL, 'landingpages_auto' => TYPE_BOOL, 'geo_regions' => TYPE_BOOL, 'publication' => TYPE_BOOL, 'quantitative' => TYPE_BOOL, ]); if (! array_sum($actions)) { $this->errors->set(_t('@listings', 'Select at least one of the available settings')); break; } $catsFields = ['id', 'settings']; # валюта по умолчанию if ($actions['currency_default']) { $currencyID = $this->input->post('currency_default', TYPE_UINT); if (! $currencyID) { $this->errors->set(_t('@listings', 'Default currency is incorrect')); break; } } # кол-во в наличии if ($actions['quantitative']) { $quantitative = $this->input->post('quantitative', TYPE_UINT); if (! in_array($quantitative, [0,1])) { $quantitative = 0; } } # максимально доступное кол-во фотографий if ($actions['photos_max']) { $photosMax = $this->input->post('photos_max', TYPE_UINT); if ($photosMax > $this->itemsImagesLimit()) { $photosMax = $this->itemsImagesLimit(); } } # использование видео if ($actions['video']) { $video = $this->input->post('video', TYPE_UINT); if (! in_array($video, [0,1])) { $video = 1; } } # вид списка по умолчанию if ($actions['list_type']) { $listType = $this->input->post('list_type', TYPE_UINT); if (! array_key_exists($listType, $this->itemsSearchListTypes())) { $actions['list_type'] = false; } } # использование регионов if ($actions['geo_regions']) { $geoRegions = $this->input->post('geo_regions', TYPE_UINT); if (! in_array($geoRegions, [0,1])) { $geoRegions = 1; } } # Бесконечный срок публикации if ($actions['publication']) { $publication = $this->input->post('publication', TYPE_UINT); if (! in_array($publication, [0,1])) { $publication = 0; } } # избавление от /search/ if ($actions['landingpages_auto']) { $updated = $this->model->catsLandingPagesAuto(); if (array_sum($actions) == 1) { break; } } $this->app->hook('listings.admin.category.packetActions.step1', [ 'actions' => &$actions, 'catsFields' => &$catsFields, ]); $catsList = $this->model->catsDataByFilter([], $catsFields); if (empty($catsList)) { $this->errors->set(_t('@listings', 'Could not find categories')); break; } foreach ($catsList as &$v) { if ($actions['currency_default']) { if (!isset($v['settings']['price']['curr'])) { continue; } $v['settings']['price']['curr'] = $currencyID; } if ($actions['photos_max']) { $v['settings']['photos']['limit'] = $photosMax; } if ($actions['list_type']) { if ( $listType != static::LIST_TYPE_MAP || ($listType == static::LIST_TYPE_MAP && $v['settings']['geo']['enabled'] && $v['settings']['geo']['addr']) ) { $v['settings']['list_type'] = $listType; } } if ($actions['geo_regions']) { $v['settings']['geo']['enabled'] = $geoRegions; if (! $geoRegions && $v['settings']['list_type'] == static::LIST_TYPE_MAP) { $v['settings']['list_type'] = static::LIST_TYPE_LIST; } } if ($actions['video']) { $v['settings']['video']['enabled'] = $video; } if ($actions['publication']) { $v['settings']['publication']['infinite'] = $publication; } if ($actions['quantitative']) { $settings['quantitative'] = $quantitative; } $this->app->hook('listings.admin.category.packetActions.step2', ['id' => $v['id'], 'data' => $v, 'actions' => &$actions]); $update = []; foreach ($catsFields as $f) { if ($f == 'id') { continue; } if (isset($v[$f])) { $update[$f] = $v[$f]; } } $res = $this->model->catSave($v['id'], $update); if (! empty($res)) { $updated++; } } unset($v); } while (false); return $this->ajaxResponseForm(['updated' => $updated]); } return $this->template('admin/categories.packetActions', $data); } public function categories_add() { if (!$this->haveAccessTo('categories')) { return $this->showAccessDenied(); } $data = $this->validateCategoryData(0); if ($this->isPOST()) { $response = ['reload' => false, 'back' => false]; if ($this->errors->no('listings.admin.category.submit', ['id' => 0, 'data' => &$data])) { $response['id'] = $this->model->catSave(0, $data); $response['back'] = true; } return $this->iframeResponseForm($response); } $data['id'] = 0; $data['pid_options'] = $this->model->catsOptions('adm-category-form-add', $data['pid']); $data['dp'] = []; return $this->template('admin/categories.form', $data); } public function categories_edit() { if (!$this->haveAccessTo('categories')) { return $this->showAccessDenied(); } $allowEditParent = true; $categoryID = $this->input->getpost('id', TYPE_UINT); if (!$categoryID) { return $this->adminRedirect(Errors::UNKNOWNRECORD, 'categories_listing'); } $data = $this->model->catData($categoryID, '*', true); $data['structure_modified'] = $this->model->catsStructureChanged(); if ($this->isPOST()) { $response = ['reload' => false, 'back' => false]; if (!$data) { $this->errors->unknownRecord(); return $this->iframeResponseForm($response); } $copySettingsToSubs = ($this->input->post('copy_to_subs', TYPE_BOOL) && $this->isAdminFordev()); if ($copySettingsToSubs) { $copySettingsToSubsParams = $this->input->post('copy_to_subs_data', TYPE_ARRAY); } $save = $this->validateCategoryData($categoryID); if ($this->errors->no('listings.admin.category.submit', ['id' => $categoryID,'data' => &$save,'before' => $data])) { # смена parent-категории if ($allowEditParent && !$copySettingsToSubs && $save['pid'] != $data['pid'] && $this->input->post('structure_modified', TYPE_STR) === $data['structure_modified']) { if ($this->model->catChangeParent($categoryID, $save['pid']) !== false) { $response['structure_modified'] = $this->db->now(); } # очищаем состояние списка категорий из-за смены порядка вложенности Response::deleteCookie($this->app->cookieKey('listings_cats_state')); } # отвязываем объявления, добавленные в виртуальную категорию if (!empty($data['virtual_ptr']) && empty($save['virtual_ptr'])) { $this->model->catVirtualDropItemsLink($categoryID); } # изменение связи виртуальной категории if ($data['virtual_ptr'] != $save['virtual_ptr']) { $this->model->itemsCountersCalculateVirtual(); } $res = $this->model->catSave($categoryID, $save); if (!empty($res)) { # если keyword был изменен и есть вложенные подкатегории: # > перестраиваем полный путь подкатегорий (и items::link) if ($data['keyword_edit'] != $save['keyword_edit'] && $data['node'] > 1) { $this->model->catSubcatsRebuildKeyword($categoryID, $data['keyword_edit']); } # сбрасываем кеш дин. свойств категории $this->dp()->onSettingsChanged($categoryID, 0, 'cat-edit'); # хук успешного сохранения $this->app->hook('listings.admin.category.submit.success', ['id' => $categoryID,'data' => &$save,'before' => $data]); } if ($this->model->catIsMain($categoryID, $save['pid'])) { $update = []; $icon = $this->categoryIcon($categoryID); foreach ($icon->getVariants() as $iconField => $v) { $icon->setVariant($iconField); $aIconData = $icon->uploadFILES($iconField, true, false); if (!empty($aIconData)) { $update[$iconField] = $aIconData['filename']; $response['reload'] = true; } else { if ($this->input->post($iconField . '_del', TYPE_BOOL)) { if ($icon->delete(false)) { $update[$iconField] = ''; } } } } if (!empty($update)) { $this->model->catSave($categoryID, $update); } } # копируем настройки во все подкатегории if ($copySettingsToSubs) { $this->model->catDataCopyToSubs($categoryID, array_unique($copySettingsToSubsParams)); } if ($this->input->post('back', TYPE_BOOL)) { $response['back'] = true; } else { $response['landing_id'] = $save['landing_id']; $response['landing_url'] = $save['landing_url']; } } return $this->iframeResponseForm($response); } else { if (!$data) { return $this->adminRedirect(Errors::UNKNOWNRECORD, 'categories_listing'); } } $data['pid_editable'] = $allowEditParent; if ($allowEditParent) { $data['pid_options'] = $this->model->catsOptions('adm-category-form-edit', $data['pid'], false, [ 'id' => $categoryID, 'numleft' => $data['numleft'], 'numright' => $data['numright'], ]); } else { $data['pid_options'] = $this->model->catParentsData($categoryID, ['id','title'], [ 'includingSelf' => false, 'exludeRoot' => false, ]); } if ($data['virtual_ptr']) { $parents = $this->model->catParentsData($data['virtual_ptr'], ['id', 'title', 'numlevel']); $data['virtual_ptr_title'] = $parents[ $data['virtual_ptr'] ]['title'] ?? ''; unset($parents[ $data['virtual_ptr'] ]); $data['virtual_ptr_parents'] = []; foreach ($parents as $v) { $data['virtual_ptr_parents'][ $v['numlevel'] ] = $v['title']; } } $data['dp'] = $this->dp()->getByOwner($categoryID, true, true, false); $data['tpl_parent'] = []; $this->catNearestParent($categoryID, ['tpl_title_list', 'tpl_title_view'], $data['tpl_parent'], false); $this->catNearestParent($categoryID, ['tpl_descr_list'], $data['tpl_parent'], false); return $this->template('admin/categories.form', $data); } public function categories_suggest() { $data = $this->model->catsListing([ 'numlevel' => 1, 'enabled' => 1, ]); $result = []; foreach ($data as $v) { $result[] = [$v['id'], $v['title']]; } return $result; } # ------------------------------------------------------------------------------------------------------------------------------- # типы категории public function types_listing($categoryID) { if (!$this->haveAccessTo('types')) { return ''; } $data['cat_id'] = $categoryID; $data['cats'] = $this->model->catParentsData($categoryID, ['id','title']); $data['types'] = $this->model->cattypesListing([$this->db->prepareIN('T.cat_id', array_keys($data['cats']))]); $data['list'] = $this->template('admin/types.listing.ajax', $data); if ($this->isAJAX()) { return $data['list']; } return $this->template('admin/types.listing', $data); } public function types() { $response = []; do { if (!$this->haveAccessTo('types')) { $this->errors->accessDenied(); break; } $categoryID = $this->input->getpost('cat_id', TYPE_UINT); if (!$categoryID) { $this->errors->impossible(); break; } switch ($this->input->postget('act', TYPE_STR)) { case 'toggle': { $typeID = $this->input->get('type_id', TYPE_UINT); if (!$this->model->cattypeToggle($typeID, 'enabled')) { $this->errors->impossible(); } } break; case 'rotate': { if (!$this->model->cattypesRotate($categoryID)) { $this->errors->impossible(); } } break; case 'form': { $typeID = $this->input->get('type_id', TYPE_UINT); if ($typeID) { $data = $this->model->cattypeData($typeID, [], true); } else { $this->validateCategoryTypeData($data, $categoryID, 0); $data['id'] = 0; } $data['form'] = $this->template('admin/types.form', $data); $response = $data; } break; case 'delete': { $typeID = $this->input->get('type_id', TYPE_UINT); if (!$this->model->cattypeDelete($typeID)) { $this->errors->impossible(); } } break; case 'add': { $this->validateCategoryTypeData($data, $categoryID, 0); if ($this->errors->no('listings.admin.category-type.submit', ['id' => 0,'data' => &$data,'cat' => $categoryID])) { $this->model->cattypeSave(0, $categoryID, $data); $response['list'] = $this->types_listing($categoryID); } } break; case 'edit': { $typeID = $this->input->post('type_id', TYPE_UINT); if (!$typeID) { $this->errors->impossible(); break; } $this->validateCategoryTypeData($data, $categoryID, $typeID); if ($this->errors->no('listings.admin.category-type.submit', ['id' => $typeID,'data' => &$data,'cat' => $categoryID])) { $this->model->cattypeSave($typeID, $categoryID, $data); $response['list'] = $this->types_listing($categoryID); } } break; default: $this->errors->impossible(); } } while (false); return $this->ajaxResponseForm($response); } public function sysSpamMinuswordsPrepare($params = []) { if (!empty($params['value'])) { $prepared = serialize(TextParser::minuswordsPrepare('to_array', json_decode($params['value'], true))); config::save('listings.items.spam.minuswords.prepared', $prepared); } } /** * Обработка данных категории * @param integer $categoryID ID категории * @return array данные */ protected function validateCategoryData($categoryID = 0) { $isSubmit = $this->isPOST(); $data['pid'] = $this->input->postget('pid', TYPE_UINT); $params = [ 'settings' => TYPE_ARRAY, 'keyword_edit' => TYPE_NOTAGS, 'mtemplate' => TYPE_BOOL, # Использовать общий шаблон SEO: Поиск в категории 'category_mtemplate' => TYPE_BOOL, # Использовать общий шаблон SEO: Страница категории 'view_mtemplate' => TYPE_BOOL, # Использовать общий шаблон SEO: Просмотр объявления 'view_msocialtemplate' => TYPE_BOOL, # Использовать общий шаблон Social: Просмотр объявления 'is_virtual' => TYPE_BOOL, 'tpl_title_enabled' => TYPE_BOOL, 'virtual_ptr' => TYPE_UINT, # Указатель на реальную категорию ]; $this->input->postm($params, $data); $this->input->postm_lang($this->model->langCategories, $data); $templateSocial = SEO::templateSocial('listings', 'view', [], ['prefix' => 'view_','validate' => true]); if (is_array($templateSocial)) { foreach ($templateSocial as $key => $value) { $data[$key] = $value; } } $langCurrent = $this->locale->current(); if (!$data['is_virtual']) { $data['virtual_ptr'] = null; } else { if ($data['virtual_ptr'] === $data['pid']) { $this->errors->set( _t('@listings', 'A virtual category cannot reference to its parent category') ); } } unset($data['is_virtual']); $this->validateCategorySettings($data['settings']); if ($isSubmit) { do { # основная категория обязательна if (!$data['pid']) { $this->errors->set(_t('@listings', 'Specify the main category')); break; } else { $parent = $this->model->catData($data['pid'], ['settings']); if (empty($parent)) { $this->errors->set(_t('@listings', 'The main category is incorrect')); break; } else { # наследуем настройки из основной категории: if (! empty($parent['settings']['seek'])) { $settings['seek'] = $parent['settings']['seek']; } } } # название обязательно if (isset($data['title'][$langCurrent]) && empty($data['title'][$langCurrent])) { $this->errors->set(_t('@listings', 'Enter the name')); break; } foreach ($data['title'] as $k => $v) { $data['title'][$k] = str_replace(['"'], '', $v); } # keyword $keyword = $data['keyword_edit']; if (empty($keyword) && !empty($data['title'][$langCurrent])) { $keyword = mb_strtolower(func::translit($data['title'][$langCurrent])); } $keyword = preg_replace('/[^\p{L}\w0-9_\-]/iu', '', mb_strtolower($keyword)); if (empty($keyword)) { $this->errors->set(_t('@listings', 'Incorrect Keyword')); break; } # проверяем уникальность keyword'a в пределах основной категории $res = $this->model->catDataByFilter([ 'pid' => $data['pid'], 'keyword_edit' => $keyword, ['C.id!=:id', ':id' => $categoryID] ], ['id']); if (!empty($res)) { $this->errors->set(_t('@listings', 'The specified keyword is already in use, enter another one')); break; } $data['keyword_edit'] = $keyword; # строим полный путь "parent-keyword / ... / keyword" $keywordsPath = []; if ($data['pid'] > static::CATS_ROOTID) { $parentCatData = $this->model->catData($data['pid'], ['keyword']); if (empty($parentCatData)) { $this->errors->set(_t('@listings', 'The main category is incorrect')); break; } else { $keywordsPath = explode('/', $parentCatData['keyword']); } } $keywordsPath[] = $keyword; $data['keyword'] = join('/', $keywordsPath); # посадочный URL $landingData = $this->seo()->joinedLandingpage( $this, 'search-category', $this->url('items.search', ['keyword' => $data['keyword'], 'region' => false], true), ['joined-id' => $categoryID, 'joined-module' => 'listings-cats'] ); $data['landing_id'] = $landingData['id']; $data['landing_url'] = $landingData['url']; } while (false); } else { if (!$categoryID) { $data['mtemplate'] = 1; $data['view_mtemplate'] = 1; $data['view_msocialtemplate'] = 1; } } $this->app->hook('listings.admin.category.form.validate', [ 'id' => $categoryID, 'data' => &$data, 'submit' => $isSubmit, ]); return $data; } /** * Обработка настроек категории * @param array $settings @ref настройки * @param bool|null $isPost * @return void */ protected function validateCategorySettings(&$settings, $isPost = null) { $isPost = $isPost ?? $this->isPOST(); $this->input->clean_array($settings, [ 'price' => TYPE_ARRAY, 'geo' => TYPE_ARRAY, 'photos' => TYPE_ARRAY, 'video' => TYPE_ARRAY, 'publication' => TYPE_ARRAY, 'owner_business' => TYPE_UINT, 'owner_search' => ($isPost ? TYPE_ARRAY_UINT : TYPE_UINT), 'seek' => TYPE_UINT, 'list_type' => TYPE_UINT, 'subcats_view' => TYPE_UINT, ]); # price $this->input->clean_array($settings['price'], [ 'enabled' => TYPE_UINT, 'title' => TYPE_ARRAY_STR, 'curr' => TYPE_UINT, 'ranges' => TYPE_ARRAY, 'ex' => ($isPost ? TYPE_ARRAY_UINT : TYPE_UINT), 'mod_title' => TYPE_ARRAY_STR, 'filter_position' => TYPE_UINT, 'modifiers' => TYPE_ARRAY, 'mod_empty' => TYPE_UINT, 'mod_empty_title' => TYPE_ARRAY_STR, ]); if ($isPost) { $ranges = & $settings['price']['ranges']; if (!empty($ranges) && is_array($ranges)) { foreach ($ranges as $k => &$v) { $v['from'] = floatval(trim(strip_tags($v['from']))); $v['to'] = floatval(trim(strip_tags($v['to']))); if (empty($v['from']) && empty($v['to'])) { unset($ranges[$k]); continue; } } } else { $ranges = []; } $settings['price']['ex'] = array_sum($settings['price']['ex']); } # geo $this->input->clean_array($settings['geo'], [ 'enabled' => TYPE_UINT, 'addr' => TYPE_UINT, 'metro' => TYPE_UINT, 'delivery' => TYPE_UINT, ]); # photos $this->input->clean_array($settings['photos'], [ 'limit' => TYPE_UINT, 'filter' => TYPE_UINT, ]); # лимит фотографий if ($settings['photos']['limit'] > $this->itemsImagesLimit()) { $settings['photos']['limit'] = $this->itemsImagesLimit(); } # video $this->input->clean_array($settings['video'], [ 'enabled' => TYPE_UINT, ]); # тип списка по умолчанию if (! array_key_exists($settings['list_type'], $this->itemsSearchListTypes())) { $settings['list_type'] = 0; } elseif ($settings['list_type'] == static::LIST_TYPE_MAP && (! $settings['geo']['enabled'] || ! $settings['geo']['addr'])) { $settings['list_type'] = 0; } if ($isPost) { $settings['owner_search'] = array_sum($settings['owner_search']); } } /** * Обработка данных типа категории * @param array $data @ref данные * @param integer $categoryID ID категории * @param integer $typeID ID типа * @return void */ protected function validateCategoryTypeData(&$data, $categoryID, $typeID) { $this->input->postm_lang($this->model->langCategoriesTypes, $data); if ($this->isPOST()) { if ($this->errors->no()) { # ... } } } public function changeSysUrlPrefix($params = []) { if (! isset($params['value'])) { $this->errors->impossible(); return; } $value = $params['value']; if (preg_match('/[^a-zA-Z0-9]/', $value, $m)) { $this->errors->set(_t('@listings', 'Value of parameter "List of Listings" contains invalid characters')); return; } if (mb_strlen($value) < 2) { $this->errors->set(_t('@listings', 'The value of the "List of Listings" parameter must contain more than 1 character')); return; } $prefix = bff::urlPrefix('listings', 'list', 'search'); if ($prefix != $value) { $this->app->cronManager()->executeOnce('listings', 'landingpagesUriRebuild', ['old' => $prefix], true); } } /** * Формирования ajax таба "Поисковые фразы" для системных настроек * @return string */ public function settingsSystemWordforms() { if (!$this->haveAccessTo('settings')) { return $this->showAccessDenied(); } return $this->itemsSearchSphinx()->wordformsManager(); } }