- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF141:CackePHP2
Материал из Linuxformat.
- Учебник CakePHP
CakePHP |
---|
|
Содержание |
CakePHP: Пишем загрузчик файлов
- Часть II Загрузчик файлов, чтобы делиться файлами с конкретными пользователями, а не со всем Интернетом, пишет Грэм Уэлдон.
Медовый месяц кончился, крошка. Займемся делом. Следующая итерация набега на CakePHP даст вам собственный загрузчик файлов. Он пригодится, когда нужно отправить файл партнеру по бизнесу или клиенту или поделиться им с другом, тем не менее сохранив контроль над доступом к выданному файлу. Например, предоставить клиенту файл ровно на неделю, причем доступ к файлу можно будет в любое время отобрать. Вдобавок у нас получится быстрое, легкое и расши-ряемое приложение.
Нам нужна возможность задать владельца для каждого файла, а также разделить этот файл с другими пользователями (доступ на чтение). Поэтому определим две различных связи между файлом и пользователем. Предоставление файла в доступ опишем связью «Имеет и принадлежит многим», или, сокращенно, ИПM. ИПM использует таблицу join для связи двух записей, и в CakePHP есть стандартное соглашение, которому мы будем следовать, чтобы каркас сделал за нас всю грязную работу (соответствующий код можно найти на LXFDVD).
В упомянутой таблице join задаются пользователи, которым разрешен доступ к заданным файлам. Согласно стандарту, имя таблицы join, используемой для связи ИПM, должно состоять из имен связываемых таблиц в алфавитном порядке, соединенных символом подчеркивания:
CREATE TABLE `uploads_users` ( `id` CHAR(36) NOT NULL PRIMARY KEY, `upload_id` CHAR(36) NOT NULL, `user_id` CHAR(36) NOT NULL );
Создайте свои таблицы в новой базе данных, и займемся выпечкой и запуском проекта.
На старт… внимание… печем!
Bake – специальная утилита, поставляемая с CakePHP и запускаемая из консоли. Чтобы работать с ней было удобнее, путь cake/console внутри CakePHP следует включить в переменную окружения PATH. Для этого (как говорилось на предыдущем уроке) выполните следующую команду в консоли:
$ export PATH=”$PATH:/path/to/cakephp/cake/console”
И, конечно, PHP должен быть доступен в командной строке (обычно это достигается установкой пакета php-cli или подобного).
Чтобы создать скелет проекта, в консоли скомандуйте:
$ cake bake project fileshare $ cd fileshare
Затем настройте соединение с базой данных. Измените параметры, приведенные в Листинге 2 в файле config/database.php, на подходящие для вашей базы данных. Достаточно задать соединение “default”.
И пока мы занимаемся выпечкой, взвалив всю черную работу на CakePHP, заодно выпечем контроллеры, модели и представления, создав основу нашего проекта.
$ cake bake all user $ cake bake all upload
Теперь у нас есть готовый к использованию сайт, который связывает пользователей с загруженными файлами и позволяет нам заполнить базу данных удобным способом. Но пока файлы загружать рановато: следует переделать то, что припас нам CakePHP, добавив функционал по загрузке файлов и предотвратив добавление и изменение информации о пользователях посторонними. Повысим также безопасность всего приложения, позаботившись, чтобы только зарегистрированные пользователи имели возможность модифицировать данные.
Запремся изнутри
В PHP у нас нет пространств имен, и классам нельзя давать имена, уже использованные в CakePHP. В CakePHP есть класс File, и мы не могли дать имя “File” нашей модели в примере – это привело бы к конфликту классов. Вместо него мы воспользовались именем “Upload”. Список всех классов CakePHP см. в открытом API: http://api.cakephp.org.
У нас есть таблица “users”, и уже работает добавление пользователя – об этом позаботился CakePHP; откройте приложение в браузере, добавив к адресу users. Например, http://localhost/fileshare/users. Итак, что же делать дальше? Ха, пароли хранятся в открытом виде; и нас как-то не просят вводить логин и пароль. Откройте файл app_controller.php в корне проекта. Это пустой контроллер, от которого наследуют функционал все остальные контроллеры приложения. Все, что мы сделаем здесь, будет доступно во всех контроллерах, так что это превосходное место, чтобы заставить пользователей вводить логин и пароль. Добавьте компоненты Auth и Session. Теперь ваш AppController будет выглядеть так:
<?php class AppController extends Controller { var $components = array(‘Auth’, ‘Session’); } ?>
Попробовав обновить список users, вы получите сообщение об ошибке, сигнализирующее о том, что действия login не существует. Если посмотрите повнимательнее, то увидите, что URL тоже изменился на /users/login. К счастью, всю самую трудную работу за нас уже сделал CakePHP, и нам осталось создать действие (функцию) в контоллере и форму для входа в систему. Откройте controllers/users_controller.php и добавьте туда действие login:
function login() { }
Ошибки тут нет – функция пуста, и это все, что нам нужно для аутентификации в контроллере.
Чтобы не перехитрить самих себя, убедимся, что мы можем зарегистрировать пользователя, если это еще не сделано. Мы почти покончили с аутентификацией пользователя, и весь функционал стал бы нам недоступен – не сделав исключение для страницы регистрации, мы не смогли бы войти в нашу клевую систему. Создайте метод beforeFilter в контроллере users и добавьте туда код, предупреждающий компонент Auth, что мы можем заходить на страницу регистрации, даже если еще не вошли в систему:
function beforeFilter() { $this->Auth->allow(‘add’); return parent::beforeFilter(); }
Создайте представление login в новом файле views/users/login.ctp, как описано в Листинге 3 LXFDVD.
Обновите страницу с сообщением об ошибке, и передвами появится форма для входа в систему. Обратите внимание, что вы сможете зайти на страницу /users/add, но все остальные ссылки будут перенаправляться на страницу /users/login. Зарегистрируйте себя как пользователя – это пригодится для тестирования приложения. Проверьте содержимое базы данных. Вы увидите, что пароль автоматически превратился в хэш. Чудесно! Также стоит удалить некоторые из автоматически сгенерированных полей и полей ввода, которыми мы не будем пользоваться, чтобы у пользователей был доступ только к разрешенным полям. Итак, удалите следующее поле из представлений add и edit в файлах views/users/add.ctp и views/users/edit.ctp:
echo $this->Form->input(‘Upload’);
Загружаем файлы
Пора создать действие для загрузки файлов. Сначала создадим подкаталог uploads в каталоге проекта, где будут храниться загруженные файлы, и сделаем его владельцем пользователя, от имени которого запускается web-сервер. В одних дистрибутивах это www-data, в других – apache или www. Укажите пользователя для своей системы.
$ mkdir uploads $ chown www-data uploads
А не понаписать ли нам код? Начнем с изменения страницы загрузки файлов, чтобы файлы можно было загружать и безопасно сохранять. Bake стряпает неплохую форму, но мы удалим из нее несколько автоматически сгенерированных полей и заменим их полем загрузки файла. Столбцы, которые мы определили в базе данных, предварительно заполняются автоматически. После загрузки файла мы получим всю необходимую информацию о нем. Удалите поля ввода filename, filesize и filemime из представления загрузки файлов в файле views/uploads/add.ctp и добавьте поле ввода file. Мы не создали столбец file в базе данных, поэтому CakePHP не сделает черновую работу для этого поля, и нужно также указать, какого типа должно быть поле, чтобы оно создалось правильно. Сделаем и еще одно небольшое изменение – удалим поле ввода user_id, созданное для нас CakePHP. Вместо него добавим в контроллер код, автоматически связывающий файлы с пользователем, который в данный момент вошел в систему. Посмотрим, как теперь выглядят поля ввода на форме.
<?php echo $this->Form->create(‘Upload’, array(‘type’ => ‘file’));?> <fieldset> <legend><?php __(‘Add Upload’); ?></legend> <?php echo $this->Form->input(‘title’); echo $this->Form->input(‘description’); echo $this->Form->input(‘file’, array(‘type’ => ‘file’)); echo $this->Form->input(‘User’); ?> </fieldset> <?php echo $this->Form->end(__(‘Submit’, true));?>
Обработка загруженного файла
Конечно, файл у нас есть, но нужно еще и обработать его должным образом, чтобы выдать сообщение о ошибке, если файл загружен некорректно, либо сохранить его в нашем каталоге uploads, если он загружен корректно. Для этого откроем файл controllers/uploads_controller.php в созданном проекте и изменим функцию add(): пусть обрабатывает файл, если он есть. В третьей строке этой функции происходит сохранение данных модели. Измените оператор условия так, чтобы в нем вызывалась функция uploadFile(), которую мы напишем чуть позже:
if ($this->uploadFile() && $this->Upload->save($this->data)) {
Просто, не правда ли? Теперь создадим в том же контроллере функцию uploadFile():
function uploadFile() { $file = $this->data[‘Upload’][‘file’]; if ($file[‘error’] === UPLOAD_ERR_OK) { $id = String::uuid(); if (move_uploaded_file($file[‘tmp_name’], APP.’uploads’. DS.$id)) { $this->data[‘Upload’][‘id’] = $id; $this->data[‘Upload’][‘user_id’] = $this->Auth->user(‘id’); $this->data[‘Upload’][‘filename’] = $file[‘name’]; $this->data[‘Upload’][‘filesize’] = $file[‘size’]; $this->data[‘Upload’][‘filemime’] = $file[‘type’]; return true; } } return false; }
Опять же, все просто. Мы загружаем файл, и если мы можем переместить его в нужный каталог, возвращаем true. В процессе этого мы вручную генерируем идентификатор, который станет безопасным именем сохраняемого файла. Это исключит неприятности от пользователей, загружающих файлы со странными и потенциально опасными для системы именами, которые в противном случае угодят прямо в вашу файловую систему. Ручная генерация UUID с помощью метода String::uuid() устраняет эту дыру в безопасности и гарантирует безопасную загрузку файла, а исходное имя файла хранится в базе данных и отправляется пользователю при скачивании.
Займемся же скачиванием. Но прежде чем отвлекаться на это, попробуйте добавить пару файлов. Вы увидите, что они успешно попадают в базу данных; а в созданном нами каталог uploads появляются файлы с соответствующими именам идентификаторами. Если на данном этапе у вас возникнут проблемы, убедитесь, что у web-сервера есть права на запись в каталог uploads.
Еще одна крутая вещь, которую мы здесь сделаем – свяжем пользователя с $this->Auth->user(‘id’): это идентификатор пользователя, вошедшего в данный момент в систему. Так как ранее мы позаботились о безопасности, то знаем, что пользователь должен зарегистрироваться, чтобы открыть эту страницу, поэтому его идентификатор не может быть пустым и всегда корректен.
Удаляем связи
Вы заметите, что мы продублировали связи для моделей User и Upload. Возьмем, например, модель User в файле models/user.php; CakePHP создал связи hasMany и hasAndBelongsToMany, обе с индексом Upload. Работать это не будет: конфликт имен приведет к тому, что в представлениях отобразятся неверные данные. Измените связь hasAndBelongsToMany в модели User на SharedUpload. Аналогично, в модели Upload в файле models/upload.php измените связь ИПМ на SharedUser:
// User Model var $hasAndBelongsToMany = array( ‘SharedUpload’ => array( ‘className’ => ‘Upload’, ... // Upload Model var $hasAndBelongsToMany = array( ‘SharedUser’ => array( ‘className’ => ‘User’, ..
Чтобы эти изменения связей корректно обработались в представлениях, измените индекс, на который ссылаются представления в разделе related в нижней части индексов view. В файле views/users/view.ctp измените две строки
<?php if (!empty($user[‘pload’])):?> foreach ($user[‘Upload’] as $upload):
на следующие:
<?php if (!empty($user[‘SharedUpload’])):?> foreach ($user[‘SharedUpload’] as $upload):
Итак, вы можете регистрировать новых пользователей, заходить в систему, загружать файлы и связывать их с пользователями. С минимумом написанного кода и затраченных усилий нам удалось достичь неплохой функциональности. Хорошенько протестируйте систему, прежде чем переходить к следующему этапу.
Просмотр и скачивание файлов
Если вы уже поиграли с навигацией по имеющимся представлениям, вам попадалась страница представления для одного из загруженных файлов. На ней показаны все метаданные файла, но сам файл скачать пока нельзя. Давайте изменим кое-что так, чтобы появилась возможность скачивания файлов, и у нас появился доступ к ним. Первым делом добавьте ссылку в файл представления views/uploads/view.ctp, в любом месте (я решил добавить ее в самый низ):
<dt<?php if ($i % 2 == 0) echo $class;?>><?php __(‘Download’);?></dt> <dd<?php if ($i++ % 2 == 0) echo $class;?>> <?php echo $this->Html->link(__(‘Download’, true), array(‘action’ => ‘download’, $upload[‘Upload’][‘id’])); ?> </dd>
С этой ссылкой пора снова приняться за дело и создать действие для контроллера: пускай оно обрабатывает скачивание файла. Откройте UploadsController в файле controllers/uploads_controller.php снова и добавьте функцию скачивания. Она инициирует скачивание и возвращает пользователю файл с исходным именем, под которым он был загружен. Код этой функции слишком велик, чтобы помещать его здесь, и вы найдете его на нашем DVD в Листинге 4.
Теперь щелкните по этой ссылке, и файл начнет загружаться через ваш браузер! В этой функции нужно кое-что доделать, но поверьте, CakePHP уже сделал много больше. Сначала проверим, есть ли идентификатор. Затем попытаемся найти в базе данных запись о загрузке файла с таким идентификатором, и пока мы там, проверим, что загрузка связана с пользователем, который в данный момент в системе, или что файл был изначально загружен данным пользователем. И в том, и в другом случае доступ разрешен. Если запросы не дают результата, значит пользователь пытается получить файл, к которому у него нет доступа, и мы перенаправим его к списку файлов.
Покажем лишь то, что можно
Чтобы список файлов имел смысл для всех пользователей, нужно также изменить действие index() в контроллере Uploads, чтобы выполнять аналогичную фильтрацию и показывать в списке только те файлы, к которым у пользователей есть права доступа, иначе в нем будет масса файлов, о которых им знать излишне! Измените действие index таким образом:
function index() { $this->Upload->bindModel(array(‘hasOne’ => array(‘UploadsUser’)), false); $this->paginate = array( ‘conditions’ => array( ‘OR’ => array( ‘UploadsUser.user_id’ => $this->Auth->user(‘id’), ‘Upload.user_id’ => $this->Auth->user(‘id’), ), ) ); $this->set(‘uploads’, $this->paginate()); }
На этот раз нам пришлось добавить параметр false к вызову bindModel(), чтобы гарантировать корректное разбиение на страницы. Функция разбиения на страницы принимает два отдельных результата из базы данных. Первый определяет число элементов в таблице, соответствущих запросу, а второй фактически возвращает данные. Параметр false велит CakePHP удержать связывание в пределах одного запроса. Простое правило: если вы пользуетесь методом bindModel и разбивкой на страницы, в конец метода добавьте false.
Действие view() от подобной фильтрации тоже выигрывает:
function view($id = null) { if (!$id) { $this->Session->setFlash(__(‘Invalid upload’, true)); $this->redirect(array(‘action’ => ‘index’)); } $this->Upload->bindModel(array(‘hasOne’ => array(‘UploadsUser’))); $upload = $this->Upload->find(‘first’, array( ‘conditions’ => array( ‘Upload.id’ => $id, ‘OR’ => array( ‘UploadsUser.user_id’ => $this->Auth->user(‘id’), ‘Upload.user_id’ => $this->Auth->user(‘id’), ), ) )); if (!$upload) { $this->Session->setFlash(__(‘Invalid upload’, true)); $this->redirect(array(‘action’ => ‘index’)); } $this->set(‘upload’, $upload); }
Закругляемся
Последнее украшение, необходимое для приведения сайта в боевую готовность – функция logout. И я великодушно дам вам код, который надо поместить в UsersController, чтобы можно было выходить из системы:
public function logout() { $this->redirect($this->Auth->logout()); }
Представления не нужно, и перенаправление производится, как только пользователь открывает URL /users/logout после уничтожения сессии и выхода пользователя из системы.
Итак, мы создали защищенное многопользовательское приложение для загрузки и разделения файлов за каких-то 20 минут. Теперь можно добавить дополнительный функционал – например, миниатюры для просмотра загруженного пользователями содержимого, или изменить механизм разделения файлов, чтобы выбирать пользователей, не показывая всем их полный список.
Также можно посылать пользователям извещения по электронной почте, что вы разделили с ними файлы. Надеюсь, вам понравился этот урок, и созданный код пригодится вам как учебный пример или даже как готовое решение для разделения файлов с клиентами.
Исходные коды урока
Коды для нашего тройного урока доступны на GitHub под моей учетной записью http://github.com/predominant/cakephp_linux_format. Можете взять код оттуда, если не получается сгенерировать его утилитой bake или вы просто хотите собрать и запустить приложение побыстрее – код можно скопировать, опустив все этапы урока.