You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

744 lines
23 KiB
PHP

<?php
namespace BSR;
use BSR\Lib\Configuration;
use BSR\Lib\db\DBHelper;
use BSR\Lib\db\Connection;
use BSR\Lib\db\User;
use BSR\Lib\Exception\AuthenticationException;
use BSR\Lib\Exception\WebException;
use BSR\Lib\Search\BookSearch;
use BSR\Lib\WebService;
class NetBiblio extends WebService
{
private $login = '';
private $client = 'website';
/**
* Set the current login and client based on information
* from the session.
*/
private function CheckSession()
{
if (! isset ($_SESSION["user"]["login"])) {
return;
}
$this->login = $_SESSION["user"]["login"];
$this->client = isset($_SESSION["user"]["client"]) ? $_SESSION["user"]["client"] : 'website';
}
/**
* Retrieve information about the current user from the database.
* If a username is given, first validate that it is the same
* currently in the session.
*
* @param string|null $login
* @return User
* @throws AuthenticationException
*/
private function getUser($login = null)
{
if(! is_null($login) && $_SESSION["user"]["login"] !== $login) {
throw new AuthenticationException("BadLogin", "Login '$login' is invalid.", AuthenticationException::BAD_LOGIN);
}
$this->checkSession();
$user = User::find($this->login);
if (!$user) {
throw new AuthenticationException("UserNotFound", "No user found for '{$this->login}'.", AuthenticationException::USER_NOT_FOUND);
}
return $user;
}
/**
* Retrieve file information (samples and zip) for a list of books based
* on their code (NoticeNr).
* This should be called only if those information are not already in Solr
* for the given books.
*
* @param array $codes
* @return array File information indexed by book code
*/
private function GetFiles(array $codes)
{
$codes = array_map('intval', $codes);
$uri = sprintf("%s%s",
Configuration::get('checkfile_url'),
http_build_query(array("book" => implode(',', $codes)))
);
$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);
}
/**
* Save files information (samples and zip) for books into Solr
* based on their id (NoticeId).
*
* @param $files
*/
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);
}
/**
* Add some information to each books :
* 1° File information if not already their (@see GetFiles),
* those information are then saved into Solr (@see SetFiles)
* 2° Correctly set hash on zip file to authenticate download
* 3° Compatibility fields for mobile apps
*
* You can pass either a single book or an array of books.
*
* @param array $books either one or a list of books
* @return array either one or a list of books
*/
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;
}
// date will be already set if we are updating Circulations
if(! isset($b['date'])) {
$b['date'] = date('Y.m.d', strtotime($b['availabilityDate']));
}
// add fields for mobile apps compatibility
$b['readBy'] = isset($b['reader']) ? $b['reader'] : 'Lecteur inconnu';
$b['category'] = $b['genre'];
$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 methods *
// **********************************
/**
* This method register a download made through the website or mobile application
* as a lent and returned book (OldCirculation).
*
* @param string $client
* @param string $login
* @param int $code
* @return array
* @throws Lib\Exception\SqlException
* @throws WebException
*/
public function AddDownloadLog($client, $login, $code)
{
$client = str_replace("'", "", $client);
$login = str_replace("'", "", $login);
$code = ltrim(str_replace("'", "", $code), '0');
$itemNr = $code . 'V';
$sql = "SELECT itemID FROM Items WHERE LTRIM(RTRIM(ItemNr)) = '$itemNr';";
$result = Connection::execute($sql, false);
if ($row = $result->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 array('success' => 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
);";
$status = Connection::execute($sql);
return array('success' => $status && ! $status->is_error() && $status->get_num_rows() > 0);
}
/**
* This method authenticates and store the login information into the session so
* that the next calls can be made for this user.
*
* @param $login
* @param $password
* @param string $client
* @return array
* @throws AuthenticationException
*/
public function Authenticate($login, $password, $client = "website")
{
session_unset(); /* destroy all session vars */
$user = User::authenticate($login, $password);
if (! $user) {
throw new AuthenticationException("AuthenticationFailed", "Invalid login or password.", AuthenticationException::AUTHENTICATION_FAILED);
}
$_SESSION["user"]["login"] = $login;
$_SESSION["user"]["client"] = $client;
$this->login = $login;
$this->client = $client;
return $user->toArray();
}
/**
* This method disconnects the current user and clear all session data.
*
* @return array
*/
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('success' => true);
}
/**
* Return the information associated with the currently authenticated user
* if any.
*
* @return array
* @throws AuthenticationException
*/
public function IsAuthenticated()
{
return $this->getUser()->toArray();
}
/**
* This method returns the account information for the given login, but only
* if it matches the currently authenticated user.
*
* @param string $login
* @return array
* @throws AuthenticationException
*/
public function FindAccount($login)
{
return $this->getUser($login)->toArray();
}
/**
* This method returns the books on the currently authenticated user wishlist.
*
* @return array
* @throws AuthenticationException
*/
public function GetWishes()
{
$books = $this->getUser()->getWishes();
return array_values($this->AddBookData($books));
}
/**
* This method returns the list of all Circulations for the currently
* authenticated user in a format suited for BSR internal tools
* (CD engraving for example).
*
* @return array
* @throws AuthenticationException
*/
public function GetCirculations()
{
return $this->getUser()->GetCirculations();
}
/**
* This method returns the list of books that are currently lent to the
* authenticated user.
*
* @return array
* @throws AuthenticationException
*/
public function GetLoans()
{
$circulations = $this->getUser()->GetLoans();
return array_values($this->AddBookData($circulations));
}
/**
* This method returns the list of books that the currently authenticated user
* has downloaded or that were lent to him.
*
* @return array
* @throws AuthenticationException
*/
public function GetOldLoans()
{
$circulations = $this->getUser()->GetOldLoans();
return array_values($this->AddBookData($circulations));
}
/**
* This method adds a book to the currently authenticated user wishlist
* based on its code.
*
* @param int $code
* @return array
* @throws AuthenticationException
*/
public function AddWish($code)
{
$status = $this->getUser()->addWish($code);
return array('success' => $status);
}
/**
* This method deletes the given book from the currently authenticated
* user wishlist based on its code.
*
* @param int $code
* @return array
* @throws AuthenticationException
*/
public function DeleteWish($code)
{
$status = $this->getUser()->deleteWish($code);
return array('success' => $status);
}
/**
* This method returns an array of books based on their code
*
* @param array|int[] $codes
* @return array
*/
public function FindBooks($codes)
{
$this->CheckSession();
return array_values($this->AddBookData(BookSearch::GetBooks(json_decode($codes))));
}
/**
* This method return a book based on its code
*
* @param int $code
* @return array
*/
public function FindBook($code)
{
$this->CheckSession();
$books = $this->AddBookData(BookSearch::GetBooks(array($code)));
return reset($books);
}
/**
* This method returns a list of random books.
* For a given seed, the list should be always the same at least while
* Solr is not restarted (or documents re-indexed)
*
* @param int $number Number of random books to return
* @param null $seed
* @param int $page
* @return array
* @throws WebException
*/
public function GetRandomBooks($number = 100, $seed = null, $page = 0) {
if(is_null($seed)) {
$seed = time();
}
$bs = new BookSearch();
$bs->addSortField('random_'.$seed);
$bs->addQuery(1, 'visible');
$results = $bs->getResults($page * $number, $number);
return $this->AddBookData($results['books']);
}
/**
* This method is used by the iOS application to perform searching.
* If a number is given, search in the book codes, otherwise perform
* a full text search.
*
* @param string $query Text to search
* @param int $start
* @param int $limit
* @return array
* @throws WebException
*/
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;
}
/**
* This method is used by the website and the Android application to perform
* book search.
*
* Search parameters :
*
* ° queryText : the text to search for
* ° queryType : the field to search in, defaults to 'text'
*
* ° genre : array of 'genreCode' to search for
* ° jeunesse : only display books for kids (must have format 'jeunesse' => array('filtrer' => 'filtrer')
* ° producerCode : filter by 'producerCode'
* ° reader : filter by 'reader'
*
* ° count : number of results we want
* ° page : page to start at (0 is the first)
*
* Deprecated, but still in use on mobile apps :
*
* ° category : synonym for 'genre' (see above)
* ° producer : synonym for 'producerCode' (see above)
*
* @param string $values JSON encoded object
* @return array
* @throws WebException
*/
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' and 'producer'
$compatibility = array(
'category' => 'genre',
'producer' => 'producerCode'
);
foreach($compatibility as $old => $new) {
if(isset($queryArray[$old])) {
$queryArray[$new] = $queryArray[$old];
unset($queryArray[$old]);
}
}
$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';
});
$bs->addOrQuery($selectedGenres, 'genreCode');
}
if(isset($queryArray['jeunesse']) && $queryArray['jeunesse']['filtrer'] === 'filtrer') {
$bs->addQuery(1, 'jeunesse');
}
$queries = array('producerCode', 'reader');
foreach($queries as $q) {
if(isset($queryArray[$q]) && strlen($queryArray[$q]) > 0) {
$bs->addQuery($queryArray[$q], $q);
}
}
// we only want visible books in search results
$bs->addQuery(1, 'visible');
$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'],
);
return array_merge($data, $this->AddBookData($results['books']));
}
/**
* This method returns the list of all volunteer readers that read book
* in the database.
* @return array
*/
public function ListOfReaders()
{
return DBHelper::ListOfReaders();
}
/**
* This method is called by the website in Drupal to get the list
* of available 'Genres'.
* @return array
*/
public function ListOfGenres()
{
return DBHelper::ListOfGenres();
}
/**
* This method is called by the Android application to get the list
* of available 'Genres'.
* @return array
*/
public function ListOfCategories()
{
return DBHelper::ListOfGenres();
}
/**
* This method is called by the iOS application to get the list
* of available 'Genres'. 'Jeunesse' must be a part of them.
* @return array
*/
public function ListOfTypes()
{
return array_map(function($g) {
return $g['text'];
}, DBHelper::ListOfGenres(true));
}
/**
* This method returns the list of all books that are currently
* being read by the volunteers.
*
* @return array
*/
public function InReadingBooks()
{
return DBHelper::InReading();
}
/**
* This method is used by the iOS application to retrieve the last
* books for a given Genre. It may receives the genre 'Jeunesse' which
* is a boolean value on Solr documents.
*
* @param string $genre Genre for which we want books (not the code)
* @param int $number number of books
* @return array
* @throws WebException
*/
public function LastBooksByType($genre, $number)
{
$this->CheckSession();
$s = new BookSearch();
if($genre == 'Jeunesse') {
$s->addQuery(1, 'jeunesse');
} else {
$s->addQuery($genre, 'genre');
}
$s->addSortField('availabilityDate');
$results = $s->getResults(0, $number);
$books = $this->AddBookData($results['books']);
$data = array();
foreach($books as $b) {
$data[$genre][] = $b;
}
return $data;
}
}