1
0
Fork 0
mirror of https://github.com/Oreolek/ifhub.club.git synced 2024-06-26 03:30:48 +03:00

Поддержка ЧПУ для топиков fix #636

This commit is contained in:
Mzhelskiy Maxim 2015-03-17 15:38:47 +07:00
parent 13b2e46f12
commit 8abbcb779e
10 changed files with 361 additions and 45 deletions

View file

@ -116,6 +116,7 @@ class ActionBlog extends Action
'ajaxbloginfo',
'ajaxblogjoin',
'ajax',
'_show_topic_url',
);
/**
@ -184,8 +185,8 @@ class ActionBlog extends Action
$this->AddEventPreg('/^ajax$/i', '/^remove-avatar$/i', '/^$/i', 'EventAjaxRemoveAvatar');
$this->AddEventPreg('/^ajax$/i', '/^modal-crop-avatar$/i', '/^$/i', 'EventAjaxModalCropAvatar');
$this->AddEventPreg('/^_show_topic_url$/i', '/^$/i', 'EventInternalShowTopicByUrl');
$this->AddEventPreg('/^(\d+)\.html$/i', '/^$/i', array('EventShowTopic', 'topic'));
$this->AddEventPreg('/^[\w\-\_]+$/i', '/^(\d+)\.html$/i', array('EventShowTopic', 'topic'));
$this->AddEventPreg('/^[\w\-\_]+$/i', '/^(page([1-9]\d{0,5}))?$/i', array('EventShowBlog', 'blog'));
$this->AddEventPreg('/^[\w\-\_]+$/i', '/^bad$/i', '/^(page([1-9]\d{0,5}))?$/i', array('EventShowBlog', 'blog'));
@ -714,23 +715,85 @@ class ActionBlog extends Action
$this->SetTemplateAction('index');
}
/**
* Обработка ЧПУ топика
*/
protected function EventInternalShowTopicByUrl()
{
$sTopicUrl = Config::Get('module.topic._router_topic_original_url');
$sSecurityHash = Config::Get('module.topic._router_topic_security_hash');
/**
* Проверяем ключ
*/
if ($sSecurityHash != Config::Get('module.security.hash')) {
return $this->EventErrorDebug();
}
/**
* Проверяем корректность URL топика
* Сначала нужно получить сам топик по ID или уникальному полю Slug (транслитерированный заголовок)
* Смотрим наличие ID или Slug в маске топика
*/
$sUrlEscape = preg_quote(trim(Config::Get('module.topic.url'), '/ '));
$aMask = array_map(function ($sItem) {
return "({$sItem})";
}, Config::Get('module.topic.url_preg'));
$sPreg = strtr($sUrlEscape, $aMask);
if (preg_match('@^' . $sPreg . '$@iu', $sTopicUrl, $aMatch)) {
$aRuleRequire = array();
if (preg_match_all('#%(\w+)%#', $sUrlEscape, $aMatch2)) {
foreach ($aMatch2[1] as $k => $sFind) {
if (in_array($sFind, array('id', 'title'))) {
if (isset($aMatch[$k + 1])) {
$aRuleRequire[$sFind] = $aMatch[$k + 1];
}
}
}
}
/**
* Не удалось найти обязательные поля - запускаем обработку дальше по цепочке
*/
if (!$aRuleRequire) {
return Router::Action($sTopicUrl);
}
$oTopic = null;
/**
* Ищем топик
*/
if (isset($aRuleRequire['id'])) {
$oTopic = $this->Topic_GetTopicById($aRuleRequire['id']);
} elseif (isset($aRuleRequire['title'])) {
$oTopic = $this->Topic_GetTopicBySlug($aRuleRequire['title']);
}
if (!$oTopic) {
return Router::Action($sTopicUrl);
}
/**
* Проверяем корректность URL топика
*/
if ($oTopic->getUrl(false) != $sTopicUrl) {
Router::Location($oTopic->getUrl());
}
/**
* Направляем на стандартную обработку топика
*/
return Router::Action('blog', "{$oTopic->getId()}.html");
}
/**
* Запускаем обработку дальше по цепочке
*/
return Router::Action($sTopicUrl);
}
/**
* Показ топика
*
*/
protected function EventShowTopic()
{
$sBlogUrl = '';
if ($this->GetParamEventMatch(0, 1)) {
// из коллективного блога
$sBlogUrl = $this->sCurrentEvent;
$iTopicId = $this->GetParamEventMatch(0, 1);
$this->sMenuItemSelect = 'blog';
} else {
// из персонального блога
$iTopicId = $this->GetEventMatch(1);
$this->sMenuItemSelect = 'log';
}
$iTopicId = $this->GetEventMatch(1);
$this->sMenuItemSelect = 'blog';
$this->sMenuSubItemSelect = '';
/**
* Проверяем есть ли такой топик
@ -744,24 +807,6 @@ class ActionBlog extends Action
if (!$this->ACL_IsAllowShowTopic($oTopic, $this->oUserCurrent)) {
return parent::EventNotFound();
}
/**
* Если запросили топик из персонального блога то перенаправляем на страницу вывода коллективного топика
*/
if ($sBlogUrl != '' and $oTopic->getBlog()->getType() == 'personal') {
Router::Location($oTopic->getUrl());
}
/**
* Если запросили не персональный топик то перенаправляем на страницу для вывода коллективного топика
*/
if ($sBlogUrl == '' and $oTopic->getBlog()->getType() != 'personal') {
Router::Location($oTopic->getUrl());
}
/**
* Если номер топика правильный но УРЛ блога косяный то корректируем его и перенаправляем на нужный адрес
*/
if ($sBlogUrl != '' and $oTopic->getBlog()->getUrl() != $sBlogUrl) {
Router::Location($oTopic->getUrl());
}
/**
* Достаём комменты к топику
*/

View file

@ -479,6 +479,24 @@ class ModuleTopic extends Module
return null;
}
/**
* Получить топик по url/slug
*
* @param string $sSlug url/slug топика
* @return ModuleTopic_EntityTopic|null
*/
public function GetTopicBySlug($sSlug)
{
if (!is_scalar($sSlug)) {
return null;
}
$aTopics = $this->GetTopicsByFilter(array('topic_slug' => $sSlug), 1, 1);
if ($aTopics['collection']) {
return reset($aTopics['collection']);
}
return null;
}
/**
* Получить список топиков по списку айдишников
*
@ -1829,4 +1847,91 @@ class ModuleTopic extends Module
$this->Cache_Clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array("topic_update"));
return $res;
}
/**
* Формирует и возвращает полный ЧПУ URL для топика
*
* @param ModuleTopic_EntityTopic $oTopic
* @param bool $bAbsolute При false вернет относительный УРЛ
* @return string
*/
public function BuildUrlForTopic($oTopic, $bAbsolute = true)
{
$sUrlMask = Config::Get('module.topic.url');
$iDateCreate = strtotime($oTopic->getDateAdd());
$aReplace = array(
'%year%' => date("Y", $iDateCreate),
'%month%' => date("m", $iDateCreate),
'%day%' => date("d", $iDateCreate),
'%hour%' => date("H", $iDateCreate),
'%minute%' => date("i", $iDateCreate),
'%second%' => date("s", $iDateCreate),
'%login%' => '',
'%blog%' => '',
'%id%' => $oTopic->getId(),
'%title%' => $oTopic->getSlug(),
'%type%' => $oTopic->getType(),
);
/**
* Получаем связанные данные только если в этом есть необходимость
*/
if (strpos($sUrlMask, '%blog%') !== false) {
if (!($oBlog = $oTopic->GetBlog())) {
$oBlog = $this->Blog_GetBlogById($oTopic->getBlogId());
}
if ($oBlog) {
if ($oBlog->getType() == 'personal') {
$sUrlMask = str_replace('%blog%', '%login%', $sUrlMask);
} else {
$aReplace['%blog%'] = $oBlog->getUrl();
}
}
}
if (strpos($sUrlMask, '%login%') !== false) {
if (!($oUser = $oTopic->GetUser())) {
$oUser = $this->User_GetUserById($oTopic->getUserId());
}
if ($oUser) {
$aReplace['%login%'] = $oUser->getLogin();
}
}
$sUrl = strtr($sUrlMask, $aReplace);
return $bAbsolute ? Router::GetPathRootWeb() . '/' . $sUrl : $sUrl;
}
/**
* Формирует из строки url
*
* @param string $sText
* @return string
*/
public function MakeSlug($sText)
{
return $this->Text_Transliteration($sText);
}
/**
* Возвращает URL с учетом уникалькости по всем топикам
*
* @param string$sSlug
* @param int|null $iSkipTopicId
* @return string
*/
public function GetUniqueSlug($sSlug, $iSkipTopicId = null)
{
$iPostfix = 0;
do {
$oTopic = $this->GetTopicBySlug($sSlug . ($iPostfix ? '-' . $iPostfix : ''));
if ($oTopic and (is_null($iSkipTopicId) or $iSkipTopicId != $oTopic->getId())) {
$iPostfix++;
$bNeedNext = true;
} else {
$bNeedNext = false;
}
} while ($bNeedNext);
return $sSlug . ($iPostfix ? '-' . $iPostfix : '');
}
}

View file

@ -59,6 +59,12 @@ class ModuleTopic_EntityTopic extends Entity
'allowEmpty' => Config::Get('module.topic.title_allow_empty'),
'label' => $this->Lang_Get('topic.add.fields.title.label')
);
$this->aValidateRules[] = array(
'topic_slug_raw',
'regexp',
'allowEmpty' => true,
'pattern' => '#^[a-z0-9\-]{1,500}$#i'
);
$this->aValidateRules[] = array(
'topic_text_source',
'string',
@ -79,6 +85,7 @@ class ModuleTopic_EntityTopic extends Entity
$this->aValidateRules[] = array('blogs_id_raw', 'blogs');
$this->aValidateRules[] = array('topic_text_source', 'topic_unique');
$this->aValidateRules[] = array('topic_slug_raw', 'slug_check');
}
/**
@ -124,6 +131,49 @@ class ModuleTopic_EntityTopic extends Entity
return $this->Lang_Get('topic.add.notices.error_type');
}
/**
* Проверка URL топика
*
* @param string $sValue Проверяемое значение
* @param array $aParams Параметры
* @return bool|string
*/
public function ValidateSlugCheck($sValue, $aParams)
{
if (!$this->User_GetIsAdmin()) {
/**
* Простому пользователю разрешаем менять url только в течении X времени после создания топика
* Причем не прямую смену url, а через транлитерацию заголовка топика
*/
if ($this->getId()) {
if (strtotime($this->getDateAdd()) < time() - 60 * 60 * 1) {
/**
* Не меняем url
*/
return true;
}
}
/**
* Для нового топика всегда формируем url
*/
$this->setSlugRaw('');
}
if ($this->getSlugRaw()) {
$this->setSlug($this->Topic_GetUniqueSlug($this->getSlugRaw(), $this->getId()));
} elseif ($this->getTitle()) {
if ($sUrl = $this->Topic_MakeSlug($this->getTitle())) {
/**
* Получаем уникальный URL
*/
$this->setSlug($this->Topic_GetUniqueSlug($sUrl, $this->getId()));
} else {
return $this->Lang_Get('topic.add.notices.error_slug');
}
}
return true;
}
/**
* Проверка топика на уникальность
*
@ -295,6 +345,16 @@ class ModuleTopic_EntityTopic extends Entity
return $this->_getDataOne('topic_title');
}
/**
* Возвращает url топика
*
* @return string|null
*/
public function getSlug()
{
return $this->_getDataOne('topic_slug');
}
/**
* Возвращает текст топика
*
@ -613,15 +673,12 @@ class ModuleTopic_EntityTopic extends Entity
/**
* Возвращает полный URL до топика
*
* @param bool $bAbsolute При false вернет относительный УРЛ
* @return string
*/
public function getUrl()
public function getUrl($bAbsolute = true)
{
if ($this->getBlog()->getType() == 'personal') {
return Router::GetPath('blog') . $this->getId() . '.html';
} else {
return Router::GetPath('blog') . $this->getBlog()->getUrl() . '/' . $this->getId() . '.html';
}
return $this->Topic_BuildUrlForTopic($this, $bAbsolute);
}
/**
@ -963,6 +1020,16 @@ class ModuleTopic_EntityTopic extends Entity
$this->_aData['topic_title'] = $data;
}
/**
* Устанавливает url топика
*
* @param string $data
*/
public function setSlug($data)
{
$this->_aData['topic_slug'] = $data;
}
/**
* Устанавливает текст топика
*

View file

@ -44,6 +44,7 @@ class ModuleTopic_MapperTopic extends Mapper
user_id,
topic_type,
topic_title,
topic_slug,
topic_tags,
topic_date_add,
topic_user_ip,
@ -55,10 +56,10 @@ class ModuleTopic_MapperTopic extends Mapper
topic_forbid_comment,
topic_text_hash
)
VALUES(?d, ?d, ?d, ?d, ?d, ?d, ?, ?, ?, ?, ?, ?d, ?d, ?d, ?d, ?, ?, ?)
VALUES(?d, ?d, ?d, ?d, ?d, ?d, ?, ?, ?, ?, ?, ?, ?d, ?d, ?d, ?d, ?, ?, ?)
";
if ($iId = $this->oDb->query($sql, $oTopic->getBlogId(), $oTopic->getBlogId2(), $oTopic->getBlogId3(),
$oTopic->getBlogId4(), $oTopic->getBlogId5(), $oTopic->getUserId(), $oTopic->getType(), $oTopic->getTitle(),
$oTopic->getBlogId4(), $oTopic->getBlogId5(), $oTopic->getUserId(), $oTopic->getType(), $oTopic->getTitle(), $oTopic->getSlug(),
$oTopic->getTags(), $oTopic->getDateAdd(), $oTopic->getUserIp(), $oTopic->getPublish(),
$oTopic->getPublishDraft(), $oTopic->getPublishIndex(), $oTopic->getSkipIndex(), $oTopic->getCutText(),
$oTopic->getForbidComment(), $oTopic->getTextHash())
@ -518,6 +519,7 @@ class ModuleTopic_MapperTopic extends Mapper
blog_id4= ?d,
blog_id5= ?d,
topic_title= ?,
topic_slug= ?,
topic_tags= ?,
topic_date_add = ?,
topic_date_edit = ?,
@ -542,7 +544,7 @@ class ModuleTopic_MapperTopic extends Mapper
topic_id = ?d
";
$res = $this->oDb->query($sql, $oTopic->getBlogId(), $oTopic->getBlogId2(), $oTopic->getBlogId3(),
$oTopic->getBlogId4(), $oTopic->getBlogId5(), $oTopic->getTitle(), $oTopic->getTags(),
$oTopic->getBlogId4(), $oTopic->getBlogId5(), $oTopic->getTitle(), $oTopic->getSlug(), $oTopic->getTags(),
$oTopic->getDateAdd(), $oTopic->getDateEdit(), $oTopic->getDateEditContent(), $oTopic->getUserIp(),
$oTopic->getPublish(), $oTopic->getPublishDraft(), $oTopic->getPublishIndex(), $oTopic->getSkipIndex(),
$oTopic->getRating(), $oTopic->getCountVote(), $oTopic->getCountVoteUp(), $oTopic->getCountVoteDown(),
@ -590,6 +592,9 @@ class ModuleTopic_MapperTopic extends Mapper
if (isset($aFilter['topic_date_more'])) {
$sWhere .= " AND t.topic_date_add > " . $this->oDb->escape($aFilter['topic_date_more']);
}
if (isset($aFilter['topic_slug'])) {
$sWhere .= " AND t.topic_slug = " . $this->oDb->escape($aFilter['topic_slug']);
}
if (isset($aFilter['topic_publish'])) {
$sWhere .= " AND t.topic_publish = " . (int)$aFilter['topic_publish'];
}

View file

@ -149,6 +149,43 @@ $config['module']['topic']['default_period_discussed'] = 1; // Дефолтны
$config['module']['topic']['max_blog_count'] = 3; // Количество блогов, которые можно задать топику. Максимальное значение 5.
$config['module']['topic']['max_rss_count'] = 20; // Максимальное количество топиков в RSS потоке
$config['module']['topic']['default_preview_size'] = '900x300crop'; // Дефолтный размер превью для топика (все размеры задаются в конфиге media), который будет использоваться, например, для Open Graph
/**
* Настройка ЧПУ URL топиков
* Допустимы шаблоны:
* %year% - год топика (2010)
* %month% - месяц (08)
* %day% - день (24)
* %hour% - час (17)
* %minute% - минуты (06)
* %second% - секунды (54)
* %login% - логин автора топика (admin)
* %blog% - url основного блога (report), если топик без блога, то этот параметр заменится на логин автора топика
* %id% - id топика (325)
* %title% - заголовок топика в транслите (moy_topic)
* %type% - тип топика (news)
*
* В шаблоне обязательно должен быть %id% или %title%, это необходимо для точной идентификации топика
*/
$config['module']['topic']['url'] = '%year%/%month%/%day%/%id%.html';
/**
* Список регулярных выражений для частей URL топика
* Без необходмых навыков лучше этот параметр не менять
*/
$config['module']['topic']['url_preg'] = array(
'%year%' => '\d{4}',
'%month%' => '\d{2}',
'%day%' => '\d{2}',
'%hour%' => '\d{2}',
'%minute%' => '\d{2}',
'%second%' => '\d{2}',
'%login%' => '[\w\-\_]+',
'%blog%' => '[\w\-\_]+',
'%id%' => '\d+',
'%title%' => '[\w\-\_]+',
'%type%' => '[\w\-\_]+',
);
// Модуль User
$config['module']['user']['per_page'] = 15; // Число юзеров на страницу на странице статистики и в профиле пользователя
$config['module']['user']['friend_on_profile'] = 15; // Ограничение на вывод числа друзей пользователя на странице его профиля

View file

@ -73,6 +73,15 @@
entity = 'ModuleTopic_EntityTopic'
label = $aLang.topic.add.fields.title.label}
{* URL топика *}
{if $oUserCurrent->isAdministrator()}
{component 'field' template='text'
name = 'topic[topic_slug_raw]'
value = {(( $topic ) ? $topic->getSlug() : '')|escape}
note = {lang 'topic.add.fields.slug.note'}
label = {lang 'topic.add.fields.slug.label'}}
{/if}
{block 'add_topic_form_text_before'}{/block}

View file

@ -1080,6 +1080,10 @@ return array(
'title' => array(
'label' => 'Заголовок'
),
'slug' => array(
'label' => 'URL',
'note' => 'Формируется автоматически из названия топика, но вы можете задать свое значение'
),
'text' => array(
'label' => 'Текст'
),
@ -1114,6 +1118,7 @@ return array(
'error_blog_not_allowed' => 'Вы не можете писать в этот блог',
'error_text_unique' => 'Вы уже писали топик с таким содержанием',
'error_type' => 'Неверный тип топика', // TODO: Remove?
'error_slug' => 'Необходимо указать URL топика',
'error_favourite_draft' => 'Топик из черновиков нельзя добавить в избранное',
'time_limit' => 'Вам нельзя создавать топики слишком часто',
'rating_limit' => 'Вам не хватает рейтинга для создания топика',

View file

@ -872,3 +872,6 @@ ALTER TABLE `prefix_invite_use`
-- 07.03.2015
ALTER TABLE `prefix_user` CHANGE `user_referal_code` `user_referral_code` VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;
-- 17.03.2015
ALTER TABLE `prefix_topic` ADD `topic_slug` VARCHAR(500) NOT NULL DEFAULT '' AFTER `topic_title`, ADD INDEX (`topic_slug`) ;

View file

@ -61,15 +61,55 @@ require_once($sPathToFramework . '/config/loader.php');
/**
* Определяем дополнительные параметры роутинга
*/
$aRouterParams = array(/*
'callback_after_parse_url' => function() {
$aRouterParams = array(
'callback_after_parse_url' => array(
function () {
/**
* Логика по ЧПУ топиков
* Если URL соответствует шаблону ЧПУ топика, перенаправляем обработку на экшен/евент /blog/_show_topic_url/
* Через свои параметры конфига передаем исходный URL и ключ из конфига 'module.security.hash', ключ нужен для проверки валидности запроса.
* Если ключ верный, то 100% это внутренняя обработка, а не произвольное внешнее обращение к URL
* Суть обработки _show_topic_url в том, чтобы определить ID топика и корректность его URL, если он некорректен, то произвести его корректировку через внешний редирект на правильный URL
* Если удалось определить топик и URL корректный, то происходит внутренний редирект на стандартный евент отображения топика по ID (/blog/12345.html)
*/
}
*/
$sUrlRequest = '';
if (Router::GetAction()) {
$sUrlRequest .= Router::GetAction();
}
if (Router::GetActionEvent()) {
$sUrlRequest .= '/' . Router::GetActionEvent();
}
if (Router::GetParams()) {
$sUrlRequest .= '/' . join('/', Router::GetParams());
}
/**
* Функция для формирования регулярного выражения по маске URL топика
*
* @param string $sUrl
* @return string
*/
$funcMakePreg = function ($sUrl) {
$sUrl = preg_quote(trim($sUrl, '/ '));
return strtr($sUrl, Config::Get('module.topic.url_preg'));
};
$sPreg = $funcMakePreg(Config::Get('module.topic.url'));
if (preg_match('@^' . $sPreg . '$@iu', $sUrlRequest)) {
Router::SetAction('blog');
Router::SetActionEvent('_show_topic_url');
Router::SetParams(array());
/**
* Хак - через конфиг передаем нужные параметры в обработчик эвента
* Модуль кеша здесь нельзя использовать, т.к. еще не произошло инициализации ядра
*/
Config::Set('module.topic._router_topic_original_url', $sUrlRequest);
Config::Set('module.topic._router_topic_security_hash', Config::Get('module.security.hash'));
}
}
)
);
/**
* Проверяем наличие директории install
*/

@ -1 +1 @@
Subproject commit ea1a9ac4c47041e76c8d2665dc0f95cc4f48dfe1
Subproject commit ae7dfb5d8bb201af9937d7040bcbab4d2f23b8d3