PHP include уязвимость: от теории к практике. Совместное использование принципов MVC и ООП

Представьте сайт, состоящий из двух страниц. На главной старице отображается некоторый текст:

Вторая страница представляет собой его редактор:


На ней мы можем изменять содержание главной страницы и сохранять изменения.

В верхней части страниц расположена "шапка" сайта, которая состоит из двух элементов. Первый из них неизменен и отражает название сайта. Второй содержит название текущей страницы: либо "Чтение", либо "Редактирование".

Ниже шапки расположено меню, которое одинаково для всех страниц сайта и содержит ссылки на эти страницы.

Также неизменным остается "подвал" страницы, в котором отображается дополнительная информация о сайте, например, адрес, телефон, информация о создателе страницы.

Центральную часть станицы "Чтение" занимает текст, который мы можем задавать на странице "Редактирование", используя обычную HTML-форму с элементом textarea

Исходные положения

Нам предстоит объединить два подхода - MVC и ООП. Для этого при разработке архитектуры системы будем руководствоваться следующими правилами.

  1. Согласно шаблону проектирования MVC файлы контроллеров, моделей и представлений будут разнесены по трем отдельным каталогам.
  2. Каждый контроллер будет представлен отдельным файлом, в котором будет объявлен класс контроллера, инкапсулирующий логику работы контроллера и предоставляющий методы для обработки HTTP-запроса пользователя.
  3. Каждой странице сайта будет соответствовать отдельный контроллер, отвечающий за ее вывод.
  4. Модели системы также будут реализованы в виде классов.
  5. Представления будут реализованы в виде обычных PHP-файлов с HTML- разметкой с вкраплениями PHP-кода для отображения значения переменных сценария.
  6. HTTP-запросы к системе будут осуществляться через специальную точку входа - файл index.php.

Хоть эта тема уже и обсуждалась, повторим, что следует четко представлять различия между назначением классов контроллеров и классов модели. Классы контроллеров служат для того, чтобы обеспечить обработку HTTP-запроса пользователя, т. е. получить параметры входного запроса, и подготовить результирующую HTML-страницу. При выполнении этой работы контроллеры задействуют классы модели, которые представляют отдельные функциональные блоки: работу с базой данных, работу с файлами, работу с аккаунтами пользователей и т. п. Каждый такой логически связный блок представляется отдельным классом. Например, в вашей системе может быть класс для работы с базой данных, который будет хранить дескриптор текущего соединения с БД и предоставлять набор методов для выполнения запросов к БД и получения различных наборов данных.

Точка входа

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

В нашем примере мы также будем пользоваться единой точкой входа - файлом index.php. Давайте рассмотрим его содержание:

Request(); ?>

Давайте разберем работу данного сценария построчно.

Include_once("inc/C_View.php"); include_once("inc/C_Edit.php");

Первые две строчки подключают файлы контроллеров для страниц "Чтение" и "Редактирование".

Switch ($_GET["c"])

Здесь происходит анализ GET-параметра с именем c :

Case "edit": $controller = new C_Edit(); break;

Если данный параметр хранит строку "edit ", тогда переменной $controller присваивается объект класса C_Edit , описание которого находится в файле inc/C_Edit.php , подключенном в самом начале сценария.

В случае если параметр с не задан или имеет значение, отличное от "edit " , выполняется другая ветка оператора switch :

Default: $controller = new C_View();

Здесь переменной $controller присваивается экземпляр класса C_View .

Наконец, последняя строка вызывает метод Request() у созданного объекта:

$controller->Request();

Обратите внимание, здесь мы можем увидеть типичный пример полиморфизма . Переменная $controller может хранить как экземпляр класса C_Edit , так и экземпляр класса C_View . Поэтому мы не можем заранее знать, метод Request() какого именно класса будет вызван в сценарии. Важно лишь то, что для корректной работы сценария оба класса должны иметь данный метод.

В целом же задача точки входа проста - передать управление одному из контроллеров. Какому именно, определяется с помощью параметра GET-запроса с именем с .

Иерархия контроллеров

Давайте приступим к проектированию системы и для начала определим, какие контроллеры будут в ней присутствовать. Из файла index.php видно, что в системе есть как минимум два контроллера, представленные в виде классов C_Edit и C_View . Задача класса C_Edit - организовать вывод страницы "Редактирование", класса C_View - страницы "Чтение". Обработка входных параметров и вывод страницы инкапсулируется внутри метода Request() , который должен присутствовать как в классе C_View , так и в C_Edit . Это может натолкнуть на мысль о создании общего родительского класса для этих двух контроллеров. И действительно, реализация С_View будет во многом совпадать с C_Edit , поэтому выделение родительского класса базового контроллера - это полезное действие.

Таким образом, мы пришли к мысли о необходимости создания базового класса контроллера. Назовем его C_Base . Он будет абстрактным и не будет иметь конкретных реализаций. Мы вводим его для того, чтобы другие классы контроллеров наследовались от него. Класс C_Base будет хранить базовые свойства и методы, характерные для каждого контроллера данного сайта.

Давайте поразмышляем , какие параметры являются общими для каждой страницы сайта? Во-первых, это название сайта. Во-вторых, текст, расположенный внизу каждой страницы. Сюда же можно отнести пункты меню, которые одинаковы для каждой страницы сайта. Обобщая, можно сказать, что контроллер C_Base отвечает за базовый шаблон сайта.

Очевидно, каждый контроллер будет содержать метод Request() , отвечающий за обработку запроса. Следовательно, его объявление также можно вынести в класс C_Base и сделать его абстрактным для того, чтобы классы-наследники были обязаны реализовать данный метод.

Возможно, имеет смысл выделить еще какие-то общие свойства или методы для всех контроллеров сайта. Подумайте над этим самостоятельно. Подчеркнем, C_Base - это базовый класс, который объединяет в себе все общие элементы каждого контроллера конкретного сайта.

Но давайте попробуем пойти еще дальше и подумаем, можно ли выделить класс контроллера, который инкапсулировал бы логику, неизменную для любого контроллера любого сайта? В такой класс мы уже не сможем вынести свойство названия сайта, но, к примеру, метод Request() , отвечающий за обработку запроса и формирование результирующей страницы, неизменно должен присутствовать в любом контроллере.

Не будем томить и продемонстрируем иерархию классов контроллеров, которую мы вам предлагаем принять за образец:

Данная диаграмма используется в описательном языке UML и называется диаграммой классов. Каждый прямоугольник на ней соответствует отдельному классу. В верхней части прямоугольника жирным выделено название класса. Стрелками показаны отношения наследования. Курсивом выделены свойства классов, прямым шрифтом - его методы. Знак перед свойствами или методами говорит о том, что свойство или метод объявлены с модификатором private , знак # соответствует модификатору protected ,+ - модификатору public .

Класс Controller

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

  1. Template() - для подстановки в шаблон набора переменных и вывода его на экран;
  2. IsGet() - для проверки, был ли выполнен GET-запрос;
  3. IsPost() - для проверки, был ли выполнен POST-запрос.

В классе Controller присутствует объявление уже известного нам метода Request() .
Давайте посмотрим на его реализацию:

Public function Request() { $this->OnInput(); $this->OnOutput(); }

В методе Request() мы делим обработку запроса на две части: обработку входных данных (метод OnInput() ) и формирование результирующей страницы (метод OnOutput() ). Такое разделение довольно удобно, и мы предлагаем его использовать и в будущих проектах. Методы OnInput() и OnOutput() должны быть переопределены в дочерних классах.

Класс C_Base

C_Base является базовым контроллером для нашего конкретного сайта. В большинстве случаев на весь сайт нужен один базовый контроллер. Класс C_Base отвечает за базовый шаблон HTML, в нем могут быть определены базовые для всего сайта переменные, например, название сайта.

Кроме того, в классе C_Base нужно переопределить методы OnInput() и OnOutput() и вынести в них действия, общие для всех контроллеров сайта. Контроллеры, которые будут наследоваться от класса C_Base , будут переопределять эти методы, но они смогут вызвать родительские методы с помощью ключевого слова parent .

Мы предлагаем следующую реализацию методов OnInput() и OnOutput() в классе C_Base .

Protected function OnInput() { $this->title = "Главная страница"; $this->content = ""; } protected function OnOutput() { $vars = array("title" => $this->title,"content" => $this->content); $page = $this->Template("main", $vars); echo $page; }

В методе OnInput() мы можем инициализировать переменные шаблона значениями по умолчанию. В методе OnOutput() мы сначала формируем массив переменных шаблона, а затем используем метод Template() , который определен в классе Controller и отвечает за подстановку переменных в шаблон. Его реализация остается за вами. По нашей задумке метод Template() принимает два параметра: первый из них - название подключаемого шаблона, второй параметр - это массив переменных, которые должны быть подставлены в шаблон. В завершение сформированная страница выводится на экран. Как вы понимаете, это последняя операция, которую должен выполнить сценарий. Поэтому функция OnOutput() класса C_Base должна вызываться в самом конце нашего PHP-сценария.

Контроллеры C_View и C_Edit

C_Viev и C_Edit - контроллеры конкретных страниц сайта, они переопределяют методы OnInput() и OnOutput() , но также передают управление контроллеру C_Base .

Давайте рассмотрим пример реализации этих методов для класса C_View .

Protected function OnInput() { parent::OnInput(); $this->title = $this->title . " :: Чтение"; $this->text = $this->getText(); } protected function OnOutput() { $vars = array("text" => $this->text); $this->content = $this->Template("theme/v_view.php", $vars); parent::OnOutput(); }

В методе OnInput() сначала идет вызов родительского метода, затем определяется заголовок страницы и извлекается ее текст с помощью метода getText() . Конкретная реализация метода getText() для нас сейчас не важна. Он может получать текст из базы данных или из файла, или же вовсе возвращать некоторую предопределенную константу. Важно то, что в результате мы сохраняем текст в некотором поле класса.

Метод OnOutput() начинается с формирования массива, который передается в функцию формирования шаблона Template() . Результат ее выполнения сохраняется в поле content . После чего вызывается родительский метод OnOutput() . Вернитесь к тексту метода OnOutput() класса C_Base и обратите внимание на то, что в нем как раз-таки используется значение поля content , который мы формируем в текущем методе OnOutput() класса С_View .

Если вы взглянете на код еще раз, то заметите, что вызов родительских методов OnInput() и OnOuput() как бы обрамляет цикл обработки запроса в классе C_View .

Цикл обработки запроса

Теперь давайте посмотрим на общий вид обработки запроса, который мы получили. Выполнение сценария начинается с файла index.php. Здесь выбирается нужный контроллер, и ему передается управление вызовом метода Request() .

Этот метод запускает две фазы обработки запроса:

  1. Обращение к модели (OnInput());
  2. Генерация HTML (OnOutput()).
Первая фаза должна отработать в такой последовательности:
  1. Базовый контроллер (C_Base);
  2. Конкретный контроллер (C_View или C_Edit).
Далее начинается фаза генерации HTML, она должна пройти в обратной последовательности:
  1. Конкретный контроллер (C_View или C_Edit);
  2. Базовый контроллер (C_Base).

На диаграмме данный цикл можно представить так:


В заключение

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

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

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

Вы можеть быть даже слышали о шаблонах проектирования и даже листали эти прекрасные книги:

  • Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидесс «Приемы объектно ориентированного проектирования. Паттерны проектирования»;
  • М. Фаулер «Архитектура корпоративных программных приложений».
А многие, не испугавшись огромных руководств и документаций, пытались изучить какой-либо из современных фреймворков и столкнувшись со сложностью понимания (в силу наличия множества архитектруных концепций хитро увязанных между собой) отложили изучение и применение современных интсрументов в «долгий ящик».

Представленная статья будет полезна в первую очередь новичкам. Во всяком случае, я надеюсь что за пару часов вы сможете получить представление о реализации MVC паттерна, который лежит в основе всех современных веб-фреймворков, а также получить «пищу» для дальнейших размышлений над тем — «как стоит делать». В конце статьи приводится подборка полезных ссылок, которые также помогут разобраться из чего состоят веб-фреймворки (помимо MVC) и как они работают.

Прожженные PHP-программисты вряд ли найдут в данной статье что-то новое для себя, но их замечания и комментарии к основному тексту были бы очень кстати! Т.к. без теории практика невозможна, а без практики теория бесполезна, то сначала будет чуть-чуть теории, а потом перейдем к практике. Если вы уже знакомы с концепцией MVC, можете пропустить раздел с теорией и сразу перейти к практике.

1. Теория

Шаблон MVC описывает простой способ построения структуры приложения, целью которого является отделение бизнес-логики от пользовательского интерфейса. В результате, приложение легче масштабируется, тестируется, сопровождается и конечно же реализуется.

Рассмотрим концептуальную схему шаблона MVC (на мой взгляд — это наиболее удачная схема из тех, что я видел):

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

Типичную последовательность работы MVC-приложения можно описать следующим образом:

  1. При заходе пользователя на веб-ресурс, скрипт инициализации создает экземпляр приложения и запускает его на выполнение.
    При этом отображается вид, скажем главной страницы сайта.
  2. Приложение получает запрос от пользователя и определяет запрошенные контроллер и действие. В случае главной страницы, выполняется действие по умолчанию (index ).
  3. Приложение создает экземпляр контроллера и запускает метод действия,
    в котором, к примеру, содержаться вызовы модели, считывающие информацию из базы данных.
  4. После этого, действие формирует представление с данными, полученными из модели и выводит результат пользователю.
Модель — содержит бизнес-логику приложения и включает методы выборки (это могут быть методы ORM), обработки (например, правила валидации) и предоставления конкретных данных, что зачастую делает ее очень толстой, что вполне нормально.
Модель не должна напрямую взаимодействовать с пользователем. Все переменные, относящиеся к запросу пользователя должны обрабатываться в контроллере.
Модель не должна генерировать HTML или другой код отображения, который может изменяться в зависимости от нужд пользователя. Такой код должен обрабатываться в видах.
Одна и та же модель, например: модель аутентификации пользователей может использоваться как в пользовательской, так и в административной части приложения. В таком случае можно вынести общий код в отдельный класс и наследоваться от него, определяя в наследниках специфичные для подприложений методы.

Вид — используется для задания внешнего отображения данных, полученных из контроллера и модели.
Виды cодержат HTML-разметку и небольшие вставки PHP-кода для обхода, форматирования и отображения данных.
Не должны напрямую обращаться к базе данных. Этим должны заниматься модели.
Не должны работать с данными, полученными из запроса пользователя. Эту задачу должен выполнять контроллер.
Может напрямую обращаться к свойствам и методам контроллера или моделей, для получения готовых к выводу данных.
Виды обычно разделяют на общий шаблон, содержащий разметку, общую для всех страниц (например, шапку и подвал) и части шаблона, которые используют для отображения данных выводимых из модели или отображения форм ввода данных.

Контроллер связующее звено, соединяющее модели, виды и другие компоненты в рабочее приложение. Контроллер отвечает за обработку запросов пользователя. Контроллер не должен содержать SQL-запросов. Их лучше держать в моделях. Контроллер не должен содержать HTML и другой разметки. Её стоит выносить в виды.
В хорошо спроектированном MVC-приложении контроллеры обычно очень тонкие и содержат только несколько десятков строк кода. Чего, не скажешь о Stupid Fat Controllers (SFC) в CMS Joomla. Логика контроллера довольно типична и большая ее часть выносится в базовые классы.
Модели, наоборот, очень толстые и содержат большую часть кода, связанную с обработкой данных, т.к. структура данных и бизнес-логика, содержащаяся в них, обычно довольно специфична для конкретного приложения.

1.1. Front Controller и Page Controller

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

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

Рассмотрим два варианта адресной строки, по которым показывается какой-то текст и профиль пользователя.

Первый вариант:

  1. www.example.com/article.php?id=3
  2. www.example.com/user.php?id=4
Здесь каждый сценарий отвечает за выполнение определённой команды.

Второй вариант:

  1. www.example.com/index.php?article=3
  2. www.example.com/index.php?user=4
А здесь все обращения происходят в одном сценарии index.php .

Подход с множеством точек взаимодействия вы можете наблюдать на форумах с движком phpBB. Просмотр форума происходит через сценарий viewforum.php , просмотр топика через viewtopic.php и т.д. Второй подход, с доступом через один физический файл сценария, можно наблюдать в моей любимой CMS MODX, где все обращения проходят черезindex.php .

Эти два подхода совершенно различны. Первый — характерен для шаблона контроллер страниц (Page Controller), а второй подход реализуется паттерном контроллер запросов (Front Controller). Контроллер страниц хорошо применять для сайтов с достаточно простой логикой. В свою очередь, контроллер запросов объединяет все действия по обработке запросов в одном месте, что даёт ему дополнительные возможности, благодаря которым можно реализовать более трудные задачи, чем обычно решаются контроллером страниц. Я не буду вдаваться в подробности реализации контроллера страниц, а скажу лишь, что в практической части будет разработан именно контроллер запросов (некоторое подобие).

1.2. Маршрутизация URL

Маршрутизация URL позволяет настроить приложение на прием запросов с URL, которые не соответствуют реальным файлам приложения, а также использовать ЧПУ , которые семантически значимы для пользователей и предпочтительны для поисковой оптимизации.

К примеру, для обычной страницы, отображающей форму обратной связи, URL мог бы выглядеть так:
http://www.example.com/contacts.php?action=feedback

Приблизительный код обработки в таком случае:
switch ($_GET ["action" ]) { case "about" : require_once ("about.php" ); // страница "О Нас" break ; case "contacts" : require_once ("contacts.php" ); // страница "Контакты" break ; case "feedback" : require_once ("feedback.php" ); // страница "Обратная связь" break ; default : require_once ("page404.php" ); // страница "404" break ; }
Думаю, почти все так раньше делали.

С использованием движка маршрутизации URL вы сможете для отображения той же информации настроить приложение на прием таких запросов:
http://www.example.com/contacts/feedback

Здесь contacts представляет собой контроллер, а feedback — это метод контроллера contacts, отображающий форму обратной связи и т.д. Мы еще вернемся к этому вопросу в практической части.

Также стоит знать, что маршрутизаторы многих веб-фреймворков позволяют создавать произвольные маршруты URL (указать, что означает каждая часть URL) и правила их обработки.
Теперь мы обладаем достаточными теоретическими знаниями, чтобы перейти к практике.

2. Практика

Для начала создадим следующую структуру файлов и папок:


Забегая вперед, скажу, что в папке core будут храниться базовые классы Model, View и Controller.
Их потомки будут храниться в директориях controllers, models и views. Файл index.php это точка в хода в приложение. Файлbootstrap.php инициирует загрузку приложения, подключая все необходимые модули и пр.

Будем идти последовательно; откроем файл index.php и наполним его следующим кодом:
ini_set("display_errors" , 1 ); require_once "application/bootstrap.php" ;
Тут вопросов возникнуть не должно.

Следом, сразу же перейдем к фалу bootstrap.php :
require_once "core/model.php" ; require_once "core/view.php" ; require_once "core/controller.php" ; require_once "core/route.php" ; Route::start(); // запускаем маршрутизатор
Первые три строки будут подключать пока что несуществующие файлы ядра. Последние строки подключают файл с классом маршрутизатора и запускают его на выполнение вызовом статического метода start.

2.1. Реализация маршрутизатора URL

Пока что отклонимся от реализации паттерна MVC и займемся мрашрутизацией. Первый шаг, который нам нужно сделать, записать следующий код в .htaccess :
RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule .* index.php [L]
Этот код перенаправит обработку всех страниц на index.php , что нам и нужно. Помните в первой части мы говорили о Front Controller?!

Маршрутизацию мы поместим в отдельный файл route.php в директорию core. В этом файле опишем класс Route, который будет запускать методы контроллеров, которые в свою очередь будут генерировать вид страниц.

Содержимое файла route.php

class Route { static function start () { // контроллер и действие по умолчанию $controller_name = "Main" ; $action_name = "index" ; $routes = explode("/" , $_SERVER ["REQUEST_URI" ]); // получаем имя контроллера if (!empty ($routes )) { $controller_name = $routes ; } // получаем имя экшена if (!empty ($routes )) { $action_name = $routes ; } // добавляем префиксы $model_name = "Model_" .$controller_name ; $controller_name = "Controller_" .$controller_name ; $action_name = "action_" .$action_name ; // подцепляем файл с классом модели (файла модели может и не быть) $model_file = strtolower($model_name ).".php" ; $model_path = "application/models/" .$model_file ; if (file_exists($model_path )) { include "application/models/" .$model_file ; } // подцепляем файл с классом контроллера $controller_file = strtolower($controller_name ).".php" ; $controller_path = "application/controllers/" .$controller_file ; if (file_exists($controller_path )) { include "application/controllers/" .$controller_file ; } else { /* правильно было бы кинуть здесь исключение, но для упрощения сразу сделаем редирект на страницу 404 */ Route::ErrorPage404(); } // создаем контроллер $controller = new $controller_name ; $action = $action_name ; if (method_exists($controller , $action )) { // вызываем действие контроллера $controller ->$action (); } else { // здесь также разумнее было бы кинуть исключение Route::ErrorPage404(); } } function ErrorPage404 () { $host = "http://" .$_SERVER ["HTTP_HOST" ]."/" ; header("HTTP/1.1 404 Not Found" ); header("Status: 404 Not Found" ); header("Location:" .$host ."404" ); } }


Замечу, что в классе реализована очень упрощенная логика (несмотря на объемный код) и возможно даже имеет проблемы безопасности. Это было сделано намерено, т.к. написание полноценного класса маршрутизации заслуживает как минимум отдельной статьи. Рассмотрим основные моменты…

В элементе глобального массива $_SERVER["REQUEST_URI"] содержится полный адрес по которому обратился пользователь.
Например: example.ru/contacts/feedback

С помощью функции explode производится разделение адреса на составлющие. В результате мы получаем имя контроллера, для приведенного примера, это контроллер contacts и имя действия, в нашем случае — feedback .

Далее подключается файл модели (модель может отсутствовать) и файл контроллера, если таковые имеются и наконец, создается экземпляр контроллера и вызывается действие, опять же, если оно было описано в классе контроллера.

Таким образом, при переходе, к примеру, по адресу:
example.com/portfolio
или
example.com/portfolio/index
роутер выполнит следующие действия:

  1. подключит файл model_portfolio.php из папки models, содержащий класс Model_Portfolio;
  2. подключит файл controller_portfolio.php из папки controllers, содержащий класс Controller_Portfolio;
  3. создаст экземпляр класса Controller_Portfolio и вызовет действие по умолчанию — action_index, описанное в нем.
Если пользователь попытается обратиться по адресу несуществующего контроллера, к примеру:
example.com/ufo
то его перебросит на страницу «404»:
example.com/404
То же самое произойдет если пользователь обратится к действию, которое не описано в контроллере.

2.2. Возвращаемся к реализации MVC

Перейдем в папку core и добавим к файлу route.php еще три файла: model.php, view.php и controller.php


Напомню, что они будут содержать базовые классы, к написанию которых мы сейчас и приступим.

Содержимое файла model.php
class Model { public function get_data () { } }
Класс модели содержит единственный пустой метод выборки данных, который будет перекрываться в классах потомках. Когда мы будем создавать классы потомки все станет понятней.

Содержимое файла view.php
class View { //public $template_view; // здесь можно указать общий вид по умолчанию. function generate ($content_view , $template_view , $data = null) { /* if(is_array($data)) { // преобразуем элементы массива в переменные extract($data); } */ include "application/views/" .$template_view ; } }
Не трудно догадаться, что метод generate предназначен для формирования вида. В него передаются следующие параметры:

  1. $content_file — виды отображающие контент страниц;
  2. $template_file — общий для всех страниц шаблон;
  3. $data — массив, содержащий элементы контента страницы. Обычно заполняется в модели.
Функцией include динамически подключается общий шаблон (вид), внутри которого будет встраиваться вид
для отображения контента конкретной страницы.

В нашем случае общий шаблон будет содержать header, menu, sidebar и footer, а контент страниц будет содержаться в отдельном виде. Опять же это сделано для упрощения.

Содержимое файла controller.php
class Controller { public $model ; public $view ; function __construct () { $this ->view = new View(); } } }
Метод action_index — это действие, вызываемое по умолчанию, его мы перекроем при реализации классов потомков.

2.3. Реализация классов потомков Model и Controller, создание View"s

Теперь начинается самое интересное! Наш сайт-визитка будет состоять из следущих страниц:
  1. Главная
  2. Услуги
  3. Портфолио
  4. Контакты
  5. А также — страница «404»
Для каждой из страниц имеется свой контроллер из папки controllers и вид из папки views. Некоторые страницы могут использовать модель или модели из папки models.


На предыдущем рисунке отдельно выделен файл template_view.php — это шаблон, содержащий общую для всех страниц разметку. В простейшем случае он мог бы выглядеть так:
<html lang ="ru" > <head > <meta charset ="utf- 8 "> <title > Главнаяtitle > head > <body > $content_view ; ?> body > html >
Для придания сайту презентабельного вида сверстаем CSS шаблон и интегририруем его в наш сайт путем изменения структуры HTML-разметки и подключения CSS и JavaScript файлов:
<link rel ="stylesheet" type ="text/css" href ="/css/style.css" /> <script src ="/js/jquery-1.6.2.js" type ="text/javascript" > script >

В конце статьи, в разделе «Результат», приводится ссылка на GitHub-репозиторий с проектом, в котором проделаны действия по интеграции простенького шаблона.

2.3.1. Создадаем главную страницу

Начнем с контроллера controller_main.php , вот его код:
class Controller_Main extends Controller { function action_index () { $this ->view->generate("main_view.php" , "template_view.php" ); } }
В метод generate экземпляра класса View передаются имена файлов общего шаблона и вида c контентом страницы.
Помимо индексного действия в контроллере конечно же могут содержаться и другие действия.

Файл с общим видом мы рассмотрели ранее. Рассмотрим файл контента main_view.php :
<h1 > Добро пожаловать!h1 > <p > <img src ="/images/office-small.jpg" align ="left" > <a href ="/" > ОЛОЛОША TEAMa > - команда первоклассных специалистов в области разработки веб-сайтов с многолетним опытом коллекционирования мексиканских масок, бронзовых и каменных статуй из Индии и Цейлона, барельефов и изваяний, созданных мастерами Экваториальной Африки пять-шесть веков назад... p >
Здесь содержиться простая разметка без каких либо PHP-вызовов.
Для отображения главной странички можно воспользоваться одним из следующих адресов:

  • методы библиотек, реализующих абстракицю данных. Например, методы библиотеки PEAR MDB2;
  • методы ORM;
  • методы для работы с NoSQL;
  • и др.
  • Для простоты, здесь мы не будем использовать SQL-запросы или ORM-операторы. Вместо этого мы сэмулируем реальные данные и сразу возвратим массив результатов.
    Файл модели model_portfolio.php поместим в папку models. Вот его содержимое:
    class Model_Portfolio extends Model { public function get_data () { return array (array ("Year" => "2012" , "Site" => "http://DunkelBeer.ru" , "Description" => "Промо-сайт темного пива Dunkel от немецкого производителя Löwenbraü выпускаемого в России пивоваренной компанией "CАН ИнБев"." ), array ("Year" => "2012" , "Site" => "http://ZopoMobile.ru" , "Description" => "Русскоязычный каталог китайских телефонов компании Zopo на базе Android OS и аксессуаров к ним." ), // todo ); } }

    Класс контроллера модели содержится в файле controller_portfolio.php , вот его код:
    class Controller_Portfolio extends Controller { function __construct () { $this ->model = new Model_Portfolio(); $this ->view = new View(); } function action_index () { $data = $this ->model->get_data(); $this ->view->generate("portfolio_view.php" , "template_view.php" , $data ); } }
    В переменную data записывается массив, возвращаемый методом get_data , который мы рассматривали ранее.
    Далее эта переменная передается в качестве параметра метода generate , в который также передаются: имя файла с общим шаблон и имя файла, содержащего вид c контентом страницы.

    Вид содержащий контент страницы находится в файле portfolio_view.php .

    Портфолио

    Все проекты в следующей таблице являются вымышленными, поэтому даже не пытайтесь перейти по приведенным ссылкам. " ; }


    Ссылка на GitHub: https://github.com/vitalyswipe/tinymvc/zipball/v0.1

    А вот в этой версии я набросал следующие классы (и соответствующие им виды):

    • Controller_Login в котором генерируется вид с формой для ввода логина и пароля, после заполнения которой производится процедура аутентификации и в случае успеха пользователь перенаправляется в админку.
    • Contorller_Admin с индексным действием, в котором проверяется был ли пользователь ранее авторизован на сайте как администратор (если был, то отображается вид админки) и действием logout для разлогинивания.
    Аутентификация и авторизация — это другая тема, поэтому здесь она не рассматривается, а лишь приводится ссылка указанная выше, чтобы было от чего оттолкнуться.

    4. Заключение

    Шаблон MVC используется в качестве архитектурной основы во многих фреймворках и CMS, которые создавались для того, чтобы иметь возможность разрабатывать качественно более сложные решения за более короткий срок. Это стало возможным благодаря повышению уровня абстракции, поскольку есть предел сложности конструкций, которыми может оперировать человеческий мозг.

    Но, использование веб-фреймворков, типа Yii или Kohana, состоящих из нескольких сотен файлов, при разработке простых веб-приложений (например, сайтов-визиткок) не всегда целесообразно. Теперь мы умеем создавать красивую MVC модель, чтобы не перемешивать Php, Html, CSS и JavaScript код в одном файле.

    Данная статья является скорее отправной точкой для изучения CMF, чем примером чего-то истинно правильного, что можно взять за основу своего веб-приложения. Возможно она даже вдохновила Вас и вы уже подумываете написать свой микрофреймворк или CMS, основанные на MVC. Но, прежде чем изобретать очередной велосипед с «блекджеком и шлюхами», еще раз подумайте, может ваши усилия разумнее направить на развитие и в помощь сообществу уже существующего проекта?!

    P.S.: Статья была переписана с учетом некоторых замечаний, оставленных в комментариях. Критика оказалась очень полезной. Судя по отклику: комментариям, обращениям в личку и количеству юзеров добавивших пост в избранное затея написать этот пост оказалось не такой уж плохой. К сожалению, не возможно учесть все пожелания и написать больше и подробнее по причине нехватки времени… но возможно это сделают те таинственные личности, кто минусовал первоначальный вариант. Удачи в проектах!

    5. Подборка полезных ссылок по сабжу

    В статье очень часто затрагивается тема веб-фреймворков — это очень обширная тема, потому что даже микрофреймворки состоят из многих компонентов хитро увязанных между собой и потребовалась бы не одна статья, чтобы рассказать об этих компонентах. Тем не менее, я решил привести здесь небольшую подборку ссылок (по которым я ходил при написаниие этой статьи), которые так или иначе касаются темы фреймворков.

    В этой статье я расскажу вам об одной из самых распространённых уязвимостей, встречающихся в php-сценариях - include "баге". Вы узнаете как принцип действия, так и некоторые способы устранения данной уязвимости.

    Внимание!!! Вся информация представленная в данной статье служит чисто в ознакомительных целях! Автор не несёт никакой ответственности за её злонамеренное применение!

    Уязвимость php - include одна из самых известных, но между тем и самых распространённых "дыр" встречающихся сегодня в php сценариях. Она возникает, когда по невнимательности, незнанию, либо по какой-то другой ведомой только ему одному причине, программист позволяет использовать данные, переданные сценарию в виде параметров, без дополнительной проверки (такие данные ещё называют "мечеными") в качестве параметра функцией include. Для того, чтобы лучше разобраться в принципе действия данной уязвимости, необходимо иметь некоторое представление о вышеназванной функции.

    php функция include, а также include_once

    Данная функция используется для подключения к запущенному php сценарию дополнительных программных модулей. Причём, в отличие от похожей по свойствам require, функция include исполняет эти модули непосредственно в своём процессе. А следовательно, прикрепляемые таким образом модули будут исполняться не как отдельные сценарии, а как части подключившего их к себе сценария. Точнее, include будет исполнять только ту часть файла, которая заключена между спец. тэгами:

    " "?>"

    Всё остальное php просто выдаёт в виде текста. Т.е. если подключить текстовый файл (например: /etc/passwd:)) не содержащий указанных тэгов, всё содержимое этого файла будет выдано интерпретатором.

    Пример вызова:

    Как вы наверное заметили, функция include имеет всего 1 параметр ($file), который указывает путь и имя файла подключаемого модуля. Стоит отметить также, что в юниксоподобных системах (в зависимости от настроек php) в качестве параметра можно передавать не только путь и имя файла, но и url (Интернет адрес) файла(!!!).

    Практика

    Предположим, на некотором ВЕБ-сервере установлен следующий php сценарий (его url http://www.superpupersite.com/index.php):

    include($http_get_vars["file"]);
    ?>

    А также множество различных подключаемых сценариев-модулей для него:

    home.php
    feedback.php
    ...

    Автор этого сценария предполагал, что все посетители сайта будут мирно переходить от одной страницы к другой нажимая кнопочки, ссылочки и прочие объекты управления. А сценарий, в зависимости от переданного параметра file, будет присоединять один или другой модуль, таким образом генерируя различные html страницы (чаще всего include используют именно таким образом).

    Он даже представить себе не мог, что однажды (в студёную зимнюю пору) на сайт забредёт некий любознательный Вася Пупкин. Который, исходя из своей любознательности рассматривая эти самые ссылки, предположил бы (пока он ещё не знает, как и что там на самом деле), что параметр file является не чем иным, как имя и путь к файлу и что сценарий использует функцию include (не удивительно, т.к. на сегодня include используется чуть ли не в каждом 3-ем скрипте). Вася тут же решил проверить своё предположение следующим образом:

    Вася выполнил следующий запрос:

    И у него получилось! Как и задумывалось, в окне браузера он увидел список файлов и каталогов. Далее по нарастающей, "дыра" была обнаружена, и в ход пошли не менее интересные сценарии, подробное описание которых заняло бы немало места и по этим соображениям не публикуется здесь:) В общем, долго ли, коротко ли, но закончилось всё дефейсом (дефейс - подмена начальной страницы на свою). Вот такая грустная история!

    Борьба с вредителем

    Прочитав всё вышеописанное, многие из вас задаются вопросом: "существуют ли методы борьбы с этой ужастной уязвимостью? ". "Да" - гордо отвечау я:) . Вот некоторые (отнюдь не все) из них:

    Самый простой способ, с точки зрения программирования, это преобразовать переменную $module в числовой формат (settype($module,”integer”)), но при этом придётся пронумеровать модули, а также собрать их в один каталог (“module1.php”, ”module2.php” …”module.php”).

    Более сложный с точки зрения реализации метод борьбы с вредителями:) - это создание отдельного файла-списка модулей, которые возможно запустить. И в зависимости, находится ли тот или иной модуль в списке, выполнять либо выдавать соответствующую ошибку (или запускать модуль по умолчанию или если Вы, хотите напугать «экспериментатора» выдать сообщение о том, что его адрес зафиксирован и чтоб он сушил сухари...).
    Пример:
    switch ($case) // $case - имя переменной передаваемой в параметре к скрипту
    {
    case news:
    include("news.php");
    break;

    case articles:
    include("guestbook.php");
    break;

    ... // и т.д.
    default:
    include("index.php"); // если в переменной $case не будет передано значение, которое учтено выше, то открывается главная страница
    break;
    }

    Третий метод является промежуточным- что-то среднее между 1-ым и 2-ым. Вам просто надо заменить все служебные символы ("..","/","") например, на прочерки. Правда, модули (там должны располагаться только выполняемые модули и ничего кроме них!!!) в этом случае должны располагаться в одном каталоге, но их названия могут быть нормальными словами (например “news”, ”guestbook” и т.д.).
    Заключение

    Вот в общем-то и всё, что я хотел рассказать вам в этот раз. Вывод из этого всего может быть такой: прежде чем используете данные полученные от пользователя в ваших web сценариях подумайте, а не надо ли их предварительно проверить и надлежащим образом обработать. Это касается не только полей данных формы передаваемых браузером (методы get и post), но и cookie (злоумышленник может добраться и до них).

    В этом руководстве Вы узнаете, как построить простую систему по архитектуре MVC (Model-View-Controller, Модель-Отображение-Контроллер) на PHP 5.1 с использованием возможностей библиотеки SPL (Standard PHP Library, Стандартная Библиотека PHP).

    Введение

    Добро пожаловать в первое полноценное руководство для PHP 5. Вам понадобится PHP 5.1 с установленной библиотекой SPL, так как мы воспользуемся некоторыми из самых последних возможностей PHP 5.

    В этом руководстве я собираюсь показать, как построить простую MVC-систему (архитектура MVC является наиболее распространённым шаблоном проектирования для больших web-приложений). Я проведу Вас через все шаги от начала и до конца создания полноценной MVC-системы.

    Одна точка входа

    Одной из важных вещей в MVC является одна точка входа в приложение вместо кучи PHP-файлов, делающих примерно следующее:

    У нас будет один файл, обрабатывающий все запросы. Это значит, что нам не придётся мучиться с подключением global.php каждый раз, когда нам нужно создать новую страницу. Эта «одна точка входа» будет называться index.php и на данный момент будет такой:

    Как Вы можете заметить, этот скрипт пока ещё ничего не делает, но погодите минутку.

    Чтобы направить все запросы на главную страницу, мы воспользуемся mod_rewrite и установим в.htaccess директиву RewriteRule. Вставим следующий код в файл.htaccess и сохраним его в той же директории, что и index.php:

    RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?route=$1

    Сперва мы проверяем, существует ли запрашиваемый файл, используя директиву RewriteCond, и, если нет, то перенаправляем запрос на index.php. Такая проверка на существование файла необходима, так как иначе index.php будет пытаться обрабатывать все запросы к сайту, включая запросы на изображения. А это нам как раз и не надо.

    Если у Вас нет возможности использовать.htaccess или mod_rewrite, то Вам придётся вручную адресовать все запросы к index.php. Другими словами, все ссылки должны будут иметь вид «index.php?route=[здесь-идёт-запрос]». Например, «index.php?route=chat/index».

    Теперь, когда все запросы идут через одну точку входа, мы можем начать написание скрипта index.php. Первая вещь, которую мы должны сделать, это инициализация системы. Создадим директорию includes, а в ней файл startup.php (он будет у нас файлом инициализации). Вставим следующий код в index.php:

    В этом примере мы объявляем некоторую константу, узнаём, где лежат файлы системы, а также проверяем, что версия PHP, ну, хотя бы, 5.1.

    Следующая вещь, которую необходимо сделать, это объект Registry (журнал, реестр) для хранения глобальных значений. Он будет передаваться в отдельные объекты системы и использоваться для доступа к глобальным значениям, причём без необходимости обозначать переменные как «global» или обращаться к массиву $GLOBALS. Почитайте статью «Использование глобальных значений в PHP» для более подробной информации об объекте реестра.

    Добавьте следующий код в файл startup.php после того кода, что приведён в предыдущем примере:

    $registry = new Registry;

    Если сейчас попробовать запустить систему, то можно увидеть следующую ошибку:

    Fatal error: Class "Registry" not found in g:\Projects\PHP\content\simple mvc php5\demo\includes\startup.php on line 12

    Это, конечно, не большой сюрприз для нас, ведь мы ещё не написали сам класс Registry. Файл с классом можно было бы просто подключить, используя функцию include() (Прим. пер.: кстати говоря, include() не такая уж и функция, а всё-таки выражение языка, управляющая структура, если смотреть по ману), но давайте воспользуемся одной из новых возможностей PHP 5: __autoload().

    Волшебная функция __autoload() используется для динамической загрузки классов. Когда PHP обнаруживает несуществующий класс, он сначала вызывает функцию __autoload() и только затем выдаёт ошибку. Мы можем воспользоваться такой возможностью для загрузки классов «на лету».

    Вставьте этот код перед кодом из предыдущего примера:

    // Загрузка классов «на лету» function __autoload($class_name) { $filename = strtolower($class_name) . ".php"; $file = site_path . "classes" . DIRSEP . $filename; if (file_exists($file) == false) { return false; } include ($file); }

    Наша функция __autoload() берёт имя класса, переданное ей как аргумент, и проверяет, существует ли файл с похожим именем в директории с классами. Если файла нет, то функция просто вернёт false и выскочит фатальная ошибка. Но если файл существует, он будет загружен. Т.е. объявится необходимый класс, и никакой ошибки не будет.

    Мы ещё не создали сам класс Registry, поэтому ошибка всё ещё будет появляться. Давайте же займёмся этим.

    Создание класса Registry

    Класс Registry используется для передачи глобальных значений между отдельными объектами. Это на самом деле довольно простой класс, в котором нужно реализовать несколько маленьких методов.

    Для начала создадим директорию classes и в ней файл registry.php. Вставим следующий код в registry.php:

    Теперь у нас есть «скелет» класса Registry и нужно нагрузить его методами. Напишем 2 метода: set(), чтобы устанавливать значения и get(), чтобы значения получать. Также можно написать метод remove() для удаления значений. Добавим эти методы в класс Registry:

    Function set($key, $var) { if (isset($this->vars[$key]) == true) { throw new Exception("Unable to set var `" . $key . "`. Already set."); } $this->vars[$key] = $var; return true; } function get($key) { if (isset($this->vars[$key]) == false) { return null; } return $this->vars[$key]; } function remove($var) { unset($this->vars[$key]); } ?>

    Эти методы простые, они устанавливают, получают и удаляют элементы из массива $vars, который является атрибутом класса. В методе set() мы заодно проверяем, не существует ли уже значение с указанным ключом, и, если существует, то мы генерируем исключение. Это нужно, чтобы избежать случайной перезаписи значений.

    Теперь у нас есть полноценный класс Registry, но мы не будем останавливаться на этом. Воспользуемся одной из возможностей библиотеки SPL: ArrayAccess. SPL (сокращённо от Standard PHP Library, Стандартная Библиотека PHP) - это коллекция интерфейсов и классов, предназначенных для решения стандартных проблем. Один из интерфейсов SPL, ArrayAccess, может быть использован, чтобы предоставить доступ к объекту, как к обычному массиву. Посмотрим на такой пример:

    set ("name", "Dennis Pallett"); // Получаем значение, используя get() echo $registry->get ("name"); // Получаем значение, используя доступ как к массиву echo $registry["name"] ?>

    Фокус в том, что $registry становится как бы массивом, хотя на самом деле это объект. Конечно, ArrayAccess не даёт никаких особых преимуществ, но он позволяет сократить объём кода, так как не придётся каждый раз писать «->get()». Чтобы воспользоваться этим интерфейсом, нужно исправить первую строчку класса («Class Registry») таким образом:

    Class Registry Implements ArrayAccess {

    Ключевое слово «Implements» говорит интерпретатору, что этим классом мы реализуем интерфейс, чем на самом деле ArrayAccess и является.

    Класс, реализующий интерфейс ArrayAccess, должен иметь следующие методы:

    Function offsetExists($offset) { return isset($this->vars[$offset]); } function offsetGet($offset) { return $this->get($offset); } function offsetSet($offset, $value) { $this->set($offset, $value); } function offsetUnset($offset) { unset($this->vars[$offset]); }

    Эти методы должны быть понятны сами по себе. Дополнительную информацию можно найти в документации SPL.

    Теперь, реализовав интерфейс ArrayAccess, мы можем обращаться к объекту, как к обычному массиву. Это наглядно продемонстрировано, как в предыдущем примере, так и в этом:

    get ("name"); // Получаем значение, используя доступ как к массиву echo $registry["name"] ?>

    Класс Registry теперь завершён, и, если попробовать запустить систему, всё должно заработать (хотя ещё ничего не будет выводиться). Мы закончили с файлом инициализации и можно приступать к следующему шагу написания нашей MVC-системы: реализация доступа к базе данных, что в архитектуре MVC называется «Model» («Модель»).

    Модель

    «M» или модель – часть MVC-системы, которая отвечает за запросы к базе данных (или другому внешнему источнику) и предоставление информации контроллеру. Можно было бы загружать необходимую модель в зависимости от запроса, но я предпочитаю немного стереть границы между моделью и контроллером именно в этом месте, т.е. контроллер работает с БД непосредственно через библиотеку взаимодействия с БД, нежели чем через отдельную модель. Может быть, Вам захочется сделать это по-другому, тут дело вкуса.

    Нам нужно написать код, необходимый для установки соединения с БД и поместить его в index.php. Существует множество замечательных библиотек для работы с БД (включая мою собственную, AutoCRUD), но в PHP 5 уже есть такая библиотека – PDO. Поэтому нет нужды использовать какую-либо другую.

    Вставим следующий код в файл index.php (после подключения файла инициализации):

    # Соединяемся с БД $db = new PDO("mysql:host=localhost;dbname=demo", "", ""); $registry->set ("db", $db);

    В этом примере мы сначала создаём новый экземпляр библиотеки PDO и соединяемся с нашей БД MySQL. Потом делаем переменную $db доступной глобально при помощи нашего класса Registry.

    Модельная компонента нашей системы готова, поэтому давайте перейдём к написанию контроллера.

    Написание контроллера подразумевает также и написание класса Router, ответственного за загрузку нужного контроллера в зависимости от запроса (вспомните, в index.php через URL передаётся переменная $route).

    Класс Router

    Класс Router будет разбирать запрос, а потом загружать требуемый контроллер. Создадим «скелет» класса:

    registry = $registry; } } ?>

    Затем добавим следующие строки в index.php:

    # Загружаем router $router = new Router($registry); $registry->set ("router", $router);

    Первая вещь, которую мы напишем, это метод setPath() для установки директории, где будут лежать все наши контроллеры. Метод выглядит следующим образом и должен быть добавлен в класс Router:

    Function setPath($path) { $path = trim($path, "/\\"); $path .= DIRSEP; if (is_dir($path) == false) { throw new Exception ("Invalid controller path: `" . $path . "`"); } $this->path = $path; }

    Потом добавим следующие строки в index.php:

    $router->setPath (site_path . "controllers");

    Теперь, когда мы установили путь до наших контроллеров, напишем сам метод, ответственный за загрузку контроллера. Этот метод будет называться delegate(), он и будет анализировать запрос. Первый кусочек этого метода такой:

    Function delegate() { // Анализируем путь $this->getController($file, $controller, $action, $args);

    Как Вы можете видеть, он использует ещё один метод, getController(), чтобы получить название контроллера и несколько других переменных. Этот метод выглядит так:

    Private function getController(&$file, &$controller, &$action, &$args) { $route = (empty($_GET["route"])) ? "" : $_GET["route"]; if (empty($route)) { $route = "index"; } // Получаем раздельные части $route = trim($route, "/\\"); $parts = explode("/", $route); // Находим правильный контроллер $cmd_path = $this->path; foreach ($parts as $part) { $fullpath = $cmd_path . $part; // Есть ли папка с таким путём? if (is_dir($fullpath)) { $cmd_path .= $part . DIRSEP; array_shift($parts); continue; } // Находим файл if (is_file($fullpath . ".php")) { $controller = $part; array_shift($parts); break; } } if (empty($controller)) { $controller = "index"; }; // Получаем действие $action = array_shift($parts); if (empty($action)) { $action = "index"; } $file = $cmd_path . $controller . ".php"; $args = $parts; }

    Пробежимся по этому методу. Сначала он берёт значение переменной $route из запроса, потом разбивает его на части с помощь функции explode(). Например, запрос «members/view» преобразуется в такой массив: array(‘members’, ‘view’).

    Потом при помощи цикла foreach он проходит по каждой части и проверяет, является ли эта часть директорией. Если является, то он приписывает её к пути до файла и проверяет следующую часть. Это позволяет поместить контроллеры в поддиректориях и, таким образом, получить иерархию контроллеров. Если же текущая часть запроса не является директорией, но является файлом, она сохраняется в переменную $controller, и мы выходим из цикла, так как нашёлся контроллер, который нам нужен.

    После цикла мы проверяем переменную с именем контроллера. Если она пустая, то используем контроллер «index», который будет у нас контроллером по умолчанию. Потом метод определяет действие, которое необходимо выполнить. Контроллер – это класс, который состоит из нескольких методов. Действие же указывает на конкретный метод. Если действие не указано, будем использовать «index» - действие по умолчанию.

    И, наконец, получаем полный путь до файла контроллера, объединяя три переменные: путь, имя контроллера и расширение «php».

    Теперь, когда мы проанализировали запрос, пора вызывать метод delegate() для загрузки контроллера и выполнения действия. Полностью метод delegate() выглядит так:

    Function delegate() { // Анализируем путь $this->getController($file, $controller, $action, $args); // Файл доступен? if (is_readable($file) == false) { die ("404 Not Found"); } // Подключаем файл include ($file); // Создаём экземпляр контроллера $class = "Controller_" . $controller; $controller = new $class($this->registry); // Действие доступно? if (is_callable(array($controller, $action)) == false) { die ("404 Not Found"); } // Выполняем действие $controller->$action(); }

    Проанализировав запрос при помощи метода getController(), мы проверяем, существует ли в действительности файл, и, если нет, то возвращаем простое сообщение об ошибке.

    После этого мы подключаем файл с контроллером и создаём экземпляр его класса, называться который должен «Controller_[имя]». Чуть позже мы поговорим о контроллерах более подробно.

    Потом мы проверяем, есть ли указанное действие (т.е. метод) и возможно ли к нему обратиться (используем для этого функцию is_callable()). Наконец, мы выполняем непосредственно само действие, на чём роль класса Router и завершается.

    Написав полностью метод delegate(), добавим следующую строчку в файл index.php:

    $router->delegate();

    Если попробовать сейчас запустить систему, то мы увидим следующую ошибку (разумеется, если директории controllers ещё нет):

    Fatal error: Uncaught exception "Exception" with message "Invalid controller path: `g:\Projects\PHP\content\simple mvc php5\demo\controllers\`" in g:\Projects\PHPit\content\simple mvc php5\demo\classes\router.php:18 Stack trace: #0 g:\Projects\PHP\content\simple mvc php5\demo\index.php(13): Router->setPath("g:\Projects\PHP...") #1 {main} thrown in g:\Projects\PHP\content\simple mvc php5\demo\classes\router.php on line 18

    Или же мы увидим ошибку «404 Not Found», так как ещё нет ни одного контроллера. Но этим-то мы сейчас и займёмся.

    Контроллер

    Контроллеры в нашей MVC-системе будут достаточно простыми и потребуют совсем немного времени. Во-первых, удостоверимся, что директория controllers существует. Создадим файл controller_base.php в директории classes и вставим в него следующий код:

    registry = $registry; } abstract function index(); } ?>

    Этот абстрактный класс будет родительским классом для всех наших контроллеров. Он будет делать всего лишь две вещи: сохранять локальную копию класса Registry и при помощи абстрактного метода index() заставлять все дочерние контроллеры реализовывать этот метод.

    Напишем наш первый контроллер. Создадим файл index.php в директории controllers и вставим в него такой код:

    Только что мы создали наш первый контроллер и, если попробовать запустить систему, то можно увидеть следующее:

    Это означает, что класс Router выполнил свою работу и запустил требуемое действие из требуемого контроллера. Давайте напишем ещё один контроллер, который будет соответствовать запросу «members/view». Создадим файл members.php в директории контроллеров и вставим в него такой код:

    Теперь зайдём в нашу MVC-систему по запросу «members/view» или же «index.php?route=members/view». Мы должны увидеть такой результат:

    Только лишь написанием нового контроллера и добавлением в него метода, мы смогли создать новую страницу, и ничего не пришлось менять в самой системе. Кроме того, нашим контроллерам не нужно подключать файл global.php или делать что-нибудь в таком роде.

    Теперь, когда у нас есть контроллеры, осталась лишь одна вещь: «V» или «View» («Отображение»).

    Отображение

    Как и в случае с моделями, есть несколько различных вариантов создания компоненты View в MVC-системе. Мы могли бы научить класс Router автоматически загружать ещё один файл, названный как-нибудь так: «view_{имя}.php». Но чтобы сделать руководство более понятным, напишем класс Template, который будет заниматься выводом шаблонов.

    Сначала создадим файл template.php в директории classes и вставим в него следующий код:

    registry = $registry; } } ?>

    Теперь у нас есть основная структура нашего класс Template. Следующим шагом добавим такой код в файл index.php прямо перед строками, связанными с классом Router:

    # Создаём объект шаблонов $template = new Template($registry); $registry->set ("template", $template);

    Так как нам понадобится использовать значения из моделей и контроллеров, то напишем метод set() для установки переменных, доступных в шаблонах. Посмотрим на пример:

    Function set($varname, $value, $overwrite=false) { if (isset($this->vars[$varname]) == true AND $overwrite == false) { trigger_error ("Unable to set var `" . $varname . "`. Already set, and overwrite not allowed.", E_USER_NOTICE); return false; } $this->vars[$varname] = $value; return true; } function remove($varname) { unset($this->vars[$varname]); return true; }

    Методы set() и remove() достаточно простые и используются, соответственно, для установки и удаления переменных.

    Займёмся написанием метода show(), который будет отображать шаблоны. Простейший путь – это создать отдельную директорию templates, где хранить все файлы шаблонов, и использовать include() для вывода шаблона. Разумеется, Ваш собственный метод show() может быть совершенно другим и загружать шаблоны из базы данных или делать что-нибудь ещё. Посмотрим на кусо

    Голосов: 745 | Просмотров: 8082

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

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

    Поиск file inclusion

    Вначале разберемся как определить параметры которые отвечают за инклуд файлов.

    Поиск параметров
    Для поиска параметров есть два варианта: автоматический или ручной поиск.

    Автоматический поиск
    Автоматический поиск можно осуществить тем же spider"ом в burpsuite. Вы можете у нас в вики найти статью burpsuite.

    Ручной поиск
    Сейчас я поговорю о ручном поиске. Предположим, что мы нашли GET параметр:

    Http://site.ru/folder/index.php?file=gallery

    Подставим под параметр строку "index":

    Http://site.ru/folder/index.php?file=index

    Если у вас открылся какой то из файлов index.(любое расширение), находящихся на сайте, то тут мы уже однозначно можем утверждать, что файл отвечает за подкачку файла.

    Именно такие параметры нам и нужны.

    Определение фильтров
    После того, как мы получили список параметров, нужно проверить, есть ли у них фильтрация.

    Нулевая фильтрация
    Попробуем подкачать файлы, которые не рассчитывали показывать=)

    Аналогом такого файла в линкусе является файл /etc/passwd

    (В данном случае за строку с параметром мы взяли http://site.ru/folder/index.php?file=index.html)

    Попробуем его подкачать:

    Http://site.ru/folder/index.php?file=/../../../../../../etc/passwd

    Объясняю что происходит - переход в папку /../ означает поднятие по иерархии вверх (точнее это уязвимость path traversal). Т.к. папка etc лежит в корневой папке, то мы должны ее достичь угадыванием: то есть чем чаще мы поднимаемся вверх, тем выше шанс, что мы окажемся в корневой папке (то мы должны написать несколько раз /../).

    Если файл показался. То считайте, что вы нашли LFI. В этом случае фильтр вообще отсутствует.

    Нулевой байт
    В данном случае за строку с параметром мы взяли http://site.ru/folder/index.php?file=index , то есть с отсутствующим окончанием.
    Но даже при отсутствии фильтра могут быть проблемы. Например в конце параметра может приписываться окончание.

    Например с запросом /../../../../../../etc/passwd может преобразоваться в

    /../../../../../../etc/passwd.php

    Но и на этот раз есть вариант исправить строку.В старых версиях PHP остался такой недостаток, как Null Byte Injection .
    Один из них - это приписывание нулевого байта. Параметры, при передаче по http, зашифровываются в url шифрование. И в этой кодировке нулевой байт выглядит именно в %00.

    А т.к. в PHP и многих других языках чтение строки идет именно до нулевого байта, то мы можем приписать его в середину строки, чтобы следующая часть строки отбрасывалась.

    В данном случае если мы впишем в параметр /../../../../../../etc/passwd.php%00 , то получим следующую строку:

    Http://site.ru/folder/index.php?file=/../../../../../../etc/passwd%00

    И строка в памяти сервера будет выглядеть как:

    /../../../../../../etc/passwd%00.php ==> /../../../../../../etc/passwd

    И в итоге мы смогли отбросить окончание и получить нужный файл.

    String limit
    Еще один вариант отбрасывания окончания возможен при String Limit - укорачиванию строки.
    И какая же от этого польза? А что если мы отбросим часть строки с окончанием, то получится нужная нам строка,но уже без окончания.

    Уже на этот раз нам может помочь строка /./ . Объясняю, что происходит:

    /./././././index === index

    Если точнее, то в bash эти две строки идентичны. Приведу пример, как это может помочь

    1) У нас есть параметр, строка которого укорачивается до 100 символов 2) Попробуем вывести файл index.txt, при условии, что приписывается окончание.php 3) Попробуем ввести index.txt - в итоге выводится index.txt.php 4) Чтобы обойти защиту, нужно ввести index.txt/././././../...<<(100-10)/2 раз>>.../././ 5) В итоге получается, что к этой длинной строке приписывается.php, которое в последствии отбрасывается 6) Profit!

    php filter
    По мне самый интересный вариант lfi является lfi с php filter. Сразу привожу пример

    Http://site.ru/folder/index.php?file=php://filter/convert.base64-encode/resource=index

    В итоге у нас в браузере не запустится php файл, а выведется его base64 исходников.
    Это в последнее время появляется на соревнованиях все чаще и чаще.

    Эксплуатация уязвимости

    Если вы читаете этот пункт,то вероятнее всего уже нашли LFI. Тогда разберем, чем она может нам быть полезна.

    Получение важных файлов (не скриптов)
    Ну самое типичное для lfi - скачивание файлов, которые мы, по задумке администратора, не должны были скачивать. Приведу пример задания:

    Задание: Дан файлообменник, есть несколько аккаунтов - admin и user(и паролем test). Нужно скачать файл flag.txt, хранящийся у аккаунта admin. Решение: 1) Загрузим свой файл, и посмотрим на ссылку на его скачивание. Она будет вида http://site.ru/download.php?file=user/image.png 2) И в правду папка user существует. Но при скачивании по ссылке http://site.ru/user/image.png идет ошибка 403, что вполне логично. 3) На всякий случай составим ссылку с однозначно отсутствующим файлом в папке user, и если ответ будет 404, то понимаем, что ответ 403 == ответу 200. 4) Проверим, верно ли, что наш файл должен быть по пути admin/flag.txt: http://site.ru/admin/flag.txt возвращает 403 (вспоминаем предыдущий пункт). 5) А почему бы, раз скачать не можем, не направить скрипт download.php на нужный нам файл? Пробуем перейти по http://site.ru/download.php?file=admin/flag.txt и получаем файл. 6) Profit!

    Получение исходников
    В получении исходников могут быть некоторые проблемы. Например, при эксплуатации исходники не высвечиваются, а запускаются. В этом случае к нам на помощь приходит как раз php filter.
    Примеры тут достаточно простые, так что не вижу смысла их приводить.

    LFI -> RCE
    Т.к. этот раздел уже переходит в более опасный раздел RCE, то вполне логично перенести дискуссию по этому поводу в нее. Ищите на сайте =)

    Множество примеров

    SharifCTF 2016 - technews

    Решение

    1. Смотрим путь до картинок, открываем напрямую папку /files/ и видим папку /flag/ 2. Проверяем существование файла /files/flag/flag.txt (403 ошибка) 3. Проверив burpsuit"ом замечаем, что некоторые картинки подкачиваются как images.php?id=files/images/heart.jpg 4. При images.php?id = php://filter/convert.base64-encode/resource=files/images/heart.jpg картинка возвращается в незашифрованом виде, что наводит на мысли регулярного выражения. 5. Изучаем форму и обходим регулярку с помощью запроса: images.php?id=php://abcdresource=files/flag/heart.jpg/resource=files/flag/flag.txt



    Есть вопросы?

    Сообщить об опечатке

    Текст, который будет отправлен нашим редакторам:

    ГодПроектОписание
    " .$row ["Year" ]."" .$row ["Site" ]."" .$row ["Description" ]."