setTemplate('form/form', 'listings');
$this->setKey('form');
$this->item = Listings::model()->item();
$this->category = Listings::model()->category();
$this->user = Users::model()->user();
$this->useBlocksRotation();
$this->rotationSettingsInTab = true;
}
public function data()
{
$data = parent::data();
$data['edit'] = $this->edit();
$data['itemId'] = $this->recordID();
$data['dividerPrefix'] = $this->rotationDividerKeyPrefix;
return $data;
}
public function blocks()
{
$this->addBlock('titleBlock', Title::class);
$this->addBlock('categoryBlock', Category::class);
$this->addBlock('publisherBlock', Publisher::class);
$this->addBlock('catTypeBlock', CatType::class);
$this->addBlock('dynpropsBlock', Dynprops::class);
$this->addBlock('priceBlock', Price::class);
$this->addBlock('descriptionBlock', Description::class);
$this->addBlock('publicatedPeriodBlock', PublicatedPeriod::class);
$this->addBlock('imagesBlock', Images::class);
$this->addBlock('videoBlock', Video::class);
$this->addBlock('geoBlock', Geo::class);
$this->addBlock('nameBlock', Name::class);
$this->addBlock('ownerTypeBlock', OwnerType::class);
$this->addBlock('phoneBlock', Phone::class);
$this->addBlock('emailBlock', Email::class);
$this->addBlock('contactsBlock', Contacts::class);
$this->addBlock('svcBlock', Svc::class);
$this->addBlock('agreeBlock', Agree::class);
$this->rotateBlock(['titleBlock', 'categoryBlock'], false);
$this->rotateBlock(['nameBlock', 'ownerTypeBlock', 'emailBlock', 'phoneBlock', 'contactsBlock'], true, ['checked' => 'always']);
$this->setRotationDividerBlock(
RotationDividerBlock::class,
function ($block) {
/** @var RotationDividerBlock $block */
$block->setTemplate('form/blocks/divider', 'listings');
},
function ($form) {
/** @var SettingsForm $form */ $form
->preload([
'title' => _t('item-form', 'Description and Price', true),
'before' => 'publisherBlock',
])
->preload([
'title' => _t('item-form', 'Contacts', true),
'before' => 'nameBlock',
]);
}
);
}
public function validate($data = [])
{
if (! empty($data['__validated'])) {
return $data;
}
$data = parent::validate($data);
if ($this->errors->any()) {
return $data;
}
$this->validateTplFillTitles($data);
$this->validateTitle($data);
$this->validateTplFillDescrList($data);
$this->validateTranslates($data);
$this->validateItemUrl($data);
$this->app->hook('listings.item.validate', ['id' => $this->recordID(), 'data' => &$data, 'adminPanel' => $this->validationAdminPanel]);
$data['__validated'] = 1;
return $data;
}
public function submit()
{
$data = $this->validate();
if ($this->errors->no()) {
if ($this->recordID()) {
$this->submitEdit($data);
} else {
$this->submitAdd($data);
}
}
return parent::submit();
}
protected function submitAdd($data)
{
# проверка токена(для авторизованных) + реферера
if (! $this->isRequestValid()) {
$this->errors->reloadPage();
return;
}
$data['user_id'] = $userID = User::id();
if (! $userID && Listings::publisherAuth()) {
# публикация объявления доступна только авторизованным пользователям
$this->errors->reloadPage();
return;
}
$needActivation = false;
if (! $userID) {
$needActivation = true;
$userID = $this->submitAddCreateUser($data);
} else {
$this->submitAddPhoneCheck($data, $needActivation);
}
if (! $this->errors->no('listings.item.add.step1', ['data' => &$data])) {
return;
}
# не чаще чем раз в {X} секунд с одного IP (для одного пользователя)
if ($this->tooManyRequests('listings-add', 20)) {
return;
}
# антиспам фильтр: проверка дубликатов
if (Listings::spamDuplicatesFound($userID, $data)) {
$this->errors->set(_t('listings', 'You have already published a similar listing. Use the listing Raise'), 'spam');
return;
}
$activation = [];
if ($needActivation) {
$this->submitAddActivationInfo($data, $activation);
} else {
$data['moderated'] = 0; # помечаем на модерацию
$data['status'] = Listings::STATUS_PUBLICATED;
$this->submitAddPublicated($data);
$this->submitAddLimits($data);
}
unset($data['email'], $data['phone']);
$this->trigger('submitAddBeforeSave', ['data' => & $data]);
if (! $this->errors->no('listings.item.add.step2', ['data' => &$data])) {
return;
}
$itemId = Listings::model()->itemSave(0, $data);
if (! $itemId) {
$this->errors->set(_t('item-form', 'Error posting your listing, contact support.'));
return;
}
$data['id'] = $itemId;
$data['activation'] = $activation;
$this->trigger('submitAddAfterSave', ['id' => $itemId, 'data' => & $data]);
if ($this->errors->any()) {
Listings::model()->itemsDelete([$itemId], false);
return;
}
# обновляем счетчик объявлений "на модерации"
if (isset($data['moderated']) && empty($data['moderated'])) {
Listings::moderationCounterUpdate(1);
}
if ($needActivation) {
$this->submitAddActivation($data);
}
$this->submitAddSvcRedirect($data);
}
protected function submitEdit($data)
{
$itemID = $this->recordID();
$userID = User::id();
if (
! $itemID ||
! $userID ||
! $this->isRequestValid()
) {
$this->errors->reloadPage();
return;
}
if (! Listings::isItemOwner($itemID, $this->item['user_id'] ?? 0)) {
$this->errors->set(_t('item-form', 'You are not the owner of this listing.'));
return;
}
# проверка статуса объявления
$companyId = $data['company_id'] ?? 0;
if ($companyId && !Business::model()->companyActive($companyId)) {
$this->errors->set(_t('item-form', 'Your company was deactivated or blocked.
It\'s not possible to place a listing from the company.', [
'link' => 'href="' . Business::url('my.listings') . '" target="_blank"'
]));
return;
}
if (
($this->item['status'] ?? 0) == Listings::STATUS_BLOCKED &&
($this->item['blocked_id'] ?? 0) == Listings::BLOCK_FOREVER
) {
# объявление заблокировано навсегда
$this->errors->set(_t('item-form', 'The listing was blocked by the moderator without the possibility of editing and republishing, you can only delete it.'));
return;
}
if (! $this->errors->no('listings.item.edit.step1', ['id' => $itemID, 'data' => &$data, 'item' => $this->item->toArray()])) {
return;
}
if (($this->item['status'] ?? 0) == Listings::STATUS_BLOCKED) {
# объявление заблокировано, помечаем на проверку модератору
$data['moderated'] = 0;
}
# помечаем на модерацию при изменении: названия, описания, категории, видео
if (
$data['title'] != ($this->item['title'] ?? '') ||
$data['descr'] != ($this->item['descr'] ?? '') ||
($data['video'] ?? '') != ($this->item['video'] ?? '') ||
(Listings::categoryFormEditable() && $data['cat_id'] != ($this->item['cat_id'] ?? 0))
) {
if ($this->item['moderated'] ?? 0) {
$data['moderated'] = (Listings::premoderationEdit() ? 0 : 2);
}
}
# изменилось от кого публикуем объявление
if (
($this->item['company_id'] ?? 0) != $companyId &&
($this->item['status'] ?? 0) == Listings::STATUS_PUBLICATED
) {
# проверим лимиты
if (
Listings::limitsExceed([
'user_id' => $userID,
'company_id' => $companyId,
'cat_id' => $data['cat_id'] ?? 0,
])
) {
return;
}
}
$this->trigger('submitEditBeforeSave', ['data' => & $data]);
if (! $this->errors->no('listings.item.edit.step2', ['id' => $itemID, 'data' => &$data, 'item' => $this->item->toArray()])) {
return;
}
# сохраняем
$success = Listings::model()->itemSave($itemID, $data);
if ($success) {
$this->trigger('submitEditAfterSave', ['data' => & $data]);
# счетчик "на модерации"
if (isset($data['moderated'])) {
Listings::moderationCounterUpdate();
}
}
$response = [];
$this->app->hook('listings.item.edit.step3', ['id' => $itemID, 'data' => &$data, 'item' => $this->item->toArray(), 'response' => &$response]);
foreach ($response as $k => $v) {
$this->respond($k, $v);
}
# URL страницы "успешно"
$this->respond('successPage', Listings::url('item.status', [
'action' => 'edit',
'id' => $itemID,
'svc' => $this->input->post('svc', TYPE_STR),
]));
}
/**
* @return bool
*/
public function edit()
{
return ! empty($this->recordID());
}
/**
*
*/
public function onLoad($data = [])
{
$recordID = $this->recordID();
if ($recordID) {
$this->item = Listings::model()->item($recordID);
}
if (! empty($data)) {
$this->item->fill($data);
}
$this->loadCategory($this->item['cat_id'] ?? 0);
$this->loadUser($this->item['user_id'] ?? ($this->validationAdminPanel ? 0 : User::id()));
parent::onLoad($data);
}
public function loadCategory($catId)
{
if (! $catId) {
return;
}
$this->category = Listings::model()->category($catId);
}
public function loadCompanyData()
{
if (empty($this->company)) {
$companyId = User::companyID();
if ($companyId) {
$this->company = Business::model()->companyData(
$companyId,
['phones','contacts','geo_city','addr_addr','addr_lat','addr_lon','status']
);
}
}
return $this->company;
}
/**
* @param int $userId
* @return \modules\users\models\User
*/
public function loadUser($userId)
{
if ($userId && (($this->user->user_id ?? 0) != $userId)) {
$this->user = Users::model()->user($userId);
}
return $this->user;
}
/**
* @return bool
*/
public function isTranslate()
{
return Listings::translate();
}
public function validateTranslates(&$data)
{
if (! $this->isTranslate()) {
return;
}
# todo: admin form + seo fields (mtitle, ...)
# для любой локали должно быть заполнено и title и desc. Если не заполнены оба, то переводим.
$languages = $this->locale->getLanguages();
foreach ($languages as $l) {
if (empty($data['translates']['title'][$l]) && empty($data['translates']['descr'][$l])) {
unset($data['translates']['title'][$l], $data['translates']['descr'][$l]);
}
}
foreach ($languages as $l) {
if (isset($data['translates']['title'][$l]) && empty($data['translates']['title'][$l])) {
$this->errors->set(_t('listings', 'Enter name for language [lng]', ['lng' => $l]), 'title');
break;
}
if (isset($data['translates']['descr'][$l]) && empty($data['translates']['descr'][$l])) {
$this->errors->set(_t('listings', 'Enter a description for language [lng]', ['lng' => $l]), 'descr');
break;
}
}
}
public function validateTplFillTitles(&$data)
{
$autoTplFields = Listings::autoTplFields();
$catID = $this->category['id'];
$catData = $this->category->toArray();
$lng = $data['lang'] ?? $this->item['lang'] ?? $this->locale->current();
$nearest = Listings::catNearestParent($catID, array_keys($autoTplFields), $catData, ['lang' => $lng]);
if (! $nearest) {
return;
}
$catLang = Listings::model()->catDataLang($nearest, array_keys($autoTplFields));
foreach ($catLang as $v) {
foreach ($autoTplFields as $kk => $vv) {
if (! $catData['tpl_title_enabled']) {
continue;
}
$tpl = Listings::dp()->buildItemTemplate($catID, $v[$kk], $data, $v['lang']);
if ($this->isTranslate()) {
$data['translates'][$vv][$v['lang']] = $tpl ;
}
if ($v['lang'] == $lng) {
$data[$vv] = $tpl;
}
}
}
}
public function validateTplFillDescrList(&$data)
{
$catID = $this->category['id'];
$catData = $this->category->toArray();
$lng = $data['lang'] ?? $this->item['lang'] ?? $this->locale->current();
$nearest = Listings::catNearestParent($catID, ['tpl_descr_list'], $catData, ['lang' => $lng]);
if (! $nearest) {
return;
}
$catLang = Listings::model()->catDataLang($nearest, ['tpl_descr_list']);
foreach ($catLang as $v) {
$tpl = Listings::dp()->buildItemTemplate($catID, $v['tpl_descr_list'], $data, $v['lang']);
if ($this->isTranslate()) {
$data['translates']['descr_list'][$v['lang']] = $tpl;
}
if ($v['lang'] == $lng) {
$data['descr_list'] = $tpl;
}
}
}
public function validateTitle(&$data)
{
$this->getBlock('titleBlock')->validateLater($data);
}
public function validateItemUrl(&$data)
{
# эскейпим заголовок
$data['title_edit'] = $data['title'];
$data['title'] = HTML::escape($data['title_edit']);
# формируем URL-keyword на основе title
$data['keyword'] = mb_strtolower(func::translit($data['title_edit']));
$data['keyword'] = preg_replace("/\-+/", '-', preg_replace('/[^a-z0-9_\-]/', '', $data['keyword']));
$itemID = $this->recordID();
# формируем URL объявления (@items.search@translit-ID.html)
if (! empty($data['geo_city'])) {
$regions = GeoBase::regionParentsCache($data['geo_city']);
}
$data['link'] = Listings::url(
'item.view',
array_merge($regions['keys'] ?? [], [
'id' => $itemID,
'keyword' => $data['keyword'],
'event' => ($itemID ? 'edit' : 'add'),
'category' => $this->category['keyword'] ?? '',
'landing_url' => $this->category['landing_url'] ?? '',
'no_region' => empty($this->category['settings']['geo']['enabled']),
]),
true
);
$data['onCreate'][] = static function ($itemID, &$data) {
if (! isset($data['link'])) {
return;
}
# дополняем ссылку ID объявления
$link = str_replace('{item-id}', $itemID, $data['link'], $replaceCount);
if (!$replaceCount && mb_stripos($link, '.html') === false) {
$link = $data['link'] . $itemID . '.html';
}
Listings::model()->itemSave($itemID, ['link' => $link]);
};
}
public function submitAddCreateUser(&$data)
{
do {
# проверяем IP для неавторизованных
$banned = Users::checkBan();
if ($banned) {
$this->errors->set(_t('users', 'Access denied due to: [reason]', ['reason' => $banned]));
break;
}
$userFilter = [];
$registerData = [];
# e-mail
if (Users::registerPhone([Users::REGISTER_TYPE_BOTH, Users::REGISTER_TYPE_EMAIL])) {
if (empty($data['email']) || ! $this->input->isEmail($data['email'])) {
$this->errors->set(_t('users', 'Incorrect email'), 'email');
break;
}
# антиспам фильтр: временные ящики
if (Users::isEmailTemporary($data['email'])) {
$this->errors->set(_t('', 'The email address you provided is in the list of forbidden ones, use for example @gmail.com'), 'email');
break;
}
$userFilter['email'] = $data['email'];
$registerData['email'] = $data['email'];
}
# номер телефона
if (Users::registerPhone([Users::REGISTER_TYPE_BOTH, Users::REGISTER_TYPE_PHONE])) {
if (empty($data['phone']) || ! $this->input->isPhoneNumber($data['phone'])) {
$this->errors->set(_t('users', 'Incorrect phone number'), 'phone');
break;
}
if (empty($userFilter)) {
$userFilter['phone_number'] = $data['phone'];
}
$registerData['phone_number'] = $data['phone'];
}
# регистрируем нового или задействуем существующего пользователя
$userData = Users::model()->userDataByFilter($userFilter, [
'user_id', 'email', 'company_id', 'activated', 'activate_key',
'phone_number', 'phone_number_verified', 'blocked', 'blocked_reason',
]);
if (empty($userData)) {
# проверяем уникальность email адреса
if (! empty($data['email']) && Users::model()->userEmailExists($data['email'], $userData['user_id'] ?? 0)) {
$this->errors->set(
_t('users', 'A user with this email address is already registered. Forgot password?', [
'link_forgot' => 'href="' . Users::url('forgot') . '"',
]),
'email'
);
break;
}
if (! empty($data['phone']) && Users::model()->userPhoneExists($data['phone'], $userData['user_id'] ?? 0)) {
$this->errors->set(
_t('users', 'A user with this phone number is already registered. Forgot your password?', [
'link_forgot' => 'href="' . Users::url('forgot') . '"'
]),
'phone'
);
break;
}
# регистрируем нового пользователя
# подставляем данные в профиль из объявления
$registerData['phone'] = '';
$fields = ['name','contacts','geo_city','addr_lat','addr_lon'];
$deep = GeoBase::maxDeep();
for ($i = 1; $i <= $deep; $i++) {
$fields[] = 'geo_region' . $i;
}
foreach ($fields as $k => $v) {
if (is_int($k)) {
$k = $v;
}
if (! empty($data[$k])) {
$registerData[$v] = $data[$k];
}
}
# сохраняем первый телефон в отдельное поле
if (!empty($data['phones'])) {
$phoneFirst = reset($data['phones']);
$registerData['phone'] = $phoneFirst['v'];
}
$registerData['phones'] = $data['phones'];
$userData = Users::userRegister($registerData);
if (empty($userData['user_id'])) {
$this->errors->set(_t('users', 'Registration error, contact the administrator'), 'name');
break;
}
} else {
# пользователь существует и его аккаунт заблокирован
if ($userData['blocked']) {
$this->errors->set(_t('users', 'Access denied due to: [reason]', ['reason' => $userData['blocked_reason']]), 'name');
break;
}
if (empty($userData['activate_key'])) {
$activation = Users::updateActivationKey($userData['user_id']);
$userData['activate_key'] = $activation['key'];
}
}
$data['user_id'] = $userData['user_id'];
} while (false);
return $data['user_id'] ?? 0;
}
public function submitAddPhoneCheck(&$data, &$needActivation)
{
$userID = $data['user_id'] ?? 0;
# проверка доступности публикации объявления
if (! empty($data['company_id']) && ! Business::model()->companyActive($data['company_id'])) {
$this->errors->set(_t('item-form', 'Only logged in companies can publish listings'), 'company');
return;
}
# если пользователь авторизован и при этом не вводил номер телефона ранее
if (Users::registerPhone([Users::REGISTER_TYPE_BOTH, Users::REGISTER_TYPE_PHONE])) {
$userData = Users::model()->userData($userID, ['phone_number', 'phone_number_verified']);
if (empty($userData['phone_number']) || !$userData['phone_number_verified']) {
if (empty($data['phone']) || ! $this->input->isPhoneNumber($data['phone'])) {
$this->errors->set(_t('users', 'Incorrect phone number'), 'phone');
return;
}
if (Users::model()->userPhoneExists($data['phone'], $userID)) {
$this->errors->set(_t('users', 'A user with this phone number is already registered'), 'phone');
return;
}
$activation = Users::getActivationInfo();
Users::model()->userSave($userID, [
'activate_key' => $activation['key'],
'activate_expire' => $activation['expire'],
'phone_number' => $data['phone'],
]);
$userData['activate_key'] = $activation['key'];
$needActivation = true;
}
}
}
public function submitAddActivationInfo(&$data, &$activation)
{
$data['status'] = Listings::STATUS_NOTACTIVATED;
$activation = Listings::getActivationInfo();
$data['activate_key'] = $activation['key'];
$data['activate_expire'] = $activation['expire'];
}
public function submitAddPublicated(&$data)
{
$period = Listings::itemPublicationPeriod();
$data['publicated'] = $this->db->now();
$data['publicated_order'] = $this->db->now();
$data['publicated_period'] = $period->publishPeriod($data['publicated_period'] ?? 0, $this->category['settings'] ?? []);
$data['publicated_to'] = $period->publishTo(['period' => $data['publicated_period']]);
}
public function submitAddLimits(&$data)
{
# Проверка лимитов
if (
Listings::limitsExceed([
'user_id' => $data['user_id'],
'company_id' => $data['company_id'] ?? 0,
'cat_id' => $data['cat_id'],
], [
'silent' => true
])
) {
$data['status'] = Listings::STATUS_PUBLICATED_OUT;
}
if ($data['status'] === Listings::STATUS_PUBLICATED_OUT) {
$data['publicated_to'] = $this->db->now();
}
}
public function submitAddActivation(&$data)
{
$userID = $data['user_id'] ?? 0;
$userData = Users::model()->userData($userID, ['name', 'email', 'phone_number', 'phone_number_verified', 'activate_key']);
if (empty($userData)) {
return;
}
$activation = $data['activation'] ?? [];
if (Users::registerPhone([Users::REGISTER_TYPE_BOTH, Users::REGISTER_TYPE_PHONE])) {
# отправляем sms c кодом активации
Users::sms(false)->sendActivationCode($userData['phone_number'], $userData['activate_key']);
} else {
# отправляем письмо cо ссылкой на активацию объявления
$mailData = [
'name' => $userData['name'],
'email' => $userData['email'],
'user_id' => $userID,
'activate_link' => ($activation['link'] ?? '') . '_' . ($data['id'] ?? ''),
];
$this->app->sendMailTemplate($mailData, 'listings_item_activate', $userData['email']);
}
}
public function submitAddSvcRedirect(&$data)
{
$itemId = $data['id'] ?? 0;
if (! $itemId) {
return;
}
$userID = $data['user_id'] ?? 0;
# если у пользователя в профиле не заполнено поле "город" берём его из объявления
if (! empty($data['geo_city']) && $userID && !User::data('geo_city')) {
Users::model()->userSave($userID, ['geo_city' => $data['geo_city']]);
}
$params = [
'id' => $itemId,
'ak' => (!empty($data['activation']['key']) ? substr($data['activation']['key'], 1, 8) : ''),
];
if (bff::servicesEnabled('listings')) {
Listings::itemServices()->formParams($params);
}
$params['action'] = 'new';
$this->app->hook('listings.item.add.response.params', [
'params' => & $params,
'data' => & $data,
]);
$this->respond('successPage', Listings::url('item.status', $params));
}
}