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