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();
}
}