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

Основное преимущество этой записной книжки — это хранение данных в зашифрованном виде, а ключ знаете только вы. Поэтому, если злоумышленник украдёт вашу базу данных, это максимум что он увидит:



Личный дневник использует два внешних php класса:

  • XTEA — для шифрования
  • SafeMySQL — для работы с базой данных

Имеет очень простую структуру базы данных:

CREATE TABLE `notest` ( `id` int(255) NOT NULL, `text` text NOT NULL, `date_d` text NOT NULL, `date_m` text NOT NULL, `date_t` text NOT NULL, `date_cat` text NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

Ключ, который использует система хранится в переменной $_SESSION['key']. Проверяем её состояние, спрашиваем у пользователя ключ, если этой переменной нет:

// Если не существует переменная ключа
  if (!isset($_SESSION['key'])) {
    $keyw = '<form action="" method="post"><input type="text" name="key" placeholder="Введите ваш ключ"><input type="submit" value="Сохранить"></form>';
  }
  // Иначе, если существует
  else {
    $keyw = '<button><a class="nodec_link" href="index.php?action=delkey">Удалить ключ</a></button>';
  }
  // Если отправлена форма сохранения ключа
  if (isset($_POST["key"])) {
    $_SESSION['key'] = $_POST["key"];
    header("Location: index.php");
    exit();
  }

Когда пользователь закончил работу, делаем unset:

if ((isset($_GET['action'])) && ($_GET['action'] == 'delkey')) {
    unset($_SESSION['key']);
    header("Location: index.php");
    exit();
  }

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

$db = new SafeMySQL($set_bd);
$option = $db->getAll("SELECT DISTINCT `date_cat` FROM `notes`");
foreach ($option as $value) {
$desc_opt = explode("-", $value['date_cat']);
 echo '<option value="index.php?date='.$value['date_cat'].'">'.num2name($desc_opt[0]).' '.$desc_opt[1].'</option>';
}

Функция num2name() переводит номер месяца в его название
function num2name($num) {
    switch ($num) {
      case '01':
        $name = "январь";
        break;
      case '02':
        $name = "февраль";
        break;
      case '03':
        $name = "март";
        break;
      case '04':
        $name = "апрель";
        break;
      case '05':
        $name = "май";
        break;
      case '06':
        $name = "июнь";
        break;
      case '07':
        $name = "июль";
        break;
      case '08':
        $name = "август";
        break;
      case '09':
        $name = "сентябрь";
        break;
      case '10':
        $name = "октябрь";
        break;
      case '11':
        $name = "ноябрь";
        break;
      case '12':
        $name = "декабрь";
        break;
      
      default:
        $name = "";
        break;
    }
    return $name;
  }


В зависимости от наличия запроса на вывод записей определённого месяца, выводим записи дневника:

// Если отправлен запрос на конкретный месяц
              if (isset($_GET["date"])) {
                $cur_date = $_GET["date"];
                $db = new SafeMySQL($set_bd);
                $data = $db->getAll("SELECT * FROM `notes` WHERE `date_cat` = ?s ORDER BY `id` DESC", $cur_date);
                foreach ($data as $key) {
                  // Декодируем текст
                  $xtea = new XTEA($_SESSION['key']);
                  $text_decode = $xtea->Decrypt($key["text"]);
                  // Выводим записи
                  echo '<tr>
                    <td class="date">
                      <div class="date_m">'.num2name($key["date_m"]).'</div>
                      <div class="date_d">'.$key["date_d"].'</div>
                      <div class="date_t">'.$key["date_t"].'</div>
                    </td>
                    <td class="note_text" valign="top">'.$text_decode.'</td>
                  </tr>';
                }
              }
              // Если нет запроса на определённый месяц, выводим последние $set_col записей
              else {
                $db = new SafeMySQL($set_bd);
                $data = $db->getAll("SELECT * FROM `notes` ORDER BY `id` DESC LIMIT ?i", $set_col);
                foreach ($data as $key) {
                  // Декодируем текст
                  $xtea = new XTEA($_SESSION['key']);
                  $text_decode = $xtea->Decrypt($key["text"]);
                  // Выводим записи
                  echo '<tr>
                    <td class="date">
                      <div class="date_m">'.num2name($key["date_m"]).'</div>
                      <div class="date_d">'.$key["date_d"].'</div>
                      <div class="date_t">'.$key["date_t"].'</div>
                    </td>
                    <td class="note_text" valign="top">'.$text_decode.'</td>
                  </tr>';
                }
              } 



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

<form method="post" action="">
<textarea rows="10" name="text" placeholder="Текст новой записи"></textarea>
<input type="submit" value="Добавить">
</form>

Которая обрабатывается таким кодом:

if (isset($_POST["text"])) {
// Получение данных в переменные
$text = nl2br($_POST["text"]);
// Шифрование текста
$xtea = new XTEA($_SESSION['key']);
$text = $xtea->Encrypt($text);

$date_d = date("d");
$date_m = date("m");
$date_t = date("Y");
$date_cat = date("n-Y");
// Отправка в базу данных
$db = new SafeMySQL($set_bd);
$sql  = "INSERT INTO `notes` (`text`, `date_d`, `date_m`, `date_t`, `date_cat`) VALUES (?s, ?s, ?s, ?s, ?s)";
$db->query($sql, $text, $date_d, $date_m, $date_t, $date_cat);
}

Для использования на открытом сервере необходимо закрывать доступ к каталогу со скриптом, так как он написан для домашнего закрытого сервера и у него нет встроенной авторизации.

Полный код основного файла
<?php
  // Отображение ошибок на странице
  ini_set('error_reporting', E_ALL);
  ini_set('display_errors', 1);
  ini_set('display_startup_errors', 1);

  include "config.php";
  include "db.php";
  include 'xtea.php';

  header('Content-Type: text/html; charset=utf-8');
  session_start();

  // Если не существует переменная ключа
  if (!isset($_SESSION['key'])) {
    $keyw = '<form action="" method="post"><input type="text" name="key" placeholder="Введите ваш ключ"><input type="submit" value="Сохранить"></form>';
  }
  // Иначе, если существует
  else {
    $keyw = '<button><a class="nodec_link" href="index.php?action=delkey">Удалить ключ</a></button>';
  }
  // Если отправлена форма сохранения ключа
  if (isset($_POST["key"])) {
    $_SESSION['key'] = $_POST["key"];
    header("Location: index.php");
    exit();
  }
  if ((isset($_GET['action'])) && ($_GET['action'] == 'delkey')) {
    unset($_SESSION['key']);
    header("Location: index.php");
    exit();
  }
  // если отправлена форма добавления записи
  if (isset($_POST["text"])) {
    $text = nl2br($_POST["text"]);

    $xtea = new XTEA($_SESSION['key']);
    $text = $xtea->Encrypt($text);

    $date_d = date("d");
    $date_m = date("m");
    $date_t = date("Y");
    $date_cat = date("n-Y");

    $db = new SafeMySQL($set_bd);
    $sql  = "INSERT INTO `notes` (`text`, `date_d`, `date_m`, `date_t`, `date_cat`) VALUES (?s, ?s, ?s, ?s, ?s)";
    $db->query($sql, $text, $date_d, $date_m, $date_t, $date_cat);
  }

  // Функия перевода номера месяца в его название
  function num2name($num) {
    switch ($num) {
      case '01':
        $name = "январь";
        break;
      case '02':
        $name = "февраль";
        break;
      case '03':
        $name = "март";
        break;
      case '04':
        $name = "апрель";
        break;
      case '05':
        $name = "май";
        break;
      case '06':
        $name = "июнь";
        break;
      case '07':
        $name = "июль";
        break;
      case '08':
        $name = "август";
        break;
      case '09':
        $name = "сентябрь";
        break;
      case '10':
        $name = "октябрь";
        break;
      case '11':
        $name = "ноябрь";
        break;
      case '12':
        $name = "декабрь";
        break;
      
      default:
        $name = "";
        break;
    }
    return $name;
  }
?>
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href='https://fonts.googleapis.com/css?family=Open+Sans&subset=latin,cyrillic' rel='stylesheet' type='text/css'>
    <title><?php echo $set_title; ?></title>
    <link href="style.css" rel="stylesheet">
</head>
<body>
    <div class="main">
      <table>
        <tr>
            <select onchange="location.href=this.options[this.selectedIndex].value;">
              <option value="index.php">Выбор месяца и года</option>
              <option value="index.php">Последине <?php echo $set_col; ?> записей</option>
              <?php
                $db = new SafeMySQL($set_bd);
                $option = $db->getAll("SELECT DISTINCT `date_cat` FROM `notes`");
                foreach ($option as $value) {
                  $desc_opt = explode("-", $value['date_cat']);
                  echo '<option value="index.php?date='.$value['date_cat'].'">'.num2name($desc_opt[0]).' '.$desc_opt[1].'</option>';
                }
              ?>
            </select>
        </tr>
        <tr>
          <?php echo $keyw; ?>
        </tr>
      </table>
      <h1><?php echo $set_title; ?></h1>
      <div class="addform">
        <form method="post" action="">
          <textarea rows="10" name="text" placeholder="Текст новой записи"></textarea>
          <input type="submit" value="Добавить">
        </form>
      </div>
      <div class="notes">
        <table class="table">
          <tbody>
            <?php 
              // Если отправлен запрос на конкретный месяц
              if (isset($_GET["date"])) {
                $cur_date = $_GET["date"];
                $db = new SafeMySQL($set_bd);
                $data = $db->getAll("SELECT * FROM `notes` WHERE `date_cat` = ?s ORDER BY `id` DESC", $cur_date);
                foreach ($data as $key) {
                  // Декодируем текст
                  $xtea = new XTEA($_SESSION['key']);
                  $text_decode = $xtea->Decrypt($key["text"]);
                  // Выводим записи
                  echo '<tr>
                    <td class="date">
                      <div class="date_m">'.num2name($key["date_m"]).'</div>
                      <div class="date_d">'.$key["date_d"].'</div>
                      <div class="date_t">'.$key["date_t"].'</div>
                    </td>
                    <td class="note_text" valign="top">'.$text_decode.'</td>
                  </tr>';
                }
              }
              // Если нет запроса на определённый месяц, выводим последние $set_col записей
              else {
                $db = new SafeMySQL($set_bd);
                $data = $db->getAll("SELECT * FROM `notes` ORDER BY `id` DESC LIMIT ?i", $set_col);
                foreach ($data as $key) {
                  // Декодируем текст
                  $xtea = new XTEA($_SESSION['key']);
                  $text_decode = $xtea->Decrypt($key["text"]);
                  // Выводим записи
                  echo '<tr>
                    <td class="date">
                      <div class="date_m">'.num2name($key["date_m"]).'</div>
                      <div class="date_d">'.$key["date_d"].'</div>
                      <div class="date_t">'.$key["date_t"].'</div>
                    </td>
                    <td class="note_text" valign="top">'.$text_decode.'</td>
                  </tr>';
                }
              } 
            ?>
          </tbody>
        </table>
      </div>

    </div>   
</body>
</html>


Ссылки:

Исходный код в репозитории
SafeMySQL
Алгоритм XTEA

Комментарии (9)


  1. andrewnester
    21.12.2015 12:23
    +4

    дорогой автор, Вам будет полезно ознакомиться www.phptherightway.com


  1. SerafimArts
    21.12.2015 12:25
    +1

    Очень слабый код, уровня продвинутого начинающего. Почему:
    1) Смешение логики и представления
    2) Процедурный код (т.е. недоступен автолоад как минимум)
    3) Шифрование уже встроено в php (openssl, mcrypt), эти решения быстрее и надёжнее
    4) Посмотрите самый последний пример, он даже не отформатирован
    5) Тема довольно проста и очевидна, как следствие — в интернете тысяча и один аналогичный пример
    6) Многие вещи можно в разы упростить (например использовать готовый объект даты времени http://php.net/manual/ru/class.datetime.php вместо date)

    Отсюда и минусы. Рекомендую скрыть статью и доработать, учитывая приведённую ссылку комментарием выше, как минимум.


  1. antness
    21.12.2015 12:41
    +1

    Добавлю:

    1. В view не предусмотрено экранирование, как следствие, имеем XSS
    2. Само шифрование/дешифрование в данной задаче логичнее реализовать на стороне клиента, чтобы избежать перехвата как ключа шифрования, так и самого защищаемого текста


  1. alterpub
    21.12.2015 12:41
    +1

    а я так и не понял зачем дату хранить в 3 разных полях?


    1. zapimir
      21.12.2015 12:50
      +2

      Еще более феерично, что эта дата хранится в полях типа TEXT, int(255) тоже интересный тип :)


  1. ZapevalovAnton
    21.12.2015 12:54

    Мы все конечно понимаем, что это статья в целях обучения и т.д. и т.п. Но конфигурационные файлы не нужно хранить в системе контроля версий. Тем более что там сейчас лежат реальные данные. Для этого обычно создают файлы вида config.php.example, и там описывают каркас конфига без реальных данных.


  1. d7s2di
    21.12.2015 13:42
    +1

    Вместо этих костылей, проще и логичнее использовать шифрованный gpg текстовичок.


    1. Delphinum
      21.12.2015 14:43
      +1

      Вот я и нашел куда внести свою лепту:

      note.sh
      #!/bin/bash
      vim -x "`date +%d-%m-%y_$1.txt`"
      


      1. d7s2di
        21.12.2015 15:22

        У меня для этих целей набор инструкций в ~/.vim/ftdetect/gpg.vim:

        gpg.vim
        augroup encrypted
        au!
        autocmd BufReadPre,FileReadPre      *.gpg set viminfo=
        autocmd BufReadPre,FileReadPre      *.gpg set noswapfile
        autocmd BufReadPre,FileReadPre      *.gpg set bin
        autocmd BufReadPre,FileReadPre      *.gpg let ch_save = &ch|set ch=2
        autocmd BufReadPre,FileReadPre      *.gpg let shsave=&sh
        autocmd BufReadPre,FileReadPre      *.gpg let &sh='sh'
        autocmd BufReadPre,FileReadPre      *.gpg let ch_save = &ch|set ch=2
        autocmd BufReadPost,FileReadPost    *.gpg '[,']!gpg --decrypt --default-recipient-self 2> /dev/null
        autocmd BufReadPost,FileReadPost    *.gpg let &sh=shsave
        autocmd BufReadPost,FileReadPost    *.gpg set nobin
        autocmd BufReadPost,FileReadPost    *.gpg let &ch = ch_save|unlet ch_save
        autocmd BufReadPost,FileReadPost    *.gpg execute ":doautocmd BufReadPost " . expand("%:r")
        autocmd BufWritePre,FileWritePre    *.gpg set bin
        autocmd BufWritePre,FileWritePre    *.gpg let shsave=&sh
        autocmd BufWritePre,FileWritePre    *.gpg let &sh='sh'
        autocmd BufWritePre,FileWritePre    *.gpg '[,']!gpg --encrypt --default-recipient-self 2>/dev/null
        autocmd BufWritePre,FileWritePre    *.gpg let &sh=shsave
        autocmd BufWritePost,FileWritePost  *.gpg silent u
        autocmd BufWritePost,FileWritePost  *.gpg set nobin
        augroup END