diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..e1cc3d5ed
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,22 @@
+.git/
+*.zip
+test/
+# keep the test database in the image here
+!test/BaseWithSomeBooks/
+vendor/
+# remove docker files
+docker/
+docker-compose.yaml
+docker-compose-dev.yaml
+# remove development files
+.vscode/
+.yarn/
+#!.yarn/releases
+#!.yarn/plugins
+.pnp.*
+# remove unused directories
+resources/epub-loader/
+resources/tbszip/stuffs/
+resources/php-epub-meta/assets/
+resources/php-epub-meta/test/
+tools/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..5aa9925c9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+/feedbook/*
+/coverage/*
+/saucetest/*
+/thumbnails/*
+/vendor/
+/clover.xml
+/test/text.atom
+/*.phar
+/cops.zip
+/cops-*.zip
+/cops.sublime-*
+/config_local.php
+/config_local.*.php
+.yarn/*
+#!.yarn/releases
+#!.yarn/plugins
+.pnp.*
+.editorconfig
+.gitattributes
diff --git a/.gitremotes b/.gitremotes
new file mode 100644
index 000000000..f8eb39775
--- /dev/null
+++ b/.gitremotes
@@ -0,0 +1,3 @@
+[remote "upstream"]
+ url = https://github.com/seblucas/cops.git
+ fetch = +refs/heads/*:refs/remotes/upstream/*
diff --git a/.hgignore b/.hgignore
deleted file mode 100644
index 96944414e..000000000
--- a/.hgignore
+++ /dev/null
@@ -1,2 +0,0 @@
-syntax: glob
-feedbook/*
diff --git a/.hgtags b/.hgtags
index 1d0aed5dc..ad6f4b22a 100644
--- a/.hgtags
+++ b/.hgtags
@@ -11,3 +11,19 @@ c5703623704b81dca4228e211830125029cf86a1 0.2.3
5cc3b8ed121d9df57e013e050a75e5602cf2198e 0.3.0
aca483636af460c93f9817e083e85d1976aa1b7d 0.3.1
5888006bc559842de0364ec3e67f641aa1653d0e 0.3.2
+2ff58ed42cecf00b24d981426dff507fa1e86c20 0.3.3
+3cdee8daedf28e6611203ce90c90bb8906003e22 0.3.4
+89ed9654ac9c5de1695f63992aa92d55ef82f2b9 0.4.0
+07e901e7e35334e3747358ac5ba829347696a5d6 0.5.0
+4525a844482c54b57cf394887541f6c483c429d7 0.6.0
+ef69515ccafa912360c35a23e303cb180d7e95b9 0.6.1
+605e2b671bf46530fed4a13b2c5924ef1266b132 0.6.2
+2e7fb43f056b5694720b5eaedc2a9a2b7d2507f6 0.9.0
+3fda691df53aacdec7c017e576d5eefb47a7aa1b 1.0.0RC1
+0fc8e1890d9be461f734bb5f93295a5af497d929 1.0.0RC2
+681dce395a601d04a60ee5505a57b861a234f472 1.0.0RC3
+728d8d2062b27af51e02330bc67633d3ade0101e 1.0.0
+87a31fd7b1de76b704d81d85ac3a8c5c18e8f441 1.0.1
+e61fd2d791c5801b8fa67bb403305287a4ee0a49 1.1.0
+0c7dc20d311f5a60780461c69b0d2e22d175f1b0 1.1.1
+a8934d81cf995c408eef0e6088e925d295775c34 1.1.2
diff --git a/.htaccess b/.htaccess
index 0603a65d0..5074d184c 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,6 +1,8 @@
+DirectoryIndex index.php
+
- XSendFile on
+ XSendFile on
@@ -13,21 +15,72 @@
###########################################
# On WAMP one user had to add this line in his httpd.conf
-# None of the abose was working
+# None of the above was working
###########################################
#XSendFilePath
+###########################################
+# If you want to use user based configuration with
+# apache 2.4 + php-fpm enable this
+# https://github.com/seblucas/cops/issues/213
+###########################################
+#SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+
RewriteEngine on
-RewriteRule ^download/(.*)/.*\.kepub\.epub$ fetch.php?data=$1&type=epub [L]
-RewriteRule ^download/(.*)/.*\.(.*)$ fetch.php?data=$1&type=$2 [L]
+RewriteRule ^download/(\d*)/(\d*)/.*\.kepub\.epub$ fetch.php?data=$1&db=$2&type=epub [L]
+RewriteRule ^view/(\d*)/(\d*)/.*\.kepub\.epub$ fetch.php?data=$1&db=$2&type=epub&view=1 [L]
+RewriteRule ^download/(\d*)/(\d*)/.*\.(.*)$ fetch.php?data=$1&db=$2&type=$3 [L]
+RewriteRule ^view/(\d*)/(\d*)/.*\.(.*)$ fetch.php?data=$1&db=$2&type=$3&view=1 [L]
+RewriteRule ^download/(\d*)/.*\.kepub\.epub$ fetch.php?data=$1&type=epub [L]
+RewriteRule ^view/(\d*)/.*\.kepub\.epub$ fetch.php?data=$1&type=epub&view=1 [L]
+RewriteRule ^download/(\d*)/.*\.(.*)$ fetch.php?data=$1&type=$2 [L]
+RewriteRule ^view/(\d*)/.*\.(.*)$ fetch.php?data=$1&type=$2&view=1 [L]
+
+
+
+ExpiresActive on
+
+# Data
+ExpiresByType text/xml "access plus 0 seconds"
+ExpiresByType application/xml "access plus 0 seconds"
+ExpiresByType application/json "access plus 0 seconds"
+ExpiresByType application/xhtml+xml "access plus 0 seconds"
+
+# Favicon (cannot be renamed)
+ExpiresByType image/x-icon "access plus 1 week"
+
+# Media: images
+ExpiresByType image/png "access plus 1 month"
+ExpiresByType image/jpg "access plus 1 month"
+ExpiresByType image/jpeg "access plus 1 month"
+
+# Webfonts
+ExpiresByType font/truetype "access plus 1 month"
+ExpiresByType font/opentype "access plus 1 month"
+ExpiresByType application/x-font-woff "access plus 1 month"
+ExpiresByType image/svg+xml "access plus 1 month"
+ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
+
+# CSS and JavaScript
+ExpiresByType text/css "access plus 1 year"
+ExpiresByType application/javascript "access plus 1 year"
+ExpiresByType text/javascript "access plus 1 year"
###########################################
# Uncomment if you wish to protect access with a password
###########################################
+# If your covers and books are not available as soon as you protect it
+# You can try replacing the FilesMatch directive by this one
+#
+# it helps for Sony PRS-TX and Aldiko, but beware fetch.php can be accessed
+# without authentication (see $config ['cops_fetch_protect'] for a workaround).
+###########################################
+#
#AuthUserFile /path/to/file
#AuthGroupFile /dev/null
#AuthName "Acces securise"
#AuthType Basic
#Require valid-user
+#
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 000000000..63ac8b5fd
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,142 @@
+filter:
+ excluded_paths:
+ - 'resources/*'
+ - 'test/*'
+ - 'tools/*'
+ dependency_paths:
+ - 'vendor/*'
+
+checks:
+ php:
+ fix_php_opening_tag: true
+ remove_php_closing_tag: true
+ avoid_closing_tag: true
+ one_class_per_file: true
+ side_effects_or_types: true
+ no_mixed_inline_html: false
+ require_braces_around_control_structures: true
+ php5_style_constructor: true
+ no_global_keyword: true
+ avoid_usage_of_logical_operators: true
+ psr2_class_declaration: true
+ no_underscore_prefix_in_properties: false
+ no_underscore_prefix_in_methods: false
+ blank_line_after_namespace_declaration: true
+ single_namespace_per_use: false
+ psr2_switch_declaration: true
+ psr2_control_structure_declaration: false
+ avoid_superglobals: false
+ security_vulnerabilities: true
+ no_exit: false
+ uppercase_constants: true
+ return_doc_comments: true
+ remove_extra_empty_lines: true
+ properties_in_camelcaps: true
+ prefer_while_loop_over_for_loop: true
+ phpunit_assertions: true
+ parameter_doc_comments: true
+ optional_parameters_at_the_end: true
+ no_long_variable_names:
+ maximum: '20'
+ no_goto: true
+ function_in_camel_caps: true
+ fix_use_statements:
+ remove_unused: true
+ preserve_multiple: false
+ preserve_blanklines: false
+ order_alphabetically: true
+ encourage_single_quotes: true
+ encourage_postdec_operator: true
+ classes_in_camel_caps: true
+ avoid_multiple_statements_on_same_line: true
+ avoid_fixme_comments: true
+
+ javascript:
+ valid_typeof: true
+ yoda:
+ setting: 'Disallow Yoda Conditions'
+ wrap_iife: true
+ no_use_before_define: true
+ no_unused_vars: true
+ no_unreachable: true
+ no_undef: true
+ no_trailing_spaces: true
+ no_space_before_semi: true
+ no_shadow: true
+ no_self_compare: true
+ no_script_url: true
+ no_return_assign: true
+ no_reserved_keys: true
+ no_redeclare: true
+ no_mixed_spaces_and_tabs: true
+ no_loop_func: true
+ no_irregular_whitespace: true
+
+coding_style:
+ php:
+ spaces:
+ around_operators:
+ concatenation: true
+ ternary_operator:
+ before_condition: false
+ after_condition: false
+ before_alternative: false
+ after_alternative: false
+ other:
+ after_type_cast: false
+ braces:
+ classes_functions:
+ class: new-line
+ function: new-line
+ closure: end-of-line
+ if:
+ opening: end-of-line
+ for:
+ opening: end-of-line
+ while:
+ opening: end-of-line
+ do_while:
+ opening: end-of-line
+ switch:
+ opening: end-of-line
+ try:
+ opening: end-of-line
+ upper_lower_casing:
+ keywords:
+ general: lower
+ constants:
+ true_false_null: lower
+
+tools:
+ php_analyzer: true
+ php_code_sniffer:
+ config:
+ standard: "PSR2"
+ php_cs_fixer:
+ enabled: true
+ config: { level: psr2 }
+ php_mess_detector: true
+ php_loc:
+ enabled: true
+ excluded_dirs: [vendor]
+ php_code_coverage:
+ test_command: vendor/bin/phpunit
+
+build:
+ environment:
+ timezone: "Europe/Berlin"
+ php: "7.1.12"
+ mysql: false
+ postgresql: false
+ redis: false
+ dependencies:
+ before:
+ - sudo composer selfupdate
+ - composer global require "fxp/composer-asset-plugin:~1.1"
+ tests:
+ override:
+ -
+ command: "vendor/bin/phpunit"
+ coverage:
+ file: "clover.xml"
+ format: "clover"
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..25db5f6f7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,61 @@
+language: php
+dist: trusty
+
+matrix:
+ include:
+ - php: "5.4"
+ - php: "5.5"
+ - php: "5.6"
+ addons:
+ sauce_connect: true
+ hosts:
+ - cops-travis
+ - php: "7.0"
+ - php: "hhvm"
+ allow_failures:
+ - php: "hhvm"
+ - php: "5.5"
+ - php: "5.4"
+
+before_script:
+ - composer selfupdate
+ - composer global require "fxp/composer-asset-plugin:~1.1"
+ - 'if [ "${TRAVIS_PHP_VERSION}" = "5.5" ] || [ "${TRAVIS_PHP_VERSION}" = "5.4" ]; then rm composer.lock && sed -i "/platform/d" composer.json ; fi'
+ - composer install
+ - npm install -g jshint
+ - jshint --version
+script:
+ - vendor/bin/phpunit
+ - jshint --verbose --show-non-errors util.js
+ - php test/coverage-checker.php clover.xml 45
+after_success:
+ - chmod +x test/prepareSauceTest.sh
+ - test/prepareSauceTest.sh
+env:
+ global:
+ - SAUCE_USERNAME=seblucas
+ - secure: VVxocvmz6WYr3tZSTA42M/LUhaHoBWw5onh85hnquoMaxspd3tDCyfQIowTTmEXikRh2T0CkTH7X3dhVwRTd/Ha9isja1qDo9Lc2flGCoWICF7WFZuom084+d+O+EWx4WZMAw4Lz4w6a5xflpPKnzNs9B0+de0BdTlQ5qSXVrcA=
+
+sudo: false
+
+before_deploy:
+ - composer install --no-dev --optimize-autoloader
+ - wget http://www.phing.info/get/phing-latest.phar
+ - php phing-latest.phar
+ - export RELEASE_FILE=$(ls ./cops-*.zip)
+
+deploy:
+ provider: releases
+ api_key:
+ secure: nUQ6uX4+x3VtyrtKjHANl3zLyc0Q9qe2O10ufOFBgcQwWETrE6Gk+Rwn/Kz6OJkg0AGfioptvjh8c28ELV98M+KmhiwhT1VQgs3pa6x9CbggA8fdbGUCq+SC8lnU7MBYIO5gZ36F3RfPwXxXkd09bajiVLbI024IqFSIAONzLXw=
+ skip_cleanup: true
+ file:
+ - "${RELEASE_FILE}"
+ on:
+ tags: true
+ repo: seblucas/cops
+ php: 5.6
+
+cache:
+ directories:
+ - $HOME/.composer/cache
diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index 5ba575123..000000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,118 +0,0 @@
-0.3.2 - 20130303
- * Add dutch translation. Provided by Northguy.
- * Fix an ugly bug introduced in 0.3.1. Reported by mariosipad.
- * Small fixes/enhancement to the update metadata tools :
- * The book's name is Author - Title.epub
- * Add the Calibre uuid so that the book is automatically recognised by Calibre.
- * Update the cover
- * Fix display of the HTML catalog on Kobo's browser.
- * Enable kepub.epub download with cover fix (enable with $config['cops_provide_kepub']).
- * Hopefully fix browsing with PRS-T1. Thanks to Northguy.
- * Hopefully fix the OPDS catalog when the summary is full of HTML crap.
- * Merged 3 patches from Tyler J. Wagner :
- * Detect empty publication date set in Calibre to avoid having (0101) as publication year.
- * Don't print "Languages" if there are none defined.
- * Don't print the tag string if there's no tags.
- * If an OPDS client try to access index.php it will be automatically redirected to feed.php.
- * Move the search & sort tool box to a new line (also fix a w3c error).
-
-
-0.3.1 - 20130127
- * Add Facets to the OPDS catalog (check config item cops_books_filter).
- So far the only OPDS client that support facets are Mantano Reader and Bluefire
- * Fix book sort in some list. Patch provided by Tyler J. Wagner.
- * Update .htaccess to check if Xsendfile is available. Thanks to Gaspine for the patch.
- * Add basic support of custom columns. Check the following config item : cops_calibre_custom_column
- * Usage of X-Accel-Redirect / X-Sendfile is not necessary anymore. Warning all Nginx users
- who wants to still use X-Accel-Redirect must add
- $config['cops_x_accel_redirect'] = "X-Accel-Redirect" in their config_local.php
- * Fix COPS on IIS / Windows. Reported by Kevnancy.
- * Simplified config_default.php
- * Add a new config_local.php.example with the minimal configuration item to change.
-
-
-0.3.0 - 20130106
- * Add a config item to avoid using Fancyapps (pop-ups). Reported by mcister and Northguy.
- * Update documentation of .htaccess. Thanks to Stephane.
- * Add a config item to specify a custom icon. Based on a patch by Tyler J. Wagner.
- * Better handling of content type for book. Reported by Morg.
- * Upped the size of thumbnails for OPDS. They look way better with Mantano.
- * Add language in OPDS feed (shown in Mantano for example).
- * Update metadata on downloaded epub. Disabled by default (check config item cops_update_epub-metadata).
- * New Catalan translation provided by David Ciscar Presas.
- * Add a permalink to books, that way direct link to books can be shared. Reported by mcister and Tyler J. Wagner.
- * Add checkconfig.php that should allow to better detect the configuration problem (page in english only for now).
- * Fix some plural strings / some missing title. Reported by David Ciscar Presas.
- * Add an hint about the OPDS catalog in the HTML catalog.
-
-0.2.3 - 20121205
- * Add a .htaccess to make it easier to use with Apache
- * Fix a typo in book download. Reported by jillmess
- * Update localization (thanks to Calibre2Opds)
- * Add some missing information from Calibre (language, rating for now). Reported by mcister
- * Upgrade Fancybox to 2.1.3
-
-
-0.2.2 - 20121020
- * Changed JQuery URL to https (thanks to Dan Greve for the patch)
- * Added paging to both OPDS and HTML catalog (use new config item cops_max_item_per_page)
- * lots of code refactoring
- * Authors are now splitted by first letter, this is the new default. You can go back to the old way with the config item cops_author_split_first_letter (reported by Northguy)
- * Fix the link to books starting by special characters (reported by vinpel)
- * Upgrade to Fancyapps 2.1.0. I had to adapt the CSS so maybe it'll display better in PRS-T1
- * Add an about box on the HTML catalog which show the current version
-
-0.2.1 - 20120916
- * Fix one last error (hopefully) in link generation (thanks to gaspine)
- * Add Sony PRS-T1 to the list of E-Ink device (thanks to Northguy)
- * Fix another HTML special characters problem (thanks to NeilBryant)
- * Add an ugly config parameter to allow search in non-compliant OPDS reader (thanks to Don Caruana and David Lee)
-
-0.2.0 - 20120722
- * Fix all rewriting rule I forgot to change it in last release
- * Fix in book comment (thanks to jillmess)
- * Fix cover zoom in HTML catalog (you can also navigate through cover with keyboard)
- * Simplify Fancybox transition for e-Ink devices (for now Kobo and Kindle)
-
-0.1.1 - 20120702
- * A lot of bug fixes in HTML catalog
- * Fixed the book comment in OPDS (broken in some rare case)
- * Fixed handling of HTML reserved characters
- * Changed book OPDS id to use an UUID (thanks to ilovejedd for the bug report)
- * Add new config item for the default timezone (thanks to gaspine)
- * Better handling of missing covers
- * Should support every book format supported by Calibre (thanks to Artem)
- * URL rewriting is off by default for the HTML catalog
- * Add some documentation about URL rewriting (thanks to gaspine and Christophe)
- * Tested and ready to use with PHP5.4
-
-0.1.0 - 20120605
- * Add localization support (thanks to Calibre2Opds)
- * Hopefully fixed an issue with & in comment
- * HTML catalog is in the sources with no support (WIP)
-
-0.0.4 - 20120523
- * More code refactoring to simplify code.
- * Changed OPDS Page id to match Calibre2Opds
- * Add icons to author, serie, tags and recent items (there is config item to disable it)
- * Fixed author URL
- * Added publishing date (works on Mantano)
- * Added Tags support
-
-0.0.3 - 20120507
- * Fixed many things blocking opensearch from working
- * There was a bug introduced in 0.0.2
- * The URL can't be relative for Mantano reader, so I added a configuration item.
- * I continued the refactoring to bring HTML to COPS
- * Thumbnails have bigger size (I'll add a configuration item later)
- * Add headers to help caching image and thumbnail to the browser
- *
-
-0.0.2 - 20120411
- * Add support for MOBI and PDF
- * Major refactoring to prepare something nice for the future ;)
- * Add a config item to make use of X-Sendfile instead of X-Accel-Redirect if needed
-
-0.0.1 - 20120302
- * First public release
-
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..c3c74cef6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,350 @@
+# Change Log for COPS
+
+1.3.3 - 20230327 Update npm asset dependencies
+ * Fix link to typeahead.css for bootstrap2 templates
+ * Move simonpioli/sortelements dev-master to resources (last updated in 2012)
+ * Switch from bower-asset/dot 1.1.3 to npm-asset/dot 1.1.3
+ * Switch from bower-asset/jquery 1.12.4 to npm-asset/jquery 1.12.4
+ * Switch from bower-asset/jquery-cookie 1.4.1 to npm-asset/js-cookie 2.2.1
+ * Switch from bower-asset/normalize.css 7.0.0 to npm-asset/normalize.css 8.0.1
+ * Switch from rsms/js-lru dev-v2 to npm-asset/lru-fast 0.2.2
+
+1.3.2 - 20230325 Improve tests and security
+ * Merge branch 'master' of https://github.com/peltos/cops - see @peltos
+
+1.3.1 - 20230325 Update epub-loader resources
+ * Merge commit 'refs/pull/424/head' of https://github.com/seblucas/cops - see seblucas/cops#424 from @marsender
+
+1.3.0 - 20230324 Add bootstrap2 templates
+ * Merge branch 'master' of https://github.com/SenorSmartyPants/cops - see seblucas/cops#497 and earlier from @SenorSmartyPants
+
+1.2.3 - 20230324 Add fixes for PHP 8.2
+
+1.2.2 - 20230324 Update fetch.php to lower memory consumption
+ * Merge commit 'refs/pull/518/head' of https://github.com/seblucas/cops - see seblucas/cops#518 from @allandanton
+
+1.2.1 - 20230321 Add phpstan baseline + fixes
+
+1.2.0 - 20230319 Migration to PHP 8.x
+
+1.1.3 - 20190624
+ * Fixed an error with PHP > 7.2.X where create_function is deprecated, also fixed another error with PHP 7.3.X. Thanks to Turkish for the report.
+ * Fixed the view button when URL Rewriting is enabled.
+ * Fixed an error in epubreader with headers. thanks to worstje for the report.
+ * Added a real logo for COPS. Thanks to horus68 for doing all the work ;).
+ * Added a proper translation for `_CLEAR_` text. Was reported for many year :(.
+ * Added Galician translation. Thanks to Sadrarin Highland.
+ * Added Afrikaan translation. Thanks to PetrusVermaak.
+ * Updated Spanish, Basque, Italian, Dutch, French, Portuguese, Romanian, Russian, Swedish, Turkish. Thanks to the translators and horus68.
+ * Upgraded to latest phpmailer 5.2.27 and bootstrap 3.4.1.
+
+1.1.2 - 20180626
+ * Fixed the download of Kepub with recent firmware from Kobo. Thanks to ospring for the report.
+ * Fixed the cache headers. Thanks to CgX for the fix.
+ * Added Bulgarian, Indonesian, Chinese (China and Taywan) translation. Check Transifex for the authors.
+ * Added an open button to use automatically your prefered reader. Thanks to ttan.
+ * Updated Hungarian, Ukranian, Polish, Spanish and Swedish translations. Check Transifex for the authors.
+ * Updated a lot of documentation and checkconfig.php to better help users.
+ * Upgraded to latest jQuery 1.12.X, Normalise 7.0, PHP Mailer 5.2.26, Font Awesome Free 5.0.13.
+
+1.1.1 - 20170502
+ * Fixed the handling of user specific configuration files. Thanks to marioscube, chadberg for the diagnostic / fix and Neil for the PR.
+ * Changed the cog on the upper right to a magnifying glass icon. Thanks to horus68.
+ * Added test on travis on PHP 5.4.
+ * Added a way to specify the secure SMTP port. Thanks to ubupl.
+
+1.1.0 - 20170402
+ * Upgraded to PHPMailer 5.2.21.
+ * Merged a huge PR that clean most of COPS source code. Thanks to Markus Birth for his work and his patience.
+ * Updated German, Greek, Italian, Polish, Romanian, Russian, Turkish translations. Check Transifex for the authors.
+ * Fixed a bad external dependency in login.html causing problem with HTTPS. Thanks to polytan02.
+ * Fixed minor gui nitpick.
+ * Added automatic redirection to the OPDS feed for many new Android apps (see #309). Thanks to horus68.
+ * Added a configuration item to set the mail subject.
+
+1.0.1 - 20161015
+ * Fixed some type of custom column showing id instead of text - Thanks to Mike Schwörer.
+ * Fixed the redirection to the OPDS catalog for Moon+ Reader.
+ * Fixed the mail character encoding, now in UTF-8.
+ * Fixed checkconfig.php to avoid sending content before headers. Thanks to Luke Stevenson.
+ * Fixed server side rendering with custom columns.
+ * Moved /icons to /images (Apache issues). Thanks to CgX.
+
+1.0.0 - 20160708
+ * Updated the OPDS icons to better looking ones. Thanks to Horus68.
+ * Updated the README.md.
+ * Updated Brazillian, French, Hungarian, Portuguese, Russian translations.
+ * Added support of language and country code. This allow to have proper Brazil Portuguese and Portugal Portuguese.
+ * Added Korean translation. Thanks to Jin, Heonkyu.
+ * Added Romanian translation. Thanks to mtzro2003.
+ * Added Greek translation. Thanks to George Litos.
+ * Added Turkish Translation. Thanks to Yunus Emre Deligöz.
+ * Added Serbian Translation. Thanks to Dalibor Vinkić.
+ * Added the transliteration of search text. You can enable it with $config ['cops_normalized_search']. Thanks to George Litos.
+ * Added Ebookdroid, Chunky and AlReader in the know OPDS clients. Thanks to Mike Ferenduros and Horus68.
+ * Added some mime types for audio books.
+ * Added the rewrite rule for IIS.
+ * Added a now parameter to set the style ($config['cops_style']). Thanks to Pablo Santiago Blum de Aguiar.
+ * Added a directory cache ($config['cops_thumbnail_cache_directory']) to store the resized thumbnails (should help on slow NAS). Thanks to O2 Graphics.
+ * Added support of all kind of custom columns (see configuration file). Thanks to Mike Schwörer.
+ * Fixed COPS so that it's completely embedded (no external resources to download needed anymore).
+ * Fixed a Reflected XSS vulnerability.
+ * Fixed the tag filters with Bootstrap. Thanks to Klaus Broelemann.
+ * Fixed some COPS path errors with reverse proxy. Thanks to Benjamin Kitt.
+ * Fixed the publication date (wasn't working for date before 1901).
+ * Fixed the download file name (replace + by %20 to be RFC compliant).
+
+
+1.0.0RC3 - 20141229
+ * Fixed server side render with Bootstrap template (a proper unit test was also added).
+ * Upgraded to latest doT-php, Typeahead 0.10.5, jquery-cookie 1.4.1, JQuery 1.11.1
+ * Fixed book count with custom columns.
+ * Updated Catalan, Dutch, French and Russian translations.
+ * Added AZW3 to the format that can be sent to Kindle (by mail).
+ * Fixed $config['cops_thumbnail_handling'] with bootstrap template.
+ * Added Hungarian translation. Thanks to harunibn.
+ * Added Ukrainian translation. Thanks to Anatoliy Zavalinich
+ * Added full PHP password check (without any need from specific webserver configuration). Thanks to Mark Bond.
+ * Added new IOS7 style with default template. Thanks to an anonymous source ;).
+ * Fixed display of authors names for books with more than one author.
+ * Added PHP version to checkconfig.php (will help debugging for me).
+ * Added a configuration item ($config['cops_template']) to change the default template. Thanks to Shin.
+ * Added a configuration item ($config['cops_language']) to force COPS language. Thanks to Sandy Pleyte.
+ * Added a trick to have user based configuration, check https://github.com/seblucas/cops/wiki/User-based-config for more information. Thanks to Sandy Pleyte.
+ * Changed the default sort order on books by author page to show books in a series before all other books.
+
+
+1.0.0RC2 - 20140731
+ * Updated Italian, Spanish, Portuguese, Norwegian translations.
+ * Added Polish translation. Thanks to macak_pl.
+ * Added Haitian Creole translation. Thanks to Ian Macdonald & Jacinta.
+ * Added Basque translation. Thanks to Turutarena.
+ * Upgraded to JQuery 1.11.0, Magnific Popup 0.9.9, Normalize 3.0.1, Typeahead 0.10.2
+ * Fixed search with accentuated characters on Internet Explorer.
+ * Author can now be searched by sort or by name (Carroll, Lewis or Lewis Carroll will work).
+ * Added a new bootstrap user interface.
+ * Added correct mimetype for *.ibooks. Reported by Flowney.
+ * Added an empty line at the end of .htaccess to make it easier to modify. Reported by Mariosipad.
+ * Modified the README and checkconfig.php to check for php5-json. Reported by Mariosipad.
+ * Handled properly the cancelling of a mail. Reported by coach0742.
+ * Added an ugly hack to try to fix bad rendering with Kindle. Please report if it's better or not.
+
+1.0.0RC1 - 20140404
+ * Updated English, Spanish, German, Italian, Portuguese, Dutch translation files. Huge thanks to all to the translators.
+ * Added Swedish translation. Thanks to Bo Rosén.
+ * Added Czech translation. Thanks to Zdenek Hadrava.
+ * Added a lot of refactoring to simplify the code.
+ * Added a lot of new unit tests.
+ * Fixed a caching bug causing problems with IE.
+ * Added an embedded Epub Reader based on Monocle. Thanks to all the beta testers.
+ * Cleaned up a lot of stuff to prepare for bootstrap template. Note to all CSS hackers, the stylesheets are now in templates/default/styles.
+ * Fixed the charset of most of the pages. Thanks to edent.
+ * Added a new category : ratings. Thanks to Michael.
+ * Fixed the URL rewriting in the OPDS stream, should fix file naming with FBReader. Reported by Rassie.
+ * Fixed a confusion between author's name and author's sort. Reported by At_Libitum.
+ * Fixed the style of the tag filters to show that they're clickable. Thanks to cycojesus.
+ * Replaced | by space in author name.
+
+0.9.0 - 20131231
+ * Add a lot of unit testing. I hope it will limit the risks of regression.
+ * Added a "smart / autocomplete" search.
+ * Updated the way locales are handled. Should be easier to add new languages.
+ * Fixed display of Cyrillic characters.
+ * Upgraded doT to version 1.0.1, Magnific-Popup to 0.9.8, Normalize.css to 2.1.3, Jquery-cookie to 1.4.0.
+ * Fixed OPDS stream validity. Reported by Didier.
+ * Added a new check in checkconfig.php to detect case problem between the actual path and the path stored in Calibre database. Try checkconfig.php?full=1. Reported by Ruud.
+ * Fixed the display of the rating stars with Chrome. Thanks to At_Libitum.
+ * Added a new parameter ($config['cops_titles_split_first_letter']) to avoid splitting the books by first letter. Thanks to At_Libitum.
+ * Fixed non compliant OPDS search (for Stanza, Moon+ Reader, ...). Reported by At_Libitum.
+ * Fixed the redirection in case the Calibre database is not found. Reported by At_Libitum
+ * Changed .htaccess to allow the use of password protected catalogs with Sony's eReader (PRS-TX). Thanks to Ruud for the beta testing.
+ * Updated Chinese, German, Norwegian, Portuguese, Russian translations. Huge thanks to all the translators.
+ * Fixed a small problem : If a book had no summary the cover could be cut.
+ * Fix COPS on Internet Explorer 9. Reported by At_Libitum.
+ * Added publishers in home categories / search / autocomplete search.
+ * Added a new configuration item ($config ['cops_ignored_categories']) to ignore some categories (author, tag, publisher, ...) in home screen and searches. It's also available in the "Customize UI" page.
+ * Updated .htaccess to allow downloading books with a password protected COPS on a Sony PRS-TX. Reported by Ruud.
+ * Changed the default search to search by categories also (should help with OPDS). Thanks to At_Libitum.
+ * Fixed the tag filtering in the HTML catalog when two tags starts by the same word. Reported by Tyler.
+
+0.6.2 - 20130913
+ * Added server side rendering for devices like PRS-TX / Kindle / Cybook. Thanks to all the testers.
+ * Added a configuration item to tweak how thumbnail are handled.
+ * Fixed the click on cog on IOS. Thanks to sb domo.
+ * Added dashboard icons / standalone mode for IOS. Thanks to sb domo.
+ * Fixed a regression about custom favicon.ico. Thanks to Tyler.
+ * Fixed another regression about COPS's version in the about box. Reported by Ian.
+ * Upgraded Magnific Popup to v0.9.5.
+ * Added a style for IPhone. Thanks to sb domo.
+ * Added Portuguese translation. Thanks to Pablo Aguiar.
+ * Fixed rendering on Internet Explorer < 9.0.
+
+0.6.1 - 20130730
+ * Properly close the lightbox when clicking in a link. Reported by le_.
+ * Fix the book by languages list when the language is not found in the resources. Reported by le_.
+ * Fix the string for Portuguese. Reported by le_.
+ * Add again the series Index in the book list. Reported by fatzgenfatz.
+
+0.6.0 - 20130724
+ * COPS HTML catalog now use templated client side rendering. You can build your own template if you want. Should be a lot faster.
+ * Fancybox has been replaced by Magnific Popup, it seems faster.
+ * Added a way to send book by mail (to send to Kindle or to send to your friends).
+ * Added expires instruction in .htaccess (won't crash if you haven't enabled mod_expires).
+ * Upgrade to JQuery 1.10.2.
+ * Changed the way thumbnails are handled to offer greater visual quality (especially on high pixel density devices : Retina, Nexus, ...).
+ * Changed all icon by a vectorial font (again better visual quality).
+ * Added a way to filter books by tags.
+ * Added a login page (login.html) to allow access to a password protected COPS on a Kobo ereader (that does not support basic auth).
+ * Fixed cookie expiry date.
+ * Added a default web.config for IIS installation.
+ * The eink style doesn't use shadow anymore.
+ * Fixed the link to the series in book detail.
+
+0.5.0 - 20130605
+ * Upgrade COPS UI to HTML5 / CSS3 to hopefully make it prettier. Most of the code was contributed by Thomas Severinsen.
+ * Add the number of books in each databases (when multiple database is enabled).
+ * Add Norwegian Bokmål strings. Thanks to Rune Mathisen for the pull request.
+ * Add a split by language of catalog. Thanks to Puiu Ionut for the pull request.
+ * You can now change the theme and fancybox use on all your devices (You have to enable cookies).
+ * Add an eink theme. Thanks to Gregory Bodin for the code.
+
+0.4.0 - 20130507
+ * Add multiple database support. Check the documentation of $config['calibre_directory'] in config-default.php to see how to enable it.
+ * Include jquery library in COPS's repository to be sure that COPS will work on LAN (without Internet access).
+ * Prepare the switch to HTML5. Thanks to Thomas Severinsen for most of the code.
+ * Update the locale strings to be more strict with plurals. Thanks to Tobias Ausländer for the code.
+ * If Fancybox is not enabled ($config['cops_use_fancyapps'] = "0") then it's not used at all (even in the about box).
+ * Fix book comments if it contains UTF8 characters. Reported by Alain.
+ * Link to the book permalink was not working correctly in some cases. Reported by celta.
+ * Moved some external resources to a resources directory.
+ * Add chinese translation. Thanks to wogong for the pull request.
+
+0.3.4 - 20130327
+ * Hopefully fix metadata update. Beware you should remove the directory php-epub-meta if you have one. Thanks to Mario for his time.
+ * Fix two warnings. Reported by Goner and Mario.
+
+0.3.3 - 20130323
+ * Fix catalog if book summary contains bad HTML again :(.
+ * Upgrade to Fancybox 2.4.0 and JQuery 1.9.1.
+ * Search is now dependant on the page you're in. For now if you're on author page it'll look for author name.
+ * Update checkconfig to check if the database provided comes from Calibre.
+ * Update to latest php-epub-meta should fix the metadata update with Epub.
+ * Fix OPDS catalog with Ibis Reader. It didn't like empty language.
+
+0.3.2 - 20130303
+ * Add dutch translation. Provided by Northguy.
+ * Fix an ugly bug introduced in 0.3.1. Reported by mariosipad.
+ * Small fixes/enhancement to the update metadata tools :
+ * The book's name is Author - Title.epub
+ * Add the Calibre uuid so that the book is automatically recognised by Calibre.
+ * Update the cover
+ * Fix display of the HTML catalog on Kobo's browser.
+ * Enable kepub.epub download with cover fix (enable with $config['cops_provide_kepub']).
+ * Hopefully fix browsing with PRS-T1. Thanks to Northguy.
+ * Hopefully fix the OPDS catalog when the summary is full of HTML crap.
+ * Merged 3 patches from Tyler J. Wagner :
+ * Detect empty publication date set in Calibre to avoid having (0101) as publication year.
+ * Don't print "Languages" if there are none defined.
+ * Don't print the tag string if there's no tags.
+ * If an OPDS client try to access index.php it will be automatically redirected to feed.php.
+ * Move the search & sort tool box to a new line (also fix a w3c error).
+
+
+0.3.1 - 20130127
+ * Add Facets to the OPDS catalog (check config item cops_books_filter).
+ So far the only OPDS client that support facets are Mantano Reader and Bluefire
+ * Fix book sort in some list. Patch provided by Tyler J. Wagner.
+ * Update .htaccess to check if Xsendfile is available. Thanks to Gaspine for the patch.
+ * Add basic support of custom columns. Check the following config item : cops_calibre_custom_column
+ * Usage of X-Accel-Redirect / X-Sendfile is not necessary anymore. Warning all Nginx users
+ who wants to still use X-Accel-Redirect must add
+ $config['cops_x_accel_redirect'] = "X-Accel-Redirect" in their config_local.php
+ * Fix COPS on IIS / Windows. Reported by Kevnancy.
+ * Simplified config_default.php
+ * Add a new config_local.php.example with the minimal configuration item to change.
+
+
+0.3.0 - 20130106
+ * Add a config item to avoid using Fancyapps (pop-ups). Reported by mcister and Northguy.
+ * Update documentation of .htaccess. Thanks to Stephane.
+ * Add a config item to specify a custom icon. Based on a patch by Tyler J. Wagner.
+ * Better handling of content type for book. Reported by Morg.
+ * Upped the size of thumbnails for OPDS. They look way better with Mantano.
+ * Add language in OPDS feed (shown in Mantano for example).
+ * Update metadata on downloaded epub. Disabled by default (check config item cops_update_epub-metadata).
+ * New Catalan translation provided by David Ciscar Presas.
+ * Add a permalink to books, that way direct link to books can be shared. Reported by mcister and Tyler J. Wagner.
+ * Add checkconfig.php that should allow to better detect the configuration problem (page in english only for now).
+ * Fix some plural strings / some missing title. Reported by David Ciscar Presas.
+ * Add an hint about the OPDS catalog in the HTML catalog.
+
+0.2.3 - 20121205
+ * Add a .htaccess to make it easier to use with Apache
+ * Fix a typo in book download. Reported by jillmess
+ * Update localization (thanks to Calibre2Opds)
+ * Add some missing information from Calibre (language, rating for now). Reported by mcister
+ * Upgrade Fancybox to 2.1.3
+
+
+0.2.2 - 20121020
+ * Changed JQuery URL to https (thanks to Dan Greve for the patch)
+ * Added paging to both OPDS and HTML catalog (use new config item cops_max_item_per_page)
+ * lots of code refactoring
+ * Authors are now splitted by first letter, this is the new default. You can go back to the old way with the config item cops_author_split_first_letter (reported by Northguy)
+ * Fix the link to books starting by special characters (reported by vinpel)
+ * Upgrade to Fancyapps 2.1.0. I had to adapt the CSS so maybe it'll display better in PRS-T1
+ * Add an about box on the HTML catalog which show the current version
+
+0.2.1 - 20120916
+ * Fix one last error (hopefully) in link generation (thanks to gaspine)
+ * Add Sony PRS-T1 to the list of E-Ink device (thanks to Northguy)
+ * Fix another HTML special characters problem (thanks to NeilBryant)
+ * Add an ugly config parameter to allow search in non-compliant OPDS reader (thanks to Don Caruana and David Lee)
+
+0.2.0 - 20120722
+ * Fix all rewriting rule I forgot to change it in last release
+ * Fix in book comment (thanks to jillmess)
+ * Fix cover zoom in HTML catalog (you can also navigate through cover with keyboard)
+ * Simplify Fancybox transition for e-Ink devices (for now Kobo and Kindle)
+
+0.1.1 - 20120702
+ * A lot of bug fixes in HTML catalog
+ * Fixed the book comment in OPDS (broken in some rare case)
+ * Fixed handling of HTML reserved characters
+ * Changed book OPDS id to use an UUID (thanks to ilovejedd for the bug report)
+ * Add new config item for the default timezone (thanks to gaspine)
+ * Better handling of missing covers
+ * Should support every book format supported by Calibre (thanks to Artem)
+ * URL rewriting is off by default for the HTML catalog
+ * Add some documentation about URL rewriting (thanks to gaspine and Christophe)
+ * Tested and ready to use with PHP5.4
+
+0.1.0 - 20120605
+ * Add localization support (thanks to Calibre2Opds)
+ * Hopefully fixed an issue with & in comment
+ * HTML catalog is in the sources with no support (WIP)
+
+0.0.4 - 20120523
+ * More code refactoring to simplify code.
+ * Changed OPDS Page id to match Calibre2Opds
+ * Add icons to author, serie, tags and recent items (there is config item to disable it)
+ * Fixed author URL
+ * Added publishing date (works on Mantano)
+ * Added Tags support
+
+0.0.3 - 20120507
+ * Fixed many things blocking opensearch from working
+ * There was a bug introduced in 0.0.2
+ * The URL can't be relative for Mantano reader, so I added a configuration item.
+ * I continued the refactoring to bring HTML to COPS
+ * Thumbnails have bigger size (I'll add a configuration item later)
+ * Add headers to help caching image and thumbnail to the browser
+ *
+
+0.0.2 - 20120411
+ * Add support for MOBI and PDF
+ * Major refactoring to prepare something nice for the future ;)
+ * Add a config item to make use of X-Sendfile instead of X-Accel-Redirect if needed
+
+0.0.1 - 20120302
+ * First public release
diff --git a/OPDS_renderer.php b/OPDS_renderer.php
deleted file mode 100644
index a8da8a3b5..000000000
--- a/OPDS_renderer.php
+++ /dev/null
@@ -1,260 +0,0 @@
-
- */
-
-require_once ("base.php");
-
-class OPDSRenderer
-{
- const PAGE_OPENSEARCH = "8";
- const PAGE_OPENSEARCH_QUERY = "9";
-
- private $xmlStream = NULL;
- private $updated = NULL;
-
- private function getUpdatedTime () {
- if (is_null ($this->updated)) {
- $this->updated = time();
- }
- return date (DATE_ATOM, $this->updated);
- }
-
- private function getXmlStream () {
- if (is_null ($this->xmlStream)) {
- $this->xmlStream = new XMLWriter();
- $this->xmlStream->openMemory();
- $this->xmlStream->setIndent (true);
- }
- return $this->xmlStream;
- }
-
- public function getOpenSearch () {
- global $config;
- $xml = new XMLWriter ();
- $xml->openMemory ();
- $xml->setIndent (true);
- $xml->startDocument('1.0','UTF-8');
- $xml->startElement ("OpenSearchDescription");
- $xml->writeAttribute ("xmlns", "http://a9.com/-/spec/opensearch/1.1/");
- $xml->startElement ("ShortName");
- $xml->text ("My catalog");
- $xml->endElement ();
- $xml->startElement ("Description");
- $xml->text ("Search for ebooks");
- $xml->endElement ();
- $xml->startElement ("InputEncoding");
- $xml->text ("UTF-8");
- $xml->endElement ();
- $xml->startElement ("OutputEncoding");
- $xml->text ("UTF-8");
- $xml->endElement ();
- $xml->startElement ("Image");
- $xml->writeAttribute ("type", "image/x-icon");
- $xml->writeAttribute ("width", "16");
- $xml->writeAttribute ("height", "16");
- $xml->text ($config['cops_icon']);
- $xml->endElement ();
- $xml->startElement ("Url");
- $xml->writeAttribute ("type", 'application/atom+xml');
- $xml->writeAttribute ("template", $config['cops_full_url'] . 'feed.php?query={searchTerms}');
- $xml->endElement ();
- $xml->startElement ("Query");
- $xml->writeAttribute ("role", "example");
- $xml->writeAttribute ("searchTerms", "robot");
- $xml->endElement ();
- $xml->endElement ();
- $xml->endDocument();
- return $xml->outputMemory(true);
- }
-
- private function startXmlDocument ($page) {
- global $config;
- self::getXmlStream ()->startDocument('1.0','UTF-8');
- self::getXmlStream ()->startElement ("feed");
- self::getXmlStream ()->writeAttribute ("xmlns", "http://www.w3.org/2005/Atom");
- self::getXmlStream ()->writeAttribute ("xmlns:xhtml", "http://www.w3.org/1999/xhtml");
- self::getXmlStream ()->writeAttribute ("xmlns:opds", "http://opds-spec.org/2010/catalog");
- self::getXmlStream ()->writeAttribute ("xmlns:opensearch", "http://a9.com/-/spec/opensearch/1.1/");
- self::getXmlStream ()->writeAttribute ("xmlns:dcterms", "http://purl.org/dc/terms/");
- self::getXmlStream ()->startElement ("title");
- self::getXmlStream ()->text ($page->title);
- self::getXmlStream ()->endElement ();
- if ($page->subtitle != "")
- {
- self::getXmlStream ()->startElement ("subtitle");
- self::getXmlStream ()->text ($page->subtitle);
- self::getXmlStream ()->endElement ();
- }
- self::getXmlStream ()->startElement ("id");
- if ($page->idPage)
- {
- self::getXmlStream ()->text ($page->idPage);
- }
- else
- {
- self::getXmlStream ()->text ($_SERVER['REQUEST_URI']);
- }
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("updated");
- self::getXmlStream ()->text (self::getUpdatedTime ());
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("icon");
- self::getXmlStream ()->text ($page->favicon);
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("author");
- self::getXmlStream ()->startElement ("name");
- self::getXmlStream ()->text (utf8_encode ("Sbastien Lucas"));
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("uri");
- self::getXmlStream ()->text ("http://blog.slucas.fr");
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("email");
- self::getXmlStream ()->text ("sebastien@slucas.fr");
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->endElement ();
- $link = new LinkNavigation ("", "start", "Home");
- self::renderLink ($link);
- $link = new LinkNavigation ("?" . $_SERVER['QUERY_STRING'], "self");
- self::renderLink ($link);
- if ($config['cops_generate_invalid_opds_stream'] == 0 || preg_match("/(MantanoReader|FBReader)/", $_SERVER['HTTP_USER_AGENT'])) {
- // Good and compliant way of handling search
- $link = new Link ("feed.php?page=" . self::PAGE_OPENSEARCH, "application/opensearchdescription+xml", "search", "Search here");
- }
- else
- {
- // Bad way, will be removed when OPDS client are fixed
- $link = new Link ($config['cops_full_url'] . 'feed.php?query={searchTerms}', "application/atom+xml", "search", "Search here");
- }
- self::renderLink ($link);
- if ($page->containsBook () && !is_null ($config['cops_books_filter']) && count ($config['cops_books_filter']) > 0) {
- $Urlfilter = getURLParam ("tag", "");
- foreach ($config['cops_books_filter'] as $lib => $filter) {
- $link = new LinkFacet ("?" . addURLParameter ($_SERVER['QUERY_STRING'], "tag", $filter), $lib, localize ("tagword.title"), $filter == $Urlfilter);
- self::renderLink ($link);
- }
- }
- }
-
- private function endXmlDocument () {
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->endDocument ();
- return self::getXmlStream ()->outputMemory(true);
- }
-
- private function renderLink ($link) {
- self::getXmlStream ()->startElement ("link");
- self::getXmlStream ()->writeAttribute ("href", $link->href);
- self::getXmlStream ()->writeAttribute ("type", $link->type);
- if (!is_null ($link->rel)) {
- self::getXmlStream ()->writeAttribute ("rel", $link->rel);
- }
- if (!is_null ($link->title)) {
- self::getXmlStream ()->writeAttribute ("title", $link->title);
- }
- if (!is_null ($link->facetGroup)) {
- self::getXmlStream ()->writeAttribute ("opds:facetGroup", $link->facetGroup);
- }
- if ($link->activeFacet) {
- self::getXmlStream ()->writeAttribute ("opds:activeFacet", "true");
- }
- self::getXmlStream ()->endElement ();
- }
-
-
- private function renderEntry ($entry) {
- self::getXmlStream ()->startElement ("title");
- self::getXmlStream ()->text ($entry->title);
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("updated");
- self::getXmlStream ()->text (self::getUpdatedTime ());
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("id");
- self::getXmlStream ()->text ($entry->id);
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("content");
- self::getXmlStream ()->writeAttribute ("type", $entry->contentType);
- if ($entry->contentType == "text") {
- self::getXmlStream ()->text ($entry->content);
- } else {
- self::getXmlStream ()->writeRaw ($entry->content);
- }
- self::getXmlStream ()->endElement ();
- foreach ($entry->linkArray as $link) {
- self::renderLink ($link);
- }
-
- if (get_class ($entry) != "EntryBook") {
- return;
- }
-
- foreach ($entry->book->getAuthors () as $author) {
- self::getXmlStream ()->startElement ("author");
- self::getXmlStream ()->startElement ("name");
- self::getXmlStream ()->text ($author->name);
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("uri");
- self::getXmlStream ()->text ("feed.php" . $author->getUri ());
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->endElement ();
- }
- foreach ($entry->book->getTags () as $category) {
- self::getXmlStream ()->startElement ("category");
- self::getXmlStream ()->writeAttribute ("term", $category->name);
- self::getXmlStream ()->writeAttribute ("label", $category->name);
- self::getXmlStream ()->endElement ();
- }
- if ($entry->book->getPubDate () != "") {
- self::getXmlStream ()->startElement ("dcterms:issued");
- self::getXmlStream ()->text (date ("Y-m-d", $entry->book->pubdate));
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("published");
- self::getXmlStream ()->text (date ("Y-m-d", $entry->book->pubdate) . "T08:08:08Z");
- self::getXmlStream ()->endElement ();
- }
-
- $lang = $entry->book->getLanguages ();
- if (!is_null ($lang)) {
- self::getXmlStream ()->startElement ("dcterms:language");
- self::getXmlStream ()->text ($lang);
- self::getXmlStream ()->endElement ();
- }
-
- }
-
- public function render ($page) {
- global $config;
- self::startXmlDocument ($page);
- if ($page->isPaginated ())
- {
- self::getXmlStream ()->startElement ("opensearch:totalResults");
- self::getXmlStream ()->text ($page->totalNumber);
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("opensearch:itemsPerPage");
- self::getXmlStream ()->text ($config['cops_max_item_per_page']);
- self::getXmlStream ()->endElement ();
- self::getXmlStream ()->startElement ("opensearch:startIndex");
- self::getXmlStream ()->text (($page->n - 1) * $config['cops_max_item_per_page'] + 1);
- self::getXmlStream ()->endElement ();
- $prevLink = $page->getPrevLink ();
- $nextLink = $page->getNextLink ();
- if (!is_null ($prevLink)) {
- self::renderLink ($prevLink);
- }
- if (!is_null ($nextLink)) {
- self::renderLink ($nextLink);
- }
- }
- foreach ($page->entryArray as $entry) {
- self::getXmlStream ()->startElement ("entry");
- self::renderEntry ($entry);
- self::getXmlStream ()->endElement ();
- }
- return self::endXmlDocument ();
- }
-}
-
-?>
diff --git a/README b/README
deleted file mode 100644
index ef898c755..000000000
--- a/README
+++ /dev/null
@@ -1,86 +0,0 @@
-= COPS =
-
-COPS stands for Calibre OPDS (and HTML) Php Server.
-
-COPS output is valid the unofficial OPDS validator :
-http://opds-validator.appspot.com/
-
-= Why ? =
-
-In my opinion Calibre is a marvelous tool but is too big and has too much
-dependencies to be used for its content server.
-
-That's the main reason why I coded this OPDS server. I needed a simple
-tool to be installed on a small server (Seagate Dockstar in my case).
-
-I initially thought of Calibre2OPDS but as it generate static file no
-search was possible.
-
-Later I added an simple HTML catalog that should be usable on my Kobo.
-
-So COPS's main advantages are :
- * No need for many dependencies.
- * No need for a lot of CPU or RAM.
- * Not much code.
- * Search is available.
- * With Dropbox / owncloud it's very easy to have an up to date OPDS server.
- * It was fun to code.
-
-If you want to use the OPDS feed don't forget to specify feed.php at the end of your URL.
-
-= Prerequisites =
-
-1. PHP 5.3 or 5.4 with GD image processing & SQLite3 support.
-2. A web server with PHP support. I only tested with various version of Nginx.
- Other people reported it working with Apache and Cherokee.
-3. The path to a calibre library (metadata.db, format, & cover files).
-
-On any Debian base Linux you can use :
- aptitude install php5-gd php5-sqlite
-
-= Install =
-
-1. Extract the zip file to a folder in web space (visible to the web server).
-2. If a first-time install, copy config_local.php.example to config_local.php
-3. Edit config_local.php to match your config.
-4. If needed add other configuration item from config_default.php
-
-If you choose to put your Calibre directory inside your web directory then you
-will have to edit /etc/nginx/mime.types to add this line :
-application/epub+zip epub;
-
-= Known problems =
-
-Not a lot ;)
-
-Please see https://github.com/seblucas/cops/issues for open issues
-
-= Need help =
-
-Please read https://github.com/seblucas/cops/wiki
-
-= Disclaimer =
-
-It's tested by me and many other users but there's still some little bugs around ;)
-
-= Credits =
-
- * All localization informations come from Calibre2OPDS (http://calibre2opds.com/)
- * Locale message handling is inspired of http://www.mind-it.info/2010/02/22/a-simple-approach-to-localization-in-php/
- * str_format function come from http://tmont.com/blargh/2010/1/string-format-in-php
- * All icons come from the package Web0 by naf1971 : http://naf1971.deviantart.com/art/Web0-182067054
- * Thanks to all testers
-
-External libraries used :
- * JQuery : http://jquery.com/
- * Fancyapps : http://fancyapps.com/fancybox/
- * Php-epub-meta : https://github.com/splitbrain/php-epub-meta with some modification by me
- https://github.com/seblucas/php-epub-meta
- * TbsZip : http://www.tinybutstrong.com/apps/tbszip/tbszip_help.html
-
-= Copyright & License =
-
-COPS - 2012-2013 (c) Sbastien Lucas
-
-See COPYING and file headers for license info
-
diff --git a/README.md b/README.md
index 8f30b32ec..0afb197e9 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,142 @@
-cops
-====
+# COPS
-Calibre OPDS (and HTML) PHP Server : light alternative to Calibre content server / Calibre2OPDS
+COPS stands for Calibre OPDS (and HTML) Php Server.
+
+See : [COPS's home](http://blog.slucas.fr/en/oss/calibre-opds-php-server) for more details.
+
+Don't forget to check the [Wiki](https://github.com/seblucas/cops/wiki).
+
+[](https://scrutinizer-ci.com/g/seblucas/cops/?branch=master)
+
+[](https://scrutinizer-ci.com/g/seblucas/cops/?branch=master)
+
+[](https://scrutinizer-ci.com/g/seblucas/cops/build-status/master)
+
+[](https://travis-ci.org/seblucas/cops)
+
+[](https://saucelabs.com/u/seblucas)
+
+# Why ?
+
+In my opinion Calibre is a marvelous tool but is too big and has too much
+dependencies to be used for its content server.
+
+That's the main reason why I coded this OPDS server. I needed a simple
+tool to be installed on a small server (Seagate Dockstar in my case).
+
+I initially thought of Calibre2OPDS but as it generate static file no
+search was possible.
+
+Later I added an simple HTML catalog that should be usable on my Kobo.
+
+So COPS's main advantages are :
+ * No need for many dependencies.
+ * No need for a lot of CPU or RAM.
+ * Not much code.
+ * Search is available.
+ * It was fun to code.
+
+If you want to use the OPDS feed don't forget to specify feed.php at the end of your URL.
+
+You just have to sync your Calibre directory to your COPS server the way you prefer (Dropbox, Bt Sync, Syncthing, use a directory shared with Nextcloud, ...).
+
+# Prerequisites (this fork)
+1. PHP 7.4 or 8.X with GD image processing, Libxml, Intl, Json & SQLite3 support (PHP 8.1 or later recommended).
+
+See [CHANGELOG](CHANGELOG.md) for changes compared to upstream repository https://github.com/seblucas/cops from @seblucas
+
+# Prerequisites
+
+1. PHP 5.3, 5.4, 5.5, 5.6, 7.X or hhvm with GD image processing, Libxml, Intl, Json & SQLite3 support (PHP 5.6 or later recommended).
+2. A web server with PHP support. I tested with various version of Nginx and Apache.
+ Other people reported it working with Apache and Cherokee. You can also use PHP embedded server (https://github.com/seblucas/cops/wiki/Howto---PhpEmbeddedServer)
+3. The path to a calibre library (metadata.db, format, & cover files).
+
+On any Debian based Linux you can use :
+ `apt-get install php5-gd php5-sqlite php5-json php5-intl`
+
+If you use Debian Stretch :
+ `apt-get install php7.0-gd php7.0-sqlite3 php7.0-json php7.0-intl php7.0-xml php7.0-mbstring php7.0-zip`
+
+On Centos you may have to add :
+ yum install php-xml
+
+# Install a release (Easiest way)
+
+1. Extract the zip file you got from [the release page](https://github.com/seblucas/cops/releases) to a folder in web space (visible to the web server).
+2. If you're doing a first-time install, copy config_local.php.example to config_local.php
+3. Edit config_local.php to match your config.
+4. If needed add other configuration item from config_default.php
+
+If you like Docker, you can also try this multiarch docker container from [linuxserver.io](https://hub.docker.com/r/linuxserver/cops/) It has builds for x64, armhf and arm64.
+
+# Install from sources
+
+```bash
+git clone https://github.com/seblucas/cops.git # or download lastest zip see below
+cd cops
+wget https://getcomposer.org/composer.phar
+php composer.phar global require "fxp/composer-asset-plugin:~1.1"
+php composer.phar install --no-dev --optimize-autoloader
+```
+
+After that you can use the previous how-to starting at the second step.
+
+Note that instead of cloning you can also get [latest master as zip](https://github.com/seblucas/cops/archive/master.zip)
+
+Note that if your PHP version is lower that 5.6, then you may have to remove `composer.lock` before starting the last line.
+
+# Where to put my Calibre directory ?
+
+Long story short : ALWAYS outside of COPS's directory especially if COPS is installed on a VPS / Server. If you follow my advice then your data will be safe.
+
+If you choose to put your Calibre directory inside your web directory and use Nginx then you will have to edit /etc/nginx/mime.types to add these lines :
+
+```
+application/epub+zip epub;
+application/x-mobipocket-ebook mobi prc azw;
+```
+
+# Known problems
+
+Not a lot, except for the bad quality of the code (first PHP project ever) ;)
+
+Please see https://github.com/seblucas/cops/issues for open issues
+
+# Need help
+
+Please read https://github.com/seblucas/cops/wiki and check the FAQ.
+
+# Contributing
+
+As you could see [here](https://github.com/seblucas/cops/graphs/contributors), I appreciate every contributions and there were a lot over time. So don't be shy and submit your Pull Requests.
+
+Note to translators : please prefer using [Transifex](https://github.com/seblucas/cops/wiki/Update-translations) instead of doing a PR.
+
+I only have one limit (I may have more but that one is the worse) : COPS' goal is to provide an alternative to Calibre's content server and not to replace Calibre entirely. So I will refuse any PR making changes to the database content.
+
+# Credits
+
+ * Locale message handling is inspired of https://www.mind-it.info/2010/02/22/a-simple-approach-to-localization-in-php
+ * str_format function come from http://tmont.com/blargh/2010/1/string-format-in-php
+ * All icons come from Font Awesome : https://github.com/FortAwesome/Font-Awesome
+ * The unofficial OPDS validator : http://opds-validator.appspot.com/
+ * Thanks to all testers, translators and contributors.
+ * Feed icons made by Freepik from Flaticon website licensed under Creative Commons BY 3.0 http://www.flaticon.com and http://www.freepik.com
+ * A huge thanks to Jetbrains for supporting COPS by providing a set of free licenses to their products for several years now!
+
+External libraries used :
+ * JQuery : http://jquery.com/
+ * Magnific Popup : http://dimsemenov.com/plugins/magnific-popup/
+ * Php-epub-meta : https://github.com/splitbrain/php-epub-meta with some modification by me (https://github.com/seblucas/php-epub-meta)
+ * TbsZip : http://www.tinybutstrong.com/apps/tbszip/tbszip_help.html
+ * DoT.js : http://olado.github.io/doT/index.html
+ * PHPMailer : https://github.com/PHPMailer/PHPMailer
+ * js-lru : https://github.com/rsms/js-lru
+
+# Copyright & License
+
+COPS - 2012-2019 (c) Sébastien Lucas
+
+See COPYING and file headers for license info
-See : http://blog.slucas.fr/en/oss/calibre-opds-php-server
diff --git a/about.html b/about.html
index 2379a8702..19029c553 100644
--- a/about.html
+++ b/about.html
@@ -1,13 +1,26 @@
-
Authors
-
COPS is developped and maintained by Sébastien Lucas.
+
+
About COPS
+
Authors
+
COPS is developed and maintained by Sébastien Lucas.
-
See full history on Github to check all authors.
-COPS use some external librairies, check README for the details.
COPS use some external libraries, check README for the details.
Copyright
-
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.
-The complete content of license is provided in file COPYING within distribution and also available online.
+
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.
+
The complete content of license is provided in file COPYING within distribution and also available online.
DISCLAIMER : COPS is an open source software free to install everywhere. So if you have questions about any books available with any installation of COPS, please ask the owner of the website and not COPS's maintainer.
Note that hosting your Calibre Library in /home is almost impossible due to access rights restriction
+';
+ }
+ ?>
+
+
+
+
+
Check if Calibre database file can be opened with PHP
+
+
+
+
+
+
Check if Calibre database file contains at least some of the needed tables
+
+ query('select count(*) FROM sqlite_master WHERE type="table" AND name in ("books", "authors", "tags", "series")')->fetchColumn();
+ if ($count == 4) {
+ echo $name . ' OK';
+ } else {
+ echo $name . ' Not all Calibre tables were found. Are you sure you\'re using the correct database.';
}
- ?>
-
-
-
-
Check if Calibre database file can be opened with PHP
-
-
+
+
+
+
+
Check if all Calibre books are found
+
+ prepare('select books.path || "/" || data.name || "." || lower (format) as fullpath from data join books on data.book = books.id');
+ $result->execute();
+ while ($post = $result->fetchObject()) {
+ if (!is_file(Base::getDbDirectory($i) . $post->fullpath)) {
+ echo '
diff --git a/js/jquery.cookies.js b/js/jquery.cookies.js
deleted file mode 100644
index 0321fcedb..000000000
--- a/js/jquery.cookies.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/*!
- * jQuery Cookie Plugin
- * https://github.com/carhartl/jquery-cookie
- * Copyright 2011, Klaus Hartl
- * Dual licensed under the MIT or GPL Version 2 licenses.
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.opensource.org/licenses/GPL-2.0
- * Last update: Sun, 03 Mar 2013 06:56:32 +0000
- */
-(function(factory){if(typeof define==='function'&&define.amd){define(['jquery'],factory)}else{factory(jQuery)}}(function($){var pluses=/\+/g;function raw(s){return s}function decoded(s){return decodeURIComponent(s.replace(pluses,' '))}function converted(s){if(s.indexOf('"')===0){s=s.slice(1,-1).replace(/\\"/g, '"').replace(/\\\\/g,'\\');}try{return config.json?JSON.parse(s):s}catch(er){}}var config=$.cookie=function(key,value,options){if(value!==undefined){options=$.extend({},config.defaults,options);if(typeof options.expires==='number'){var days=options.expires,t=options.expires=new Date();t.setDate(t.getDate()+days)}value=config.json?JSON.stringify(value):String(value);return(document.cookie=[config.raw?key:encodeURIComponent(key),'=',config.raw?value:encodeURIComponent(value),options.expires?'; expires='+options.expires.toUTCString():'',options.path?'; path='+options.path:'',options.domain?'; domain='+options.domain:'',options.secure?'; secure':''].join(''))}var decode=config.raw?raw:decoded;var cookies=document.cookie.split('; ');var result=key?undefined:{};for(var i=0,l=cookies.length;i
+ */
+
+class Author extends Base
+{
+ public const ALL_AUTHORS_ID = "cops:authors";
+
+ public const AUTHOR_COLUMNS = "authors.id as id, authors.name as name, authors.sort as sort, count(*) as count";
+ public const SQL_AUTHORS_BY_FIRST_LETTER = "select {0} from authors, books_authors_link where author = authors.id and upper (authors.sort) like ? group by authors.id, authors.name, authors.sort order by sort";
+ public const SQL_AUTHORS_FOR_SEARCH = "select {0} from authors, books_authors_link where author = authors.id and (upper (authors.sort) like ? or upper (authors.name) like ?) group by authors.id, authors.name, authors.sort order by sort";
+ public const SQL_ALL_AUTHORS = "select {0} from authors, books_authors_link where author = authors.id group by authors.id, authors.name, authors.sort order by sort";
+
+ public $id;
+ public $name;
+ public $sort;
+
+ public function __construct($post)
+ {
+ $this->id = $post->id;
+ $this->name = str_replace("|", ",", $post->name);
+ $this->sort = $post->sort;
+ }
+
+ public function getUri()
+ {
+ return "?page=".parent::PAGE_AUTHOR_DETAIL."&id=$this->id";
+ }
+
+ public function getEntryId()
+ {
+ return self::ALL_AUTHORS_ID.":".$this->id;
+ }
+
+ public static function getEntryIdByLetter($startingLetter)
+ {
+ return self::ALL_AUTHORS_ID.":letter:".$startingLetter;
+ }
+
+ public static function getCount()
+ {
+ // str_format (localize("authors.alphabetical", count(array))
+ return parent::getCountGeneric("authors", self::ALL_AUTHORS_ID, parent::PAGE_ALL_AUTHORS);
+ }
+
+ public static function getAllAuthorsByFirstLetter()
+ {
+ [, $result] = parent::executeQuery("select {0}
+from authors
+group by substr (upper (sort), 1, 1)
+order by substr (upper (sort), 1, 1)", "substr (upper (sort), 1, 1) as title, count(*) as count", "", [], -1);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ array_push($entryArray, new Entry(
+ $post->title,
+ Author::getEntryIdByLetter($post->title),
+ str_format(localize("authorword", $post->count), $post->count),
+ "text",
+ [ new LinkNavigation("?page=".parent::PAGE_AUTHORS_FIRST_LETTER."&id=". rawurlencode($post->title))],
+ "",
+ $post->count
+ ));
+ }
+ return $entryArray;
+ }
+
+ public static function getAuthorsByStartingLetter($letter)
+ {
+ return self::getEntryArray(self::SQL_AUTHORS_BY_FIRST_LETTER, [$letter . "%"]);
+ }
+
+ public static function getAuthorsForSearch($query)
+ {
+ return self::getEntryArray(self::SQL_AUTHORS_FOR_SEARCH, [$query . "%", $query . "%"]);
+ }
+
+ public static function getAllAuthors()
+ {
+ return self::getEntryArray(self::SQL_ALL_AUTHORS, []);
+ }
+
+ public static function getEntryArray($query, $params)
+ {
+ return Base::getEntryArrayWithBookNumber($query, self::AUTHOR_COLUMNS, $params, "Author");
+ }
+
+ public static function getAuthorById($authorId)
+ {
+ $result = parent::getDb()->prepare('select ' . self::AUTHOR_COLUMNS . ' from authors where id = ?');
+ $result->execute([$authorId]);
+ $post = $result->fetchObject();
+ return new Author($post);
+ }
+
+ public static function getAuthorByBookId($bookId)
+ {
+ $result = parent::getDb()->prepare('select authors.id as id, authors.name as name, authors.sort as sort from authors, books_authors_link
+where author = authors.id
+and book = ? order by books_authors_link.id');
+ $result->execute([$bookId]);
+ $authorArray = [];
+ while ($post = $result->fetchObject()) {
+ array_push($authorArray, new Author($post));
+ }
+ return $authorArray;
+ }
+}
diff --git a/lib/Base.php b/lib/Base.php
new file mode 100644
index 000000000..6320f6b44
--- /dev/null
+++ b/lib/Base.php
@@ -0,0 +1,257 @@
+
+ */
+
+abstract class Base
+{
+ public const PAGE_INDEX = "index";
+ public const PAGE_ALL_AUTHORS = "1";
+ public const PAGE_AUTHORS_FIRST_LETTER = "2";
+ public const PAGE_AUTHOR_DETAIL = "3";
+ public const PAGE_ALL_BOOKS = "4";
+ public const PAGE_ALL_BOOKS_LETTER = "5";
+ public const PAGE_ALL_SERIES = "6";
+ public const PAGE_SERIE_DETAIL = "7";
+ public const PAGE_OPENSEARCH = "8";
+ public const PAGE_OPENSEARCH_QUERY = "9";
+ public const PAGE_ALL_RECENT_BOOKS = "10";
+ public const PAGE_ALL_TAGS = "11";
+ public const PAGE_TAG_DETAIL = "12";
+ public const PAGE_BOOK_DETAIL = "13";
+ public const PAGE_ALL_CUSTOMS = "14";
+ public const PAGE_CUSTOM_DETAIL = "15";
+ public const PAGE_ABOUT = "16";
+ public const PAGE_ALL_LANGUAGES = "17";
+ public const PAGE_LANGUAGE_DETAIL = "18";
+ public const PAGE_CUSTOMIZE = "19";
+ public const PAGE_ALL_PUBLISHERS = "20";
+ public const PAGE_PUBLISHER_DETAIL = "21";
+ public const PAGE_ALL_RATINGS = "22";
+ public const PAGE_RATING_DETAIL = "23";
+
+ public const COMPATIBILITY_XML_ALDIKO = "aldiko";
+
+ private static $db = null;
+
+ public static function isMultipleDatabaseEnabled()
+ {
+ global $config;
+ return is_array($config['calibre_directory']);
+ }
+
+ public static function useAbsolutePath()
+ {
+ global $config;
+ $path = self::getDbDirectory();
+ return preg_match('/^\//', $path) || // Linux /
+ preg_match('/^\w\:/', $path); // Windows X:
+ }
+
+ public static function noDatabaseSelected()
+ {
+ return self::isMultipleDatabaseEnabled() && is_null(GetUrlParam(DB));
+ }
+
+ public static function getDbList()
+ {
+ global $config;
+ if (self::isMultipleDatabaseEnabled()) {
+ return $config['calibre_directory'];
+ } else {
+ return ["" => $config['calibre_directory']];
+ }
+ }
+
+ public static function getDbNameList()
+ {
+ global $config;
+ if (self::isMultipleDatabaseEnabled()) {
+ return array_keys($config['calibre_directory']);
+ } else {
+ return [""];
+ }
+ }
+
+ public static function getDbName($database = null)
+ {
+ global $config;
+ if (self::isMultipleDatabaseEnabled()) {
+ if (is_null($database)) {
+ $database = GetUrlParam(DB, 0);
+ }
+ if (!is_null($database) && !preg_match('/^\d+$/', $database)) {
+ self::error($database);
+ }
+ $array = array_keys($config['calibre_directory']);
+ return $array[$database];
+ }
+ return "";
+ }
+
+ public static function getDbDirectory($database = null)
+ {
+ global $config;
+ if (self::isMultipleDatabaseEnabled()) {
+ if (is_null($database)) {
+ $database = GetUrlParam(DB, 0);
+ }
+ if (!is_null($database) && !preg_match('/^\d+$/', $database)) {
+ self::error($database);
+ }
+ $array = array_values($config['calibre_directory']);
+ return $array[$database];
+ }
+ return $config['calibre_directory'];
+ }
+
+ // -DC- Add image directory
+ public static function getImgDirectory($database = null)
+ {
+ global $config;
+ if (self::isMultipleDatabaseEnabled()) {
+ if (is_null($database)) {
+ $database = GetUrlParam(DB, 0);
+ }
+ $array = array_values($config['image_directory']);
+ return $array[$database];
+ }
+ return $config['image_directory'];
+ }
+
+ public static function getDbFileName($database = null)
+ {
+ return self::getDbDirectory($database) .'metadata.db';
+ }
+
+ private static function error($database)
+ {
+ if (php_sapi_name() != "cli") {
+ header("location: checkconfig.php?err=1");
+ }
+ throw new Exception("Database <{$database}> not found.");
+ }
+
+ public static function getDb($database = null)
+ {
+ if (is_null(self::$db)) {
+ try {
+ if (is_readable(self::getDbFileName($database))) {
+ self::$db = new PDO('sqlite:'. self::getDbFileName($database));
+ if (useNormAndUp()) {
+ self::$db->sqliteCreateFunction('normAndUp', 'normAndUp', 1);
+ }
+ } else {
+ self::error($database);
+ }
+ } catch (Exception $e) {
+ self::error($database);
+ }
+ }
+ return self::$db;
+ }
+
+ public static function checkDatabaseAvailability()
+ {
+ if (self::noDatabaseSelected()) {
+ for ($i = 0; $i < count(self::getDbList()); $i++) {
+ self::getDb($i);
+ self::clearDb();
+ }
+ } else {
+ self::getDb();
+ }
+ return true;
+ }
+
+ public static function clearDb()
+ {
+ self::$db = null;
+ }
+
+ public static function executeQuerySingle($query, $database = null)
+ {
+ return self::getDb($database)->query($query)->fetchColumn();
+ }
+
+ public static function getCountGeneric($table, $id, $pageId, $numberOfString = null)
+ {
+ if (!$numberOfString) {
+ $numberOfString = $table . ".alphabetical";
+ }
+ $count = self::executeQuerySingle('select count(*) from ' . $table);
+ if ($count == 0) {
+ return null;
+ }
+ $entry = new Entry(
+ localize($table . ".title"),
+ $id,
+ str_format(localize($numberOfString, $count), $count),
+ "text",
+ [ new LinkNavigation("?page=".$pageId)],
+ "",
+ $count
+ );
+ return $entry;
+ }
+
+ public static function getEntryArrayWithBookNumber($query, $columns, $params, $category)
+ {
+ /* @var $result PDOStatement */
+
+ [, $result] = self::executeQuery($query, $columns, "", $params, -1);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ /* @var $instance Author|Tag|Serie|Publisher */
+
+ $instance = new $category($post);
+ if (property_exists($post, "sort")) {
+ $title = $post->sort;
+ } else {
+ $title = $post->name;
+ }
+ array_push($entryArray, new Entry(
+ $title,
+ $instance->getEntryId(),
+ str_format(localize("bookword", $post->count), $post->count),
+ "text",
+ [ new LinkNavigation($instance->getUri())],
+ "",
+ $post->count
+ ));
+ }
+ return $entryArray;
+ }
+
+ public static function executeQuery($query, $columns, $filter, $params, $n, $database = null, $numberPerPage = null)
+ {
+ $totalResult = -1;
+
+ if (useNormAndUp()) {
+ $query = preg_replace("/upper/", "normAndUp", $query);
+ $columns = preg_replace("/upper/", "normAndUp", $columns);
+ }
+
+ if (is_null($numberPerPage)) {
+ $numberPerPage = getCurrentOption("max_item_per_page");
+ }
+
+ if ($numberPerPage != -1 && $n != -1) {
+ // First check total number of results
+ $result = self::getDb($database)->prepare(str_format($query, "count(*)", $filter));
+ $result->execute($params);
+ $totalResult = $result->fetchColumn();
+
+ // Next modify the query and params
+ $query .= " limit ?, ?";
+ array_push($params, ($n - 1) * $numberPerPage, $numberPerPage);
+ }
+
+ $result = self::getDb($database)->prepare(str_format($query, $columns, $filter));
+ $result->execute($params);
+ return [$totalResult, $result];
+ }
+}
diff --git a/lib/Book.php b/lib/Book.php
new file mode 100644
index 000000000..cd4853175
--- /dev/null
+++ b/lib/Book.php
@@ -0,0 +1,843 @@
+
+ */
+
+// Silly thing because PHP forbid string concatenation in class const
+define('SQL_BOOKS_LEFT_JOIN', 'left outer join comments on comments.book = books.id
+ left outer join books_ratings_link on books_ratings_link.book = books.id
+ left outer join ratings on books_ratings_link.rating = ratings.id ');
+define('SQL_BOOKS_ALL', 'select {0} from books ' . SQL_BOOKS_LEFT_JOIN . ' order by books.sort ');
+define('SQL_BOOKS_BY_PUBLISHER', 'select {0} from books_publishers_link, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where books_publishers_link.book = books.id and publisher = ? {1} order by publisher');
+define('SQL_BOOKS_BY_FIRST_LETTER', 'select {0} from books ' . SQL_BOOKS_LEFT_JOIN . '
+ where upper (books.sort) like ? order by books.sort');
+define('SQL_BOOKS_BY_AUTHOR', 'select {0} from books_authors_link, books ' . SQL_BOOKS_LEFT_JOIN . '
+ left outer join books_series_link on books_series_link.book = books.id
+ where books_authors_link.book = books.id and author = ? {1} order by series desc, series_index asc, pubdate asc');
+define('SQL_BOOKS_BY_SERIE', 'select {0} from books_series_link, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where books_series_link.book = books.id and series = ? {1} order by series_index');
+define('SQL_BOOKS_BY_TAG', 'select {0} from books_tags_link, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where books_tags_link.book = books.id and tag = ? {1} order by sort');
+define('SQL_BOOKS_BY_LANGUAGE', 'select {0} from books_languages_link, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where books_languages_link.book = books.id and lang_code = ? {1} order by sort');
+define('SQL_BOOKS_BY_CUSTOM', 'select {0} from {2}, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where {2}.book = books.id and {2}.{3} = ? {1} order by sort');
+define('SQL_BOOKS_BY_CUSTOM_BOOL_TRUE', 'select {0} from {2}, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where {2}.book = books.id and {2}.value = 1 {1} order by sort');
+define('SQL_BOOKS_BY_CUSTOM_BOOL_FALSE', 'select {0} from {2}, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where {2}.book = books.id and {2}.value = 0 {1} order by sort');
+define('SQL_BOOKS_BY_CUSTOM_BOOL_NULL', 'select {0} from books ' . SQL_BOOKS_LEFT_JOIN . '
+ where books.id not in (select book from {2}) {1} order by sort');
+define('SQL_BOOKS_BY_CUSTOM_RATING', 'select {0} from books ' . SQL_BOOKS_LEFT_JOIN . '
+ left join {2} on {2}.book = books.id
+ left join {3} on {3}.id = {2}.{4}
+ where {3}.value = ? order by sort');
+define('SQL_BOOKS_BY_CUSTOM_RATING_NULL', 'select {0} from books ' . SQL_BOOKS_LEFT_JOIN . '
+ left join {2} on {2}.book = books.id
+ left join {3} on {3}.id = {2}.{4}
+ where ((books.id not in (select {2}.book from {2})) or ({3}.value = 0)) {1} order by sort');
+define('SQL_BOOKS_BY_CUSTOM_DATE', 'select {0} from {2}, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where {2}.book = books.id and date({2}.value) = ? {1} order by sort');
+define('SQL_BOOKS_BY_CUSTOM_DIRECT', 'select {0} from {2}, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where {2}.book = books.id and {2}.value = ? {1} order by sort');
+define('SQL_BOOKS_BY_CUSTOM_DIRECT_ID', 'select {0} from {2}, books ' . SQL_BOOKS_LEFT_JOIN . '
+ where {2}.book = books.id and {2}.id = ? {1} order by sort');
+define('SQL_BOOKS_QUERY', 'select {0} from books ' . SQL_BOOKS_LEFT_JOIN . '
+ where (
+ exists (select null from authors, books_authors_link where book = books.id and author = authors.id and authors.name like ?) or
+ exists (select null from tags, books_tags_link where book = books.id and tag = tags.id and tags.name like ?) or
+ exists (select null from series, books_series_link on book = books.id and books_series_link.series = series.id and series.name like ?) or
+ exists (select null from publishers, books_publishers_link where book = books.id and books_publishers_link.publisher = publishers.id and publishers.name like ?) or
+ title like ?) {1} order by books.sort');
+define('SQL_BOOKS_RECENT', 'select {0} from books ' . SQL_BOOKS_LEFT_JOIN . '
+ where 1=1 {1} order by timestamp desc limit ');
+define('SQL_BOOKS_BY_RATING', 'select {0} from books ' . SQL_BOOKS_LEFT_JOIN . '
+ where books_ratings_link.book = books.id and ratings.id = ? {1} order by sort');
+
+require('Identifier.php');
+
+class Book extends Base
+{
+ public const ALL_BOOKS_UUID = 'urn:uuid';
+ public const ALL_BOOKS_ID = 'cops:books';
+ public const ALL_RECENT_BOOKS_ID = 'cops:recentbooks';
+ public const BOOK_COLUMNS = 'books.id as id, books.title as title, text as comment, path, timestamp, pubdate, series_index, uuid, has_cover, ratings.rating';
+
+ public const SQL_BOOKS_LEFT_JOIN = SQL_BOOKS_LEFT_JOIN;
+ public const SQL_BOOKS_ALL = SQL_BOOKS_ALL;
+ public const SQL_BOOKS_BY_PUBLISHER = SQL_BOOKS_BY_PUBLISHER;
+ public const SQL_BOOKS_BY_FIRST_LETTER = SQL_BOOKS_BY_FIRST_LETTER;
+ public const SQL_BOOKS_BY_AUTHOR = SQL_BOOKS_BY_AUTHOR;
+ public const SQL_BOOKS_BY_SERIE = SQL_BOOKS_BY_SERIE;
+ public const SQL_BOOKS_BY_TAG = SQL_BOOKS_BY_TAG;
+ public const SQL_BOOKS_BY_LANGUAGE = SQL_BOOKS_BY_LANGUAGE;
+ public const SQL_BOOKS_BY_CUSTOM = SQL_BOOKS_BY_CUSTOM;
+ public const SQL_BOOKS_BY_CUSTOM_BOOL_TRUE = SQL_BOOKS_BY_CUSTOM_BOOL_TRUE;
+ public const SQL_BOOKS_BY_CUSTOM_BOOL_FALSE = SQL_BOOKS_BY_CUSTOM_BOOL_FALSE;
+ public const SQL_BOOKS_BY_CUSTOM_BOOL_NULL = SQL_BOOKS_BY_CUSTOM_BOOL_NULL;
+ public const SQL_BOOKS_BY_CUSTOM_RATING = SQL_BOOKS_BY_CUSTOM_RATING;
+ public const SQL_BOOKS_BY_CUSTOM_RATING_NULL = SQL_BOOKS_BY_CUSTOM_RATING_NULL;
+ public const SQL_BOOKS_BY_CUSTOM_DATE = SQL_BOOKS_BY_CUSTOM_DATE;
+ public const SQL_BOOKS_BY_CUSTOM_DIRECT = SQL_BOOKS_BY_CUSTOM_DIRECT;
+ public const SQL_BOOKS_BY_CUSTOM_DIRECT_ID = SQL_BOOKS_BY_CUSTOM_DIRECT_ID;
+ public const SQL_BOOKS_QUERY = SQL_BOOKS_QUERY;
+ public const SQL_BOOKS_RECENT = SQL_BOOKS_RECENT;
+ public const SQL_BOOKS_BY_RATING = SQL_BOOKS_BY_RATING;
+
+ public const BAD_SEARCH = 'QQQQQ';
+
+ public $id;
+ public $title;
+ public $timestamp;
+ public $pubdate;
+ public $path;
+ public $uuid;
+ public $hasCover;
+ public $relativePath;
+ public $seriesIndex;
+ public $comment;
+ public $rating;
+ public $datas = null;
+ public $authors = null;
+ public $publisher = null;
+ public $serie = null;
+ public $tags = null;
+ public $identifiers = null;
+ public $languages = null;
+ public $format = [];
+ private $coverFileName = null;
+
+ public function __construct($line)
+ {
+ global $config;
+
+ $this->id = $line->id;
+ $this->title = $line->title;
+ $this->timestamp = strtotime($line->timestamp);
+ $this->pubdate = $line->pubdate;
+ //$this->path = Base::getDbDirectory() . $line->path;
+ //$this->relativePath = $line->path;
+ // -DC- Init relative or full path
+ $this->path = $line->path;
+ if (!is_dir($this->path)) {
+ $this->path = Base::getDbDirectory() . $line->path;
+ }
+ $this->seriesIndex = $line->series_index;
+ $this->comment = $line->comment ?? '';
+ $this->uuid = $line->uuid;
+ $this->hasCover = $line->has_cover;
+ // -DC- Use cover file name
+ //if (!file_exists($this->getFilePath('jpg'))) {
+ // // double check
+ // $this->hasCover = 0;
+ //}
+ if ($this->hasCover) {
+ if (!empty($config['calibre_database_field_cover'])) {
+ $imgDirectory = Base::getImgDirectory();
+ $this->coverFileName = $line->cover;
+ if (!file_exists($this->coverFileName)) {
+ $this->coverFileName = null;
+ }
+ if (empty($this->coverFileName)) {
+ $this->coverFileName = sprintf('%s%s', $imgDirectory, $line->cover);
+ if (!file_exists($this->coverFileName)) {
+ $this->coverFileName = null;
+ }
+ }
+ if (empty($this->coverFileName)) {
+ // Try with the epub file name
+ $data = $this->getDataFormat('EPUB');
+ if ($data) {
+ $this->coverFileName = sprintf('%s%s/%s', $imgDirectory, $data->name, $line->cover);
+ if (!file_exists($this->coverFileName)) {
+ $this->coverFileName = null;
+ }
+ if (empty($this->coverFileName)) {
+ $this->coverFileName = sprintf('%s%s.jpg', $imgDirectory, $data->name);
+ if (!file_exists($this->coverFileName)) {
+ $this->coverFileName = null;
+ }
+ }
+ }
+ }
+ }
+ // Else try with default cover file name
+ if (empty($this->coverFileName)) {
+ $cover = $this->getFilePath("jpg");
+ if ($cover === false || !file_exists($cover)) {
+ $cover = $this->getFilePath("png");
+ }
+ if ($cover === false || !file_exists($cover)) {
+ $this->hasCover = 0;
+ } else {
+ $this->coverFileName = $cover;
+ }
+ }
+ }
+ $this->rating = $line->rating;
+ }
+
+ // -DC- Get customisable book columns
+ private static function getBookColumns()
+ {
+ global $config;
+
+ $res = self::BOOK_COLUMNS;
+ if (!empty($config['calibre_database_field_cover'])) {
+ $res = str_replace('has_cover,', 'has_cover, ' . $config['calibre_database_field_cover'] . ',', $res);
+ }
+
+ return $res;
+ }
+
+ public function getEntryId()
+ {
+ return self::ALL_BOOKS_UUID.':'.$this->uuid;
+ }
+
+ public static function getEntryIdByLetter($startingLetter)
+ {
+ return self::ALL_BOOKS_ID.':letter:'.$startingLetter;
+ }
+
+ public function getUri()
+ {
+ return '?page='.parent::PAGE_BOOK_DETAIL.'&id=' . $this->id;
+ }
+
+ public function getDetailUrl()
+ {
+ $urlParam = $this->getUri();
+ if (!is_null(GetUrlParam(DB))) {
+ $urlParam = addURLParameter($urlParam, DB, GetUrlParam(DB));
+ }
+ return 'index.php' . $urlParam;
+ }
+
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /* Other class (author, series, tag, ...) initialization and accessors */
+
+ /**
+ * @return Author[]
+ */
+ public function getAuthors()
+ {
+ if (is_null($this->authors)) {
+ $this->authors = Author::getAuthorByBookId($this->id);
+ }
+ return $this->authors;
+ }
+
+ public function getAuthorsName()
+ {
+ return implode(', ', array_map(function ($author) {
+ return $author->name;
+ }, $this->getAuthors()));
+ }
+
+ public function getAuthorsSort()
+ {
+ return implode(', ', array_map(function ($author) {
+ return $author->sort;
+ }, $this->getAuthors()));
+ }
+
+ public function getPublisher()
+ {
+ if (is_null($this->publisher)) {
+ $this->publisher = Publisher::getPublisherByBookId($this->id);
+ }
+ return $this->publisher;
+ }
+
+ /**
+ * @return Serie
+ */
+ public function getSerie()
+ {
+ if (is_null($this->serie)) {
+ $this->serie = Serie::getSerieByBookId($this->id);
+ }
+ return $this->serie;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLanguages()
+ {
+ $lang = [];
+ $result = parent::getDb()->prepare('select languages.lang_code
+ from books_languages_link, languages
+ where books_languages_link.lang_code = languages.id
+ and book = ?
+ order by item_order');
+ $result->execute([$this->id]);
+ while ($post = $result->fetchObject()) {
+ array_push($lang, Language::getLanguageString($post->lang_code));
+ }
+ return implode(', ', $lang);
+ }
+
+ /**
+ * @return Tag[]
+ */
+ public function getTags()
+ {
+ if (is_null($this->tags)) {
+ $this->tags = [];
+
+ $result = parent::getDb()->prepare('select tags.id as id, name
+ from books_tags_link, tags
+ where tag = tags.id
+ and book = ?
+ order by name');
+ $result->execute([$this->id]);
+ while ($post = $result->fetchObject()) {
+ array_push($this->tags, new Tag($post));
+ }
+ }
+ return $this->tags;
+ }
+
+ public function getTagsName()
+ {
+ return implode(', ', array_map(function ($tag) {
+ return $tag->name;
+ }, $this->getTags()));
+ }
+
+ /**
+ * @return Identifier[]
+ */
+ public function getIdentifiers()
+ {
+ if (is_null($this->identifiers)) {
+ $this->identifiers = [];
+
+ $result = parent::getDb()->prepare('select type, val, id
+ from identifiers
+ where book = ?
+ order by type');
+ $result->execute([$this->id]);
+ while ($post = $result->fetchObject()) {
+ array_push($this->identifiers, new Identifier($post));
+ }
+ }
+ return $this->identifiers;
+ }
+
+ /**
+ * @return Data[]
+ */
+ public function getDatas()
+ {
+ if (is_null($this->datas)) {
+ $this->datas = Data::getDataByBook($this);
+ }
+ return $this->datas;
+ }
+
+ /* End of other class (author, series, tag, ...) initialization and accessors */
+
+ public static function getFilterString()
+ {
+ $filter = getURLParam('tag', null);
+ if (empty($filter)) {
+ return '';
+ }
+
+ $exists = true;
+ if (preg_match("/^!(.*)$/", $filter, $matches)) {
+ $exists = false;
+ $filter = $matches[1];
+ }
+
+ $result = 'exists (select null from books_tags_link, tags where books_tags_link.book = books.id and books_tags_link.tag = tags.id and tags.name = "' . $filter . '")';
+
+ if (!$exists) {
+ $result = 'not ' . $result;
+ }
+
+ return 'and ' . $result;
+ }
+
+ public function GetMostInterestingDataToSendToKindle()
+ {
+ $bestFormatForKindle = ['EPUB', 'PDF', 'AZW3', 'MOBI'];
+ $bestRank = -1;
+ $bestData = null;
+ foreach ($this->getDatas() as $data) {
+ $key = array_search($data->format, $bestFormatForKindle);
+ if ($key !== false && $key > $bestRank) {
+ $bestRank = $key;
+ $bestData = $data;
+ }
+ }
+ return $bestData;
+ }
+
+ public function getDataById($idData)
+ {
+ $reduced = array_filter($this->getDatas(), function ($data) use ($idData) {
+ return $data->id == $idData;
+ });
+ return reset($reduced);
+ }
+
+ public function getRating()
+ {
+ if (is_null($this->rating) || $this->rating == 0) {
+ return '';
+ }
+ $retour = '';
+ for ($i = 0; $i < $this->rating / 2; $i++) {
+ $retour .= '★'; // full star
+ }
+ for ($i = 0; $i < 5 - $this->rating / 2; $i++) {
+ $retour .= '☆'; // empty star
+ }
+ return $retour;
+ }
+
+ public function getPubDate()
+ {
+ if (empty($this->pubdate)) {
+ return '';
+ }
+ $dateY = (int) substr($this->pubdate, 0, 4);
+ if ($dateY > 102) {
+ return str_pad(strval($dateY), 4, '0', STR_PAD_LEFT);
+ }
+ return '';
+ }
+
+ public function getComment($withSerie = true)
+ {
+ $addition = '';
+ $se = $this->getSerie();
+ if (!is_null($se) && $withSerie) {
+ $addition = $addition . '' . localize('content.series') . '' . str_format(localize('content.series.data'), $this->seriesIndex, htmlspecialchars($se->name)) . " \n";
+ }
+ //if (preg_match('/<\/(div|p|a|span)>/', $this->comment)) {
+ return $addition . html2xhtml($this->comment);
+ //} else {
+ // return $addition . htmlspecialchars($this->comment);
+ //}
+ }
+
+ public function getDataFormat($format)
+ {
+ $reduced = array_filter($this->getDatas(), function ($data) use ($format) {
+ return $data->format == $format;
+ });
+ return reset($reduced);
+ }
+
+ /**
+ * @checkme always returns absolute path for single DB in PHP app here - cfr. internal dir for X-Accel-Redirect with Nginx
+ * @param false $relative Deprecated
+ */
+ public function getFilePath($extension, $idData = null, $relative = false)
+ {
+ /*if ($extension == 'jpg')
+ {
+ $file = 'cover.jpg';
+ } else {
+ $data = $this->getDataById($idData);
+ if (!$data) {
+ return null;
+ }
+ $file = $data->name . '.' . strtolower($data->format);
+ }
+
+ if ($relative) {
+ return $this->relativePath.'/'.$file;
+ } else {
+ return $this->path.'/'.$file;
+ }*/
+ if ($extension == "jpg" || $extension == "png") {
+ if (empty($this->coverFileName)) {
+ return $this->path . '/cover.' . $extension;
+ } else {
+ $ext = strtolower(pathinfo($this->coverFileName, PATHINFO_EXTENSION));
+ if ($ext == $extension) {
+ return $this->coverFileName;
+ }
+ }
+ return false;
+ } else {
+ $data = $this->getDataById($idData);
+ if (!$data) {
+ return null;
+ }
+ $file = $data->name . "." . strtolower($data->format);
+ return $this->path . '/' . $file;
+ }
+ }
+
+ public function getUpdatedEpub($idData)
+ {
+ global $config;
+ $data = $this->getDataById($idData);
+
+ try {
+ $epub = new EPub($data->getLocalPath());
+
+ $epub->Title($this->title);
+ $authorArray = [];
+ foreach ($this->getAuthors() as $author) {
+ $authorArray[$author->sort] = $author->name;
+ }
+ $epub->Authors($authorArray);
+ $epub->Language($this->getLanguages());
+ $epub->Description($this->getComment(false));
+ $epub->Subjects($this->getTagsName());
+ // -DC- Use cover file name
+ // $epub->Cover2($this->getFilePath('jpg'), 'image/jpeg');
+ $epub->Cover2($this->coverFileName, 'image/jpeg');
+ $epub->Calibre($this->uuid);
+ $se = $this->getSerie();
+ if (!is_null($se)) {
+ $epub->Serie($se->name);
+ $epub->SerieIndex($this->seriesIndex);
+ }
+ $filename = $data->getUpdatedFilenameEpub();
+ if ($config['cops_provide_kepub'] == '1' && preg_match('/Kobo/', $_SERVER['HTTP_USER_AGENT'])) {
+ $epub->updateForKepub();
+ $filename = $data->getUpdatedFilenameKepub();
+ }
+ $epub->download($filename);
+ } catch (Exception $e) {
+ echo 'Exception : ' . $e->getMessage();
+ }
+ }
+
+ public function getThumbnail($width, $height, $outputfile = null, $inType = 'jpg')
+ {
+ if (is_null($width) && is_null($height)) {
+ return false;
+ }
+
+ // -DC- Use cover file name
+ //$file = $this->getFilePath('jpg');
+ $file = $this->coverFileName;
+ // get image size
+ if ($size = GetImageSize($file)) {
+ $w = $size[0];
+ $h = $size[1];
+ //set new size
+ if (!is_null($width)) {
+ $nw = $width;
+ if ($nw >= $w) {
+ return false;
+ }
+ $nh = intval(($nw*$h)/$w);
+ } else {
+ $nh = $height;
+ if ($nh >= $h) {
+ return false;
+ }
+ $nw = intval(($nh*$w)/$h);
+ }
+ } else {
+ return false;
+ }
+
+ // Draw the image
+ if ($inType == 'png') {
+ $src_img = imagecreatefrompng($file);
+ } else {
+ $src_img = imagecreatefromjpeg($file);
+ }
+ $dst_img = imagecreatetruecolor($nw, $nh);
+ if (!imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $nw, $nh, $w, $h)) {
+ return false;
+ }
+ if ($inType == 'png') {
+ if (!imagepng($dst_img, $outputfile, 9)) {
+ return false;
+ }
+ } else {
+ if (!imagejpeg($dst_img, $outputfile, 80)) {
+ return false;
+ }
+ }
+ imagedestroy($src_img);
+ imagedestroy($dst_img);
+
+ return true;
+ }
+
+ public function getLinkArray()
+ {
+ $linkArray = [];
+
+ if ($this->hasCover) {
+ // -DC- Use cover file name
+ //array_push($linkArray, Data::getLink($this, 'jpg', 'image/jpeg', Link::OPDS_IMAGE_TYPE, 'cover.jpg', NULL));
+ //array_push($linkArray, Data::getLink($this, 'jpg', 'image/jpeg', Link::OPDS_THUMBNAIL_TYPE, 'cover.jpg', NULL));
+ $ext = strtolower(pathinfo($this->coverFileName, PATHINFO_EXTENSION));
+ if ($ext == 'png') {
+ array_push($linkArray, Data::getLink($this, "png", "image/png", Link::OPDS_IMAGE_TYPE, "cover.png", null));
+ array_push($linkArray, Data::getLink($this, "png", "image/png", Link::OPDS_THUMBNAIL_TYPE, "cover.png", null));
+ } else {
+ array_push($linkArray, Data::getLink($this, 'jpg', 'image/jpeg', Link::OPDS_IMAGE_TYPE, 'cover.jpg', null));
+ array_push($linkArray, Data::getLink($this, "jpg", "image/jpeg", Link::OPDS_THUMBNAIL_TYPE, "cover.jpg", null));
+ }
+ }
+
+ foreach ($this->getDatas() as $data) {
+ if ($data->isKnownType()) {
+ array_push($linkArray, $data->getDataLink(Link::OPDS_ACQUISITION_TYPE, $data->format));
+ }
+ }
+
+ foreach ($this->getAuthors() as $author) {
+ /* @var $author Author */
+ array_push($linkArray, new LinkNavigation($author->getUri(), 'related', str_format(localize('bookentry.author'), localize('splitByLetter.book.other'), $author->name)));
+ }
+
+ $serie = $this->getSerie();
+ if (!is_null($serie)) {
+ array_push($linkArray, new LinkNavigation($serie->getUri(), 'related', str_format(localize('content.series.data'), $this->seriesIndex, $serie->name)));
+ }
+
+ return $linkArray;
+ }
+
+
+ public function getEntry()
+ {
+ return new EntryBook(
+ $this->getTitle(),
+ $this->getEntryId(),
+ $this->getComment(),
+ 'text/html',
+ $this->getLinkArray(),
+ $this
+ );
+ }
+
+ public static function getBookCount($database = null)
+ {
+ return parent::executeQuerySingle('select count(*) from books', $database);
+ }
+
+ public static function getCount()
+ {
+ global $config;
+ $nBooks = parent::executeQuerySingle('select count(*) from books');
+ $result = [];
+ $entry = new Entry(
+ localize('allbooks.title'),
+ self::ALL_BOOKS_ID,
+ str_format(localize('allbooks.alphabetical', $nBooks), $nBooks),
+ 'text',
+ [new LinkNavigation('?page='.parent::PAGE_ALL_BOOKS)],
+ '',
+ $nBooks
+ );
+ array_push($result, $entry);
+ if ($config['cops_recentbooks_limit'] > 0) {
+ $entry = new Entry(
+ localize('recent.title'),
+ self::ALL_RECENT_BOOKS_ID,
+ str_format(localize('recent.list'), $config['cops_recentbooks_limit']),
+ 'text',
+ [ new LinkNavigation('?page='.parent::PAGE_ALL_RECENT_BOOKS)],
+ '',
+ $config['cops_recentbooks_limit']
+ );
+ array_push($result, $entry);
+ }
+ return $result;
+ }
+
+ public static function getBooksByAuthor($authorId, $n)
+ {
+ return self::getEntryArray(self::SQL_BOOKS_BY_AUTHOR, [$authorId], $n);
+ }
+
+ public static function getBooksByRating($ratingId, $n)
+ {
+ return self::getEntryArray(self::SQL_BOOKS_BY_RATING, [$ratingId], $n);
+ }
+
+ public static function getBooksByPublisher($publisherId, $n)
+ {
+ return self::getEntryArray(self::SQL_BOOKS_BY_PUBLISHER, [$publisherId], $n);
+ }
+
+ public static function getBooksBySeries($serieId, $n)
+ {
+ return self::getEntryArray(self::SQL_BOOKS_BY_SERIE, [$serieId], $n);
+ }
+
+ public static function getBooksByTag($tagId, $n)
+ {
+ return self::getEntryArray(self::SQL_BOOKS_BY_TAG, [$tagId], $n);
+ }
+
+ public static function getBooksByLanguage($languageId, $n)
+ {
+ return self::getEntryArray(self::SQL_BOOKS_BY_LANGUAGE, [$languageId], $n);
+ }
+
+ /**
+ * @param $customColumn CustomColumn
+ * @param $id integer
+ * @param $n integer
+ * @return array
+ */
+ public static function getBooksByCustom($customColumn, $id, $n)
+ {
+ [$query, $params] = $customColumn->getQuery($id);
+
+ return self::getEntryArray($query, $params, $n);
+ }
+
+ public static function getBookById($bookId)
+ {
+ $result = parent::getDb()->prepare('select ' . self::getBookColumns() . '
+from books ' . self::SQL_BOOKS_LEFT_JOIN . '
+where books.id = ?');
+ $result->execute([$bookId]);
+ while ($post = $result->fetchObject()) {
+ $book = new Book($post);
+ return $book;
+ }
+ return null;
+ }
+
+ public static function getBookByDataId($dataId)
+ {
+ $result = parent::getDb()->prepare('select ' . self::getBookColumns() . ', data.name, data.format
+from data, books ' . self::SQL_BOOKS_LEFT_JOIN . '
+where data.book = books.id and data.id = ?');
+ $result->execute([$dataId]);
+ while ($post = $result->fetchObject()) {
+ $book = new Book($post);
+ $data = new Data($post, $book);
+ $data->id = $dataId;
+ $book->datas = [$data];
+ return $book;
+ }
+ return null;
+ }
+
+ public static function getBooksByQuery($query, $n, $database = null, $numberPerPage = null)
+ {
+ $i = 0;
+ $critArray = [];
+ foreach ([PageQueryResult::SCOPE_AUTHOR,
+ PageQueryResult::SCOPE_TAG,
+ PageQueryResult::SCOPE_SERIES,
+ PageQueryResult::SCOPE_PUBLISHER,
+ PageQueryResult::SCOPE_BOOK] as $key) {
+ if (in_array($key, getCurrentOption('ignored_categories')) ||
+ (!array_key_exists($key, $query) && !array_key_exists('all', $query))) {
+ $critArray[$i] = self::BAD_SEARCH;
+ } else {
+ if (array_key_exists($key, $query)) {
+ $critArray[$i] = $query[$key];
+ } else {
+ $critArray[$i] = $query["all"];
+ }
+ }
+ $i++;
+ }
+ return self::getEntryArray(self::SQL_BOOKS_QUERY, $critArray, $n, $database, $numberPerPage);
+ }
+
+ public static function getBooks($n)
+ {
+ [$entryArray, $totalNumber] = self::getEntryArray(self::SQL_BOOKS_ALL, [], $n);
+ return [$entryArray, $totalNumber];
+ }
+
+ public static function getAllBooks()
+ {
+ /* @var $result PDOStatement */
+
+ [, $result] = parent::executeQuery('select {0}
+from books
+group by substr (upper (sort), 1, 1)
+order by substr (upper (sort), 1, 1)', 'substr (upper (sort), 1, 1) as title, count(*) as count', self::getFilterString(), [], -1);
+
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ array_push($entryArray, new Entry(
+ $post->title,
+ Book::getEntryIdByLetter($post->title),
+ str_format(localize('bookword', $post->count), $post->count),
+ 'text',
+ [new LinkNavigation('?page='.parent::PAGE_ALL_BOOKS_LETTER.'&id='. rawurlencode($post->title))],
+ '',
+ $post->count
+ ));
+ }
+ return $entryArray;
+ }
+
+ public static function getBooksByStartingLetter($letter, $n, $database = null, $numberPerPage = null)
+ {
+ return self::getEntryArray(self::SQL_BOOKS_BY_FIRST_LETTER, [$letter . '%'], $n, $database, $numberPerPage);
+ }
+
+ public static function getEntryArray($query, $params, $n, $database = null, $numberPerPage = null)
+ {
+ /* @var $totalNumber integer */
+ /* @var $result PDOStatement */
+ [$totalNumber, $result] = parent::executeQuery($query, self::getBookColumns(), self::getFilterString(), $params, $n, $database, $numberPerPage);
+
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $book = new Book($post);
+ array_push($entryArray, $book->getEntry());
+ }
+ return [$entryArray, $totalNumber];
+ }
+
+ public static function getAllRecentBooks()
+ {
+ global $config;
+ [$entryArray, ] = self::getEntryArray(self::SQL_BOOKS_RECENT . $config['cops_recentbooks_limit'], [], -1);
+ return $entryArray;
+ }
+
+ /**
+ * The values of all the specified columns
+ *
+ * @param string[] $columns
+ * @return CustomColumn[]
+ */
+ public function getCustomColumnValues($columns, $asArray = false)
+ {
+ $result = [];
+
+ foreach ($columns as $lookup) {
+ $col = CustomColumnType::createByLookup($lookup);
+ if (!is_null($col)) {
+ $cust = $col->getCustomByBook($this);
+ if (!is_null($cust)) {
+ if ($asArray) {
+ array_push($result, $cust->toArray());
+ } else {
+ array_push($result, $cust);
+ }
+ }
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/CustomColumn.php b/lib/CustomColumn.php
new file mode 100644
index 000000000..9a52ff0fc
--- /dev/null
+++ b/lib/CustomColumn.php
@@ -0,0 +1,109 @@
+
+ */
+
+/**
+ * A CustomColumn with an value
+ */
+class CustomColumn extends Base
+{
+ /* @var string|integer the ID of the value */
+ public $valueID;
+ /* @var string the (string) representation of the value */
+ public $value;
+ /* @var CustomColumnType the custom column that contains the value */
+ public $customColumnType;
+ /* @var string the value encoded for HTML displaying */
+ public $htmlvalue;
+
+ /**
+ * CustomColumn constructor.
+ *
+ * @param integer|string|null $pid id of the chosen value
+ * @param string $pvalue string representation of the value
+ * @param CustomColumnType $pcustomColumnType the CustomColumn this value lives in
+ */
+ public function __construct($pid, $pvalue, $pcustomColumnType)
+ {
+ $this->valueID = $pid;
+ $this->value = $pvalue;
+ $this->customColumnType = $pcustomColumnType;
+ $this->htmlvalue = $this->customColumnType->encodeHTMLValue($this->value);
+ }
+
+ /**
+ * Get the URI to show all books with this value
+ *
+ * @return string
+ */
+ public function getUri()
+ {
+ return $this->customColumnType->getUri($this->valueID);
+ }
+
+ /**
+ * Get the EntryID to show all books with this value
+ *
+ * @return string
+ */
+ public function getEntryId()
+ {
+ return $this->customColumnType->getEntryId($this->valueID);
+ }
+
+ /**
+ * Get the query to find all books with this value
+ * the returning array has two values:
+ * - first the query (string)
+ * - second an array of all PreparedStatement parameters
+ *
+ * @return array
+ */
+ public function getQuery()
+ {
+ return $this->customColumnType->getQuery($this->valueID);
+ }
+
+ /**
+ * Return the value of this column as an HTML snippet
+ *
+ * @return string
+ */
+ public function getHTMLEncodedValue()
+ {
+ return $this->htmlvalue;
+ }
+
+ /**
+ * Create an CustomColumn by CustomColumnID and ValueID
+ *
+ * @param integer $customId the id of the customColumn
+ * @param integer $id the id of the chosen value
+ * @return CustomColumn|null
+ */
+ public static function createCustom($customId, $id)
+ {
+ $columnType = CustomColumnType::createByCustomID($customId);
+
+ return $columnType->getCustom($id);
+ }
+
+ /**
+ * Return this object as an array
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ return [
+ 'valueID' => $this->valueID,
+ 'value' => $this->value,
+ 'customColumnType' => (array)$this->customColumnType,
+ 'htmlvalue' => $this->htmlvalue,
+ ];
+ }
+}
diff --git a/lib/CustomColumnType.php b/lib/CustomColumnType.php
new file mode 100644
index 000000000..38fc343b8
--- /dev/null
+++ b/lib/CustomColumnType.php
@@ -0,0 +1,317 @@
+
+ */
+
+/**
+ * A single calibre custom column
+ */
+abstract class CustomColumnType extends Base
+{
+ public const ALL_CUSTOMS_ID = "cops:custom";
+
+ public const CUSTOM_TYPE_TEXT = "text"; // type 1 + 2
+ public const CUSTOM_TYPE_COMMENT = "comments"; // type 3
+ public const CUSTOM_TYPE_SERIES = "series"; // type 4
+ public const CUSTOM_TYPE_ENUM = "enumeration"; // type 5
+ public const CUSTOM_TYPE_DATE = "datetime"; // type 6
+ public const CUSTOM_TYPE_FLOAT = "float"; // type 7
+ public const CUSTOM_TYPE_INT = "int"; // type 8
+ public const CUSTOM_TYPE_RATING = "rating"; // type 9
+ public const CUSTOM_TYPE_BOOL = "bool"; // type 10
+ public const CUSTOM_TYPE_COMPOSITE = "composite"; // type 11 + 12
+
+ /** @var array[integer]CustomColumnType */
+ private static $customColumnCacheID = [];
+
+ /** @var array[string]CustomColumnType */
+ private static $customColumnCacheLookup = [];
+
+ /** @var integer the id of this column */
+ public $customId;
+ /** @var string name/title of this column */
+ public $columnTitle;
+ /** @var string the datatype of this column (one of the CUSTOM_TYPE_* constant values) */
+ public $datatype;
+ /** @var null|Entry[] */
+ private $customValues = null;
+
+ protected function __construct($pcustomId, $pdatatype)
+ {
+ $this->columnTitle = self::getTitleByCustomID($pcustomId);
+ $this->customId = $pcustomId;
+ $this->datatype = $pdatatype;
+ $this->customValues = null;
+ }
+
+ /**
+ * The URI to show all book swith a specific value in this column
+ *
+ * @param string|integer $id the id of the value to show
+ * @return string
+ */
+ public function getUri($id)
+ {
+ return "?page=" . parent::PAGE_CUSTOM_DETAIL . "&custom={$this->customId}&id={$id}";
+ }
+
+ /**
+ * The URI to show all the values of this column
+ *
+ * @return string
+ */
+ public function getUriAllCustoms()
+ {
+ return "?page=" . parent::PAGE_ALL_CUSTOMS . "&custom={$this->customId}";
+ }
+
+ /**
+ * The EntryID to show all book swith a specific value in this column
+ *
+ * @param string|integer $id the id of the value to show
+ * @return string
+ */
+ public function getEntryId($id)
+ {
+ return self::ALL_CUSTOMS_ID . ":" . $this->customId . ":" . $id;
+ }
+
+ /**
+ * The EntryID to show all the values of this column
+ *
+ * @return string
+ */
+ public function getAllCustomsId()
+ {
+ return self::ALL_CUSTOMS_ID . ":" . $this->customId;
+ }
+
+ /**
+ * The title of this column
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->columnTitle;
+ }
+
+ /**
+ * The description of this column as it is definied in the database
+ *
+ * @return string|null
+ */
+ public function getDatabaseDescription()
+ {
+ $result = $this->getDb()->prepare('SELECT display FROM custom_columns WHERE id = ?');
+ $result->execute([$this->customId]);
+ if ($post = $result->fetchObject()) {
+ $json = json_decode($post->display);
+ return (isset($json->description) && !empty($json->description)) ? $json->description : null;
+ }
+ return null;
+ }
+
+ /**
+ * Get the Entry for this column
+ * This is used in the initializeContent method to display e.g. the index page
+ *
+ * @return Entry
+ */
+ public function getCount()
+ {
+ $ptitle = $this->getTitle();
+ $pid = $this->getAllCustomsId();
+ $pcontent = $this->getDescription();
+ $pcontentType = $this->datatype;
+ $plinkArray = [new LinkNavigation($this->getUriAllCustoms())];
+ $pclass = "";
+ $pcount = $this->getDistinctValueCount();
+
+ return new Entry($ptitle, $pid, $pcontent, $pcontentType, $plinkArray, $pclass, $pcount);
+ }
+
+ /**
+ * Get the amount of distinct values for this column
+ *
+ * @return int
+ */
+ protected function getDistinctValueCount()
+ {
+ return count($this->getAllCustomValues());
+ }
+
+ /**
+ * Encode a value of this column ready to be displayed in an HTML document
+ *
+ * @param integer|string $value
+ * @return string
+ */
+ public function encodeHTMLValue($value)
+ {
+ return htmlspecialchars($value);
+ }
+
+ /**
+ * Get the datatype of a CustomColumn by its customID
+ *
+ * @param integer $customId
+ * @return string|null
+ */
+ private static function getDatatypeByCustomID($customId)
+ {
+ $result = parent::getDb()->prepare('SELECT datatype FROM custom_columns WHERE id = ?');
+ $result->execute([$customId]);
+ if ($post = $result->fetchObject()) {
+ return $post->datatype;
+ }
+ return null;
+ }
+
+ /**
+ * Create a CustomColumnType by CustomID
+ *
+ * @param integer $customId the id of the custom column
+ * @return CustomColumnType|null
+ * @throws Exception If the $customId is not found or the datatype is unknown
+ */
+ public static function createByCustomID($customId)
+ {
+ // Reuse already created CustomColumns for performance
+ if (array_key_exists($customId, self::$customColumnCacheID)) {
+ return self::$customColumnCacheID[$customId];
+ }
+
+ $datatype = self::getDatatypeByCustomID($customId);
+
+ switch ($datatype) {
+ case self::CUSTOM_TYPE_TEXT:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeText($customId);
+ case self::CUSTOM_TYPE_SERIES:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeSeries($customId);
+ case self::CUSTOM_TYPE_ENUM:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeEnumeration($customId);
+ case self::CUSTOM_TYPE_COMMENT:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeComment($customId);
+ case self::CUSTOM_TYPE_DATE:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeDate($customId);
+ case self::CUSTOM_TYPE_FLOAT:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeFloat($customId);
+ case self::CUSTOM_TYPE_INT:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeInteger($customId);
+ case self::CUSTOM_TYPE_RATING:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeRating($customId);
+ case self::CUSTOM_TYPE_BOOL:
+ return self::$customColumnCacheID[$customId] = new CustomColumnTypeBool($customId);
+ case self::CUSTOM_TYPE_COMPOSITE:
+ return null; //TODO Currently not supported
+ default:
+ throw new Exception("Unkown column type: " . $datatype);
+ }
+ }
+
+ /**
+ * Create a CustomColumnType by its lookup name
+ *
+ * @param string $lookup the lookup-name of the custom column
+ * @return CustomColumnType|null
+ */
+ public static function createByLookup($lookup)
+ {
+ // Reuse already created CustomColumns for performance
+ if (array_key_exists($lookup, self::$customColumnCacheLookup)) {
+ return self::$customColumnCacheLookup[$lookup];
+ }
+
+ $result = parent::getDb()->prepare('SELECT id FROM custom_columns WHERE label = ?');
+ $result->execute([$lookup]);
+ if ($post = $result->fetchObject()) {
+ return self::$customColumnCacheLookup[$lookup] = self::createByCustomID($post->id);
+ }
+ return self::$customColumnCacheLookup[$lookup] = null;
+ }
+
+ /**
+ * Return an entry array for all possible (in the DB used) values of this column
+ * These are the values used in the getUriAllCustoms() page
+ *
+ * @return Entry[]
+ */
+ public function getAllCustomValues()
+ {
+ // lazy loading
+ if ($this->customValues == null) {
+ $this->customValues = $this->getAllCustomValuesFromDatabase();
+ }
+
+ return $this->customValues;
+ }
+
+ /**
+ * Get the title of a CustomColumn by its customID
+ *
+ * @param integer $customId
+ * @return string
+ */
+ protected static function getTitleByCustomID($customId)
+ {
+ $result = parent::getDb()->prepare('SELECT name FROM custom_columns WHERE id = ?');
+ $result->execute([$customId]);
+ if ($post = $result->fetchObject()) {
+ return $post->name;
+ }
+ return "";
+ }
+
+ /**
+ * Get the query to find all books with a specific value of this column
+ * the returning array has two values:
+ * - first the query (string)
+ * - second an array of all PreparedStatement parameters
+ *
+ * @param string|integer $id the id of the searched value
+ * @return array|null
+ */
+ abstract public function getQuery($id);
+
+ /**
+ * Get a CustomColumn for a specified (by ID) value
+ *
+ * @param string|integer $id the id of the searched value
+ * @return CustomColumn|null
+ */
+ abstract public function getCustom($id);
+
+ /**
+ * Return an entry array for all possible (in the DB used) values of this column by querying the database
+ *
+ * @return Entry[]|null
+ */
+ abstract protected function getAllCustomValuesFromDatabase();
+
+ /**
+ * The description used in the index page
+ *
+ * @return string
+ */
+ abstract public function getDescription();
+
+ /**
+ * Find the value of this column for a specific book
+ *
+ * @param Book $book
+ * @return CustomColumn
+ */
+ abstract public function getCustomByBook($book);
+
+ /**
+ * Is this column searchable by value
+ * only searchable columns can be displayed on the index page
+ *
+ * @return bool
+ */
+ abstract public function isSearchable();
+}
diff --git a/lib/CustomColumnTypeBool.php b/lib/CustomColumnTypeBool.php
new file mode 100644
index 000000000..b6ce5275a
--- /dev/null
+++ b/lib/CustomColumnTypeBool.php
@@ -0,0 +1,94 @@
+
+ */
+
+class CustomColumnTypeBool extends CustomColumnType
+{
+ // PHP pre 5.6 does not support const arrays
+ private $BOOLEAN_NAMES = [
+ -1 => "customcolumn.boolean.unknown", // localize("customcolumn.boolean.unknown")
+ 0 => "customcolumn.boolean.no", // localize("customcolumn.boolean.no")
+ +1 => "customcolumn.boolean.yes", // localize("customcolumn.boolean.yes")
+ ];
+
+ protected function __construct($pcustomId)
+ {
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_BOOL);
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ public function getQuery($id)
+ {
+ if ($id == -1) {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_BOOL_NULL, "{0}", "{1}", $this->getTableName());
+ return [$query, []];
+ } elseif ($id == 0) {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_BOOL_FALSE, "{0}", "{1}", $this->getTableName());
+ return [$query, []];
+ } elseif ($id == 1) {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_BOOL_TRUE, "{0}", "{1}", $this->getTableName());
+ return [$query, []];
+ } else {
+ return null;
+ }
+ }
+
+ public function getCustom($id)
+ {
+ return new CustomColumn($id, localize($this->BOOLEAN_NAMES[$id]), $this);
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ $queryFormat = "SELECT coalesce({0}.value, -1) AS id, count(*) AS count FROM books LEFT JOIN {0} ON books.id = {0}.book GROUP BY {0}.value ORDER BY {0}.value";
+ $query = str_format($queryFormat, $this->getTableName());
+ $result = $this->getDb()->query($query);
+
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $entryPContent = str_format(localize("bookword", $post->count), $post->count);
+ $entryPLinkArray = [new LinkNavigation($this->getUri($post->id))];
+
+ $entry = new Entry(localize($this->BOOLEAN_NAMES[$post->id]), $this->getEntryId($post->id), $entryPContent, $this->datatype, $entryPLinkArray, "", $post->count);
+
+ array_push($entryArray, $entry);
+ }
+ return $entryArray;
+ }
+
+ public function getDescription()
+ {
+ return localize("customcolumn.description.bool");
+ }
+
+ public function getCustomByBook($book)
+ {
+ $queryFormat = "SELECT {0}.value AS boolvalue FROM {0} WHERE {0}.book = {1}";
+ $query = str_format($queryFormat, $this->getTableName(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($post->boolvalue, localize($this->BOOLEAN_NAMES[$post->boolvalue]), $this);
+ } else {
+ return new CustomColumn(-1, localize($this->BOOLEAN_NAMES[-1]), $this);
+ }
+ }
+
+ public function isSearchable()
+ {
+ return true;
+ }
+}
diff --git a/lib/CustomColumnTypeComment.php b/lib/CustomColumnTypeComment.php
new file mode 100644
index 000000000..b8f2400da
--- /dev/null
+++ b/lib/CustomColumnTypeComment.php
@@ -0,0 +1,72 @@
+
+ */
+
+class CustomColumnTypeComment extends CustomColumnType
+{
+ protected function __construct($pcustomId)
+ {
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_COMMENT);
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ public function getQuery($id)
+ {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_DIRECT_ID, "{0}", "{1}", $this->getTableName());
+ return [$query, [$id]];
+ }
+
+ public function getCustom($id)
+ {
+ return new CustomColumn($id, $id, $this);
+ }
+
+ public function encodeHTMLValue($value)
+ {
+ return "
" . $value . "
"; // no htmlspecialchars, this is already HTML
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ return null;
+ }
+
+ public function getDescription()
+ {
+ $desc = $this->getDatabaseDescription();
+ if ($desc === null || empty($desc)) {
+ $desc = str_format(localize("customcolumn.description"), $this->getTitle());
+ }
+ return $desc;
+ }
+
+ public function getCustomByBook($book)
+ {
+ $queryFormat = "SELECT {0}.id AS id, {0}.value AS value FROM {0} WHERE {0}.book = {1}";
+ $query = str_format($queryFormat, $this->getTableName(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($post->id, $post->value, $this);
+ }
+ return new CustomColumn(null, localize("customcolumn.float.unknown"), $this);
+ }
+
+ public function isSearchable()
+ {
+ return false;
+ }
+}
diff --git a/lib/CustomColumnTypeDate.php b/lib/CustomColumnTypeDate.php
new file mode 100644
index 000000000..e0d09952e
--- /dev/null
+++ b/lib/CustomColumnTypeDate.php
@@ -0,0 +1,89 @@
+
+ */
+
+class CustomColumnTypeDate extends CustomColumnType
+{
+ protected function __construct($pcustomId)
+ {
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_DATE);
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ public function getQuery($id)
+ {
+ $date = new DateTime($id);
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_DATE, "{0}", "{1}", $this->getTableName());
+ return [$query, [$date->format("Y-m-d")]];
+ }
+
+ public function getCustom($id)
+ {
+ $date = new DateTime($id);
+
+ return new CustomColumn($id, $date->format(localize("customcolumn.date.format")), $this);
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ $queryFormat = "SELECT date(value) AS datevalue, count(*) AS count FROM {0} GROUP BY datevalue";
+ $query = str_format($queryFormat, $this->getTableName());
+ $result = $this->getDb()->query($query);
+
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $date = new DateTime($post->datevalue);
+ $id = $date->format("Y-m-d");
+
+ $entryPContent = str_format(localize("bookword", $post->count), $post->count);
+ $entryPLinkArray = [new LinkNavigation($this->getUri($id))];
+
+ $entry = new Entry($date->format(localize("customcolumn.date.format")), $this->getEntryId($id), $entryPContent, $this->datatype, $entryPLinkArray, "", $post->count);
+
+ array_push($entryArray, $entry);
+ }
+
+ return $entryArray;
+ }
+
+ public function getDescription()
+ {
+ $desc = $this->getDatabaseDescription();
+ if ($desc === null || empty($desc)) {
+ $desc = str_format(localize("customcolumn.description"), $this->getTitle());
+ }
+ return $desc;
+ }
+
+ public function getCustomByBook($book)
+ {
+ $queryFormat = "SELECT date({0}.value) AS datevalue FROM {0} WHERE {0}.book = {1}";
+ $query = str_format($queryFormat, $this->getTableName(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ $date = new DateTime($post->datevalue);
+
+ return new CustomColumn($date->format("Y-m-d"), $date->format(localize("customcolumn.date.format")), $this);
+ }
+ return new CustomColumn(null, localize("customcolumn.date.unknown"), $this);
+ }
+
+ public function isSearchable()
+ {
+ return true;
+ }
+}
diff --git a/lib/CustomColumnTypeEnumeration.php b/lib/CustomColumnTypeEnumeration.php
new file mode 100644
index 000000000..388cf8abc
--- /dev/null
+++ b/lib/CustomColumnTypeEnumeration.php
@@ -0,0 +1,102 @@
+
+ */
+
+class CustomColumnTypeEnumeration extends CustomColumnType
+{
+ protected function __construct($pcustomId)
+ {
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_ENUM);
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ /**
+ * Get the name of the linking sqlite table for this column
+ * (or NULL if there is no linktable)
+ *
+ * @return string
+ */
+ private function getTableLinkName()
+ {
+ return "books_custom_column_{$this->customId}_link";
+ }
+
+ /**
+ * Get the name of the linking column in the linktable
+ *
+ * @return string
+ */
+ private function getTableLinkColumn()
+ {
+ return "value";
+ }
+
+ public function getQuery($id)
+ {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM, "{0}", "{1}", $this->getTableLinkName(), $this->getTableLinkColumn());
+ return [$query, [$id]];
+ }
+
+ public function getCustom($id)
+ {
+ $result = $this->getDb()->prepare(str_format("SELECT id, value AS name FROM {0} WHERE id = ?", $this->getTableName()));
+ $result->execute([$id]);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($id, $post->name, $this);
+ }
+ return null;
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ $queryFormat = "SELECT {0}.id AS id, {0}.value AS name, count(*) AS count FROM {0}, {1} WHERE {0}.id = {1}.{2} GROUP BY {0}.id, {0}.value ORDER BY {0}.value";
+ $query = str_format($queryFormat, $this->getTableName(), $this->getTableLinkName(), $this->getTableLinkColumn());
+
+ $result = $this->getDb()->query($query);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $entryPContent = str_format(localize("bookword", $post->count), $post->count);
+ $entryPLinkArray = [new LinkNavigation($this->getUri($post->id))];
+
+ $entry = new Entry($post->name, $this->getEntryId($post->id), $entryPContent, $this->datatype, $entryPLinkArray, "", $post->count);
+
+ array_push($entryArray, $entry);
+ }
+ return $entryArray;
+ }
+
+ public function getDescription()
+ {
+ return str_format(localize("customcolumn.description.enum", $this->getDistinctValueCount()), $this->getDistinctValueCount());
+ }
+
+ public function getCustomByBook($book)
+ {
+ $queryFormat = "SELECT {0}.id AS id, {0}.{2} AS name FROM {0}, {1} WHERE {0}.id = {1}.{2} AND {1}.book = {3}";
+ $query = str_format($queryFormat, $this->getTableName(), $this->getTableLinkName(), $this->getTableLinkColumn(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($post->id, $post->name, $this);
+ }
+ return new CustomColumn(null, localize("customcolumn.enum.unknown"), $this);
+ }
+
+ public function isSearchable()
+ {
+ return true;
+ }
+}
diff --git a/lib/CustomColumnTypeFloat.php b/lib/CustomColumnTypeFloat.php
new file mode 100644
index 000000000..f92cb3990
--- /dev/null
+++ b/lib/CustomColumnTypeFloat.php
@@ -0,0 +1,80 @@
+
+ */
+
+class CustomColumnTypeFloat extends CustomColumnType
+{
+ protected function __construct($pcustomId)
+ {
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_FLOAT);
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ public function getQuery($id)
+ {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_DIRECT, "{0}", "{1}", $this->getTableName());
+ return [$query, [$id]];
+ }
+
+ public function getCustom($id)
+ {
+ return new CustomColumn($id, $id, $this);
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ $queryFormat = "SELECT value AS id, count(*) AS count FROM {0} GROUP BY value";
+ $query = str_format($queryFormat, $this->getTableName());
+
+ $result = $this->getDb()->query($query);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $entryPContent = str_format(localize("bookword", $post->count), $post->count);
+ $entryPLinkArray = [new LinkNavigation($this->getUri($post->id))];
+
+ $entry = new Entry($post->id, $this->getEntryId($post->id), $entryPContent, $this->datatype, $entryPLinkArray, "", $post->count);
+
+ array_push($entryArray, $entry);
+ }
+ return $entryArray;
+ }
+
+ public function getDescription()
+ {
+ $desc = $this->getDatabaseDescription();
+ if ($desc === null || empty($desc)) {
+ $desc = str_format(localize("customcolumn.description"), $this->getTitle());
+ }
+ return $desc;
+ }
+
+ public function getCustomByBook($book)
+ {
+ $queryFormat = "SELECT {0}.value AS value FROM {0} WHERE {0}.book = {1}";
+ $query = str_format($queryFormat, $this->getTableName(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($post->value, $post->value, $this);
+ }
+ return new CustomColumn(null, localize("customcolumn.float.unknown"), $this);
+ }
+
+ public function isSearchable()
+ {
+ return true;
+ }
+}
diff --git a/lib/CustomColumnTypeInteger.php b/lib/CustomColumnTypeInteger.php
new file mode 100644
index 000000000..7d4afcef9
--- /dev/null
+++ b/lib/CustomColumnTypeInteger.php
@@ -0,0 +1,93 @@
+
+ */
+
+class CustomColumnTypeInteger extends CustomColumnType
+{
+ private static $type;
+
+ protected function __construct($pcustomId, $datatype = self::CUSTOM_TYPE_INT)
+ {
+ self::$type = $datatype;
+
+ switch ($datatype) {
+ case self::CUSTOM_TYPE_INT:
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_INT);
+ break;
+ case self::CUSTOM_TYPE_FLOAT:
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_FLOAT);
+ break;
+ default:
+ throw new UnexpectedValueException();
+ }
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ public function getQuery($id)
+ {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_DIRECT, "{0}", "{1}", $this->getTableName());
+ return [$query, [$id]];
+ }
+
+ public function getCustom($id)
+ {
+ return new CustomColumn($id, $id, $this);
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ $queryFormat = "SELECT value AS id, count(*) AS count FROM {0} GROUP BY value";
+ $query = str_format($queryFormat, $this->getTableName());
+
+ $result = $this->getDb()->query($query);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $entryPContent = str_format(localize("bookword", $post->count), $post->count);
+ $entryPLinkArray = [new LinkNavigation($this->getUri($post->id))];
+
+ $entry = new Entry($post->id, $this->getEntryId($post->id), $entryPContent, $this->datatype, $entryPLinkArray, "", $post->count);
+
+ array_push($entryArray, $entry);
+ }
+ return $entryArray;
+ }
+
+ public function getDescription()
+ {
+ $desc = $this->getDatabaseDescription();
+ if ($desc === null || empty($desc)) {
+ $desc = str_format(localize("customcolumn.description"), $this->getTitle());
+ }
+ return $desc;
+ }
+
+ public function getCustomByBook($book)
+ {
+ $queryFormat = "SELECT {0}.value AS value FROM {0} WHERE {0}.book = {1}";
+ $query = str_format($queryFormat, $this->getTableName(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($post->value, $post->value, $this);
+ }
+ return new CustomColumn(null, localize("customcolumn.int.unknown"), $this);
+ }
+
+ public function isSearchable()
+ {
+ return true;
+ }
+}
diff --git a/lib/CustomColumnTypeRating.php b/lib/CustomColumnTypeRating.php
new file mode 100644
index 000000000..4a74c7ffa
--- /dev/null
+++ b/lib/CustomColumnTypeRating.php
@@ -0,0 +1,110 @@
+
+ */
+
+class CustomColumnTypeRating extends CustomColumnType
+{
+ protected function __construct($pcustomId)
+ {
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_RATING);
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ /**
+ * Get the name of the linking sqlite table for this column
+ * (or NULL if there is no linktable)
+ *
+ * @return string
+ */
+ private function getTableLinkName()
+ {
+ return "books_custom_column_{$this->customId}_link";
+ }
+
+ /**
+ * Get the name of the linking column in the linktable
+ *
+ * @return string
+ */
+ private function getTableLinkColumn()
+ {
+ return "value";
+ }
+
+ public function getQuery($id)
+ {
+ if ($id == 0) {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_RATING_NULL, "{0}", "{1}", $this->getTableLinkName(), $this->getTableName(), $this->getTableLinkColumn());
+ return [$query, []];
+ } else {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM_RATING, "{0}", "{1}", $this->getTableLinkName(), $this->getTableName(), $this->getTableLinkColumn());
+ return [$query, [$id]];
+ }
+ }
+
+ public function getCustom($id)
+ {
+ return new CustomColumn($id, str_format(localize("customcolumn.stars", $id / 2), $id / 2), $this);
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ $queryFormat = "SELECT coalesce({0}.value, 0) AS value, count(*) AS count FROM books LEFT JOIN {1} ON books.id = {1}.book LEFT JOIN {0} ON {0}.id = {1}.value GROUP BY coalesce({0}.value, -1)";
+ $query = str_format($queryFormat, $this->getTableName(), $this->getTableLinkName());
+ $result = $this->getDb()->query($query);
+
+ $countArray = [0 => 0, 2 => 0, 4 => 0, 6 => 0, 8 => 0, 10 => 0];
+ while ($row = $result->fetchObject()) {
+ $countArray[$row->value] = $row->count;
+ }
+
+ $entryArray = [];
+
+ for ($i = 0; $i <= 5; $i++) {
+ $count = $countArray[$i * 2];
+ $name = str_format(localize("customcolumn.stars", $i), $i);
+ $entryid = $this->getEntryId($i * 2);
+ $content = str_format(localize("bookword", $count), $count);
+ $linkarray = [new LinkNavigation($this->getUri($i * 2))];
+ $entry = new Entry($name, $entryid, $content, $this->datatype, $linkarray, "", $count);
+ array_push($entryArray, $entry);
+ }
+
+ return $entryArray;
+ }
+
+ public function getDescription()
+ {
+ return localize("customcolumn.description.rating");
+ }
+
+ public function getCustomByBook($book)
+ {
+ $queryFormat = "SELECT {0}.value AS value FROM {0}, {1} WHERE {0}.id = {1}.{2} AND {1}.book = {3}";
+ $query = str_format($queryFormat, $this->getTableName(), $this->getTableLinkName(), $this->getTableLinkColumn(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($post->value, str_format(localize("customcolumn.stars", $post->value / 2), $post->value / 2), $this);
+ }
+ return new CustomColumn(null, localize("customcolumn.rating.unknown"), $this);
+ }
+
+ public function isSearchable()
+ {
+ return true;
+ }
+}
diff --git a/lib/CustomColumnTypeSeries.php b/lib/CustomColumnTypeSeries.php
new file mode 100644
index 000000000..8d49f0895
--- /dev/null
+++ b/lib/CustomColumnTypeSeries.php
@@ -0,0 +1,102 @@
+
+ */
+
+class CustomColumnTypeSeries extends CustomColumnType
+{
+ protected function __construct($pcustomId)
+ {
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_SERIES);
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ /**
+ * Get the name of the linking sqlite table for this column
+ * (or NULL if there is no linktable)
+ *
+ * @return string
+ */
+ private function getTableLinkName()
+ {
+ return "books_custom_column_{$this->customId}_link";
+ }
+
+ /**
+ * Get the name of the linking column in the linktable
+ *
+ * @return string
+ */
+ private function getTableLinkColumn()
+ {
+ return "value";
+ }
+
+ public function getQuery($id)
+ {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM, "{0}", "{1}", $this->getTableLinkName(), $this->getTableLinkColumn());
+ return [$query, [$id]];
+ }
+
+ public function getCustom($id)
+ {
+ $result = $this->getDb()->prepare(str_format("SELECT id, value AS name FROM {0} WHERE id = ?", $this->getTableName()));
+ $result->execute([$id]);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($id, $post->name, $this);
+ }
+ return null;
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ $queryFormat = "SELECT {0}.id AS id, {0}.value AS name, count(*) AS count FROM {0}, {1} WHERE {0}.id = {1}.{2} GROUP BY {0}.id, {0}.value ORDER BY {0}.value";
+ $query = str_format($queryFormat, $this->getTableName(), $this->getTableLinkName(), $this->getTableLinkColumn());
+
+ $result = $this->getDb()->query($query);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $entryPContent = str_format(localize("bookword", $post->count), $post->count);
+ $entryPLinkArray = [new LinkNavigation($this->getUri($post->id))];
+
+ $entry = new Entry($post->name, $this->getEntryId($post->id), $entryPContent, $this->datatype, $entryPLinkArray, "", $post->count);
+
+ array_push($entryArray, $entry);
+ }
+ return $entryArray;
+ }
+
+ public function getDescription()
+ {
+ return str_format(localize("customcolumn.description.series", $this->getDistinctValueCount()), $this->getDistinctValueCount());
+ }
+
+ public function getCustomByBook($book)
+ {
+ $queryFormat = "SELECT {0}.id AS id, {1}.{2} AS name, {1}.extra AS extra FROM {0}, {1} WHERE {0}.id = {1}.{2} AND {1}.book = {3}";
+ $query = str_format($queryFormat, $this->getTableName(), $this->getTableLinkName(), $this->getTableLinkColumn(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($post->id, $post->name . " [" . $post->extra . "]", $this);
+ }
+ return new CustomColumn(null, "", $this);
+ }
+
+ public function isSearchable()
+ {
+ return true;
+ }
+}
diff --git a/lib/CustomColumnTypeText.php b/lib/CustomColumnTypeText.php
new file mode 100644
index 000000000..4f609f01a
--- /dev/null
+++ b/lib/CustomColumnTypeText.php
@@ -0,0 +1,135 @@
+
+ */
+
+class CustomColumnTypeText extends CustomColumnType
+{
+ private static $type;
+
+ protected function __construct($pcustomId, $datatype = self::CUSTOM_TYPE_TEXT)
+ {
+ self::$type = $datatype;
+
+ switch ($datatype) {
+ case self::CUSTOM_TYPE_TEXT:
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_TEXT);
+ break;
+ case self::CUSTOM_TYPE_ENUM:
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_ENUM);
+ break;
+ case self::CUSTOM_TYPE_SERIES:
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_SERIES);
+ break;
+ default:
+ throw new UnexpectedValueException();
+ }
+ parent::__construct($pcustomId, self::CUSTOM_TYPE_TEXT);
+ }
+
+ /**
+ * Get the name of the sqlite table for this column
+ *
+ * @return string
+ */
+ private function getTableName()
+ {
+ return "custom_column_{$this->customId}";
+ }
+
+ /**
+ * Get the name of the linking sqlite table for this column
+ * (or NULL if there is no linktable)
+ *
+ * @return string
+ */
+ private function getTableLinkName()
+ {
+ return "books_custom_column_{$this->customId}_link";
+ }
+
+ /**
+ * Get the name of the linking column in the linktable
+ *
+ * @return string
+ */
+ private function getTableLinkColumn()
+ {
+ return "value";
+ }
+
+ public function getQuery($id)
+ {
+ $query = str_format(Book::SQL_BOOKS_BY_CUSTOM, "{0}", "{1}", $this->getTableLinkName(), $this->getTableLinkColumn());
+ return [$query, [$id]];
+ }
+
+ public function getCustom($id)
+ {
+ $result = $this->getDb()->prepare(str_format("SELECT id, value AS name FROM {0} WHERE id = ?", $this->getTableName()));
+ $result->execute([$id]);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($id, $post->name, $this);
+ }
+ return null;
+ }
+
+ protected function getAllCustomValuesFromDatabase()
+ {
+ $queryFormat = "SELECT {0}.id AS id, {0}.value AS name, count(*) AS count FROM {0}, {1} WHERE {0}.id = {1}.{2} GROUP BY {0}.id, {0}.value ORDER BY {0}.value";
+ $query = str_format($queryFormat, $this->getTableName(), $this->getTableLinkName(), $this->getTableLinkColumn());
+
+ $result = $this->getDb()->query($query);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $entryPContent = str_format(localize("bookword", $post->count), $post->count);
+ $entryPLinkArray = [new LinkNavigation($this->getUri($post->id))];
+
+ $entry = new Entry($post->name, $this->getEntryId($post->id), $entryPContent, $this->datatype, $entryPLinkArray, "", $post->count);
+
+ array_push($entryArray, $entry);
+ }
+ return $entryArray;
+ }
+
+ public function getDescription()
+ {
+ $desc = $this->getDatabaseDescription();
+ if ($desc === null || empty($desc)) {
+ $desc = str_format(localize("customcolumn.description"), $this->getTitle());
+ }
+ return $desc;
+ }
+
+ public function getCustomByBook($book)
+ {
+ switch (self::$type) {
+ case self::CUSTOM_TYPE_TEXT:
+ $queryFormat = "SELECT {0}.id AS id, {0}.{2} AS name FROM {0}, {1} WHERE {0}.id = {1}.{2} AND {1}.book = {3} ORDER BY {0}.value";
+ break;
+ case self::CUSTOM_TYPE_ENUM:
+ $queryFormat = "SELECT {0}.id AS id, {0}.{2} AS name FROM {0}, {1} WHERE {0}.id = {1}.{2} AND {1}.book = {3}";
+ break;
+ case self::CUSTOM_TYPE_SERIES:
+ $queryFormat = "SELECT {0}.id AS id, {1}.{2} AS name, {1}.extra AS extra FROM {0}, {1} WHERE {0}.id = {1}.{2} AND {1}.book = {3}";
+ break;
+ default:
+ throw new UnexpectedValueException();
+ }
+ $query = str_format($queryFormat, $this->getTableName(), $this->getTableLinkName(), $this->getTableLinkColumn(), $book->id);
+
+ $result = $this->getDb()->query($query);
+ if ($post = $result->fetchObject()) {
+ return new CustomColumn($post->id, $post->name, $this);
+ }
+ return new CustomColumn(null, "", $this);
+ }
+
+ public function isSearchable()
+ {
+ return true;
+ }
+}
diff --git a/lib/Data.php b/lib/Data.php
new file mode 100644
index 000000000..c2fa6b50f
--- /dev/null
+++ b/lib/Data.php
@@ -0,0 +1,245 @@
+
+ */
+
+class Data extends Base
+{
+ public $id;
+ public $name;
+ public $format;
+ public $realFormat;
+ public $extension;
+ public $book;
+
+ public static $mimetypes = [
+ 'aac' => 'audio/aac',
+ 'azw' => 'application/x-mobipocket-ebook',
+ 'azw1' => 'application/x-topaz-ebook',
+ 'azw2' => 'application/x-kindle-application',
+ 'azw3' => 'application/x-mobi8-ebook',
+ 'cbz' => 'application/x-cbz',
+ 'cbr' => 'application/x-cbr',
+ 'djv' => 'image/vnd.djvu',
+ 'djvu' => 'image/vnd.djvu',
+ 'doc' => 'application/msword',
+ 'epub' => 'application/epub+zip',
+ 'fb2' => 'text/fb2+xml',
+ 'ibooks'=> 'application/x-ibooks+zip',
+ 'kepub' => 'application/epub+zip',
+ 'kobo' => 'application/x-koboreader-ebook',
+ 'm4a' => 'audio/mp4',
+ 'mobi' => 'application/x-mobipocket-ebook',
+ 'mp3' => 'audio/mpeg',
+ 'lit' => 'application/x-ms-reader',
+ 'lrs' => 'text/x-sony-bbeb+xml',
+ 'lrf' => 'application/x-sony-bbeb',
+ 'lrx' => 'application/x-sony-bbeb',
+ 'ncx' => 'application/x-dtbncx+xml',
+ 'opf' => 'application/oebps-package+xml',
+ 'otf' => 'application/x-font-opentype',
+ 'pdb' => 'application/vnd.palm',
+ 'pdf' => 'application/pdf',
+ 'prc' => 'application/x-mobipocket-ebook',
+ 'rtf' => 'application/rtf',
+ 'svg' => 'image/svg+xml',
+ 'ttf' => 'application/x-font-truetype',
+ 'tpz' => 'application/x-topaz-ebook',
+ 'wav' => 'audio/wav',
+ 'wmf' => 'image/wmf',
+ 'xhtml' => 'application/xhtml+xml',
+ 'xpgt' => 'application/adobe-page-template+xml',
+ 'zip' => 'application/zip',
+ ];
+
+ public function __construct($post, $book = null)
+ {
+ $this->id = $post->id;
+ $this->name = $post->name;
+ $this->format = $post->format;
+ $this->realFormat = str_replace("ORIGINAL_", "", $post->format);
+ $this->extension = strtolower($this->realFormat);
+ $this->book = $book;
+ }
+
+ public function isKnownType()
+ {
+ return array_key_exists($this->extension, self::$mimetypes);
+ }
+
+ public function getMimeType()
+ {
+ $result = "application/octet-stream";
+ if ($this->isKnownType()) {
+ return self::$mimetypes [$this->extension];
+ } elseif (function_exists('finfo_open') === true) {
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+
+ if ($finfo !== false) {
+ $result = finfo_file($finfo, $this->getLocalPath());
+ finfo_close($finfo);
+ }
+ }
+ return $result;
+ }
+
+ public function isEpubValidOnKobo()
+ {
+ return $this->format == "EPUB" || $this->format == "KEPUB";
+ }
+
+ public function getFilename()
+ {
+ return $this->name . "." . strtolower($this->format);
+ }
+
+ public function getUpdatedFilename()
+ {
+ return $this->book->getAuthorsSort() . " - " . $this->book->title;
+ }
+
+ public function getUpdatedFilenameEpub()
+ {
+ return $this->getUpdatedFilename() . ".epub";
+ }
+
+ public function getUpdatedFilenameKepub()
+ {
+ $str = $this->getUpdatedFilename() . ".kepub.epub";
+ return str_replace(
+ [':', '#', '&'],
+ ['-', '-', ' '],
+ $str
+ );
+ }
+
+ public function getDataLink($rel, $title = null, $view = false)
+ {
+ global $config;
+
+ if ($rel == Link::OPDS_ACQUISITION_TYPE && $config['cops_use_url_rewriting'] == "1") {
+ return $this->getHtmlLinkWithRewriting($title, $view);
+ }
+
+ return self::getLink($this->book, $this->extension, $this->getMimeType(), $rel, $this->getFilename(), $this->id, $title, null, $view);
+ }
+
+ public function getHtmlLink()
+ {
+ return $this->getDataLink(Link::OPDS_ACQUISITION_TYPE)->href;
+ }
+
+ public function getViewHtmlLink()
+ {
+ return $this->getDataLink(Link::OPDS_ACQUISITION_TYPE, null, true)->href;
+ }
+
+ public function getLocalPath()
+ {
+ return $this->book->path . "/" . $this->getFilename();
+ }
+
+ public function getHtmlLinkWithRewriting($title = null, $view = false)
+ {
+ global $config;
+
+ $database = "";
+ if (!is_null(GetUrlParam(DB))) {
+ $database = GetUrlParam(DB) . "/";
+ }
+
+ $prefix = "download";
+ if ($view) {
+ $prefix = "view";
+ }
+ $href = $prefix . "/" . $this->id . "/" . $database;
+
+ if ($config['cops_provide_kepub'] == "1" &&
+ $this->isEpubValidOnKobo() &&
+ preg_match("/Kobo/", $_SERVER['HTTP_USER_AGENT'])) {
+ $href .= rawurlencode($this->getUpdatedFilenameKepub());
+ } else {
+ $href .= rawurlencode($this->getFilename());
+ }
+ return new Link($href, $this->getMimeType(), Link::OPDS_ACQUISITION_TYPE, $title);
+ }
+
+ public static function getDataByBook($book)
+ {
+ global $config;
+
+ $out = [];
+
+ $sql = 'select id, format, name from data where book = ?';
+
+ $ignored_formats = $config['cops_ignored_formats'];
+ if (count($ignored_formats) > 0) {
+ $sql .= " and format not in ('"
+ . implode("','", $ignored_formats)
+ . "')";
+ }
+
+ $result = parent::getDb()->prepare($sql);
+ $result->execute([$book->id]);
+
+ while ($post = $result->fetchObject()) {
+ array_push($out, new Data($post, $book));
+ }
+ return $out;
+ }
+
+ public static function handleThumbnailLink($urlParam, $height)
+ {
+ global $config;
+
+ if (is_null($height)) {
+ if (preg_match('/feed.php/', $_SERVER["SCRIPT_NAME"])) {
+ $height = $config['cops_opds_thumbnail_height'];
+ } else {
+ $height = $config['cops_html_thumbnail_height'];
+ }
+ }
+ if ($config['cops_thumbnail_handling'] != "1") {
+ $urlParam = addURLParameter($urlParam, "height", $height);
+ }
+
+ return $urlParam;
+ }
+
+ public static function getLink($book, $type, $mime, $rel, $filename, $idData, $title = null, $height = null, $view = false)
+ {
+ global $config;
+
+ $urlParam = addURLParameter("", "data", $idData);
+ if ($view) {
+ $urlParam = addURLParameter($urlParam, "view", 1);
+ }
+
+ if (Base::useAbsolutePath() ||
+ $rel == Link::OPDS_THUMBNAIL_TYPE ||
+ ($type == "epub" && $config['cops_update_epub-metadata'])) {
+ if ($type != "jpg") {
+ $urlParam = addURLParameter($urlParam, "type", $type);
+ }
+ if ($rel == Link::OPDS_THUMBNAIL_TYPE) {
+ $urlParam = self::handleThumbnailLink($urlParam, $height);
+ }
+ $urlParam = addURLParameter($urlParam, "id", $book->id);
+ if (!is_null(GetUrlParam(DB))) {
+ $urlParam = addURLParameter($urlParam, DB, GetUrlParam(DB));
+ }
+ if ($config['cops_thumbnail_handling'] != "1" &&
+ !empty($config['cops_thumbnail_handling']) &&
+ $rel == Link::OPDS_THUMBNAIL_TYPE) {
+ return new Link($config['cops_thumbnail_handling'], $mime, $rel, $title);
+ } else {
+ return new Link("fetch.php?" . $urlParam, $mime, $rel, $title);
+ }
+ } else {
+ return new Link(str_replace('%2F', '/', rawurlencode($book->path."/".$filename)), $mime, $rel, $title);
+ }
+ }
+}
diff --git a/lib/Entry.php b/lib/Entry.php
new file mode 100644
index 000000000..8b2b59742
--- /dev/null
+++ b/lib/Entry.php
@@ -0,0 +1,83 @@
+
+ */
+
+class Entry
+{
+ public $title;
+ public $id;
+ public $content;
+ public $numberOfElement;
+ public $contentType;
+ public $linkArray;
+ public $localUpdated;
+ public $className;
+ private static $updated = null;
+
+ public static $icons = [
+ Author::ALL_AUTHORS_ID => 'images/author.png',
+ Serie::ALL_SERIES_ID => 'images/serie.png',
+ Book::ALL_RECENT_BOOKS_ID => 'images/recent.png',
+ Tag::ALL_TAGS_ID => 'images/tag.png',
+ Language::ALL_LANGUAGES_ID => 'images/language.png',
+ CustomColumnType::ALL_CUSTOMS_ID => 'images/custom.png',
+ Rating::ALL_RATING_ID => 'images/rating.png',
+ "cops:books$" => 'images/allbook.png',
+ "cops:books:letter" => 'images/allbook.png',
+ Publisher::ALL_PUBLISHERS_ID => 'images/publisher.png',
+ ];
+
+ public function getUpdatedTime()
+ {
+ if (!is_null($this->localUpdated)) {
+ return date(DATE_ATOM, $this->localUpdated);
+ }
+ if (is_null(self::$updated)) {
+ self::$updated = time();
+ }
+ return date(DATE_ATOM, self::$updated);
+ }
+
+ public function getNavLink()
+ {
+ foreach ($this->linkArray as $link) {
+ /* @var $link LinkNavigation */
+
+ if ($link->type != Link::OPDS_NAVIGATION_TYPE) {
+ continue;
+ }
+
+ return $link->hrefXhtml();
+ }
+ return "#";
+ }
+
+ public function __construct($ptitle, $pid, $pcontent, $pcontentType, $plinkArray, $pclass = "", $pcount = 0)
+ {
+ global $config;
+ $this->title = $ptitle;
+ $this->id = $pid;
+ $this->content = $pcontent;
+ $this->contentType = $pcontentType;
+ $this->linkArray = $plinkArray;
+ $this->className = $pclass;
+ $this->numberOfElement = $pcount;
+
+ if ($config['cops_show_icons'] == 1) {
+ foreach (self::$icons as $reg => $image) {
+ if (preg_match("/" . $reg . "/", $pid)) {
+ array_push($this->linkArray, new Link(getUrlWithVersion($image), "image/png", Link::OPDS_THUMBNAIL_TYPE));
+ break;
+ }
+ }
+ }
+
+ if (!is_null(GetUrlParam(DB))) {
+ $this->id = str_replace("cops:", "cops:" . GetUrlParam(DB) . ":", $this->id);
+ }
+ }
+}
diff --git a/lib/EntryBook.php b/lib/EntryBook.php
new file mode 100644
index 000000000..77ea80e70
--- /dev/null
+++ b/lib/EntryBook.php
@@ -0,0 +1,52 @@
+
+ */
+
+class EntryBook extends Entry
+{
+ public $book;
+
+ /**
+ * EntryBook constructor.
+ * @param string $ptitle
+ * @param integer $pid
+ * @param string $pcontent
+ * @param string $pcontentType
+ * @param array $plinkArray
+ * @param Book $pbook
+ */
+ public function __construct($ptitle, $pid, $pcontent, $pcontentType, $plinkArray, $pbook)
+ {
+ parent::__construct($ptitle, $pid, $pcontent, $pcontentType, $plinkArray);
+ $this->book = $pbook;
+ $this->localUpdated = $pbook->timestamp;
+ }
+
+ public function getCoverThumbnail()
+ {
+ foreach ($this->linkArray as $link) {
+ /* @var $link LinkNavigation */
+
+ if ($link->rel == Link::OPDS_THUMBNAIL_TYPE) {
+ return $link->hrefXhtml();
+ }
+ }
+ return null;
+ }
+
+ public function getCover()
+ {
+ foreach ($this->linkArray as $link) {
+ /* @var $link LinkNavigation */
+
+ if ($link->rel == Link::OPDS_IMAGE_TYPE) {
+ return $link->hrefXhtml();
+ }
+ }
+ return null;
+ }
+}
diff --git a/lib/Identifier.php b/lib/Identifier.php
new file mode 100644
index 000000000..a795bd1a0
--- /dev/null
+++ b/lib/Identifier.php
@@ -0,0 +1,80 @@
+
+ */
+
+class Identifier
+{
+ public $id;
+ public $type;
+ public $formattedType;
+ public $val;
+ public $uri;
+
+ public function __construct($post)
+ {
+ $this->id = $post->id;
+ $this->type = strtolower($post->type);
+ $this->val = $post->val;
+ $this->formatType();
+ }
+
+ public function formatType()
+ {
+ if ($this->type == 'amazon') {
+ $this->formattedType = "Amazon";
+ $this->uri = sprintf("https://amazon.com/dp/%s", $this->val);
+ } elseif ($this->type == "asin") {
+ $this->formattedType = $this->type;
+ $this->uri = sprintf("https://amazon.com/dp/%s", $this->val);
+ } elseif (substr($this->type, 0, 7) == "amazon_") {
+ $this->formattedType = sprintf("Amazon.co.%s", substr($this->type, 7));
+ $this->uri = sprintf("https://amazon.co.%s/dp/%s", substr($this->type, 7), $this->val);
+ } elseif ($this->type == "isbn") {
+ $this->formattedType = "ISBN";
+ $this->uri = sprintf("https://www.worldcat.org/isbn/%s", $this->val);
+ } elseif ($this->type == "doi") {
+ $this->formattedType = "DOI";
+ $this->uri = sprintf("https://dx.doi.org/%s", $this->val);
+ } elseif ($this->type == "douban") {
+ $this->formattedType = "Douban";
+ $this->uri = sprintf("https://book.douban.com/subject/%s", $this->val);
+ } elseif ($this->type == "goodreads") {
+ $this->formattedType = "Goodreads";
+ $this->uri = sprintf("https://www.goodreads.com/book/show/%s", $this->val);
+ } elseif ($this->type == "google") {
+ $this->formattedType = "Google Books";
+ $this->uri = sprintf("https://books.google.com/books?id=%s", $this->val);
+ } elseif ($this->type == "kobo") {
+ $this->formattedType = "Kobo";
+ $this->uri = sprintf("https://www.kobo.com/ebook/%s", $this->val);
+ } elseif ($this->type == "litres") {
+ $this->formattedType = "ЛитРес";
+ $this->uri = sprintf("https://www.litres.ru/%s", $this->val);
+ } elseif ($this->type == "issn") {
+ $this->formattedType = "ISSN";
+ $this->uri = sprintf("https://portal.issn.org/resource/ISSN/%s", $this->val);
+ } elseif ($this->type == "isfdb") {
+ $this->formattedType = "ISFDB";
+ $this->uri = sprintf("http://www.isfdb.org/cgi-bin/pl.cgi?%s", $this->val);
+ } elseif ($this->type == "lubimyczytac") {
+ $this->formattedType = "Lubimyczytac";
+ $this->uri = sprintf("https://lubimyczytac.pl/ksiazka/%s/ksiazka", $this->val);
+ } elseif ($this->type == "url") {
+ $this->formattedType = $this->type;
+ $this->uri = $this->val;
+ } else {
+ $this->formattedType = $this->type;
+ $this->uri = '';
+ }
+ }
+
+ public function getUri()
+ {
+ return $this->uri;
+ }
+}
diff --git a/lib/JSON_renderer.php b/lib/JSON_renderer.php
new file mode 100644
index 000000000..ebea26a18
--- /dev/null
+++ b/lib/JSON_renderer.php
@@ -0,0 +1,277 @@
+
+ */
+
+require_once dirname(__FILE__) . '/../base.php';
+
+class JSONRenderer
+{
+ /**
+ * @param Book $book
+ * @return array
+ */
+ public static function getBookContentArray($book)
+ {
+ global $config;
+ $i = 0;
+ $preferedData = [];
+ foreach ($config['cops_prefered_format'] as $format) {
+ if ($i == 2) {
+ break;
+ }
+ if ($data = $book->getDataFormat($format)) {
+ $i++;
+ array_push($preferedData, ["url" => $data->getHtmlLink(),
+ "viewUrl" => $data->getViewHtmlLink(), "name" => $format]);
+ }
+ }
+
+ $publisher = $book->getPublisher();
+ if (is_null($publisher)) {
+ $pn = "";
+ $pu = "";
+ } else {
+ $pn = $publisher->name;
+ $link = new LinkNavigation($publisher->getUri());
+ $pu = $link->hrefXhtml();
+ }
+
+ $serie = $book->getSerie();
+ if (is_null($serie)) {
+ $sn = "";
+ $scn = "";
+ $su = "";
+ } else {
+ $sn = $serie->name;
+ $scn = str_format(localize("content.series.data"), $book->seriesIndex, $serie->name);
+ $link = new LinkNavigation($serie->getUri());
+ $su = $link->hrefXhtml();
+ }
+ $cc = $book->getCustomColumnValues($config['cops_calibre_custom_column_list'], true);
+
+ return ["id" => $book->id,
+ "hasCover" => $book->hasCover,
+ "preferedData" => $preferedData,
+ "rating" => $book->getRating(),
+ "publisherName" => $pn,
+ "publisherurl" => $pu,
+ "pubDate" => $book->getPubDate(),
+ "languagesName" => $book->getLanguages(),
+ "authorsName" => $book->getAuthorsName(),
+ "tagsName" => $book->getTagsName(),
+ "seriesName" => $sn,
+ "seriesIndex" => $book->seriesIndex,
+ "seriesCompleteName" => $scn,
+ "seriesurl" => $su,
+ "customcolumns_list" => $cc];
+ }
+
+ /**
+ * @param Book $book
+ * @return array
+ */
+ public static function getFullBookContentArray($book)
+ {
+ global $config;
+ $out = self::getBookContentArray($book);
+ $database = GetUrlParam(DB);
+
+ $out ["coverurl"] = Data::getLink($book, "jpg", "image/jpeg", Link::OPDS_IMAGE_TYPE, "cover.jpg", null)->hrefXhtml();
+ $out ["thumbnailurl"] = Data::getLink($book, "jpg", "image/jpeg", Link::OPDS_THUMBNAIL_TYPE, "cover.jpg", null, null, $config['cops_html_thumbnail_height'] * 2)->hrefXhtml();
+ $out ["content"] = $book->getComment(false);
+ $out ["datas"] = [];
+ $dataKindle = $book->GetMostInterestingDataToSendToKindle();
+ foreach ($book->getDatas() as $data) {
+ $tab = ["id" => $data->id,
+ "format" => $data->format,
+ "url" => $data->getHtmlLink(),
+ "viewUrl" => $data->getViewHtmlLink(),
+ "mail" => 0,
+ "readerUrl" => ""];
+ if (!empty($config['cops_mail_configuration']) && !is_null($dataKindle) && $data->id == $dataKindle->id) {
+ $tab ["mail"] = 1;
+ }
+ if ($data->format == "EPUB") {
+ $tab ["readerUrl"] = "epubreader.php?data={$data->id}&db={$database}";
+ }
+ array_push($out ["datas"], $tab);
+ }
+ $out ["authors"] = [];
+ foreach ($book->getAuthors() as $author) {
+ $link = new LinkNavigation($author->getUri());
+ array_push($out ["authors"], ["name" => $author->name, "url" => $link->hrefXhtml()]);
+ }
+ $out ["tags"] = [];
+ foreach ($book->getTags() as $tag) {
+ $link = new LinkNavigation($tag->getUri());
+ array_push($out ["tags"], ["name" => $tag->name, "url" => $link->hrefXhtml()]);
+ }
+
+ $out ["identifiers"] = [];
+ foreach ($book->getIdentifiers() as $ident) {
+ array_push($out ["identifiers"], ["name" => $ident->formattedType, "url" => $ident->getUri()]);
+ }
+
+ $out ["customcolumns_preview"] = $book->getCustomColumnValues($config['cops_calibre_custom_column_preview'], true);
+
+ return $out;
+ }
+
+ public static function getContentArray($entry)
+ {
+ if ($entry instanceof EntryBook) {
+ $out = [ "title" => $entry->title];
+ $out ["book"] = self::getBookContentArray($entry->book);
+ return $out;
+ }
+ return [ "title" => $entry->title, "content" => $entry->content, "navlink" => $entry->getNavLink(), "number" => $entry->numberOfElement ];
+ }
+
+ public static function getContentArrayTypeahead($page)
+ {
+ $out = [];
+ foreach ($page->entryArray as $entry) {
+ if ($entry instanceof EntryBook) {
+ array_push($out, ["class" => $entry->className, "title" => $entry->title, "navlink" => $entry->book->getDetailUrl()]);
+ } else {
+ if (empty($entry->className) xor Base::noDatabaseSelected()) {
+ array_push($out, ["class" => $entry->className, "title" => $entry->title, "navlink" => $entry->getNavLink()]);
+ } else {
+ array_push($out, ["class" => $entry->className, "title" => $entry->content, "navlink" => $entry->getNavLink()]);
+ }
+ }
+ }
+ return $out;
+ }
+
+ public static function addCompleteArray($in)
+ {
+ global $config;
+ $out = $in;
+
+ $out ["c"] = ["version" => VERSION, "i18n" => [
+ "coverAlt" => localize("i18n.coversection"),
+ "authorsTitle" => localize("authors.title"),
+ "bookwordTitle" => localize("bookword.title"),
+ "tagsTitle" => localize("tags.title"),
+ "linksTitle" => localize("links.title"),
+ "seriesTitle" => localize("series.title"),
+ "customizeTitle" => localize("customize.title"),
+ "aboutTitle" => localize("about.title"),
+ "previousAlt" => localize("paging.previous.alternate"),
+ "nextAlt" => localize("paging.next.alternate"),
+ "searchAlt" => localize("search.alternate"),
+ "sortAlt" => localize("sort.alternate"),
+ "homeAlt" => localize("home.alternate"),
+ "cogAlt" => localize("cog.alternate"),
+ "permalinkAlt" => localize("permalink.alternate"),
+ "publisherName" => localize("publisher.name"),
+ "pubdateTitle" => localize("pubdate.title"),
+ "languagesTitle" => localize("language.title"),
+ "contentTitle" => localize("content.summary"),
+ "filterClearAll" => localize("filter.clearall"),
+ "sortorderAsc" => localize("search.sortorder.asc"),
+ "sortorderDesc" => localize("search.sortorder.desc"),
+ "customizeEmail" => localize("customize.email")],
+ "url" => [
+ "detailUrl" => "index.php?page=13&id={0}&db={1}",
+ "coverUrl" => "fetch.php?id={0}&db={1}",
+ "thumbnailUrl" => "fetch.php?height=" . $config['cops_html_thumbnail_height'] . "&id={0}&db={1}"],
+ "config" => [
+ "use_fancyapps" => $config ["cops_use_fancyapps"],
+ "max_item_per_page" => $config['cops_max_item_per_page'],
+ "kindleHack" => "",
+ "server_side_rendering" => useServerSideRendering(),
+ "html_tag_filter" => $config['cops_html_tag_filter']]];
+ if ($config['cops_thumbnail_handling'] == "1") {
+ $out ["c"]["url"]["thumbnailUrl"] = $out ["c"]["url"]["coverUrl"];
+ } elseif (!empty($config['cops_thumbnail_handling'])) {
+ $out ["c"]["url"]["thumbnailUrl"] = $config['cops_thumbnail_handling'];
+ }
+ if (preg_match("/./", $_SERVER['HTTP_USER_AGENT'])) {
+ $out ["c"]["config"]["kindleHack"] = 'style="text-decoration: none !important;"';
+ }
+ return $out;
+ }
+
+ public static function getJson($complete = false)
+ {
+ global $config;
+ $page = getURLParam("page", Base::PAGE_INDEX);
+ $query = getURLParam("query");
+ $search = getURLParam("search");
+ $qid = getURLParam("id");
+ $n = getURLParam("n", "1");
+ $database = GetUrlParam(DB);
+
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+
+ if ($search) {
+ return self::getContentArrayTypeahead($currentPage);
+ }
+
+ $out = [ "title" => $currentPage->title];
+ $entries = [];
+ foreach ($currentPage->entryArray as $entry) {
+ array_push($entries, self::getContentArray($entry));
+ }
+ if (!is_null($currentPage->book)) {
+ $out ["book"] = self::getFullBookContentArray($currentPage->book);
+ }
+ $out ["databaseId"] = GetUrlParam(DB, "");
+ $out ["databaseName"] = Base::getDbName();
+ if ($out ["databaseId"] == "") {
+ $out ["databaseName"] = "";
+ }
+ $out ["fullTitle"] = $out ["title"];
+ if ($out ["databaseId"] != "" && $out ["databaseName"] != $out ["fullTitle"]) {
+ $out ["fullTitle"] = $out ["databaseName"] . " > " . $out ["fullTitle"];
+ }
+ $out ["page"] = $page;
+ $out ["multipleDatabase"] = Base::isMultipleDatabaseEnabled() ? 1 : 0;
+ $out ["entries"] = $entries;
+ $out ["isPaginated"] = 0;
+ if ($currentPage->isPaginated()) {
+ $prevLink = $currentPage->getPrevLink();
+ $nextLink = $currentPage->getNextLink();
+ $out ["isPaginated"] = 1;
+ $out ["prevLink"] = "";
+ if (!is_null($prevLink)) {
+ $out ["prevLink"] = $prevLink->hrefXhtml();
+ }
+ $out ["nextLink"] = "";
+ if (!is_null($nextLink)) {
+ $out ["nextLink"] = $nextLink->hrefXhtml();
+ }
+ $out ["maxPage"] = $currentPage->getMaxPage();
+ $out ["currentPage"] = $currentPage->n;
+ }
+ if (!is_null(getURLParam("complete")) || $complete) {
+ $out = self::addCompleteArray($out);
+ }
+
+ $out ["containsBook"] = 0;
+ if ($currentPage->containsBook()) {
+ $out ["containsBook"] = 1;
+ }
+
+ $out["abouturl"] = "index.php" . addURLParameter("?page=" . Base::PAGE_ABOUT, DB, $database);
+
+ if ($page == Base::PAGE_ABOUT) {
+ $temp = preg_replace("/\
About COPS\<\/h1\>/", "
About COPS " . VERSION . "
", file_get_contents('about.html'));
+ $out ["fullhtml"] = $temp;
+ }
+
+ $out ["homeurl"] = "index.php";
+ if ($page != Base::PAGE_INDEX && !is_null($database)) {
+ $out ["homeurl"] = $out ["homeurl"] . "?" . addURLParameter("", DB, $database);
+ }
+
+ return $out;
+ }
+}
diff --git a/lib/Language.php b/lib/Language.php
new file mode 100644
index 000000000..d56691f71
--- /dev/null
+++ b/lib/Language.php
@@ -0,0 +1,81 @@
+
+ */
+
+class Language extends Base
+{
+ public const ALL_LANGUAGES_ID = "cops:languages";
+
+ public $id;
+ public $lang_code;
+
+ public function __construct($pid, $plang_code)
+ {
+ $this->id = $pid;
+ $this->lang_code = $plang_code;
+ }
+
+ public function getUri()
+ {
+ return "?page=".parent::PAGE_LANGUAGE_DETAIL."&id=$this->id";
+ }
+
+ public function getEntryId()
+ {
+ return self::ALL_LANGUAGES_ID.":".$this->id;
+ }
+
+ public static function getLanguageString($code)
+ {
+ $string = localize("languages.".$code);
+ if (preg_match("/^languages/", $string)) {
+ return $code;
+ }
+ return $string;
+ }
+
+ public static function getCount()
+ {
+ // str_format (localize("languages.alphabetical", count(array))
+ return parent::getCountGeneric("languages", self::ALL_LANGUAGES_ID, parent::PAGE_ALL_LANGUAGES);
+ }
+
+ public static function getLanguageById($languageId)
+ {
+ $result = parent::getDb()->prepare('select id, lang_code from languages where id = ?');
+ $result->execute([$languageId]);
+ if ($post = $result->fetchObject()) {
+ return new Language($post->id, Language::getLanguageString($post->lang_code));
+ }
+ return null;
+ }
+
+
+
+ public static function getAllLanguages()
+ {
+ $result = parent::getDb()->query('select languages.id as id, languages.lang_code as lang_code, count(*) as count
+from languages, books_languages_link
+where languages.id = books_languages_link.lang_code
+group by languages.id, books_languages_link.lang_code
+order by languages.lang_code');
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $language = new Language($post->id, $post->lang_code);
+ array_push($entryArray, new Entry(
+ Language::getLanguageString($language->lang_code),
+ $language->getEntryId(),
+ str_format(localize("bookword", $post->count), $post->count),
+ "text",
+ [ new LinkNavigation($language->getUri())],
+ "",
+ $post->count
+ ));
+ }
+ return $entryArray;
+ }
+}
diff --git a/lib/Link.php b/lib/Link.php
new file mode 100644
index 000000000..f571740d1
--- /dev/null
+++ b/lib/Link.php
@@ -0,0 +1,44 @@
+
+ */
+
+class Link
+{
+ public const OPDS_THUMBNAIL_TYPE = "http://opds-spec.org/image/thumbnail";
+ public const OPDS_IMAGE_TYPE = "http://opds-spec.org/image";
+ public const OPDS_ACQUISITION_TYPE = "http://opds-spec.org/acquisition";
+ public const OPDS_NAVIGATION_TYPE = "application/atom+xml;profile=opds-catalog;kind=navigation";
+ public const OPDS_PAGING_TYPE = "application/atom+xml;profile=opds-catalog;kind=acquisition";
+
+ public $href;
+ public $type;
+ public $rel;
+ public $title;
+ public $facetGroup;
+ public $activeFacet;
+
+ public function __construct($phref, $ptype, $prel = null, $ptitle = null, $pfacetGroup = null, $pactiveFacet = false)
+ {
+ $this->href = $phref;
+ $this->type = $ptype;
+ $this->rel = $prel;
+ $this->title = $ptitle;
+ $this->facetGroup = $pfacetGroup;
+ $this->activeFacet = $pactiveFacet;
+ }
+
+ public function hrefXhtml()
+ {
+ return $this->href;
+ }
+
+ public function getScriptName()
+ {
+ $parts = explode('/', $_SERVER["SCRIPT_NAME"]);
+ return $parts[count($parts) - 1];
+ }
+}
diff --git a/lib/LinkFacet.php b/lib/LinkFacet.php
new file mode 100644
index 000000000..0df9bf49b
--- /dev/null
+++ b/lib/LinkFacet.php
@@ -0,0 +1,19 @@
+
+ */
+
+class LinkFacet extends Link
+{
+ public function __construct($phref, $ptitle = null, $pfacetGroup = null, $pactiveFacet = false)
+ {
+ parent::__construct($phref, Link::OPDS_PAGING_TYPE, "http://opds-spec.org/facet", $ptitle, $pfacetGroup, $pactiveFacet);
+ if (!is_null(GetUrlParam(DB))) {
+ $this->href = addURLParameter($this->href, DB, GetUrlParam(DB));
+ }
+ $this->href = parent::getScriptName() . $this->href;
+ }
+}
diff --git a/lib/LinkNavigation.php b/lib/LinkNavigation.php
new file mode 100644
index 000000000..28cf0ee38
--- /dev/null
+++ b/lib/LinkNavigation.php
@@ -0,0 +1,26 @@
+
+ */
+
+class LinkNavigation extends Link
+{
+ public function __construct($phref, $prel = null, $ptitle = null)
+ {
+ parent::__construct($phref, Link::OPDS_NAVIGATION_TYPE, $prel, $ptitle);
+ if (!is_null(GetUrlParam(DB))) {
+ $this->href = addURLParameter($this->href, DB, GetUrlParam(DB));
+ }
+ if (!preg_match("#^\?(.*)#", $this->href) && !empty($this->href)) {
+ $this->href = "?" . $this->href;
+ }
+ if (preg_match("/(bookdetail|getJSON).php/", parent::getScriptName())) {
+ $this->href = "index.php" . $this->href;
+ } else {
+ $this->href = parent::getScriptName() . $this->href;
+ }
+ }
+}
diff --git a/lib/OPDS_renderer.php b/lib/OPDS_renderer.php
new file mode 100644
index 000000000..3f01be854
--- /dev/null
+++ b/lib/OPDS_renderer.php
@@ -0,0 +1,284 @@
+
+ */
+
+require_once dirname(__FILE__) . '/../base.php';
+
+class OPDSRenderer
+{
+ private $xmlStream = null;
+ private $updated = null;
+
+ private function getUpdatedTime()
+ {
+ if (is_null($this->updated)) {
+ $this->updated = time();
+ }
+ return date(DATE_ATOM, $this->updated);
+ }
+
+ private function getXmlStream()
+ {
+ if (is_null($this->xmlStream)) {
+ $this->xmlStream = new XMLWriter();
+ $this->xmlStream->openMemory();
+ $this->xmlStream->setIndent(true);
+ }
+ return $this->xmlStream;
+ }
+
+ public function getOpenSearch()
+ {
+ global $config;
+ $xml = new XMLWriter();
+ $xml->openMemory();
+ $xml->setIndent(true);
+ $xml->startDocument('1.0', 'UTF-8');
+ $xml->startElement("OpenSearchDescription");
+ $xml->writeAttribute("xmlns", "http://a9.com/-/spec/opensearch/1.1/");
+ $xml->startElement("ShortName");
+ $xml->text("My catalog");
+ $xml->endElement();
+ $xml->startElement("Description");
+ $xml->text("Search for ebooks");
+ $xml->endElement();
+ $xml->startElement("InputEncoding");
+ $xml->text("UTF-8");
+ $xml->endElement();
+ $xml->startElement("OutputEncoding");
+ $xml->text("UTF-8");
+ $xml->endElement();
+ $xml->startElement("Image");
+ $xml->writeAttribute("type", "image/x-icon");
+ $xml->writeAttribute("width", "16");
+ $xml->writeAttribute("height", "16");
+ $xml->text($config['cops_icon']);
+ $xml->endElement();
+ $xml->startElement("Url");
+ $xml->writeAttribute("type", 'application/atom+xml');
+ $urlparam = "?query={searchTerms}";
+ if (!is_null(GetUrlParam(DB))) {
+ $urlparam = addURLParameter($urlparam, DB, GetUrlParam(DB));
+ }
+ $urlparam = str_replace("%7B", "{", $urlparam);
+ $urlparam = str_replace("%7D", "}", $urlparam);
+ $xml->writeAttribute("template", $config['cops_full_url'] . 'feed.php' . $urlparam);
+ $xml->endElement();
+ $xml->startElement("Query");
+ $xml->writeAttribute("role", "example");
+ $xml->writeAttribute("searchTerms", "robot");
+ $xml->endElement();
+ $xml->endElement();
+ $xml->endDocument();
+ return $xml->outputMemory(true);
+ }
+
+ private function startXmlDocument($page)
+ {
+ global $config;
+ self::getXmlStream()->startDocument('1.0', 'UTF-8');
+ self::getXmlStream()->startElement("feed");
+ self::getXmlStream()->writeAttribute("xmlns", "http://www.w3.org/2005/Atom");
+ self::getXmlStream()->writeAttribute("xmlns:xhtml", "http://www.w3.org/1999/xhtml");
+ self::getXmlStream()->writeAttribute("xmlns:opds", "http://opds-spec.org/2010/catalog");
+ self::getXmlStream()->writeAttribute("xmlns:opensearch", "http://a9.com/-/spec/opensearch/1.1/");
+ self::getXmlStream()->writeAttribute("xmlns:dcterms", "http://purl.org/dc/terms/");
+ self::getXmlStream()->startElement("title");
+ self::getXmlStream()->text($page->title);
+ self::getXmlStream()->endElement();
+ if ($page->subtitle != "") {
+ self::getXmlStream()->startElement("subtitle");
+ self::getXmlStream()->text($page->subtitle);
+ self::getXmlStream()->endElement();
+ }
+ self::getXmlStream()->startElement("id");
+ if ($page->idPage) {
+ $idPage = $page->idPage;
+ if (!is_null(GetUrlParam(DB))) {
+ $idPage = str_replace("cops:", "cops:" . GetUrlParam(DB) . ":", $idPage);
+ }
+ self::getXmlStream()->text($idPage);
+ } else {
+ self::getXmlStream()->text($_SERVER['REQUEST_URI']);
+ }
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("updated");
+ self::getXmlStream()->text(self::getUpdatedTime());
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("icon");
+ self::getXmlStream()->text($page->favicon);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("author");
+ self::getXmlStream()->startElement("name");
+ self::getXmlStream()->text($page->authorName);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("uri");
+ self::getXmlStream()->text($page->authorUri);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("email");
+ self::getXmlStream()->text($page->authorEmail);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->endElement();
+ $link = new LinkNavigation("", "start", "Home");
+ self::renderLink($link);
+ $link = new LinkNavigation("?" . getQueryString(), "self");
+ self::renderLink($link);
+ $urlparam = "?";
+ if (!is_null(GetUrlParam(DB))) {
+ $urlparam = addURLParameter($urlparam, DB, GetUrlParam(DB));
+ }
+ if ($config['cops_generate_invalid_opds_stream'] == 0 || preg_match("/(MantanoReader|FBReader)/", $_SERVER['HTTP_USER_AGENT'])) {
+ // Good and compliant way of handling search
+ $urlparam = addURLParameter($urlparam, "page", Base::PAGE_OPENSEARCH);
+ $link = new Link("feed.php" . $urlparam, "application/opensearchdescription+xml", "search", "Search here");
+ } else {
+ // Bad way, will be removed when OPDS client are fixed
+ $urlparam = addURLParameter($urlparam, "query", "{searchTerms}");
+ $urlparam = str_replace("%7B", "{", $urlparam);
+ $urlparam = str_replace("%7D", "}", $urlparam);
+ $link = new Link($config['cops_full_url'] . 'feed.php' . $urlparam, "application/atom+xml", "search", "Search here");
+ }
+ self::renderLink($link);
+ if ($page->containsBook() && !is_null($config['cops_books_filter']) && count($config['cops_books_filter']) > 0) {
+ $Urlfilter = getURLParam("tag", "");
+ foreach ($config['cops_books_filter'] as $lib => $filter) {
+ $link = new LinkFacet("?" . addURLParameter(getQueryString(), "tag", $filter), $lib, localize("tagword.title"), $filter == $Urlfilter);
+ self::renderLink($link);
+ }
+ }
+ }
+
+ private function endXmlDocument()
+ {
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->endDocument();
+ return self::getXmlStream()->outputMemory(true);
+ }
+
+ private function renderLink($link)
+ {
+ self::getXmlStream()->startElement("link");
+ self::getXmlStream()->writeAttribute("href", $link->href);
+ self::getXmlStream()->writeAttribute("type", $link->type);
+ if (!is_null($link->rel)) {
+ self::getXmlStream()->writeAttribute("rel", $link->rel);
+ }
+ if (!is_null($link->title)) {
+ self::getXmlStream()->writeAttribute("title", $link->title);
+ }
+ if (!is_null($link->facetGroup)) {
+ self::getXmlStream()->writeAttribute("opds:facetGroup", $link->facetGroup);
+ }
+ if ($link->activeFacet) {
+ self::getXmlStream()->writeAttribute("opds:activeFacet", "true");
+ }
+ self::getXmlStream()->endElement();
+ }
+
+ private function getPublicationDate($book)
+ {
+ $dateYmd = substr($book->pubdate, 0, 10);
+ $pubdate = \DateTime::createFromFormat('Y-m-d', $dateYmd);
+ if ($pubdate === false ||
+ $pubdate->format("Y") == "0101" ||
+ $pubdate->format("Y") == "0100") {
+ return "";
+ }
+ return $pubdate->format("Y-m-d");
+ }
+
+ private function renderEntry($entry)
+ {
+ self::getXmlStream()->startElement("title");
+ self::getXmlStream()->text($entry->title);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("updated");
+ self::getXmlStream()->text(self::getUpdatedTime());
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("id");
+ self::getXmlStream()->text($entry->id);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("content");
+ self::getXmlStream()->writeAttribute("type", $entry->contentType);
+ if ($entry->contentType == "text") {
+ self::getXmlStream()->text($entry->content);
+ } else {
+ self::getXmlStream()->writeRaw($entry->content);
+ }
+ self::getXmlStream()->endElement();
+ foreach ($entry->linkArray as $link) {
+ self::renderLink($link);
+ }
+
+ if (get_class($entry) != "EntryBook") {
+ return;
+ }
+
+ foreach ($entry->book->getAuthors() as $author) {
+ self::getXmlStream()->startElement("author");
+ self::getXmlStream()->startElement("name");
+ self::getXmlStream()->text($author->name);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("uri");
+ self::getXmlStream()->text("feed.php" . $author->getUri());
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->endElement();
+ }
+ foreach ($entry->book->getTags() as $category) {
+ self::getXmlStream()->startElement("category");
+ self::getXmlStream()->writeAttribute("term", $category->name);
+ self::getXmlStream()->writeAttribute("label", $category->name);
+ self::getXmlStream()->endElement();
+ }
+ if ($entry->book->getPubDate() != "") {
+ self::getXmlStream()->startElement("dcterms:issued");
+ self::getXmlStream()->text(self::getPublicationDate($entry->book));
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("published");
+ self::getXmlStream()->text(self::getPublicationDate($entry->book) . "T08:08:08Z");
+ self::getXmlStream()->endElement();
+ }
+
+ $lang = $entry->book->getLanguages();
+ if (!empty($lang)) {
+ self::getXmlStream()->startElement("dcterms:language");
+ self::getXmlStream()->text($lang);
+ self::getXmlStream()->endElement();
+ }
+ }
+
+ public function render($page)
+ {
+ global $config;
+ self::startXmlDocument($page);
+ if ($page->isPaginated()) {
+ self::getXmlStream()->startElement("opensearch:totalResults");
+ self::getXmlStream()->text($page->totalNumber);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("opensearch:itemsPerPage");
+ self::getXmlStream()->text($config['cops_max_item_per_page']);
+ self::getXmlStream()->endElement();
+ self::getXmlStream()->startElement("opensearch:startIndex");
+ self::getXmlStream()->text(($page->n - 1) * $config['cops_max_item_per_page'] + 1);
+ self::getXmlStream()->endElement();
+ $prevLink = $page->getPrevLink();
+ $nextLink = $page->getNextLink();
+ if (!is_null($prevLink)) {
+ self::renderLink($prevLink);
+ }
+ if (!is_null($nextLink)) {
+ self::renderLink($nextLink);
+ }
+ }
+ foreach ($page->entryArray as $entry) {
+ self::getXmlStream()->startElement("entry");
+ self::renderEntry($entry);
+ self::getXmlStream()->endElement();
+ }
+ return self::endXmlDocument();
+ }
+}
diff --git a/lib/Page.php b/lib/Page.php
new file mode 100644
index 000000000..94b3cd25d
--- /dev/null
+++ b/lib/Page.php
@@ -0,0 +1,203 @@
+
+ */
+
+class Page
+{
+ public $title;
+ public $subtitle = "";
+ public $authorName = "";
+ public $authorUri = "";
+ public $authorEmail = "";
+ public $idPage;
+ public $idGet;
+ public $query;
+ public $favicon;
+ public $n;
+ public $book;
+ public $totalNumber = -1;
+
+ /* @var Entry[] */
+ public $entryArray = [];
+
+ public static function getPage($pageId, $id, $query, $n)
+ {
+ switch ($pageId) {
+ case Base::PAGE_ALL_AUTHORS :
+ return new PageAllAuthors($id, $query, $n);
+ case Base::PAGE_AUTHORS_FIRST_LETTER :
+ return new PageAllAuthorsLetter($id, $query, $n);
+ case Base::PAGE_AUTHOR_DETAIL :
+ return new PageAuthorDetail($id, $query, $n);
+ case Base::PAGE_ALL_TAGS :
+ return new PageAllTags($id, $query, $n);
+ case Base::PAGE_TAG_DETAIL :
+ return new PageTagDetail($id, $query, $n);
+ case Base::PAGE_ALL_LANGUAGES :
+ return new PageAllLanguages($id, $query, $n);
+ case Base::PAGE_LANGUAGE_DETAIL :
+ return new PageLanguageDetail($id, $query, $n);
+ case Base::PAGE_ALL_CUSTOMS :
+ return new PageAllCustoms($id, $query, $n);
+ case Base::PAGE_CUSTOM_DETAIL :
+ return new PageCustomDetail($id, $query, $n);
+ case Base::PAGE_ALL_RATINGS :
+ return new PageAllRating($id, $query, $n);
+ case Base::PAGE_RATING_DETAIL :
+ return new PageRatingDetail($id, $query, $n);
+ case Base::PAGE_ALL_SERIES :
+ return new PageAllSeries($id, $query, $n);
+ case Base::PAGE_ALL_BOOKS :
+ return new PageAllBooks($id, $query, $n);
+ case Base::PAGE_ALL_BOOKS_LETTER:
+ return new PageAllBooksLetter($id, $query, $n);
+ case Base::PAGE_ALL_RECENT_BOOKS :
+ return new PageRecentBooks($id, $query, $n);
+ case Base::PAGE_SERIE_DETAIL :
+ return new PageSerieDetail($id, $query, $n);
+ case Base::PAGE_OPENSEARCH_QUERY :
+ return new PageQueryResult($id, $query, $n);
+ case Base::PAGE_BOOK_DETAIL :
+ return new PageBookDetail($id, $query, $n);
+ case Base::PAGE_ALL_PUBLISHERS:
+ return new PageAllPublishers($id, $query, $n);
+ case Base::PAGE_PUBLISHER_DETAIL :
+ return new PagePublisherDetail($id, $query, $n);
+ case Base::PAGE_ABOUT :
+ return new PageAbout($id, $query, $n);
+ case Base::PAGE_CUSTOMIZE :
+ return new PageCustomize($id, $query, $n);
+ default:
+ $page = new Page($id, $query, $n);
+ $page->idPage = "cops:catalog";
+ return $page;
+ }
+ }
+
+ public function __construct($pid, $pquery, $pn)
+ {
+ global $config;
+
+ $this->idGet = $pid;
+ $this->query = $pquery;
+ $this->n = $pn;
+ $this->favicon = $config['cops_icon'];
+ $this->authorName = $config['cops_author_name'] ?: 'Sébastien Lucas';
+ $this->authorUri = $config['cops_author_uri'] ?: 'http://blog.slucas.fr';
+ $this->authorEmail = $config['cops_author_email'] ?: 'sebastien@slucas.fr';
+ }
+
+ public function InitializeContent()
+ {
+ global $config;
+ $this->title = $config['cops_title_default'];
+ $this->subtitle = $config['cops_subtitle_default'];
+ if (Base::noDatabaseSelected()) {
+ $i = 0;
+ foreach (Base::getDbNameList() as $key) {
+ $nBooks = Book::getBookCount($i);
+ array_push($this->entryArray, new Entry(
+ $key,
+ "cops:{$i}:catalog",
+ str_format(localize("bookword", $nBooks), $nBooks),
+ "text",
+ [ new LinkNavigation("?" . DB . "={$i}")],
+ "",
+ $nBooks
+ ));
+ $i++;
+ Base::clearDb();
+ }
+ } else {
+ if (!in_array(PageQueryResult::SCOPE_AUTHOR, getCurrentOption('ignored_categories'))) {
+ array_push($this->entryArray, Author::getCount());
+ }
+ if (!in_array(PageQueryResult::SCOPE_SERIES, getCurrentOption('ignored_categories'))) {
+ $series = Serie::getCount();
+ if (!is_null($series)) {
+ array_push($this->entryArray, $series);
+ }
+ }
+ if (!in_array(PageQueryResult::SCOPE_PUBLISHER, getCurrentOption('ignored_categories'))) {
+ $publisher = Publisher::getCount();
+ if (!is_null($publisher)) {
+ array_push($this->entryArray, $publisher);
+ }
+ }
+ if (!in_array(PageQueryResult::SCOPE_TAG, getCurrentOption('ignored_categories'))) {
+ $tags = Tag::getCount();
+ if (!is_null($tags)) {
+ array_push($this->entryArray, $tags);
+ }
+ }
+ if (!in_array(PageQueryResult::SCOPE_RATING, getCurrentOption('ignored_categories'))) {
+ $rating = Rating::getCount();
+ if (!is_null($rating)) {
+ array_push($this->entryArray, $rating);
+ }
+ }
+ if (!in_array("language", getCurrentOption('ignored_categories'))) {
+ $languages = Language::getCount();
+ if (!is_null($languages)) {
+ array_push($this->entryArray, $languages);
+ }
+ }
+ foreach ($config['cops_calibre_custom_column'] as $lookup) {
+ $customColumn = CustomColumnType::createByLookup($lookup);
+ if (!is_null($customColumn) && $customColumn->isSearchable()) {
+ array_push($this->entryArray, $customColumn->getCount());
+ }
+ }
+ $this->entryArray = array_merge($this->entryArray, Book::getCount());
+
+ if (Base::isMultipleDatabaseEnabled()) {
+ $this->title = Base::getDbName();
+ }
+ }
+ }
+
+ public function isPaginated()
+ {
+ return (getCurrentOption("max_item_per_page") != -1 &&
+ $this->totalNumber != -1 &&
+ $this->totalNumber > getCurrentOption("max_item_per_page"));
+ }
+
+ public function getNextLink()
+ {
+ $currentUrl = preg_replace("/\&n=.*?$/", "", "?" . getQueryString());
+ if (($this->n) * getCurrentOption("max_item_per_page") < $this->totalNumber) {
+ return new LinkNavigation($currentUrl . "&n=" . ($this->n + 1), "next", localize("paging.next.alternate"));
+ }
+ return null;
+ }
+
+ public function getPrevLink()
+ {
+ $currentUrl = preg_replace("/\&n=.*?$/", "", "?" . getQueryString());
+ if ($this->n > 1) {
+ return new LinkNavigation($currentUrl . "&n=" . ($this->n - 1), "previous", localize("paging.previous.alternate"));
+ }
+ return null;
+ }
+
+ public function getMaxPage()
+ {
+ return ceil($this->totalNumber / getCurrentOption("max_item_per_page"));
+ }
+
+ public function containsBook()
+ {
+ if (count($this->entryArray) == 0) {
+ return false;
+ }
+ if (get_class($this->entryArray [0]) == "EntryBook") {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/lib/PageAbout.php b/lib/PageAbout.php
new file mode 100644
index 000000000..9aef7a6af
--- /dev/null
+++ b/lib/PageAbout.php
@@ -0,0 +1,15 @@
+
+ */
+
+class PageAbout extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("about.title");
+ }
+}
diff --git a/lib/PageAllAuthors.php b/lib/PageAllAuthors.php
new file mode 100644
index 000000000..507f5497a
--- /dev/null
+++ b/lib/PageAllAuthors.php
@@ -0,0 +1,21 @@
+
+ */
+
+class PageAllAuthors extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("authors.title");
+ if (getCurrentOption("author_split_first_letter") == 1) {
+ $this->entryArray = Author::getAllAuthorsByFirstLetter();
+ } else {
+ $this->entryArray = Author::getAllAuthors();
+ }
+ $this->idPage = Author::ALL_AUTHORS_ID;
+ }
+}
diff --git a/lib/PageAllAuthorsLetter.php b/lib/PageAllAuthorsLetter.php
new file mode 100644
index 000000000..cb6367f0b
--- /dev/null
+++ b/lib/PageAllAuthorsLetter.php
@@ -0,0 +1,17 @@
+
+ */
+
+class PageAllAuthorsLetter extends Page
+{
+ public function InitializeContent()
+ {
+ $this->idPage = Author::getEntryIdByLetter($this->idGet);
+ $this->entryArray = Author::getAuthorsByStartingLetter($this->idGet);
+ $this->title = str_format(localize("splitByLetter.letter"), str_format(localize("authorword", count($this->entryArray)), count($this->entryArray)), $this->idGet);
+ }
+}
diff --git a/lib/PageAllBooks.php b/lib/PageAllBooks.php
new file mode 100644
index 000000000..ee89606f2
--- /dev/null
+++ b/lib/PageAllBooks.php
@@ -0,0 +1,21 @@
+
+ */
+
+class PageAllBooks extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("allbooks.title");
+ if (getCurrentOption("titles_split_first_letter") == 1) {
+ $this->entryArray = Book::getAllBooks();
+ } else {
+ [$this->entryArray, $this->totalNumber] = Book::getBooks($this->n);
+ }
+ $this->idPage = Book::ALL_BOOKS_ID;
+ }
+}
diff --git a/lib/PageAllBooksLetter.php b/lib/PageAllBooksLetter.php
new file mode 100644
index 000000000..ef99d6f23
--- /dev/null
+++ b/lib/PageAllBooksLetter.php
@@ -0,0 +1,23 @@
+
+ */
+
+class PageAllBooksLetter extends Page
+{
+ public function InitializeContent()
+ {
+ [$this->entryArray, $this->totalNumber] = Book::getBooksByStartingLetter($this->idGet, $this->n);
+ $this->idPage = Book::getEntryIdByLetter($this->idGet);
+
+ $count = $this->totalNumber;
+ if ($count == -1) {
+ $count = count($this->entryArray);
+ }
+
+ $this->title = str_format(localize("splitByLetter.letter"), str_format(localize("bookword", $count), $count), $this->idGet);
+ }
+}
diff --git a/lib/PageAllCustoms.php b/lib/PageAllCustoms.php
new file mode 100644
index 000000000..abae5db52
--- /dev/null
+++ b/lib/PageAllCustoms.php
@@ -0,0 +1,20 @@
+
+ */
+
+class PageAllCustoms extends Page
+{
+ public function InitializeContent()
+ {
+ $customId = getURLParam("custom", null);
+ $columnType = CustomColumnType::createByCustomID($customId);
+
+ $this->title = $columnType->getTitle();
+ $this->entryArray = $columnType->getAllCustomValues();
+ $this->idPage = $columnType->getAllCustomsId();
+ }
+}
diff --git a/lib/PageAllLanguages.php b/lib/PageAllLanguages.php
new file mode 100644
index 000000000..15dc93b14
--- /dev/null
+++ b/lib/PageAllLanguages.php
@@ -0,0 +1,17 @@
+
+ */
+
+class PageAllLanguages extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("languages.title");
+ $this->entryArray = Language::getAllLanguages();
+ $this->idPage = Language::ALL_LANGUAGES_ID;
+ }
+}
diff --git a/lib/PageAllPublishers.php b/lib/PageAllPublishers.php
new file mode 100644
index 000000000..d5b7e67b7
--- /dev/null
+++ b/lib/PageAllPublishers.php
@@ -0,0 +1,17 @@
+
+ */
+
+class PageAllPublishers extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("publishers.title");
+ $this->entryArray = Publisher::getAllPublishers();
+ $this->idPage = Publisher::ALL_PUBLISHERS_ID;
+ }
+}
diff --git a/lib/PageAllRating.php b/lib/PageAllRating.php
new file mode 100644
index 000000000..fc5815e36
--- /dev/null
+++ b/lib/PageAllRating.php
@@ -0,0 +1,17 @@
+
+ */
+
+class PageAllRating extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("ratings.title");
+ $this->entryArray = Rating::getAllRatings();
+ $this->idPage = Rating::ALL_RATING_ID;
+ }
+}
diff --git a/lib/PageAllSeries.php b/lib/PageAllSeries.php
new file mode 100644
index 000000000..d70cd211a
--- /dev/null
+++ b/lib/PageAllSeries.php
@@ -0,0 +1,17 @@
+
+ */
+
+class PageAllSeries extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("series.title");
+ $this->entryArray = Serie::getAllSeries();
+ $this->idPage = Serie::ALL_SERIES_ID;
+ }
+}
diff --git a/lib/PageAllTags.php b/lib/PageAllTags.php
new file mode 100644
index 000000000..e90c71eea
--- /dev/null
+++ b/lib/PageAllTags.php
@@ -0,0 +1,17 @@
+
+ */
+
+class PageAllTags extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("tags.title");
+ $this->entryArray = Tag::getAllTags();
+ $this->idPage = Tag::ALL_TAGS_ID;
+ }
+}
diff --git a/lib/PageAuthorDetail.php b/lib/PageAuthorDetail.php
new file mode 100644
index 000000000..aea53ae1b
--- /dev/null
+++ b/lib/PageAuthorDetail.php
@@ -0,0 +1,18 @@
+
+ */
+
+class PageAuthorDetail extends Page
+{
+ public function InitializeContent()
+ {
+ $author = Author::getAuthorById($this->idGet);
+ $this->idPage = $author->getEntryId();
+ $this->title = $author->name;
+ [$this->entryArray, $this->totalNumber] = Book::getBooksByAuthor($this->idGet, $this->n);
+ }
+}
diff --git a/lib/PageBookDetail.php b/lib/PageBookDetail.php
new file mode 100644
index 000000000..f182bd706
--- /dev/null
+++ b/lib/PageBookDetail.php
@@ -0,0 +1,16 @@
+
+ */
+
+class PageBookDetail extends Page
+{
+ public function InitializeContent()
+ {
+ $this->book = Book::getBookById($this->idGet);
+ $this->title = $this->book->title;
+ }
+}
diff --git a/lib/PageCustomDetail.php b/lib/PageCustomDetail.php
new file mode 100644
index 000000000..bc8a60ddd
--- /dev/null
+++ b/lib/PageCustomDetail.php
@@ -0,0 +1,19 @@
+
+ */
+
+class PageCustomDetail extends Page
+{
+ public function InitializeContent()
+ {
+ $customId = getURLParam("custom", null);
+ $custom = CustomColumn::createCustom($customId, $this->idGet);
+ $this->idPage = $custom->getEntryId();
+ $this->title = $custom->value;
+ [$this->entryArray, $this->totalNumber] = Book::getBooksByCustom($custom, $this->idGet, $this->n);
+ }
+}
diff --git a/lib/PageCustomize.php b/lib/PageCustomize.php
new file mode 100644
index 000000000..5f1396568
--- /dev/null
+++ b/lib/PageCustomize.php
@@ -0,0 +1,156 @@
+
+ */
+
+class PageCustomize extends Page
+{
+ private function isChecked($key, $testedValue = 1)
+ {
+ $value = getCurrentOption($key);
+ if (is_array($value)) {
+ if (in_array($testedValue, $value)) {
+ return "checked='checked'";
+ }
+ } else {
+ if ($value == $testedValue) {
+ return "checked='checked'";
+ }
+ }
+ return "";
+ }
+
+ private function isSelected($key, $value)
+ {
+ if (getCurrentOption($key) == $value) {
+ return "selected='selected'";
+ }
+ return "";
+ }
+
+ private function getTemplateList()
+ {
+ $result = [];
+ foreach (glob("templates/*") as $filename) {
+ if (preg_match('/templates\/(.*)/', $filename, $m)) {
+ array_push($result, $m [1]);
+ }
+ }
+ return $result;
+ }
+
+ private function getStyleList()
+ {
+ $result = [];
+ foreach (glob("templates/" . getCurrentTemplate() . "/styles/style-*.css") as $filename) {
+ if (preg_match('/styles\/style-(.*?)\.css/', $filename, $m)) {
+ array_push($result, $m [1]);
+ }
+ }
+ return $result;
+ }
+
+ public function InitializeContent()
+ {
+ $this->title = localize("customize.title");
+ $this->entryArray = [];
+
+ $ignoredBaseArray = [PageQueryResult::SCOPE_AUTHOR,
+ PageQueryResult::SCOPE_TAG,
+ PageQueryResult::SCOPE_SERIES,
+ PageQueryResult::SCOPE_PUBLISHER,
+ PageQueryResult::SCOPE_RATING,
+ "language"];
+
+ $content = "";
+ if (!preg_match("/(Kobo|Kindle\/3.0|EBRD1101)/", $_SERVER['HTTP_USER_AGENT'])) {
+ $content .= "';
+ } else {
+ foreach ($this-> getTemplateList() as $filename) {
+ $content .= "isChecked("template", $filename) . " />";
+ }
+ }
+ array_push($this->entryArray, new Entry(
+ "Template",
+ "",
+ $content,
+ "text",
+ []
+ ));
+
+ $content = "";
+ if (!preg_match("/(Kobo|Kindle\/3.0|EBRD1101)/", $_SERVER['HTTP_USER_AGENT'])) {
+ $content .= '';
+ } else {
+ foreach ($this-> getStyleList() as $filename) {
+ $content .= "isChecked("style", $filename) . " />";
+ }
+ }
+ array_push($this->entryArray, new Entry(
+ localize("customize.style"),
+ "",
+ $content,
+ "text",
+ []
+ ));
+ if (!useServerSideRendering()) {
+ $content = 'isChecked("use_fancyapps") . ' />';
+ array_push($this->entryArray, new Entry(
+ localize("customize.fancybox"),
+ "",
+ $content,
+ "text",
+ []
+ ));
+ }
+ $content = '';
+ array_push($this->entryArray, new Entry(
+ localize("customize.paging"),
+ "",
+ $content,
+ "text",
+ []
+ ));
+ $content = '';
+ array_push($this->entryArray, new Entry(
+ localize("customize.email"),
+ "",
+ $content,
+ "text",
+ []
+ ));
+ $content = 'isChecked("html_tag_filter") . ' />';
+ array_push($this->entryArray, new Entry(
+ localize("customize.filter"),
+ "",
+ $content,
+ "text",
+ []
+ ));
+ $content = "";
+ foreach ($ignoredBaseArray as $key) {
+ $keyPlural = preg_replace('/(ss)$/', 's', $key . "s");
+ $content .= 'isChecked("ignored_categories", $key) . ' > ' . localize("{$keyPlural}.title") . ' ';
+ }
+
+ array_push($this->entryArray, new Entry(
+ localize("customize.ignored"),
+ "",
+ $content,
+ "text",
+ []
+ ));
+ }
+}
diff --git a/lib/PageLanguageDetail.php b/lib/PageLanguageDetail.php
new file mode 100644
index 000000000..0bb2511cb
--- /dev/null
+++ b/lib/PageLanguageDetail.php
@@ -0,0 +1,18 @@
+
+ */
+
+class PageLanguageDetail extends Page
+{
+ public function InitializeContent()
+ {
+ $language = Language::getLanguageById($this->idGet);
+ $this->idPage = $language->getEntryId();
+ $this->title = $language->lang_code;
+ [$this->entryArray, $this->totalNumber] = Book::getBooksByLanguage($this->idGet, $this->n);
+ }
+}
diff --git a/lib/PagePublisherDetail.php b/lib/PagePublisherDetail.php
new file mode 100644
index 000000000..f99903598
--- /dev/null
+++ b/lib/PagePublisherDetail.php
@@ -0,0 +1,18 @@
+
+ */
+
+class PagePublisherDetail extends Page
+{
+ public function InitializeContent()
+ {
+ $publisher = Publisher::getPublisherById($this->idGet);
+ $this->title = $publisher->name;
+ [$this->entryArray, $this->totalNumber] = Book::getBooksByPublisher($this->idGet, $this->n);
+ $this->idPage = $publisher->getEntryId();
+ }
+}
diff --git a/lib/PageQueryResult.php b/lib/PageQueryResult.php
new file mode 100644
index 000000000..27659e22a
--- /dev/null
+++ b/lib/PageQueryResult.php
@@ -0,0 +1,186 @@
+
+ */
+
+class PageQueryResult extends Page
+{
+ public const SCOPE_TAG = "tag";
+ public const SCOPE_RATING = "rating";
+ public const SCOPE_SERIES = "series";
+ public const SCOPE_AUTHOR = "author";
+ public const SCOPE_BOOK = "book";
+ public const SCOPE_PUBLISHER = "publisher";
+
+ private function useTypeahead()
+ {
+ return !is_null(getURLParam("search"));
+ }
+
+ private function searchByScope($scope, $limit = false)
+ {
+ $n = $this->n;
+ $numberPerPage = null;
+ $queryNormedAndUp = trim($this->query);
+ if (useNormAndUp()) {
+ $queryNormedAndUp = normAndUp($this->query);
+ }
+ if ($limit) {
+ $n = 1;
+ $numberPerPage = 5;
+ }
+ switch ($scope) {
+ case self::SCOPE_BOOK :
+ $array = Book::getBooksByStartingLetter('%' . $queryNormedAndUp, $n, null, $numberPerPage);
+ break;
+ case self::SCOPE_AUTHOR :
+ $array = Author::getAuthorsForSearch('%' . $queryNormedAndUp);
+ break;
+ case self::SCOPE_SERIES :
+ $array = Serie::getAllSeriesByQuery($queryNormedAndUp);
+ break;
+ case self::SCOPE_TAG :
+ $array = Tag::getAllTagsByQuery($queryNormedAndUp, $n, null, $numberPerPage);
+ break;
+ case self::SCOPE_PUBLISHER :
+ $array = Publisher::getAllPublishersByQuery($queryNormedAndUp);
+ break;
+ default:
+ $array = Book::getBooksByQuery(
+ ["all" => "%" . $queryNormedAndUp . "%"],
+ $n
+ );
+ }
+
+ return $array;
+ }
+
+ public function doSearchByCategory()
+ {
+ $database = GetUrlParam(DB);
+ $out = [];
+ $pagequery = Base::PAGE_OPENSEARCH_QUERY;
+ $dbArray = [""];
+ $d = $database;
+ $query = $this->query;
+ // Special case when no databases were chosen, we search on all databases
+ if (Base::noDatabaseSelected()) {
+ $dbArray = Base::getDbNameList();
+ $d = 0;
+ }
+ foreach ($dbArray as $key) {
+ if (Base::noDatabaseSelected()) {
+ array_push($this->entryArray, new Entry(
+ $key,
+ DB . ":query:{$d}",
+ " ",
+ "text",
+ [ new LinkNavigation("?" . DB . "={$d}")],
+ "tt-header"
+ ));
+ Base::getDb($d);
+ }
+ foreach ([PageQueryResult::SCOPE_BOOK,
+ PageQueryResult::SCOPE_AUTHOR,
+ PageQueryResult::SCOPE_SERIES,
+ PageQueryResult::SCOPE_TAG,
+ PageQueryResult::SCOPE_PUBLISHER] as $key) {
+ if (in_array($key, getCurrentOption('ignored_categories'))) {
+ continue;
+ }
+ $array = $this->searchByScope($key, true);
+
+ $i = 0;
+ if (count($array) == 2 && is_array($array [0])) {
+ $total = $array [1];
+ $array = $array [0];
+ } else {
+ $total = count($array);
+ }
+ if ($total > 0) {
+ // Comment to help the perl i18n script
+ // str_format (localize("bookword", count($array))
+ // str_format (localize("authorword", count($array))
+ // str_format (localize("seriesword", count($array))
+ // str_format (localize("tagword", count($array))
+ // str_format (localize("publisherword", count($array))
+ array_push($this->entryArray, new Entry(
+ str_format(localize("search.result.{$key}"), $this->query),
+ DB . ":query:{$d}:{$key}",
+ str_format(localize("{$key}word", $total), $total),
+ "text",
+ [ new LinkNavigation("?page={$pagequery}&query={$query}&db={$d}&scope={$key}")],
+ Base::noDatabaseSelected() ? "" : "tt-header",
+ $total
+ ));
+ }
+ if (!Base::noDatabaseSelected() && $this->useTypeahead()) {
+ foreach ($array as $entry) {
+ array_push($this->entryArray, $entry);
+ $i++;
+ if ($i > 4) {
+ break;
+ };
+ }
+ }
+ }
+ $d++;
+ if (Base::noDatabaseSelected()) {
+ Base::clearDb();
+ }
+ }
+ return $out;
+ }
+
+ public function InitializeContent()
+ {
+ $scope = getURLParam("scope");
+ if (empty($scope)) {
+ $this->title = str_format(localize("search.result"), $this->query);
+ } else {
+ // Comment to help the perl i18n script
+ // str_format (localize ("search.result.author"), $this->query)
+ // str_format (localize ("search.result.tag"), $this->query)
+ // str_format (localize ("search.result.series"), $this->query)
+ // str_format (localize ("search.result.book"), $this->query)
+ // str_format (localize ("search.result.publisher"), $this->query)
+ $this->title = str_format(localize("search.result.{$scope}"), $this->query);
+ }
+
+ $crit = "%" . $this->query . "%";
+
+ // Special case when we are doing a search and no database is selected
+ if (Base::noDatabaseSelected() && !$this->useTypeahead()) {
+ $i = 0;
+ foreach (Base::getDbNameList() as $key) {
+ Base::clearDb();
+ [$array, $totalNumber] = Book::getBooksByQuery(["all" => $crit], 1, $i, 1);
+ array_push($this->entryArray, new Entry(
+ $key,
+ DB . ":query:{$i}",
+ str_format(localize("bookword", $totalNumber), $totalNumber),
+ "text",
+ [ new LinkNavigation("?" . DB . "={$i}&page=9&query=" . $this->query)],
+ "",
+ $totalNumber
+ ));
+ $i++;
+ }
+ return;
+ }
+ if (empty($scope)) {
+ $this->doSearchByCategory();
+ return;
+ }
+
+ $array = $this->searchByScope($scope);
+ if (count($array) == 2 && is_array($array [0])) {
+ [$this->entryArray, $this->totalNumber] = $array;
+ } else {
+ $this->entryArray = $array;
+ }
+ }
+}
diff --git a/lib/PageRatingDetail.php b/lib/PageRatingDetail.php
new file mode 100644
index 000000000..09ccdfce2
--- /dev/null
+++ b/lib/PageRatingDetail.php
@@ -0,0 +1,18 @@
+
+ */
+
+class PageRatingDetail extends Page
+{
+ public function InitializeContent()
+ {
+ $rating = Rating::getRatingById($this->idGet);
+ $this->idPage = $rating->getEntryId();
+ $this->title =str_format(localize("ratingword", $rating->name/2), $rating->name/2);
+ [$this->entryArray, $this->totalNumber] = Book::getBooksByRating($this->idGet, $this->n);
+ }
+}
diff --git a/lib/PageRecentBooks.php b/lib/PageRecentBooks.php
new file mode 100644
index 000000000..6085ba827
--- /dev/null
+++ b/lib/PageRecentBooks.php
@@ -0,0 +1,17 @@
+
+ */
+
+class PageRecentBooks extends Page
+{
+ public function InitializeContent()
+ {
+ $this->title = localize("recent.title");
+ $this->entryArray = Book::getAllRecentBooks();
+ $this->idPage = Book::ALL_RECENT_BOOKS_ID;
+ }
+}
diff --git a/lib/PageSerieDetail.php b/lib/PageSerieDetail.php
new file mode 100644
index 000000000..f1e3d5391
--- /dev/null
+++ b/lib/PageSerieDetail.php
@@ -0,0 +1,18 @@
+
+ */
+
+class PageSerieDetail extends Page
+{
+ public function InitializeContent()
+ {
+ $serie = Serie::getSerieById($this->idGet);
+ $this->title = $serie->name;
+ [$this->entryArray, $this->totalNumber] = Book::getBooksBySeries($this->idGet, $this->n);
+ $this->idPage = $serie->getEntryId();
+ }
+}
diff --git a/lib/PageTagDetail.php b/lib/PageTagDetail.php
new file mode 100644
index 000000000..f47b75981
--- /dev/null
+++ b/lib/PageTagDetail.php
@@ -0,0 +1,18 @@
+
+ */
+
+class PageTagDetail extends Page
+{
+ public function InitializeContent()
+ {
+ $tag = Tag::getTagById($this->idGet);
+ $this->idPage = $tag->getEntryId();
+ $this->title = $tag->name;
+ [$this->entryArray, $this->totalNumber] = Book::getBooksByTag($this->idGet, $this->n);
+ }
+}
diff --git a/lib/Publisher.php b/lib/Publisher.php
new file mode 100644
index 000000000..cf81fdda0
--- /dev/null
+++ b/lib/Publisher.php
@@ -0,0 +1,74 @@
+
+ */
+
+class Publisher extends Base
+{
+ public const ALL_PUBLISHERS_ID = "cops:publishers";
+ public const PUBLISHERS_COLUMNS = "publishers.id as id, publishers.name as name, count(*) as count";
+ public const SQL_ALL_PUBLISHERS = "select {0} from publishers, books_publishers_link where publishers.id = publisher group by publishers.id, publishers.name order by publishers.name";
+ public const SQL_PUBLISHERS_FOR_SEARCH = "select {0} from publishers, books_publishers_link where publishers.id = publisher and upper (publishers.name) like ? group by publishers.id, publishers.name order by publishers.name";
+
+
+ public $id;
+ public $name;
+
+ public function __construct($post)
+ {
+ $this->id = $post->id;
+ $this->name = $post->name;
+ }
+
+ public function getUri()
+ {
+ return "?page=".parent::PAGE_PUBLISHER_DETAIL."&id=$this->id";
+ }
+
+ public function getEntryId()
+ {
+ return self::ALL_PUBLISHERS_ID.":".$this->id;
+ }
+
+ public static function getCount()
+ {
+ // str_format (localize("publishers.alphabetical", count(array))
+ return parent::getCountGeneric("publishers", self::ALL_PUBLISHERS_ID, parent::PAGE_ALL_PUBLISHERS);
+ }
+
+ public static function getPublisherByBookId($bookId)
+ {
+ $result = parent::getDb()->prepare('select publishers.id as id, name
+from books_publishers_link, publishers
+where publishers.id = publisher and book = ?');
+ $result->execute([$bookId]);
+ if ($post = $result->fetchObject()) {
+ return new Publisher($post);
+ }
+ return null;
+ }
+
+ public static function getPublisherById($publisherId)
+ {
+ $result = parent::getDb()->prepare('select id, name
+from publishers where id = ?');
+ $result->execute([$publisherId]);
+ if ($post = $result->fetchObject()) {
+ return new Publisher($post);
+ }
+ return null;
+ }
+
+ public static function getAllPublishers()
+ {
+ return Base::getEntryArrayWithBookNumber(self::SQL_ALL_PUBLISHERS, self::PUBLISHERS_COLUMNS, [], "Publisher");
+ }
+
+ public static function getAllPublishersByQuery($query)
+ {
+ return Base::getEntryArrayWithBookNumber(self::SQL_PUBLISHERS_FOR_SEARCH, self::PUBLISHERS_COLUMNS, ['%' . $query . '%'], "Publisher");
+ }
+}
diff --git a/lib/Rating.php b/lib/Rating.php
new file mode 100644
index 000000000..6363f9aa3
--- /dev/null
+++ b/lib/Rating.php
@@ -0,0 +1,72 @@
+id = $pid;
+ $this->name = $pname;
+ }
+
+ public function getUri()
+ {
+ return "?page=".parent::PAGE_RATING_DETAIL."&id=$this->id";
+ }
+
+ public function getEntryId()
+ {
+ return self::ALL_RATING_ID.":".$this->id;
+ }
+
+ public static function getCount()
+ {
+ // str_format (localize("ratings", count(array))
+ return parent::getCountGeneric("ratings", self::ALL_RATING_ID, parent::PAGE_ALL_RATINGS, "ratings");
+ }
+
+ public static function getAllRatings()
+ {
+ return self::getEntryArray(self::SQL_ALL_RATINGS, []);
+ }
+
+ public static function getEntryArray($query, $params)
+ {
+ [, $result] = parent::executeQuery($query, self::RATING_COLUMNS, "", $params, -1);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $ratingObj = new Rating($post->id, $post->rating);
+ $rating=$post->rating/2;
+ $rating = str_format(localize("ratingword", $rating), $rating);
+ array_push($entryArray, new Entry(
+ $rating,
+ $ratingObj->getEntryId(),
+ str_format(localize("bookword", $post->count), $post->count),
+ "text",
+ [ new LinkNavigation($ratingObj->getUri())],
+ "",
+ $post->count
+ ));
+ }
+ return $entryArray;
+ }
+
+ public static function getRatingById($ratingId)
+ {
+ $result = parent::getDb()->prepare('select rating from ratings where id = ?');
+ $result->execute([$ratingId]);
+ return new Rating($ratingId, $result->fetchColumn());
+ }
+}
diff --git a/lib/SQLQueries.php b/lib/SQLQueries.php
new file mode 100644
index 000000000..51e2556f5
--- /dev/null
+++ b/lib/SQLQueries.php
@@ -0,0 +1,29 @@
+
+ */
+
+class Serie extends Base
+{
+ public const ALL_SERIES_ID = "cops:series";
+ public const SERIES_COLUMNS = "series.id as id, series.name as name, series.sort as sort, count(*) as count";
+ public const SQL_ALL_SERIES = "select {0} from series, books_series_link where series.id = series group by series.id, series.name, series.sort order by series.sort";
+ public const SQL_SERIES_FOR_SEARCH = "select {0} from series, books_series_link where series.id = series and upper (series.name) like ? group by series.id, series.name, series.sort order by series.sort";
+
+ public $id;
+ public $name;
+
+ public function __construct($post)
+ {
+ $this->id = $post->id;
+ $this->name = $post->name;
+ }
+
+ public function getUri()
+ {
+ return "?page=".parent::PAGE_SERIE_DETAIL."&id=$this->id";
+ }
+
+ public function getEntryId()
+ {
+ return self::ALL_SERIES_ID.":".$this->id;
+ }
+
+ public static function getCount()
+ {
+ // str_format (localize("series.alphabetical", count(array))
+ return parent::getCountGeneric("series", self::ALL_SERIES_ID, parent::PAGE_ALL_SERIES);
+ }
+
+ public static function getSerieByBookId($bookId)
+ {
+ $result = parent::getDb()->prepare('select series.id as id, name
+from books_series_link, series
+where series.id = series and book = ?');
+ $result->execute([$bookId]);
+ if ($post = $result->fetchObject()) {
+ return new Serie($post);
+ }
+ return null;
+ }
+
+ public static function getSerieById($serieId)
+ {
+ $result = parent::getDb()->prepare('select id, name from series where id = ?');
+ $result->execute([$serieId]);
+ if ($post = $result->fetchObject()) {
+ return new Serie($post);
+ }
+ return null;
+ }
+
+ public static function getAllSeries()
+ {
+ return Base::getEntryArrayWithBookNumber(self::SQL_ALL_SERIES, self::SERIES_COLUMNS, [], "Serie");
+ }
+
+ public static function getAllSeriesByQuery($query)
+ {
+ return Base::getEntryArrayWithBookNumber(self::SQL_SERIES_FOR_SEARCH, self::SERIES_COLUMNS, ['%' . $query . '%'], "Serie");
+ }
+}
diff --git a/lib/Tag.php b/lib/Tag.php
new file mode 100644
index 000000000..d01580d15
--- /dev/null
+++ b/lib/Tag.php
@@ -0,0 +1,81 @@
+
+ */
+
+class Tag extends Base
+{
+ public const ALL_TAGS_ID = "cops:tags";
+ public const TAG_COLUMNS = "tags.id as id, tags.name as name, count(*) as count";
+ public const SQL_ALL_TAGS = "select {0} from tags, books_tags_link where tags.id = tag group by tags.id, tags.name order by tags.name";
+
+ public $id;
+ public $name;
+
+ public function __construct($post)
+ {
+ $this->id = $post->id;
+ $this->name = $post->name;
+ }
+
+ public function getUri()
+ {
+ return "?page=".parent::PAGE_TAG_DETAIL."&id=$this->id";
+ }
+
+ public function getEntryId()
+ {
+ return self::ALL_TAGS_ID.":".$this->id;
+ }
+
+ public static function getCount()
+ {
+ // str_format (localize("tags.alphabetical", count(array))
+ return parent::getCountGeneric("tags", self::ALL_TAGS_ID, parent::PAGE_ALL_TAGS);
+ }
+
+ public static function getTagById($tagId)
+ {
+ $result = parent::getDb()->prepare('select id, name from tags where id = ?');
+ $result->execute([$tagId]);
+ if ($post = $result->fetchObject()) {
+ return new Tag($post);
+ }
+ return null;
+ }
+
+ public static function getAllTags()
+ {
+ global $config;
+
+ $sql = self::SQL_ALL_TAGS;
+ $sortField = $config['calibre_database_field_sort'] ?? '';
+ if (!empty($sortField)) {
+ $sql = str_replace('tags.name', 'tags.' . $sortField, $sql);
+ }
+
+ return Base::getEntryArrayWithBookNumber($sql, self::TAG_COLUMNS, [], "Tag");
+ }
+
+ public static function getAllTagsByQuery($query, $n, $database = null, $numberPerPage = null)
+ {
+ $columns = "tags.id as id, tags.name as name, (select count(*) from books_tags_link where tags.id = tag) as count";
+ $sql = 'select {0} from tags where upper (tags.name) like ? {1} order by tags.name';
+ [$totalNumber, $result] = parent::executeQuery($sql, $columns, "", ['%' . $query . '%'], $n, $database, $numberPerPage);
+ $entryArray = [];
+ while ($post = $result->fetchObject()) {
+ $tag = new Tag($post);
+ array_push($entryArray, new Entry(
+ $tag->name,
+ $tag->getEntryId(),
+ str_format(localize("bookword", $post->count), $post->count),
+ "text",
+ [ new LinkNavigation($tag->getUri())]
+ ));
+ }
+ return [$entryArray, $totalNumber];
+ }
+}
diff --git a/login.html b/login.html
new file mode 100644
index 000000000..9a8c64073
--- /dev/null
+++ b/login.html
@@ -0,0 +1,97 @@
+
+
+
+
+
+ COPS
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..d4d93673e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "seblucas-cops",
+ "packageManager": "yarn@3.5.0",
+ "dependencies": {
+ "dot": "^1.1.3",
+ "jquery": "^1.12.4",
+ "js-cookie": "^2.2.1",
+ "lru-fast": "^0.2.2",
+ "magnific-popup": "^1.1.0",
+ "normalize.css": "^8.0.1"
+ }
+}
diff --git a/php-epub-meta/LICENSE b/php-epub-meta/LICENSE
deleted file mode 100644
index 128bf1f66..000000000
--- a/php-epub-meta/LICENSE
+++ /dev/null
@@ -1,19 +0,0 @@
-Copyright (c) 2012 Andreas Gohr
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/php-epub-meta/epub.php b/php-epub-meta/epub.php
deleted file mode 100644
index f6cf71f23..000000000
--- a/php-epub-meta/epub.php
+++ /dev/null
@@ -1,664 +0,0 @@
-
- */
-
-require_once('tbszip.php');
-
-define ("METADATA_FILE", "META-INF/container.xml");
-
-class EPub {
- public $xml; //FIXME change to protected, later
- protected $xpath;
- protected $file;
- protected $meta;
- protected $zip;
- protected $coverpath='';
- protected $namespaces;
- protected $imagetoadd='';
-
- /**
- * Constructor
- *
- * @param string $file path to epub file to work on
- * @throws Exception if metadata could not be loaded
- */
- public function __construct($file){
- // open file
- $this->file = $file;
- $this->zip = new clsTbsZip();
- if(!$this->zip->Open($this->file)){
- throw new Exception('Failed to read epub file');
- }
-
- // read container data
- if (!$this->zip->FileExists(METADATA_FILE)) {
- throw new Exception ("Unable to find metadata.xml");
- }
-
- $data = $this->zip->FileRead(METADATA_FILE);
- if($data == false){
- throw new Exception('Failed to access epub container data');
- }
- $xml = new DOMDocument();
- $xml->registerNodeClass('DOMElement','EPubDOMElement');
- $xml->loadXML($data);
- $xpath = new EPubDOMXPath($xml);
- $nodes = $xpath->query('//n:rootfiles/n:rootfile[@media-type="application/oebps-package+xml"]');
- $this->meta = $nodes->item(0)->attr('full-path');
-
- // load metadata
- if (!$this->zip->FileExists($this->meta)) {
- throw new Exception ("Unable to find " . $this->meta);
- }
-
- $data = $this->zip->FileRead($this->meta);
- if(!$data){
- throw new Exception('Failed to access epub metadata');
- }
- $this->xml = new DOMDocument();
- $this->xml->registerNodeClass('DOMElement','EPubDOMElement');
- $this->xml->loadXML($data);
- $this->xml->formatOutput = true;
- $this->xpath = new EPubDOMXPath($this->xml);
- }
-
- /**
- * file name getter
- */
- public function file(){
- return $this->file;
- }
-
- /**
- * Close the epub file
- */
- public function close (){
- $this->zip->FileCancelModif($this->meta);
- // TODO : Add cancelation of cover image
- $this->zip->Close ();
- }
-
- /**
- * Writes back all meta data changes
- * TODO update
- */
- public function save(){
- $this->download ();
- $zip->close();
- }
-
- /**
- * Get the updated epub
- */
- public function download($file=false){
- $this->zip->FileReplace($this->meta,$this->xml->saveXML());
- // add the cover image
- if($this->imagetoadd){
- $this->zip->FileReplace($this->coverpath,file_get_contents($this->imagetoadd));
- $this->imagetoadd='';
- }
- if ($file) $this->zip->Flush(TBSZIP_DOWNLOAD, $file);
- }
-
-
-
- /**
- * Get or set the book author(s)
- *
- * Authors should be given with a "file-as" and a real name. The file as
- * is used for sorting in e-readers.
- *
- * Example:
- *
- * array(
- * 'Pratchett, Terry' => 'Terry Pratchett',
- * 'Simpson, Jacqeline' => 'Jacqueline Simpson',
- * )
- *
- * @params array $authors
- */
- public function Authors($authors=false){
- // set new data
- if($authors !== false){
- // Author where given as a comma separated list
- if(is_string($authors)){
- if($authors == ''){
- $authors = array();
- }else{
- $authors = explode(',',$authors);
- $authors = array_map('trim',$authors);
- }
- }
-
- // delete existing nodes
- $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]');
- foreach($nodes as $node) $node->delete();
-
- // add new nodes
- $parent = $this->xpath->query('//opf:metadata')->item(0);
- foreach($authors as $as => $name){
- if(is_int($as)) $as = $name; //numeric array given
- $node = $parent->newChild('dc:creator',$name);
- $node->attr('opf:role', 'aut');
- $node->attr('opf:file-as', $as);
- }
-
- $this->reparse();
- }
-
- // read current data
- $rolefix = false;
- $authors = array();
- $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]');
- if($nodes->length == 0){
- // no nodes where found, let's try again without role
- $nodes = $this->xpath->query('//opf:metadata/dc:creator');
- $rolefix = true;
- }
- foreach($nodes as $node){
- $name = $node->nodeValue;
- $as = $node->attr('opf:file-as');
- if(!$as){
- $as = $name;
- $node->attr('opf:file-as',$as);
- }
- if($rolefix){
- $node->attr('opf:role','aut');
- }
- $authors[$as] = $name;
- }
- return $authors;
- }
-
- /**
- * Set or get the book title
- *
- * @param string $title
- */
- public function Title($title=false){
- return $this->getset('dc:title',$title);
- }
-
- /**
- * Set or get the book's language
- *
- * @param string $lang
- */
- public function Language($lang=false){
- return $this->getset('dc:language',$lang);
- }
-
- /**
- * Set or get the book' publisher info
- *
- * @param string $publisher
- */
- public function Publisher($publisher=false){
- return $this->getset('dc:publisher',$publisher);
- }
-
- /**
- * Set or get the book's copyright info
- *
- * @param string $rights
- */
- public function Copyright($rights=false){
- return $this->getset('dc:rights',$rights);
- }
-
- /**
- * Set or get the book's description
- *
- * @param string $description
- */
- public function Description($description=false){
- return $this->getset('dc:description',$description);
- }
-
- /**
- * Set or get the book's ISBN number
- *
- * @param string $isbn
- */
- public function ISBN($isbn=false){
- return $this->getset('dc:identifier',$isbn,'opf:scheme','ISBN');
- }
-
- /**
- * Set or get the Google Books ID
- *
- * @param string $google
- */
- public function Google($google=false){
- return $this->getset('dc:identifier',$google,'opf:scheme','GOOGLE');
- }
-
- /**
- * Set or get the Amazon ID of the book
- *
- * @param string $amazon
- */
- public function Amazon($amazon=false){
- return $this->getset('dc:identifier',$amazon,'opf:scheme','AMAZON');
- }
-
- /**
- * Set or get the Calibre UUID of the book
- *
- * @param string $uuid
- */
- public function Calibre($uuid=false){
- return $this->getset('dc:identifier',$uuid,'opf:scheme','calibre');
- }
-
- /**
- * Set or get the Serie of the book
- *
- * @param string $serie
- */
- public function Serie($serie=false){
- return $this->getset('opf:meta',$serie,'name','calibre:series','content');
- }
-
- /**
- * Set or get the Serie Index of the book
- *
- * @param string $serieIndex
- */
- public function SerieIndex($serieIndex=false){
- return $this->getset('opf:meta',$serieIndex,'name','calibre:series_index','content');
- }
-
- /**
- * Set or get the book's subjects (aka. tags)
- *
- * Subject should be given as array, but a comma separated string will also
- * be accepted.
- *
- * @param array $subjects
- */
- public function Subjects($subjects=false){
- // setter
- if($subjects !== false){
- if(is_string($subjects)){
- if($subjects === ''){
- $subjects = array();
- }else{
- $subjects = explode(',',$subjects);
- $subjects = array_map('trim',$subjects);
- }
- }
-
- // delete previous
- $nodes = $this->xpath->query('//opf:metadata/dc:subject');
- foreach($nodes as $node){
- $node->delete();
- }
- // add new ones
- $parent = $this->xpath->query('//opf:metadata')->item(0);
- foreach($subjects as $subj){
- $node = $this->xml->createElement('dc:subject',htmlspecialchars($subj));
- $node = $parent->appendChild($node);
- }
-
- $this->reparse();
- }
-
- //getter
- $subjects = array();
- $nodes = $this->xpath->query('//opf:metadata/dc:subject');
- foreach($nodes as $node){
- $subjects[] = $node->nodeValue;
- }
- return $subjects;
- }
-
- /**
- * Read the cover data
- *
- * Returns an associative array with the following keys:
- *
- * mime - filetype (usually image/jpeg)
- * data - the binary image data
- * found - the internal path, or false if no image is set in epub
- *
- * When no image is set in the epub file, the binary data for a transparent
- * GIF pixel is returned.
- *
- * When adding a new image this function return no or old data because the
- * image contents are not in the epub file, yet. The image will be added when
- * the save() method is called.
- *
- * @param string $path local filesystem path to a new cover image
- * @param string $mime mime type of the given file
- * @return array
- */
- public function Cover($path=false, $mime=false){
- // set cover
- if($path !== false){
- // remove current pointer
- $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
- foreach($nodes as $node) $node->delete();
- // remove previous manifest entries if they where made by us
- $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="php-epub-meta-cover"]');
- foreach($nodes as $node) $node->delete();
-
- if($path){
- // add pointer
- $parent = $this->xpath->query('//opf:metadata')->item(0);
- $node = $parent->newChild('opf:meta');
- $node->attr('opf:name','cover');
- $node->attr('opf:content','php-epub-meta-cover');
-
- // add manifest
- $parent = $this->xpath->query('//opf:manifest')->item(0);
- $node = $parent->newChild('opf:item');
- $node->attr('id','php-epub-meta-cover');
- $node->attr('opf:href','php-epub-meta-cover.img');
- $node->attr('opf:media-type',$mime);
-
- // remember path for save action
- $this->imagetoadd = $path;
- }
-
- $this->reparse();
- }
-
- // load cover
- $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
- if(!$nodes->length) return $this->no_cover();
- $coverid = (String) $nodes->item(0)->attr('opf:content');
- if(!$coverid) return $this->no_cover();
-
- $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]');
- if(!$nodes->length) return $this->no_cover();
- $mime = $nodes->item(0)->attr('opf:media-type');
- $path = $nodes->item(0)->attr('opf:href');
- $path = dirname('/'.$this->meta).'/'.$path; // image path is relative to meta file
- $path = ltrim($path,'/');
-
- $zip = new ZipArchive();
- if(!@$zip->open($this->file)){
- throw new Exception('Failed to read epub file');
- }
- $data = $zip->getFromName($path);
-
- return array(
- 'mime' => $mime,
- 'data' => $data,
- 'found' => $path
- );
- }
-
- public function getCoverItem () {
- $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]');
- if(!$nodes->length) return NULL;
-
- $coverid = (String) $nodes->item(0)->attr('opf:content');
- if(!$coverid) return NULL;
-
- $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]');
- if(!$nodes->length) return NULL;
-
- return $nodes->item(0);
- }
-
- public function updateForKepub () {
- $item = $this->getCoverItem ();
- if (!is_null ($item)) {
- $item->attr('opf:properties', 'cover-image');
- }
- }
-
-
- public function Cover2($path=false, $mime=false){
- $hascover = true;
- $item = $this->getCoverItem ();
- if (is_null ($item)) {
- $hascover = false;
- } else {
- $mime = $item->attr('opf:media-type');
- $this->coverpath = $item->attr('opf:href');
- $this->coverpath = dirname('/'.$this->meta).'/'.$this->coverpath; // image path is relative to meta file
- $this->coverpath = ltrim($this->coverpath,'/');
- }
-
- // set cover
- if($path !== false){
- if (!$hascover) return; // TODO For now only update
-
- if($path){
- $item->attr('opf:media-type',$mime);
-
- // remember path for save action
- $this->imagetoadd = $path;
- }
-
- $this->reparse();
- }
-
- if (!$hascover) return $this->no_cover();
-
- $zip = new ZipArchive();
- if(!@$zip->open($this->file)){
- throw new Exception('Failed to read epub file');
- }
- $data = $zip->getFromName($this->coverpath);
-
- return array(
- 'mime' => $mime,
- 'data' => $data,
- 'found' => $this->coverpath
- );
- }
-
- /**
- * A simple getter/setter for simple meta attributes
- *
- * It should only be used for attributes that are expected to be unique
- *
- * @param string $item XML node to set/get
- * @param string $value New node value
- * @param string $att Attribute name
- * @param string $aval Attribute value
- * @param string $datt Destination attribute
- */
- protected function getset($item,$value=false,$att=false,$aval=false,$datt=false){
- // construct xpath
- $xpath = '//opf:metadata/'.$item;
- if($att){
- $xpath .= "[@$att=\"$aval\"]";
- }
-
- // set value
- if($value !== false){
- $value = htmlspecialchars($value);
- $nodes = $this->xpath->query($xpath);
- if($nodes->length == 1 ){
- if($value === ''){
- // the user want's to empty this value -> delete the node
- $nodes->item(0)->delete();
- }else{
- // replace value
- if ($datt){
- $nodes->item(0)->attr ($datt, $value);
- }else{
- $nodes->item(0)->nodeValue = $value;
- }
- }
- }else{
- // if there are multiple matching nodes for some reason delete
- // them. we'll replace them all with our own single one
- foreach($nodes as $n) $n->delete();
- // readd them
- if($value){
- $parent = $this->xpath->query('//opf:metadata')->item(0);
-
- $node = $parent->newChild ($item);
- if($att) $node->attr($att,$aval);
- if ($datt){
- $node->attr ($datt, $value);
- }else{
- $node->nodeValue = $value;
- }
- }
- }
-
- $this->reparse();
- }
-
- // get value
- $nodes = $this->xpath->query($xpath);
- if($nodes->length){
- if ($datt){
- return $nodes->item(0)->attr ($datt);
- }else{
- return $nodes->item(0)->nodeValue;
- }
- }else{
- return '';
- }
- }
-
- /**
- * Return a not found response for Cover()
- */
- protected function no_cover(){
- return array(
- 'data' => base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'),
- 'mime' => 'image/gif',
- 'found' => false
- );
- }
-
- /**
- * Reparse the DOM tree
- *
- * I had to rely on this because otherwise xpath failed to find the newly
- * added nodes
- */
- protected function reparse() {
- $this->xml->loadXML($this->xml->saveXML());
- $this->xpath = new EPubDOMXPath($this->xml);
- }
-}
-
-class EPubDOMXPath extends DOMXPath {
- public function __construct(DOMDocument $doc){
- parent::__construct($doc);
-
- if(is_a($doc->documentElement, 'EPubDOMElement')){
- foreach($doc->documentElement->namespaces as $ns => $url){
- $this->registerNamespace($ns,$url);
- }
- }
- }
-}
-
-class EPubDOMElement extends DOMElement {
- public $namespaces = array(
- 'n' => 'urn:oasis:names:tc:opendocument:xmlns:container',
- 'opf' => 'http://www.idpf.org/2007/opf',
- 'dc' => 'http://purl.org/dc/elements/1.1/'
- );
-
-
- public function __construct($name, $value='', $namespaceURI=''){
- list($ns,$name) = $this->splitns($name);
- $value = htmlspecialchars($value);
- if(!$namespaceURI && $ns){
- $namespaceURI = $this->namespaces[$ns];
- }
- parent::__construct($name, $value, $namespaceURI);
- }
-
-
- /**
- * Create and append a new child
- *
- * Works with our epub namespaces and omits default namespaces
- */
- public function newChild($name, $value=''){
- list($ns,$local) = $this->splitns($name);
- if($ns){
- $nsuri = $this->namespaces[$ns];
- if($this->isDefaultNamespace($nsuri)){
- $name = $local;
- $nsuri = '';
- }
- }
-
- // this doesn't call the construcor: $node = $this->ownerDocument->createElement($name,$value);
- $node = new EPubDOMElement($name,$value,$nsuri);
- return $this->appendChild($node);
- }
-
- /**
- * Split given name in namespace prefix and local part
- *
- * @param string $name
- * @return array (namespace, name)
- */
- public function splitns($name){
- $list = explode(':',$name,2);
- if(count($list) < 2) array_unshift($list,'');
- return $list;
- }
-
- /**
- * Simple EPub namespace aware attribute accessor
- */
- public function attr($attr,$value=null){
- list($ns,$attr) = $this->splitns($attr);
-
- $nsuri = '';
- if($ns){
- $nsuri = $this->namespaces[$ns];
- if(!$this->namespaceURI){
- if($this->isDefaultNamespace($nsuri)){
- $nsuri = '';
- }
- }elseif($this->namespaceURI == $nsuri){
- $nsuri = '';
- }
- }
-
- if(!is_null($value)){
- if($value === false){
- // delete if false was given
- if($nsuri){
- $this->removeAttributeNS($nsuri,$attr);
- }else{
- $this->removeAttribute($attr);
- }
- }else{
- // modify if value was given
- if($nsuri){
- $this->setAttributeNS($nsuri,$attr,$value);
- }else{
- $this->setAttribute($attr,$value);
- }
- }
- }else{
- // return value if none was given
- if($nsuri){
- return $this->getAttributeNS($nsuri,$attr);
- }else{
- return $this->getAttribute($attr);
- }
- }
- }
-
- /**
- * Remove this node from the DOM
- */
- public function delete(){
- $this->parentNode->removeChild($this);
- }
-
-}
-
-
diff --git a/php-epub-meta/tbszip.php b/php-epub-meta/tbszip.php
deleted file mode 100644
index ba1214de9..000000000
--- a/php-epub-meta/tbszip.php
+++ /dev/null
@@ -1,883 +0,0 @@
-Meth8Ok = extension_loaded('zlib'); // check if Zlib extension is available. This is need for compress and uncompress with method 8.
- $this->DisplayError = true;
- $this->ArchFile = '';
- $this->Error = false;
- }
-
- function CreateNew($ArchName='new.zip') {
- // Create a new virtual empty archive, the name will be the default name when the archive is flushed.
- if (!isset($this->Meth8Ok)) $this->__construct(); // for PHP 4 compatibility
- $this->Close(); // note that $this->ArchHnd is set to false here
- $this->Error = false;
- $this->ArchFile = $ArchName;
- $this->ArchIsNew = true;
- $bin = 'PK'.chr(05).chr(06).str_repeat(chr(0), 18);
- $this->CdEndPos = strlen($bin) - 4;
- $this->CdInfo = array('disk_num_curr'=>0, 'disk_num_cd'=>0, 'file_nbr_curr'=>0, 'file_nbr_tot'=>0, 'l_cd'=>0, 'p_cd'=>0, 'l_comm'=>0, 'v_comm'=>'', 'bin'=>$bin);
- $this->CdPos = $this->CdInfo['p_cd'];
- }
-
- function Open($ArchFile) {
- // Open the zip archive
- if (!isset($this->Meth8Ok)) $this->__construct(); // for PHP 4 compatibility
- $this->Close(); // close handle and init info
- $this->Error = false;
- $this->ArchFile = $ArchFile;
- $this->ArchIsNew = false;
- // open the file
- $this->ArchHnd = fopen($ArchFile, 'rb');
- $ok = !($this->ArchHnd===false);
- if ($ok) $ok = $this->CentralDirRead();
- return $ok;
- }
-
- function Close() {
- if (isset($this->ArchHnd) and ($this->ArchHnd!==false)) fclose($this->ArchHnd);
- $this->ArchFile = '';
- $this->ArchHnd = false;
- $this->CdInfo = array();
- $this->CdFileLst = array();
- $this->CdFileNbr = 0;
- $this->CdFileByName = array();
- $this->VisFileLst = array();
- $this->ArchCancelModif();
- }
-
- function ArchCancelModif() {
- $this->LastReadComp = false; // compression of the last read file (1=compressed, 0=stored not compressed, -1= stored compressed but read uncompressed)
- $this->LastReadIdx = false; // index of the last file read
- $this->ReplInfo = array();
- $this->ReplByPos = array();
- $this->AddInfo = array();
- }
-
- function FileAdd($Name, $Data, $DataType=TBSZIP_STRING, $Compress=true) {
-
- if ($Data===false) return $this->FileCancelModif($Name, false); // Cancel a previously added file
-
- // Save information for adding a new file into the archive
- $Diff = 30 + 46 + 2*strlen($Name); // size of the header + cd info
- $Ref = $this->_DataCreateNewRef($Data, $DataType, $Compress, $Diff, $Name);
- if ($Ref===false) return false;
- $Ref['name'] = $Name;
- $this->AddInfo[] = $Ref;
- return $Ref['res'];
-
- }
-
- function CentralDirRead() {
- $cd_info = 'PK'.chr(05).chr(06); // signature of the Central Directory
- $cd_pos = -22;
- $this->_MoveTo($cd_pos, SEEK_END);
- $b = $this->_ReadData(4);
- if ($b!==$cd_info) return $this->RaiseError('The footer of the Central Directory is not found.');
-
- $this->CdEndPos = ftell($this->ArchHnd) - 4;
- $this->CdInfo = $this->CentralDirRead_End($cd_info);
- $this->CdFileLst = array();
- $this->CdFileNbr = $this->CdInfo['file_nbr_curr'];
- $this->CdPos = $this->CdInfo['p_cd'];
-
- if ($this->CdFileNbr<=0) return $this->RaiseError('No file found in the Central Directory.');
- if ($this->CdPos<=0) return $this->RaiseError('No position found for the Central Directory listing.');
-
- $this->_MoveTo($this->CdPos);
- for ($i=0;$i<$this->CdFileNbr;$i++) {
- $x = $this->CentralDirRead_File($i);
- if ($x!==false) {
- $this->CdFileLst[$i] = $x;
- $this->CdFileByName[$x['v_name']] = $i;
- }
- }
- return true;
- }
-
- function CentralDirRead_End($cd_info) {
- $b = $cd_info.$this->_ReadData(18);
- $x = array();
- $x['disk_num_curr'] = $this->_GetDec($b,4,2); // number of this disk
- $x['disk_num_cd'] = $this->_GetDec($b,6,2); // number of the disk with the start of the central directory
- $x['file_nbr_curr'] = $this->_GetDec($b,8,2); // total number of entries in the central directory on this disk
- $x['file_nbr_tot'] = $this->_GetDec($b,10,2); // total number of entries in the central directory
- $x['l_cd'] = $this->_GetDec($b,12,4); // size of the central directory
- $x['p_cd'] = $this->_GetDec($b,16,4); // offset of start of central directory with respect to the starting disk number
- $x['l_comm'] = $this->_GetDec($b,20,2); // .ZIP file comment length
- $x['v_comm'] = $this->_ReadData($x['l_comm']); // .ZIP file comment
- $x['bin'] = $b.$x['v_comm'];
- return $x;
- }
-
- function CentralDirRead_File($idx) {
-
- $b = $this->_ReadData(46);
-
- $x = $this->_GetHex($b,0,4);
- if ($x!=='h:02014b50') return $this->RaiseError('Signature of file information not found in the Central Directory in position '.(ftell($this->ArchHnd)-46).' for file #'.$idx.'.');
-
- $x = array();
- $x['vers_used'] = $this->_GetDec($b,4,2);
- $x['vers_necess'] = $this->_GetDec($b,6,2);
- $x['purp'] = $this->_GetBin($b,8,2);
- $x['meth'] = $this->_GetDec($b,10,2);
- $x['time'] = $this->_GetDec($b,12,2);
- $x['date'] = $this->_GetDec($b,14,2);
- $x['crc32'] = $this->_GetDec($b,16,4);
- $x['l_data_c'] = $this->_GetDec($b,20,4);
- $x['l_data_u'] = $this->_GetDec($b,24,4);
- $x['l_name'] = $this->_GetDec($b,28,2);
- $x['l_fields'] = $this->_GetDec($b,30,2);
- $x['l_comm'] = $this->_GetDec($b,32,2);
- $x['disk_num'] = $this->_GetDec($b,34,2);
- $x['int_file_att'] = $this->_GetDec($b,36,2);
- $x['ext_file_att'] = $this->_GetDec($b,38,4);
- $x['p_loc'] = $this->_GetDec($b,42,4);
- $x['v_name'] = $this->_ReadData($x['l_name']);
- $x['v_fields'] = $this->_ReadData($x['l_fields']);
- $x['v_comm'] = $this->_ReadData($x['l_comm']);
-
- $x['bin'] = $b.$x['v_name'].$x['v_fields'].$x['v_comm'];
-
- return $x;
- }
-
- function RaiseError($Msg) {
- if ($this->DisplayError) echo ''.get_class($this).' ERROR : '.$Msg.' '."\r\n";
- $this->Error = $Msg;
- return false;
- }
-
- function Debug($FileHeaders=false) {
-
- $this->DisplayError = true;
-
- echo " \r\n";
- echo "------------------ \r\n";
- echo "Central Directory: \r\n";
- echo "------------------ \r\n";
- print_r($this->CdInfo);
-
- echo " \r\n";
- echo "----------------------------------- \r\n";
- echo "File List in the Central Directory: \r\n";
- echo "----------------------------------- \r\n";
- print_r($this->CdFileLst);
-
- if ($FileHeaders) {
- echo " \r\n";
- echo "------------------------------ \r\n";
- echo "File List in the Data Section: \r\n";
- echo "------------------------------ \r\n";
- $idx = 0;
- $pos = 0;
- $this->_MoveTo($pos);
- while ($ok = $this->_ReadFile($idx,false)) {
- $this->VisFileLst[$idx]['debug_pos'] = $pos;
- $pos = ftell($this->ArchHnd);
- $idx++;
- }
- print_r($this->VisFileLst);
- }
-
- }
-
- function FileExists($NameOrIdx) {
- return ($this->FileGetIdx($NameOrIdx)!==false);
- }
-
- function FileGetIdx($NameOrIdx) {
- // Check if a file name, or a file index exists in the Central Directory, and return its index
- if (is_string($NameOrIdx)) {
- if (isset($this->CdFileByName[$NameOrIdx])) {
- return $this->CdFileByName[$NameOrIdx];
- } else {
- return false;
- }
- } else {
- if (isset($this->CdFileLst[$NameOrIdx])) {
- return $NameOrIdx;
- } else {
- return false;
- }
- }
- }
-
- function FileGetIdxAdd($Name) {
- // Check if a file name exists in the list of file to add, and return its index
- if (!is_string($Name)) return false;
- $idx_lst = array_keys($this->AddInfo);
- foreach ($idx_lst as $idx) {
- if ($this->AddInfo[$idx]['name']===$Name) return $idx;
- }
- return false;
- }
-
- function FileRead($NameOrIdx, $Uncompress=true) {
-
- $this->LastReadComp = false; // means the file is not found
- $this->LastReadIdx = false;
-
- $idx = $this->FileGetIdx($NameOrIdx);
- if ($idx===false) return $this->RaiseError('File "'.$NameOrIdx.'" is not found in the Central Directory.');
-
- $pos = $this->CdFileLst[$idx]['p_loc'];
- $this->_MoveTo($pos);
-
- $this->LastReadIdx = $idx; // Can be usefull to get the idx
-
- $Data = $this->_ReadFile($idx, true);
-
- // Manage uncompression
- $Comp = 1; // means the contents stays compressed
- $meth = $this->CdFileLst[$idx]['meth'];
- if ($meth==8) {
- if ($Uncompress) {
- if ($this->Meth8Ok) {
- $Data = gzinflate($Data);
- $Comp = -1; // means uncompressed
- } else {
- $this->RaiseError('Unable to uncompress file "'.$NameOrIdx.'" because extension Zlib is not installed.');
- }
- }
- } elseif($meth==0) {
- $Comp = 0; // means stored without compression
- } else {
- if ($Uncompress) $this->RaiseError('Unable to uncompress file "'.$NameOrIdx.'" because it is compressed with method '.$meth.'.');
- }
- $this->LastReadComp = $Comp;
-
- return $Data;
-
- }
-
- function _ReadFile($idx, $ReadData) {
- // read the file header (and maybe the data ) in the archive, assuming the cursor in at a new file position
-
- $b = $this->_ReadData(30);
-
- $x = $this->_GetHex($b,0,4);
- if ($x!=='h:04034b50') return $this->RaiseError('Signature of file information not found in the Data Section in position '.(ftell($this->ArchHnd)-30).' for file #'.$idx.'.');
-
- $x = array();
- $x['vers'] = $this->_GetDec($b,4,2);
- $x['purp'] = $this->_GetBin($b,6,2);
- $x['meth'] = $this->_GetDec($b,8,2);
- $x['time'] = $this->_GetDec($b,10,2);
- $x['date'] = $this->_GetDec($b,12,2);
- $x['crc32'] = $this->_GetDec($b,14,4);
- $x['l_data_c'] = $this->_GetDec($b,18,4);
- $x['l_data_u'] = $this->_GetDec($b,22,4);
- $x['l_name'] = $this->_GetDec($b,26,2);
- $x['l_fields'] = $this->_GetDec($b,28,2);
- $x['v_name'] = $this->_ReadData($x['l_name']);
- $x['v_fields'] = $this->_ReadData($x['l_fields']);
-
- $x['bin'] = $b.$x['v_name'].$x['v_fields'];
-
- // Read Data
- $len_cd = $this->CdFileLst[$idx]['l_data_c'];
- if ($x['l_data_c']==0) {
- // Sometimes, the size is not specified in the local information.
- $len = $len_cd;
- } else {
- $len = $x['l_data_c'];
- if ($len!=$len_cd) {
- //echo "TbsZip Warning: Local information for file #".$idx." says len=".$len.", while Central Directory says len=".$len_cd.".";
- }
- }
-
- if ($ReadData) {
- $Data = $this->_ReadData($len);
- } else {
- $this->_MoveTo($len, SEEK_CUR);
- }
-
- // Description information
- $desc_ok = ($x['purp'][2+3]=='1');
- if ($desc_ok) {
- $b = $this->_ReadData(16);
- $x['desc_bin'] = $b;
- $x['desc_sign'] = $this->_GetHex($b,0,4); // not specified in the documentation sign=h:08074b50
- $x['desc_crc32'] = $this->_GetDec($b,4,4);
- $x['desc_l_data_c'] = $this->_GetDec($b,8,4);
- $x['desc_l_data_u'] = $this->_GetDec($b,12,4);
- }
-
- // Save file info without the data
- $this->VisFileLst[$idx] = $x;
-
- // Return the info
- if ($ReadData) {
- return $Data;
- } else {
- return true;
- }
-
- }
-
- function FileReplace($NameOrIdx, $Data, $DataType=TBSZIP_STRING, $Compress=true) {
- // Store replacement information.
-
- $idx = $this->FileGetIdx($NameOrIdx);
- if ($idx===false) return $this->RaiseError('File "'.$NameOrIdx.'" is not found in the Central Directory.');
-
- $pos = $this->CdFileLst[$idx]['p_loc'];
-
- if ($Data===false) {
- // file to delete
- $this->ReplInfo[$idx] = false;
- $Result = true;
- } else {
- // file to replace
- $Diff = - $this->CdFileLst[$idx]['l_data_c'];
- $Ref = $this->_DataCreateNewRef($Data, $DataType, $Compress, $Diff, $NameOrIdx);
- if ($Ref===false) return false;
- $this->ReplInfo[$idx] = $Ref;
- $Result = $Ref['res'];
- }
-
- $this->ReplByPos[$pos] = $idx;
-
- return $Result;
-
- }
-
- function FileCancelModif($NameOrIdx, $ReplacedAndDeleted=true) {
- // cancel added, modified or deleted modifications on a file in the archive
- // return the number of cancels
-
- $nbr = 0;
-
- if ($ReplacedAndDeleted) {
- // replaced or deleted files
- $idx = $this->FileGetIdx($NameOrIdx);
- if ($idx!==false) {
- if (isset($this->ReplInfo[$idx])) {
- $pos = $this->CdFileLst[$idx]['p_loc'];
- unset($this->ReplByPos[$pos]);
- unset($this->ReplInfo[$idx]);
- $nbr++;
- }
- }
- }
-
- // added files
- $idx = $this->FileGetIdxAdd($NameOrIdx);
- if ($idx!==false) {
- unset($this->AddInfo[$idx]);
- $nbr++;
- }
-
- return $nbr;
-
- }
-
- function Flush($Render=TBSZIP_DOWNLOAD, $File='', $ContentType='') {
-
- if ( ($File!=='') && ($this->ArchFile===$File)) {
- $this->RaiseError('Method Flush() cannot overwrite the current opened archive: \''.$File.'\''); // this makes corrupted zip archives without PHP error.
- return false;
- }
-
- $ArchPos = 0;
- $Delta = 0;
- $FicNewPos = array();
- $DelLst = array(); // idx of deleted files
- $DeltaCdLen = 0; // delta of the CD's size
-
- $now = time();
- $date = $this->_MsDos_Date($now);
- $time = $this->_MsDos_Time($now);
-
- if (!$this->OutputOpen($Render, $File, $ContentType)) return false;
-
- // output modified zipped files and unmodified zipped files that are beetween them
- ksort($this->ReplByPos);
- foreach ($this->ReplByPos as $ReplPos => $ReplIdx) {
- // output data from the zip archive which is before the data to replace
- $this->OutputFromArch($ArchPos, $ReplPos);
- // get current file information
- if (!isset($this->VisFileLst[$ReplIdx])) $this->_ReadFile($ReplIdx, false);
- $FileInfo =& $this->VisFileLst[$ReplIdx];
- $b1 = $FileInfo['bin'];
- if (isset($FileInfo['desc_bin'])) {
- $b2 = $FileInfo['desc_bin'];
- } else {
- $b2 = '';
- }
- $info_old_len = strlen($b1) + $this->CdFileLst[$ReplIdx]['l_data_c'] + strlen($b2); // $FileInfo['l_data_c'] may have a 0 value in some archives
- // get replacement information
- $ReplInfo =& $this->ReplInfo[$ReplIdx];
- if ($ReplInfo===false) {
- // The file is to be deleted
- $Delta = $Delta - $info_old_len; // headers and footers are also deleted
- $DelLst[$ReplIdx] = true;
- } else {
- // prepare the header of the current file
- $this->_DataPrepare($ReplInfo); // get data from external file if necessary
- $this->_PutDec($b1, $time, 10, 2); // time
- $this->_PutDec($b1, $date, 12, 2); // date
- $this->_PutDec($b1, $ReplInfo['crc32'], 14, 4); // crc32
- $this->_PutDec($b1, $ReplInfo['len_c'], 18, 4); // l_data_c
- $this->_PutDec($b1, $ReplInfo['len_u'], 22, 4); // l_data_u
- if ($ReplInfo['meth']!==false) $this->_PutDec($b1, $ReplInfo['meth'], 8, 2); // meth
- // prepare the bottom description if the zipped file, if any
- if ($b2!=='') {
- $this->_PutDec($b2, $ReplInfo['crc32'], 4, 4); // crc32
- $this->_PutDec($b2, $ReplInfo['len_c'], 8, 4); // l_data_c
- $this->_PutDec($b2, $ReplInfo['len_u'], 12, 4); // l_data_u
- }
- // output data
- $this->OutputFromString($b1.$ReplInfo['data'].$b2);
- unset($ReplInfo['data']); // save PHP memory
- $Delta = $Delta + $ReplInfo['diff'] + $ReplInfo['len_c'];
- }
- // Update the delta of positions for zipped files which are physically after the currently replaced one
- for ($i=0;$i<$this->CdFileNbr;$i++) {
- if ($this->CdFileLst[$i]['p_loc']>$ReplPos) {
- $FicNewPos[$i] = $this->CdFileLst[$i]['p_loc'] + $Delta;
- }
- }
- // Update the current pos in the archive
- $ArchPos = $ReplPos + $info_old_len;
- }
-
- // Ouput all the zipped files that remain before the Central Directory listing
- if ($this->ArchHnd!==false) $this->OutputFromArch($ArchPos, $this->CdPos); // ArchHnd is false if CreateNew() has been called
- $ArchPos = $this->CdPos;
-
- // Output file to add
- $AddNbr = count($this->AddInfo);
- $AddDataLen = 0; // total len of added data (inlcuding file headers)
- if ($AddNbr>0) {
- $AddPos = $ArchPos + $Delta; // position of the start
- $AddLst = array_keys($this->AddInfo);
- foreach ($AddLst as $idx) {
- $n = $this->_DataOuputAddedFile($idx, $AddPos);
- $AddPos += $n;
- $AddDataLen += $n;
- }
- }
-
- // Modifiy file information in the Central Directory for replaced files
- $b2 = '';
- $old_cd_len = 0;
- for ($i=0;$i<$this->CdFileNbr;$i++) {
- $b1 = $this->CdFileLst[$i]['bin'];
- $old_cd_len += strlen($b1);
- if (!isset($DelLst[$i])) {
- if (isset($FicNewPos[$i])) $this->_PutDec($b1, $FicNewPos[$i], 42, 4); // p_loc
- if (isset($this->ReplInfo[$i])) {
- $ReplInfo =& $this->ReplInfo[$i];
- $this->_PutDec($b1, $time, 12, 2); // time
- $this->_PutDec($b1, $date, 14, 2); // date
- $this->_PutDec($b1, $ReplInfo['crc32'], 16, 4); // crc32
- $this->_PutDec($b1, $ReplInfo['len_c'], 20, 4); // l_data_c
- $this->_PutDec($b1, $ReplInfo['len_u'], 24, 4); // l_data_u
- if ($ReplInfo['meth']!==false) $this->_PutDec($b1, $ReplInfo['meth'], 10, 2); // meth
- }
- $b2 .= $b1;
- }
- }
- $this->OutputFromString($b2);
- $ArchPos += $old_cd_len;
- $DeltaCdLen = $DeltaCdLen + strlen($b2) - $old_cd_len;
-
- // Output until Central Directory footer
- if ($this->ArchHnd!==false) $this->OutputFromArch($ArchPos, $this->CdEndPos); // ArchHnd is false if CreateNew() has been called
-
- // Output file information of the Central Directory for added files
- if ($AddNbr>0) {
- $b2 = '';
- foreach ($AddLst as $idx) {
- $b2 .= $this->AddInfo[$idx]['bin'];
- }
- $this->OutputFromString($b2);
- $DeltaCdLen += strlen($b2);
- }
-
- // Output Central Directory footer
- $b2 = $this->CdInfo['bin'];
- $DelNbr = count($DelLst);
- if ( ($AddNbr>0) or ($DelNbr>0) ) {
- // total number of entries in the central directory on this disk
- $n = $this->_GetDec($b2, 8, 2);
- $this->_PutDec($b2, $n + $AddNbr - $DelNbr, 8, 2);
- // total number of entries in the central directory
- $n = $this->_GetDec($b2, 10, 2);
- $this->_PutDec($b2, $n + $AddNbr - $DelNbr, 10, 2);
- // size of the central directory
- $n = $this->_GetDec($b2, 12, 4);
- $this->_PutDec($b2, $n + $DeltaCdLen, 12, 4);
- $Delta = $Delta + $AddDataLen;
- }
- $this->_PutDec($b2, $this->CdPos+$Delta , 16, 4); // p_cd (offset of start of central directory with respect to the starting disk number)
- $this->OutputFromString($b2);
-
- $this->OutputClose();
-
- return true;
-
- }
-
- // ----------------
- // output functions
- // ----------------
-
- function OutputOpen($Render, $File, $ContentType) {
-
- if (($Render & TBSZIP_FILE)==TBSZIP_FILE) {
- $this->OutputMode = TBSZIP_FILE;
- if (''.$File=='') $File = basename($this->ArchFile).'.zip';
- $this->OutputHandle = @fopen($File, 'w');
- if ($this->OutputHandle===false) {
- $this->RaiseError('Method Flush() cannot overwrite the target file \''.$File.'\'. This may not be a valid file path or the file may be locked by another process or because of a denied permission.');
- return false;
- }
- } elseif (($Render & TBSZIP_STRING)==TBSZIP_STRING) {
- $this->OutputMode = TBSZIP_STRING;
- $this->OutputSrc = '';
- } elseif (($Render & TBSZIP_DOWNLOAD)==TBSZIP_DOWNLOAD) {
- $this->OutputMode = TBSZIP_DOWNLOAD;
- // Output the file
- if (''.$File=='') $File = basename($this->ArchFile);
- if (($Render & TBSZIP_NOHEADER)==TBSZIP_NOHEADER) {
- } else {
- header ('Pragma: no-cache');
- if ($ContentType!='') header ('Content-Type: '.$ContentType);
- header('Content-Disposition: attachment; filename="'.$File.'"');
- header('Expires: 0');
- header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
- header('Cache-Control: public');
- header('Content-Description: File Transfer');
- header('Content-Transfer-Encoding: binary');
- $Len = $this->_EstimateNewArchSize();
- if ($Len!==false) header('Content-Length: '.$Len);
- }
- }
-
- return true;
-
- }
-
- function OutputFromArch($pos, $pos_stop) {
- $len = $pos_stop - $pos;
- if ($len<0) return;
- $this->_MoveTo($pos);
- $block = 1024;
- while ($len>0) {
- $l = min($len, $block);
- $x = $this->_ReadData($l);
- $this->OutputFromString($x);
- $len = $len - $l;
- }
- unset($x);
- }
-
- function OutputFromString($data) {
- if ($this->OutputMode===TBSZIP_DOWNLOAD) {
- echo $data; // donwload
- } elseif ($this->OutputMode===TBSZIP_STRING) {
- $this->OutputSrc .= $data; // to string
- } elseif (TBSZIP_FILE) {
- fwrite($this->OutputHandle, $data); // to file
- }
- }
-
- function OutputClose() {
- if ( ($this->OutputMode===TBSZIP_FILE) && ($this->OutputHandle!==false) ) {
- fclose($this->OutputHandle);
- $this->OutputHandle = false;
- }
- }
-
- // ----------------
- // Reading functions
- // ----------------
-
- function _MoveTo($pos, $relative = SEEK_SET) {
- fseek($this->ArchHnd, $pos, $relative);
- }
-
- function _ReadData($len) {
- if ($len>0) {
- $x = fread($this->ArchHnd, $len);
- return $x;
- } else {
- return '';
- }
- }
-
- // ----------------
- // Take info from binary data
- // ----------------
-
- function _GetDec($txt, $pos, $len) {
- $x = substr($txt, $pos, $len);
- $z = 0;
- for ($i=0;$i<$len;$i++) {
- $asc = ord($x[$i]);
- if ($asc>0) $z = $z + $asc*pow(256,$i);
- }
- return $z;
- }
-
- function _GetHex($txt, $pos, $len) {
- $x = substr($txt, $pos, $len);
- return 'h:'.bin2hex(strrev($x));
- }
-
- function _GetBin($txt, $pos, $len) {
- $x = substr($txt, $pos, $len);
- $z = '';
- for ($i=0;$i<$len;$i++) {
- $asc = ord($x[$i]);
- if (isset($x[$i])) {
- for ($j=0;$j<8;$j++) {
- $z .= ($asc & pow(2,$j)) ? '1' : '0';
- }
- } else {
- $z .= '00000000';
- }
- }
- return 'b:'.$z;
- }
-
- // ----------------
- // Put info into binary data
- // ----------------
-
- function _PutDec(&$txt, $val, $pos, $len) {
- $x = '';
- for ($i=0;$i<$len;$i++) {
- if ($val==0) {
- $z = 0;
- } else {
- $z = intval($val % 256);
- if (($val<0) && ($z!=0)) { // ($z!=0) is very important, example: val=-420085702
- // special opration for negative value. If the number id too big, PHP stores it into a signed integer. For example: crc32('coucou') => -256185401 instead of 4038781895. NegVal = BigVal - (MaxVal+1) = BigVal - 256^4
- $val = ($val - $z)/256 -1;
- $z = 256 + $z;
- } else {
- $val = ($val - $z)/256;
- }
- }
- $x .= chr($z);
- }
- $txt = substr_replace($txt, $x, $pos, $len);
- }
-
- function _MsDos_Date($Timestamp = false) {
- // convert a date-time timstamp into the MS-Dos format
- $d = ($Timestamp===false) ? getdate() : getdate($Timestamp);
- return (($d['year']-1980)*512) + ($d['mon']*32) + $d['mday'];
- }
- function _MsDos_Time($Timestamp = false) {
- // convert a date-time timstamp into the MS-Dos format
- $d = ($Timestamp===false) ? getdate() : getdate($Timestamp);
- return ($d['hours']*2048) + ($d['minutes']*32) + intval($d['seconds']/2); // seconds are rounded to an even number in order to save 1 bit
- }
-
- function _MsDos_Debug($date, $time) {
- // Display the formated date and time. Just for debug purpose.
- // date end time are encoded on 16 bits (2 bytes) : date = yyyyyyymmmmddddd , time = hhhhhnnnnnssssss
- $y = ($date & 65024)/512 + 1980;
- $m = ($date & 480)/32;
- $d = ($date & 31);
- $h = ($time & 63488)/2048;
- $i = ($time & 1984)/32;
- $s = ($time & 31) * 2; // seconds have been rounded to an even number in order to save 1 bit
- return $y.'-'.str_pad($m,2,'0',STR_PAD_LEFT).'-'.str_pad($d,2,'0',STR_PAD_LEFT).' '.str_pad($h,2,'0',STR_PAD_LEFT).':'.str_pad($i,2,'0',STR_PAD_LEFT).':'.str_pad($s,2,'0',STR_PAD_LEFT);
- }
-
- function _DataOuputAddedFile($Idx, $PosLoc) {
-
- $Ref =& $this->AddInfo[$Idx];
- $this->_DataPrepare($Ref); // get data from external file if necessary
-
- // Other info
- $now = time();
- $date = $this->_MsDos_Date($now);
- $time = $this->_MsDos_Time($now);
- $len_n = strlen($Ref['name']);
- $purp = 2048 ; // purpose // +8 to indicates that there is an extended local header
-
- // Header for file in the data section
- $b = 'PK'.chr(03).chr(04).str_repeat(' ',26); // signature
- $this->_PutDec($b,20,4,2); //vers = 20
- $this->_PutDec($b,$purp,6,2); // purp
- $this->_PutDec($b,$Ref['meth'],8,2); // meth
- $this->_PutDec($b,$time,10,2); // time
- $this->_PutDec($b,$date,12,2); // date
- $this->_PutDec($b,$Ref['crc32'],14,4); // crc32
- $this->_PutDec($b,$Ref['len_c'],18,4); // l_data_c
- $this->_PutDec($b,$Ref['len_u'],22,4); // l_data_u
- $this->_PutDec($b,$len_n,26,2); // l_name
- $this->_PutDec($b,0,28,2); // l_fields
- $b .= $Ref['name']; // name
- $b .= ''; // fields
-
- // Output the data
- $this->OutputFromString($b.$Ref['data']);
- $OutputLen = strlen($b) + $Ref['len_c']; // new position of the cursor
- unset($Ref['data']); // save PHP memory
-
- // Information for file in the Central Directory
- $b = 'PK'.chr(01).chr(02).str_repeat(' ',42); // signature
- $this->_PutDec($b,20,4,2); // vers_used = 20
- $this->_PutDec($b,20,6,2); // vers_necess = 20
- $this->_PutDec($b,$purp,8,2); // purp
- $this->_PutDec($b,$Ref['meth'],10,2); // meth
- $this->_PutDec($b,$time,12,2); // time
- $this->_PutDec($b,$date,14,2); // date
- $this->_PutDec($b,$Ref['crc32'],16,4); // crc32
- $this->_PutDec($b,$Ref['len_c'],20,4); // l_data_c
- $this->_PutDec($b,$Ref['len_u'],24,4); // l_data_u
- $this->_PutDec($b,$len_n,28,2); // l_name
- $this->_PutDec($b,0,30,2); // l_fields
- $this->_PutDec($b,0,32,2); // l_comm
- $this->_PutDec($b,0,34,2); // disk_num
- $this->_PutDec($b,0,36,2); // int_file_att
- $this->_PutDec($b,0,38,4); // ext_file_att
- $this->_PutDec($b,$PosLoc,42,4); // p_loc
- $b .= $Ref['name']; // v_name
- $b .= ''; // v_fields
- $b .= ''; // v_comm
-
- $Ref['bin'] = $b;
-
- return $OutputLen;
-
- }
-
- function _DataCreateNewRef($Data, $DataType, $Compress, $Diff, $NameOrIdx) {
-
- if (is_array($Compress)) {
- $result = 2;
- $meth = $Compress['meth'];
- $len_u = $Compress['len_u'];
- $crc32 = $Compress['crc32'];
- $Compress = false;
- } elseif ($Compress and ($this->Meth8Ok)) {
- $result = 1;
- $meth = 8;
- $len_u = false; // means unknown
- $crc32 = false;
- } else {
- $result = ($Compress) ? -1 : 0;
- $meth = 0;
- $len_u = false;
- $crc32 = false;
- $Compress = false;
- }
-
- if ($DataType==TBSZIP_STRING) {
- $path = false;
- if ($Compress) {
- // we compress now in order to save PHP memory
- $len_u = strlen($Data);
- $crc32 = crc32($Data);
- $Data = gzdeflate($Data);
- $len_c = strlen($Data);
- } else {
- $len_c = strlen($Data);
- if ($len_u===false) {
- $len_u = $len_c;
- $crc32 = crc32($Data);
- }
- }
- } else {
- $path = $Data;
- $Data = false;
- if (file_exists($path)) {
- $fz = filesize($path);
- if ($len_u===false) $len_u = $fz;
- $len_c = ($Compress) ? false : $fz;
- } else {
- return $this->RaiseError("Cannot add the file '".$path."' because it is not found.");
- }
- }
-
- // at this step $Data and $crc32 can be false only in case of external file, and $len_c is false only in case of external file to compress
- return array('data'=>$Data, 'path'=>$path, 'meth'=>$meth, 'len_u'=>$len_u, 'len_c'=>$len_c, 'crc32'=>$crc32, 'diff'=>$Diff, 'res'=>$result);
-
- }
-
- function _DataPrepare(&$Ref) {
- // returns the real size of data
- if ($Ref['path']!==false) {
- $Ref['data'] = file_get_contents($Ref['path']);
- if ($Ref['crc32']===false) $Ref['crc32'] = crc32($Ref['data']);
- if ($Ref['len_c']===false) {
- // means the data must be compressed
- $Ref['data'] = gzdeflate($Ref['data']);
- $Ref['len_c'] = strlen($Ref['data']);
- }
- }
- }
-
- function _EstimateNewArchSize($Optim=true) {
- // Return the size of the new archive, or false if it cannot be calculated (because of external file that must be compressed before to be insered)
-
- if ($this->ArchIsNew) {
- $Len = strlen($this->CdInfo['bin']);
- } else {
- $Len = filesize($this->ArchFile);
- }
-
- // files to replace or delete
- foreach ($this->ReplByPos as $i) {
- $Ref =& $this->ReplInfo[$i];
- if ($Ref===false) {
- // file to delete
- $Info =& $this->CdFileLst[$i];
- if (!isset($this->VisFileLst[$i])) {
- if ($Optim) return false; // if $Optimization is set to true, then we d'ont rewind to read information
- $this->_MoveTo($Info['p_loc']);
- $this->_ReadFile($i, false);
- }
- $Vis =& $this->VisFileLst[$i];
- $Len += -strlen($Vis['bin']) -strlen($Info['bin']) - $Info['l_data_c'];
- if (isset($Vis['desc_bin'])) $Len += -strlen($Vis['desc_bin']);
- } elseif ($Ref['len_c']===false) {
- return false; // information not yet known
- } else {
- // file to replace
- $Len += $Ref['len_c'] + $Ref['diff'];
- }
- }
-
- // files to add
- $i_lst = array_keys($this->AddInfo);
- foreach ($i_lst as $i) {
- $Ref =& $this->AddInfo[$i];
- if ($Ref['len_c']===false) {
- return false; // information not yet known
- } else {
- $Len += $Ref['len_c'] + $Ref['diff'];
- }
- }
-
- return $Len;
-
- }
-
-}
\ No newline at end of file
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 000000000..6d59e5233
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,11 @@
+parameters:
+ ignoreErrors:
+ -
+ message: "#^Variable \\$sortField in empty\\(\\) always exists and is not falsy\\.$#"
+ count: 2
+ path: resources/epub-loader/CalibreDbLoader.class.php
+
+ -
+ message: "#^Call to an undefined method DOMNode\\:\\:attr\\(\\)\\.$#"
+ count: 1
+ path: resources/php-epub-meta/lib/EPub.php
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 000000000..4cef5b9c2
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,12 @@
+includes:
+ - phpstan-baseline.neon
+
+parameters:
+ level: 5
+ paths:
+ - .
+ bootstrapFiles:
+ - vendor/autoload.php
+ excludePaths:
+ - vendor/*
+ - test/Sauce.php
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 000000000..e901cad13
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,26 @@
+
+
+
+
+ ./
+
+
+ ./resources
+ ./test
+ ./saucetest
+ ./vendor
+ config.php
+ config_default.php
+
+
+
+
+
+
+
+
+
+ ./test/
+
+
+
diff --git a/resources/dot-php/.hgtags b/resources/dot-php/.hgtags
new file mode 100644
index 000000000..a24748477
--- /dev/null
+++ b/resources/dot-php/.hgtags
@@ -0,0 +1 @@
+9a405bbdd1a9eb1ba72eecfc67115d3bd3efdcbb 1.0.0
diff --git a/resources/dot-php/LICENSE b/resources/dot-php/LICENSE
new file mode 100644
index 000000000..52df22e70
--- /dev/null
+++ b/resources/dot-php/LICENSE
@@ -0,0 +1,339 @@
+GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ PHP rendering engine for doT.js (The fastest + concise javascript template engine for nodejs and browsers)
+ Copyright (C) 2013 Sébastien Lucas
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ {signature of Ty Coon}, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/resources/dot-php/README.md b/resources/dot-php/README.md
new file mode 100644
index 000000000..a31fe87b9
--- /dev/null
+++ b/resources/dot-php/README.md
@@ -0,0 +1,41 @@
+doT-php
+=======
+
+PHP rendering engine for [doT.js (The fastest + concise javascript template engine for nodejs and browsers)](https://github.com/olado/doT).
+
+
+How to use it
+-------------
+
+```php
+// Load the library
+require_once('resources/doT-php/doT.php');
+
+// Load the template
+$page = file_get_contents('templates/page.html');
+
+// instanciate the object
+$template = new doT();
+
+// Compile your templace in a PHP function ($dot)
+$dot = $template->template($page);
+
+// the data is simple PHP array
+$data = array('title' => 'My custom title');
+
+// Write the HTML
+echo $dot($data);
+```
+
+
+Warning
+-------
+
+It's far from complete. I needed it just to provide a server side rendering engine
+for another project ([COPS](https://github.com/seblucas/cops)).
+
+So the code provided works perfectly for the templates of COPS and was never tested
+elsewhere, doT's unit test were also never tested.
+
+That being said, You can fork, enhance it and send me some pull request, I'll
+happily merge them.
diff --git a/resources/dot-php/composer.json b/resources/dot-php/composer.json
new file mode 100644
index 000000000..30afa9262
--- /dev/null
+++ b/resources/dot-php/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "seblucas/dot-php",
+ "type": "library",
+ "description": "PHP rendering engine for doT.js (The fastest + concise javascript template engine for nodejs and browsers)",
+ "keywords": ["doT", "template", "engine", "rendering"],
+ "homepage": "https://github.com/seblucas/doT-php",
+ "license": "GPL-2.0-or-later",
+ "authors": [
+ {
+ "name": "Sébastien Lucas",
+ "email": "sebastien@slucas.fr",
+ "homepage": "http://www.slucas.fr/",
+ "role": "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "autoload": {
+ "classmap": ["doT.php"]
+ }
+}
diff --git a/resources/dot-php/doT.php b/resources/dot-php/doT.php
new file mode 100644
index 000000000..d38b59d6f
--- /dev/null
+++ b/resources/dot-php/doT.php
@@ -0,0 +1,114 @@
+
+ */
+
+
+class doT
+{
+ public $functionBody;
+ /**
+ * @var callable
+ */
+ private $functionCode;
+ public $def;
+
+ public function resolveDefs($block)
+ {
+ $me = $this;
+ return preg_replace_callback("/\{\{#([\s\S]+?)\}\}/", function ($m) use ($me) {
+ $d = $m[1];
+ $d = substr($d, 4);
+ if (!array_key_exists($d, $me->def)) {
+ return "";
+ }
+ if (preg_match("/\{\{#([\s\S]+?)\}\}/", $me->def [$d])) {
+ //return $me->resolveDefs($me->def [$d], $me->def);
+ return $me->resolveDefs($me->def [$d]);
+ } else {
+ return $me->def [$d];
+ }
+ }, $block);
+ }
+
+ public function handleDotNotation($string)
+ {
+ $out = preg_replace("/(\w+)\.(.*?)([\s,\)])/", "\$$1[\"$2\"]$3", $string);
+ $out = preg_replace("/(\w+)\.([\w\.]*?)$/", "\$$1[\"$2\"] ", $out);
+ $out = preg_replace("/\./", '"]["', $out);
+
+ // Special hideous case : shouldn't be committed
+ $out = preg_replace("/^i /", ' $i ', $out);
+ return $out;
+ }
+
+ public function template($string, $def)
+ {
+ $me = $this;
+
+ $func = $string;
+
+ // deps
+ if (empty($def)) {
+ $func = preg_replace("/\{\{#([\s\S]+?)\}\}/", "", $func);
+ } else {
+ $this->def = $def;
+ $func = $this->resolveDefs($func);
+ }
+
+ // @todo this messes up serverside rendering for client-side javascript, e.g. in header template:
+ // handleDotNotation($m[1]) . " . '";
+ }, $func);
+ // Conditional
+ $func = preg_replace_callback("/\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/", function ($m) use ($me) {
+ $elsecase = $m[1];
+ $code = $m[2];
+ if ($elsecase) {
+ if ($code) {
+ return "';} else if (" . $me->handleDotNotation($code) . ") { $" . "out.='";
+ } else {
+ return "';} else { $" . "out.='";
+ }
+ } else {
+ if ($code) {
+ return "'; if (" . $me->handleDotNotation($code) . ") { $" . "out.='";
+ } else {
+ return "';} $" . "out.='";
+ }
+ }
+ }, $func);
+ // Iterate
+ $func = preg_replace_callback("/\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/", function ($m) use ($me) {
+ if (count($m) > 1) {
+ $iterate = $m[1];
+ $vname = $m[2];
+ $iname = $m[3];
+ $iterate = $me->handleDotNotation($iterate);
+ return "'; for (\$$iname = 0; \$$iname < count($iterate); \$$iname++) { \$$vname = $iterate [\$$iname]; \$out.='";
+ } else {
+ return "';} $" . "out.='";
+ }
+ }, $func);
+ $func = '$out = \'' . $func . '\'; return $out;';
+
+ $this->functionBody = $func;
+
+ //return @create_function('$it', $func);
+ return function ($it) use ($func) {
+ return eval($func);
+ };
+ }
+
+ public function execute($data)
+ {
+ return ($this->functionCode)($data);
+ }
+}
diff --git a/resources/monocle/README.md b/resources/monocle/README.md
new file mode 100644
index 000000000..34a3771d6
--- /dev/null
+++ b/resources/monocle/README.md
@@ -0,0 +1,150 @@
+# Monocle
+
+A silky, tactile browser-based ebook reader.
+
+Initial development by Joseph Pearson of Inventive Labs. Released under the
+MIT license.
+
+____________________________________________________________________________
+THIS PROJECT IS NOW OVER EIGHT YEARS OLD.
+IT HAS NOT BEEN ACTIVELY MAINTAINED SINCE 2015.
+You are welcome to explore and learn from the code, but it is no longer the
+best approach for modern browsers and devices, and it is not recommended for
+new projects or production applications.
+____________________________________________________________________________
+
+
+## Getting Monocle
+
+There's a few different ways to get Monocle. The easiest way to explore
+it is from the test site, which is always running the latest `master`:
+
+http://test.monoclejs.com/test
+
+To grab the code for your own use, see:
+
+https://github.com/joseph/Monocle/wiki/Getting-Monocle-running
+
+The scripts and stylesheets are separated into:
+
+* `monocore` - the essential Monocle functionality
+* `monoctrl` - the optional basic controls for page numbers, font-sizing, etc
+
+
+## Integrating Monocle
+
+Here's the simplest thing that could possibly work.
+
+
+
+
+
+
+
+
+
+
+
` elements, but the elements you actually want to move within the DOM are the `
` (each `
`'s parent):
+
+ $('td').sortElements(myComparator, function(){
+ // Return a reference to the desired element:
+ return this.parentNode;
+ });
+
+See more info here: [http://james.padolsey.com/javascript/sorting-elements-with-jquery/](http://james.padolsey.com/javascript/sorting-elements-with-jquery/).
\ No newline at end of file
diff --git a/js/jquery.sortElements.js b/resources/simonpioli/sortelements/jquery.sortElements.js
similarity index 65%
rename from js/jquery.sortElements.js
rename to resources/simonpioli/sortelements/jquery.sortElements.js
index bf1902d10..1cd6bf879 100644
--- a/js/jquery.sortElements.js
+++ b/resources/simonpioli/sortelements/jquery.sortElements.js
@@ -1,69 +1,69 @@
-/**
- * jQuery.fn.sortElements
- * --------------
- * @author James Padolsey (http://james.padolsey.com)
- * @version 0.11
- * @updated 18-MAR-2010
- * --------------
- * @param Function comparator:
- * Exactly the same behaviour as [1,2,3].sort(comparator)
- *
- * @param Function getSortable
- * A function that should return the element that is
- * to be sorted. The comparator will run on the
- * current collection, but you may want the actual
- * resulting sort to occur on a parent or another
- * associated element.
- *
- * E.g. $('td').sortElements(comparator, function(){
- * return this.parentNode;
- * })
- *
- * The
's parent (
) will be sorted instead
- * of the
itself.
- */
-jQuery.fn.sortElements = (function(){
-
- var sort = [].sort;
-
- return function(comparator, getSortable) {
-
- getSortable = getSortable || function(){return this;};
-
- var placements = this.map(function(){
-
- var sortElement = getSortable.call(this),
- parentNode = sortElement.parentNode,
-
- // Since the element itself will change position, we have
- // to have some way of storing it's original position in
- // the DOM. The easiest way is to have a 'flag' node:
- nextSibling = parentNode.insertBefore(
- document.createTextNode(''),
- sortElement.nextSibling
- );
-
- return function() {
-
- if (parentNode === this) {
- throw new Error(
- "You can't sort elements if any one is a descendant of another."
- );
- }
-
- // Insert before flag:
- parentNode.insertBefore(this, nextSibling);
- // Remove flag:
- parentNode.removeChild(nextSibling);
-
- };
-
- });
-
- return sort.call(this, comparator).each(function(i){
- placements[i].call(getSortable.call(this));
- });
-
- };
-
+/**
+* jQuery.fn.sortElements
+* --------------
+* @author James Padolsey (http://james.padolsey.com)
+* @version 0.11
+* @updated 18-MAR-2010
+* --------------
+* @param Function comparator:
+* Exactly the same behaviour as [1,2,3].sort(comparator)
+*
+* @param Function getSortable
+* A function that should return the element that is
+* to be sorted. The comparator will run on the
+* current collection, but you may want the actual
+* resulting sort to occur on a parent or another
+* associated element.
+*
+* E.g. $('td').sortElements(comparator, function(){
+* return this.parentNode;
+* })
+*
+* The
diff --git a/templates/bootstrap/page.html b/templates/bootstrap/page.html
new file mode 100644
index 000000000..88139c888
--- /dev/null
+++ b/templates/bootstrap/page.html
@@ -0,0 +1,3 @@
+{{#def.header}}
+{{#def.main}}
+{{#def.footer}}
diff --git a/templates/bootstrap/scripts/cops.js b/templates/bootstrap/scripts/cops.js
new file mode 100644
index 000000000..3383895c0
--- /dev/null
+++ b/templates/bootstrap/scripts/cops.js
@@ -0,0 +1,4 @@
+function postRefresh()
+{
+ $('[data-toggle="tooltip"]').tooltip();
+}
\ No newline at end of file
diff --git a/templates/bootstrap/styles/style-base.css b/templates/bootstrap/styles/style-base.css
new file mode 100644
index 000000000..4823668e1
--- /dev/null
+++ b/templates/bootstrap/styles/style-base.css
@@ -0,0 +1,3 @@
+.panel-body { padding: 5px; }
+
+.bottomright {position:absolute; bottom:0; margin-bottom:25px; right: 20px;}
\ No newline at end of file
diff --git a/templates/bootstrap/styles/style-default.css b/templates/bootstrap/styles/style-default.css
new file mode 100644
index 000000000..4823668e1
--- /dev/null
+++ b/templates/bootstrap/styles/style-default.css
@@ -0,0 +1,3 @@
+.panel-body { padding: 5px; }
+
+.bottomright {position:absolute; bottom:0; margin-bottom:25px; right: 20px;}
\ No newline at end of file
diff --git a/templates/bootstrap/suggestion.html b/templates/bootstrap/suggestion.html
new file mode 100644
index 000000000..1107dadee
--- /dev/null
+++ b/templates/bootstrap/suggestion.html
@@ -0,0 +1 @@
+
{{=it.title}}
\ No newline at end of file
diff --git a/templates/bootstrap2/bookdetail.html b/templates/bootstrap2/bookdetail.html
new file mode 100644
index 000000000..b45cfbd20
--- /dev/null
+++ b/templates/bootstrap2/bookdetail.html
@@ -0,0 +1,91 @@
+
\ No newline at end of file
diff --git a/templates/default/webfonts/fa-solid-900.eot b/templates/default/webfonts/fa-solid-900.eot
new file mode 100644
index 000000000..a32dc8aeb
Binary files /dev/null and b/templates/default/webfonts/fa-solid-900.eot differ
diff --git a/templates/default/webfonts/fa-solid-900.svg b/templates/default/webfonts/fa-solid-900.svg
new file mode 100644
index 000000000..94bb8f27b
--- /dev/null
+++ b/templates/default/webfonts/fa-solid-900.svg
@@ -0,0 +1,1896 @@
+
+
+
+
diff --git a/templates/default/webfonts/fa-solid-900.ttf b/templates/default/webfonts/fa-solid-900.ttf
new file mode 100644
index 000000000..4e518ad49
Binary files /dev/null and b/templates/default/webfonts/fa-solid-900.ttf differ
diff --git a/templates/default/webfonts/fa-solid-900.woff b/templates/default/webfonts/fa-solid-900.woff
new file mode 100644
index 000000000..277d8cebc
Binary files /dev/null and b/templates/default/webfonts/fa-solid-900.woff differ
diff --git a/templates/default/webfonts/fa-solid-900.woff2 b/templates/default/webfonts/fa-solid-900.woff2
new file mode 100644
index 000000000..69bd4299c
Binary files /dev/null and b/templates/default/webfonts/fa-solid-900.woff2 differ
diff --git a/test/BaseWithCustomColumns/metadata.db b/test/BaseWithCustomColumns/metadata.db
new file mode 100644
index 000000000..ce359f91c
Binary files /dev/null and b/test/BaseWithCustomColumns/metadata.db differ
diff --git a/test/BaseWithOneBook/metadata.db b/test/BaseWithOneBook/metadata.db
new file mode 100644
index 000000000..a1f24b374
Binary files /dev/null and b/test/BaseWithOneBook/metadata.db differ
diff --git a/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/Alice's Adventures in Wonderland - Lewis Carroll.epub b/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/Alice's Adventures in Wonderland - Lewis Carroll.epub
new file mode 100644
index 000000000..376ab504e
Binary files /dev/null and b/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/Alice's Adventures in Wonderland - Lewis Carroll.epub differ
diff --git a/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/cover.jpg b/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/cover.jpg
new file mode 100644
index 000000000..da5d852d9
Binary files /dev/null and b/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/cover.jpg differ
diff --git a/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/metadata.opf b/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/metadata.opf
new file mode 100644
index 000000000..9d3085ce0
--- /dev/null
+++ b/test/BaseWithSomeBooks/Lewis Carroll/Alice's Adventures in Wonderland (17)/metadata.opf
@@ -0,0 +1,32 @@
+
+
+
+ 15
+ cdcb34f1-d554-409b-8620-9a4a56c58c68
+ Alice's Adventures in Wonderland
+ Lewis Carroll
+ calibre (1.3.0) [http://calibre-ebook.com]
+ 1897-01-13T23:00:00+00:00
+ Alice's Adventures in Wonderland (1865) is a novel written by English author Charles Lutwidge Dodgson, better known under the pseudonym Lewis Carroll. It tells the story of a girl named Alice who falls down a rabbit-hole into a fantasy world populated by peculiar and anthropomorphic creatures.
+The tale is filled with allusions to Dodgson's friends (and enemies), and to the lessons that British schoolchildren were expected to memorize. The tale plays with logic in ways that have made the story of lasting popularity with adults as well as children. It is considered to be one of the most characteristic examples of the genre of literary nonsense, and its narrative course and structure has been enormously influential, mainly in the fantasy genre.
+ Feedbooks
+ urn|uuid|bca234de-5f3c-11e1-837d-001cc0a62c0b
+ http|//www.feedbooks.com/book/22
+ eng
+ Fantasy
+ Juvenile
+ Fiction
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/BaseWithSomeBooks/metadata.db b/test/BaseWithSomeBooks/metadata.db
new file mode 100644
index 000000000..cc869580e
Binary files /dev/null and b/test/BaseWithSomeBooks/metadata.db differ
diff --git a/test/EpubFsTest.php b/test/EpubFsTest.php
new file mode 100644
index 000000000..4b1a98548
--- /dev/null
+++ b/test/EpubFsTest.php
@@ -0,0 +1,111 @@
+
+ */
+
+require(dirname(__FILE__) . "/../epubfs.php");
+require(dirname(__FILE__) . "/config_test.php");
+use PHPUnit\Framework\TestCase;
+
+class EpubFsTest extends TestCase
+{
+ private static $book;
+ private static $add;
+
+
+ public static function setUpBeforeClass(): void
+ {
+ $idData = 20;
+ self::$add = "data=$idData&";
+ $myBook = Book::getBookByDataId($idData);
+
+ self::$book = new EPub($myBook->getFilePath("EPUB", $idData));
+ self::$book->initSpineComponent();
+ }
+
+ public function testUrlImage()
+ {
+ $data = getComponentContent(self::$book, "cover.xml", self::$add);
+
+ $src = "";
+ if (preg_match("/src\=\'(.*?)\'/", $data, $matches)) {
+ $src = $matches [1];
+ }
+ $this->assertEquals('epubfs.php?data=20&comp=images~SLASH~cover.png', $src);
+ }
+
+ public function testUrlHref()
+ {
+ $data = getComponentContent(self::$book, "title.xml", self::$add);
+
+ $src = "";
+ if (preg_match("/src\=\'(.*?)\'/", $data, $matches)) {
+ $src = $matches [1];
+ }
+ $this->assertEquals('epubfs.php?data=20&comp=images~SLASH~logo~DASH~feedbooks~DASH~tiny.png', $src);
+
+ $href = "";
+ if (preg_match("/href\=\'(.*?)\'/", $data, $matches)) {
+ $href = $matches [1];
+ }
+ $this->assertEquals('epubfs.php?data=20&comp=css~SLASH~title.css', $href);
+ }
+
+ public function testImportCss()
+ {
+ $data = getComponentContent(self::$book, "css~SLASH~title.css", self::$add);
+
+ $import = "";
+ if (preg_match("/import \'(.*?)\'/", $data, $matches)) {
+ $import = $matches [1];
+ }
+ $this->assertEquals('epubfs.php?data=20&comp=css~SLASH~page.css', $import);
+ }
+
+ public function testUrlInCss()
+ {
+ $data = getComponentContent(self::$book, "css~SLASH~main.css", self::$add);
+
+ $src = "";
+ if (preg_match("/url\s*\(\'(.*?)\'\)/", $data, $matches)) {
+ $src = $matches [1];
+ }
+ $this->assertEquals('epubfs.php?data=20&comp=fonts~SLASH~times.ttf', $src);
+ }
+
+ public function testDirectLink()
+ {
+ $data = getComponentContent(self::$book, "main10.xml", self::$add);
+
+ $src = "";
+ if (preg_match("/href\='(.*?)' title=\"Direct Link\"/", $data, $matches)) {
+ $src = $matches [1];
+ }
+ $this->assertEquals('epubfs.php?data=20&comp=main2.xml', $src);
+ }
+
+ public function testDirectLinkWithAnchor()
+ {
+ $data = getComponentContent(self::$book, "main10.xml", self::$add);
+
+ $src = "";
+ if (preg_match("/href\='(.*?)' title=\"Direct Link with anchor\"/", $data, $matches)) {
+ $src = $matches [1];
+ }
+ $this->assertEquals('epubfs.php?data=20&comp=main2.xml#anchor', $src);
+ }
+
+ public function testAnchorOnly()
+ {
+ $data = getComponentContent(self::$book, "main10.xml", self::$add);
+
+ $src = "";
+ if (preg_match("/href\='(.*?)' title=\"Link to anchor\"/", $data, $matches)) {
+ $src = $matches [1];
+ }
+ $this->assertEquals('#anchor', $src);
+ }
+}
diff --git a/test/OPDSTest.php b/test/OPDSTest.php
new file mode 100644
index 000000000..b8eb509fb
--- /dev/null
+++ b/test/OPDSTest.php
@@ -0,0 +1,291 @@
+
+ */
+
+require_once(dirname(__FILE__) . "/config_test.php");
+use PHPUnit\Framework\TestCase;
+
+define("OPDS_RELAX_NG", dirname(__FILE__) . "/opds-relax-ng/opds_catalog_1_1.rng");
+define("OPENSEARCHDESCRIPTION_RELAX_NG", dirname(__FILE__) . "/opds-relax-ng/opensearchdescription.rng");
+define("JING_JAR", dirname(__FILE__) . "/jing.jar");
+define("OPDSVALIDATOR_JAR", dirname(__FILE__) . "/OPDSValidator.jar");
+define("TEST_FEED", dirname(__FILE__) . "/text.atom");
+
+class OpdsTest extends TestCase
+{
+ public static function setUpBeforeClass(): void
+ {
+ global $config;
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ if (!file_exists(TEST_FEED)) {
+ return;
+ }
+ unlink(TEST_FEED);
+ }
+
+ public function jingValidateSchema($feed, $relax = OPDS_RELAX_NG)
+ {
+ $path = "";
+ $code = null;
+ $res = system($path . 'java -jar "' . JING_JAR . '" "' . $relax . '" "' . $feed . '"', $code);
+ if ($res != '') {
+ echo 'RelaxNG validation error: '.$res;
+ return false;
+ //} elseif (isset($code) && $code > 0) {
+ // echo 'Return code: '.strval($code);
+ // return false;
+ } else {
+ return true;
+ }
+ }
+
+ public function opdsValidator($feed)
+ {
+ $oldcwd = getcwd(); // Save the old working directory
+ chdir("test");
+ $path = "";
+ $res = system($path . 'java -jar "' . OPDSVALIDATOR_JAR . '" "' . $feed . '"');
+ chdir($oldcwd);
+ if ($res != '') {
+ echo 'OPDS validation error: '.$res;
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public function opdsCompleteValidation($feed)
+ {
+ return $this->jingValidateSchema($feed) && $this->opdsValidator($feed);
+ }
+
+ public function testPageIndex()
+ {
+ global $config;
+ $page = Base::PAGE_INDEX;
+ $query = null;
+ $qid = null;
+ $n = "1";
+
+ $_SERVER['QUERY_STRING'] = "";
+ $config['cops_subtitle_default'] = "My subtitle";
+
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertTrue($this->jingValidateSchema(TEST_FEED));
+ $this->AssertTrue($this->opdsCompleteValidation(TEST_FEED));
+
+ $_SERVER ["HTTP_USER_AGENT"] = "XXX";
+ $config['cops_generate_invalid_opds_stream'] = "1";
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertFalse($this->jingValidateSchema(TEST_FEED));
+ $this->AssertFalse($this->opdsValidator(TEST_FEED));
+
+ unset($_SERVER['QUERY_STRING']);
+ unset($_SERVER['HTTP_USER_AGENT']);
+ $config['cops_generate_invalid_opds_stream'] = "0";
+ }
+
+ /**
+ * @dataProvider providerPage
+ */
+ public function testMostPages($page, $query)
+ {
+ $qid = null;
+ $n = "1";
+ $_SERVER['QUERY_STRING'] = "?page={$page}";
+ if (!empty($query)) {
+ $_SERVER['QUERY_STRING'] .= "&query={$query}";
+ }
+ $_SERVER['REQUEST_URI'] = "feed.php" . $_SERVER['QUERY_STRING'];
+
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertTrue($this->opdsCompleteValidation(TEST_FEED));
+
+ unset($_SERVER['QUERY_STRING']);
+ unset($_SERVER['REQUEST_URI']);
+ }
+
+ public function providerPage()
+ {
+ return [
+ [Base::PAGE_OPENSEARCH, "car"],
+ [Base::PAGE_ALL_AUTHORS, null],
+ [Base::PAGE_ALL_SERIES, null],
+ [Base::PAGE_ALL_TAGS, null],
+ [Base::PAGE_ALL_PUBLISHERS, null],
+ [Base::PAGE_ALL_LANGUAGES, null],
+ [Base::PAGE_ALL_RECENT_BOOKS, null],
+ [Base::PAGE_ALL_BOOKS, null],
+ ];
+ }
+
+ public function testPageIndexMultipleDatabase()
+ {
+ global $config;
+ $config['calibre_directory'] = ["Some books" => dirname(__FILE__) . "/BaseWithSomeBooks/",
+ "One book" => dirname(__FILE__) . "/BaseWithOneBook/"];
+ Base::clearDb();
+ $page = Base::PAGE_INDEX;
+ $query = null;
+ $qid = "1";
+ $n = "1";
+ $_SERVER['QUERY_STRING'] = "";
+
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertTrue($this->opdsCompleteValidation(TEST_FEED));
+
+ unset($_SERVER['QUERY_STRING']);
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ }
+
+ public function testOpenSearchDescription()
+ {
+ $_SERVER['QUERY_STRING'] = "";
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->getOpenSearch());
+ $this->AssertTrue($this->jingValidateSchema(TEST_FEED, OPENSEARCHDESCRIPTION_RELAX_NG));
+
+ unset($_SERVER['QUERY_STRING']);
+ }
+
+ public function testPageAuthorMultipleDatabase()
+ {
+ global $config;
+ $config['calibre_directory'] = ["Some books" => dirname(__FILE__) . "/BaseWithSomeBooks/",
+ "One book" => dirname(__FILE__) . "/BaseWithOneBook/"];
+ Base::clearDb();
+ $page = Base::PAGE_AUTHOR_DETAIL;
+ $query = null;
+ $qid = "1";
+ $n = "1";
+ $_SERVER['QUERY_STRING'] = "page=" . Base::PAGE_AUTHOR_DETAIL . "&id=1";
+ $_GET ["db"] = "0";
+
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertTrue($this->opdsCompleteValidation(TEST_FEED));
+
+ unset($_SERVER['QUERY_STRING']);
+ unset($_GET['db']);
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ }
+
+ public function testPageAuthorsDetail()
+ {
+ global $config;
+ $page = Base::PAGE_AUTHOR_DETAIL;
+ $query = null;
+ $qid = "1";
+ $n = "1";
+ $_SERVER['QUERY_STRING'] = "page=" . Base::PAGE_AUTHOR_DETAIL . "&id=1&n=1";
+
+ $config['cops_max_item_per_page'] = 2;
+
+ // First page
+
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertTrue($this->opdsCompleteValidation(TEST_FEED));
+
+ // Second page
+
+ $n = 2;
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertTrue($this->opdsCompleteValidation(TEST_FEED));
+
+ unset($_SERVER['QUERY_STRING']);
+ // No pagination
+ $config['cops_max_item_per_page'] = -1;
+ }
+
+ public function testPageAuthorsDetail_WithFacets()
+ {
+ global $config;
+ $page = Base::PAGE_AUTHOR_DETAIL;
+ $query = null;
+ $qid = "1";
+ $n = "1";
+ $_SERVER['QUERY_STRING'] = "page=" . Base::PAGE_AUTHOR_DETAIL . "&id=1&n=1";
+ $_GET["tag"] = "Short Stories";
+
+ $config['cops_books_filter'] = ["Only Short Stories" => "Short Stories", "No Short Stories" => "!Short Stories"];
+
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertTrue($this->opdsCompleteValidation(TEST_FEED));
+
+ unset($_SERVER['QUERY_STRING']);
+ unset($_GET['tag']);
+ $config['cops_books_filter'] = [];
+ }
+
+ public function testPageAuthorsDetail_WithoutAnyId()
+ {
+ global $config;
+ $page = Base::PAGE_AUTHOR_DETAIL;
+ $query = null;
+ $qid = "1";
+ $n = "1";
+ $_SERVER['QUERY_STRING'] = "page=" . Base::PAGE_AUTHOR_DETAIL . "&id=1&n=1";
+ $_SERVER['REQUEST_URI'] = "index.php?XXXX";
+
+
+ $currentPage = Page::getPage($page, $qid, $query, $n);
+ $currentPage->InitializeContent();
+ $currentPage->idPage = null;
+
+ $OPDSRender = new OPDSRenderer();
+
+ file_put_contents(TEST_FEED, $OPDSRender->render($currentPage));
+ $this->AssertTrue($this->opdsCompleteValidation(TEST_FEED));
+
+ unset($_SERVER['QUERY_STRING']);
+ unset($_SERVER['REQUEST_URI']);
+ }
+}
diff --git a/test/OPDSValidator.jar b/test/OPDSValidator.jar
new file mode 100644
index 000000000..3fe7b3390
Binary files /dev/null and b/test/OPDSValidator.jar differ
diff --git a/test/Sauce.php b/test/Sauce.php
new file mode 100644
index 000000000..447a59b23
--- /dev/null
+++ b/test/Sauce.php
@@ -0,0 +1,225 @@
+ 'firefox',
+ 'desiredCapabilities' => [
+ 'version' => '28',
+ 'platform' => 'Windows 8.1',
+ ],
+ ],
+ // run IE11 on Windows 8 on Sauce
+ [
+ 'browserName' => 'internet explorer',
+ 'desiredCapabilities' => [
+ 'version' => '11',
+ 'platform' => 'Windows 8.1',
+ ],
+ ],
+ // run Safari 7 on Maverick on Sauce
+ [
+ 'browserName' => 'safari',
+ 'desiredCapabilities' => [
+ 'version' => '7',
+ 'platform' => 'OS X 10.9',
+ ],
+ ],
+ // run Mobile Safari on iOS
+ [
+ 'browserName' => 'iphone',
+ 'desiredCapabilities' => [
+ 'app' => 'safari',
+ 'device' => 'iPhone 6',
+ 'version' => '9.2',
+ 'platform' => 'OS X 10.10',
+ ],
+ ],
+ // run Mobile Browser on Android
+ [
+ 'browserName' => 'Android',
+ 'desiredCapabilities' => [
+ 'version' => '5.1',
+ 'platform' => 'Linux',
+ ],
+ ],
+ // run Chrome on Linux on Sauce
+ [
+ 'browserName' => 'chrome',
+ 'desiredCapabilities' => [
+ 'version' => '33',
+ 'platform' => 'Linux',
+ ],
+ ],
+
+
+ // run Chrome locally
+ //array(
+ //'browserName' => 'chrome',
+ //'local' => true,
+ //'sessionStrategy' => 'shared'
+ //)
+ ];
+
+ public function setUp()
+ {
+ if (isset($_SERVER["TRAVIS_JOB_NUMBER"])) {
+ $caps = $this->getDesiredCapabilities();
+ $caps['build'] = getenv("TRAVIS_JOB_NUMBER");
+ $caps['tunnel-identifier'] = getenv("TRAVIS_JOB_NUMBER");
+ $caps['idle-timeout'] = "180";
+ $this->setDesiredCapabilities($caps);
+ }
+ parent::setUp();
+ }
+
+ public function setUpPage()
+ {
+ if (isset($_SERVER["TRAVIS_JOB_NUMBER"])) {
+ $this->url('http://127.0.0.1:8080/index.php');
+ } else {
+ $this->url('http://cops-demo.slucas.fr/index.php');
+ }
+
+ $driver = $this;
+ $title_test = function ($value) use ($driver) {
+ $text = $driver->byXPath('//h1')->text();
+ return $text == $value;
+ };
+
+ $this->spinAssert("Home Title", $title_test, [ "COPS DEMO" ]);
+ }
+
+ public function string_to_ascii($string)
+ {
+ $ascii = null;
+
+ for ($i = 0; $i < strlen($string); $i++) {
+ $ascii += ord($string[$i]);
+ }
+
+ return mb_detect_encoding($string) . "X" . $ascii;
+ }
+
+ // public function testTitle()
+ // {
+ // $driver = $this;
+ // $title_test = function($value) use ($driver) {
+ // $text = $driver->byXPath('//h1')->text ();
+ // return $text == $value;
+ // };
+
+ // $author = $this->byXPath ('//h2[contains(text(), "Authors")]');
+ // $author->click ();
+
+ // $this->spinAssert("Author Title", $title_test, [ "AUTHORS" ]);
+ // }
+
+ // public function testCog()
+ // {
+ // $cog = $this->byId ("searchImage");
+
+ // $search = $this->byName ("query");
+ // $this->assertFalse ($search->displayed ());
+
+ // $cog->click ();
+
+ // $search = $this->byName ("query");
+ // $this->assertTrue ($search->displayed ());
+ // }
+
+ public function testFilter()
+ {
+ $driver = $this;
+ $title_test = function ($value) use ($driver) {
+ $text = $driver->byXPath('//h1')->text();
+ return $text == $value;
+ };
+
+ $element_present = function ($using, $id) use ($driver) {
+ $elements = $driver->elements($driver->using($using)->value($id));
+ return count($elements) == 1;
+ };
+
+ // Click on the wrench to enable tag filtering
+ $this->spinWait("", $element_present, [ "class name", 'icon-wrench']);
+ $this->byClassName("icon-wrench")->click();
+
+ $this->spinWait("", $element_present, [ "id", "html_tag_filter"]);
+ $this->byId("html_tag_filter")->click();
+
+ // Go back to home screen
+ $this->byClassName("icon-home")->click();
+
+ $this->spinAssert("Home Title", $title_test, [ "COPS DEMO" ]);
+
+ // Go on the recent page
+ $author = $this->byXPath('//h2[contains(text(), "Recent")]');
+ $author->click();
+
+ $this->spinAssert("Recent book title", $title_test, [ "RECENT ADDITIONS" ]);
+
+ // Click on the cog to show tag filters
+ $cog = $this->byId("searchImage");
+ $cog->click();
+ sleep(1);
+ // Filter on War & Military
+ $filter = $this->byXPath('//li[contains(text(), "War")]');
+ $filter->click();
+ sleep(1);
+ // Only one book
+ $filtered = $this->elements($this->using('css selector')->value('*[class="books"]'));
+ $this->assertEquals(1, count($filtered));
+ $filter->click();
+ sleep(1);
+ // 13 book
+ $filtered = $this->elements($this->using('css selector')->value('*[class="books"]'));
+ $this->assertEquals(14, count($filtered));
+ }
+
+ public function normalSearch($src, $out)
+ {
+ $driver = $this;
+ $title_test = function ($value) use ($driver) {
+ $text = $driver->byXPath('//h1')->text();
+ return $text == $value;
+ };
+
+ // Click on the cog to show the search
+ $cog = $this->byId("searchImage");
+ $cog->click();
+ //sleep (1);
+
+ // Focus the input and type
+ $this->waitUntil(function () {
+ if ($this->byName("query")) {
+ return true;
+ }
+ return null;
+ }, 1000);
+ $queryInput = $this->byName("query");
+ $queryInput->click();
+ $queryInput->value($src);
+ $queryInput->submit();
+
+ $this->spinAssert("Home Title", $title_test, [ "SEARCH RESULT FOR *" . $out . "*" ]);
+ }
+
+ public function testSearchWithoutAccentuatedCharacters()
+ {
+ $this->normalSearch("ali", "ALI");
+ }
+
+ public function testSearchWithAccentuatedCharacters()
+ {
+ if ($this->getBrowser() == "Android") {
+ $this->markTestIncomplete();
+ return;
+ }
+ $this->normalSearch("é", "É");
+ }
+}
diff --git a/test/baseTest.php b/test/baseTest.php
new file mode 100644
index 000000000..78b35a1e1
--- /dev/null
+++ b/test/baseTest.php
@@ -0,0 +1,264 @@
+
+ */
+
+require_once(dirname(__FILE__) . "/../base.php");
+require_once(dirname(__FILE__) . "/config_test.php");
+//require_once(dirname(__FILE__) . "/../base.php");
+use PHPUnit\Framework\TestCase;
+
+class BaseTest extends TestCase
+{
+ public static function setUpBeforeClass(): void
+ {
+ global $config;
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ }
+
+ public function testAddURLParameter()
+ {
+ $this->assertEquals("?db=0", addURLParameter("?", "db", "0"));
+ $this->assertEquals("?key=value&db=0", addURLParameter("?key=value", "db", "0"));
+ $this->assertEquals("?key=value&otherKey=&db=0", addURLParameter("?key=value&otherKey", "db", "0"));
+ }
+
+ /**
+ * FALSE is returned if the create_function failed (meaning there was a syntax error)
+ * @dataProvider providerTemplate
+ */
+ public function testServerSideRender($template)
+ {
+ $_COOKIE["template"] = $template;
+ $this->assertNull(serverSideRender(null));
+
+ unset($_COOKIE['template']);
+ }
+
+ /**
+ * The function for the head of the HTML catalog
+ * @dataProvider providerTemplate
+ */
+ public function testGenerateHeader($templateName)
+ {
+ $_SERVER["HTTP_USER_AGENT"] = "Firefox";
+ global $config;
+ $headcontent = file_get_contents(dirname(__FILE__) . '/../templates/' . $templateName . '/file.html');
+ $template = new doT();
+ $tpl = $template->template($headcontent, null);
+ $data = ["title" => $config['cops_title_default'],
+ "version" => VERSION,
+ "opds_url" => $config['cops_full_url'] . "feed.php",
+ "customHeader" => "",
+ "template" => $templateName,
+ "server_side_rendering" => useServerSideRendering(),
+ "current_css" => getCurrentCss(),
+ "favico" => $config['cops_icon'],
+ "getjson_url" => "getJSON.php?" . addURLParameter(getQueryString(), "complete", 1)];
+
+ $head = $tpl($data);
+ $this->assertStringContainsString("", $head);
+ $this->assertStringContainsString("", $head);
+ }
+
+ public function providerTemplate()
+ {
+ return [
+ ["bootstrap"],
+ ["default"],
+ ];
+ }
+
+ public function testLocalize()
+ {
+ $this->assertEquals("Authors", localize("authors.title"));
+
+ $this->assertEquals("unknow.key", localize("unknow.key"));
+ }
+
+ public function testLocalizeFr()
+ {
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = "fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3";
+ $this->assertEquals("Auteurs", localize("authors.title", -1, true));
+
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = "en";
+ localize("authors.title", -1, true);
+ }
+
+ public function testLocalizeUnknown()
+ {
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = "aa";
+ $this->assertEquals("Authors", localize("authors.title", -1, true));
+
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = "en";
+ localize("authors.title", -1, true);
+ }
+
+ /**
+ * @dataProvider providerGetLangAndTranslationFile
+ */
+ public function testGetLangAndTranslationFile($acceptLanguage, $result)
+ {
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $acceptLanguage;
+ [$lang, $lang_file] = GetLangAndTranslationFile();
+ $this->assertEquals($result, $lang);
+
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = "en";
+ localize("authors.title", -1, true);
+ }
+
+ public function providerGetLangAndTranslationFile()
+ {
+ return [
+ ["en", "en"],
+ ["fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3", "fr"],
+ ["fr-FR", "fr"],
+ ["pt,en-us;q=0.7,en;q=0.3", "en"],
+ ["pt-br,pt;q=0.8,en-us;q=0.5,en;q=0.3", "pt_BR"],
+ ["pt-pt,pt;q=0.8,en;q=0.5,en-us;q=0.3", "pt_PT"],
+ ["zl", "en"],
+ ];
+ }
+
+ /**
+ * @dataProvider providerGetAcceptLanguages
+ */
+ public function testGetAcceptLanguages($acceptLanguage, $result)
+ {
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $acceptLanguage;
+ $langs = array_keys(GetAcceptLanguages());
+ $this->assertEquals($result, $langs[0]);
+
+ $_SERVER['HTTP_ACCEPT_LANGUAGE'] = "en";
+ localize("authors.title", -1, true);
+ }
+
+ public function providerGetAcceptLanguages()
+ {
+ return [
+ ["en", "en"],
+ ["en-US", "en_US"],
+ ["fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3", "fr"], // French locale with Firefox
+ ["fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4", "fr_FR"], // French locale with Chrome
+ ["fr-FR", "fr_FR"], // French locale with IE11
+ ["pt-br,pt;q=0.8,en-us;q=0.5,en;q=0.3", "pt_BR"],
+ ["zl", "zl"],
+ ];
+ }
+
+ public function testBaseFunction()
+ {
+ global $config;
+
+ $this->assertFalse(Base::isMultipleDatabaseEnabled());
+ $this->assertEquals(["" => dirname(__FILE__) . "/BaseWithSomeBooks/"], Base::getDbList());
+
+ $config['calibre_directory'] = ["Some books" => dirname(__FILE__) . "/BaseWithSomeBooks/",
+ "One book" => dirname(__FILE__) . "/BaseWithOneBook/"];
+ Base::clearDb();
+
+ $this->assertTrue(Base::isMultipleDatabaseEnabled());
+ $this->assertEquals("Some books", Base::getDbName(0));
+ $this->assertEquals("One book", Base::getDbName(1));
+ $this->assertEquals($config['calibre_directory'], Base::getDbList());
+
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ }
+
+ public function testCheckDatabaseAvailability_1()
+ {
+ $this->assertTrue(Base::checkDatabaseAvailability());
+ }
+
+ public function testCheckDatabaseAvailability_2()
+ {
+ global $config;
+
+ $config['calibre_directory'] = ["Some books" => dirname(__FILE__) . "/BaseWithSomeBooks/",
+ "One book" => dirname(__FILE__) . "/BaseWithOneBook/"];
+ Base::clearDb();
+
+ $this->assertTrue(Base::checkDatabaseAvailability());
+
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage Database <1> not found.
+ */
+ public function testCheckDatabaseAvailability_Exception1()
+ {
+ global $config;
+
+ $config['calibre_directory'] = ["Some books" => dirname(__FILE__) . "/BaseWithSomeBooks/",
+ "One book" => dirname(__FILE__) . "/OneBook/"];
+ Base::clearDb();
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Database <1> not found.');
+
+ $this->assertTrue(Base::checkDatabaseAvailability());
+
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ }
+
+ /**
+ * @expectedException Exception
+ * @expectedExceptionMessage Database <0> not found.
+ */
+ public function testCheckDatabaseAvailability_Exception2()
+ {
+ global $config;
+
+ $config['calibre_directory'] = ["Some books" => dirname(__FILE__) . "/SomeBooks/",
+ "One book" => dirname(__FILE__) . "/BaseWithOneBook/"];
+ Base::clearDb();
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Database <0> not found.');
+
+ $this->assertTrue(Base::checkDatabaseAvailability());
+
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ }
+
+ /*
+ Test normalized utf8 string according to unicode.org output
+ more here :
+ http://unicode.org/cldr/utility/transform.jsp?a=Latin-ASCII&b=%C3%80%C3%81%C3%82%C3%83%C3%84%C3%85%C3%87%C3%88%C3%89%C3%8A%C3%8B%C3%8C%C3%8D%C3%8E%C3%8F%C5%92%C3%92%C3%93%C3%94%C3%95%C3%96%C3%99%C3%9A%C3%9B%C3%9C%C3%9D%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A7%C3%A8%C3%A9%C3%AA%C3%AB%C3%AC%C3%AD%C3%AE%C3%AF%C5%93%C3%B0%C3%B2%C3%B3%C3%B4%C3%B5%C3%B6%C3%B9%C3%BA%C3%BB%C3%BC%C3%BD%C3%BF%C3%B1
+ */
+ public function testNormalizeUtf8String()
+ {
+ $this->assertEquals(
+ "AAAAAACEEEEIIIIOEOOOOOUUUUYaaaaaaceeeeiiiioedooooouuuuyyn",
+ normalizeUtf8String("ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏŒÒÓÔÕÖÙÚÛÜÝàáâãäåçèéêëìíîïœðòóôõöùúûüýÿñ")
+ );
+ }
+
+ public function testLoginEnabledWithoutCreds()
+ {
+ global $config;
+ require_once __DIR__.'/../verifyLogin.php';
+ $config['cops_basic_authentication'] = [ "username" => "xxx", "password" => "secret"];
+ $this->assertFalse(verifyLogin());
+ }
+
+ public function testLoginEnabledAndLoggingIn()
+ {
+ global $config;
+ require_once __DIR__.'/../verifyLogin.php';
+ $config['cops_basic_authentication'] = [ "username" => "xxx", "password" => "secret"];
+ $_SERVER['PHP_AUTH_USER'] = 'xxx';
+ $_SERVER['PHP_AUTH_PW'] = 'secret';
+ $this->assertTrue(verifyLogin());
+ }
+}
diff --git a/test/bookTest.php b/test/bookTest.php
new file mode 100644
index 000000000..206183541
--- /dev/null
+++ b/test/bookTest.php
@@ -0,0 +1,640 @@
+
+ */
+
+require_once(dirname(__FILE__) . "/config_test.php");
+use PHPUnit\Framework\TestCase;
+
+/*
+Publishers:
+id:2 (2 books) Macmillan and Co. London: Lewis Caroll
+id:3 (2 books) D. Appleton and Company Alexander Dumas
+id:4 (1 book) Macmillan Publishers USA: Jack London
+id:5 (1 book) Pierson's Magazine: H. G. Wells
+id:6 (8 books) Strand Magazine: Arthur Conan Doyle
+*/
+
+define("TEST_THUMBNAIL", dirname(__FILE__) . "/thumbnail.jpg");
+define("COVER_WIDTH", 400);
+define("COVER_HEIGHT", 600);
+
+class BookTest extends TestCase
+{
+ public static function setUpBeforeClass(): void
+ {
+ global $config;
+ $config['calibre_directory'] = dirname(__FILE__) . "/BaseWithSomeBooks/";
+ Base::clearDb();
+ $book = Book::getBookById(2);
+ if (!is_dir($book->path)) {
+ mkdir($book->path, 0777, true);
+ }
+ $im = imagecreatetruecolor(COVER_WIDTH, COVER_HEIGHT);
+ $text_color = imagecolorallocate($im, 255, 0, 0);
+ imagestring($im, 1, 5, 5, 'Book cover', $text_color);
+ imagejpeg($im, $book->path . "/cover.jpg", 80);
+ }
+
+ public static function tearDownAfterClass(): void
+ {
+ $book = Book::getBookById(2);
+ if (!file_exists($book->path . "/cover.jpg")) {
+ return;
+ }
+ unlink($book->path . "/cover.jpg");
+ rmdir($book->path);
+ rmdir(dirname($book->path));
+ }
+
+ public function testGetBookCount()
+ {
+ $this->assertEquals(15, Book::getBookCount());
+ }
+
+ public function testGetCount()
+ {
+ $entryArray = Book::getCount();
+ $this->assertEquals(2, count($entryArray));
+
+ $entryAllBooks = $entryArray [0];
+ $this->assertEquals("Alphabetical index of the 15 books", $entryAllBooks->content);
+
+ $entryRecentBooks = $entryArray [1];
+ $this->assertEquals("50 most recent books", $entryRecentBooks->content);
+ }
+
+ public function testGetCountRecent()
+ {
+ global $config;
+ $config['cops_recentbooks_limit'] = 0;
+ $entryArray = Book::getCount();
+
+ $this->assertEquals(1, count($entryArray));
+
+ $config['cops_recentbooks_limit'] = 2;
+ $entryArray = Book::getCount();
+
+ $entryRecentBooks = $entryArray [1];
+ $this->assertEquals("2 most recent books", $entryRecentBooks->content);
+
+ $config['cops_recentbooks_limit'] = 50;
+ }
+
+ public function testGetBooksByAuthor()
+ {
+ // All book by Arthur Conan Doyle
+ global $config;
+
+ $config['cops_max_item_per_page'] = 5;
+ [$entryArray, $totalNumber] = Book::getBooksByAuthor(1, 1);
+ $this->assertEquals(5, count($entryArray));
+ $this->assertEquals(8, $totalNumber);
+
+ [$entryArray, $totalNumber] = Book::getBooksByAuthor(1, 2);
+ $this->assertEquals(3, count($entryArray));
+ $this->assertEquals(8, $totalNumber);
+
+ $config['cops_max_item_per_page'] = -1;
+ [$entryArray, $totalNumber] = Book::getBooksByAuthor(1, -1);
+ $this->assertEquals(8, count($entryArray));
+ $this->assertEquals(-1, $totalNumber);
+ }
+
+ public function testGetBooksBySeries()
+ {
+ // All book from the Sherlock Holmes series
+ [$entryArray, $totalNumber] = Book::getBooksBySeries(1, -1);
+ $this->assertEquals(7, count($entryArray));
+ $this->assertEquals(-1, $totalNumber);
+ }
+
+ public function testGetBooksByPublisher()
+ {
+ // All books from Strand Magazine
+ [$entryArray, $totalNumber] = Book::getBooksByPublisher(6, -1);
+ $this->assertEquals(8, count($entryArray));
+ $this->assertEquals(-1, $totalNumber);
+ }
+
+ public function testGetBooksByTag()
+ {
+ // All book with the Fiction tag
+ [$entryArray, $totalNumber] = Book::getBooksByTag(1, -1);
+ $this->assertEquals(14, count($entryArray));
+ $this->assertEquals(-1, $totalNumber);
+ }
+
+ public function testGetBooksByLanguage()
+ {
+ // All english book (= all books)
+ [$entryArray, $totalNumber] = Book::getBooksByLanguage(1, -1);
+ $this->assertEquals(14, count($entryArray));
+ $this->assertEquals(-1, $totalNumber);
+ }
+
+ public function testGetAllBooks()
+ {
+ // All books by first letter
+ $entryArray = Book::getAllBooks();
+ $this->assertCount(9, $entryArray);
+ }
+
+ public function testGetBooksByStartingLetter()
+ {
+ // All books by first letter
+ [$entryArray, $totalNumber] = Book::getBooksByStartingLetter("T", -1);
+ $this->assertEquals(-1, $totalNumber);
+ $this->assertCount(3, $entryArray);
+ }
+
+ public function testGetBookByDataId()
+ {
+ $book = Book::getBookByDataId(17);
+
+ $this->assertEquals("Alice's Adventures in Wonderland", $book->getTitle());
+ }
+
+ public function testGetAllRecentBooks()
+ {
+ // All recent books
+ global $config;
+
+ $config['cops_recentbooks_limit'] = 2;
+
+ $entryArray = Book::getAllRecentBooks();
+ $this->assertCount(2, $entryArray);
+
+ $config['cops_recentbooks_limit'] = 50;
+
+ $entryArray = Book::getAllRecentBooks();
+ $this->assertCount(15, $entryArray);
+ }
+
+ /**
+ * @dataProvider providerPublicationDate
+ */
+ public function testGetPubDate($pubdate, $expectedYear)
+ {
+ $book = Book::getBookById(2);
+ $book->pubdate = $pubdate;
+ $this->assertEquals($expectedYear, $book->getPubDate());
+ }
+
+ public function providerPublicationDate()
+ {
+ return [
+ ['2010-10-05 22:00:00+00:00', '2010'],
+ ['1982-11-15 13:05:29.908657+00:00', '1982'],
+ ['1562-10-05 00:00:00+00:00', '1562'],
+ ['0100-12-31 23:00:00+00:00', ''],
+ ['', ''],
+ [null, ''],
+ ];
+ }
+
+ public function testGetBookById()
+ {
+ // also check most of book's class methods
+ $book = Book::getBookById(2);
+
+ $linkArray = $book->getLinkArray();
+ $this->assertCount(5, $linkArray);
+
+ $this->assertEquals("The Return of Sherlock Holmes", $book->getTitle());
+ $this->assertEquals("urn:uuid:87ddbdeb-1e27-4d06-b79b-4b2a3bfc6a5f", $book->getEntryId());
+ $this->assertEquals("index.php?page=13&id=2", $book->getDetailUrl());
+ $this->assertEquals("Arthur Conan Doyle", $book->getAuthorsName());
+ $this->assertEquals("Fiction, Mystery & Detective, Short Stories", $book->getTagsName());
+ $this->assertEquals('
The Return of Sherlock Holmes is a collection of 13 Sherlock Holmes stories, originally published in 1903-1904, by Arthur Conan Doyle. The book was first published on March 7, 1905 by Georges Newnes, Ltd and in a Colonial edition by Longmans. 30,000 copies were made of the initial print run. The US edition by McClure, Phillips & Co. added another 28,000 to the run. This was the first Holmes collection since 1893, when Holmes had "died" in "The Adventure of the Final Problem". Having published The Hound of the Baskervilles in 1901–1902 (although setting it before Holmes\' death) Doyle came under intense pressure to revive his famous character.