Баг в методе CMain::GetFileRecursive

В битриксе есть метод CMain::GetFileRecursive, который я часто использую в своих проектах. Вообще в битриксе не так много вещей, которые мне нравятся, и GetFileRecursive один из них. Этакий лучик света в темном царстве.

GetFileRecursive ищет заданный файл в текущем разделе сайта и во всех родительских разделах. Если файла нет в текущем разделе, то метод поднимается на уровень вверх - в родительский раздел - и ищет там. Если и там нет, то поднимается еще выше. И так вплоть до корня сайта. Если файл найден, то возвращается путь до этого файла.

Очень простой, но при этом очень эффективный метод. Можно использовать для различных целей. Например для подключения включаемых областей.

С недавних пор этот функционал перестал корректно работать. Если искомый файл находится в текущем разделе, то GetFileRecursive его не видит. Возвращает false. Когда именно это случилось, история умалчивает. Проверить это сложно, поскольку сложно найти более ранние версии битрикса. Обнаружен этот баг был в главном модуле версии 12.5.1.

Поведение более чем загадочное, но ни на сайте битрикса, ни в других интернетах я так и не смог найти ни единого упоминания об этом баге. При отсутствии какой-либо документации или любой другой информации всегда на помощь приходит исходный код.

# /bitrix/modules/main/classes/general/main.php

function GetFileRecursive($strFileName, $strDir=false)
{
    if($strDir === false)
        $strDir = $this->GetCurDir();

    $io = CBXVirtualIo::GetInstance();
    $fn = $io->CombinePath("/", $strDir, $strFileName);

    $p = false;
    while(!$io->FileExists($io->RelativeToAbsolutePath($fn)))
    {
        $p = bxstrrpos($strDir, "/");
        if($p === false)
            break;
        $strDir = substr($strDir, 0, $p);
        $fn = $io->CombinePath("/", $strDir, $strFileName);
    }
    if($p === false)
        return false;

    return $fn;
}

Далеко ходить не пришлось. Баг на поверхности. Если искомый файл находится в текущей папке, то $io->FileExists($io->RelativeToAbsolutePath($fn)) вернет true и мы в цикл вообще не попадаем и, следовательно, загадочная переменная $p останется равной false и в результате получаем false вместо пути до файла.

Как же этот метод работал в более ранних версиях битрикса? Возьмем для примера код главного модуля версии 12.0.5. Смотрим и сразу становится ясно откуда ноги растут.

function GetFileRecursive($strFileName, $strDir=false)
{
    if($strDir === false)
        $strDir = $this->GetCurDir();

    $io = CBXVirtualIo::GetInstance();
    $fn = $io->CombinePath("/", $strDir, $strFileName);

    while(!$io->FileExists($io->RelativeToAbsolutePath($fn)))
    {
        $p = bxstrrpos($strDir, "/");
        if($p === false)
            break;
        $strDir = substr($strDir, 0, $p);
        $fn = $io->CombinePath("/", $strDir, $strFileName);
    }
    if($p === false)
        return false;

    return $fn;
}

Код метода практически не изменился. Отличие только в инициализации переменной $p перед циклом. Использование переменной без ее объявления, несомненно, моветон и то, что разработчики битрикса решили исправить этот недостаток, это, конечно, хорошо. Плохо то, что разработчикам не хватило дальновидности.

Во-общем, причина найдена и для исправления этой ошибки всего лишь достаточно проинициализировать переменную $p перед циклом значением null вместо false. Но мы не можем вносить изменения в исходный код битрикса, поэтому придется искать другое решение, либо способ обхода пока этот баг не будет исправлен.

Конечно мы всегда можем написать свою реализацию этой функции, но хотелось бы найти более правильное решение с использованием платформы. Иначе для чего она нам вообще нужна.

Вспоминаем, что есть еще чудо компонент bitrix:main.include который тоже умеет подключать файлы с рекурсивным поиском. И он отлично работает. Любопытно посмотреть на реализацию этого компонента. Ведь там же наверняка используется GetFileRecursive. Смотрим код и понимаем, что ожидания не оправдались. GetFileRecursive здесь нет и в помине. Вместо этого используется своя реализация рекурсивного поиска.

Компонент bitrix:main.include нам ничем не помог. Поэтому предложу свое решение, не очень изящное, но действующее. Поскольку метод сбоит только в том случае, когда искомый файл находится в текущем разделе, нужно подменить текущий раздел таким образом, чтобы искомый файл всегда находился выше по иерархии разделов. В этом нам поможет второй, необязательный параметр метода. Укажим в нем фейковый, несуществующий раздел, который якобы находится уровнем ниже текущего раздела, то есть является вложенным для текущего раздела.

$APPLICATION->GetFileReсursive($filename, $APPLICATION->GetCurDir().’fake_inserted_dir/’)

И вуаля - заработало. Теперь у нас поиск идет не от текущей директории, а уровнем ниже. Решение далеко не самое изящное, но для применения в полевых условиях пойдет.

Пока готовил статью, нашел все-таки на сайте битрикса упоминание об этом баге, точнее упоминание об исправлении. В истории изменений указано, что в версии 12.5.2 главного модуля “Исправлена ошибка рекурсивного поиска файла”. Исправление очень простое - заменили инициализацию переменной $p = false, на $p = null. Спасибо.