Jacek Kowalski
2014-07-06 6d8764c5366e0b1baf66da50230dac623edb2450
[core] Umożliwienie ustawienia katalogu z danymi sesji użytkowników i dostosowanie testów jednostkowych.
2 files modified
371 ■■■■■ changed files
class/BotSession.php 299 ●●●●● patch | view | raw | blame | history
tests/Core/BotSessionTest.php 72 ●●●●● patch | view | raw | blame | history
class/BotSession.php
@@ -4,122 +4,99 @@
 * w szczególności jego ustawienia.
 */
class BotSession {
    private $PDO;
    /**
     * Nazwa modułu, którego zmienne klasa przetwarza
     * @var string $class max. 40 znaków
     * 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 = '';
    protected $class_empty = TRUE;
    
    /**
     * Pseudo-URL użytkownika.
     * @see BotUser
     * @var string $user URL użytkownika
     * @var string $user
     */
    private $user;
    protected $user;
    /**
     * Klasa z identyfikatorem użytkownika
     * @var BotUser $user_struct
     * 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
     */
    private $user_struct;
    /**
     * Inicjuje klasę w zależności od użytkownika
     */
    function __construct($user) {
        $this->user = sha1($user);
        $this->user_struct = parse_url($user);
        $this->class_empty = FALSE;
    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(strlen($this->class) == 0 && !$this->class_empty) {
            throw new Exception('Przed użyciem $msg->session należy ustawić nazwę modułu za pomocą metody setClass - patrz "Poradnik tworzenia modułów", dział "Klasa BotMessage", rozdział "Pole $session".');
    /**
     * 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($this->PDO) {
            return NULL;
        }
        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);
            $st = $this->PDO->query('SELECT value FROM data WHERE class=\'\' AND name=\'_version\'');
            $row = $st->fetch(PDO::FETCH_ASSOC);
            if(is_array($row)) {
                $version = (int)$row['value'];
            }
            else
            {
                $version = 0;
            }
            $st->closeCursor();
            if($version < 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)');
                $version = 1;
            }
            if($version < 4) {
                $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)');
                $version = 4;
            }
            // 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) 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)');
            $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, serialize($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;
        }
    }
    
@@ -127,16 +104,16 @@
     * Ustawia nazwę modułu/klasy, której zmienne będą przetwarzane
     * @param string $class Nazwa modułu
     */
    function setClass($class) {
    public function setClass($class) {
        $this->class = $class;
    }
    
    /**
     * Pobiera zmienną modułu o podanej nazwie (getter).
     * @param string $name Nazwa zmiennej
     * @return mixed Wartość zmiennej lub NULL
     * Pobiera zmienną o podanej nazwie (getter).
     * @param string $name Nazwa zmiennej.
     * @return mixed Wartość zmiennej lub NULL, jeśli zmienna nie istnieje.
     */
    function __get($name) {
    public function __get($name) {
        $this->init();
        
        $st = $this->PDO->prepare('SELECT value FROM data WHERE class=? AND name=?');
@@ -146,18 +123,16 @@
        if(is_array($st)) {
            return unserialize($st['value']);
        }
        else
        {
            return NULL;
        }
        return NULL;
    }
    
    /**
     * Ustawia zmienną o podanej nazwie
     * @param string $name Nazwa zmiennej
     * @param mixed $value Wartość zmiennej
     * Ustawia zmienną o podanej nazwie.
     * @param string $name Nazwa zmiennej.
     * @param mixed $value Wartość do ustawienia.
     */
    function __set($name, $value) {
    public function __set($name, $value) {
        $this->init();
        
        $st = $this->PDO->prepare('INSERT OR REPLACE INTO data (class, name, value) VALUES (?, ?, ?)');
@@ -166,24 +141,24 @@
    
    /**
     * Sprawdza czy podana zmienna została ustawiona.
     * @param string $name Nazwa zmiennej
     * @param string $name Nazwa zmiennej do sprawdzenia.
     * @return bool Czy zmienna istnieje?
     */
    function __isset($name) {
    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);
    }
    
    /**
     * Usuwa zmienną o podanej nazwie
     * @param string $name Nazwa zmiennej
     * Usuwa zmienną o podanej nazwie.
     * @param string $name Nazwa zmiennej do usunięcia.
     */
    function __unset($name) {
    public function __unset($name) {
        $this->init();
        
        $st = $this->PDO->prepare('DELETE FROM data WHERE class=? AND name=?');
@@ -191,10 +166,12 @@
    }
    
    /**
     * Zapamiętuje tablicę zmiennych danego modułu
     * @param array $array Tablica zmiennych
     * Dodaje tablicę zmiennych do danych użytkownika.
     * @param array $array Tablica zmiennych do dodania.
     */
    function push($array) {
    public function push($array) {
        $this->init();
        $this->PDO->beginTransaction();
        foreach($array as $name => $value) {
            $this->__set($name, $value);
@@ -203,18 +180,18 @@
    }
    
    /**
     * Zwraca wszystkie ustawione zmienne danego modułu
     * @return array Lista wszystkich zmiennych
     * Zwraca wszystkie ustawione zmienne dla modułu.
     * @return array Lista wszystkich zmiennych.
     */
    function pull() {
    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) {
        foreach($rows as $row) {
            $return[$row['name']] = unserialize($row['value']);
        }
        
@@ -224,11 +201,81 @@
    /**
     * Usuwa wszystkie zmienne sesyjne danego modułu.
     */
    function truncate() {
    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);
        }
    }
}
?>
tests/Core/BotSessionTest.php
@@ -1,68 +1,77 @@
<?php
class BotSessionTest extends PHPUnit_Framework_TestCase {
    function testSessionFolder() {
        $dbFolder = dirname(__FILE__).'/../../database';
    private static $dataFolder;
    private static $legacyFolder;
    private static function tmpdir() {
        $tmpName = tempnam(sys_get_temp_dir(), 'Bot');
        unlink($tmpName);
        mkdir($tmpName);
        return $tmpName;
    }
    private static function rmdir($dir) {
        foreach(glob($dir.'/*', GLOB_NOSORT) as $name) {
            if($name == '.' || $name == '..') continue;
            if(is_dir($name)) {
                self::rmdir($name);
            } else {
                unlink($name);
            }
        }
        
        $this->assertTrue(is_writable($dbFolder));
        $this->assertTrue(count(glob($dbFolder.'/*.sqlite')) == 0);
        rmdir($dir);
    }
    
    /**
     * @depends testSessionFolder
     * Create one-time directories for testing purposes.
     */
    static function setUpBeforeClass() {
        self::$dataFolder = self::tmpdir();
        self::$legacyFolder = self::tmpdir();
    }
    function testPullEmpty() {
        $dbFolder = dirname(__FILE__).'/../../database';
        $session = new BotSession('test://user1@test');
        $session = new BotSession('test://user1@test', self::$dataFolder, self::$legacyFolder);
        $session->setClass('test');
        
        $this->assertEquals(array(), $session->pull());
        $this->assertTrue(count(glob($dbFolder.'/*.sqlite')) == 1);
        $this->assertTrue(count(glob(self::$dataFolder.'/*.sqlite')) == 1);
    }
    
    /**
     * @depends testPullEmpty
     * @expectedException Exception
     */
    function testSetClass() {
        $session = new BotSession('test://user1');
        $session = new BotSession('test://testException', self::$dataFolder, self::$legacyFolder);
        $session->pull();
    }
    
    /**
     * @depends testPullEmpty
     */
    function testLegacyImport() {
        $dbFolder = dirname(__FILE__).'/../../database';
        $oldDbFolder = $dbFolder = dirname(__FILE__).'/../../db';
        $data = array('test' => true, 'other' => 'yes, sir!');
        $data_serialized = serialize($data);
        
        $this->assertTrue(mkdir($oldDbFolder));
        $this->assertTrue(is_writable($oldDbFolder));
        $this->assertTrue(mkdir($oldDbFolder.'/test'));
        $this->assertTrue(mkdir(self::$legacyFolder.'/test'));
        
        $filename = $oldDbFolder.'/test/testUser.ggdb';
        $filename = self::$legacyFolder.'/test/legacyUser.ggdb';
        $this->assertEquals(strlen($data_serialized), file_put_contents($filename, $data_serialized));
        $this->assertEquals($data_serialized, file_get_contents($filename));
        
        $session = new BotSession('test://testUser@test');
        $session = new BotSession('test://legacyUser@test', self::$dataFolder, self::$legacyFolder);
        $session->setClass('test');
        
        $this->assertTrue(isset($session->test));
        $this->assertEquals($data, $session->pull());
        
        $this->assertFalse(file_exists($filename));
        $this->assertTrue(rmdir($oldDbFolder.'/test'));
        $this->assertTrue(rmdir($oldDbFolder));
    }
    
    /**
     * @depends testPullEmpty
     */
    function testManualExample() {
        $session = new BotSession('test://user1@test');
        $session = new BotSession('test://user1@test', self::$dataFolder, self::$legacyFolder);
        $session->setClass('test');
        
        // Ustawienie pojedynczej wartości
@@ -108,7 +117,7 @@
     * @depends testManualExample
     */
    function testManualExample2() {
        $session = new BotSession('test://user1@test');
        $session = new BotSession('test://user1@test', self::$dataFolder, self::$legacyFolder);
        $session->setClass('test');
        
        $array = array(
@@ -124,13 +133,12 @@
        $this->assertEquals(array(), $session->pull());
    }
    
    /**
     * @depends testManualExample2
     */
    function testCleanup() {
        $dbFolder = dirname(__FILE__).'/../../database';
        foreach(glob($dbFolder.'/*.sqlite') as $file) {
    static function tearDownAfterClass() {
        foreach(glob(self::$dataFolder.'/*.sqlite') as $file) {
            unlink($file);
        }
        self::rmdir(self::$dataFolder);
        self::rmdir(self::$legacyFolder);
    }
}