1. Создаём в корне сайта папку, например, my_scripts (в моём случае я сделал в ней ещё одну подпапку for_us - это не обязательно);
2. На всякий случай создаем в этой папке файл .htaccess, которым блокируем доступ для всех, кроме себя. Что-то типо:
# Закрываем доступ к PHP файлам извне
<FilesMatch ".php$">
Deny from all
# Мой домашний провайдер
Allow from 888.88.888.8 ----- разумеется, это меняете на свой IP или подсеть
# Рабочий IP
# Allow from 88.888.88.88
</FilesMatch>
3. Создаём в этой папке файл rename.php
<?php
ini_set("display_errors", "1"); // выводим ошибки php
error_reporting(E_ALL);
// SETTING
$url_start = 'catalog/';
$url_end = '-detail';
$max_h1_length = 60; // макс. длина заголовка
$max_alias_length = 47; // макс. длина псевдонима
$root_folder = 'public_html'; // название корневой папки сайта на хостинге (обычно public_html или www)
$debug_mode = 0; // 0 или 1
?>
<!DOCTYPE html>
<html lang="ru-ru" dir="ltr">
<head>
<title>Исправление длинных названий и псевдонимов товаров</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
<style>
body {font-family: monospace;}
.wrap {max-width: 1366px; margin: 0 auto 100px; /* border-right: 1px solid #ccc; */}
ul {padding: 0;}
.item {border-top: 1px solid #ccc; padding: 10px 0 10px 10px; list-style-type: none;}
.item:last-child {border-bottom: 1px solid #ccc;}
.item:nth-child(even) {background: #f5f5f5;}
.item:hover {background: #ecfcff;}
.item.done, .item.done * {color: #ccc !important; border-color: #ddd;}
.item-head pre, .item-dop pre {display: inline-block; margin-right: 30px;}
.item-head pre:last-child, .item-dop pre:last-child {margin-right: 0;}
.item-name {font-weight: bold; margin-left: 2px;}
.item-sku {color: rgba(0,0,0,0.5);}
.item-id {color: rgba(0,0,0,0.5);}
.item-alias {color: rgba(0,0,0,0.5); max-width: 510px;}
.item-fields {padding-left: 0;}
.item-header {display: inline-block; margin-right: 30px;}
.item-slug {display: inline-block;}
.item-actions {position: relative;}
.item-actions, .item-actions > * {display: inline-block;}
.item-actions .result {position: absolute; top: 0; width: 150px; right: -150px;}
.item-dop {color: rgba(0,0,0,0.5);}
button {margin: 0 15px 0 25px; padding: 4px 10px; border: 1px solid #aaa; border-radius: 3px; cursor: pointer;}
input {border: 1px solid #ccc; border-radius: 3px; font-family: monospace; padding: 4px 1px;}
pre {margin: 10px 0;}
pre.label {margin-right: 10px;}
.length {margin: -5px 0 0 3px;}
.item .error {color: red !important;}
.item .success {color: green !important;}
a {color: rgba(0,0,0,0.5); text-decoration: none;}
a:hover {color: #333; text-decoration: underline;}
</style>
</head>
<body>
<div class="wrap">
<h1>Исправление длинных названий и псевдонимов товаров</h1>
Одновременно с обновлением псевдонимов создаются редиректы со старых ulr на новые.<br>
<?php
// определяем корневую папку сайта
$script_folder = dirname(__FILE__);
$root_arr = explode('/', $script_folder);
while (end($root_arr) != $root_folder) array_pop($root_arr);
$root = '';
foreach ($root_arr as $root_item) {
if ($root_item != '') $root .= '/'.$root_item;
}
if ($debug_mode == 1) echo "root = $root<br>\n";
// определим путь к скрипту от Главной
$root_arr = explode('/', $script_folder);
while (reset($root_arr) != $root_folder) array_shift($root_arr);
array_shift($root_arr);
$ajax_file_path = '';
foreach ($root_arr as $root_item) $ajax_file_path .= '/'.$root_item;
if ($debug_mode == 1) echo "ajax_file_path = $ajax_file_path<br>\n";
// Работа с БД
$require = require_once "$root/configuration.php"; // настройки сайта и БД
if ($require != true) die('Не удалось подключить файл конфигурации Joomla.'); else if ($debug_mode == 1) echo "Подключили файл конфигурации Joomla.<br>\n";
$config = new JConfig;
$prefix = $config->dbprefix;
$mysqli = new mysqli($config->host, $config->user, $config->password, $config->db);
if ($mysqli->connect_error) {
die('Ошибка соединения с БД: ('.$mysqli->connect_errno.') '.$mysqli->connect_error);
} else if ($debug_mode == 1) echo "Подключились к БД.<br>\n";
$results = $mysqli->query("
SELECT
COUNT(*) AS count
FROM
".$prefix."virtuemart_products p
LEFT JOIN
".$prefix."virtuemart_products_ru_ru pru USING(virtuemart_product_id)
WHERE
(LENGTH(pru.slug) > $max_alias_length OR CHAR_LENGTH(pru.product_name) > $max_h1_length)
AND p.published = 1
");
if ($results) {
if ($debug_mode == 1) echo "Получили результаты запроса SELECT с общим кол-вом.<br>\n";
} else {
print 'Ошибка выполнения запроса в БД: ('.$mysqli->errno.') '.$mysqli->error;
}
$row = $results->fetch_assoc();
$number = $row['count'];
$results = $mysqli->query("
SELECT
p.virtuemart_product_id, p.product_sku, pru.product_name, pru.slug, p.product_mpn, mru.mf_name
FROM
".$prefix."virtuemart_products p
LEFT JOIN
".$prefix."virtuemart_products_ru_ru pru USING(virtuemart_product_id)
LEFT JOIN
".$prefix."virtuemart_product_manufacturers pm USING(virtuemart_product_id)
LEFT JOIN
".$prefix."virtuemart_manufacturers_ru_ru mru ON pm.virtuemart_manufacturer_id = mru.virtuemart_manufacturer_id
WHERE
(LENGTH(pru.slug) > $max_alias_length OR CHAR_LENGTH(pru.product_name) > $max_h1_length)
AND p.published = 1
GROUP BY
p.virtuemart_product_id
ORDER BY
LENGTH(pru.slug) DESC, pru.product_name ASC
LIMIT 20
");
// GROUP BY - чтобы не выводить один и тот же товар несколько раз, если ему назначено сразу несколько производителей
if ($results) {
echo "По результатам запросов к БД выведено записей: ".$mysqli->affected_rows." из $number.<br>\n";
} else {
print 'Ошибка выполнения запроса в БД: ('.$mysqli->errno.') '.$mysqli->error;
}
// закрываем подключение
$mysqli->close();
unset($mysqli);
if ($debug_mode == 1) echo "Закрыли подключение к БД.<br>\n<br>\n";
echo "<ul>\n";
while ($row = $results->fetch_assoc()) {
if ($debug_mode == 1) {
echo "<pre>";
print_r($row);
echo "</pre>";
}
$search_text = '';
if (isset($row['mf_name'])) $search_text .= $row['mf_name'].' ';
if ($row['product_mpn']) $search_text .= $row['product_mpn'].' ';
$search_text .= $row['product_name'];
?>
<li class='item' id='<?=$row['virtuemart_product_id'] ?>'>
<div class='item-head'>
<pre class='item-name '><?=$row['product_name'] ?></pre>
<pre class='item-sku '><?=$row['product_sku'] ?></pre>
<pre class='item-id '><?=$row['virtuemart_product_id'] ?></pre>
<pre class='item-alias'><?=$row['slug'] ?></pre>
</div>
<div class='item-fields'>
<div class='item-header'>
<input type='text' class='product-name' id='prod-name-<?=$row['virtuemart_product_id'] ?>' name='product-name-<?=$row['virtuemart_product_id'] ?>' value='<?=$row['product_name'] ?>' size='70' />
</div>
<div class='item-slug'>
<input type='text' class='product-slug' name='product-slug-<?=$row['virtuemart_product_id'] ?>' value='<?=$row['slug'] ?>' size='70' />
</div>
<div class='item-actions'>
<button class='' type='submit'>ОК</button>
<div class='result'></div>
</div>
</div>
<div class='item-dop'>
<?php if (!empty($row['mf_name'])) { ?>
<pre class='label'>Производитель:</pre><pre class='item-manuf'><?=$row['mf_name'] ?></pre>
<?php }
if (!empty($row['product_mpn'])) { ?>
<pre class='label'>Артикул произ.:</pre><pre class='item-mpn'><?=$row['product_mpn'] ?></pre>
<?php } ?>
<pre class='item-admin-link'><a href='/administrator/index.php?option=com_virtuemart&view=product&task=edit&virtuemart_product_id=<?=$row['virtuemart_product_id'] ?>' target='_blank'>Товар в админке</a></pre>
<pre class='item-front-link'><a href='/<?=$url_start.$row['slug'].$url_end ?>' target='_blank'>Товар на сайте</a></pre>
<pre class='item-yandex-link'><a href='//yandex.ru/search/?text=<?=$search_text ?>' target='_blank'>Найти в Яндекс</a></pre>
<pre class='item-google-link'><a href='//www.google.ru/search?q=<?=$search_text ?>' target='_blank'>Найти в Google</a></pre>
</div>
</li>
<?php }
echo "</ul>";
?>
</div>
<script>
// функция пригодится для изменения счетчиков
function countChange(inputObj, max) {
var simb_new = $(inputObj).val().length;
var id = $(inputObj).parent().parent().parent().attr('id');
var whatClass = $(inputObj).attr('class');
if (whatClass == 'product-name') { var what = 'name'; }
else if (whatClass == 'product-slug') { var what = 'slug'; }
$('span.'+ what + '.length_' + id).text(simb_new);
if (simb_new > max) {
$('span.' + what + '.length_' + id).css({'color':'red'});
} else {
$('span.' + what + '.length_' + id).css({'color':'green'});
}
}
// Счётчики симоволов в названиях
var max_h1_length = <?=$max_h1_length ?>;
var selector_name = 'input.product-name';
$(selector_name).each(function() {
var inputObj = $(this);
var id = $(this).parent().parent().parent().attr('id');
$(this).after('<span class="length name length_' + id + '" style="position: absolute;"></span>');
countChange($(this), max_h1_length);
$(this).on('input', function() {
countChange(inputObj, max_h1_length);
});
});
// Счётчики симоволов в псевдонимах
var max_alias_length = <?=$max_alias_length ?>;
var selector_slug = 'input.product-slug';
$(selector_slug).each(function() {
var inputObj = $(this);
var id = $(this).parent().parent().parent().attr('id');
$(this).after('<span class="length slug length_' + id + '" style="position: absolute;"></span>');
countChange($(this), max_alias_length);
$(this).on('input', function() {
countChange(inputObj, max_alias_length);
});
});
// функция транслитерации
function urlLit(w,v) {
//var tr='a b v g d e ["zh","j"] z i y k l m n o p r s t u f h c ch sh ["shh","shch"] ~ y ~ e yu ya ~ ["jo","e"]'.split(' ');
var tr='a b v g d e ["j","zh"] z i y k l m n o p r s t u f h c ch sh ["shh","shch"] ~ y ~ e yu ya ~ ["jo","e"]'.split(' ');
var ww=''; w=w.toLowerCase();
for (i=0; i<w.length; ++i) {
cc=w.charCodeAt(i); ch=(cc>=1072?tr[cc-1072]:w[i]);
if(ch.length<3) ww+=ch; else ww+=eval(ch)[v];
}
return(ww.replace(/[^a-zA-Z0-9\-]/g,'-').replace(/[-]{2,}/gim, '-').replace( /^\-+/g, '').replace( /\-+$/g, ''));
}
$('input.product-slug').change(function() {
// при снятии фокуса с алиаса делаем транслит
$(this).val(urlLit($(this).val(),1)); // 1 - это версия (для ж, щ, ё)
// и после транслитра пересчет символов
countChange($(this), max_alias_length);
});
$('button').click(function() {
// очистим предыдущий результат, если был
$(this).siblings('.result').html('');
var buttonObj = $(this);
// сначала сделаем транслит алиаса
$(this).parent().parent().children('.item-slug').children('.product-slug').val(
urlLit($(this).parent().parent().children('.item-slug').children('.product-slug').val(),1)
);
// затем пересчитаем символы
countChange($(this).parent().parent().children('.item-slug').children('.product-slug'), max_alias_length);
var name_symb = $(this).parent().parent().children('.item-header').children('.length').text();
var alias_symb = $(this).parent().parent().children('.item-slug').children('.length').text();
if (name_symb <= max_h1_length && alias_symb <= max_alias_length) {
// alert('Название и алиас норм: ' + name_symb + ' и ' + alias_symb + ' символов.');
var old_name = $(this).parent().parent().parent().children('.item-head').children('.item-name').text();
var new_name = $(this).parent().parent().children('.item-header').children('input').val();
var old_alias = $(this).parent().parent().parent().children('.item-head').children('.item-alias').text();
var new_alias = $(this).parent().parent().children('.item-slug').children('input').val();
var update_data = {}; // создали объект
update_data.prod_id = $(this).parent().parent().parent().children('.item-head').children('.item-id').text();
update_data.url_start = '<?=$url_start ?>';
update_data.url_end = '<?=$url_end ?>';
if (new_name != old_name) {
update_data.new_name = new_name;
} else {
new_name = null;
}
if (new_alias != old_alias) {
update_data.old_alias = old_alias;
update_data.new_alias = new_alias;
} else {
new_alias = null;
}
if (new_name != null || new_alias != null) {
var ajax = $.ajax({
type: 'POST',
url: '<?=$ajax_file_path ?>/rename_ajax.php',
data: update_data
});
ajax.done(function(data) {
$(buttonObj).siblings('.result').html(data);
// если в ответе есть Done
if (data.search(/done/i) > -1) {
$(buttonObj).parent().parent().parent().addClass('done');
}
});
ajax.fail(function() {
$(buttonObj).siblings('.result').html('<span class="error">Error</span>');
});
}
} else {
alert('Не косячить!\n\nCимволов в названии: ' + name_symb + '\n' + 'Символов в алиасе: ' + alias_symb);
}
});
</script>
</body>
</html>
4. В созданном файле есть блок настроек. Правим значения, разумеется, под свой сайт:
// SETTING
$url_start = 'catalog/';
$url_end = '-detail';
$max_h1_length = 60; // макс. длина заголовка
$max_alias_length = 47; // макс. длина псевдонима
$root_folder = 'public_html'; // название корневой папки сайта на хостинге (обычно public_html или www)
$debug_mode = 0; // 0 или 1
5. Создаём там же второй файл rename_ajax.php
<?php
ini_set("display_errors", "1"); // выводим ошибки php
error_reporting(E_ALL);
// print_r($_POST);
if (isset($_POST['prod_id']) && (isset($_POST['new_name']) || isset($_POST['new_alias']))) {
$url_start = $_POST['url_start'];
$url_end = $_POST['url_end'];
$script_folder = dirname(__FILE__);
// определяем корневую папку сайта
$root_arr = explode('/', $script_folder);
while (end($root_arr) != 'public_html') array_pop($root_arr);
$root = '';
foreach ($root_arr as $root_item) $root .= '/'.$root_item;
// Работа с БД
$require = require_once "$root/configuration.php"; // настройки сайта и БД
if ($require != true) die('Не удалось подключить файл конфигурации Joomla.');
$config = new JConfig;
$prefix = $config->dbprefix;
$mysqli = new mysqli($config->host, $config->user, $config->password, $config->db);
if ($mysqli->connect_error) die('Ошибка соединения с БД: ('.$mysqli->connect_errno.') '.$mysqli->connect_error);
$sql = "UPDATE ".$prefix."virtuemart_products_ru_ru AS pru, ".$prefix."virtuemart_products AS p SET ";
$nowGmDate = gmdate('Y-m-d H:i:s');
$sql .= "p.modified_by = 0, p.modified_on = '$nowGmDate' ";
if (isset($_POST['new_name'])) {
$new_name = $mysqli->real_escape_string($_POST['new_name']);
$sql .= ", pru.product_name = '$new_name' ";
}
$was_alias = false;
if (isset($_POST['new_alias'])) {
$new_alias = $_POST['new_alias'];
$new_alias = htmlspecialchars($new_alias, ENT_QUOTES);
// сначала проверим, нет ли уже такого алиаса у других товаров
$subSql = "SELECT COUNT(*) AS count FROM ".$prefix."virtuemart_products_ru_ru WHERE slug = '$new_alias'";
$results0 = $mysqli->query($subSql);
$row = $results0->fetch_assoc();
if ($row['count'] > 0) $was_alias = true;
unset($subSql, $results0, $row);
// продолжаем формировать запрос
if ($was_alias != true) $sql .= ", pru.slug = '$new_alias' ";
}
$sql .= "WHERE pru.virtuemart_product_id = ".$_POST['prod_id']." AND p.virtuemart_product_id = pru.virtuemart_product_id";
// echo $sql;
$results = $mysqli->query($sql);
if ($results) {
echo "<span class='success'>Done</span>";
if ($was_alias == true) echo "<br><span class='error'>Error: alias exists</span>";
} else {
echo "<span class='error'>Error: ($mysqli->errno) $mysqli->error</span>";
}
// делаем редирект (через стандартный Менеджер перенаправлений)
if (isset($new_alias) && isset($_POST['old_alias'])) {
if ($was_alias == true) {
echo "<br><span class='error'>301 not created</span>";
}
else {
// echo "Надо редирект";
$old_alias = $_POST['old_alias'];
// $new_alias определили выше
$old_url = $url_start.$old_alias.$url_end;
$new_url = $url_start.$new_alias.$url_end;
// проверим, не было ли ранее такого редиректа
$subSql = "SELECT COUNT(*) AS count FROM ".$prefix."redirect_links WHERE old_url LIKE '%$old_url%'";
// echo $subSql;
$results1 = $mysqli->query($subSql);
$row = $results1->fetch_assoc();
unset($subSql, $results1);
if ($row['count'] > 0) {
$sql = "UPDATE ".$prefix."redirect_links
SET new_url = '$new_url', comment = 'Product renamed', published = 1, modified_date = '$nowGmDate', header = 301
WHERE old_url LIKE '%$old_url%'
";
// echo $sql;
$results2 = $mysqli->query($sql);
if ($results2) {
echo "<br><span class='success'>301 rewritten</span>";
} else {
echo "<br><span class='error'>Error: ($mysqli->errno) $mysqli->error</span>";
}
}
else {
$sql = "INSERT INTO ".$prefix."redirect_links (
old_url, new_url, comment, published, created_date, modified_date, header
)
VALUES (
'$old_url', '$new_url', 'Product renamed', 1, '$nowGmDate', '$nowGmDate', 301
)";
// echo $sql;
$results2 = $mysqli->query($sql);
if ($results2) {
echo "<br><span class='success'>301 done</span>";
} else {
echo "<br><span class='error'>Error: ($mysqli->errno) $mysqli->error</span>";
}
}
}
}
// закрываем подключение
$mysqli->close();
unset($mysqli);
}
?>
6. Пробуем открыть скрипт в браузере примерно по такой ссылке: site.ru/my_scripts/for_us/rename.php
UPD: Чтобы стандартные редиректы Joomla работали, в VirtueMart надо отключить обработку ошибки 404.
Примечания:
Вероятно, у многих не такие настройки роутера VirtueMart (и пунктов меню), как у меня на этом сайте.
Поэтому, скорее всего, копирование 1 в 1 не даст нужного эффекта для редиректов.
Однако, дело это поправимое, если что
Мысли есть, как это проще реализовать с другими настройками Вирта.