Это ошибка в моих регулярных выражениях в RewriteRule или ошибка mod_rewrite?

Вопрос или проблема

У меня есть следующая структура моего проекта:

.htaccess

RewriteEngine On
RewriteRule ^file/(.*)$ files.php?get=$1 [QSA,L]
RewriteRule ^api/(.*)$  api.php?get=$1   [QSA,L]
RewriteRule ^(.*)$      pages.php?get=$1 [QSA,L]

files.php

<?php
echo 'files:<br>';
var_dump($_GET);
// TODO: реализация генерации файлов
?>

api.php

<?php
echo 'api:<br>';
var_dump($_GET);
// TODO: реализация команд API
?>

pages.php

<?php
echo 'pages:<br>';
var_dump($_GET);
// TODO: маршрутизация и генерация html страниц. адреса страниц могут иметь разное вложение
?>

Моя проблема заключается в том, что для любого запроса выполняется код из файла pages.php.

ВОЗМОЖНО, решение моей проблемы заключается в:

.htaccess

RewriteEngine On
RewriteRule .* index.php [L]

index.php

<?php
switch (explode("https://webmasters.stackexchange.com/", $_SERVER['REQUEST_URI'])[1])
{
    case 'file':
        require 'files.php';
        break;
    case 'api':
        require 'api.php';
        break;
    default:
        require 'pages.php';
        break;
}
?>

НО я столкнулся с некоторым странным поведением mod_rewrite при изменении регулярных выражений. И мне хотелось бы разобраться в этой ситуации.

Если у меня есть следующие регулярные выражения:

RewriteRule ^file/(.*)$ files.php?get=$1 [QSA,L]
RewriteRule ^api/(.*)$  api.php?get=$1   [QSA,L]
RewriteRule ^(.*)$      pages.php?get=$1 [QSA,L]

Для URL http://localhost/any_text_1/any_text_2/any_text_3 у меня правильный результат:

pages
array(1) { ["get"]=> string(32) "any_text_1/any_text_2/any_text_3" }

Но для URL http://localhost/api/cmd1/param1 у меня неправильный результат (управление должно было перейти к файлу api.php, а не pages.php):

pages (ПРИМЕЧАНИЕ: здесь я ожидал получить "api")
array(1) { ["get"]=> string(11) "cmd1/param1" }

НО если я изменю регулярные выражения (уберу символ “/”):

RewriteRule ^file(.*)$ files.php?get=$1 [QSA,L]
RewriteRule ^api(.*)$  api.php?get=$1   [QSA,L]
RewriteRule ^(.*)$     pages.php?get=$1 [QSA,L]

Тогда я получу нужный результат (за исключением одной проблемы):

Для URL http://localhost/any_text_1/any_text_2/any_text_3 у меня правильный результат (за исключением лишнего символа “/” в параметре):

pages
array(1) { ["get"]=> string(32) "/any_text_1/any_text_2/any_text_3" }

А для URL http://localhost/api/cmd1/param1 у меня правильный результат (за исключением лишнего символа “/” в параметре):

api
array(1) { ["get"]=> string(12) "/cmd1/param1" }

НО проблема, о которой я говорил ранее, заключается в следующем:

Для URL http://localhost/api11111/cm1/param1 я получаю результат:

api
array(1) { ["get"]=> string(16) "11111/cm1/param1" }

Но этот адрес должен был обрабатывать файл pages.php, а не api.php.

Получается, что регулярные выражения с символом “/” не выполняют маршрутизацию. А регулярные выражения без символа “/” выполняют маршрутизацию, но делают это не совсем корректно.

Почему? Это ошибка в моих регулярных выражениях или в mod_rewrite?

П.С.: Извините, если мой вопрос слишком длинный или плохо сформулирован. Честно, я старался написать лучше.

URL вроде http://localhost/file/1 соответствует правилу ^file/(.*)$ и код из файла files.php должен быть выполнен. Но выполняется код из файла pages.php. … (В коде из файла pages.php я получаю правильное значение параметра get).

В каталоге/.htaccess флага L просто завершает текущий проход через движок переписывания. Он не завершает все выполнение. Затем происходит второй проход, который передает обратно вновь переписанный URL (например, files.php?get=1 в этом примере).

Поскольку последнее правило соответствует всему, оно захватывает вновь переписанный URL во время второго прохода через движок переписывания и в конечном итоге переписывает URL в pages.php?get=files.php&get=1 (предыдущая переписанная строка запроса присоединяется из-за флага QSA). Что касается PHP, параметр get кажется правильным, потому что второй экземпляр перезаписывает первый (в суперглобальной $_GET).

Ваше регулярное выражение должно быть более специфичным. Вам действительно нужно соответствовать всему в последнем правиле (и в первых двух правилах для этого вопроса)? Или оно предназначено только для соответствия отдельным сегментам пути (и без точек), как в вашем примере “news“?

Если да, то сделайте регулярное выражение более строгим. Например:

:
RewriteRule ^([^/.]*)$      pages.php?get=$1 [QSA,L]

Регулярное выражение ^([^/.]*)$ соответствует news, но не соответствует news/something или news.php.

Или/И на Apache 2.4 вы можете предотвратить второй проход через движок переписывания, используя флаг END вместо L в ваших правилах.

Однако, если вы не сделаете регулярное выражение более специфичным (или не добавите дополнительные условия), то вы потенциально также перепишите свои статические активы (CSS, JS, изображения и т.д.), что обычно нежелательно.


Непосредственно:

RewriteRule ^api/(.*)$  api.php?get=$1   [QSA,L]

Для того чтобы это правило сработало, вам нужно убедиться, что MultiViews отключен (в противном случае mod_negotiation “переписывает” запрос в api.php до обработки этого правила, и параметр get не передается). MultiViews отключен по умолчанию в Apache, однако некоторые серверы/дистрибутивы переопределяют это и включают его по некоторым причинам.

В основном, MultiViews позволяет URL файлов без расширения просто “работать”. Но это относится ко всему, потенциально приводит к проблемам с дублированием контента и может конфликтовать с вашими правилами mod_rewrite!

Например, в верхней части вашего .htaccess файла:

# Отключить MultiViews
Options -MultiViews

Ответ или решение

Конечно, давайте подробно рассмотрим вашу проблему с правилами переписывания (RewriteRule) в файле .htaccess, проанализируем возможные ошибки и дадим рекомендации по их устранению.

Проблема

Ваши правила переписывания выглядят так:

RewriteEngine On
RewriteRule ^file/(.*)$ files.php?get=$1 [QSA,L]
RewriteRule ^api/(.*)$  api.php?get=$1   [QSA,L]
RewriteRule ^(.*)$      pages.php?get=$1 [QSA,L]

Как вы заметили, для запросов, которые должны обрабатываться файлами files.php и api.php, вызывается pages.php. Это поведение может быть связано с тем, как работает модуль mod_rewrite, и спецификой правил, установленных в вашей конфигурации.

Анализ причин

  1. Порядок правил и флаг L: Флаг [L] прекращает дальнейшую обработку правил, только если правило успешно сработало. Однако, если переопределение срабатывает, тогда происходит повторное применение правил для нового URL. В вашем случае, если files.php?get=... обрабатывается, следующая строка (т.е. правило для pages.php) может перекрыть его. Таким образом, срабатывает pages.php.

  2. Неявное совпадение с правилами: Поскольку ваше третье правило ^(.*)$ срабатывает для всех URL, оно может перехватывать запросы, которые должны обрабатываться первыми двумя правилами. Это происходит потому, что выражение ^(.*)$ действительно является универсальным и подходит под любые строки.

  3. Проблемы с регулярными выражениями: Регулярные выражения, которые вы используете, могут быть недостаточно строгими. Например, если у вас есть URL http://localhost/api11111/cm1/param1, то срабатывает правило ^api/(.*)$, потому что api11111 начинается с api. Это не то, что вам нужно, для обработки API требуется строгое соответствие.

Рекомендации по исправлению

  1. Уточнение регулярных выражений: Измените регулярные выражения, чтобы они более явно проверяли свои условия. Например:
RewriteRule ^file/(.+)$ files.php?get=$1 [QSA,L]
RewriteRule ^api/(.+)$  api.php?get=$1   [QSA,L]
RewriteRule ^([^/]+)$   pages.php?get=$1 [QSA,L]

В этом случае (.+) требует хотя бы один символ после file/ или api/, что предотвращает захват пустой строки. Также ^([^/]+)$ разрешает только одиночные сегменты, что исключает захват подстрок.

  1. Использование флага END: Если у вас версия Apache 2.4 или новее, подумайте о замене флага [L] на [END]. Это останавливает процесс переписывания сразу после выполнения текущего правила.
RewriteRule ^file/(.+)$ files.php?get=$1 [QSA,END]
RewriteRule ^api/(.+)$  api.php?get=$1   [QSA,END]
RewriteRule ^([^/]+)$   pages.php?get=$1 [QSA,END]
  1. Отключение MultiViews: Если у вас включен режим MultiViews, он может конфликтовать с вашими правилами. Предположите добавление следующей строки в начале файла .htaccess:
Options -MultiViews

Заключение

Проблема, с которой вы столкнулись, скорее всего, не является багом в mod_rewrite, а результатом не слишком строгих регулярных выражений и порядка обработки. Уточнение ваших правил поможет достичь того поведения, которого вы ожидаете. Использование флага [END] и отключение MultiViews сделает ваши правила еще более предсказуемыми и надежными.

Если у вас остались дополнительные вопросы или потребуются дальнейшие разъяснения, пожалуйста, не стесняйтесь задавать их.

Оцените материал
Добавить комментарий

Капча загружается...