Автоматическая замена HTTP на HTTPS в контенте сайта на 1С-Битрикс

Автоматическая замена 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>