<?php
/**
 * Модуль реализации работы с Пользователями
 * Реализация UserList0
*/


/**
 * Класс модели работы с БД
*/
class DB 
{
	private $host = "localhost";
	private $dbname = "users";
	private $user = "";
	private $pass = "";	
	
	private static $DBH;
	
	private function __construct () {
		// MySQL через PDO_MYSQL  
		try {
            self::$DBH = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->dbname, $this->user, $this->pass);  
            self::$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); 
            self::$DBH->query('SET NAMES utf8');
        } catch(Exception $e) { 
            throw new Exception ("[DB PDO] Ошибка БД - " . $e->getMessage());
        }
	}
	
	public static function GetDBH () {
		if ( empty ( self::$DBH )) {
			new DB();
		}
		return self::$DBH; 
	}
}


/**
 * Класс модели работы с пользователями
*/
class UserModel 
{
    private $dbInctance;
    private $dbStruct = array ( 'table' => 'users',
                                'columns' => array ('id' => 'key', 
                                                    'login' => 'txt', 
                                                    'nick' => 'txt', 
                                                    'email' => 'txt'
                                                   ) ,
                                'id' => 'id',
                                'unic' => array ('login', 'email'),
                                'nochange' => array ('id', 'login'),
                                'require' => array ('login')
                              );
    
    public function __construct(PDO $dbInctance)
    {
        $this->dbInctance = $dbInctance; 
    }
    
    /**
	 * получает пользователя по ID, алиас к selectUsers с параметром.
     * Возвращает массив данных пользователя
	 */
    public function selectUserByID($id)
    {
        $retArray = $this->selectUsers( array($this->dbStruct['id'] => $id) );
        return $retArray[0];
    }
    
    /**
	 * получает список пользователей с учетом $filter, с условием LIKE % если в поле указана *.
     * если указано orCond = true, ищет по условию OR
	 */
    public function selectUsers(array $filter = array(), $orCond = false)
    {
        $retArray = array();
        $sqlStr = "SELECT * FROM " . $this->dbStruct['table'];
        $condStr = "";
        
        // экранируем - необходимо, т.к. запрос сборный и мы не сможем использовать paramValue PDO
        $filter = $this->addslashesIt($filter);
        
        foreach ($filter as $name => $val) {
            $val = addslashes($val);                
            if (strpos($val, '*') !== false) {      // найдены условия, нужен LIKE
                $val = str_replace('*', '%', $val);
                $val = $name . ' LIKE ' . "'" . $val . "'";
            } else {                                // * нет, нужно сравнение
                $val = $name . ' = ' . "'" . $val . "'";
            }
            
            if (!strlen($condStr)) {   // условия пустые
                $condStr = $val;
            } else {                   // есть условия
                $condStr .= $orCond?' OR ':' AND ';
                $condStr .= $val;
            }
        }
        
        if (strlen($condStr)) { // есть условия для WHERE
            $sqlStr .= ' WHERE ' . $condStr;
        }
        
        $sth = $this->dbInctance->query($sqlStr);
        $sth->setFetchMode(PDO::FETCH_ASSOC);
        
        if ($rows = $sth->fetchAll()) {
            $retArray = $rows;
        }
        
        return $retArray;
    }
       
    /**
	 * создает пользователя на основании данных полей, указанных в $data.
     * возвращает id созданного пользователя
	 */
    public function addUser(array $data)
    {
        $retArray = array();
        $sqlStr = "INSERT INTO " . $this->dbStruct['table'] . " (:col) VALUES (':val')";
        $requiredCol = $this->dbStruct['require'];
        $mustBeUnic = $this->dbStruct['unic'];
        
        // экранируем - необходимо, т.к. запрос сборный и мы не сможем использовать paramValue PDO
        $data = $this->addslashesIt($data);
        
        foreach ($requiredCol as $column) {
            if ( !array_key_exists($column, $data) || !strlen($data[$column])) {  // не найдено обязательное поле или оно пустое
                throw new Exception ( "[" . __CLASS__ . "] Не указано обязательное поле " . $column . "!");
            }
        }
        
        $keys = implode(", ", array_keys($data));
        $values = implode("', '", array_values($data));
        $sqlStr = str_replace(':col', $keys, $sqlStr);
        $sqlStr = str_replace(':val', $values, $sqlStr);
            

        // проверяем уникальность данных, если задано
        if (count($mustBeUnic)) {
            $filter = array();
            
            foreach ($mustBeUnic as $column) {
                if (array_key_exists($column, $data) && strlen($data[$column])) {  
                    $filter[$column] = $data[$column];
                }
            }

            if ( count($filter) && count($this->selectUsers($filter, true)) ) {
                throw new Exception ("[" . __CLASS__ . "] Пользователь с такими данными уже существует!");
            }
        }
        
        $sth = $this->dbInctance->query($sqlStr);
		return $this->dbInctance->lastInsertId();
    }
    
    /**
	 * изменяет данные полей, указанных в $data пользователей по ID.
	 */
    public function updateUser($id, array $data)
    {
        $retArray = array();
        $sqlStr = "UPDATE " . $this->dbStruct['table'] . " SET ";
        $condStr = "WHERE " . $this->dbStruct['id'] . " = " . $this->addslashesIt($id);
        
        $mustBeUnic = $this->dbStruct['unic'];
        $cantBeChange = $this->dbStruct['nochange'];
        
        // экранируем - необходимо, т.к. запрос сборный и мы не сможем использовать paramValue PDO
        $data = $this->addslashesIt($data);
        
        $setLine = "";
            
        foreach ($data as $key => $val) {
            
            if (strlen($setLine)) {   // еще одно поле
                $setLine .= ", ";
            }
            $setLine .= $key . ' = ' .  "'" . $val . "'";
        }
           
        $sqlStr .= $setLine . $condStr;
        
        
        if (count($cantBeChange)) {
            foreach ($cantBeChange as $column) {
                if (array_key_exists($column, $data)) {  
                    throw new Exception ("[" . __CLASS__ . "] Нельзя изменить поле " . $column . "!");
                }
            }
        }
        
        // проверяем уникальность данных, если задано
        if (count($mustBeUnic)) {
            $filter = array();
            
            foreach ($mustBeUnic as $column) {
                if (array_key_exists($column, $data) && strlen($data[$column])) {  // существует и не пустой (пустое поле мб не уникальным)
                    $filter[$column] = $data[$column];
                }
            }
            // проверяем наличие пользователя, у которого есть такие данные, кроме собственно изменяемой записи  
            if ( count($filter) ) {
            
                $searchedUsers = $this->selectUsers($filter, true);
                
                if (count ($searchedUsers) > 1) {
                    throw new Exception ("[" . __CLASS__ . "] Пользователь с такими данными уже существует - в базе есть несколько пользователей с не уникальными данными!");
                }
                
                if (count ($searchedUsers) && $searchedUsers[0][ $this->dbStruct['id'] ] !== $id) {  
                    throw new Exception ("[" . __CLASS__ . "] Пользователь с такими данными уже существует!");
                }
            }
        }
        
        $sth = $this->dbInctance->query($sqlStr);
        if (!$sth->rowCount()) {
            throw new Exception ("[" . __CLASS__ . "] Не удалось обновить данные!");
        }
		return true;        
    }   
    
    /**
	 * удаляет пользователя по id.
     * возвращает id созданного пользователя
	 */
    public function deleteUser($id)
    {
        $retArray = array();
        $sqlStr = "DELETE FROM " . $this->dbStruct['table'] . " WHERE " . $this->dbStruct['id'] . " = " . $this->addslashesIt($id);
        
        $sth = $this->dbInctance->query($sqlStr);
        
		if (!$sth->rowCount()) {
            throw new Exception ("[" . __CLASS__ . "] Не удалось удалить запись!");
        }
		return true;              
    }
    
    /**
	 * Экранирует данные. Может принимать массив и строку
     * возвращает массив \ строку с экранированными данными
	 */
    private function addslashesIt($data)
    {
        if (is_array($data)) {  // массив
            foreach ($data as $key => $val) {
                $data[$key] = addslashes($val);  
            }
        } else {                // строка
            $data = addslashes($data);
        }
        
        return $data;
    }
    
    /**
	 * Получает структуру таблицы для дальнейшей обработки \ построения запроса
     * возвращает массив данных
	 */
    public function getTableStructure()
    {
        return $this->dbStruct;
    }
}

/**
 * Форматирует полученные данные в соответствии с форматом выдачи
 * возвращает данные в запрошенном формате
 */
class FormatUtil
{
    public static function toXML(array $data)
    {
        header('Content-Type: application/xml; charset=utf8');
        $xml = new SimpleXMLElement('<root/>');
        self::to_xml($xml, $data);
        return $xml->asXML();
    }
    
    public static function toJSON(array $data)
    {
        header('Content-Type: application/json; charset=utf8');
        return json_encode($data); 
    }
    
    private static function to_xml(SimpleXMLElement $object, array $data)
    {   
        foreach ($data as $key => $value)
        {   
            if (is_array($value))
            {   
                //$new_object = $object->addChild( is_numeric($key) ? 'user'.$key : $key);
                $new_object = $object->addChild( is_numeric($key) ? 'user' : $key);
                self::to_xml($new_object, $value);
            }   
            else
            {   
                $object->addChild( is_numeric($key) ? 'user'.$key : $key , $value);
            }   
        }   
    } 
}

/**
 * Класс авторизации, проверяет наличие куки, производит аутентификацию
 * возвращает true \ false
 */

class Authorization {
    private static $OBJ;
    private $cookieName = 'auth';
    private $authName = 'authorization';
    private $paramArray;
    private $paramType;
    
    private $login = 'test';
    private $pass = 'test';
    
    private $isAuth = false; 
    
    private function __construct () 
    {
        $this->paramType = $_SERVER["REQUEST_METHOD"];
        if($this->paramType == 'GET') {
            $this->paramArray = $_GET;
        } else {
            $this->paramArray = $_POST;
        }
        
        if (!isset($_COOKIE[$this->cookieName])) {
            if (array_key_exists($this->authName, $this->paramArray) 
            && array_key_exists('login', $this->paramArray[$this->authName]) 
            && array_key_exists('pass', $this->paramArray[$this->authName]))
            {
                $login = $this->paramArray[$this->authName]['login'];
                $pass = $this->paramArray[$this->authName]['pass'];
                
                if ($login == $this->login && $pass == $this->pass)
                {
                    setcookie ($this->cookieName, md5($pass), time() + 3600);
                    $this->isAuth = true;
                }
            }
        } else {
            if ($_COOKIE[$this->cookieName] == md5($this->pass)) {
                $this->isAuth = true;
            }
        }
        
        
    }
    
    public static function init()
    {
        if ( empty ( self::$OBJ )) {
			self::$OBJ = new Authorization();
		}
		return self::$OBJ; 
    }
    
    public function isAuth ()
    {
        return $this->isAuth;
    }
}


/**
 * Класс-роутер, получает параметры и вызывает модель
 * возвращает данные в json или запрошенном формате
 */
class Route {
    
    private static $OBJ;
    private $paramArray;
    private $paramType;

    private $format = 'json';
    
    private $actionName = 'action';
    private $api = array ( 'get'    => array( 'metod' => 'selectUsers',
                                              'param' => array ('id', 'login', 'nick', 'email') 
                                            ),
                           'getbyid'=> array( 'metod' => 'selectUserByID',
                                              'param' => array ('id') 
                                            ),
                           'add'    => array( 'metod' => 'addUser',
                                              'param' => array ('data') 
                                            ),
                           'update' => array( 'metod' => 'updateUser',
                                              'param' => array ('id', 'data') 
                                            ),
                           'delete' => array( 'metod' => 'deleteUser',
                                              'param' => array ('id') 
                                            )
                         ); 
    
    private function __construct () 
    {
        $this->paramType = $_SERVER["REQUEST_METHOD"];
        if($this->paramType == 'GET') {
            $this->paramArray = $_GET;
        } else {
            $this->paramArray = $_POST;
        }
    }
    
    public static function init()
    {
        if ( empty ( self::$OBJ )) {
			self::$OBJ = new Route();
		}
		return self::$OBJ; 
    }
    
    public function run()
    {
        // проверяем авторизацию
        if (!Authorization::init()->isAuth()) {
            $this->makeError("[" . __CLASS__ . "] API доступно только авторизованным пользователям!");
        }
        
        $action = $this->getParam($this->actionName);
        
        if (!array_key_exists($action, $this->api)) {
           $this->makeError("[" . __CLASS__ . "] не найдена комманда " . $action . '!');
        }
        
        $metod = $this->api[$action]['metod'];
        $param = array();
        
        switch ($action) { // дополнительная проверка \ обработка параметров для комманд
            case 'get':
                $filter = array();
                foreach ($this->api[$action]['param'] as $val) {
                    $paramElem = $this->getParam($val, false);
                    if ($paramElem !== false) {
                        $filter[$val] = $paramElem;
                    }
                }
                $param[] = $filter;
                break;
                
            default:
                foreach ($this->api[$action]['param'] as $val) {
                    $param[] = $this->getParam($val);
                }
                break;
        }
        
        $model = new UserModel(DB::GetDBH());
        
        
        try {
            $returnData = call_user_func_array(array($model,$metod), $param);
        } catch(Exception $e) { 
            $this->makeError( $e->getMessage() );
        }
        
        echo $this->outputFormated(array ('error'=>'0', 'data' => $returnData) );
        return;
    }
    
    private function getParam ($name, $required = true)
    {
        if ( !array_key_exists($name, $this->paramArray) ) {
            if ($required) {
                $this->makeError("[" . __CLASS__ . "] Нет обязательного элемента " . $name . '!');
            } else {
                return false;
            }
        }
        return $this->paramArray[$name];
    }
    
    private function outputFormated($data)
    {    
        $format = $this->getParam('format', false);
        
        if ($format === false) {
            $format = $this->format;
        }
        
        if (!method_exists('FormatUtil', 'to' . $format)) {
            die("[" . __CLASS__ . "] Неверный формат вывода - " . $format . '!');
        }
        
        $this->format = $format;
        
        return FormatUtil::{'to' . $this->format}($data);
    }
    
    private function makeError($errMess)
    {
        die ( $this->outputFormated( array('error'=>'1', 'message'=>$errMess) ) );
    }
}


    setlocale(LC_ALL, 'ru_RU.utf8');
    Header("Content-Type: text/html;charset=UTF-8");

    $_GET['format'] = 'xml';
    $_GET['action'] = 'get';
    $_GET['authorization'] = array('login'=>'test', 'pass'=>'test');
	
	try {
        Route::init()->run();
    } catch(Exception $e) { 
            echo $e->getMessage() . '<br>'; 
    }


















