Создаём собственный простой to-do list. Часть 3

Создаём собственный простой to-do list. Часть 3

Итак, следующая часть серии уроков — Создаём собственный простой to-do list.

В прошлом уроке Создаём собственный простой to-do list. Часть 2 мы создали несколько основных страниц PHP и добавили простую аутентификацию. Сегодня мы будем это дополнять, добавив поддержку базы данных. Это позволит нам дополнить аутентификацию и начать сохранять задачи.

Также на прошлой недели мы устанавливали XAMPP, так что у вас уже должен быть установлен и котов к работе MySQL. Если нет, то зайдите на предыдущий урок. Если вы решили использовать IIS, вы можете скачать MySQL используя Microsoft Web Platform Installer или загрузить инструмент MySQL Workbench tool

Создание схемы

Теперь когда MySQL и PHP установлены, можем приступать у созданию схемы. До сих пор нам известно что понадобиться как минимум две таблицы. Таблицы пользователя для входа в систему и задачи. Мы также знаем что у пользователя есть конкретные задачи, что говорит нам о том, что надо использовать Внешний ключ для определения связей.
Для начала надо создать базу данных и таблицы. Следующий код создаст базу данных «TODO» а в ней 2 таблицы «users» и «tasks».

CREATE DATABASE todo;

USE todo;

CREATE TABLE IF NOT EXISTS `users` (
  `user_id` bigint(20) unsigned NOT NULL auto_increment,
  `user_login` varchar(100) NOT NULL,
  `user_password` varchar(64) NOT NULL,
  `user_firstname` varchar(50) NOT NULL,
  `user_surname` varchar(50) NOT NULL,
  `user_email` varchar(100) NOT NULL,
  `user_registered` datetime NOT NULL default '0000-00-00 00:00:00',  
  PRIMARY KEY (`user_id`),
  KEY `idx_user_login_key` (`user_login`)
) DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;

CREATE TABLE IF NOT EXISTS `tasks` (
  `task_id` bigint(20) unsigned NOT NULL auto_increment,
  `user_id` bigint(20) NOT NULL REFERENCES `users`(`user_id`),
  `task_name` varchar(60) NOT NULL,
  `task_priority` tinyint(2) NOT NULL default '2',
  `task_color` varchar(7) NOT NULL default '#ffffff',
  `task_description` varchar(150) NULL,
  `task_attendees` varchar(4000) NULL,
  `task_date` datetime NOT NULL default '0000-00-00 00:00:00',  
  PRIMARY KEY (`task_id`),
  KEY `idx_task_name_key` (`task_name`)
) DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;

Создаём собственный простой to-do list, phpmyadmin

Для создания нашего первого пользователя нам надо вставить строку в таблицу «users». Следующий SQL определит пользователя «developerdrive». И вызывается функцией password, это встроенная функция которая использует хэш-функцию строки sha1. Рекомендуется также использовать соль перед хэшированием. Идея соли в том, чтобы добавить перед или после пароля , случайно генерируемые данные (например «password + gfdgfhghfgdgsdfasa» это повышает безопасность пароля и делает его более устойчивым к подбору.

Вы можете смело заменить данные на свои, только не забудьте их!

INSERT INTO `users` ( `user_login`, `user_password`, `user_firstname`, 
	`user_surname`, `user_email`, `user_registered` )
SELECT 'developerdrive', PASSWORD('to-do-password'), 'developer',
	'drive', 'developer@email.com', NOW();

Идентификация

На данном этапе создания собственного простого to-do list приложения, у нас есть таблица пользователей и наш собственный пользователь, теперь нам необходимо обновить login.php для подключения к базе и проверить предоставленный логин. Для этого мы будем использовать библиотеку MySQLi. Для создания соединения с MySQLi, нам надо предоставить информацию от MySQL. По умолчанию XAMPP использует логин «root» и пустой пароль. Поскольку мы используем локальную базу данных нам надо указать «localhost» в качестве сервера и «todo в качестве названия базы».

$connection = new mysqli("localhost", "root", "", "todo");

if (mysqli_connect_errno()) {
    die(sprintf("Connect failed: %s\n", mysqli_connect_error()));
}

Мы будем использовать это соединение во всём нашем to-do list приложении, для этого создадим новый файл с именем database.php и вставим следующий код в него:

global $connection;

if ( isset( $connection ) )
    return;

$connection = new mysqli("localhost", "root", "", "todo");

if (mysqli_connect_errno()) {        
    die(sprintf("Connect failed: %s\n", mysqli_connect_error()));
}

Теперь мы можем обновлять данные из login.php для запросов к database.php и создать свой первый запрос.

Мы будем использовать параметризованный SQL, что поможет нам защититься от SQL инъекции

Теперь не большая проверка.

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    
    require_once('database.php');
    
    if(!empty($_POST["username"]) && !empty($_POST["password"])) {
        $username = $_POST["username"];
        $password = $_POST["password"];
    
        $query = $connection->prepare("SELECT `user_id` FROM `users` WHERE `user_login` = ? and `user_password` = PASSWORD(?)");
        $query->bind_param("ss", $username, $password);
        $query->execute();
        $query->bind_result($userid);
        $query->fetch();
        $query->close();
        
        if(!empty($userid)) {
            session_start();
            $_SESSION["authenticated"] = 'true';
            header('Location: index.php');
        }
        else {
            header('Location: login.php');
        }
        
    } else {
        header('Location: login.php');
    }
}

Функция которая устанавливает параметры в запросе bind_param . Первый параметр «ss» устанавливает количество параметров и их типы данных

  • i для целых чисел, например 9999
  • d для десятичного числа, например 10,69
  • s для строки, например «testing»
  • b для бинарных и blob значений

Функция bind_result связывает переменную в столбце результата с соответствующим порядковым номером.В приведенном примере я выбираю только «user_id», если бы я выбирал col1, col2,col3 я бы использовал bind_result($val1, $val2, $val3) чтобы получить их значения.

Хотя на данном этапе по разработке собственного простого to-do list мы добились более высокого уровня безопасности, мы по прежнему можем пострадать от того что флаг идентификации хранится в сессии. Было бы намного разумнее хранить сессии в БД всё время до истечения срока, а также IP адрес пользователя. Для того что бы это сделать нам надо создать ещё одну таблицу «sessions» и вставить строку которая может быть проверенна файлом authenticate.php.

CREATE TABLE IF NOT EXISTS `sessions` (
  `session_id` bigint(20) unsigned NOT NULL auto_increment,
  `user_id` bigint(20) NOT NULL REFERENCES `users`(`user_id`),
  `session_key` varchar(60) NOT NULL,
  `session_address` varchar(100) NOT NULL,
  `session_useragent` varchar(200) NOT NULL,
  `session_expires` datetime NOT NULL default '0000-00-00 00:00:00',  
  PRIMARY KEY (`session_id`),
  KEY `idx_session_key` (`session_key`)
) DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;

Вот полное содержание файла login.php

$username = null;
$password = null;

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    
    require_once('database.php');
    
    if(!empty($_POST["username"]) && !empty($_POST["password"])) {
        $username = $_POST["username"];
        $password = $_POST["password"];
    
        $query = $connection->prepare("SELECT `user_id` FROM `users` WHERE `user_login` = ? and `user_password` = PASSWORD(?)");
        $query->bind_param("ss", $username, $password);
        $query->execute();
        $query->bind_result($userid);
        $query->fetch();
        $query->close();
        
        if(!empty($userid)) {
            session_start();
            $session_key = session_id();
            
            $query = $connection->prepare("INSERT INTO `sessions` ( `user_id`, `session_key`, `session_address`, `session_useragent`, `session_expires`) VALUES ( ?, ?, ?, ?, DATE_ADD(NOW(),INTERVAL 1 HOUR) );");
            $query->bind_param("isss", $userid, $session_key, $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] );
            $query->execute();
            $query->close();
            
            header('Location: index.php');
        }
        else {
            header('Location: login.php');
        }
        
    } else {
        header('Location: login.php');
    }
} else {
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title>Creating a simple to-do application - Part 1</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="page">
    <!-- [banner] -->
    <header id="banner">
        <hgroup>
            <h1>Login</h1>
        </hgroup>        
    </header>
    <!-- [content] -->
    <section id="content">
        <form id="login" method="post">
            <label for="username">Username:</label>
            <input id="username" name="username" type="text" required>
            <label for="password">Password:</label>
            <input id="password" name="password" type="password" required>                    
            <br />
            <input type="submit" value="Login">
        </form>
    </section>
    <!-- [/content] -->
    
    <footer id="footer">
        <details>
            <summary>Copyright 2013</summary>
        </details>
    </footer>
</div>
<!-- [/page] -->
</body>
</html>
<?php } ?>

Создаём собственный просто to-do lisd, сессии
Теперь мы можем обновлять authenticate.php для проверки правильного сессии.Мы делаем это запрашивая «session_key», «session_address» и «session_agent». Таким образом, любые изменения в сессии будут обнаружены и пользователю будет предложено повторно идентифицироваться. Мы также подтверждаем что сессия еще не истекла.

session_start();
$session_key = session_id();

require_once('database.php');

$query = $connection->prepare("SELECT `session_id`, `user_id` FROM `sessions` WHERE `session_key` = ? AND `session_address` = ? AND `session_useragent` = ? AND `session_expires` > NOW();");
$query->bind_param("sss", $session_key, $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT']);
$query->execute();
$query->bind_result($session_id, $user_id);
$query->fetch();
$query->close();

if(empty($session_id)) {
    header('Location: login.php');
}

Это вероятно хорошая идея, обновлять время истечения сессии каждый раз, когда пользователь что-то делает, таким образом, мы не заставляем их авторизироватся каждый час. Если мы добавим следующий PHP код в конце authenticate.php время истечения сессии будет продлено ещё на час.

$query = $connection->prepare("UPDATE `sessions` SET `session_expires` = DATE_ADD(NOW(),INTERVAL 1 HOUR) WHERE `session_id` = ?;");
$query->bind_param("i", $session_id );
$query->execute();
$query->close();

Важно также отметить, что некоторые браузеры делиться session_id между вкладками. Это означает, что поскольку мы сохраняем сессии в базе данных, если пользователь открывает новую вкладку, она будет автоматически авторизирована. Если вы хотите этого избежать, вы можете использовать функцию session_regenerate_id для генерирования нового session_id

Перечень и сохранение заданий

Теперь, когда у нас есть аутентификация, мы можем сохранить наше первое задание и просмотреть список существующих. Сначала мы будем обновлять index.php, чтобы иметь возможность перечислить любые существующие задачи. Мы должны быть уверены, что только задачи для вошедшего в систему пользователя будут показаны. Для этого мы будем использовать переменную $user_id которую мы установили в authentication.php. Следующий код из таблицы между тегами TBODY в index.php

global $user_id;
$query = $connection->prepare("SELECT `task_id`, `task_name`, `task_priority`, `task_color`, `task_description`, `task_attendees`, `task_date` FROM `tasks` WHERE `user_id` = ?");
$query->bind_param("i", $user_id);
$query->execute();

$query->bind_result($id, $name, $priority, $color, $description, $attendees, $date);
while ($query->fetch()) {
    echo '' . $date . '' . $priority . '' . $name . '' . $description . '' . $attendees . '';
}

$query->close();

Создаём собственный просто to-do lisd, сессии, задачи

Затем пробуем сохранить нашу первую задачу. Для этого нам необходимо обновить submit.php. Здесь мы будем добавлять инструкцию insert, как только мы подтвердили представленные данные.

require_once('authenticate.php');

$name = null;
$date = date('c');
$desc = '';
$email = '';
$priority = 2;
$color = '#ffffff';

$result = array();
$result['error'] = array();

if (!empty($_POST["new-task-name"]))
    $name = $_POST["new-task-name"];
else 
    array_push($result['error'], 'Please specify a name for your task');

if (!empty($_POST["new-task-date"]))
    $date = new DateTime($_POST["new-task-date"]);
else 
    array_push($result['error'], 'Please specify a date for your task');

if (!empty($_POST["new-task-desc"]))
    $desc = $_POST["new-task-desc"];

if (!empty($_POST["new-task-email"]))
    $email = explode(',', $_POST["new-task-email"]);

if (!empty($_POST["new-task-priority"]))
    $priority = intval($_POST["new-task-priority"]);
else 
    array_push($result['error'], 'Please specify a valid priority for your task');

if (!empty($_POST["new-task-color"]))
    $color = $_POST["new-task-color"];

if(isset($result['error']) && count($result['error']) > 0){
    $result['success'] = false;
} else {
    if(!empty($email)){
        $email = implode(',', $email);
    }
    $date = $date->format('c');
    
    $query = $connection->prepare("INSERT INTO `tasks` ( `user_id`, `task_name`, `task_priority`, `task_color`, `task_description`, `task_attendees`, `task_date` ) VALUES ( ?, ?, ?, ?, ?, ?, ? );");
    $query->bind_param("isissss", $user_id, $name, $priority, $color, $desc, $email, $date );
    $query->execute();
    $result['id'] = $query->insert_id;
    $query->close();

    $result['success'] = true;
    $result['name'] = $name;
    $result['date'] = $date;
    $result['desc'] = $desc;
    $result['email'] = $email;
    $result['priority'] = $priority;
    $result['color'] = $color;
}

echo json_encode($result);

Здесь вы можете видеть, что я возвращаю query->insert_id в массив $result . Это и есть «task_id ‘ созданный при добавлении новой задачи.

Создаём собственный просто to-do lisd, сессии, список задач

Вот «создаём собственный простой to-do list. Часть 3″ подошла к концу.
Теперь мы можем войти и создать или просмотреть существующие задачи.
Далее мы будем оптимизировать код для использования функций и добавления улучшенного обработчика ошибок. Мы также будем работать с cron, чтобы привести в порядок истекшие сессии и возможно добавим некую статистику, чтобы видеть какие функции или страницы нашего собственного простого to-do list приложения пользователи используют и просматривают.

Следите за продолжением уроков по созданию собственной to-do list. Подписывайтесь на обновление по RSS, по почте или twitter.

comments powered by Disqus
Subscribe to RSS Feed Следить в Twitter