From 905c85813a24f02050421df8a7b1bf7ffbf4d987 Mon Sep 17 00:00:00 2001 From: Jacek Kowalski <Jacek@jacekk.info> Date: Sat, 01 Nov 2014 23:52:41 +0000 Subject: [PATCH] [core] Zablokowanie przetwarzania XML External Entities --- class/BotSession.php | 286 +++++++++++++++++++++++++++++++++++++++++---------------- 1 files changed, 206 insertions(+), 80 deletions(-) diff --git a/class/BotSession.php b/class/BotSession.php index cec7f45..28bf0f5 100644 --- a/class/BotSession.php +++ b/class/BotSession.php @@ -1,88 +1,119 @@ <?php /** - * Klasa przechowująca dane użytkownika. Całość przypomina mechanizm sesji w PHP. + * Klasa przechowująca dane przekazane przez użytkownika, + * w szczególności jego ustawienia. */ class BotSession { - private $PDO; + /** + * Instancja PDO tworzona w metodzie {@link BotSession::init()}. + * @var PDO $PDO + */ + protected $PDO; + + /** + * Katalog, w którym trzymane są dane sesyjne użytkowników. + * @var string $sessionDir + */ + protected $sessionDir; + + /** + * Katalog, w którym trzymane są dane sesyjne użytkowników + * z poprzedniej wersji bota. + * @var string $legacySessionDir + */ + protected $legacySessionDir; + + /** + * Nazwa modułu (max. 40 znaków), którego zmienne klasa aktualnie przetwarza, + * ustawiana metodą {@link BotSession::setClass()}. + * @var string $class + */ + protected $class = ''; /** - * Nazwa modułu, którego zmienne klasa przetwarza - * @var string max. 40 znak�w + * Pseudo-URL użytkownika. + * @see BotUser + * @var string $user */ - var $class; - - private $user; - - + protected $user; + /** - * Inicjuje klasę w zależności od użytkownika + * Inicjuje klasę dla podanego użytkownika + * @param string $user Pseudo-URL użytkownika + * @param string $sessionDir Katalog z danymi, domyślnie BOT_TOPDIR/database + * @param string $legacySessionDir Katalog z danymi ze starej wersji bota, domyślnie BOT_TOPDIR/db */ - function __construct($user) { - $this->user = sha1($user); - $this->user_struct = parse_url($user); - - $this->class = ''; + public function __construct($user, $sessionDir = NULL, $legacySessionDir = NULL) { + if(empty($sessionDir)) { + $sessionDir = BOT_TOPDIR.'/database'; + } + if(empty($legacySessionDir)) { + $legacySessionDir = BOT_TOPDIR.'/db'; + } + + $this->user = $user; + $this->sessionDir = $sessionDir; + $this->legacySessionDir = $legacySessionDir; } - - private function init() { - if($this->PDO) { - return NULL; + + /** + * Sprawdza ustawienie pola {@link BotSession::$class} oraz, jeśli nie została wykonana wcześniej, + * dokonuje inicjalizacji klasy. + * Metoda ta winna być wywoływana przez każdą publiczną funkcję operującą na danych. + * @throws Exception Wyjątek rzucany, gdy przed użyciem metody, nazwa klasy + * nie została ustawiona metodą {@link BotSession::setClass()} + */ + protected function init() { + if(empty($this->class)) { + throw new Exception('Przed użyciem mechanizmu sesji należy ustawić nazwę modułu za pomocą metody setClass - patrz "Poradnik tworzenia modułów", dział "Klasa BotMessage", rozdział "Pole $session".'); } - if(is_file(BOT_TOPDIR.'/database/'.sha1($this->user).'.sqlite')) { - $this->PDO = new PDO('sqlite:'.BOT_TOPDIR.'/database/'.sha1($this->user).'.sqlite'); - $this->PDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->PDO->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_TO_STRING); + if($this->PDO) { + // Inicjalizacja została już przeprowadzona - wyjdź. return; } - - try { - $this->PDO = new PDO('sqlite:'.BOT_TOPDIR.'/database/'.sha1($this->user).'.sqlite'); - $this->PDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->PDO->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_TO_STRING); - - $this->PDO->query( - 'CREATE TABLE data ( - class VARCHAR(50), - name VARCHAR(40) NOT NULL, - value TEXT NOT NULL, - PRIMARY KEY ( - class ASC, - name ASC - ) - )' - ); - - $files = glob(BOT_TOPDIR.'/db/*/'.$this->user_struct['user'].'.ggdb'); - if(!$files) { - return; + + $dbFile = $this->sessionDir.'/'.sha1(sha1($this->user)).'.sqlite'; + + $this->PDO = new PDO('sqlite:'.$dbFile); + $this->PDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->PDO->setAttribute(PDO::ATTR_ORACLE_NULLS, PDO::NULL_TO_STRING); + + $st = $this->PDO->query('SELECT COUNT(name) FROM sqlite_master WHERE type=\'table\' AND name=\'data\''); + $num = $st->fetch(PDO::FETCH_NUM); + $schemaExists = $num[0] > 0; + + if($schemaExists) { + $this->updateDatabase(); + } else { + try { + $this->createSchema(); + $this->importLegacyData(); } - - $this->PDO->beginTransaction(); - $st = $this->PDO->prepare('INSERT OR REPLACE INTO data (class, name, value) VALUES (?, ?, ?)'); - - foreach($files as $file) { - $data = unserialize(file_get_contents($file)); - foreach($data as $name => $value) { - $st->execute(array($this->class, $name, $value)); + catch(Exception $e) { + // Import danych nie udał się - usuń pozostałości. + if(file_exists($dbFile)) { + @unlink($dbFile); } + throw $e; } - - $this->PDO->commit(); - - foreach($files as $file) { - unlink($file); - } - } - catch(Exception $e) { - if(file_exists(BOT_TOPDIR.'/database/'.sha1($this->user).'.sqlite')) { - @unlink(BOT_TOPDIR.'/database/'.sha1($this->user).'.sqlite'); - } - throw $e; } } - function __get($name) { + /** + * Ustawia nazwę modułu/klasy, której zmienne będą przetwarzane + * @param string $class Nazwa modułu + */ + public function setClass($class) { + $this->class = $class; + } + + /** + * Pobiera zmienną o podanej nazwie (getter). + * @param string $name Nazwa zmiennej. + * @return mixed Wartość zmiennej lub NULL, jeśli zmienna nie istnieje. + */ + public function __get($name) { $this->init(); $st = $this->PDO->prepare('SELECT value FROM data WHERE class=? AND name=?'); @@ -92,37 +123,55 @@ if(is_array($st)) { return unserialize($st['value']); } - else - { - return NULL; - } + + return NULL; } - function __set($name, $value) { + /** + * Ustawia zmienną o podanej nazwie. + * @param string $name Nazwa zmiennej. + * @param mixed $value Wartość do ustawienia. + */ + public function __set($name, $value) { $this->init(); $st = $this->PDO->prepare('INSERT OR REPLACE INTO data (class, name, value) VALUES (?, ?, ?)'); $st->execute(array($this->class, $name, serialize($value))); } - function __isset($name) { + /** + * Sprawdza czy podana zmienna została ustawiona. + * @param string $name Nazwa zmiennej do sprawdzenia. + * @return bool Czy zmienna istnieje? + */ + public function __isset($name) { $this->init(); $st = $this->PDO->prepare('SELECT COUNT(name) FROM data WHERE class=? AND name=?'); $st->execute(array($this->class, $name)); $st = $st->fetch(PDO::FETCH_NUM); - return ($st[0]>0); + return ($st[0] > 0); } - function __unset($name) { + /** + * Usuwa zmienną o podanej nazwie. + * @param string $name Nazwa zmiennej do usunięcia. + */ + public function __unset($name) { $this->init(); $st = $this->PDO->prepare('DELETE FROM data WHERE class=? AND name=?'); $st->execute(array($this->class, $name)); } - function push($array) { + /** + * Dodaje tablicę zmiennych do danych użytkownika. + * @param array $array Tablica zmiennych do dodania. + */ + public function push($array) { + $this->init(); + $this->PDO->beginTransaction(); foreach($array as $name => $value) { $this->__set($name, $value); @@ -130,26 +179,103 @@ $this->PDO->commit(); } - function pull() { + /** + * Zwraca wszystkie ustawione zmienne dla modułu. + * @return array Lista wszystkich zmiennych. + */ + public function pull() { $this->init(); $st = $this->PDO->prepare('SELECT name, value FROM data WHERE class=?'); $st->execute(array($this->class)); - $st = $st->fetchAll(PDO::FETCH_ASSOC); + $rows = $st->fetchAll(PDO::FETCH_ASSOC); $return = array(); - foreach($st as $row) { - $return[$row['name']] = $row['value']; + foreach($rows as $row) { + $return[$row['name']] = unserialize($row['value']); } return $return; } - function truncate() { + /** + * Usuwa wszystkie zmienne sesyjne danego modułu. + */ + public function truncate() { $this->init(); $st = $this->PDO->prepare('DELETE FROM data WHERE class=?'); $st->execute(array($this->class)); } + + /** + * Aktualizuje schemat bazy danych oraz dane, w szczególności poprawia błędy + * wprowadzone we wcześniejszych wersjach (np. brak ustawionej nazwy klasy). + */ + private function updateDatabase() { + $st = $this->PDO->query('SELECT value FROM data WHERE class=\'\' AND name=\'_version\''); + $row = $st->fetch(PDO::FETCH_ASSOC); + + $version = 0; + if (is_array($row)) { + $version = (int)$row['value']; + } + + $st->closeCursor(); + + switch($version) { + case 1: + $this->PDO->query('UPDATE data SET class=\'kino\' WHERE class=\'\' AND name=\'kino\''); + $this->PDO->query('INSERT OR REPLACE INTO data (class, name, value) VALUES (\'\', \'_version\', 1)'); + case 2: + case 3: + $this->PDO->query('DELETE FROM data WHERE class IS NULL AND name=\'user_struct\''); + $this->PDO->query('INSERT OR REPLACE INTO data (class, name, value) VALUES (\'\', \'_version\', 4)'); + break; + } + } + + /** + * Tworzy schemat bazy danych sesyjnych. + */ + private function createSchema() { + $this->PDO->query( + 'CREATE TABLE data ( + class VARCHAR(50) NOT NULL DEFAULT \'\', + name VARCHAR(40) NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY ( + class ASC, + name ASC + ) + )' + ); + $this->PDO->query('INSERT INTO data (class, name, value) VALUES (\'\', \'_version\', 4)'); + } + + /** + * Importuje dane użytkowników z poprzedniej wersji bota. + */ + private function importLegacyData() { + $userData = parse_url($this->user); + $files = glob($this->legacySessionDir.'/*/'.$userData['user'].'.ggdb'); + if(!$files) { + return; + } + + $this->PDO->beginTransaction(); + $st = $this->PDO->prepare('INSERT OR REPLACE INTO data (class, name, value) VALUES (?, ?, ?)'); + + foreach($files as $file) { + $data = unserialize(file_get_contents($file)); + foreach($data as $name => $value) { + $st->execute(array($this->class, $name, serialize($value))); + } + } + $this->PDO->commit(); + + foreach($files as $file) { + unlink($file); + } + } } -?> \ No newline at end of file -- Gitblit v1.9.1