Completed
Pull Request — master (#233)
by
unknown
11:31
created

virtuallib.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * COPS (Calibre OPDS PHP Server) class file
4
 *
5
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6
 * @author     Klaus Broelemann <[email protected]>
7
 */
8
9
require_once('base.php');
10
11
/**
12
 * Read and filter virtual libraries
13
 */
14
class VirtualLib {
15
	const SQL_VL_KEY = "virtual_libraries";  // Key for the virtual library entries.
16
	
17
	private $db_id = null;   // Database Index for the virtual lib
18
	private $vl_id = null;   // Library Index
19
	private $filter = null;  // structured representation of the current filter 
20
	
21
	private static $currentVL = null;  // Singleton: current virtual lib
22
	
23
	/**
24
	 * The constructor parses the calibre search string and creates a array-based representation out of it.
25
	 * 
26
	 * @param int $database The database id of the new object.
27
	 * @param int $virtualLib The virtual library id of the new object.
28
	 */
29
	private function __construct($database, $virtualLib) {
30
		$this->db_id  = $database;
31
		$this->vl_id  = $virtualLib;
32
		
33
		// Get the current search string
34
		$vlList = self::getVLList($database);
35
		$vlList = array_values($vlList);
36
		$searchStr = $vlList[$virtualLib];
37
		
38
		$this->filter = Filter::parseFilter($searchStr);
39
	}
40
	
41
	/**
42
	 * Returns a SQL query that finds the IDs of all books accepted by the filter.
43
	 *
44
	 * The sql statement return only one column with the name 'id'.
45
	 * This statement can be included into other sql statements in order to apply the filter, e.g. by using inner joins
46
	 * like "select books.* from books inner join ({0}) as filter on books.id = filter.id"
47
	 * @see Filter
48
	 * 
49
	 * @return string an sql query
50
	 */
51
	public function getFilterQuery() {
52
		return $this->filter->toSQLQuery();
53
	}
54
	
55
	/**
56
	 * Get the name of the virtual library.
57
	 * 
58
	 * @return string Name of the virtual library.
59
	 */
60
	public function getName() {
61
		$names = self::getVLNameList($this->db_id);
62
		return $names[$this->vl_id];
63
	}
64
	
65
	/**
66
	 * Get the current VirtualLib object. 
67
	 *  
68
	 * @param int $database The current database id.
69
	 * @param int $virtualLib The current virtual library id.
70
	 * @return VirtualLib The corresponding VirtualLib object.
71
	 */
72
	public static function getVL($database = null, $virtualLib = null) {
73
		if (is_null($database))
74
			$database = getURLParam(DB, 0);
75
		if ( is_null(self::$currentVL) || self::$currentVL->db_id != $database || (self::$currentVL->vl_id != $virtualLib && !is_null($virtualLib))) {
76
			if (is_null($virtualLib))
77
				$virtualLib = GetUrlParam (VL, 0);
78
			self::$currentVL = new VirtualLib($database, $virtualLib);
79
		}
80
		return self::$currentVL;
81
	}
82
	
83
	/**
84
	 * Checks if the support for virtual libraries is enabled in the settings.
85
	 * 
86
	 * @return boolean true, if virtual libraries are enabled.
87
	 */
88
	public static function isVLEnabled() {
89
		global $config;
90
		return ($config['enable_virtual_libraries'] == 1);
91
	}
92
	
93
	/**
94
	 * Gets a list of all virtual libraries in a database.
95
	 * If virtual libraries are disabled, only an empty entry is returned.
96
	 * 
97
	 * @param int $database id of the database
98
	 * @return array An array of virtual libraries with the names as keys and the filter strings as values.  
99
	 */
100
	public static function getVLList($database = NULL) {
101
		// Standard return if virtual libraries are not enabled
102
		if (!self::isVLEnabled())
103
			return array("" => "");
104
		// Load list from Database
105
		$vLibs = json_decode(Base::getCalibreSetting(self::SQL_VL_KEY, $database), true);
106
		// Add "All Books" at the beginning
107
		if (is_null($vLibs))
108
			return array(localize ("allbooks.title") => "");
109
		else
110
			return array_merge(array(localize ("allbooks.title") => ""), $vLibs);
111
	}
112 41
	
113 41
	/**
114 41
	 * Gets a list of all virtual libraries in a database.
115
	 *
116
	 * @param int $database id of the database
117
	 * @return array An array of virtual libraries with the names as keys and the filter strings as values.
118
	 */
119
	public static function getVLNameList($database = NULL) {
120
		return array_keys(self::getVLList($database));
121
	}
122
	
123 22
	/**
124
	 * Combines the database and virtual lib names into a merged name.
125 22
	 * 
126
	 * The resulting name has the form "{$dbName} - {$vlName}". If one of these parameters is empty, the dash will also be removed.
127 22
	 * If the support for virtual libraries is not enabled, this function simply returns the database name.
128 22
	 * 
129
	 * @param string $dbName The database name. If NULL, the name of the current db is taken.
130
	 * @param string $vlName The name of the virtual library. If NULL, the name of the current vl is taken.
131
	 */
132
	public static function getDisplayName($dbName = NULL, $vlName = NULL) {
133
		if (is_null($dbName))
134
			$dbName = Base::getDbName();
135
		if (is_null($vlName))
136
			$vlName = self::getVL()->getName();
137
		if (self::isVLEnabled())
138
			return trim(str_format('{0} - {1}', $dbName, $vlName), ' -');
139 22
		else
140 22
			return $dbName;
141
	}
142
}
143
144
/**
145
 * Abstract classe to store filters internally. It's derived classes represent the different filter types.
146
 *
147
 */
148
abstract class Filter {
149
	// Special settings for known attributes
150
	private static $KNOWN_ATTRIBUTES = array(
151
		"authors"    => array(),
152 1
		"series"     => array("link_join_on" => "series"),
153 1
		"publishers" => array(),
154 1
		"tags"       => array(),
155
		"ratings"    => array("filterColumn" => "rating"),
156 1
		"languages"  => array("filterColumn" => "lang_code", "link_join_on" => "lang_code"),
157
		"formats"    => array("table" => "data", "filterColumn" => "format", "link_table" => "data", "link_join_on" => "id"),
158
	);
159
	
160
	private $isNegated = false;
161
	
162
	/**
163
	 * Creates the attribute settings
164
	 * @param string $attr the name of the attribute, e.g. "tags"
165
	 * @return array an assotiative array with the keys "table", "filterColumn", "link_table", "link_join_on", "bookID".
166
	 */
167
	public static function getAttributeSettings($attr) {
168
		$attr = self::normalizeAttribute($attr);
169
		if (!array_key_exists($attr, self::$KNOWN_ATTRIBUTES))
170
			return null;
171
		return self::$KNOWN_ATTRIBUTES[$attr] + array(
172
			"table"        => $attr,
173
			"filterColumn" => "name",
174
			"link_table"   => "books_" . $attr . "_link",
175
			"link_join_on" => substr($attr, 0, strlen($attr) - 1),
176
			"bookID"       => "book"
177
		);
178
	}
179
	
180
	/**
181
	 * Normalizes the attribute. 
182
	 * 
183
	 * Some attributes can be used in plural (e.g. languages) and singular (e.g. language). This function appends a missing s and puts everything to lower case
184
	 * @param string $attr the attribute, like it was used in calibre
185
	 * @return the normalized attribute name
186
	 */
187
	public static function normalizeAttribute($attr) {
188
		$attr = strtolower($attr);
189
		if (substr($attr, -1) != 's')
190
			$attr .= 's';
191
		return $attr;
192
	}
193
	
194
	/**
195
	 * Gets the from - part of a table, its link-table and a placeholder for the filter
196
	 * 
197
	 * @param string $table a table, e.g. "authors"
198
	 * @return string a from string with a placeholder for the filter query
199
	 */
200
	public static function getLinkedTable($table) {
201
		foreach (array_keys(self::$KNOWN_ATTRIBUTES) as $attr) {
202
			$tabInfo = self::getAttributeSettings($attr);
203
			if ($tabInfo["table"] == $table) {
204
				return str_format_n(
205
						"{table} inner join {link_table} as link on {table}.id = link.{link_join_on} 
206
							inner join ({placeholder}) as filter on filter.id = link.{bookID}", $tabInfo + array("placeholder" => "{0}"));
207
			}
208
		}
209
		return $table;
210
	}
211
	/**
212
	 * Converts the calibre search string into afilter object
213
	 *
214
	 * @param string $searchStr The calibre string
215
	 * @return Filter The internal, array-based representation
216
	 */
217
	public static function parseFilter($searchStr) {
218
		// deal with empty input strings
219
		if (strlen($searchStr) == 0)
220
			return new EmptyFilter();
221
	
222
		// Simple search string pattern. It recognizes search string of the form
223
		//     [attr]:[value]
224
		// and their negation
225
		//     not [attr]:[value]
226
		// where value is either a number, a boolean or a string in double quote.
227
		// In the latter case, the string starts with an operator (= or ~), followed by the search text.
228
		// TODO: deal with more complex search terms that can contain "and", "or" and brackets
229
		$pattern = '#(?P<neg>not)?\s*(?P<attr>\w+):(?P<value>"(?P<op>=|~|\>|<|>=|<=)(?P<text>.*)"|true|false|\d+)#i';
230
		if (!preg_match($pattern, $searchStr, $match)) {
231
			trigger_error("Virtual Library Filter is not supported.", E_USER_WARNING);
232
			return new EmptyFilter();
233
		}
234
	
235
		// Create the actual filter object
236
		$value = $match["value"];
237
		$filter   = null;
0 ignored issues
show
$filter is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
238
		if (substr($value, 0, 1) == '"') {
239
			$filter = new ComparingFilter($match["attr"], $match["text"], $match["op"]);
240
		} elseif (preg_match("#\d+#", $value)) {
241
			$filter = new ComparingFilter($match["attr"], $value);
242
		} else {
243
			$value = (strcasecmp($value, "true") == 0);
244
			$filter = new ExistenceFilter($match["attr"], $value);
245
		}
246
	
247
		// Negate if a leading "not" is given
248
		if (strlen($match["neg"]) > 0)
249
			$filter->negate();
250
	
251
		return $filter;
252
	}
253
	
254
	/**
255
	 * Returns a SQL query that finds the IDs of all books accepted by the filter. The single columns name is id.
256
	 */
257
	public abstract function toSQLQuery();
258
	
259
	/**
260
	 * Negates the current filter. A second call will undo it.
261
	 */
262
	public function negate() {
263
		$this->isNegated = !$this->isNegated;
264
	}
265
	
266
	public function isNegated() {
267
		return $this->isNegated;
268
	}
269
}
270
271
/**
272
 * Class that represents an empty filter
273
 *
274
 */
275
class EmptyFilter extends Filter {
276
	public function __construct() {
277
		// Do Nothing
278
	}
279
	
280
	// Return all books (or no book if the filter is negated)
281
	public function toSQLQuery() {
282
		if ($this->isNegated())
283
			return "select id from books where 1 = 0";
284
		return "select id from books";
285
	}
286
}
287
288
/**
289
 * Class that represents a filter, that compares an attribute with a given value, e.g. tags with "Fiction" 
290
 *
291
 * This class allows for other comparation operators beside "="
292
 */
293
class ComparingFilter extends Filter {
294
	private $attr = null;   // The attribute that is filtered
295
	private $value = null;  // The value with which to compare
296
	private $op = null;     // The operator that is used for comparing
297
	
298
	/**
299
	 * Creates a comparing filter
300
	 * 
301
	 * @param string $attr The attribute that is filtered.
302
	 * @param mixed $value The value with which to compare.
303
	 * @param string $op The operator that is used for comparing, optional.
304
	 */
305
	public function __construct($attr, $value, $op = "=") {
306
		$this->attr = self::normalizeAttribute($attr);
307
		$this->value = $value;
308
		if ($op == "~")
309
			$op = "like";
310
		$this->op = $op;
311
		
312
		// Specialty of ratings
313
		if ($this->attr == "ratings") 
314
			$this->value *= 2;
315
		
316
		// Specialty of languages
317
		// This only works if calibre and cops use the same language!!!
318
		if ($this->attr == "languages")
319
			$this->value = Language::getLanguageCode($this->value);
320
	}
321
	
322
	public function toSQLQuery() {
323
		$queryParams = self::getAttributeSettings($this->attr);
324
		// Do not filter if attribute is not valid
325
		if (is_null($queryParams))
326
			return "select id from books";
327
		
328
		// Include parameters into the sql query 
329
		$queryParams["value"] = $this->value;
330
		$queryParams["op"] = $this->op;
331
		$queryParams["neg"] = $this->isNegated() ? "not" : "";
332
		if ($this->attr == "formats")
333
			$sql = str_format_n(
334
					"select distinct {table}.{bookID} as id ".
335
					"from {table} ".
336
					"where {neg} ({table}.{filterColumn} {op} '{value}')",
337
					$queryParams);
338
		else
339
			$sql = str_format_n(
340
					"select distinct {link_table}.{bookID} as id ".
341
					"from {table} inner join {link_table} on {table}.id = {link_table}.{link_join_on} ".
342
					"where {neg} ({table}.{filterColumn} {op} '{value}')",
343
					$queryParams);
344
		return $sql;
345
	}
346
}
347
348
/**
349
 * Class that represents a filter, that checks if a given attribute exists for a book.
350
 */
351
class ExistenceFilter extends Filter {
352
	private $attr = null;   // The attribute that is filtered
353
354
	/**
355
	 * Creates an existence filter
356
	 *
357
	 * @param string $attr The attribute that is filtered.
358
	 * @param boolean $value True, if objects with that attribute are accepted by the filter, false if not.
359
	 */
360
	public function __construct($attr, $value = true) {
361
		$this->attr = $attr;
362
		
363
		// $value == false is the negation of $value == true 
364
		if (!$value)
365
			$this->negate();
366
	}
367
	
368
	public function toSQLQuery() {
369
		$queryParams = self::getAttributeSettings($this->attr);
370
		// Do not filter if attribute is not valid
371
		if (is_null($queryParams))
372
			return "select id from books";
373
	
374
		// Include parameters into the sql query
375
		$queryParams["op"] = $this->isNegated() ? "==" : ">";
376
		$sql = str_format_n(
377
				"select books.id as id from books left join {link_table} as link on link.{bookID} = books.id group by books.id having count(link.{link_join_on}) {op} 0",
378
				$queryParams);
379
		return $sql;
380
	}
381
}