From 018846e56741d74a0152800b49a47d6beaa94c62 Mon Sep 17 00:00:00 2001 From: Gilles Crettenand Date: Tue, 2 Jun 2015 13:33:09 +0200 Subject: [PATCH] Streamline app compatibility between Solr and WS --- Lib/Exception/BookNotFoundException.php | 18 +- Lib/Exception/InvalidAttributeException.php | 16 +- Lib/Exception/SqlException.php | 38 +- Lib/Exception/WebException.php | 48 +- Lib/WebService.php | 244 ++--- Lib/db/AudioBook.php | 43 +- NetBiblio.php | 1064 ++++++++++--------- README.md | 8 +- index.php | 40 +- mobile.php | 6 +- 10 files changed, 765 insertions(+), 760 deletions(-) diff --git a/Lib/Exception/BookNotFoundException.php b/Lib/Exception/BookNotFoundException.php index f8eec20..d647580 100644 --- a/Lib/Exception/BookNotFoundException.php +++ b/Lib/Exception/BookNotFoundException.php @@ -1,9 +1,9 @@ -query = $query; - parent::__construct($message, 0); - } - - public function getSqlError() - { - return $this->getMessage().' while executing: '.$this->query; - } -} +query = $query; + parent::__construct($message, 0); + } + + public function getSqlError() + { + return $this->getMessage().' while executing: '.$this->query; + } +} diff --git a/Lib/Exception/WebException.php b/Lib/Exception/WebException.php index 7476e72..aec0f4b 100644 --- a/Lib/Exception/WebException.php +++ b/Lib/Exception/WebException.php @@ -1,24 +1,24 @@ -excname = $name; - parent::__construct($reason, $code); - } - - public function getName() - { - return $this->excname; - } -} +excname = $name; + parent::__construct($reason, $code); + } + + public function getName() + { + return $this->excname; + } +} diff --git a/Lib/WebService.php b/Lib/WebService.php index 0aed98a..4a5ea78 100644 --- a/Lib/WebService.php +++ b/Lib/WebService.php @@ -1,122 +1,122 @@ -log .= $message."\n"; - } - - public function Run() - { - $this->log("------------------"); - $this->log("Start request", 1, true); - $data = array(); - - try { - $result = $this->Call(); - $data["result"][$this->func] = $result; - } catch (WebException $e) { - $data["error"]["code"] = $e->getCode(); - $data["error"]["name"] = $e->getName(); - $data["error"]["reason"] = $e->getMessage(); - $this->status = 400; - - $this->log(sprintf("Error : [%s] %s", $e->getCode(), $e->getName())); - } catch (\Exception $e) { - $data["failure"]["message"] = $e->getMessage(); - $this->status = 500; - - $this->log(sprintf("Failure : %s", $e->getMessage())); - } - - $this->Send($data); - - $this->log("Request finished", 1, true); - $this->log("------------------\n\n"); - - if(Configuration::get('log.verbosity') > 0) { - file_put_contents(Configuration::get('log.file'), $this->log, FILE_APPEND | LOCK_EX); - } - } - - private function Call() - { - ob_start(); - session_save_path(Configuration::get('session.save_path')); - session_start(); - - $params = empty($_GET) ? $_POST : $_GET; - if (empty($params)) { - throw new WebException ("CallArgument", "arguments error", -1); - } - - if (!array_key_exists("func", $params)) { - throw new WebException ("CallArgFunction", "no 'func' specified", -2); - } - - $this->func = $params["func"]; - unset($params['func']); - - if (!is_callable(array($this, $this->func))) { - throw new WebException ("CallFunction", "'func' method not available", -3); - } - - $rm = new \ReflectionMethod($this, $this->func); - $nbParams = count($params); - $nbArgsFix = $rm->getNumberOfRequiredParameters(); - $nbArgs = $rm->getNumberOfParameters(); - - /* Check the number of arguments. */ - if ($nbParams < $nbArgsFix) { - throw new WebException ("CallArgNumber", "you must provide at least " . $nbArgsFix . " arguments", 4); - } - if ($nbParams > $nbArgs) { - throw new WebException ("CallArgNumber", "you must provide at most " . $nbArgs . " arguments", 4); - } - - $this->log("Calling '".$this->func."'"); - $this->log("Params: ".print_r($params, true), 2); - return call_user_func_array(array($this, $this->func), $params); - } - - private function Send(array $data) - { - static $status_messages = array( - 200 => 'Ok', - 400 => 'Bad request', - 404 => 'Not Found', - 403 => 'Not Authorized', - 500 => 'Server Error', - ); - - header(sprintf('HTTP/1.0 %s %s', $this->status, $status_messages[$this->status])); - - ob_clean(); - flush(); - - $this->log("Data: ".print_r($data, true), 2); - echo json_encode($data); - } -} +log .= $message."\n"; + } + + public function Run() + { + $this->log("------------------"); + $this->log("Start request", 1, true); + $data = array(); + + try { + $result = $this->Call(); + $data["result"][$this->func] = $result; + } catch (WebException $e) { + $data["error"]["code"] = $e->getCode(); + $data["error"]["name"] = $e->getName(); + $data["error"]["reason"] = $e->getMessage(); + $this->status = 400; + + $this->log(sprintf("Error : [%s] %s", $e->getCode(), $e->getName())); + } catch (\Exception $e) { + $data["failure"]["message"] = $e->getMessage(); + $this->status = 500; + + $this->log(sprintf("Failure : %s", $e->getMessage())); + } + + $this->Send($data); + + $this->log("Request finished", 1, true); + $this->log("------------------\n\n"); + + if(Configuration::get('log.verbosity') > 0) { + file_put_contents(Configuration::get('log.file'), $this->log, FILE_APPEND | LOCK_EX); + } + } + + private function Call() + { + ob_start(); + session_save_path(Configuration::get('session.save_path')); + session_start(); + + $params = empty($_GET) ? $_POST : $_GET; + if (empty($params)) { + throw new WebException ("CallArgument", "arguments error", -1); + } + + if (!array_key_exists("func", $params)) { + throw new WebException ("CallArgFunction", "no 'func' specified", -2); + } + + $this->func = $params["func"]; + unset($params['func']); + + if (!is_callable(array($this, $this->func))) { + throw new WebException ("CallFunction", "'func' method not available", -3); + } + + $rm = new \ReflectionMethod($this, $this->func); + $nbParams = count($params); + $nbArgsFix = $rm->getNumberOfRequiredParameters(); + $nbArgs = $rm->getNumberOfParameters(); + + /* Check the number of arguments. */ + if ($nbParams < $nbArgsFix) { + throw new WebException ("CallArgNumber", "you must provide at least " . $nbArgsFix . " arguments", 4); + } + if ($nbParams > $nbArgs) { + throw new WebException ("CallArgNumber", "you must provide at most " . $nbArgs . " arguments", 4); + } + + $this->log("Calling '".$this->func."'"); + $this->log("Params: ".print_r($params, true), 2); + return call_user_func_array(array($this, $this->func), $params); + } + + private function Send(array $data) + { + static $status_messages = array( + 200 => 'Ok', + 400 => 'Bad request', + 404 => 'Not Found', + 403 => 'Not Authorized', + 500 => 'Server Error', + ); + + header(sprintf('HTTP/1.0 %s %s', $this->status, $status_messages[$this->status])); + + ob_clean(); + flush(); + + $this->log("Data: ".print_r($data, true), 2); + echo json_encode($data); + } +} diff --git a/Lib/db/AudioBook.php b/Lib/db/AudioBook.php index 281350f..72667cf 100644 --- a/Lib/db/AudioBook.php +++ b/Lib/db/AudioBook.php @@ -23,18 +23,18 @@ namespace BSR\Lib\db; * @property string sql_collection * @property string isbn * @property string sql_isbn - * @property string readBy - * @property string sql_readBy + * @property string reader + * @property string sql_reader * @property string cover * @property string sql_cover * @property string category * @property string sql_category - * @property string date - * @property string sql_date - * @property string code3 - * @property string sql_code3 - * @property string code3Long - * @property string sql_code3Long + * @property string availabilityDate + * @property string sql_availabilityDate + * @property string producerCode + * @property string sql_producerCode + * @property string producer + * @property string sql_producer * @property string genre * @property string sql_genre * @property string genreCode @@ -45,12 +45,12 @@ namespace BSR\Lib\db; * @property string sql_link * @property string linkTitle * @property string sql_linkTitle - * @property string typeMedia1 - * @property string sql_typeMedia1 + * @property string mediaType + * @property string sql_mediaType */ class AudioBook extends DbMapping { - protected $attributeNames = 'id title author code summary editor media collection isbn readBy reader cover category date code3 code3Long genre genreCode coverdisplay link linkTitle mediaType typeMedia1'; + protected $attributeNames = 'id title author code summary editor media collection isbn reader cover category availabilityDate producerCode producer genre genreCode coverdisplay link linkTitle mediaType'; public static function find($id) { return self::findBy('NoticeID', $id); @@ -88,22 +88,20 @@ class AudioBook extends DbMapping Fields.[490a] AS collection, isbn.DisplayText AS isbn, Fields.[901] AS reader, - Fields.[901] AS readBy, -- for compatibility Fields.[899a] AS cover, '' AS category, -- supposed to come from tags 600, 610, 650, 651, 655, 690, 691, 695, 696 but always empty anyway - CONVERT(VARCHAR, Notices.[CreationDate], 102) AS date, - LTRIM(RTRIM(Notices.[userdefined3code])) AS code3, - [Code3].TextFre AS code3Long, - [GenreCode].TextFre AS genre, - [GenreCode].Code AS genreCode, + item1.AcquisitionDate AS availabilityDate, + ProducerCode.TextFre As producer, + LTRIM(RTRIM(ProducerCode.Code)) AS producerCode, + GenreCode.TextFre As genre, + LTRIM(RTRIM(GenreCode.Code)) AS genreCode, Notices.[coverdisplay], Fields.[856u] AS link, Fields.[856z] AS linkTitle, Notices.[MediaType1Code] AS mediaType, - Notices.[MediaType1Code] AS typeMedia1 -- for compatibility FROM Notices - INNER JOIN Codes AS Code3 ON Notices.userdefined3code = Code3.Code AND Code3.Type=6 - INNER JOIN Codes AS GenreCode ON MediaType2Code = GenreCode.Code AND GenreCode.Type = 2 + INNER JOIN Codes As GenreCode ON Notices.MediaType2Code = GenreCode.Code AND GenreCode.Type = 2 + INNER JOIN Codes AS ProducerCode ON Notices.userdefined3code = ProducerCode.Code AND ProducerCode.Type=6 LEFT OUTER JOIN ( SELECT * FROM ( @@ -137,6 +135,11 @@ class AudioBook extends DbMapping FOR Field IN ([520], [901], [300], [020], [490a], [260b], [260c], [856u], [856z], [899a]) ) AS pvt ) Fields ON Notices.NoticeID = Fields.NoticeID + OUTER APPLY ( + SELECT TOP 1 * + FROM Items + WHERE Notices.NoticeID = NoticeId + ) AS item1 LEFT JOIN Authorities AS isbn ON isbn.AuthorityID = Fields.[020] WHERE LTRIM(RTRIM(Notices.[%s])) IN ('%s') diff --git a/NetBiblio.php b/NetBiblio.php index 7143dc6..2554e4c 100644 --- a/NetBiblio.php +++ b/NetBiblio.php @@ -1,531 +1,533 @@ -current()) { - $itemId = $row['itemID']; - } else { - throw new WebException("ItemNotFound", "cannot find item", -1030); - } - - $sql = "SELECT UserAccountID FROM UserAccounts WHERE LTRIM(RTRIM(UserAccountNr)) = '$login';"; - $result = Connection::execute($sql, false); - if ($row = $result->current()) { - $userId = $row['UserAccountID']; - } else { - throw new WebException("UserNotFound", "cannot find user", -1031); - } - - $sql = "SELECT circulationId - FROM OldCirculations - WHERE - useraccountID= $userId AND - itemID = $itemId AND - LTRIM(RTRIM(remark)) = '$client';"; - $result = Connection::execute($sql, false); - - if ($row = $result->current()) { - $id = $row['circulationId']; - $sql = "UPDATE OldCirculations - SET - CheckInDate=GETDATE(), - CheckOutDate=GETDATE() - WHERE circulationID = $id"; - Connection::execute($sql); - return true; - } - - $sql = "SELECT TOP 1 circulationID FROM OldCirculations ORDER BY CirculationID DESC"; - $result = Connection::execute($sql, false); - if ($row = $result->current()) { - $nextId = $row['circulationID'] + 1; - } else { - $nextId = 1; - } - - $sql = "UPDATE Useraccounts - SET - Circulations = Circulations + 1, - TotalCirculations = TotalCirculations + 1 - WHERE UseraccountID = $userId;"; - Connection::execute($sql); - - $sql = "UPDATE Items - SET - Circulations = Circulations + 1, - TotalCirculations = TotalCirculations + 1 - WHERE ItemID = $itemId;"; - Connection::execute($sql); - - $worker_id = Configuration::get('netbiblio_worker_id'); - $sql = "INSERT INTO OldCirculations ( - CirculationID, ItemID, UseraccountID, - Remark, - DueDate, CheckOutDate, CheckInDate, - CheckOutBranchofficeID, CheckOutEmployeeID, CheckInBranchofficeID, CheckInEmployeeID, - Reminders, Renewals, Prereminder, InfoCode, CheckOutSIP2Info, CheckInSIP2Info - ) VALUES ( - $nextId, $itemId, $userId, - '$client', - DATEADD(month, 2, GETDATE()), GETDATE(), GETDATE(), - 2, $worker_id, 2, $worker_id, - 0, 0, 1, '-', 1, 1 - );"; - Connection::execute($sql); - return true; - } - - public function Authenticate($login, $password, $client = "website") - { - session_unset(); /* destroy all session vars */ - - $user = User::authenticate($login, $password); - - if (!$user) { - throw new WebException ("AuthenticateBad", "authentication failed", -100); - } - - $_SESSION["user"]["login"] = $login; - $_SESSION["user"]["client"] = $client; - - $this->login = $login; - $this->client = $client; - - return $user->toArray(); - } - - public function Disconnect() - { - $_SESSION = array(); - - if (ini_get("session.use_cookies")) { - $params = session_get_cookie_params(); - setcookie(session_name(), '', time() - 42000, - $params["path"], $params["domain"], - $params["secure"], $params["httponly"]); - } - - return array(); - } - - public function IsAuthenticated() - { - return $this->getUser()->toArray(); - } - - /** - * Adds entries to OldCirculations in Netbiblio database and increments counters on items and useraccounts tables - * For now, keeps a separate log in BSRDownload Database to store IPs - * In case a download has already been logged, only the date of the existing entry is updated, no counter incremented. - * @param string $login - * @return User - * @throws WebException in case the login cannot be found in the database - */ - private function getUser($login = null) - { - if (!$login) { - $login = $_SESSION["user"]["login"]; - } - - $this->checkSession($login); - $user = User::find($this->login); - - if (!$user) { - throw new WebException ("UserNotFound", "cannot find account", -130); - } - - return $user; - } - - private function CheckSession($login = null, $client = null) - { - if (!isset ($_SESSION["user"]["login"])) { - return; - } - - if(!$client) { - $client = isset($_SESSION["user"]["client"]) ? $_SESSION["user"]["client"] : 'website'; - } - - if (!$login) { - $login = $_SESSION["user"]["login"]; - } else if ($_SESSION["user"]["login"] !== $login) { - throw new WebException ("CheckSessionBadAuth", "bad authentication", -1001); - } - - $this->login = $login; - $this->client = $client; - } - - public function FindAccount($login) - { - return $this->getUser($login)->toArray(); - } - - public function GetWishes() - { - $books = $this->getUser()->getWishes(); - return array_values($this->AddBookData($books)); - } - - public function GetCirculations() - { - $circulations = $this->getUser()->getCirculations(); - return array_values($this->AddBookData($circulations)); - } - - public function GetOldCirculations() - { - $circulations = $this->getUser()->getOldCirculations(); - return array_values($this->AddBookData($circulations)); - } - - public function AddWish($bookNr) - { - return $this->getUser()->addWish($bookNr); - } - - public function DeleteWish($bookNr) - { - $this->getUser()->deleteWish($bookNr); - } - - public function FindBooks($codes) - { - $this->CheckSession(); - - $codes = json_decode($codes, true); - $codes = array_map('intval', $codes); - $books = AudioBook::findBy('NoticeNr', $codes, true); - return array_values($this->AddBookData($books)); - } - - private function GetFiles(array $ids) - { - $ids = array_map('intval', $ids); - - $uri = sprintf("%s%s", - Configuration::get('checkfile_url'), - http_build_query(array("book" => implode(',', $ids))) - ); - - $ch = curl_init($uri); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADER, 0); - $json = curl_exec($ch); - curl_close($ch); - - return json_decode($json, true); - } - - private function SetFiles($files) { - $json = json_encode(array_values(array_map(function($f) { - return array( - 'id' => $f['id'], - 'samples' => array('set' => isset($f['samples']) ? $f['samples'] : array()), - 'zip' => array('set' => isset($f['zip']['uri']) ? $f['zip']['uri'] : ''), - 'zip_size' => array('set' => isset($f['zip']['size']) ? $f['zip']['size'] : 0), - ); - }, $files))); - - $uri = sprintf('%s:%s/%s/update?commitWithin=500', - Configuration::get('solr.server'), - Configuration::get('solr.port'), - Configuration::get('solr.path') - ); - $ch = curl_init($uri); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); - curl_setopt($ch, CURLOPT_POSTFIELDS, $json); - curl_setopt($ch, CURLOPT_HTTPHEADER, array( - 'Content-Type: application/json', - 'Content-Length: ' . strlen($json) - )); - - curl_exec($ch); - curl_close($ch); - } - - private function AddBookData(array $books) - { - if(isset($books['code'])) { - $result = $this->AddBookData(array($books)); - return reset($result); - } - - // add complementary data to each book - $books = array_map(function($b) { - // add files if we already have them - $files = array(); - if(isset($b['samples']) && count($b['zip']) > 0) { - $files['samples'] = $b['samples']; - } - unset($b['samples']); - if(isset($b['zip']) && strlen($b['zip']) > 0) { - $files['zip'] = array( - 'uri' => $b['zip'], - 'size' => $b['zip_size'], - ); - } - unset($b['zip']); - unset($b['zip_size']); - - if(! empty($files)) { - $b['files'] = $files; - } - - // add date if we have an availabilityDate for Mobile apps compatibility - if(! isset($b['date']) && isset($b['availabilityDate'])) { - $b['date'] = date('Y.m.d', strtotime($b['availabilityDate'])); - } - - return $b; - }, $books); - - // retrieve files information for the book that don't have all of them at this time - $booksWithoutFiles = array_filter($books, function($b) { - return ! ( - isset($b['files']) && - isset($b['files']['samples']) && - count($b['files']['samples']) == 2 && // we want two samples (mp3 and ogg) - (strlen($this->login) == 0 || isset($b['files']['zip'])) // we want a zip file only for logged in people - ); - }); - - if(count($booksWithoutFiles) > 0) { - $ids = array_map(function($b) { return $b['code']; }, $booksWithoutFiles); - $files = $this->GetFiles($ids); - - if(count($files) > 0) { - foreach($booksWithoutFiles as $k => $b) { - $fileCode = sprintf("%05u", $b['code']); - if(isset($files[$fileCode])) { - $books[$k]['files'] = $files[$fileCode]; - $files[$fileCode]['id'] = $b['id']; - } else { - // we need to have an empty array for mobile apps compatibility. - $books[$k]['files'] = array(); - } - } - } - - $this->SetFiles($files); - } - - // add hash, client and login into zip file uri - $books = array_map(function($b) { - if(strlen($this->login) > 0 && isset($b['files']['zip']['uri'])) { - $key = 'babf2cfbe2633c3082f8cfffdb3d9008b4b3b300'; - $code = sprintf("%05u", $b['code']); - $hash = sha1($this->client.$this->login.$key.$code.date('Ymd')); - - $b['files']['zip']['uri'] = str_replace(array( - '{client}', - '{login}', - '{hash}', - ), array( - $this->client, - $this->login, - $hash, - ), $b['files']['zip']['uri']); - } else { - unset($b['files']['zip']); - } - - return $b; - }, $books); - - return $books; - } - - public function FindBook($code) - { - $this->CheckSession(); - - $code = intval($code); - $book = AudioBook::findBy('NoticeNr', $code, true); - return $this->AddBookData($book); - } - - public function GetRandomBooks($number = 100, $seed = null) { - if(is_null($seed)) { - $seed = time(); - } - - $bs = new BookSearch(); - $bs->addSortField('random_'.$seed); - $results = $bs->getResults(0, $number); - return $results['books'] ? $this->AddBookData($results['books']) : array(); - } - - public function Search($query, $start, $limit) - { - $query = array( - 'queryText' => $query, - 'queryType' => is_numeric($query) && strlen($query) <= 5 ? 'code' : 'text', - 'count' => $limit, - 'page' => max(intval($start) - 1, 0), - ); - $data = $this->NewSearch(json_encode($query)); - - // remove fields that are not used in "old" search - unset($data['count']); - unset($data['facets']); - return $data; - } - - public function NewSearch($values) - { - $this->CheckSession(); - - $queryArray = json_decode($values, true); - if(! is_array($queryArray)) { - throw new WebException("CallArg", "Argument must be valid JSON.", -42); - } - - // The iOS and Android applications still uses 'category' instead of 'genre' - if(isset($queryArray['category']) && is_array($queryArray['category'])) { - $queryArray['genre'] = $queryArray['category']; - unset($queryArray['category']); - } - - $bs = new BookSearch(); - - if (isset($queryArray['queryType'])) { - $bs->addSortField('author', \SolrQuery::ORDER_ASC); - $bs->addSortField('title', \SolrQuery::ORDER_ASC); - $bs->addSortField('producerCode'); - $bs->addSortField('mediaType', \SolrQuery::ORDER_ASC); - } else { - $bs->addSortField('availabilityDate'); - $bs->addSortField('author', \SolrQuery::ORDER_ASC); - $bs->addSortField('title', \SolrQuery::ORDER_ASC); - } - - if (isset($queryArray['queryText']) && strlen($queryArray['queryText']) > 0) { - $type = isset($queryArray['queryType']) ? $queryArray['queryType'] : null; - - if($this->client != 'website' && in_array($type, array('title', 'author', 'reader'))) { - // we don't want an exact search on mobile apps - $type = $type.'_fr'; - } - - $bs->addQuery($queryArray['queryText'], $type); - } - - if(isset($queryArray['genre']) && is_array($queryArray['genre'])) { - $selectedGenres = array_filter($queryArray['genre'], function ($c) { - return $c != '0'; - }); - if (count($selectedGenres) > 0) { - $selectedGenres = array_map(function ($c) { - return 'genreCode:'.\SolrUtils::escapeQueryChars($c); - }, $selectedGenres); - $bs->addQuery('('.implode(' OR ', $selectedGenres).')', null, false); - } - } - - if(isset($queryArray['jeunesse']) && $queryArray['jeunesse']['filtrer'] === 'filtrer') { - $bs->addQuery(1, 'jeunesse'); - } - - // The following query filter is used by the mobile applications - if(isset($queryArray['producer']) && strlen($queryArray['producer']) > 0) { - $bs->addQuery($queryArray['producer'], 'producerCode'); - } - - $count = isset($queryArray['count']) ? (int) $queryArray['count'] : Configuration::get('solr.result_count'); - $start = isset($queryArray['page']) ? $queryArray['page'] * $count : 0; - - $results = $bs->getResults($start, $count); - $data = array( - 'count' => $results['count'], - 'facets' => $results['facets'], - ); - - if($results['books']) { - $data = array_merge($data, $this->AddBookData($results['books'])); - } - - return $data; - } - - public function ListOfReaders() - { - return AudioBook::listOfReaders(); - } - - public function ListOfGenres() - { - return AudioBook::ListOfGenres(); - } - - public function ListOfCategories() - { - // this method exists for compatibility purpose with the Android and iOS applications - return $this->ListOfGenres(); - } - - public function ListOfTypes() - { - return array_filter(AudioBook::listOfTypes(), function ($t) { - return strlen($t) > 0; - }); - } - - public function InReadingBooks() - { - return AudioBook::inReading(); - } - - public function LastBooksByType($type, $itemsByGroup) - { - $this->CheckSession(); - - $s = new BookSearch(); - if($type == 'Jeunesse') { - $s->addQuery(1, 'jeunesse'); - } else { - $s->addQuery($type, 'genre'); - } - $s->addSortField('availabilityDate'); - - try { - $results = $s->getResults(0, $itemsByGroup); - } catch(\SolrClientException $e) { - throw new WebException ("SolrError", $e->getMessage(), -710); - } - - $ids = array_map(function($r) { return $r['id']; }, $results['response']['docs']); - $books = AudioBook::findBy('NoticeID', $ids, true); - $books = $this->AddBookData($books); - - $data = array(); - foreach($books as $b) { - $data[$b['type']][] = $b; - } - return $data; - } -} +current()) { + $itemId = $row['itemID']; + } else { + throw new WebException("ItemNotFound", "cannot find item", -1030); + } + + $sql = "SELECT UserAccountID FROM UserAccounts WHERE LTRIM(RTRIM(UserAccountNr)) = '$login';"; + $result = Connection::execute($sql, false); + if ($row = $result->current()) { + $userId = $row['UserAccountID']; + } else { + throw new WebException("UserNotFound", "cannot find user", -1031); + } + + $sql = "SELECT circulationId + FROM OldCirculations + WHERE + useraccountID= $userId AND + itemID = $itemId AND + LTRIM(RTRIM(remark)) = '$client';"; + $result = Connection::execute($sql, false); + + if ($row = $result->current()) { + $id = $row['circulationId']; + $sql = "UPDATE OldCirculations + SET + CheckInDate=GETDATE(), + CheckOutDate=GETDATE() + WHERE circulationID = $id"; + Connection::execute($sql); + return true; + } + + $sql = "SELECT TOP 1 circulationID FROM OldCirculations ORDER BY CirculationID DESC"; + $result = Connection::execute($sql, false); + if ($row = $result->current()) { + $nextId = $row['circulationID'] + 1; + } else { + $nextId = 1; + } + + $sql = "UPDATE Useraccounts + SET + Circulations = Circulations + 1, + TotalCirculations = TotalCirculations + 1 + WHERE UseraccountID = $userId;"; + Connection::execute($sql); + + $sql = "UPDATE Items + SET + Circulations = Circulations + 1, + TotalCirculations = TotalCirculations + 1 + WHERE ItemID = $itemId;"; + Connection::execute($sql); + + $worker_id = Configuration::get('netbiblio_worker_id'); + $sql = "INSERT INTO OldCirculations ( + CirculationID, ItemID, UseraccountID, + Remark, + DueDate, CheckOutDate, CheckInDate, + CheckOutBranchofficeID, CheckOutEmployeeID, CheckInBranchofficeID, CheckInEmployeeID, + Reminders, Renewals, Prereminder, InfoCode, CheckOutSIP2Info, CheckInSIP2Info + ) VALUES ( + $nextId, $itemId, $userId, + '$client', + DATEADD(month, 2, GETDATE()), GETDATE(), GETDATE(), + 2, $worker_id, 2, $worker_id, + 0, 0, 1, '-', 1, 1 + );"; + Connection::execute($sql); + return true; + } + + public function Authenticate($login, $password, $client = "website") + { + session_unset(); /* destroy all session vars */ + + $user = User::authenticate($login, $password); + + if (!$user) { + throw new WebException ("AuthenticateBad", "authentication failed", -100); + } + + $_SESSION["user"]["login"] = $login; + $_SESSION["user"]["client"] = $client; + + $this->login = $login; + $this->client = $client; + + return $user->toArray(); + } + + public function Disconnect() + { + $_SESSION = array(); + + if (ini_get("session.use_cookies")) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, + $params["path"], $params["domain"], + $params["secure"], $params["httponly"]); + } + + return array(); + } + + public function IsAuthenticated() + { + return $this->getUser()->toArray(); + } + + /** + * Adds entries to OldCirculations in Netbiblio database and increments counters on items and useraccounts tables + * For now, keeps a separate log in BSRDownload Database to store IPs + * In case a download has already been logged, only the date of the existing entry is updated, no counter incremented. + * @param string $login + * @return User + * @throws WebException in case the login cannot be found in the database + */ + private function getUser($login = null) + { + if (!$login) { + $login = $_SESSION["user"]["login"]; + } + + $this->checkSession($login); + $user = User::find($this->login); + + if (!$user) { + throw new WebException ("UserNotFound", "cannot find account", -130); + } + + return $user; + } + + private function CheckSession($login = null, $client = null) + { + if (!isset ($_SESSION["user"]["login"])) { + return; + } + + if(!$client) { + $client = isset($_SESSION["user"]["client"]) ? $_SESSION["user"]["client"] : 'website'; + } + + if (!$login) { + $login = $_SESSION["user"]["login"]; + } else if ($_SESSION["user"]["login"] !== $login) { + throw new WebException ("CheckSessionBadAuth", "bad authentication", -1001); + } + + $this->login = $login; + $this->client = $client; + } + + public function FindAccount($login) + { + return $this->getUser($login)->toArray(); + } + + public function GetWishes() + { + $books = $this->getUser()->getWishes(); + return array_values($this->AddBookData($books)); + } + + public function GetCirculations() + { + $circulations = $this->getUser()->getCirculations(); + return array_values($this->AddBookData($circulations)); + } + + public function GetOldCirculations() + { + $circulations = $this->getUser()->getOldCirculations(); + return array_values($this->AddBookData($circulations)); + } + + public function AddWish($bookNr) + { + return $this->getUser()->addWish($bookNr); + } + + public function DeleteWish($bookNr) + { + $this->getUser()->deleteWish($bookNr); + } + + public function FindBooks($codes) + { + $this->CheckSession(); + + $codes = json_decode($codes, true); + $codes = array_map('intval', $codes); + $books = AudioBook::findBy('NoticeNr', $codes, true); + return array_values($this->AddBookData($books)); + } + + private function GetFiles(array $ids) + { + $ids = array_map('intval', $ids); + + $uri = sprintf("%s%s", + Configuration::get('checkfile_url'), + http_build_query(array("book" => implode(',', $ids))) + ); + + $ch = curl_init($uri); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, 0); + $json = curl_exec($ch); + curl_close($ch); + + return json_decode($json, true); + } + + private function SetFiles($files) { + $json = json_encode(array_values(array_map(function($f) { + return array( + 'id' => $f['id'], + 'samples' => array('set' => isset($f['samples']) ? $f['samples'] : array()), + 'zip' => array('set' => isset($f['zip']['uri']) ? $f['zip']['uri'] : ''), + 'zip_size' => array('set' => isset($f['zip']['size']) ? $f['zip']['size'] : 0), + ); + }, $files))); + + $uri = sprintf('%s:%s/%s/update?commitWithin=500', + Configuration::get('solr.server'), + Configuration::get('solr.port'), + Configuration::get('solr.path') + ); + $ch = curl_init($uri); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, $json); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Content-Length: ' . strlen($json) + )); + + curl_exec($ch); + curl_close($ch); + } + + private function AddBookData(array $books) + { + if(isset($books['code'])) { + $result = $this->AddBookData(array($books)); + return reset($result); + } + + // add complementary data to each book + $books = array_map(function($b) { + // add files if we already have them + $files = array(); + if(isset($b['samples']) && count($b['zip']) > 0) { + $files['samples'] = $b['samples']; + } + unset($b['samples']); + if(isset($b['zip']) && strlen($b['zip']) > 0) { + $files['zip'] = array( + 'uri' => $b['zip'], + 'size' => $b['zip_size'], + ); + } + unset($b['zip']); + unset($b['zip_size']); + + if(! empty($files)) { + $b['files'] = $files; + } + + // add fields for mobile apps compatibility + $b['date'] = date('Y.m.d', strtotime($b['availabilityDate'])); + $b['readBy'] = $b['reader']; + $b['code3'] = $b['producerCode']; + $b['code3Long'] = $b['producer']; + $b['typeMedia1'] = $b['mediaType']; + + return $b; + }, $books); + + // retrieve files information for the book that don't have all of them at this time + $booksWithoutFiles = array_filter($books, function($b) { + return ! ( + isset($b['files']) && + isset($b['files']['samples']) && + count($b['files']['samples']) == 2 && // we want two samples (mp3 and ogg) + (strlen($this->login) == 0 || isset($b['files']['zip'])) // we want a zip file only for logged in people + ); + }); + + if(count($booksWithoutFiles) > 0) { + $ids = array_map(function($b) { return $b['code']; }, $booksWithoutFiles); + $files = $this->GetFiles($ids); + + if(count($files) > 0) { + foreach($booksWithoutFiles as $k => $b) { + $fileCode = sprintf("%05u", $b['code']); + if(isset($files[$fileCode])) { + $books[$k]['files'] = $files[$fileCode]; + $files[$fileCode]['id'] = $b['id']; + } else { + // we need to have an empty array for mobile apps compatibility. + $books[$k]['files'] = array(); + } + } + } + + $this->SetFiles($files); + } + + // add hash, client and login into zip file uri + $books = array_map(function($b) { + if(strlen($this->login) > 0 && isset($b['files']['zip']['uri'])) { + $key = 'babf2cfbe2633c3082f8cfffdb3d9008b4b3b300'; + $code = sprintf("%05u", $b['code']); + $hash = sha1($this->client.$this->login.$key.$code.date('Ymd')); + + $b['files']['zip']['uri'] = str_replace(array( + '{client}', + '{login}', + '{hash}', + ), array( + $this->client, + $this->login, + $hash, + ), $b['files']['zip']['uri']); + } else { + unset($b['files']['zip']); + } + + return $b; + }, $books); + + return $books; + } + + public function FindBook($code) + { + $this->CheckSession(); + + $code = intval($code); + $book = AudioBook::findBy('NoticeNr', $code, true); + return $this->AddBookData($book); + } + + public function GetRandomBooks($number = 100, $seed = null) { + if(is_null($seed)) { + $seed = time(); + } + + $bs = new BookSearch(); + $bs->addSortField('random_'.$seed); + $results = $bs->getResults(0, $number); + return $results['books'] ? $this->AddBookData($results['books']) : array(); + } + + public function Search($query, $start, $limit) + { + $query = array( + 'queryText' => $query, + 'queryType' => is_numeric($query) && strlen($query) <= 5 ? 'code' : 'text', + 'count' => $limit, + 'page' => max(intval($start) - 1, 0), + ); + $data = $this->NewSearch(json_encode($query)); + + // remove fields that are not used in "old" search + unset($data['count']); + unset($data['facets']); + return $data; + } + + public function NewSearch($values) + { + $this->CheckSession(); + + $queryArray = json_decode($values, true); + if(! is_array($queryArray)) { + throw new WebException("CallArg", "Argument must be valid JSON.", -42); + } + + // The iOS and Android applications still uses 'category' instead of 'genre' + if(isset($queryArray['category']) && is_array($queryArray['category'])) { + $queryArray['genre'] = $queryArray['category']; + unset($queryArray['category']); + } + + $bs = new BookSearch(); + + if (isset($queryArray['queryType'])) { + $bs->addSortField('author', \SolrQuery::ORDER_ASC); + $bs->addSortField('title', \SolrQuery::ORDER_ASC); + $bs->addSortField('producerCode'); + $bs->addSortField('mediaType', \SolrQuery::ORDER_ASC); + } else { + $bs->addSortField('availabilityDate'); + $bs->addSortField('author', \SolrQuery::ORDER_ASC); + $bs->addSortField('title', \SolrQuery::ORDER_ASC); + } + + if (isset($queryArray['queryText']) && strlen($queryArray['queryText']) > 0) { + $type = isset($queryArray['queryType']) ? $queryArray['queryType'] : null; + + if($this->client != 'website' && in_array($type, array('title', 'author', 'reader'))) { + // we don't want an exact search on mobile apps + $type = $type.'_fr'; + } + + $bs->addQuery($queryArray['queryText'], $type); + } + + if(isset($queryArray['genre']) && is_array($queryArray['genre'])) { + $selectedGenres = array_filter($queryArray['genre'], function ($c) { + return $c != '0'; + }); + if (count($selectedGenres) > 0) { + $selectedGenres = array_map(function ($c) { + return 'genreCode:'.\SolrUtils::escapeQueryChars($c); + }, $selectedGenres); + $bs->addQuery('('.implode(' OR ', $selectedGenres).')', null, false); + } + } + + if(isset($queryArray['jeunesse']) && $queryArray['jeunesse']['filtrer'] === 'filtrer') { + $bs->addQuery(1, 'jeunesse'); + } + + // The following query filter is used by the mobile applications + if(isset($queryArray['producer']) && strlen($queryArray['producer']) > 0) { + $bs->addQuery($queryArray['producer'], 'producerCode'); + } + + $count = isset($queryArray['count']) ? (int) $queryArray['count'] : Configuration::get('solr.result_count'); + $start = isset($queryArray['page']) ? $queryArray['page'] * $count : 0; + + $results = $bs->getResults($start, $count); + $data = array( + 'count' => $results['count'], + 'facets' => $results['facets'], + ); + + if($results['books']) { + $data = array_merge($data, $this->AddBookData($results['books'])); + } + + return $data; + } + + public function ListOfReaders() + { + return AudioBook::listOfReaders(); + } + + public function ListOfGenres() + { + return AudioBook::ListOfGenres(); + } + + public function ListOfCategories() + { + // this method exists for compatibility purpose with the Android and iOS applications + return $this->ListOfGenres(); + } + + public function ListOfTypes() + { + return array_filter(AudioBook::listOfTypes(), function ($t) { + return strlen($t) > 0; + }); + } + + public function InReadingBooks() + { + return AudioBook::inReading(); + } + + public function LastBooksByType($type, $itemsByGroup) + { + $this->CheckSession(); + + $s = new BookSearch(); + if($type == 'Jeunesse') { + $s->addQuery(1, 'jeunesse'); + } else { + $s->addQuery($type, 'genre'); + } + $s->addSortField('availabilityDate'); + + try { + $results = $s->getResults(0, $itemsByGroup); + } catch(\SolrClientException $e) { + throw new WebException ("SolrError", $e->getMessage(), -710); + } + + $ids = array_map(function($r) { return $r['id']; }, $results['response']['docs']); + $books = AudioBook::findBy('NoticeID', $ids, true); + $books = $this->AddBookData($books); + + $data = array(); + foreach($books as $b) { + $data[$b['type']][] = $b; + } + return $data; + } +} diff --git a/README.md b/README.md index 5268aaa..a889953 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# README # - -This repository contains the code for the WebService used internally at the [BSR](http://bibliothequesonore.ch/) by the Drupal website and both the iOS and Android applications. - +# README # + +This repository contains the code for the WebService used internally at the [BSR](http://bibliothequesonore.ch/) by the Drupal website and both the iOS and Android applications. + For more information, please consult the internal documentation : http://192.168.1.250/dokuwiki/doku.php?id=webservice \ No newline at end of file diff --git a/index.php b/index.php index de230c4..a0935a4 100644 --- a/index.php +++ b/index.php @@ -1,20 +1,20 @@ -Run(); +Run(); diff --git a/mobile.php b/mobile.php index a600d04..2daa4aa 100644 --- a/mobile.php +++ b/mobile.php @@ -1,4 +1,4 @@ -