dkLab | конструктор | PHP_CodeFilter: перехоплення фатальних помилок PHP? Це можливо
- Перехоплення помилок в Perl
- Фатальні помилки в PHP: E_ERROR
- Перехопити за 60 секунд
- Префільтр коду: модуль PHP_CodeFilter
- Що далі?
- резюме
23 квітня 2005 р Обговорити на форумі
Дана стаття може здатися досить складною для розуміння.
Пристебніть ремені безпеки!
Perl можна по праву назвати мовою, в якому однаково легко маніпулювати як даними, так і кодом програми. Дійсно, мова дозволяє створювати «на льоту» нові підпрограми і працювати з ними, як зі звичайними змінними. Завдяки виключно потужною інструкції eval і розвиненому механізму роботи з винятками дуже зручно писати транслятори з різних мета-мов в Perl-код (наприклад, шаблонизатор). При цьому програміст може бути впевненим: він має повний контроль над ходом роботи оттранслировать коду, і навіть в разі фатальної помилки в такому коді він отримає можливість її обробити (наприклад, сформувати осмислене повідомлення).
Перехоплення помилок в Perl
Розглянемо типовий приклад транслятора з деякого мета-мови шаблонів в Perl-код:
лістинг 1
# Перетворює HTML-шаблон з "псевдо-PHP вставками" (<? ...?>) # В коректний Perl-код з серіями print-ів. При цьому всі, що # розташоване поза тегів, виводиться, а то, що всередині, - # виповнюється як Perl-програма. sub сompile {my ($ html) = @ _; $ C = chr (1); $ Code = "?> $ Html <?"; $ Code = ~ s {<\? =} {<? print} sgo; $ Code = ~ s {\?> (. *?) <\?} {Print (qq $ c $ 1 $ c);} sgo; return $ code; } # Прочитуємо шаблон з файлу. my $ template = "template.htm"; local $ /; open (local * F, $ template); my $ code = compile (<F>); # Стартуємо код з "підміною" імені поточного файлу. eval ( "# line 1 \" $ template \ "\ n $ code"); # У разі будь-помилки в оттранслировать коді (будь то # помилка синтаксису, виклик невизначеною функції - що завгодно) # ми потрапимо сюди. Робота програми НЕ завершиться! if ($ @) die "Handled error: $ @";
Вам зараз немає потреби особливо вникати в наведений код. Замість цього ви можете звернутися до дев'ятої Набла , Де детально розглянуто запропонований підхід по найпростішої трансляції шаблонів в Perl (модуль CGI :: Embedder).
Директива Perl #line, що стоїть на окремому рядку, змушує інтерпретатор «прикинутися», нібито наступний за нею код виповнюється «в контексті» зазначеного файлу, починаючи з наведеної рядки. У нашому випадку це файл template.htm, рядок 1. Тепер, навіть якщо в коді $ code відбудеться що-небудь серйозне (наприклад, виникне помилка), Perl повідомить про неї як про «проблеми in file template.htm line N», а не як про «проблеми в eval-коді»!
Щоб остаточно зрозуміти, як працює #line, спробуйте запустити наступну Perl-програму:
Ви побачите наступний результат:
Як бачите, Perl «впевнений», що він виконує код на рядку 100 файлу first.htm, в той час як насправді запускається команда на рядку 2 файли зі скриптом!
Але це ще не всі переваги Perl. Інструкція eval працює як універсальний перехоплювач винятків (помилок), за аналогією з блоком try ... catch в мовах програмування C ++ і Java. Іншими словами, будь-який виник всередині eval виключення (наприклад, виклик стандартної функції die () або синтаксична помилка) призведе не до завершення всієї програми, а тільки лише до виходу з інструкції eval. Це відкриває нам великі можливості по обробці виняткових ситуацій: дійсно, досить «обернути» небезпечну ділянку коду в eval і потім перевірити змінну $ @: в разі проблеми там міститиметься повідомлення про помилку (або об'єкт-виняток, що те ж саме).
Таким чином, Perl дозволяє програмісту:
- Контролювати реакцію на будь-які помилки в динамічно створеному коді.
- «Підставляти» в контекст виконуваного коду довільне ім'я файлу і номер рядка.
Фатальні помилки в PHP: E_ERROR
На жаль, мова PHP (поки що?) Не може похвалитися всіма цими можливостями. У ньому, звичайно, є вбудована функція eval (), проте вона зовсім не така потужна. Дійсно, фатальна помилка (E_ERROR), що виникла в занедбаному через eval () коді, призводить до негайної (і безумовної) зупинці скрипта!
Що таке «фатальна помилка»? Наприклад, досить типовий випадок - виклик невизначеною функції.
PHP також не підтримує директиву #line, а тому все діагностичні повідомлення, згенеровані в eval-коді, будуть повідомляти: «проблема в eval () 'd code on line N». Звичайно, якщо ми намагаємося написати транслятор з мови шаблонів в PHP, це нас ні в якій мірі не влаштує.
Як же виходять з положення автори різних транслюють оброблювачів шаблонів (на зразок Smarty), що перетворюють свої шаблони в PHP-код з подальшим його запуском? А ніяк не виходять. Той, хто хоч раз використовував Smarty, знає, що повідомлення про помилки, які він генерує, посилаються на рядки в уже оттранслировать PHP-коді, розташованому в тимчасових файлах, а зовсім навіть не на рядки вихідного шаблону. Звичайно, це дуже незручно при налагодженні сайту.
Втім, перехопити нефатальні помилки (E_WARNING, E_NOTICE і т. Д.) Можливе за допомогою функції set_error_handler () . Однак виклик невизначеною функції (і будь-яка інша помилка класу E_ERROR) як і раніше призведе до негайного завершення роботи всієї програми і видачі безстороннього повідомлення про помилку в «eval () 'd code».
Перехопити за 60 секунд
З «слабкістю» PHP-шной функції eval (), що стосується безумовного завершення скрипта при помилку E_ERROR, ми ніяк боротися не можемо. Проте, формування «упередженими» посилання на файл, який містить помилку, нам все ж під силу!
Ми скористаємося одним «побічним ефектом», що має місце в усіх версіях PHP 4 і 5. Мова йде про «спрацьовуванні» обробника вихідного потоку, встановленого по ob_start () , При будь-якому завершенні скрипта - не важливо, помилковому або легальному.
Найпростіше продемонструвати цей ефект може наступний код:
лістинг 5
<? Php # Встановлюємо оброблювач вихідного потоку скрипта. ob_start ( 'ob_handler'); # Друкуємо що-небудь. echo "Something"; # Викликаємо невизначену функцію всередині eval! eval ( 'undefinedFunc ();'); # Друкуємо ще щось (до цього моменту скрипт вже мертвий!). echo "Other"; # Функція-обробник просто додає деякий "хвіст" # до тексту, виведеному скриптом раніше. function ob_handler ($ text) {return "$ text <hr> Hello from handler!"; }?>
Ви побачите, що, не дивлячись на неперехвативаемой помилку класу E_ERROR, обробник ob_handler () успішно запустився:
Як же в обробнику визначити, завершився скрипт аварійно або ж коректно? Існує всього лише один спосіб зробити це, але він досить «брудний». Необхідно розібрати регулярним виразом «хвіст» тексту, переданого в функцію, і грубо перевірити, чи міститься в ньому повідомлення про помилку «в eval () 'd code». У разі необхідності саме це повідомлення і потрібно замінити на текст, що містить «фальшиве» ім'я файлу.
Префільтр коду: модуль PHP_CodeFilter
Подолання всіх наведених вище складнощів виливається в досить значний обсяг коду, який я помістив в модуль PHP_CodeFilter .
На щастя, ignorance is bliss , І тому ви можете не розбиратися у всій цій купі «брудних хаков», а просто використовувати модуль в своїх програмах.
Перетворення (трансляцію) файлу з деякого мета-мови шаблонів в PHP-код з подальшим його запуском я назвав Префільтрація коду (за аналогією з відомим модулем для Perl Filter :: Util :: Call ). З використанням цієї ідеології можна «запускати» деякий шаблон, як ніби-то він є звичайним PHP-кодом, але проводити в ньому попередню обробку - наприклад, «розгортати» власні директиви в «чистий» PHP-код.
Щоб не ходити далеко за прикладом, розглянемо одне з можливих застосувань префільтр коду: підвищення безпеки скрипта. Часто доводиться зустрічати в програмах такі конструкції:
Перед виведенням $ var її необхідно «квоти» - перетворювати лапки в entity & quot ;, знак <( «менше») - в & lt; і т. д., щоб не виникло конфліктів з лапками атрибутів або HTML-тегами. Годі й казати, що утомливо весь час писати одне і те ж. А що в програмуванні утомливо, то найбільшою мірою схильний до помилок і проблем з безпекою.
І ось, за допомогою префільтр коду ми можемо змусити оператор
автоматично
задіяти квотінг при виведенні! Для цього достатньо замінювати даний оператор «на льоту» на наступний код:
У прикладі, наведеному нижче, якраз для такої префільтраціі використовується бібліотека PHP_CodeFilter. Зверніть увагу, що замість include ми застосовуємо виклик $ filter-> includeFile (...): так відбувається не просто включення файлу шаблону, але і його Префільтрація.
Звичайно, ніхто не заважає вам створити функцію з ім'ям include_html () і записати в ній виклик $ filter-> includeFile (...), щоб надалі всюди писати просто include_html (...). Бо коротше.
лістинг 10
<? Php # # Приклад скрипта, що використовує префільтр коду для шаблонів. show_source (__ FILE__); echo "<hr>"; require_once "../../lib/config.php"; require_once "PHP / CodeFilter.php"; // Якщо потрібно, підключаємо Debug_BacktraceDumper для // поліпшеного виведення налагоджувальних повідомлень PHP. if (true) {require_once "Debug / BacktraceDumper.php"; Debug_BacktraceDumper :: set_error_handler (); } // Деяка змінна з HTML-розміткою. $ Test = "<b> test </ b>"; // Створюємо об'єкт-фільтр і налаштовуємо його параметри. $ Filter = new PHP_CodeFilter (); $ Filter-> addFilter ( 'filter_phpOutputOperator'); // Запускаємо включається файл (аналог include), але його // код перед виконанням обробляється префільтром. $ Filter-> includeFile ( 'template.php'); // string filter_phpOutputOperator (string $ code) // Префільтр коду: замінює оператори '<? = ...? > '// (без пробілів, звичайно) на' <? = Htmlspecialchars (...)? > ', // що забезпечує автоматичний квотінг HTML в шаблонах // і підвищує безпеку скриптів. function filter_phpOutputOperator ($ code) {return preg_replace ( '/ (<\? =) (. *?) (?: \ s *;) * \ s * (\?>) / sx', '$ 1htmlspecialchars ($ 2, ENT_QUOTES) $ 3 ', $ code); }?>
В скрипті також демонструється застосування модуля Debug_BacktraceDumper , Що дозволяє зробити все попередження інтерпретатора більш «доброзичливими», що сильно упрщает налагодження. Звичайно, ви можете і не підключати його, якщо не хочете.
Логіка квотінга тексту реалізована в функції filter_phpOutputOperator (), як раз і замінює стандартний оператор PHP <? = ...?> На його «квотірущій» варіант.
Для повноти картини давайте подивимося на шаблон template.php , Що підключається в скрипті:
лістинг 11<! - Шаблон, код якого пре-фільтрується перед запуском. -> <body> <p> <b> Перевірка htmlspecialchars-квотінга виведення: </ b> <br> <i> (Повинна бути виведена HTML-розмітка, як після <tt> htmlspecialchars () </ tt>.) </ i> <br> <? = $ Test?> <P> <b> Перевірка використання невизначеною змінної: </ b> <br> <i> (Повинна бути посилання на <tt> <? = $ __ FILE__?> </ Tt>.) </ I > <br> <? = $ Undefined_variable?> <? $ Filter-> includeFile ( 'template2.php')?> <P> <b> Перевірка виклику неіснуючої функції (фатальна помилка): </ b> <br> <i> (Повинна бути посилання на <tt> <? = $ __ FILE__?> </ tt>!) </ i> <? aaa ()?> </ body>
Погляньте прямо зараз на результат роботи скрипта .
- Хоча змінна $ test містить HTML-розмітку, при виведенні операторами <? = ...?> Вона «проквотілась», і теги стали «нешкідливими». Здавалося б, найпростіший PHP-код, а поведінка його змінилося. Магія? Ні, Префільтрація!
- Всі повідомлення про помилки (навіть фатальних) виявилися забезпечені вірними посиланнями на файли, у яких вони були згенеровані, - незважаючи на те, що механізм префільтраціі використовує функцію eval () для запуску результуючого коду!
Бібліотека PHP_CodeFilter влаштована так, що вона коректно обробляє навіть режим log_errors = on, так що файли журнали не будуть захаращуватися.
Що далі?
Метод, реалізований в модулі PHP_CodeFilter, теоретично дозволяє в майбутньому ввести підтримку директиви #line, відсутньої в PHP. Справді, досить просто відразу після перехоплення помилки просканувати останній «відфільтрований» код в пошуках рядки, згаданої в повідомленні, і знайти директиву #line, безпосередньо їй передує.
Для оптимізації швидкості можлива реалізація кешуючих версій префільтри, які будуть транслювати код тільки при його першому запуску, в подальшому «підсовуючи» інтерпретатора вже готову PHP-програму. Кешування коду, наприклад, використовується в Smarty і сильно підвищує продуктивність.
Ну і, звичайно, простір для фантазії при написанні функцій-префільтри воістину необмежений! З їх допомогою ви можете реалізовувати будь-які транслятори - наприклад, підтримку мов на зразок Smarty, XTemplate і т. Д. При цьому з точки зору налагоджувальних повідомлень все буде виглядати так, ніби-то код шаблону «запускається» безпосередньо, минаючи фазу трансляції в PHP-код .
резюме
Ризикну припустити, що з застосуванням методу, реалізованого в модулі PHP_CodeFilter, процес написання шаблонних мов виходить на якісно новий щабель, раніше доступну тільки в мові Perl. Наприклад, з'являється можливість створення Smarty-подібного транслятора, що допускає зручну налагодження шаблонних файлів.
Всі вихідні коди скриптів і бібліотек цієї статті доступні в директорії прикладів ( zip ). Отже:
Інші корисні посилання:
Lt;?
Sub сompile {my ($ html) = @ _; $ C = chr (1); $ Code = "?
Code = ~ s {<\?
Lt;?
Print} sgo; $ Code = ~ s {\?
Gt; (. *?) <\?
Поки що?
Що таке «фатальна помилка»?
Як же виходять з положення автори різних транслюють оброблювачів шаблонів (на зразок Smarty), що перетворюють свої шаблони в PHP-код з подальшим його запуском?
Htmlspecialchars (...)?