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 = self::includeBookFilter( |
||
39 | Filter::parseFilter($searchStr) |
||
40 | ); |
||
41 | } |
||
42 | |||
43 | /** |
||
44 | * Includes the booke filter (see $config['cops_books_filter']) |
||
45 | * @param Filter $filter |
||
46 | * @return string |
||
47 | */ |
||
48 | private static function includeBookFilter($filter) { |
||
49 | $bookFilter = getURLParam ("tag", NULL); |
||
50 | if (empty ($bookFilter)) return $filter; |
||
51 | |||
52 | $negated = false; |
||
53 | if (preg_match ("/^!(.*)$/", $bookFilter, $matches)) { |
||
54 | $negated = true; |
||
55 | $bookFilter = $matches[1]; |
||
56 | } |
||
57 | $bookFilter = new ComparingFilter("tags", $bookFilter, "="); |
||
58 | if ($negated) |
||
59 | $bookFilter->negate(); |
||
60 | |||
61 | $result = new CombinationFilter(array($filter, $bookFilter)); |
||
62 | return $result->simplify(); |
||
63 | } |
||
64 | |||
65 | /** |
||
66 | * Returns a SQL query that finds the IDs of all books accepted by the filter. |
||
67 | * |
||
68 | * The sql statement return only one column with the name 'id'. |
||
69 | * This statement can be included into other sql statements in order to apply the filter, e.g. by using inner joins |
||
70 | * like "select books.* from books inner join ({0}) as filter on books.id = filter.id" |
||
71 | * @see Filter |
||
72 | * |
||
73 | * @return string an sql query |
||
74 | */ |
||
75 | public function getFilterQuery() { |
||
76 | return $this->filter->toSQLQuery(); |
||
77 | } |
||
78 | |||
79 | /** |
||
80 | * Get the name of the virtual library. |
||
81 | * |
||
82 | * @return string Name of the virtual library. |
||
83 | */ |
||
84 | public function getName() { |
||
85 | $names = self::getVLNameList($this->db_id); |
||
86 | return $names[$this->vl_id]; |
||
87 | } |
||
88 | |||
89 | /** |
||
90 | * Get the current VirtualLib object. |
||
91 | * |
||
92 | * @param int $database The current database id. |
||
93 | * @param int $virtualLib The current virtual library id. |
||
94 | * @return VirtualLib The corresponding VirtualLib object. |
||
95 | */ |
||
96 | public static function getVL($database = null, $virtualLib = null) { |
||
97 | if (is_null($database)) |
||
98 | $database = getURLParam(DB, 0); |
||
99 | if ( is_null(self::$currentVL) || self::$currentVL->db_id != $database || (self::$currentVL->vl_id != $virtualLib && !is_null($virtualLib))) { |
||
100 | if (is_null($virtualLib)) |
||
101 | $virtualLib = GetUrlParam (VL, 0); |
||
102 | self::$currentVL = new VirtualLib($database, $virtualLib); |
||
103 | } |
||
104 | return self::$currentVL; |
||
105 | } |
||
106 | |||
107 | /** |
||
108 | * Checks if the support for virtual libraries is enabled in the settings. |
||
109 | * |
||
110 | * @return boolean true, if virtual libraries are enabled. |
||
111 | */ |
||
112 | public static function isVLEnabled() { |
||
113 | global $config; |
||
114 | return ($config['enable_virtual_libraries'] == 1); |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * Gets a list of all virtual libraries in a database. |
||
119 | * If virtual libraries are disabled, only an empty entry is returned. |
||
120 | * |
||
121 | * @param int $database id of the database |
||
122 | * @return array An array of virtual libraries with the names as keys and the filter strings as values. |
||
123 | */ |
||
124 | public static function getVLList($database = NULL) { |
||
125 | // Standard return if virtual libraries are not enabled |
||
126 | if (!self::isVLEnabled()) |
||
127 | return array("" => ""); |
||
128 | // Load list from Database |
||
129 | $vLibs = json_decode(Base::getCalibreSetting(self::SQL_VL_KEY, $database), true); |
||
130 | // Add "All Books" at the beginning |
||
131 | if (is_null($vLibs)) |
||
132 | return array(localize ("allbooks.title") => ""); |
||
133 | else |
||
134 | return array_merge(array(localize ("allbooks.title") => ""), $vLibs); |
||
135 | } |
||
136 | |||
137 | /** |
||
138 | * Gets a list of all virtual libraries in a database. |
||
139 | * |
||
140 | * @param int $database id of the database |
||
141 | * @return array An array of virtual libraries with the names as keys and the filter strings as values. |
||
142 | */ |
||
143 | public static function getVLNameList($database = NULL) { |
||
144 | return array_keys(self::getVLList($database)); |
||
145 | } |
||
146 | |||
147 | /** |
||
148 | * Combines the database and virtual lib names into a merged name. |
||
149 | * |
||
150 | * The resulting name has the form "{$dbName} - {$vlName}". If one of these parameters is empty, the dash will also be removed. |
||
151 | * If the support for virtual libraries is not enabled, this function simply returns the database name. |
||
152 | * |
||
153 | * @param string $dbName The database name. If NULL, the name of the current db is taken. |
||
154 | * @param string $vlName The name of the virtual library. If NULL, the name of the current vl is taken. |
||
155 | */ |
||
156 | public static function getDisplayName($dbName = NULL, $vlName = NULL) { |
||
157 | if (is_null($dbName)) |
||
158 | $dbName = Base::getDbName(); |
||
159 | if (is_null($vlName)) |
||
160 | $vlName = self::getVL()->getName(); |
||
161 | if (self::isVLEnabled()) |
||
162 | return trim(str_format('{0} - {1}', $dbName, $vlName), ' -'); |
||
163 | else |
||
164 | return $dbName; |
||
165 | } |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * Abstract classe to store filters internally. It's derived classes represent the different filter types. |
||
170 | * |
||
171 | */ |
||
172 | abstract class Filter { |
||
173 | // Special settings for known attributes |
||
174 | private static $KNOWN_ATTRIBUTES = array( |
||
175 | "authors" => array(), |
||
176 | "series" => array("link_join_on" => "series"), |
||
177 | "publishers" => array(), |
||
178 | "tags" => array(), |
||
179 | "ratings" => array("filterColumn" => "rating"), |
||
180 | "languages" => array("filterColumn" => "lang_code", "link_join_on" => "lang_code"), |
||
181 | "formats" => array("table" => "data", "filterColumn" => "format", "link_table" => "data", "link_join_on" => "id"), |
||
182 | ); |
||
183 | |||
184 | private $isNegated = false; |
||
185 | |||
186 | /** |
||
187 | * Creates the attribute settings |
||
188 | * @param string $attr the name of the attribute, e.g. "tags" |
||
189 | * @return array an assotiative array with the keys "table", "filterColumn", "link_table", "link_join_on", "bookID". |
||
190 | */ |
||
191 | public static function getAttributeSettings($attr) { |
||
192 | $attr = self::normalizeAttribute($attr); |
||
193 | if (!array_key_exists($attr, self::$KNOWN_ATTRIBUTES)) { |
||
194 | if (substr($attr, 0, 1) == "#") { |
||
195 | // search for a custom column |
||
196 | $lookup = substr($attr, 1); |
||
197 | $id = CustomColumn::getCustomId($lookup); |
||
198 | if (is_null($id)) |
||
199 | // custom column unknown |
||
200 | return NULL; |
||
201 | // Read and add custom column information |
||
202 | self::$KNOWN_ATTRIBUTES[$attr] = array( |
||
203 | "table" => CustomColumn::getTableName($id), |
||
204 | "filterColumn" => "value", |
||
205 | "link_table" => CustomColumn::getTableLinkName($id), |
||
206 | "link_join_on" => CustomColumn::getTableLinkColumn($id), |
||
207 | "bookID" => "book" |
||
208 | ); |
||
209 | } else |
||
210 | // invalid attribute name |
||
211 | return NULL; |
||
212 | } |
||
213 | return self::$KNOWN_ATTRIBUTES[$attr] + array( |
||
214 | "table" => $attr, |
||
215 | "filterColumn" => "name", |
||
216 | "link_table" => "books_" . $attr . "_link", |
||
217 | "link_join_on" => substr($attr, 0, strlen($attr) - 1), |
||
218 | "bookID" => "book" |
||
219 | ); |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * Normalizes the attribute. |
||
224 | * |
||
225 | * 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 |
||
226 | * @param string $attr the attribute, like it was used in calibre |
||
227 | * @return the normalized attribute name |
||
228 | */ |
||
229 | public static function normalizeAttribute($attr) { |
||
230 | $attr = strtolower($attr); |
||
231 | if (substr($attr, -1) != 's' && substr($attr, 0, 1) != '#') |
||
232 | $attr .= 's'; |
||
233 | return $attr; |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Gets the from - part of a table, its link-table and a placeholder for the filter |
||
238 | * |
||
239 | * @param string $table a table, e.g. "authors" |
||
240 | * @return string a from string with a placeholder for the filter query |
||
241 | */ |
||
242 | public static function getLinkedTable($table) { |
||
243 | foreach (array_keys(self::$KNOWN_ATTRIBUTES) as $attr) { |
||
244 | $tabInfo = self::getAttributeSettings($attr); |
||
245 | if ($tabInfo["table"] == $table) { |
||
246 | return str_format_n( |
||
247 | "{table} inner join {link_table} as link on {table}.id = link.{link_join_on} |
||
248 | inner join ({placeholder}) as filter on filter.id = link.{bookID}", $tabInfo + array("placeholder" => "{0}")); |
||
249 | } |
||
250 | } |
||
251 | return $table; |
||
252 | } |
||
253 | /** |
||
254 | * Converts the calibre search string into afilter object |
||
255 | * |
||
256 | * @param string $searchStr The calibre string |
||
257 | * @return Filter The internal, array-based representation |
||
258 | */ |
||
259 | public static function parseFilter($searchStr) { |
||
260 | // deal with empty input strings |
||
261 | if (strlen($searchStr) == 0) |
||
262 | return new EmptyFilter(); |
||
263 | |||
264 | // Simple search string pattern. It recognizes search string of the form |
||
265 | // [attr]:[value] |
||
266 | // and their negation |
||
267 | // not [attr]:[value] |
||
268 | // where value is either a number, a boolean or a string in double quote. |
||
269 | // In the latter case, the string starts with an operator (= or ~), followed by the search text. |
||
270 | // TODO: deal with more complex search terms that can contain "and", "or" and brackets |
||
271 | $pattern = '%(?P<neg>not)?\s*(?P<attr>#?\w+):(?P<value>"(?P<op>=|~|\>|<|>=|<=)(?P<text>([^"]|\\\\")*)(?<!\\\\)"|true|false|\d+)%i'; |
||
272 | if (!preg_match($pattern, $searchStr, $match)) { |
||
273 | trigger_error("Virtual Library Filter is not supported.", E_USER_WARNING); |
||
274 | return new EmptyFilter(); |
||
275 | } |
||
276 | |||
277 | // Postprocess escaped quotes |
||
278 | if (array_key_exists("text", $match)) |
||
279 | $match["text"] = str_replace("\\\"", "\"", $match["text"]); |
||
280 | |||
281 | // Create the actual filter object |
||
282 | $value = $match["value"]; |
||
283 | $filter = null; |
||
0 ignored issues
–
show
|
|||
284 | if (substr($value, 0, 1) == '"') { |
||
285 | $filter = new ComparingFilter($match["attr"], $match["text"], $match["op"]); |
||
286 | } elseif (preg_match("#\d+#", $value)) { |
||
287 | $filter = new ComparingFilter($match["attr"], $value); |
||
288 | } else { |
||
289 | $value = (strcasecmp($value, "true") == 0); |
||
290 | $filter = new ExistenceFilter($match["attr"], $value); |
||
291 | } |
||
292 | |||
293 | // Negate if a leading "not" is given |
||
294 | if (strlen($match["neg"]) > 0) |
||
295 | $filter->negate(); |
||
296 | |||
297 | return $filter; |
||
298 | } |
||
299 | |||
300 | /** |
||
301 | * Returns a SQL query that finds the IDs of all books accepted by the filter. The single columns name is id. |
||
302 | */ |
||
303 | public abstract function toSQLQuery(); |
||
304 | |||
305 | /** |
||
306 | * Negates the current filter. A second call will undo it. |
||
307 | */ |
||
308 | public function negate() { |
||
309 | $this->isNegated = !$this->isNegated; |
||
310 | } |
||
311 | |||
312 | public function isNegated() { |
||
313 | return $this->isNegated; |
||
314 | } |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * Class that represents an empty filter |
||
319 | * |
||
320 | */ |
||
321 | class EmptyFilter extends Filter { |
||
322 | public function __construct() { |
||
323 | // Do Nothing |
||
324 | } |
||
325 | |||
326 | // Return all books (or no book if the filter is negated) |
||
327 | public function toSQLQuery() { |
||
328 | if ($this->isNegated()) |
||
329 | return "select id from books where 1 = 0"; |
||
330 | return "select id from books"; |
||
331 | } |
||
332 | } |
||
333 | |||
334 | /** |
||
335 | * Class that represents a filter, that compares an attribute with a given value, e.g. tags with "Fiction" |
||
336 | * |
||
337 | * This class allows for other comparation operators beside "=" |
||
338 | */ |
||
339 | class ComparingFilter extends Filter { |
||
340 | private $attr = null; // The attribute that is filtered |
||
341 | private $value = null; // The value with which to compare |
||
342 | private $op = null; // The operator that is used for comparing |
||
343 | |||
344 | /** |
||
345 | * Creates a comparing filter |
||
346 | * |
||
347 | * @param string $attr The attribute that is filtered. |
||
348 | * @param mixed $value The value with which to compare. |
||
349 | * @param string $op The operator that is used for comparing, optional. |
||
350 | */ |
||
351 | public function __construct($attr, $value, $op = "=") { |
||
352 | $this->attr = self::normalizeAttribute($attr); |
||
353 | $this->value = $value; |
||
354 | if ($op == "~") |
||
355 | $op = "like"; |
||
356 | $this->op = $op; |
||
357 | |||
358 | // Specialty of ratings |
||
359 | if ($this->attr == "ratings") |
||
360 | $this->value *= 2; |
||
361 | |||
362 | // Specialty of languages |
||
363 | // This only works if calibre and cops use the same language!!! |
||
364 | if ($this->attr == "languages") |
||
365 | $this->value = Language::getLanguageCode($this->value); |
||
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["value"] = $this->value; |
||
376 | $queryParams["op"] = $this->op; |
||
377 | $queryParams["neg"] = $this->isNegated() ? "not" : ""; |
||
378 | if ($this->attr == "formats") |
||
379 | $sql = str_format_n( |
||
380 | "select distinct {table}.{bookID} as id ". |
||
381 | "from {table} ". |
||
382 | "where {neg} ({table}.{filterColumn} {op} '{value}')", |
||
383 | $queryParams); |
||
384 | else |
||
385 | $sql = str_format_n( |
||
386 | "select distinct {link_table}.{bookID} as id ". |
||
387 | "from {table} inner join {link_table} on {table}.id = {link_table}.{link_join_on} ". |
||
388 | "where {neg} ({table}.{filterColumn} {op} '{value}')", |
||
389 | $queryParams); |
||
390 | return $sql; |
||
391 | } |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * Class that represents a filter, that checks if a given attribute exists for a book. |
||
396 | */ |
||
397 | class ExistenceFilter extends Filter { |
||
398 | private $attr = null; // The attribute that is filtered |
||
399 | |||
400 | /** |
||
401 | * Creates an existence filter |
||
402 | * |
||
403 | * @param string $attr The attribute that is filtered. |
||
404 | * @param boolean $value True, if objects with that attribute are accepted by the filter, false if not. |
||
405 | */ |
||
406 | public function __construct($attr, $value = true) { |
||
407 | $this->attr = $attr; |
||
408 | |||
409 | // $value == false is the negation of $value == true |
||
410 | if (!$value) |
||
411 | $this->negate(); |
||
412 | } |
||
413 | |||
414 | public function toSQLQuery() { |
||
415 | $queryParams = self::getAttributeSettings($this->attr); |
||
416 | // Do not filter if attribute is not valid |
||
417 | if (is_null($queryParams)) |
||
418 | return "select id from books"; |
||
419 | |||
420 | // Include parameters into the sql query |
||
421 | $queryParams["op"] = $this->isNegated() ? "==" : ">"; |
||
422 | $sql = str_format_n( |
||
423 | "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", |
||
424 | $queryParams); |
||
425 | return $sql; |
||
426 | } |
||
427 | } |
||
428 | |||
429 | /** |
||
430 | * Filter class that represents the combination of two or more filter using and / or |
||
431 | * |
||
432 | */ |
||
433 | class CombinationFilter extends Filter { |
||
434 | private $op = null; |
||
435 | private $parts = array(); |
||
436 | |||
437 | /** |
||
438 | * Constructor that combines multiple filters to one filter |
||
439 | * @param array $parts An array of Filter objects |
||
440 | * @param string $op The operator for combining. Either "and" or "or" (case insensitive). |
||
441 | */ |
||
442 | public function __construct($parts, $op = "and") { |
||
443 | $this->parts = $parts; |
||
444 | $this->op = strtolower($op); |
||
445 | } |
||
446 | |||
447 | /** |
||
448 | * Checks if the combination is a conjunction (=and) |
||
449 | * @return boolean true, iff the filter is a conjunction |
||
450 | */ |
||
451 | public function isConjunction() { |
||
452 | return ($this->op == "and"); |
||
453 | } |
||
454 | |||
455 | /** |
||
456 | * Simplifys the filter, by merging parts that are CombinationFilter objects into this filter, if they have the same operator. |
||
457 | * This means "(x and y) and z" becomes "x and y and z". |
||
458 | * Furthermore, empty filters are removed and if only one part exists, this part is returned. |
||
459 | * |
||
460 | * @return the simplified filter. The result might not be a CombinationFilter |
||
461 | */ |
||
462 | public function simplify() { |
||
463 | $newParts = array(); |
||
464 | foreach ($this->parts as $part) { |
||
465 | // Simplify inner combination filters |
||
466 | if ($part instanceof CombinationFilter) |
||
467 | $part = $part->simplify(); |
||
468 | |||
469 | if ($part instanceof CombinationFilter && $part->isConjunction() == $this->isConjunction()) { |
||
470 | // Part is a CombinationFilter with the same operator --> merge it's parts into this filter |
||
471 | foreach ($part->parts as $innerPart) |
||
472 | array_push($newParts, $innerPart); |
||
473 | } elseif ($part instanceof EmptyFilter) { |
||
474 | // A positiv empty filter can be ignored in a conjunction and makes a disjunction always positiv. |
||
475 | // A negative empty filter can be ignored in a disjunction and makes a conjunction always negative. |
||
476 | if ($part->isNegated() == $this->isConjunction()) |
||
477 | return $part; |
||
478 | } else |
||
479 | array_push($newParts, $part); |
||
480 | } |
||
481 | $this->parts = $newParts; |
||
482 | return $this; |
||
483 | } |
||
484 | |||
485 | public function negate() { |
||
486 | // Use De Morgan's laws to avoid direct negation |
||
487 | |||
488 | // 1. Negate the operator |
||
489 | if ($this->isConjunction()) |
||
490 | $this->op = "or"; |
||
491 | else |
||
492 | $this->op = "and"; |
||
493 | |||
494 | // 2. negate all parts |
||
495 | foreach ($this->parts as $filter) |
||
496 | $filer->negate(); |
||
0 ignored issues
–
show
|
|||
497 | } |
||
498 | |||
499 | public function toSQLQuery() { |
||
500 | $sql = ""; |
||
0 ignored issues
–
show
$sql 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 ![]() |
|||
501 | if ($this->isConjunction()) { |
||
502 | // combining all parts by inner joins implements the "and" logic |
||
503 | $sql = str_format("({0}) as f0", $this->parts[0]->toSQLQuery()); |
||
504 | for ($i = 1; $i < count($this->parts); $i++) |
||
0 ignored issues
–
show
It seems like you are calling the size function
count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}
// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
![]() |
|||
505 | $sql .= str_format(" inner join ({0}) as f{1} on f0.id = f{1}.id", $this->parts[$i]->toSQLQuery(), $i); |
||
506 | } else { |
||
507 | // combining all parts by union implements the "or" logic |
||
508 | $sql = str_format("{0}", $this->parts[0]->toSQLQuery()); |
||
509 | for ($i = 1; $i < count($this->parts); $i++) |
||
0 ignored issues
–
show
It seems like you are calling the size function
count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}
// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
![]() |
|||
510 | $sql .= str_format(" union {0}", $this->parts[$i]->toSQLQuery()); |
||
511 | } |
||
512 | return $sql; |
||
513 | } |
||
514 | } |
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.
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.