
Автоматическая замена HTTP на HTTPS в контенте сайта на 1С-Битрикс
Проблема смешанного контента (Mixed Content)
Даже после перевода сайта на HTTPS на практике часто остаётся скрытая проблема — HTTP-ссылки внутри контента.
Они могут находиться:
- в детальных описаниях товаров
- в анонсах
- в HTML-свойствах инфоблоков
- в старых текстах, импортированных из внешних источников
В результате возникают:
- предупреждения Mixed Content
- дубли URL (http:// и https://)
- некорректная передача ссылочного веса
- снижение доверия поисковых систем
Особенно часто это встречается на сайтах, работающих на 1С-Битрикс с большим количеством инфоблоков.
Почему ручное исправление — плохое решение
Ручная правка контента в админке:
- занимает десятки часов
- не масштабируется
- легко приводит к ошибкам
не подходит для сайтов с тысячами элементов
Для технического SEO нужен автоматизированный и безопасный способ.
Решение: SEO-скрипт массовой замены HTTP → HTTPS
Мы разработали PHP-скрипт для 1С-Битрикс, который автоматически приводит все ссылки в контенте к HTTPS.
Что делает скрипт
Скрипт проходит по всем инфоблокам сайта и анализирует HTML-контент элементов.
Он обрабатывает:
- анонс (PREVIEW_TEXT, тип HTML)
- детальное описание (DETAIL_TEXT, тип HTML)
- все свойства инфоблоков с типом HTML
- одиночные
- множественные
- Как именно происходит замена
Поиск ссылок - Скрипт находит все вхождения http://, включая:
- ссылки в HTML-атрибутах:
- href
- src
- srcset
- data-src
- data-href
- action
- poster
- formaction
«голые» URL внутри текста (без тегов)
Замена
http://example.com → https://example.com
https:// ссылки не трогаются
структура HTML не нарушается
домен и путь URL не меняются
Что важно для SEO: что скрипт НЕ делает
Это принципиально:
❌ не меняет домены
❌ не добавляет редиректы
❌ не трогает canonical
❌ не вмешивается в robots / meta-теги
❌ не изменяет структуру URL
❌ не ломает вёрстку и HTML
Скрипт решает ровно одну SEO-задачу — нормализацию протокола.
<?php
/**
* Bitrix: replace http:// -> https:// in ALL iblocks:
* - elements: PREVIEW_TEXT, DETAIL_TEXT
* - element properties with USER_TYPE = HTML
*
* Safe batching + resume + logging.
*/
@set_time_limit(0);
@ini_set('memory_limit', '1024M');
@ini_set('display_errors', '1');
error_reporting(E_ALL);
if (PHP_SAPI !== 'cli') {
die("Run from CLI only.\n");
}
/** Find DOCUMENT_ROOT automatically (walk up until /bitrix exists) */
function findDocRoot(string $startDir): string {
$dir = $startDir;
for ($i = 0; $i < 10; $i++) {
if (is_dir($dir . '/bitrix')) return $dir;
$parent = dirname($dir);
if ($parent === $dir) break;
$dir = $parent;
}
return $startDir;
}
$_SERVER['DOCUMENT_ROOT'] = findDocRoot(__DIR__);
define('BX_ROOT', '/bitrix');
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_NO_ACCELERATOR_RESET', true);
require($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
if (!\Bitrix\Main\Loader::includeModule('iblock')) {
die("Cannot include iblock module.\n");
}
$args = getopt('', [
'step::', // batch size
'iblock::', // only this iblock id
'dry-run::', // 1 = no save
'progress::', // custom progress file
'log::', // custom log file
]);
$step = isset($args['step']) ? max(10, (int)$args['step']) : 200;
$onlyIblock= isset($args['iblock']) ? (int)$args['iblock'] : 0;
$dryRun = !empty($args['dry-run']) && (int)$args['dry-run'] === 1;
$progressFile = !empty($args['progress'])
? (string)$args['progress']
: ($_SERVER['DOCUMENT_ROOT'] . '/upload/http2https_progress.json');
$logFile = !empty($args['log'])
? (string)$args['log']
: ($_SERVER['DOCUMENT_ROOT'] . '/upload/http2https_replace.log');
function logLine(string $file, string $line): void {
file_put_contents($file, '[' . date('Y-m-d H:i:s') . '] ' . $line . "\n", FILE_APPEND);
}
function loadProgress(string $file): array {
if (!file_exists($file)) return [];
$json = file_get_contents($file);
$data = json_decode($json, true);
return is_array($data) ? $data : [];
}
function saveProgress(string $file, array $data): void {
file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
/**
* Replace http:// -> https:// ONLY in links:
* - in common HTML attributes: href, src, srcset, data-src, data-href, action, poster, formaction
* - and in naked URLs inside text
*/
function replaceHttpToHttpsInHtml(string $html, &$changedCount = 0): string {
$original = $html;
// 1) attributes with quoted values
$attrPattern = '~\b(href|src|srcset|data-src|data-href|action|poster|formaction)\s*=\s*(["\'])(.*?)\2~is';
$html = preg_replace_callback($attrPattern, function ($m) use (&$changedCount) {
$attr = $m[1];
$q = $m[2];
$val = $m[3];
// replace any http:// occurrences inside attribute value
$newVal = preg_replace('~\bhttp://~i', 'https://', $val, -1, $c);
if ($c > 0) $changedCount += $c;
return $attr . '=' . $q . $newVal . $q;
}, $html);
// 2) naked URLs in text nodes (simple, but effective)
$html = preg_replace_callback('~(?<![a-z0-9+.-])http://([^\s<>"\']+)~i', function($m) use (&$changedCount) {
$changedCount++;
return 'https://' . $m[1];
}, $html);
return $html;
}
/** Get all iblocks (or one) */
$iblocks = [];
$rsIblocks = CIBlock::GetList(
['ID' => 'ASC'],
$onlyIblock > 0 ? ['ID' => $onlyIblock] : ['ACTIVE' => 'Y'],
false
);
while ($ib = $rsIblocks->Fetch()) {
$iblocks[] = (int)$ib['ID'];
}
if (!$iblocks) {
die("No iblocks found.\n");
}
$progress = loadProgress($progressFile);
// init progress structure
foreach ($iblocks as $iblockId) {
if (!isset($progress[$iblockId])) {
$progress[$iblockId] = [
'last_element_id' => 0,
'done' => false,
];
}
}
logLine($logFile, "=== START: step={$step}, dryRun=" . ($dryRun ? '1' : '0') . ", onlyIblock=" . ($onlyIblock ?: 'ALL') . " ===");
$totalChangedElements = 0;
$totalChangedFields = 0;
$totalChangedLinks = 0;
foreach ($iblocks as $iblockId) {
if (!empty($progress[$iblockId]['done'])) {
logLine($logFile, "IBLOCK {$iblockId}: already done, skip.");
continue;
}
$lastId = (int)$progress[$iblockId]['last_element_id'];
// Get HTML properties (USER_TYPE=HTML)
$htmlPropCodes = [];
$rsProps = CIBlockProperty::GetList(
['SORT' => 'ASC', 'ID' => 'ASC'],
['IBLOCK_ID' => $iblockId, 'ACTIVE' => 'Y', 'USER_TYPE' => 'HTML']
);
while ($p = $rsProps->Fetch()) {
if (!empty($p['CODE'])) $htmlPropCodes[] = $p['CODE'];
}
logLine($logFile, "IBLOCK {$iblockId}: last_element_id={$lastId}, HTML_PROPS=" . count($htmlPropCodes));
// Batch elements by ID > lastId
$filter = [
'IBLOCK_ID' => $iblockId,
'>ID' => $lastId,
];
$select = ['ID', 'IBLOCK_ID', 'NAME', 'PREVIEW_TEXT', 'PREVIEW_TEXT_TYPE', 'DETAIL_TEXT', 'DETAIL_TEXT_TYPE'];
$rsEl = CIBlockElement::GetList(['ID' => 'ASC'], $filter, false, ['nTopCount' => $step], $select);
$batchCount = 0;
while ($el = $rsEl->Fetch()) {
$batchCount++;
$elementId = (int)$el['ID'];
$changed = false;
$changedLinksInElement = 0;
$fieldsToUpdate = [];
// PREVIEW_TEXT
if ((string)$el['PREVIEW_TEXT_TYPE'] === 'html' && (string)$el['PREVIEW_TEXT'] !== '') {
$cnt = 0;
$new = replaceHttpToHttpsInHtml((string)$el['PREVIEW_TEXT'], $cnt);
if ($new !== (string)$el['PREVIEW_TEXT']) {
$fieldsToUpdate['PREVIEW_TEXT'] = $new;
$changed = true;
$changedLinksInElement += $cnt;
$totalChangedFields++;
}
}
// DETAIL_TEXT
if ((string)$el['DETAIL_TEXT_TYPE'] === 'html' && (string)$el['DETAIL_TEXT'] !== '') {
$cnt = 0;
$new = replaceHttpToHttpsInHtml((string)$el['DETAIL_TEXT'], $cnt);
if ($new !== (string)$el['DETAIL_TEXT']) {
$fieldsToUpdate['DETAIL_TEXT'] = $new;
$changed = true;
$changedLinksInElement += $cnt;
$totalChangedFields++;
}
}
// HTML properties
$propsToUpdate = [];
if (!empty($htmlPropCodes)) {
foreach ($htmlPropCodes as $code) {
$rsPropVal = CIBlockElement::GetProperty($iblockId, $elementId, ['SORT' => 'ASC', 'ID' => 'ASC'], ['CODE' => $code]);
$values = [];
$hasPropChange = false;
while ($pv = $rsPropVal->Fetch()) {
// HTML property stored as array: ['TEXT'=>..., 'TYPE'=>'html']
$val = $pv['VALUE'];
if (is_array($val) && isset($val['TEXT']) && is_string($val['TEXT']) && $val['TEXT'] !== '') {
$cnt = 0;
$newText = replaceHttpToHttpsInHtml($val['TEXT'], $cnt);
if ($newText !== $val['TEXT']) {
$val['TEXT'] = $newText;
$hasPropChange = true;
$changed = true;
$changedLinksInElement += $cnt;
$totalChangedFields++;
}
}
$values[] = [
'VALUE' => $val,
'DESCRIPTION' => $pv['DESCRIPTION'],
];
}
if ($hasPropChange) {
// For SetPropertyValuesEx: pass array of VALUE/ DESCRIPTION preserving multiplicity
$propsToUpdate[$code] = array_map(function($v){
return $v['VALUE'];
}, $values);
}
}
}
// Save changes
if ($changed) {
$totalChangedElements++;
$totalChangedLinks += $changedLinksInElement;
logLine(
$logFile,
"IBLOCK {$iblockId} ELEMENT {$elementId} \"{$el['NAME']}\" changed_links={$changedLinksInElement} dryRun=" . ($dryRun ? '1' : '0')
);
if (!$dryRun) {
$ob = new CIBlockElement();
if (!empty($fieldsToUpdate)) {
// keep types unchanged; update only text
if (!$ob->Update($elementId, $fieldsToUpdate, false, true, true)) {
logLine($logFile, "ERROR Update element {$elementId}: " . $ob->LAST_ERROR);
}
}
if (!empty($propsToUpdate)) {
CIBlockElement::SetPropertyValuesEx($elementId, $iblockId, $propsToUpdate);
}
}
}
// Update progress after each element (safe resume)
$progress[$iblockId]['last_element_id'] = $elementId;
saveProgress($progressFile, $progress);
}
// If batch returned less than step => finished this iblock
if ($batchCount < $step) {
$progress[$iblockId]['done'] = true;
saveProgress($progressFile, $progress);
logLine($logFile, "IBLOCK {$iblockId}: DONE.");
} else {
logLine($logFile, "IBLOCK {$iblockId}: batch processed {$batchCount}, continue next run.");
}
}
logLine($logFile, "=== END: changed_elements={$totalChangedElements}, changed_fields={$totalChangedFields}, changed_links={$totalChangedLinks} ===");
echo "DONE. Log: {$logFile}\nProgress: {$progressFile}\n";
<pre>
