aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--CONTRIBUTING.md23
-rw-r--r--README.md2
-rwxr-xr-xTRANSLATION.md67
-rw-r--r--check_setup.php10
-rw-r--r--inc/3rdparty/Session.class.php40
-rw-r--r--inc/3rdparty/class.messages.php3
-rwxr-xr-xinc/3rdparty/config.php104
-rw-r--r--inc/3rdparty/libraries/PHPePub/EPub.HtmlEntities.php266
-rw-r--r--inc/3rdparty/libraries/PHPePub/EPub.NCX.php782
-rw-r--r--inc/3rdparty/libraries/PHPePub/EPub.OPF.php1226
-rw-r--r--inc/3rdparty/libraries/PHPePub/EPub.php2438
-rw-r--r--inc/3rdparty/libraries/PHPePub/EPubChapterSplitter.php201
-rw-r--r--inc/3rdparty/libraries/PHPePub/Logger.php92
-rw-r--r--inc/3rdparty/libraries/PHPePub/Zip.php818
-rw-r--r--inc/3rdparty/libraries/PHPePub/lib.uuid.LICENCE.txt31
-rw-r--r--inc/3rdparty/libraries/PHPePub/lib.uuid.php314
-rw-r--r--inc/3rdparty/libraries/content-extractor/ContentExtractor.php1455
-rw-r--r--inc/3rdparty/libraries/content-extractor/SiteConfig.php681
-rwxr-xr-x[-rw-r--r--]inc/3rdparty/libraries/feedwriter/FeedItem.php365
-rwxr-xr-x[-rw-r--r--]inc/3rdparty/libraries/feedwriter/FeedWriter.php856
-rw-r--r--inc/3rdparty/libraries/html5/TreeBuilder.php13
-rw-r--r--inc/3rdparty/libraries/humble-http-agent/CookieJar.php807
-rw-r--r--inc/3rdparty/libraries/humble-http-agent/HumbleHttpAgent.php1589
-rw-r--r--inc/3rdparty/libraries/humble-http-agent/SimplePie_HumbleHttpAgent.php157
-rw-r--r--inc/3rdparty/libraries/language-detect/LanguageDetect.php992
-rw-r--r--inc/3rdparty/libraries/language-detect/LanguageDetect/Exception.php57
-rw-r--r--inc/3rdparty/libraries/language-detect/LanguageDetect/ISO639.php339
-rw-r--r--inc/3rdparty/libraries/language-detect/LanguageDetect/Parser.php (renamed from inc/3rdparty/libraries/language-detect/Parser.php)19
-rwxr-xr-x[-rw-r--r--]inc/3rdparty/libraries/readability/Readability.php2281
-rwxr-xr-xinc/3rdparty/makefulltextfeed.php714
-rwxr-xr-xinc/3rdparty/makefulltextfeedHelpers.php389
-rwxr-xr-x[-rw-r--r--]inc/3rdparty/simple_html_dom.php105
-rwxr-xr-xinc/3rdparty/site_config/custom/dailymotion.com.txt12
-rw-r--r--inc/3rdparty/site_config/custom/index.php3
-rw-r--r--inc/3rdparty/site_config/custom/mobile.lemondeinformatique.fr.txt6
-rwxr-xr-xinc/3rdparty/site_config/custom/ted.com.txt11
-rw-r--r--inc/3rdparty/site_config/index.php5
-rw-r--r--inc/3rdparty/site_config/standard/.about.com.txt14
-rw-r--r--inc/3rdparty/site_config/standard/moo.nac.uci.edu.txt9
-rwxr-xr-x[-rw-r--r--]inc/3rdparty/site_config/standard/politico.com.txt4
-rw-r--r--inc/3rdparty/site_config/standard/version.txt2
-rwxr-xr-xinc/poche/Database.class.php182
-rwxr-xr-xinc/poche/Poche.class.php947
-rwxr-xr-x[-rw-r--r--]inc/poche/Tools.class.php93
-rwxr-xr-xinc/poche/config.inc.default.php68
-rwxr-xr-xinc/poche/config.inc.php.new63
-rwxr-xr-x[-rw-r--r--]inc/poche/global.inc.php8
-rw-r--r--inc/poche/pochePictures.php58
-rwxr-xr-x[-rw-r--r--]index.php30
-rwxr-xr-x[-rw-r--r--]install/index.php81
-rw-r--r--[-rwxr-xr-x]install/poche.sqlitebin393216 -> 393216 bytes
-rw-r--r--install/postgres.sql14
-rw-r--r--locale/cs_CZ.utf8/LC_MESSAGES/cs_CZ.utf8.po496
-rw-r--r--locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.mobin4776 -> 10620 bytes
-rw-r--r--locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.po647
-rw-r--r--locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.mobin4481 -> 11884 bytes
-rw-r--r--locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.po488
-rw-r--r--locale/es_ES.utf8/LC_MESSAGES/es_ES.utf8.po495
-rw-r--r--locale/fa_IR.utf8/LC_MESSAGES/fa_IR.utf8.po491
-rw-r--r--locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mobin4810 -> 16343 bytes
-rw-r--r--locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po662
-rw-r--r--locale/it_IT.utf8/LC_MESSAGES/it_IT.utf8.po503
-rw-r--r--[-rwxr-xr-x]locale/pl_PL.utf8/LC_MESSAGES/pl_PL.utf8.mobin12632 -> 12632 bytes
-rw-r--r--locale/pt_BR.utf8/LC_MESSAGES/pt_BR.utf8.mobin0 -> 12451 bytes
-rw-r--r--locale/pt_BR.utf8/LC_MESSAGES/pt_BR.utf8.po549
-rwxr-xr-xlocale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.mobin13000 -> 13552 bytes
-rwxr-xr-xlocale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.po421
-rw-r--r--locale/sl_SI.utf8/LC_MESSAGES/sl_SI.utf8.po507
-rwxr-xr-xlocale/tools/fillCache.php59
-rwxr-xr-xlocale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.mobin13258 -> 15670 bytes
-rwxr-xr-xlocale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.po278
-rwxr-xr-xthemes/baggy/_display-mode.twig5
-rwxr-xr-x[-rw-r--r--]themes/baggy/_head.twig28
-rw-r--r--themes/baggy/_menu.twig8
-rwxr-xr-xthemes/baggy/_pocheit-form.twig10
-rw-r--r--themes/baggy/_search-form.twig7
-rwxr-xr-x[-rw-r--r--]themes/baggy/_top.twig2
-rwxr-xr-x[-rw-r--r--]themes/baggy/config.twig74
-rwxr-xr-xthemes/baggy/css/main.css254
-rwxr-xr-xthemes/baggy/css/print.css14
-rwxr-xr-x[-rw-r--r--]themes/baggy/edit-tags.twig8
-rw-r--r--[-rwxr-xr-x]themes/baggy/fonts/icomoon.eotbin3724 -> 3896 bytes
-rw-r--r--[-rwxr-xr-x]themes/baggy/fonts/icomoon.svg17
-rw-r--r--[-rwxr-xr-x]themes/baggy/fonts/icomoon.ttfbin3560 -> 3732 bytes
-rw-r--r--[-rwxr-xr-x]themes/baggy/fonts/icomoon.woffbin3528 -> 3664 bytes
-rwxr-xr-x[-rw-r--r--]themes/baggy/home.twig25
-rwxr-xr-xthemes/baggy/img/baggy/blank.pngbin0 -> 141 bytes
-rw-r--r--themes/baggy/img/baggy/down.pngbin0 -> 216 bytes
-rwxr-xr-xthemes/baggy/img/baggy/list.pngbin0 -> 201 bytes
-rwxr-xr-xthemes/baggy/img/baggy/table.pngbin0 -> 229 bytes
-rw-r--r--themes/baggy/img/baggy/top.pngbin0 -> 212 bytes
-rw-r--r--themes/baggy/js/autoClose.js6
-rwxr-xr-xthemes/baggy/js/init.js56
-rw-r--r--themes/baggy/js/jquery-2.0.3.min.js1
-rwxr-xr-xthemes/baggy/js/jquery.cookie.js117
-rwxr-xr-x[-rw-r--r--]themes/baggy/layout.twig3
-rwxr-xr-x[-rw-r--r--]themes/baggy/tags.twig2
-rwxr-xr-x[-rw-r--r--]themes/baggy/view.twig71
-rwxr-xr-xthemes/courgette/_head.twig18
-rwxr-xr-xthemes/courgette/_top.twig2
-rwxr-xr-xthemes/courgette/_view.twig3
-rwxr-xr-xthemes/courgette/config.twig59
-rwxr-xr-xthemes/courgette/home.twig21
-rwxr-xr-x[-rw-r--r--]themes/courgette/tags.twig2
-rw-r--r--themes/dark/img/dark/rss.pngbin0 -> 288 bytes
-rw-r--r--themes/default/_bookmarklet.twig2
-rwxr-xr-x[-rw-r--r--]themes/default/_head.twig26
-rwxr-xr-xthemes/default/_import.twig15
-rw-r--r--themes/default/_menu.twig4
-rwxr-xr-xthemes/default/_pocheit-form.twig26
-rwxr-xr-xthemes/default/_search-form.twig9
-rwxr-xr-xthemes/default/_sorting.twig6
-rwxr-xr-x[-rw-r--r--]themes/default/_top.twig2
-rwxr-xr-x[-rw-r--r--]themes/default/config.twig66
-rw-r--r--themes/default/css/images/animated-overlay.gifbin0 -> 1738 bytes
-rw-r--r--themes/default/css/images/ui-bg_flat_0_aaaaaa_40x100.pngbin0 -> 212 bytes
-rw-r--r--themes/default/css/images/ui-bg_flat_75_ffffff_40x100.pngbin0 -> 208 bytes
-rw-r--r--themes/default/css/images/ui-bg_glass_55_fbf9ee_1x400.pngbin0 -> 335 bytes
-rw-r--r--themes/default/css/images/ui-bg_glass_65_ffffff_1x400.pngbin0 -> 207 bytes
-rw-r--r--themes/default/css/images/ui-bg_glass_75_dadada_1x400.pngbin0 -> 262 bytes
-rw-r--r--themes/default/css/images/ui-bg_glass_75_e6e6e6_1x400.pngbin0 -> 262 bytes
-rw-r--r--themes/default/css/images/ui-bg_glass_95_fef1ec_1x400.pngbin0 -> 332 bytes
-rw-r--r--themes/default/css/images/ui-bg_highlight-soft_75_cccccc_1x100.pngbin0 -> 280 bytes
-rw-r--r--themes/default/css/images/ui-icons_222222_256x240.pngbin0 -> 6922 bytes
-rw-r--r--themes/default/css/images/ui-icons_2e83ff_256x240.pngbin0 -> 4549 bytes
-rw-r--r--themes/default/css/images/ui-icons_454545_256x240.pngbin0 -> 6992 bytes
-rw-r--r--themes/default/css/images/ui-icons_888888_256x240.pngbin0 -> 6999 bytes
-rw-r--r--themes/default/css/images/ui-icons_cd0a0a_256x240.pngbin0 -> 4549 bytes
-rw-r--r--themes/default/css/jquery-ui-1.10.4.custom.css560
-rw-r--r--themes/default/css/jquery-ui-1.10.4.custom.min.css7
-rwxr-xr-x[-rw-r--r--]themes/default/css/style-default.css4
-rwxr-xr-x[-rw-r--r--]themes/default/css/style.css85
-rwxr-xr-x[-rw-r--r--]themes/default/edit-tags.twig10
-rwxr-xr-x[-rw-r--r--]themes/default/home.twig30
-rwxr-xr-xthemes/default/img/default/print.pngbin0 -> 321 bytes
-rwxr-xr-xthemes/default/js/autoCompleteTags.js47
-rw-r--r--themes/default/js/jquery-ui-1.10.4.custom.js2519
-rw-r--r--themes/default/js/jquery-ui-1.10.4.custom.min.js6
-rw-r--r--themes/default/js/popupForm.js20
-rwxr-xr-xthemes/default/js/saveLink.js101
-rwxr-xr-x[-rw-r--r--]themes/default/tags.twig12
-rwxr-xr-x[-rw-r--r--]themes/default/view.twig69
-rw-r--r--themes/dmagenta/img/dmagenta/rss.pngbin0 -> 288 bytes
-rw-r--r--themes/solarized-dark/img/solarized-dark/rss.pngbin0 -> 288 bytes
-rw-r--r--themes/solarized/img/solarized/rss.pngbin0 -> 288 bytes
-rw-r--r--wallabag_compatibility_test.php176
147 files changed, 22163 insertions, 6897 deletions
diff --git a/.gitignore b/.gitignore
index 2abed7ed..aec2e3ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ cache/*
3vendor 3vendor
4composer.phar 4composer.phar
5db/poche.sqlite 5db/poche.sqlite
6inc/poche/config.inc.php \ No newline at end of file 6inc/poche/config.inc.php
7inc/3rdparty/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/ \ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 85c09e52..9ccb0b14 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,11 +1,28 @@
1# How contributing 1# How to contribute
2 2
3## You found a bug 3## You found a bug
4Please [open a new issue](https://github.com/wallabag/wallabag/issues/new). 4Please [open a new issue](https://github.com/wallabag/wallabag/issues/new).
5 5
6To fix the bug quickly, we need some infos: 6To fix the bug quickly, we need some infos:
7* your wallabag version (in ./index.php) 7* your wallabag version (on top of the ./index.php file, and also on config page)
8* your webserver installation :
9 * type of hosting (shared or dedicaced)
10 * in case of a dedicaced server, the server and OS used
11 * the php version used, eventually `phpinfo()`
12* which storage system you choose at install (SQLite, MySQL/MariaDB or PostgreSQL)
13* any problem on the `wallabag_compatibility_test.php` page
14* any particular details which could be related
15
16
17If relevant :
8* the link you want to save and which causes problem 18* the link you want to save and which causes problem
19* the file you want to import into wallabag, or just an extract
20
21If you have the skills :
22* enable DEBUG mode and look the output at cache/log.txt
23* look for errors into php and server logs
24
25Note : If you have large portions of text, use [Github's Gist service](https://gist.github.com/) or other pastebin-like.
9 26
10## You want to fix a bug or to add a feature 27## You want to fix a bug or to add a feature
11Please fork wallabag and work with **the dev branch** only. **Do not work on master branch**. \ No newline at end of file 28Please fork wallabag and work with **the dev branch** only. **Do not work on master branch**.
diff --git a/README.md b/README.md
index fe6a06b6..0b54dff4 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ wallabag is a self hostable application allowing you to not miss any content any
4More informations on our website: [wallabag.org](http://wallabag.org) 4More informations on our website: [wallabag.org](http://wallabag.org)
5 5
6## License 6## License
7Copyright © 2010-2013 Nicolas Lœuillet <nicolas@loeuillet.org> 7Copyright © 2010-2014 Nicolas Lœuillet <nicolas@loeuillet.org>
8This work is free. You can redistribute it and/or modify it under the 8This work is free. You can redistribute it and/or modify it under the
9terms of the Do What The Fuck You Want To Public License, Version 2, 9terms of the Do What The Fuck You Want To Public License, Version 2,
10as published by Sam Hocevar. See the COPYING file for more details. 10as published by Sam Hocevar. See the COPYING file for more details.
diff --git a/TRANSLATION.md b/TRANSLATION.md
new file mode 100755
index 00000000..2e38d5cc
--- /dev/null
+++ b/TRANSLATION.md
@@ -0,0 +1,67 @@
1# How to manage translations of wallabag
2
3This guide will describe procedure of translation management of wallabag web application.
4
5All translation are made using [gettext](http://en.wikipedia.org/wiki/Gettext) system and tools.
6
7You will need [Poedit](http://www.poedit.net/download.php) editor to update, edit and create your translation files comfortably. In general, you can handle translations also without it: all can be done using gettext tools and your favorite plain text editor only. This guide, however, describes editing with Poedit. If you want to use gettext only, pls refer to xgettext manual page to update po files from sources (see also how it is used by Poedit below) and use msgunfmt tool to compile .mo files manually.
8
9You need to know, that translation phrases are stored in **".po"** files (for example: `locale/pl_PL.utf8/LC_MESSAGES/pl_PL.utf8.po`), which are then complied in **".mo"** files using **msgfmt** gettext tool or by Poedit, which will run msgfmt for you in background.
10
11**It's assumed, that you have wallabag installed locally on your computer or on the server you have access to.**
12
13## To change existing translation you will need to do:
14
15### 1. Clear cache
16You can do this using **http://your-wallabag-host.com/?empty-cache** link (replace http://your-wallabag-host.com/ with real url of your wallabag application)
17
18OR
19
20from command line:
21go to root of your installation of wallabag project and run next command:
22
23`rm -rf ./cache/*`
24
25(this may require root privileges if you run, for example Apatche web server with mod_php)
26
27### 2. Generate php files from all twig templates
28Do this using next command:
29
30`php ./locale/tools/fillCache.php`
31
32OR
33
34from your browser: **http://your-wallabag-host.com/locale/tools/fillCache.php** (this may require removal of .htacces file in locale/ directory).
35
36### 3. Configure your Poedit
37Open Poedit editor, open Edit->Preferences. Go to "Parsers" tab, click on PHP and press "Edit" button. Make sure your "Parser command:" looks like
38
39`xgettext --no-location --force-po -o %o %C %K %F`
40
41Usualy it is required to add "--no-location" to default value.
42
43### 4. Open .po file you want to edit in Poedit and change it's settings
44Open, for example `locale/pl_PL.utf8/LC_MESSAGES/pl_PL.utf8.po` file in your Poedit.
45
46Go to "Catalog"->"Settings..." menu. Go to "Path" tab and add path to wallabag installaion in your local file system. This step can't be ommited as you will not be able to update phrases otherwise.
47
48You can also check "project into" tab to be sure, that "Language" is set correctly (this will allow you to spell check your translation).
49
50### 5. Update opened .po file from sources
51Once you have set your path correctly, you are able to update phrases from sources. Press "Update catalog - synchronize it with sources" button or go to "Catalog"->"Update from sources" menu.
52
53As a result you will see confirmation popup with two tabs: "New strings" and "Obsolete strings". Pls review and accept changes (or press "Undo" if you see too many obsolete strings, as Poedit will remove them all - in this case please make sure all previous steps are performed w/o errors).
54
55### 6. Translate and save your .po file
56If you have any dificulties on this step, please consult with Poedit manual.
57Every time you save your .po file, Poedit will also comple appropriate .mo file by default (of course, if not disabled in preferences).
58
59So, you are almost done.
60
61### 7. Clear cache again
62This step may be required if your web server runs php scripts in name of, say, www user (i.e. Apache with mod_php, not cgi).
63
64
65##To create new translation
66Please simple create appropriate directories in locale folder and perform all steps, described above. Instead of opening an existing file just create new one.
67
diff --git a/check_setup.php b/check_setup.php
index 96dd0f7d..2b84a744 100644
--- a/check_setup.php
+++ b/check_setup.php
@@ -13,16 +13,6 @@ if (version_compare(PHP_VERSION, '5.4.0', '<')) {
13 } 13 }
14} 14}
15 15
16// Check PDO Sqlite
17if (! extension_loaded('pdo_sqlite')) {
18 die('PHP extension required: pdo_sqlite');
19}
20
21// Check ZIP
22if (! extension_loaded('zip')) {
23 die('PHP extension required: zip');
24}
25
26// Check if /cache is writeable 16// Check if /cache is writeable
27if (! is_writable('cache')) { 17if (! is_writable('cache')) {
28 die('The directory "cache" must be writeable by your web server user'); 18 die('The directory "cache" must be writeable by your web server user');
diff --git a/inc/3rdparty/Session.class.php b/inc/3rdparty/Session.class.php
index b30a31f3..59dfbe67 100644
--- a/inc/3rdparty/Session.class.php
+++ b/inc/3rdparty/Session.class.php
@@ -31,9 +31,9 @@ class Session
31 public static $sessionName = ''; 31 public static $sessionName = '';
32 // If the user does not access any page within this time, 32 // If the user does not access any page within this time,
33 // his/her session is considered expired (3600 sec. = 1 hour) 33 // his/her session is considered expired (3600 sec. = 1 hour)
34 public static $inactivityTimeout = 86400; 34 public static $inactivityTimeout = 3600;
35 // Extra timeout for long sessions (if enabled) (82800 sec. = 23 hours) 35 // Extra timeout for long sessions (if enabled) (82800 sec. = 23 hours)
36 public static $longSessionTimeout = 31536000; 36 public static $longSessionTimeout = 7776000; // 7776000 = 90 days
37 // If you get disconnected often or if your IP address changes often. 37 // If you get disconnected often or if your IP address changes often.
38 // Let you disable session cookie hijacking protection 38 // Let you disable session cookie hijacking protection
39 public static $disableSessionProtection = false; 39 public static $disableSessionProtection = false;
@@ -48,8 +48,13 @@ class Session
48 /** 48 /**
49 * Initialize session 49 * Initialize session
50 */ 50 */
51 public static function init() 51 public static function init($longlastingsession = false)
52 { 52 {
53 //check if session name is correct
54 if ( (session_id() && !empty(self::$sessionName) && session_name()!=self::$sessionName) || $longlastingsession ) {
55 session_destroy();
56 }
57
53 // Force cookie path (but do not change lifetime) 58 // Force cookie path (but do not change lifetime)
54 $cookie = session_get_cookie_params(); 59 $cookie = session_get_cookie_params();
55 // Default cookie expiration and path. 60 // Default cookie expiration and path.
@@ -61,12 +66,22 @@ class Session
61 if (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") { 66 if (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") {
62 $ssl = true; 67 $ssl = true;
63 } 68 }
64 session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['HTTP_HOST'], $ssl); 69
70 if ( $longlastingsession ) {
71 session_set_cookie_params(self::$longSessionTimeout, $cookiedir, null, $ssl, true);
72 }
73 else {
74 session_set_cookie_params(0, $cookiedir, null, $ssl, true);
75 }
76 //set server side valid session timeout
77 //WARNING! this may not work in shared session environment. See http://www.php.net/manual/en/session.configuration.php#ini.session.gc-maxlifetime about min value: it can be set in any application
78 ini_set('session.gc_maxlifetime', self::$longSessionTimeout);
79
65 // Use cookies to store session. 80 // Use cookies to store session.
66 ini_set('session.use_cookies', 1); 81 ini_set('session.use_cookies', 1);
67 // Force cookies for session (phpsessionID forbidden in URL) 82 // Force cookies for session (phpsessionID forbidden in URL)
68 ini_set('session.use_only_cookies', 1); 83 ini_set('session.use_only_cookies', 1);
69 if (!session_id()) { 84 if ( !session_id() ) {
70 // Prevent php to use sessionID in URL if cookies are disabled. 85 // Prevent php to use sessionID in URL if cookies are disabled.
71 ini_set('session.use_trans_sid', false); 86 ini_set('session.use_trans_sid', false);
72 if (!empty(self::$sessionName)) { 87 if (!empty(self::$sessionName)) {
@@ -115,6 +130,9 @@ class Session
115 if (self::banCanLogin()) { 130 if (self::banCanLogin()) {
116 if ($login === $loginTest && $password === $passwordTest) { 131 if ($login === $loginTest && $password === $passwordTest) {
117 self::banLoginOk(); 132 self::banLoginOk();
133
134 self::init($longlastingsession);
135
118 // Generate unique random number to sign forms (HMAC) 136 // Generate unique random number to sign forms (HMAC)
119 $_SESSION['uid'] = sha1(uniqid('', true).'_'.mt_rand()); 137 $_SESSION['uid'] = sha1(uniqid('', true).'_'.mt_rand());
120 $_SESSION['ip'] = self::_allIPs(); 138 $_SESSION['ip'] = self::_allIPs();
@@ -135,6 +153,7 @@ class Session
135 self::banLoginFailed(); 153 self::banLoginFailed();
136 } 154 }
137 155
156 self::init();
138 return false; 157 return false;
139 } 158 }
140 159
@@ -143,7 +162,14 @@ class Session
143 */ 162 */
144 public static function logout() 163 public static function logout()
145 { 164 {
146 unset($_SESSION['uid'],$_SESSION['ip'],$_SESSION['expires_on'],$_SESSION['tokens'], $_SESSION['login'], $_SESSION['pass'], $_SESSION['longlastingsession'], $_SESSION['poche_user']); 165 // unset($_SESSION['uid'],$_SESSION['ip'],$_SESSION['expires_on'],$_SESSION['tokens'], $_SESSION['login'], $_SESSION['pass'], $_SESSION['longlastingsession'], $_SESSION['poche_user']);
166
167 // Destruction du cookie (le code peut paraître complexe mais c'est pour être certain de reprendre les mêmes paramètres)
168 $args = array_merge(array(session_name(), ''), array_values(session_get_cookie_params()));
169 $args[2] = time() - 3600;
170 call_user_func_array('setcookie', $args);
171 // Suppression physique de la session
172 session_destroy();
147 } 173 }
148 174
149 /** 175 /**
@@ -157,7 +183,7 @@ class Session
157 || (self::$disableSessionProtection === false 183 || (self::$disableSessionProtection === false
158 && $_SESSION['ip'] !== self::_allIPs()) 184 && $_SESSION['ip'] !== self::_allIPs())
159 || time() >= $_SESSION['expires_on']) { 185 || time() >= $_SESSION['expires_on']) {
160 self::logout(); 186 //self::logout();
161 187
162 return false; 188 return false;
163 } 189 }
diff --git a/inc/3rdparty/class.messages.php b/inc/3rdparty/class.messages.php
index e60bd3a1..27c28f43 100644
--- a/inc/3rdparty/class.messages.php
+++ b/inc/3rdparty/class.messages.php
@@ -59,6 +59,7 @@ class Messages {
59 $this->msgId = md5(uniqid()); 59 $this->msgId = md5(uniqid());
60 60
61 // Create the session array if it doesnt already exist 61 // Create the session array if it doesnt already exist
62 settype($_SESSION, 'array');
62 if( !array_key_exists('flash_messages', $_SESSION) ) $_SESSION['flash_messages'] = array(); 63 if( !array_key_exists('flash_messages', $_SESSION) ) $_SESSION['flash_messages'] = array();
63 64
64 } 65 }
@@ -228,4 +229,4 @@ class Messages {
228 229
229 230
230} // end class 231} // end class
231?> \ No newline at end of file 232?>
diff --git a/inc/3rdparty/config.php b/inc/3rdparty/config.php
index e618117b..ec680d86 100755
--- a/inc/3rdparty/config.php
+++ b/inc/3rdparty/config.php
@@ -19,7 +19,7 @@ if (!isset($options)) $options = new stdClass();
19// Enable service 19// Enable service
20// ---------------------- 20// ----------------------
21// Set this to false if you want to disable the service. 21// Set this to false if you want to disable the service.
22// If set to false, no feed is produced and users will 22// If set to false, no feed is produced and users will
23// be told that the service is disabled. 23// be told that the service is disabled.
24$options->enabled = true; 24$options->enabled = true;
25 25
@@ -43,10 +43,64 @@ $options->default_entries = 5;
43// ---------------------- 43// ----------------------
44// The maximum number of feed items to process when no access key is supplied. 44// The maximum number of feed items to process when no access key is supplied.
45// This limits the user-supplied &max=x value. For example, if the user 45// This limits the user-supplied &max=x value. For example, if the user
46// asks for 20 items to be processed (&max=20), if max_entries is set to 46// asks for 20 items to be processed (&max=20), if max_entries is set to
47// 10, only 10 will be processed. 47// 10, only 10 will be processed.
48$options->max_entries = 10; 48$options->max_entries = 10;
49 49
50// Full content
51// ----------------------
52// By default Full-Text RSS includes the extracted content in the output.
53// You can exclude this from the output by passing '&content=0' in the querystring.
54//
55// Possible values...
56// Always include: true
57// Never include: false
58// Include unless user overrides (&content=0): 'user' (default)
59//
60// Note: currently this does not disable full content extraction. It simply omits it
61// from the output.
62$options->content = 'user';
63
64// Excerpts
65// ----------------------
66// By default Full-Text RSS does not include excerpts in the output.
67// You can enable this by passing '&summary=1' in the querystring.
68// This will include a plain text excerpt from the extracted content.
69//
70// Possible values...
71// Always include: true (recommended for new users)
72// Never include: false
73// Don't include unless user overrides (&summary=1): 'user' (default)
74//
75// Important: if both content and excerpts are requested, the excerpt will be
76// placed in the description element and the full content inside content:encoded.
77// If excerpts are not requested, the full content will go inside the description element.
78//
79// Why are we not returning both excerpts and content by default?
80// Mainly for backward compatibility.
81// Excerpts should appear in the feed item's description element. Previous versions
82// of Full-Text RSS did not return excerpts, so the description element was always
83// used for the full content (as recommended by the RSS advisory). When returning both,
84// we need somewhere else to place the content (content:encoded).
85// Having both enabled should not create any problems for news readers, but it may create
86// problems for developers upgrading from one of our earlier versions who may now find
87// their applications are returning excerpts instead of the full content they were
88// expecting. To avoid such surprises for users who are upgrading Full-Text RSS,
89// excerpts must be explicitly requested in the querystring by default.
90//
91// Why not use a different element name for excerpts?
92// According to the RSS advisory:
93// "Publishers who employ summaries should store the summary in description and
94// the full content in content:encoded, ordering description first within the item.
95// On items with no summary, the full content should be stored in description."
96// See: http://www.rssboard.org/rss-profile#namespace-elements-content-encoded
97//
98// For more consistent element naming, we recommend new users set this option to true.
99// The full content can still be excluded via the querystring, but the element names
100// will not change: when $options->summary = true, the description element will always
101// be reserved for the excerpt and content:encoded always for full content.
102$options->summary = 'user';
103
50// Rewrite relative URLs 104// Rewrite relative URLs
51// ---------------------- 105// ----------------------
52// With this enabled relative URLs found in the extracted content 106// With this enabled relative URLs found in the extracted content
@@ -67,7 +121,7 @@ $options->exclude_items_on_fail = 'user';
67// Enable multi-page support 121// Enable multi-page support
68// ------------------------- 122// -------------------------
69// If enabled, we will try to follow next page links on multi-page articles. 123// If enabled, we will try to follow next page links on multi-page articles.
70// Currently this only happens for sites where next_page_link has been defined 124// Currently this only happens for sites where next_page_link has been defined
71// in a site config file. 125// in a site config file.
72$options->multipage = true; 126$options->multipage = true;
73 127
@@ -125,10 +179,10 @@ $options->detect_language = 1;
125 179
126// Registration key 180// Registration key
127// --------------- 181// ---------------
128// The registration key is optional. It is not required to use Full-Text RSS, 182// The registration key is optional. It is not required to use Full-Text RSS,
129// and does not affect the normal operation of Full-Text RSS. It is currently 183// and does not affect the normal operation of Full-Text RSS. It is currently
130// only used on admin pages which help you update site patterns with the 184// only used on admin pages which help you update site patterns with the
131// latest version offered by FiveFilters.org. For these admin-related 185// latest version offered by FiveFilters.org. For these admin-related
132// tasks to complete, we will require a valid registration key. 186// tasks to complete, we will require a valid registration key.
133// If you would like one, you can purchase the latest version of Full-Text RSS 187// If you would like one, you can purchase the latest version of Full-Text RSS
134// at http://fivefilters.org/content-only/ 188// at http://fivefilters.org/content-only/
@@ -144,12 +198,12 @@ $options->registration_key = '';
144// ---------------------- 198// ----------------------
145// Certain pages/actions, e.g. updating site patterns with our online tool, will require admin credentials. 199// Certain pages/actions, e.g. updating site patterns with our online tool, will require admin credentials.
146// To use these pages, enter a password here and you'll be prompted for it when you try to access those pages. 200// To use these pages, enter a password here and you'll be prompted for it when you try to access those pages.
147// If no password or username is set, pages requiring admin privelages will be inaccessible. 201// If no password or username is set, pages requiring admin privelages will be inaccessible.
148// The default username is 'admin'. 202// The default username is 'admin'.
149// If overriding with an environment variable, separate username and password with a colon, e.g.: 203// If overriding with an environment variable, separate username and password with a colon, e.g.:
150// ftr_admin_credentials: admin:my-secret-password 204// ftr_admin_credentials: admin:my-secret-password
151// Example: $options->admin_credentials = array('username'=>'admin', 'password'=>'my-secret-password'); 205// Example: $options->admin_credentials = array('username'=>'admin', 'password'=>'my-secret-password');
152$options->admin_credentials = array('username'=>'admin', 'password'=>'admin'); 206$options->admin_credentials = array('username'=>'admin', 'password'=>'');
153 207
154// URLs to allow 208// URLs to allow
155// ---------------------- 209// ----------------------
@@ -178,12 +232,12 @@ $options->key_required = false;
178// ---------------------- 232// ----------------------
179// By default, when processing feeds, we assume item titles in the feed 233// By default, when processing feeds, we assume item titles in the feed
180// have not been truncated. So after processing web pages, the extracted titles 234// have not been truncated. So after processing web pages, the extracted titles
181// are not used in the generated feed. If you prefer to have extracted titles in 235// are not used in the generated feed. If you prefer to have extracted titles in
182// the feed you can either set this to false, in which case we will always favour 236// the feed you can either set this to false, in which case we will always favour
183// extracted titles. Alternatively, if set to 'user' (default) we'll use the 237// extracted titles. Alternatively, if set to 'user' (default) we'll use the
184// extracted title if you pass '&use_extracted_title' in the querystring. 238// extracted title if you pass '&use_extracted_title' in the querystring.
185// Possible values: 239// Possible values:
186// * Favour feed titles: true 240// * Favour feed titles: true
187// * Favour extracted titles: false 241// * Favour extracted titles: false
188// * Favour feed titles with user override: 'user' (default) 242// * Favour feed titles with user override: 'user' (default)
189// Note: this has no effect when the input URL is to a web page - in these cases 243// Note: this has no effect when the input URL is to a web page - in these cases
@@ -192,17 +246,17 @@ $options->favour_feed_titles = 'user';
192 246
193// Access keys (password protected access) 247// Access keys (password protected access)
194// ------------------------------------ 248// ------------------------------------
195// NOTE: You do not need an API key from fivefilters.org to run your own 249// NOTE: You do not need an API key from fivefilters.org to run your own
196// copy of the code. This is here if you'd like to restrict access to 250// copy of the code. This is here if you'd like to restrict access to
197// _your_ copy. 251// _your_ copy.
198// Keys let you group users - those with a key and those without - and 252// Keys let you group users - those with a key and those without - and
199// restrict access to the service to those without a key. 253// restrict access to the service to those without a key.
200// If you want everyone to access the service in the same way, you can 254// If you want everyone to access the service in the same way, you can
201// leave the array below empty and ignore the access key options further down. 255// leave the array below empty and ignore the access key options further down.
202// The options further down let you control how the service should behave 256// The options further down let you control how the service should behave
203// in each mode. 257// in each mode.
204// Note: Explicitly including the index number (1 and 2 in the examples below) 258// Note: Explicitly including the index number (1 and 2 in the examples below)
205// is highly recommended (when generating feeds, we encode the key and 259// is highly recommended (when generating feeds, we encode the key and
206// refer to it by index number and hash). 260// refer to it by index number and hash).
207$options->api_keys = array(); 261$options->api_keys = array();
208// Example: 262// Example:
@@ -232,13 +286,13 @@ $options->max_entries_with_key = 10;
232// filter the resulting HTML for XSS attacks, making it redundant for 286// filter the resulting HTML for XSS attacks, making it redundant for
233// Full-Text RSS do the same. Similarly with frameworks/CMS which display 287// Full-Text RSS do the same. Similarly with frameworks/CMS which display
234// feed content - the content should be treated like any other user-submitted content. 288// feed content - the content should be treated like any other user-submitted content.
235// 289//
236// If you are writing an application yourself which is processing feeds generated by 290// If you are writing an application yourself which is processing feeds generated by
237// Full-Text RSS, you can either filter the HTML yourself to remove potential XSS attacks 291// Full-Text RSS, you can either filter the HTML yourself to remove potential XSS attacks
238// or enable this option. This might be useful if you are processing our generated 292// or enable this option. This might be useful if you are processing our generated
239// feeds with JavaScript on the client side - although there's client side xss 293// feeds with JavaScript on the client side - although there's client side xss
240// filtering available too, e.g. https://code.google.com/p/google-caja/wiki/JsHtmlSanitizer 294// filtering available too, e.g. https://code.google.com/p/google-caja/wiki/JsHtmlSanitizer
241// 295//
242// If enabled, we'll pass retrieved HTML content through htmLawed with 296// If enabled, we'll pass retrieved HTML content through htmLawed with
243// safe flag on and style attributes denied, see 297// safe flag on and style attributes denied, see
244// http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/htmLawed_README.htm#s3.6 298// http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/htmLawed_README.htm#s3.6
@@ -253,8 +307,8 @@ $options->xss_filter = 'user';
253// Allowed parsers 307// Allowed parsers
254// ---------------------- 308// ----------------------
255// Full-Text RSS attempts to use PHP's libxml extension to process HTML. 309// Full-Text RSS attempts to use PHP's libxml extension to process HTML.
256// While fast, on some sites it may not always produce good results. 310// While fast, on some sites it may not always produce good results.
257// For these sites, you can specify an alternative HTML parser: 311// For these sites, you can specify an alternative HTML parser:
258// parser: html5lib 312// parser: html5lib
259// The html5lib parser is bundled with Full-Text RSS. 313// The html5lib parser is bundled with Full-Text RSS.
260// see http://code.google.com/p/html5lib/ 314// see http://code.google.com/p/html5lib/
@@ -273,7 +327,7 @@ $options->cors = false;
273 327
274// Use APC user cache? 328// Use APC user cache?
275// ---------------------- 329// ----------------------
276// If enabled we will store site config files (when requested 330// If enabled we will store site config files (when requested
277// for the first time) in APC's user cache. Keys prefixed with 'sc.' 331// for the first time) in APC's user cache. Keys prefixed with 'sc.'
278// This improves performance by reducing disk access. 332// This improves performance by reducing disk access.
279// Note: this has no effect if APC is unavailable on your server. 333// Note: this has no effect if APC is unavailable on your server.
@@ -346,7 +400,7 @@ $options->rewrite_url = array(
346// Valid actions: 400// Valid actions:
347// * 'exclude' - exclude this item from the result 401// * 'exclude' - exclude this item from the result
348// * 'link' - create HTML link to the item 402// * 'link' - create HTML link to the item
349$options->content_type_exc = array( 403$options->content_type_exc = array(
350 'application/pdf' => array('action'=>'link', 'name'=>'PDF'), 404 'application/pdf' => array('action'=>'link', 'name'=>'PDF'),
351 'image' => array('action'=>'link', 'name'=>'Image'), 405 'image' => array('action'=>'link', 'name'=>'Image'),
352 'audio' => array('action'=>'link', 'name'=>'Audio'), 406 'audio' => array('action'=>'link', 'name'=>'Audio'),
@@ -375,13 +429,13 @@ $options->cache_cleanup = 100;
375/// DO NOT CHANGE ANYTHING BELOW THIS /////////// 429/// DO NOT CHANGE ANYTHING BELOW THIS ///////////
376///////////////////////////////////////////////// 430/////////////////////////////////////////////////
377 431
378if (!defined('_FF_FTR_VERSION')) define('_FF_FTR_VERSION', '3.1'); 432if (!defined('_FF_FTR_VERSION')) define('_FF_FTR_VERSION', '3.2');
379 433
380if (basename(__FILE__) == 'config.php') { 434if (basename(__FILE__) == 'config.php') {
381 if (file_exists(dirname(__FILE__).'/custom_config.php')) { 435 if (file_exists(dirname(__FILE__).'/custom_config.php')) {
382 require_once dirname(__FILE__).'/custom_config.php'; 436 require_once dirname(__FILE__).'/custom_config.php';
383 } 437 }
384 438
385 // check for environment variables - often used on cloud platforms 439 // check for environment variables - often used on cloud platforms
386 // environment variables should be prefixed with 'ftr_', e.g. 440 // environment variables should be prefixed with 'ftr_', e.g.
387 // ftr_max_entries: 1 441 // ftr_max_entries: 1
diff --git a/inc/3rdparty/libraries/PHPePub/EPub.HtmlEntities.php b/inc/3rdparty/libraries/PHPePub/EPub.HtmlEntities.php
new file mode 100644
index 00000000..376b6133
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/EPub.HtmlEntities.php
@@ -0,0 +1,266 @@
1<?php
2/**
3 * This should be a complete list of all HTML entities, mapped to their UTF-8 character codes.
4 *
5 * @author A. Grandt
6 * @copyright A. Grandt 2009-2013
7 * @license GNU LGPL, Attribution required for commercial implementations, requested for everything else.
8 * @version 3.00
9 */
10global $htmlEntities;
11$htmlEntities = array();
12
13$htmlEntities["&quot;"] ="\x22"; // &#34; ((double) quotation mark)
14$htmlEntities["&amp;"] ="\x26"; // &#38; (ampersand)
15$htmlEntities["&apos;"] ="\x27"; // &#39; (apostrophe = apostrophe-quote)
16$htmlEntities["&lt;"] ="\x3C"; // &#60; (less-than sign)
17$htmlEntities["&gt;"] ="\x3E"; // &#62; (greater-than sign)
18$htmlEntities["&nbsp;"] ="\xC2\xA0"; // &#160; (non-breaking space)
19$htmlEntities["&iexcl;"] ="\xC2\xA1"; // &#161; (inverted exclamation mark)
20$htmlEntities["&cent;"] ="\xC2\xA2"; // &#162; (cent)
21$htmlEntities["&pound;"] ="\xC2\xA3"; // &#163; (pound)
22$htmlEntities["&curren;"] ="\xC2\xA4"; // &#164; (currency)
23$htmlEntities["&yen;"] ="\xC2\xA5"; // &#165; (yen)
24$htmlEntities["&brvbar;"] ="\xC2\xA6"; // &#166; (broken vertical bar)
25$htmlEntities["&sect;"] ="\xC2\xA7"; // &#167; (section)
26$htmlEntities["&uml;"] ="\xC2\xA8"; // &#168; (spacing diaeresis)
27$htmlEntities["&copy;"] ="\xC2\xA9"; // &#169; (copyright)
28$htmlEntities["&ordf;"] ="\xC2\xAA"; // &#170; (feminine ordinal indicator)
29$htmlEntities["&laquo;"] ="\xC2\xAB"; // &#171; (angle quotation mark (left))
30$htmlEntities["&not;"] ="\xC2\xAC"; // &#172; (negation)
31$htmlEntities["&shy;"] ="\xC2\xAD"; // &#173; (soft hyphen)
32$htmlEntities["&reg;"] ="\xC2\xAE"; // &#174; (registered trademark)
33$htmlEntities["&macr;"] ="\xC2\xAF"; // &#175; (spacing macron)
34$htmlEntities["&deg;"] ="\xC2\xB0"; // &#176; (degree)
35$htmlEntities["&plusmn;"] ="\xC2\xB1"; // &#177; (plus-or-minus)
36$htmlEntities["&sup2;"] ="\xC2\xB2"; // &#178; (superscript 2)
37$htmlEntities["&sup3;"] ="\xC2\xB3"; // &#179; (superscript 3)
38$htmlEntities["&acute;"] ="\xC2\xB4"; // &#180; (spacing acute)
39$htmlEntities["&micro;"] ="\xC2\xB5"; // &#181; (micro)
40$htmlEntities["&para;"] ="\xC2\xB6"; // &#182; (paragraph)
41$htmlEntities["&middot;"] ="\xC2\xB7"; // &#183; (middle dot)
42$htmlEntities["&cedil;"] ="\xC2\xB8"; // &#184; (spacing cedilla)
43$htmlEntities["&sup1;"] ="\xC2\xB9"; // &#185; (superscript 1)
44$htmlEntities["&ordm;"] ="\xC2\xBA"; // &#186; (masculine ordinal indicator)
45$htmlEntities["&raquo;"] ="\xC2\xBB"; // &#187; (angle quotation mark (right))
46$htmlEntities["&frac14;"] ="\xC2\xBC"; // &#188; (fraction 1/4)
47$htmlEntities["&frac12;"] ="\xC2\xBD"; // &#189; (fraction 1/2)
48$htmlEntities["&frac34;"] ="\xC2\xBE"; // &#190; (fraction 3/4)
49$htmlEntities["&iquest;"] ="\xC2\xBF"; // &#191; (inverted question mark)
50$htmlEntities["&Agrave;"] ="\xC3\x80"; // &#192; (capital a, grave accent)
51$htmlEntities["&Aacute;"] ="\xC3\x81"; // &#193; (capital a, acute accent)
52$htmlEntities["&Acirc;"] ="\xC3\x82"; // &#194; (capital a, circumflex accent)
53$htmlEntities["&Atilde;"] ="\xC3\x83"; // &#195; (capital a, tilde)
54$htmlEntities["&Auml;"] ="\xC3\x84"; // &#196; (capital a, umlaut mark)
55$htmlEntities["&Aring;"] ="\xC3\x85"; // &#197; (capital a, ring)
56$htmlEntities["&AElig;"] ="\xC3\x86"; // &#198; (capital ae)
57$htmlEntities["&Ccedil;"] ="\xC3\x87"; // &#199; (capital c, cedilla)
58$htmlEntities["&Egrave;"] ="\xC3\x88"; // &#200; (capital e, grave accent)
59$htmlEntities["&Eacute;"] ="\xC3\x89"; // &#201; (capital e, acute accent)
60$htmlEntities["&Ecirc;"] ="\xC3\x8A"; // &#202; (capital e, circumflex accent)
61$htmlEntities["&Euml;"] ="\xC3\x8B"; // &#203; (capital e, umlaut mark)
62$htmlEntities["&Igrave;"] ="\xC3\x8C"; // &#204; (capital i, grave accent)
63$htmlEntities["&Iacute;"] ="\xC3\x8D"; // &#205; (capital i, acute accent)
64$htmlEntities["&Icirc;"] ="\xC3\x8E"; // &#206; (capital i, circumflex accent)
65$htmlEntities["&Iuml;"] ="\xC3\x8F"; // &#207; (capital i, umlaut mark)
66$htmlEntities["&ETH;"] ="\xC3\x90"; // &#208; (capital eth, Icelandic)
67$htmlEntities["&Ntilde;"] ="\xC3\x91"; // &#209; (capital n, tilde)
68$htmlEntities["&Ograve;"] ="\xC3\x92"; // &#210; (capital o, grave accent)
69$htmlEntities["&Oacute;"] ="\xC3\x93"; // &#211; (capital o, acute accent)
70$htmlEntities["&Ocirc;"] ="\xC3\x94"; // &#212; (capital o, circumflex accent)
71$htmlEntities["&Otilde;"] ="\xC3\x95"; // &#213; (capital o, tilde)
72$htmlEntities["&Ouml;"] ="\xC3\x96"; // &#214; (capital o, umlaut mark)
73$htmlEntities["&times;"] ="\xC3\x97"; // &#215; (multiplication)
74$htmlEntities["&Oslash;"] ="\xC3\x98"; // &#216; (capital o, slash)
75$htmlEntities["&Ugrave;"] ="\xC3\x99"; // &#217; (capital u, grave accent)
76$htmlEntities["&Uacute;"] ="\xC3\x9A"; // &#218; (capital u, acute accent)
77$htmlEntities["&Ucirc;"] ="\xC3\x9B"; // &#219; (capital u, circumflex accent)
78$htmlEntities["&Uuml;"] ="\xC3\x9C"; // &#220; (capital u, umlaut mark)
79$htmlEntities["&Yacute;"] ="\xC3\x9D"; // &#221; (capital y, acute accent)
80$htmlEntities["&THORN;"] ="\xC3\x9E"; // &#222; (capital THORN, Icelandic)
81$htmlEntities["&szlig;"] ="\xC3\x9F"; // &#223; (small sharp s, German)
82$htmlEntities["&agrave;"] ="\xC3\xA0"; // &#224; (small a, grave accent)
83$htmlEntities["&aacute;"] ="\xC3\xA1"; // &#225; (small a, acute accent)
84$htmlEntities["&acirc;"] ="\xC3\xA2"; // &#226; (small a, circumflex accent)
85$htmlEntities["&atilde;"] ="\xC3\xA3"; // &#227; (small a, tilde)
86$htmlEntities["&auml;"] ="\xC3\xA4"; // &#228; (small a, umlaut mark)
87$htmlEntities["&aring;"] ="\xC3\xA5"; // &#229; (small a, ring)
88$htmlEntities["&aelig;"] ="\xC3\xA6"; // &#230; (small ae)
89$htmlEntities["&ccedil;"] ="\xC3\xA7"; // &#231; (small c, cedilla)
90$htmlEntities["&egrave;"] ="\xC3\xA8"; // &#232; (small e, grave accent)
91$htmlEntities["&eacute;"] ="\xC3\xA9"; // &#233; (small e, acute accent)
92$htmlEntities["&ecirc;"] ="\xC3\xAA"; // &#234; (small e, circumflex accent)
93$htmlEntities["&euml;"] ="\xC3\xAB"; // &#235; (small e, umlaut mark)
94$htmlEntities["&igrave;"] ="\xC3\xAC"; // &#236; (small i, grave accent)
95$htmlEntities["&iacute;"] ="\xC3\xAD"; // &#237; (small i, acute accent)
96$htmlEntities["&icirc;"] ="\xC3\xAE"; // &#238; (small i, circumflex accent)
97$htmlEntities["&iuml;"] ="\xC3\xAF"; // &#239; (small i, umlaut mark)
98$htmlEntities["&eth;"] ="\xC3\xB0"; // &#240; (small eth, Icelandic)
99$htmlEntities["&ntilde;"] ="\xC3\xB1"; // &#241; (small n, tilde)
100$htmlEntities["&ograve;"] ="\xC3\xB2"; // &#242; (small o, grave accent)
101$htmlEntities["&oacute;"] ="\xC3\xB3"; // &#243; (small o, acute accent)
102$htmlEntities["&ocirc;"] ="\xC3\xB4"; // &#244; (small o, circumflex accent)
103$htmlEntities["&otilde;"] ="\xC3\xB5"; // &#245; (small o, tilde)
104$htmlEntities["&ouml;"] ="\xC3\xB6"; // &#246; (small o, umlaut mark)
105$htmlEntities["&divide;"] ="\xC3\xB7"; // &#247; (division)
106$htmlEntities["&oslash;"] ="\xC3\xB8"; // &#248; (small o, slash)
107$htmlEntities["&ugrave;"] ="\xC3\xB9"; // &#249; (small u, grave accent)
108$htmlEntities["&uacute;"] ="\xC3\xBA"; // &#250; (small u, acute accent)
109$htmlEntities["&ucirc;"] ="\xC3\xBB"; // &#251; (small u, circumflex accent)
110$htmlEntities["&uuml;"] ="\xC3\xBC"; // &#252; (small u, umlaut mark)
111$htmlEntities["&yacute;"] ="\xC3\xBD"; // &#253; (small y, acute accent)
112$htmlEntities["&thorn;"] ="\xC3\xBE"; // &#254; (small thorn, Icelandic)
113$htmlEntities["&yuml;"] ="\xC3\xBF"; // &#255; (small y, umlaut mark)
114$htmlEntities["&OElig;"] ="\xC5\x92"; // &#338; (capital ligature OE)
115$htmlEntities["&oelig;"] ="\xC5\x93"; // &#339; (small ligature oe)
116$htmlEntities["&Scaron;"] ="\xC5\xA0"; // &#352; (capital S with caron)
117$htmlEntities["&scaron;"] ="\xC5\xA1"; // &#353; (small S with caron)
118$htmlEntities["&Yuml;"] ="\xC5\xB8"; // &#376; (capital Y with diaeres)
119$htmlEntities["&fnof;"] ="\xC6\x92"; // &#402; (f with hook)
120$htmlEntities["&circ;"] ="\xCB\x86"; // &#710; (modifier letter circumflex accent)
121$htmlEntities["&tilde;"] ="\xCB\x9C"; // &#732; (small tilde)
122$htmlEntities["&Alpha;"] ="\xCE\x91"; // &#913; (Alpha)
123$htmlEntities["&Beta;"] ="\xCE\x92"; // &#914; (Beta)
124$htmlEntities["&Gamma;"] ="\xCE\x93"; // &#915; (Gamma)
125$htmlEntities["&Delta;"] ="\xCE\x94"; // &#916; (Delta)
126$htmlEntities["&Epsilon;"] ="\xCE\x95"; // &#917; (Epsilon)
127$htmlEntities["&Zeta;"] ="\xCE\x96"; // &#918; (Zeta)
128$htmlEntities["&Eta;"] ="\xCE\x97"; // &#919; (Eta)
129$htmlEntities["&Theta;"] ="\xCE\x98"; // &#920; (Theta)
130$htmlEntities["&Iota;"] ="\xCE\x99"; // &#921; (Iota)
131$htmlEntities["&Kappa;"] ="\xCE\x9A"; // &#922; (Kappa)
132$htmlEntities["&Lambda;"] ="\xCE\x9B"; // &#923; (Lambda)
133$htmlEntities["&Mu;"] ="\xCE\x9C"; // &#924; (Mu)
134$htmlEntities["&Nu;"] ="\xCE\x9D"; // &#925; (Nu)
135$htmlEntities["&Xi;"] ="\xCE\x9E"; // &#926; (Xi)
136$htmlEntities["&Omicron;"] ="\xCE\x9F"; // &#927; (Omicron)
137$htmlEntities["&Pi;"] ="\xCE\xA0"; // &#928; (Pi)
138$htmlEntities["&Rho;"] ="\xCE\xA1"; // &#929; (Rho)
139$htmlEntities["&Sigma;"] ="\xCE\xA3"; // &#931; (Sigma)
140$htmlEntities["&Tau;"] ="\xCE\xA4"; // &#932; (Tau)
141$htmlEntities["&Upsilon;"] ="\xCE\xA5"; // &#933; (Upsilon)
142$htmlEntities["&Phi;"] ="\xCE\xA6"; // &#934; (Phi)
143$htmlEntities["&Chi;"] ="\xCE\xA7"; // &#935; (Chi)
144$htmlEntities["&Psi;"] ="\xCE\xA8"; // &#936; (Psi)
145$htmlEntities["&Omega;"] ="\xCE\xA9"; // &#937; (Omega)
146$htmlEntities["&alpha;"] ="\xCE\xB1"; // &#945; (alpha)
147$htmlEntities["&beta;"] ="\xCE\xB2"; // &#946; (beta)
148$htmlEntities["&gamma;"] ="\xCE\xB3"; // &#947; (gamma)
149$htmlEntities["&delta;"] ="\xCE\xB4"; // &#948; (delta)
150$htmlEntities["&epsilon;"] ="\xCE\xB5"; // &#949; (epsilon)
151$htmlEntities["&zeta;"] ="\xCE\xB6"; // &#950; (zeta)
152$htmlEntities["&eta;"] ="\xCE\xB7"; // &#951; (eta)
153$htmlEntities["&theta;"] ="\xCE\xB8"; // &#952; (theta)
154$htmlEntities["&iota;"] ="\xCE\xB9"; // &#953; (iota)
155$htmlEntities["&kappa;"] ="\xCE\xBA"; // &#954; (kappa)
156$htmlEntities["&lambda;"] ="\xCE\xBB"; // &#955; (lambda)
157$htmlEntities["&mu;"] ="\xCE\xBC"; // &#956; (mu)
158$htmlEntities["&nu;"] ="\xCE\xBD"; // &#957; (nu)
159$htmlEntities["&xi;"] ="\xCE\xBE"; // &#958; (xi)
160$htmlEntities["&omicron;"] ="\xCE\xBF"; // &#959; (omicron)
161$htmlEntities["&pi;"] ="\xCF\x80"; // &#960; (pi)
162$htmlEntities["&rho;"] ="\xCF\x81"; // &#961; (rho)
163$htmlEntities["&sigmaf;"] ="\xCF\x82"; // &#962; (sigmaf)
164$htmlEntities["&sigma;"] ="\xCF\x83"; // &#963; (sigma)
165$htmlEntities["&tau;"] ="\xCF\x84"; // &#964; (tau)
166$htmlEntities["&upsilon;"] ="\xCF\x85"; // &#965; (upsilon)
167$htmlEntities["&phi;"] ="\xCF\x86"; // &#966; (phi)
168$htmlEntities["&chi;"] ="\xCF\x87"; // &#967; (chi)
169$htmlEntities["&psi;"] ="\xCF\x88"; // &#968; (psi)
170$htmlEntities["&omega;"] ="\xCF\x89"; // &#969; (omega)
171$htmlEntities["&thetasym;"] ="\xCF\x91"; // &#977; (theta symbol)
172$htmlEntities["&upsih;"] ="\xCF\x92"; // &#978; (upsilon symbol)
173$htmlEntities["&piv;"] ="\xCF\x96"; // &#982; (pi symbol)
174$htmlEntities["&ensp;"] ="\xE2\x80\x82"; // &#8194; (en space)
175$htmlEntities["&emsp;"] ="\xE2\x80\x83"; // &#8195; (em space)
176$htmlEntities["&thinsp;"] ="\xE2\x80\x89"; // &#8201; (thin space)
177$htmlEntities["&zwnj;"] ="‌\xE2\x80\x8C"; // &#8204; (zero width non-joiner)
178$htmlEntities["&zwj;"] ="\xE2\x80\x8D‍"; // &#8205; (zero width joiner)
179$htmlEntities["&lrm;"] ="‎\xE2\x80\x8E"; // &#8206; (left-to-right mark)
180$htmlEntities["&rlm;"] ="\xE2\x80\x8F"; // &#8207; (right-to-left mark)
181$htmlEntities["&ndash;"] ="\xE2\x80\x93"; // &#8211; (en dash)
182$htmlEntities["&mdash;"] ="\xE2\x80\x94"; // &#8212; (em dash)
183$htmlEntities["&lsquo;"] ="\xE2\x80\x98"; // &#8216; (left single quotation mark)
184$htmlEntities["&rsquo;"] ="\xE2\x80\x99"; // &#8217; (right single quotation mark)
185$htmlEntities["&sbquo;"] ="\xE2\x80\x9A"; // &#8218; (single low-9 quotation mark)
186$htmlEntities["&ldquo;"] ="\xE2\x80\x9C"; // &#8220; (left double quotation mark)
187$htmlEntities["&rdquo;"] ="\xE2\x80\x9D"; // &#8221; (right double quotation mark)
188$htmlEntities["&bdquo;"] ="\xE2\x80\x9E"; // &#8222; (double low-9 quotation mark)
189$htmlEntities["&dagger;"] ="\xE2\x80\xA0"; // &#8224; (dagger)
190$htmlEntities["&Dagger;"] ="\xE2\x80\xA1"; // &#8225; (double dagger)
191$htmlEntities["&bull;"] ="\xE2\x80\xA2"; // &#8226; (bullet)
192$htmlEntities["&hellip;"] ="\xE2\x80\xA6"; // &#8230; (horizontal ellipsis)
193$htmlEntities["&permil;"] ="\xE2\x80\xB0"; // &#8240; (per mille)
194$htmlEntities["&prime;"] ="\xE2\x80\xB2"; // &#8242; (minutes or prime)
195$htmlEntities["&Prime;"] ="\xE2\x80\xB3"; // &#8243; (seconds or Double Prime)
196$htmlEntities["&lsaquo;"] ="\xE2\x80\xB9"; // &#8249; (single left angle quotation)
197$htmlEntities["&rsaquo;"] ="\xE2\x80\xBA"; // &#8250; (single right angle quotation)
198$htmlEntities["&oline;"] ="\xE2\x80\xBE"; // &#8254; (overline)
199$htmlEntities["&frasl;"] ="\xE2\x81\x84"; // &#8260; (fraction slash)
200$htmlEntities["&euro;"] ="\xE2\x82\xAC"; // &#8364; (euro)
201$htmlEntities["&image;"] ="\xE2\x84\x91"; // &#8465; (blackletter capital I)
202$htmlEntities["&weierp;"] ="\xE2\x84\x98"; // &#8472; (script capital P)
203$htmlEntities["&real;"] ="\xE2\x84\x9C"; // &#8476; (blackletter capital R)
204$htmlEntities["&trade;"] ="\xE2\x84\xA2"; // &#8482; (trademark)
205$htmlEntities["&alefsym;"] ="\xE2\x84\xB5"; // &#8501; (alef)
206$htmlEntities["&larr;"] ="\xE2\x86\x90"; // &#8592; (left arrow)
207$htmlEntities["&uarr;"] ="\xE2\x86\x91"; // &#8593; (up arrow)
208$htmlEntities["&rarr;"] ="\xE2\x86\x92"; // &#8594; (right arrow)
209$htmlEntities["&darr;"] ="\xE2\x86\x93"; // &#8595; (down arrow)
210$htmlEntities["&harr;"] ="\xE2\x86\x94"; // &#8596; (left right arrow)
211$htmlEntities["&crarr;"] ="\xE2\x86\xB5"; // &#8629; (carriage return arrow)
212$htmlEntities["&lArr;"] ="\xE2\x87\x90"; // &#8656; (left double arrow)
213$htmlEntities["&uArr;"] ="\xE2\x87\x91"; // &#8657; (up double arrow)
214$htmlEntities["&rArr;"] ="\xE2\x87\x92"; // &#8658; (right double arrow)
215$htmlEntities["&dArr;"] ="\xE2\x87\x93"; // &#8659; (down double arrow)
216$htmlEntities["&hArr;"] ="\xE2\x87\x94"; // &#8660; (left right double arrow)
217$htmlEntities["&forall;"] ="\xE2\x88\x80"; // &#8704; (for all)
218$htmlEntities["&part;"] ="\xE2\x88\x82"; // &#8706; (partial differential)
219$htmlEntities["&exist;"] ="\xE2\x88\x83"; // &#8707; (there exists)
220$htmlEntities["&empty;"] ="\xE2\x88\x85"; // &#8709; (empty set)
221$htmlEntities["&nabla;"] ="\xE2\x88\x87"; // &#8711; (backward difference)
222$htmlEntities["&isin;"] ="\xE2\x88\x88"; // &#8712; (element of)
223$htmlEntities["&notin;"] ="\xE2\x88\x89"; // &#8713; (not an element of)
224$htmlEntities["&ni;"] ="\xE2\x88\x8B"; // &#8715; (ni = contains as member)
225$htmlEntities["&prod;"] ="\xE2\x88\x8F"; // &#8719; (n-ary product)
226$htmlEntities["&sum;"] ="\xE2\x88\x91"; // &#8721; (n-ary sumation)
227$htmlEntities["&minus;"] ="\xE2\x88\x92"; // &#8722; (minus)
228$htmlEntities["&lowast;"] ="\xE2\x88\x97"; // &#8727; (asterisk operator)
229$htmlEntities["&radic;"] ="\xE2\x88\x9A"; // &#8730; (square root)
230$htmlEntities["&prop;"] ="\xE2\x88\x9D"; // &#8733; (proportional to)
231$htmlEntities["&infin;"] ="\xE2\x88\x9E"; // &#8734; (infinity)
232$htmlEntities["&ang;"] ="\xE2\x88\xA0"; // &#8736; (angle)
233$htmlEntities["&and;"] ="\xE2\x88\xA7"; // &#8743; (logical and)
234$htmlEntities["&or;"] ="\xE2\x88\xA8"; // &#8744; (logical or)
235$htmlEntities["&cap;"] ="\xE2\x88\xA9"; // &#8745; (intersection)
236$htmlEntities["&cup;"] ="\xE2\x88\xAA"; // &#8746; (union)
237$htmlEntities["&int;"] ="\xE2\x88\xAB"; // &#8747; (integral)
238$htmlEntities["&there4;"] ="\xE2\x88\xB4"; // &#8756; (therefore)
239$htmlEntities["&sim;"] ="\xE2\x88\xBC"; // &#8764; (similar to)
240$htmlEntities["&cong;"] ="\xE2\x89\x85"; // &#8773; (congruent to)
241$htmlEntities["&asymp;"] ="\xE2\x89\x88"; // &#8776; (approximately equal)
242$htmlEntities["&ne;"] ="\xE2\x89\xA0"; // &#8800; (not equal)
243$htmlEntities["&equiv;"] ="\xE2\x89\xA1"; // &#8801; (equivalent)
244$htmlEntities["&le;"] ="\xE2\x89\xA4"; // &#8804; (less or equal)
245$htmlEntities["&ge;"] ="\xE2\x89\xA5"; // &#8805; (greater or equal)
246$htmlEntities["&sub;"] ="\xE2\x8A\x82"; // &#8834; (subset of)
247$htmlEntities["&sup;"] ="\xE2\x8A\x83"; // &#8835; (superset of)
248$htmlEntities["&nsub;"] ="\xE2\x8A\x84"; // &#8836; (not subset of)
249$htmlEntities["&sube;"] ="\xE2\x8A\x86"; // &#8838; (subset or equal)
250$htmlEntities["&supe;"] ="\xE2\x8A\x87"; // &#8839; (superset or equal)
251$htmlEntities["&oplus;"] ="\xE2\x8A\x95"; // &#8853; (circled plus)
252$htmlEntities["&otimes;"] ="\xE2\x8A\x87"; // &#8855; (circled times)
253$htmlEntities["&perp;"] ="\xE2\x8A\xA5"; // &#8869; (perpendicular)
254$htmlEntities["&sdot;"] ="\xE2\x8C\x85"; // &#8901; (dot operator)
255$htmlEntities["&lceil;"] ="\xE2\x8C\x88"; // &#8968; (left ceiling)
256$htmlEntities["&rceil;"] ="\xE2\x8C\x89"; // &#8969; (right ceiling)
257$htmlEntities["&lfloor;"] ="\xE2\x8C\x8A"; // &#8970; (left floor)
258$htmlEntities["&rfloor;"] ="\xE2\x8C\x8B"; // &#8971; (right floor)
259$htmlEntities["&lang;"] ="\xE2\x8C\xA9"; // &#9001; (left angle bracket = bra)
260$htmlEntities["&rang;"] ="\xE2\x8C\xAA"; // &#9002; (right angle bracket = ket)
261$htmlEntities["&loz;"] ="\xE2\x97\x8A"; // &#9674; (lozenge)
262$htmlEntities["&spades;"] ="\xE2\x99\xA0"; // &#9824; (spade)
263$htmlEntities["&clubs;"] ="\xE2\x99\xA3"; // &#9827; (club)
264$htmlEntities["&hearts;"] ="\xE2\x99\xA5"; // &#9829; (heart)
265$htmlEntities["&diams;"] ="\xE2\x99\xA6"; // &#9830; (diamond)
266?> \ No newline at end of file
diff --git a/inc/3rdparty/libraries/PHPePub/EPub.NCX.php b/inc/3rdparty/libraries/PHPePub/EPub.NCX.php
new file mode 100644
index 00000000..e5da05cd
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/EPub.NCX.php
@@ -0,0 +1,782 @@
1<?php
2/**
3 * ePub NCX file structure
4 *
5 * @author A. Grandt <php@grandt.com>
6 * @copyright 2009-2014 A. Grandt
7 * @license GNU LGPL, Attribution required for commercial implementations, requested for everything else.
8 * @version 3.20
9 */
10class Ncx {
11 const _VERSION = 3.20;
12
13 const MIMETYPE = "application/x-dtbncx+xml";
14
15 private $bookVersion = EPub::BOOK_VERSION_EPUB2;
16
17 private $navMap = NULL;
18 private $uid = NULL;
19 private $meta = array();
20 private $docTitle = NULL;
21 private $docAuthor = NULL;
22
23 private $currentLevel = NULL;
24 private $lastLevel = NULL;
25
26 private $languageCode = "en";
27 private $writingDirection = EPub::DIRECTION_LEFT_TO_RIGHT;
28
29 public $chapterList = array();
30 public $referencesTitle = "Guide";
31 public $referencesClass = "references";
32 public $referencesId = "references";
33 public $referencesList = array();
34 public $referencesName = array();
35 public $referencesOrder = NULL;
36
37 /**
38 * Class constructor.
39 *
40 * @param string $uid
41 * @param string $docTitle
42 * @param string $docAuthor
43 * @param string $languageCode
44 * @param string $writingDirection
45 */
46 function __construct($uid = NULL, $docTitle = NULL, $docAuthor = NULL, $languageCode = "en", $writingDirection = EPub::DIRECTION_LEFT_TO_RIGHT) {
47 $this->navMap = new NavMap($writingDirection);
48 $this->currentLevel = $this->navMap;
49 $this->setUid($uid);
50 $this->setDocTitle($docTitle);
51 $this->setDocAuthor($docAuthor);
52 $this->setLanguageCode($languageCode);
53 $this->setWritingDirection($writingDirection);
54 }
55
56 /**
57 * Class destructor
58 *
59 * @return void
60 */
61 function __destruct() {
62 unset($this->bookVersion, $this->navMap, $this->uid, $this->meta);
63 unset($this->docTitle, $this->docAuthor, $this->currentLevel, $this->lastLevel);
64 unset($this->languageCode, $this->writingDirection, $this->chapterList, $this->referencesTitle);
65 unset($this->referencesClass, $this->referencesId, $this->referencesList, $this->referencesName);
66 unset($this->referencesOrder);
67 }
68
69 /**
70 *
71 * Enter description here ...
72 *
73 * @param string $bookVersion
74 */
75 function setVersion($bookVersion) {
76 $this->bookVersion = is_string($bookVersion) ? trim($bookVersion) : EPub::BOOK_VERSION_EPUB2;
77 }
78
79 /**
80 *
81 * @return bool TRUE if the book is set to type ePub 2
82 */
83 function isEPubVersion2() {
84 return $this->bookVersion === EPub::BOOK_VERSION_EPUB2;
85 }
86
87 /**
88 *
89 * Enter description here ...
90 *
91 * @param string $uid
92 */
93 function setUid($uid) {
94 $this->uid = is_string($uid) ? trim($uid) : NULL;
95 }
96
97 /**
98 *
99 * Enter description here ...
100 *
101 * @param string $docTitle
102 */
103 function setDocTitle($docTitle) {
104 $this->docTitle = is_string($docTitle) ? trim($docTitle) : NULL;
105 }
106
107 /**
108 *
109 * Enter description here ...
110 *
111 * @param string $docAuthor
112 */
113 function setDocAuthor($docAuthor) {
114 $this->docAuthor = is_string($docAuthor) ? trim($docAuthor) : NULL;
115 }
116
117 /**
118 *
119 * Enter description here ...
120 *
121 * @param string $languageCode
122 */
123 function setLanguageCode($languageCode) {
124 $this->languageCode = is_string($languageCode) ? trim($languageCode) : "en";
125 }
126
127 /**
128 *
129 * Enter description here ...
130 *
131 * @param string $writingDirection
132 */
133 function setWritingDirection($writingDirection) {
134 $this->writingDirection = is_string($writingDirection) ? trim($writingDirection) : EPub::DIRECTION_LEFT_TO_RIGHT;
135 }
136
137 /**
138 *
139 * Enter description here ...
140 *
141 * @param NavMap $navMap
142 */
143 function setNavMap($navMap) {
144 if ($navMap != NULL && is_object($navMap) && get_class($navMap) === "NavMap") {
145 $this->navMap = $navMap;
146 }
147 }
148
149 /**
150 * Add one chapter level.
151 *
152 * Subsequent chapters will be added to this level.
153 *
154 * @param string $navTitle
155 * @param string $navId
156 * @param string $navClass
157 * @param string $isNavHidden
158 * @param string $writingDirection
159 * @return NavPoint
160 */
161 function subLevel($navTitle = NULL, $navId = NULL, $navClass = NULL, $isNavHidden = FALSE, $writingDirection = NULL) {
162 $navPoint = FALSE;
163 if (isset($navTitle) && isset($navClass)) {
164 $navPoint = new NavPoint($navTitle, NULL, $navId, $navClass, $isNavHidden, $writingDirection);
165 $this->addNavPoint($navPoint);
166 }
167 if ($this->lastLevel !== NULL) {
168 $this->currentLevel = $this->lastLevel;
169 }
170 return $navPoint;
171 }
172
173 /**
174 * Step back one chapter level.
175 *
176 * Subsequent chapters will be added to this chapters parent level.
177 */
178 function backLevel() {
179 $this->lastLevel = $this->currentLevel;
180 $this->currentLevel = $this->currentLevel->getParent();
181 }
182
183 /**
184 * Step back to the root level.
185 *
186 * Subsequent chapters will be added to the rooot NavMap.
187 */
188 function rootLevel() {
189 $this->lastLevel = $this->currentLevel;
190 $this->currentLevel = $this->navMap;
191 }
192
193 /**
194 * Step back to the given level.
195 * Useful for returning to a previous level from deep within the structure.
196 * Values below 2 will have the same effect as rootLevel()
197 *
198 * @param int $newLevel
199 */
200 function setCurrentLevel($newLevel) {
201 if ($newLevel <= 1) {
202 $this->rootLevel();
203 } else {
204 while ($this->currentLevel->getLevel() > $newLevel) {
205 $this->backLevel();
206 }
207 }
208 }
209
210 /**
211 * Get current level count.
212 * The indentation of the current structure point.
213 *
214 * @return current level count;
215 */
216 function getCurrentLevel() {
217 return $this->currentLevel->getLevel();
218 }
219
220 /**
221 * Add child NavPoints to current level.
222 *
223 * @param NavPoint $navPoint
224 */
225 function addNavPoint($navPoint) {
226 $this->lastLevel = $this->currentLevel->addNavPoint($navPoint);
227 }
228
229 /**
230 *
231 * Enter description here ...
232 *
233 * @return NavMap
234 */
235 function getNavMap() {
236 return $this->navMap;
237 }
238
239 /**
240 *
241 * Enter description here ...
242 *
243 * @param string $name
244 * @param string $content
245 */
246 function addMetaEntry($name, $content) {
247 $name = is_string($name) ? trim($name) : NULL;
248 $content = is_string($content) ? trim($content) : NULL;
249
250 if ($name != NULL && $content != NULL) {
251 $this->meta[] = array($name => $content);
252 }
253 }
254
255 /**
256 *
257 * Enter description here ...
258 *
259 * @return string
260 */
261 function finalize() {
262 $nav = $this->navMap->finalize();
263
264 $ncx = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
265 if ($this->isEPubVersion2()) {
266 $ncx .= "<!DOCTYPE ncx PUBLIC \"-//NISO//DTD ncx 2005-1//EN\"\n"
267 . " \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">\n";
268 }
269 $ncx .= "<ncx xmlns=\"http://www.daisy.org/z3986/2005/ncx/\" version=\"2005-1\" xml:lang=\"" . $this->languageCode . "\" dir=\"" . $this->writingDirection . "\">\n"
270 . "\t<head>\n"
271 . "\t\t<meta name=\"dtb:uid\" content=\"" . $this->uid . "\" />\n"
272 . "\t\t<meta name=\"dtb:depth\" content=\"" . $this->navMap->getNavLevels() . "\" />\n"
273 . "\t\t<meta name=\"dtb:totalPageCount\" content=\"0\" />\n"
274 . "\t\t<meta name=\"dtb:maxPageNumber\" content=\"0\" />\n";
275
276 if (sizeof($this->meta)) {
277 foreach ($this->meta as $metaEntry) {
278 list($name, $content) = each($metaEntry);
279 $ncx .= "\t\t<meta name=\"" . $name . "\" content=\"" . $content . "\" />\n";
280 }
281 }
282
283 $ncx .= "\t</head>\n\n\t<docTitle>\n\t\t<text>"
284 . $this->docTitle
285 . "</text>\n\t</docTitle>\n\n\t<docAuthor>\n\t\t<text>"
286 . $this->docAuthor
287 . "</text>\n\t</docAuthor>\n\n"
288 . $nav;
289
290 return $ncx . "</ncx>\n";
291 }
292
293 /**
294 *
295 * @param string $title
296 * @param string $cssFileName
297 * @return string
298 */
299 function finalizeEPub3($title = "Table of Contents", $cssFileName = NULL) {
300 $end = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
301 . "<html xmlns=\"http://www.w3.org/1999/xhtml\"\n"
302 . " xmlns:epub=\"http://www.idpf.org/2007/ops\"\n"
303 . " xml:lang=\"" . $this->languageCode . "\" lang=\"" . $this->languageCode . "\" dir=\"" . $this->writingDirection . "\">\n"
304 . "\t<head>\n"
305 . "\t\t<title>" . $this->docTitle . "</title>\n"
306 . "\t\t<meta http-equiv=\"default-style\" content=\"text/html; charset=utf-8\"/>\n";
307 if ($cssFileName !== NULL) {
308 $end .= "\t\t<link rel=\"stylesheet\" href=\"" . $cssFileName . "\" type=\"text/css\"/>\n";
309 }
310 $end .= "\t</head>\n"
311 . "\t<body epub:type=\"frontmatter toc\">\n"
312 . "\t\t<header>\n"
313 . "\t\t\t<h1>" . $title . "</h1>\n"
314 . "\t\t</header>\n"
315 . $this->navMap->finalizeEPub3()
316 . $this->finalizeEPub3Landmarks()
317 . "\t</body>\n"
318 . "</html>\n";
319
320 return $end;
321 }
322
323 /**
324 * Build the references for the ePub 2 toc.
325 * These are merely reference pages added to the end of the navMap though.
326 *
327 * @return string
328 */
329 function finalizeReferences() {
330 if (isset($this->referencesList) && sizeof($this->referencesList) > 0) {
331 $this->rootLevel();
332 $this->subLevel($this->referencesTitle, $this->referencesId, $this->referencesClass);
333 $refId = 1;
334 while (list($item, $descriptive) = each($this->referencesOrder)) {
335 if (array_key_exists($item, $this->referencesList)) {
336 $name = (empty($this->referencesName[$item]) ? $descriptive : $this->referencesName[$item]);
337 $navPoint = new NavPoint($name, $this->referencesList[$item], "ref-" . $refId++);
338 $this->addNavPoint($navPoint);
339 }
340 }
341 }
342 }
343
344 /**
345 * Build the landmarks for the ePub 3 toc.
346 * @return string
347 */
348 function finalizeEPub3Landmarks() {
349 $lm = "";
350 if (isset($this->referencesList) && sizeof($this->referencesList) > 0) {
351 $lm = "\t\t\t<nav epub:type=\"landmarks\">\n"
352 . "\t\t\t\t<h2"
353 . ($this->writingDirection === EPub::DIRECTION_RIGHT_TO_LEFT ? " dir=\"rtl\"" : "")
354 . ">" . $this->referencesTitle . "</h2>\n"
355 . "\t\t\t\t<ol>\n";
356
357 $li = "";
358 while (list($item, $descriptive) = each($this->referencesOrder)) {
359 if (array_key_exists($item, $this->referencesList)) {
360 $li .= "\t\t\t\t\t<li><a epub:type=\""
361 . $item
362 . "\" href=\"" . $this->referencesList[$item] . "\">"
363 . (empty($this->referencesName[$item]) ? $descriptive : $this->referencesName[$item])
364 . "</a></li>\n";
365 }
366 }
367 if (empty($li)) {
368 return "";
369 }
370
371 $lm .= $li
372 . "\t\t\t\t</ol>\n"
373 . "\t\t\t</nav>\n";
374 }
375 return $lm;
376 }
377}
378
379/**
380 * ePub NavMap class
381 */
382class NavMap {
383 const _VERSION = 3.00;
384
385 private $navPoints = array();
386 private $navLevels = 0;
387 private $writingDirection = NULL;
388
389 /**
390 * Class constructor.
391 *
392 * @return void
393 */
394 function __construct($writingDirection = NULL) {
395 $this->setWritingDirection($writingDirection);
396 }
397
398 /**
399 * Class destructor
400 *
401 * @return void
402 */
403 function __destruct() {
404 unset($this->navPoints, $this->navLevels, $this->writingDirection);
405 }
406
407 /**
408 * Set the writing direction to be used for this NavPoint.
409 *
410 * @param string $writingDirection
411 */
412 function setWritingDirection($writingDirection) {
413 $this->writingDirection = isset($writingDirection) && is_string($writingDirection) ? trim($writingDirection) : NULL;
414 }
415
416 function getWritingDirection() {
417 return $this->writingDirection;
418 }
419
420 /**
421 * Add a navPoint to the root of the NavMap.
422 *
423 * @param NavPoint $navPoint
424 * @return NavMap
425 */
426 function addNavPoint($navPoint) {
427 if ($navPoint != NULL && is_object($navPoint) && get_class($navPoint) === "NavPoint") {
428 $navPoint->setParent($this);
429 if ($navPoint->getWritingDirection() == NULL) {
430 $navPoint->setWritingDirection($this->writingDirection);
431 }
432 $this->navPoints[] = $navPoint;
433 return $navPoint;
434 }
435 return $this;
436 }
437
438 /**
439 * The final max depth for the "dtb:depth" meta attribute
440 * Only available after finalize have been called.
441 *
442 * @return number
443 */
444 function getNavLevels() {
445 return $this->navLevels+1;
446 }
447
448 function getLevel() {
449 return 1;
450 }
451
452 function getParent() {
453 return $this;
454 }
455
456 /**
457 * Finalize the navMap, the final max depth for the "dtb:depth" meta attribute can be retrieved with getNavLevels after finalization
458 *
459 */
460 function finalize() {
461 $playOrder = 0;
462 $this->navLevels = 0;
463
464 $nav = "\t<navMap>\n";
465 if (sizeof($this->navPoints) > 0) {
466 $this->navLevels++;
467 foreach ($this->navPoints as $navPoint) {
468 $retLevel = $navPoint->finalize($nav, $playOrder, 0);
469 if ($retLevel > $this->navLevels) {
470 $this->navLevels = $retLevel;
471 }
472 }
473 }
474 return $nav . "\t</navMap>\n";
475 }
476
477 /**
478 * Finalize the navMap, the final max depth for the "dtb:depth" meta attribute can be retrieved with getNavLevels after finalization
479 *
480 */
481 function finalizeEPub3() {
482 $playOrder = 0;
483 $level = 0;
484 $this->navLevels = 0;
485
486 $nav = "\t\t<nav epub:type=\"toc\" id=\"toc\">\n";
487
488 if (sizeof($this->navPoints) > 0) {
489 $this->navLevels++;
490
491 $nav .= str_repeat("\t", $level) . "\t\t\t<ol epub:type=\"list\">\n";
492 foreach ($this->navPoints as $navPoint) {
493 $retLevel = $navPoint->finalizeEPub3($nav, $playOrder, 0);
494 if ($retLevel > $this->navLevels) {
495 $this->navLevels = $retLevel;
496 }
497 }
498 $nav .= str_repeat("\t", $level) . "\t\t\t</ol>\n";
499 }
500
501 return $nav . "\t\t</nav>\n";
502 }
503}
504
505/**
506 * ePub NavPoint class
507 */
508class NavPoint {
509 const _VERSION = 3.00;
510
511 private $label = NULL;
512 private $contentSrc = NULL;
513 private $id = NULL;
514 private $navClass = NULL;
515 private $isNavHidden = FALSE;
516 private $navPoints = array();
517 private $parent = NULL;
518
519 /**
520 * Class constructor.
521 *
522 * All three attributes are mandatory, though if ID is set to null (default) the value will be generated.
523 *
524 * @param string $label
525 * @param string $contentSrc
526 * @param string $id
527 * @param string $navClass
528 * @param bool $isNavHidden
529 * @param string $writingDirection
530 */
531 function __construct($label, $contentSrc = NULL, $id = NULL, $navClass = NULL, $isNavHidden = FALSE, $writingDirection = NULL) {
532 $this->setLabel($label);
533 $this->setContentSrc($contentSrc);
534 $this->setId($id);
535 $this->setNavClass($navClass);
536 $this->setNavHidden($isNavHidden);
537 $this->setWritingDirection($writingDirection);
538 }
539
540 /**
541 * Class destructor
542 *
543 * @return void
544 */
545 function __destruct() {
546 unset($this->label, $this->contentSrc, $this->id, $this->navClass);
547 unset($this->isNavHidden, $this->navPoints, $this->parent);
548 }
549
550 /**
551 * Set the Text label for the NavPoint.
552 *
553 * The label is mandatory.
554 *
555 * @param string $label
556 */
557 function setLabel($label) {
558 $this->label = is_string($label) ? trim($label) : NULL;
559 }
560
561 /**
562 * Get the Text label for the NavPoint.
563 *
564 * @return string Label
565 */
566 function getLabel() {
567 return $this->label;
568 }
569
570 /**
571 * Set the src reference for the NavPoint.
572 *
573 * The src is mandatory for ePub 2.
574 *
575 * @param string $contentSrc
576 */
577 function setContentSrc($contentSrc) {
578 $this->contentSrc = isset($contentSrc) && is_string($contentSrc) ? trim($contentSrc) : NULL;
579 }
580
581 /**
582 * Get the src reference for the NavPoint.
583 *
584 * @return string content src url.
585 */
586 function getContentSrc() {
587 return $this->contentSrc;
588 }
589 /**
590 * Set the parent for this NavPoint.
591 *
592 * @param NavPoint or NavMap $parent
593 */
594 function setParent($parent) {
595 if ($parent != NULL && is_object($parent) &&
596 (get_class($parent) === "NavPoint" || get_class($parent) === "NavMap") ) {
597 $this->parent = $parent;
598 }
599 }
600
601 /**
602 * Get the parent to this NavPoint.
603 *
604 * @return NavPoint, or NavMap if the parent is the root.
605 */
606 function getParent() {
607 return $this->parent;
608 }
609
610 /**
611 * Get the current level. 1 = document root.
612 *
613 * @return int level
614 */
615 function getLevel() {
616 return $this->parent === NULL ? 1 : $this->parent->getLevel()+1;
617 }
618
619 /**
620 * Set the id for the NavPoint.
621 *
622 * The id must be unique, and is mandatory.
623 *
624 * @param string $id
625 */
626 function setId($id) {
627 $this->id = is_string($id) ? trim($id) : NULL;
628 }
629
630 /**
631 * Set the class to be used for this NavPoint.
632 *
633 * @param string $navClass
634 */
635 function setNavClass($navClass) {
636 $this->navClass = isset($navClass) && is_string($navClass) ? trim($navClass) : NULL;
637 }
638
639 /**
640 * Set the class to be used for this NavPoint.
641 *
642 * @param string $navClass
643 */
644 function setNavHidden($isNavHidden) {
645 $this->isNavHidden = $isNavHidden === TRUE;
646 }
647
648 /**
649 * Set the writing direction to be used for this NavPoint.
650 *
651 * @param string $writingDirection
652 */
653 function setWritingDirection($writingDirection) {
654 $this->writingDirection = isset($writingDirection) && is_string($writingDirection) ? trim($writingDirection) : NULL;
655 }
656
657 function getWritingDirection() {
658 return $this->writingDirection;
659 }
660
661 /**
662 * Add child NavPoints for multi level NavMaps.
663 *
664 * @param NavPoint $navPoint
665 */
666 function addNavPoint($navPoint) {
667 if ($navPoint != NULL && is_object($navPoint) && get_class($navPoint) === "NavPoint") {
668 $navPoint->setParent($this);
669 if ($navPoint->getWritingDirection() == NULL) {
670 $navPoint->setWritingDirection($this->writingDirection);
671 }
672 $this->navPoints[] = $navPoint;
673 return $navPoint;
674 }
675 return $this;
676 }
677
678 /**
679 *
680 * Enter description here ...
681 *
682 * @param string $nav
683 * @param int $playOrder
684 * @param int $level
685 * @return int
686 */
687 function finalize(&$nav = "", &$playOrder = 0, $level = 0) {
688 $maxLevel = $level;
689 $levelAdjust = 0;
690
691 if ($this->isNavHidden) {
692 return $maxLevel;
693 }
694
695 if (isset($this->contentSrc)) {
696 $playOrder++;
697
698 if ($this->id == NULL) {
699 $this->id = "navpoint-" . $playOrder;
700 }
701 $nav .= str_repeat("\t", $level) . "\t\t<navPoint id=\"" . $this->id . "\" playOrder=\"" . $playOrder . "\">\n"
702 . str_repeat("\t", $level) . "\t\t\t<navLabel>\n"
703 . str_repeat("\t", $level) . "\t\t\t\t<text>" . $this->label . "</text>\n"
704 . str_repeat("\t", $level) . "\t\t\t</navLabel>\n"
705 . str_repeat("\t", $level) . "\t\t\t<content src=\"" . $this->contentSrc . "\" />\n";
706 } else {
707 $levelAdjust++;
708 }
709
710 if (sizeof($this->navPoints) > 0) {
711 $maxLevel++;
712 foreach ($this->navPoints as $navPoint) {
713 $retLevel = $navPoint->finalize($nav, $playOrder, ($level+1+$levelAdjust));
714 if ($retLevel > $maxLevel) {
715 $maxLevel = $retLevel;
716 }
717 }
718 }
719
720 if (isset($this->contentSrc)) {
721 $nav .= str_repeat("\t", $level) . "\t\t</navPoint>\n";
722 }
723
724 return $maxLevel;
725 }
726
727 /**
728 *
729 * Enter description here ...
730 *
731 * @param string $nav
732 * @param int $playOrder
733 * @param int $level
734 * @return int
735 */
736 function finalizeEPub3(&$nav = "", &$playOrder = 0, $level = 0, $subLevelClass = NULL, $subLevelHidden = FALSE) {
737 $maxLevel = $level;
738
739 if ($this->id == NULL) {
740 $this->id = "navpoint-" . $playOrder;
741 }
742 $indent = str_repeat("\t", $level) . "\t\t\t\t";
743
744 $nav .= $indent . "<li id=\"" . $this->id . "\"";
745 if (isset($this->writingDirection)) {
746 $nav .= " dir=\"" . $this->writingDirection . "\"";
747 }
748 $nav .= ">\n";
749
750 if (isset($this->contentSrc)) {
751 $nav .= $indent . "\t<a href=\"" . $this->contentSrc . "\">" . $this->label . "</a>\n";
752 } else {
753 $nav .= $indent . "\t<span>" . $this->label . "</span>\n";
754 }
755
756 if (sizeof($this->navPoints) > 0) {
757 $maxLevel++;
758
759 $nav .= $indent . "\t<ol epub:type=\"list\"";
760 if (isset($subLevelClass)) {
761 $nav .= " class=\"" . $subLevelClass . "\"";
762 }
763 if ($subLevelHidden) {
764 $nav .= " hidden=\"hidden\"";
765 }
766 $nav .= ">\n";
767
768 foreach ($this->navPoints as $navPoint) {
769 $retLevel = $navPoint->finalizeEPub3($nav, $playOrder, ($level+2), $subLevelClass, $subLevelHidden);
770 if ($retLevel > $maxLevel) {
771 $maxLevel = $retLevel;
772 }
773 }
774 $nav .= $indent . "\t</ol>\n";
775 }
776
777 $nav .= $indent . "</li>\n";
778
779 return $maxLevel;
780 }
781}
782?> \ No newline at end of file
diff --git a/inc/3rdparty/libraries/PHPePub/EPub.OPF.php b/inc/3rdparty/libraries/PHPePub/EPub.OPF.php
new file mode 100644
index 00000000..803a2108
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/EPub.OPF.php
@@ -0,0 +1,1226 @@
1<?php
2/**
3 * ePub OPF file structure
4 *
5 * @author A. Grandt <php@grandt.com>
6 * @copyright 2009-2014 A. Grandt
7 * @license GNU LGPL, Attribution required for commercial implementations, requested for everything else.
8 * @version 3.20
9 */
10class Opf {
11 const _VERSION = 3.20;
12
13 /* Core Media types.
14 * These types are the only guaranteed mime types any ePub reader must understand.
15 * Any other type muse define a fall back whose fallback chain will end in one of these.
16 */
17 const TYPE_GIF = "image/gif";
18 const TYPE_JPEG = "image/jpeg";
19 const TYPE_PNG = "image/png";
20 const TYPE_SVG = "image/svg+xml";
21 const TYPE_XHTML = "application/xhtml+xml";
22 const TYPE_DTBOOK = "application/x-dtbook+xml";
23 const TYPE_CSS = "text/css";
24 const TYPE_XML = "application/xml";
25 const TYPE_OEB1_DOC = "text/x-oeb1-document"; // Deprecated
26 const TYPE_OEB1_CSS = "text/x-oeb1-css"; // Deprecated
27 const TYPE_NCX = "application/x-dtbncx+xml";
28
29 private $bookVersion = EPub::BOOK_VERSION_EPUB2;
30 private $ident = "BookId";
31
32 public $date = NULL;
33 public $metadata = NULL;
34 public $manifest = NULL;
35 public $spine = NULL;
36 public $guide = NULL;
37
38 /**
39 * Class constructor.
40 *
41 * @return void
42 */
43 function __construct($ident = "BookId", $bookVersion = EPub::BOOK_VERSION_EPUB2) {
44 $this->setIdent($ident);
45 $this->setVersion($bookVersion);
46 $this->metadata = new Metadata();
47 $this->manifest = new Manifest();
48 $this->spine = new Spine();
49 $this->guide = new Guide();
50 }
51
52 /**
53 * Class destructor
54 *
55 * @return void
56 */
57 function __destruct() {
58 unset ($this->bookVersion, $this->ident, $this->date, $this->metadata, $this->manifest, $this->spine, $this->guide);
59 }
60
61 /**
62 *
63 * Enter description here ...
64 *
65 * @param string $ident
66 */
67 function setVersion($bookVersion) {
68 $this->bookVersion = is_string($bookVersion) ? trim($bookVersion) : EPub::BOOK_VERSION_EPUB2;
69 }
70
71 function isEPubVersion2() {
72 return $this->bookVersion === EPub::BOOK_VERSION_EPUB2;
73 }
74
75 /**
76 *
77 * Enter description here ...
78 *
79 * @param string $ident
80 */
81 function setIdent($ident = "BookId") {
82 $this->ident = is_string($ident) ? trim($ident) : "BookId";
83 }
84
85 /**
86 *
87 * Enter description here ...
88 *
89 * @return string
90 */
91 function finalize() {
92 $opf = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
93 . "<package xmlns=\"http://www.idpf.org/2007/opf\" unique-identifier=\"" . $this->ident . "\" version=\"" . $this->bookVersion . "\">\n";
94
95 $opf .= $this->metadata->finalize($this->bookVersion, $this->date);
96 $opf .= $this->manifest->finalize($this->bookVersion);
97 $opf .= $this->spine->finalize();
98
99 if ($this->guide->length() > 0) {
100 $opf .= $this->guide->finalize();
101 }
102
103 return $opf . "</package>\n";
104 }
105
106 // Convenience functions:
107
108 /**
109 *
110 * Enter description here ...
111 *
112 * @param string $title
113 * @param string $language
114 * @param string $identifier
115 * @param string $identifierScheme
116 */
117 function initialize($title, $language, $identifier, $identifierScheme) {
118 $this->metadata->addDublinCore(new DublinCore("title", $title));
119 $this->metadata->addDublinCore(new DublinCore("language", $language));
120
121 $dc = new DublinCore("identifier", $identifier);
122 $dc->addAttr("id", $this->ident);
123 $dc->addOpfAttr("scheme", $identifierScheme);
124 $this->metadata->addDublinCore($dc);
125 }
126
127 /**
128 *
129 * Enter description here ...
130 *
131 * @param string $id
132 * @param string $href
133 * @param string $mediaType
134 */
135 function addItem($id, $href, $mediaType, $properties = NULL) {
136 $this->manifest->addItem(new Item($id, $href, $mediaType, $properties));
137 }
138
139 /**
140 *
141 * Enter description here ...
142 *
143 * @param string $idref
144 * @param bool $linear
145 */
146 function addItemRef($idref, $linear = TRUE) {
147 $this->spine->addItemref(new Itemref($idref, $linear));
148 }
149
150 /**
151 *
152 * Enter description here ...
153 *
154 * @param string $type
155 * @param string $title
156 * @param string $href
157 */
158 function addReference($type, $title, $href) {
159 $this->guide->addReference(new Reference($type, $title, $href));
160 }
161
162 /**
163 *
164 * Enter description here ...
165 *
166 * @param string $name
167 * @param string $value
168 */
169 function addDCMeta($name, $value) {
170 $this->metadata->addDublinCore(new DublinCore($name, $value));
171 }
172
173 /**
174 *
175 * Enter description here ...
176 *
177 * @param string $name
178 * @param string $content
179 */
180 function addMeta($name, $content) {
181 $this->metadata->addMeta($name, $content);
182 }
183
184 /**
185 *
186 * Enter description here ...
187 *
188 * @param string $name
189 * @param string $fileAs
190 * @param string $role Use the MarcCode constants
191 */
192 function addCreator($name, $fileAs = NULL, $role = NULL) {
193 $dc = new DublinCore(DublinCore::CREATOR, trim($name));
194
195 if ($fileAs !== NULL) {
196 $dc->addOpfAttr("file-as", trim($fileAs));
197 }
198
199 if ($role !== NULL) {
200 $dc->addOpfAttr("role", trim($role));
201 }
202
203 $this->metadata->addDublinCore($dc);
204 }
205
206 /**
207 *
208 * Enter description here ...
209 *
210 * @param string $name
211 * @param string $fileAs
212 * @param string $role Use the MarcCode constants
213 */
214 function addColaborator($name, $fileAs = NULL, $role = NULL) {
215 $dc = new DublinCore(DublinCore::CONTRIBUTOR, trim($name));
216
217 if ($fileAs !== NULL) {
218 $dc->addOpfAttr("file-as", trim($fileAs));
219 }
220
221 if ($role !== NULL) {
222 $dc->addOpfAttr("role", trim($role));
223 }
224
225 $this->metadata->addDublinCore($dc);
226 }
227}
228
229/**
230 * ePub OPF Metadata structures
231 */
232class Metadata {
233 const _VERSION = 3.00;
234
235 private $dc = array();
236 private $meta = array();
237
238 /**
239 * Class constructor.
240 *
241 * @return void
242 */
243 function __construct() {
244 }
245
246 /**
247 * Class destructor
248 *
249 * @return void
250 */
251 function __destruct() {
252 unset ($this->dc, $this->meta);
253 }
254
255 /**
256 *
257 * Enter description here ...
258 *
259 * @param DublinCore $dc
260 */
261 function addDublinCore($dc) {
262 if ($dc != NULL && is_object($dc) && get_class($dc) === "DublinCore") {
263 $this->dc[] = $dc;
264 }
265 }
266
267 /**
268 *
269 * Enter description here ...
270 *
271 * @param string $name
272 * @param string $content
273 */
274 function addMeta($name, $content) {
275 $name = is_string($name) ? trim($name) : NULL;
276 if (isset($name)) {
277 $content = is_string($content) ? trim($content) : NULL;
278 }
279 if (isset($content)) {
280 $this->meta[] = array ($name => $content);
281 }
282 }
283
284 /**
285 *
286 * @param string $bookVersion
287 * @param int $date
288 * @return string
289 */
290 function finalize($bookVersion = EPub::BOOK_VERSION_EPUB2, $date = NULL) {
291 $metadata = "\t<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n";
292 if ($bookVersion === EPub::BOOK_VERSION_EPUB2) {
293 $metadata .= "\t\txmlns:opf=\"http://www.idpf.org/2007/opf\"\n\t\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n";
294 } else {
295 $metadata .= "\t\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n";
296 if (!isset($date)) {
297 $date = time();
298 }
299 $metadata .= "\t\t<meta property=\"dcterms:modified\">" . gmdate("Y-m-d\TH:i:s\Z", $date) . "</meta>\n";
300 }
301
302 foreach ($this->dc as $dc) {
303 $metadata .= $dc->finalize($bookVersion);
304 }
305
306 foreach ($this->meta as $data) {
307 list($name, $content) = each($data);
308 $metadata .= "\t\t<meta name=\"" . $name . "\" content=\"" . $content . "\" />\n";
309 }
310
311 return $metadata . "\t</metadata>\n";
312 }
313}
314
315/**
316 * ePub OPF Dublin Core (dc:) Metadata structures
317 */
318class DublinCore {
319 const _VERSION = 3.00;
320
321 const CONTRIBUTOR = "contributor";
322 const COVERAGE = "coverage";
323 const CREATOR = "creator";
324 const DATE = "date";
325 const DESCRIPTION = "description";
326 const FORMAT = "format";
327 const IDENTIFIER = "identifier";
328 const LANGUAGE = "language";
329 const PUBLISHER = "publisher";
330 const RELATION = "relation";
331 const RIGHTS = "rights";
332 const SOURCE = "source";
333 const SUBJECT = "subject";
334 const TITLE = "title";
335 const TYPE = "type";
336
337 private $dcName = NULL;
338 private $dcValue = NULL;
339 private $attr = array();
340 private $opfAttr = array();
341
342 /**
343 * Class constructor.
344 *
345 * @return void
346 */
347 function __construct($name, $value) {
348 $this->setDc($name, $value);
349 }
350
351 /**
352 * Class destructor
353 *
354 * @return void
355 */
356 function __destruct() {
357 unset ($this->dcName, $this->dcValue, $this->attr, $this->opfAttr);
358 }
359
360 /**
361 *
362 * Enter description here ...
363 *
364 * @param string $name
365 * @param string $value
366 */
367 function setDc($name, $value) {
368 $this->dcName = is_string($name) ? trim($name) : NULL;
369 if (isset($this->dcName)) {
370 $this->dcValue = isset($value) ? (string)$value : NULL;
371 }
372 if (! isset($this->dcValue)) {
373 $this->dcName = NULL;
374 }
375 }
376
377 /**
378 *
379 * Enter description here ...
380 *
381 * @param string $attrName
382 * @param string $attrValue
383 */
384 function addAttr($attrName, $attrValue) {
385 $attrName = is_string($attrName) ? trim($attrName) : NULL;
386 if (isset($attrName)) {
387 $attrValue = is_string($attrValue) ? trim($attrValue) : NULL;
388 }
389 if (isset($attrValue)) {
390 $this->attr[$attrName] = $attrValue;
391 }
392 }
393
394 /**
395 *
396 * Enter description here ...
397 *
398 * @param string $opfAttrName
399 * @param string $opfAttrValue
400 */
401 function addOpfAttr($opfAttrName, $opfAttrValue) {
402 $opfAttrName = is_string($opfAttrName) ? trim($opfAttrName) : NULL;
403 if (isset($opfAttrName)) {
404 $opfAttrValue = is_string($opfAttrValue) ? trim($opfAttrValue) : NULL;
405 }
406 if (isset($opfAttrValue)) {
407 $this->opfAttr[$opfAttrName] = $opfAttrValue;
408 }
409 }
410
411
412 /**
413 *
414 * @param string $bookVersion
415 * @return string
416 */
417 function finalize($bookVersion = EPub::BOOK_VERSION_EPUB2) {
418 $dc = "\t\t<dc:" . $this->dcName;
419
420 if (sizeof($this->attr) > 0) {
421 while (list($name, $content) = each($this->attr)) {
422 $dc .= " " . $name . "=\"" . $content . "\"";
423 }
424 }
425
426 if ($bookVersion === EPub::BOOK_VERSION_EPUB2 && sizeof($this->opfAttr) > 0) {
427 while (list($name, $content) = each($this->opfAttr)) {
428 $dc .= " opf:" . $name . "=\"" . $content . "\"";
429 }
430 }
431
432 return $dc . ">" . $this->dcValue . "</dc:" . $this->dcName . ">\n";
433 }
434}
435
436/**
437 * ePub OPF Manifest structure
438 */
439class Manifest {
440 const _VERSION = 3.00;
441
442 private $items = array();
443
444 /**
445 * Class constructor.
446 *
447 * @return void
448 */
449 function __construct() {
450 }
451
452 /**
453 * Class destructor
454 *
455 * @return void
456 */
457 function __destruct() {
458 unset ($this->items);
459 }
460
461 /**
462 *
463 * Enter description here ...
464 *
465 * @param Item $item
466 */
467 function addItem($item) {
468 if ($item != NULL && is_object($item) && get_class($item) === "Item") {
469 $this->items[] = $item;
470 }
471 }
472
473 /**
474 *
475 * @param string $bookVersion
476 * @return string
477 */
478 function finalize($bookVersion = EPub::BOOK_VERSION_EPUB2) {
479 $manifest = "\n\t<manifest>\n";
480 foreach ($this->items as $item) {
481 $manifest .= $item->finalize($bookVersion);
482 }
483 return $manifest . "\t</manifest>\n";
484 }
485}
486
487/**
488 * ePub OPF Item structure
489 */
490class Item {
491 const _VERSION = 3.00;
492
493 private $id = NULL;
494 private $href = NULL;
495 private $mediaType = NULL;
496 private $properties = NULL;
497 private $requiredNamespace = NULL;
498 private $requiredModules = NULL;
499 private $fallback = NULL;
500 private $fallbackStyle = NULL;
501
502 /**
503 * Class constructor.
504 *
505 * @return void
506 */
507 function __construct($id, $href, $mediaType, $properties = NULL) {
508 $this->setId($id);
509 $this->setHref($href);
510 $this->setMediaType($mediaType);
511 $this->setProperties($properties);
512 }
513
514 /**
515 * Class destructor
516 *
517 * @return void
518 */
519 function __destruct() {
520 unset ($this->id, $this->href, $this->mediaType);
521 unset ($this->properties, $this->requiredNamespace, $this->requiredModules, $this->fallback, $this->fallbackStyle);
522 }
523
524 /**
525 *
526 * Enter description here ...
527 *
528 * @param string $id
529 */
530 function setId($id) {
531 $this->id = is_string($id) ? trim($id) : NULL;
532 }
533
534 /**
535 *
536 * Enter description here ...
537 *
538 * @param string $href
539 */
540 function setHref($href) {
541 $this->href = is_string($href) ? trim($href) : NULL;
542 }
543
544 /**
545 *
546 * Enter description here ...
547 *
548 * @param string $mediaType
549 */
550 function setMediaType($mediaType) {
551 $this->mediaType = is_string($mediaType) ? trim($mediaType) : NULL;
552 }
553
554 /**
555 *
556 * Enter description here ...
557 *
558 * @param string $properties
559 */
560 function setProperties($properties) {
561 $this->properties = is_string($properties) ? trim($properties) : NULL;
562 }
563
564 /**
565 *
566 * Enter description here ...
567 *
568 * @param string $requiredNamespace
569 */
570 function setRequiredNamespace($requiredNamespace) {
571 $this->requiredNamespace = is_string($requiredNamespace) ? trim($requiredNamespace) : NULL;
572 }
573
574 /**
575 *
576 * Enter description here ...
577 *
578 * @param string $requiredModules
579 */
580 function setRequiredModules($requiredModules) {
581 $this->requiredModules = is_string($requiredModules) ? trim($requiredModules) : NULL;
582 }
583
584 /**
585 *
586 * Enter description here ...
587 *
588 * @param string $fallback
589 */
590 function setfallback($fallback) {
591 $this->fallback = is_string($fallback) ? trim($fallback) : NULL;
592 }
593
594 /**
595 *
596 * Enter description here ...
597 *
598 * @param string $fallbackStyle
599 */
600 function setFallbackStyle($fallbackStyle) {
601 $this->fallbackStyle = is_string($fallbackStyle) ? trim($fallbackStyle) : NULL;
602 }
603
604 /**
605 *
606 * @param string $bookVersion
607 * @return string
608 */
609 function finalize($bookVersion = EPub::BOOK_VERSION_EPUB2) {
610 $item = "\t\t<item id=\"" . $this->id . "\" href=\"" . $this->href . "\" media-type=\"" . $this->mediaType . "\" ";
611 if ($bookVersion === EPub::BOOK_VERSION_EPUB3 && isset($this->properties)) {
612 $item .= "properties=\"" . $this->properties . "\" ";
613 }
614 if (isset($this->requiredNamespace)) {
615 $item .= "\n\t\t\trequired-namespace=\"" . $this->requiredNamespace . "\" ";
616 if (isset($this->requiredModules)) {
617 $item .= "required-modules=\"" . $this->requiredModules . "\" ";
618 }
619 }
620 if (isset($this->fallback)) {
621 $item .= "\n\t\t\tfallback=\"" . $this->fallback . "\" ";
622 }
623 if (isset($this->fallbackStyle)) {
624 $item .= "\n\t\t\tfallback-style=\"" . $this->fallbackStyle . "\" ";
625 }
626 return $item . "/>\n";
627 }
628}
629
630/**
631 * ePub OPF Spine structure
632 */
633class Spine {
634 const _VERSION = 1.00;
635
636 private $itemrefs = array();
637 private $toc = NULL;
638
639 /**
640 * Class constructor.
641 *
642 * @return void
643 */
644 function __construct($toc = "ncx") {
645 $this->setToc($toc);
646 }
647
648 /**
649 * Class destructor
650 *
651 * @return void
652 */
653 function __destruct() {
654 unset ($this->itemrefs, $this->toc);
655 }
656
657 /**
658 *
659 * Enter description here ...
660 *
661 * @param string $toc
662 */
663 function setToc($toc) {
664 $this->toc = is_string($toc) ? trim($toc) : NULL;
665 }
666
667 /**
668 *
669 * Enter description here ...
670 *
671 * @param Itemref $itemref
672 */
673 function addItemref($itemref) {
674 if ($itemref != NULL
675 && is_object($itemref)
676 && get_class($itemref) === "Itemref"
677 && !isset($this->itemrefs[$itemref->getIdref()])) {
678 $this->itemrefs[$itemref->getIdref()] = $itemref;
679 }
680 }
681
682 /**
683 *
684 * Enter description here ...
685 *
686 * @return string
687 */
688 function finalize() {
689 $spine = "\n\t<spine toc=\"" . $this->toc . "\">\n";
690 foreach ($this->itemrefs as $itemref) {
691 $spine .= $itemref->finalize();
692 }
693 return $spine . "\t</spine>\n";
694 }
695}
696
697/**
698 * ePub OPF ItemRef structure
699 */
700class Itemref {
701 const _VERSION = 3.00;
702
703 private $idref = NULL;
704 private $linear = TRUE;
705
706 /**
707 * Class constructor.
708 *
709 * @return void
710 */
711 function __construct($idref, $linear = TRUE) {
712 $this->setIdref($idref);
713 $this->setLinear($linear);
714 }
715
716 /**
717 * Class destructor
718 *
719 * @return void
720 */
721 function __destruct() {
722 unset ($this->idref, $this->linear);
723 }
724
725 /**
726 *
727 * Enter description here ...
728 *
729 * @param string $idref
730 */
731 function setIdref($idref) {
732 $this->idref = is_string($idref) ? trim($idref) : NULL;
733 }
734
735 /**
736 *
737 * Enter description here ...
738 *
739 * @return string $idref
740 */
741 function getIdref() {
742 return $this->idref;
743 }
744
745 /**
746 *
747 * Enter description here ...
748 *
749 * @param bool $linear
750 */
751 function setLinear($linear = TRUE) {
752 $this->linear = $linear === TRUE;
753 }
754
755 /**
756 *
757 * Enter description here ...
758 *
759 * @return string
760 */
761 function finalize() {
762 $itemref = "\t\t<itemref idref=\"" . $this->idref . "\"";
763 if ($this->linear == FALSE) {
764 return $itemref .= " linear=\"no\" />\n";
765 }
766 return $itemref . " />\n";
767 }
768}
769
770/**
771 * ePub OPF Guide structure
772 */
773class Guide {
774 const _VERSION = 3.00;
775
776 private $references = array();
777
778 /**
779 * Class constructor.
780 *
781 * @return void
782 */
783 function __construct() {
784 }
785
786 /**
787 * Class destructor
788 *
789 * @return void
790 */
791 function __destruct() {
792 unset ($this->references);
793 }
794
795 /**
796 *
797 * Enter description here ...
798 *
799 */
800 function length() {
801 return sizeof($this->references);
802 }
803
804 /**
805 *
806 * Enter description here ...
807 *
808 * @param Reference $reference
809 */
810 function addReference($reference) {
811 if ($reference != NULL && is_object($reference) && get_class($reference) === "Reference") {
812 $this->references[] = $reference;
813 }
814 }
815
816 /**
817 *
818 * Enter description here ...
819 *
820 * @return string
821 */
822 function finalize() {
823 $ref = "";
824 if (sizeof($this->references) > 0) {
825 $ref = "\n\t<guide>\n";
826 foreach ($this->references as $reference) {
827 $ref .= $reference->finalize();
828 }
829 $ref .= "\t</guide>\n";
830 }
831 return $ref;
832 }
833}
834
835/**
836 * Reference constants
837 */
838class Reference {
839 const _VERSION = 1.00;
840
841 /* REFERENCE types are derived from the "Chicago Manual of Style"
842 */
843
844 /** Acknowledgements page */
845 const ACKNOWLEDGEMENTS = "acknowledgements";
846
847 /** Bibliography page */
848 const BIBLIOGRAPHY = "bibliography";
849
850 /** Colophon page */
851 const COLOPHON = "colophon";
852
853 /** Copyright page */
854 const COPYRIGHT_PAGE = "copyright-page";
855
856 /** Dedication */
857 const DEDICATION = "dedication";
858
859 /** Epigraph */
860 const EPIGRAPH = "epigraph";
861
862 /** Foreword */
863 const FOREWORD = "foreword";
864
865 /** Glossary page */
866 const GLOSSARY = "glossary";
867
868 /** back-of-book style index */
869 const INDEX = "index";
870
871 /** List of illustrations */
872 const LIST_OF_ILLUSTRATIONS = "loi";
873
874 /** List of tables */
875 const LIST_OF_TABLES = "lot";
876
877 /** Notes page */
878 const NOTES = "notes";
879
880 /** Preface page */
881 const PREFACE = "preface";
882
883 /** Table of contents */
884 const TABLE_OF_CONTENTS = "toc";
885
886 /** Page with possibly title, author, publisher, and other metadata */
887 const TITLE_PAGE = "titlepage";
888
889 /** First page of the book, ie. first page of the first chapter */
890 const TEXT = "text";
891
892 // ******************
893 // ePub3 constants
894 // ******************
895
896 // Document partitions
897 /** The publications cover(s), jacket information, etc. This is officially in ePub3, but works for ePub 2 as well */
898 const COVER = "cover";
899
900 /** Preliminary material to the content body, such as tables of contents, dedications, etc. */
901 const FRONTMATTER = "frontmatter";
902
903 /** The main (body) content of a document. */
904 const BODYMATTER = "bodymatter";
905
906 /** Ancillary material occurring after the document body, such as indices, appendices, etc. */
907 const BACKMATTER = "backmatter";
908
909
910 private $type = NULL;
911 private $title = NULL;
912 private $href = NULL;
913
914 /**
915 * Class constructor.
916 *
917 * @param string $type
918 * @param string $title
919 * @param string $href
920 */
921 function __construct($type, $title, $href) {
922 $this->setType($type);
923 $this->setTitle($title);
924 $this->setHref($href);
925 }
926
927 /**
928 * Class destructor
929 *
930 * @return void
931 */
932 function __destruct() {
933 unset ($this->type, $this->title, $this->href);
934 }
935
936 /**
937 *
938 * Enter description here ...
939 *
940 * @param string $type
941 */
942 function setType($type) {
943 $this->type = is_string($type) ? trim($type) : NULL;
944 }
945
946 /**
947 *
948 * Enter description here ...
949 *
950 * @param string $title
951 */
952 function setTitle($title) {
953 $this->title = is_string($title) ? trim($title) : NULL;
954 }
955
956 /**
957 *
958 * Enter description here ...
959 *
960 * @param string $href
961 */
962 function setHref($href) {
963 $this->href = is_string($href) ? trim($href) : NULL;
964 }
965
966 /**
967 *
968 * Enter description here ...
969 *
970 * @return string
971 */
972 function finalize() {
973 return "\t\t<reference type=\"" . $this->type . "\" title=\"" . $this->title . "\" href=\"" . $this->href . "\" />\n";
974 }
975}
976
977/**
978 * Common Marc codes.
979 * Ref: http://www.loc.gov/marc/relators/
980 */
981class MarcCode {
982 const _VERSION = 3.00;
983
984 /**
985 * Adapter
986 *
987 * Use for a person who
988 * 1) reworks a musical composition, usually for a different medium, or
989 * 2) rewrites novels or stories for motion pictures or other audiovisual medium.
990 */
991 const ADAPTER = "adp";
992
993 /**
994 * Annotator
995 *
996 * Use for a person who writes manuscript annotations on a printed item.
997 */
998 const ANNOTATOR = "ann";
999
1000 /**
1001 * Arranger
1002 *
1003 * Use for a person who transcribes a musical composition, usually for a different
1004 * medium from that of the original; in an arrangement the musical substance remains
1005 * essentially unchanged.
1006 */
1007 const ARRANGER = "arr";
1008
1009 /**
1010 * Artist
1011 *
1012 * Use for a person (e.g., a painter) who conceives, and perhaps also implements,
1013 * an original graphic design or work of art, if specific codes (e.g., [egr],
1014 * [etr]) are not desired. For book illustrators, prefer Illustrator [ill].
1015 */
1016 const ARTIST = "art";
1017
1018 /**
1019 * Associated name
1020 *
1021 * Use as a general relator for a name associated with or found in an item or
1022 * collection, or which cannot be determined to be that of a Former owner [fmo]
1023 * or other designated relator indicative of provenance.
1024 */
1025 const ASSOCIATED_NAME = "asn";
1026
1027 /**
1028 * Author
1029 *
1030 * Use for a person or corporate body chiefly responsible for the intellectual
1031 * or artistic content of a work. This term may also be used when more than one
1032 * person or body bears such responsibility.
1033 */
1034 const AUTHOR = "aut";
1035
1036 /**
1037 * Author in quotations or text extracts
1038 *
1039 * Use for a person whose work is largely quoted or extracted in a works to which
1040 * he or she did not contribute directly. Such quotations are found particularly
1041 * in exhibition catalogs, collections of photographs, etc.
1042 */
1043 const AUTHOR_IN_QUOTES = "aqt";
1044
1045 /**
1046 * Author of afterword, colophon, etc.
1047 *
1048 * Use for a person or corporate body responsible for an afterword, postface,
1049 * colophon, etc. but who is not the chief author of a work.
1050 */
1051 const AUTHOR_OF_AFTERWORD = "aft";
1052
1053 /**
1054 * Author of introduction, etc.
1055 *
1056 * Use for a person or corporate body responsible for an introduction, preface,
1057 * foreword, or other critical matter, but who is not the chief author.
1058 */
1059 const AUTHOR_OF_INTRO = "aui";
1060
1061 /**
1062 * Bibliographic antecedent
1063 *
1064 * Use for the author responsible for a work upon which the work represented by
1065 * the catalog record is based. This can be appropriate for adaptations, sequels,
1066 * continuations, indexes, etc.
1067 */
1068 const BIB_ANTECEDENT = "ant";
1069
1070 /**
1071 * Book producer
1072 *
1073 * Use for the person or firm responsible for the production of books and other
1074 * print media, if specific codes (e.g., [bkd], [egr], [tyd], [prt]) are not desired.
1075 */
1076 const BOOK_PRODUCER = "bkp";
1077
1078 /**
1079 * Collaborator
1080 *
1081 * Use for a person or corporate body that takes a limited part in the elaboration
1082 * of a work of another author or that brings complements (e.g., appendices, notes)
1083 * to the work of another author.
1084 */
1085 const COLABORATOR = "clb";
1086
1087 /**
1088 * Commentator
1089 *
1090 * Use for a person who provides interpretation, analysis, or a discussion of the
1091 * subject matter on a recording, motion picture, or other audiovisual medium.
1092 * Compiler [com] Use for a person who produces a work or publication by selecting
1093 * and putting together material from the works of various persons or bodies.
1094 */
1095 const COMMENTATOR = "cmm";
1096
1097 /**
1098 * Designer
1099 *
1100 * Use for a person or organization responsible for design if specific codes (e.g.,
1101 * [bkd], [tyd]) are not desired.
1102 */
1103 const DESIGNER = "dsr";
1104
1105 /**
1106 * Editor
1107 *
1108 * Use for a person who prepares for publication a work not primarily his/her own,
1109 * such as by elucidating text, adding introductory or other critical matter, or
1110 * technically directing an editorial staff.
1111 */
1112 const EDITORT = "edt";
1113
1114 /**
1115 * Illustrator
1116 *
1117 * Use for the person who conceives, and perhaps also implements, a design or
1118 * illustration, usually to accompany a written text.
1119 */
1120 const ILLUSTRATOR = "ill";
1121
1122 /**
1123 * Lyricist
1124 *
1125 * Use for the writer of the text of a song.
1126 */
1127 const LYRICIST = "lyr";
1128
1129 /**
1130 * Metadata contact
1131 *
1132 * Use for the person or organization primarily responsible for compiling and
1133 * maintaining the original description of a metadata set (e.g., geospatial
1134 * metadata set).
1135 */
1136 const METADATA_CONTACT = "mdc";
1137
1138 /**
1139 * Musician
1140 *
1141 * Use for the person who performs music or contributes to the musical content
1142 * of a work when it is not possible or desirable to identify the function more
1143 * precisely.
1144 */
1145 const MUSICIAN = "mus";
1146
1147 /**
1148 * Narrator
1149 *
1150 * Use for the speaker who relates the particulars of an act, occurrence, or
1151 * course of events.
1152 */
1153 const NARRATOR = "nrt";
1154
1155 /**
1156 * Other
1157 *
1158 * Use for relator codes from other lists which have no equivalent in the MARC
1159 * list or for terms which have not been assigned a code.
1160 */
1161 const OTHER = "oth";
1162
1163 /**
1164 * Photographer
1165 *
1166 * Use for the person or organization responsible for taking photographs, whether
1167 * they are used in their original form or as reproductions.
1168 */
1169 const PHOTOGRAPHER = "pht";
1170
1171 /**
1172 * Printer
1173 *
1174 * Use for the person or organization who prints texts, whether from type or plates.
1175 */
1176 const PRINTER = "prt";
1177
1178 /**
1179 * Redactor
1180 *
1181 * Use for a person who writes or develops the framework for an item without
1182 * being intellectually responsible for its content.
1183 */
1184 const REDACTOR = "red";
1185
1186 /**
1187 * Reviewer
1188 *
1189 * Use for a person or corporate body responsible for the review of book, motion
1190 * picture, performance, etc.
1191 */
1192 const REVIEWER = "rev";
1193
1194 /**
1195 * Sponsor
1196 *
1197 * Use for the person or agency that issued a contract, or under whose auspices
1198 * a work has been written, printed, published, etc.
1199 */
1200 const SPONSOR = "spn";
1201
1202 /**
1203 * Thesis advisor
1204 *
1205 * Use for the person under whose supervision a degree candidate develops and
1206 * presents a thesis, memoir, or text of a dissertation.
1207 */
1208 const THESIS_ADVISOR = "ths";
1209
1210 /**
1211 * Transcriber
1212 *
1213 * Use for a person who prepares a handwritten or typewritten copy from original
1214 * material, including from dictated or orally recorded material.
1215 */
1216 const TRANSCRIBER = "trc";
1217
1218 /**
1219 * Translator
1220 *
1221 * Use for a person who renders a text from one language into another, or from
1222 * an older form of a language into the modern form.
1223 */
1224 const TRANSLATOR = "trl";
1225}
1226?>
diff --git a/inc/3rdparty/libraries/PHPePub/EPub.php b/inc/3rdparty/libraries/PHPePub/EPub.php
new file mode 100644
index 00000000..d9b990b7
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/EPub.php
@@ -0,0 +1,2438 @@
1<?php
2/**
3 * Create an ePub compatible book file.
4 *
5 * Please note, once finalized a book can no longer have chapters of data added or changed.
6 *
7 * License: GNU LGPL, Attribution required for commercial implementations, requested for everything else.
8 *
9 * Thanks to: Adam Schmalhofer and Kirstyn Fox for invaluable input and for "nudging" me in the right direction :)
10 *
11 * @author A. Grandt <php@grandt.com>
12 * @copyright 2009-2014 A. Grandt
13 * @license GNU LGPL 2.1
14 * @version 3.20
15 * @link http://www.phpclasses.org/package/6115
16 * @link https://github.com/Grandt/PHPePub
17 * @uses Zip.php version 1.50; http://www.phpclasses.org/browse/package/6110.html or https://github.com/Grandt/PHPZip
18 */
19class EPub {
20 const VERSION = 3.20;
21 const REQ_ZIP_VERSION = 1.60;
22
23 const IDENTIFIER_UUID = 'UUID';
24 const IDENTIFIER_URI = 'URI';
25 const IDENTIFIER_ISBN = 'ISBN';
26
27 /** Ignore all external references, and do not process the file for these */
28 const EXTERNAL_REF_IGNORE = 0;
29 /** Process the file for external references and add them to the book */
30 const EXTERNAL_REF_ADD = 1;
31 /** Process the file for external references and add them to the book, but remove images, and img tags */
32 const EXTERNAL_REF_REMOVE_IMAGES = 2;
33 /** Process the file for external references and add them to the book, but replace images, and img tags with [image] */
34 const EXTERNAL_REF_REPLACE_IMAGES = 3;
35
36 const DIRECTION_LEFT_TO_RIGHT = "ltr";
37 const DIRECTION_RIGHT_TO_LEFT = "rtl";
38
39 const BOOK_VERSION_EPUB2 = "2.0";
40 const BOOK_VERSION_EPUB3 = "3.0";
41
42 private $bookVersion = EPub::BOOK_VERSION_EPUB2;
43
44 private $debugInside = FALSE;
45
46 public $maxImageWidth = 768;
47 public $maxImageHeight = 1024;
48
49 public $splitDefaultSize = 250000;
50 /** Gifs can crash some early ADE based readers, and are disabled by default.
51 * getImage will convert these if it can, unless this is set to TRUE.
52 */
53 public $isGifImagesEnabled = FALSE;
54 public $isReferencesAddedToToc = TRUE;
55
56 private $zip;
57
58 private $title = "";
59 private $language = "en";
60 private $identifier = "";
61 private $identifierType = "";
62 private $description = "";
63 private $author = "";
64 private $authorSortKey = "";
65 private $publisherName = "";
66 private $publisherURL = "";
67 private $date = 0;
68 private $rights = "";
69 private $coverage = "";
70 private $relation = "";
71 private $sourceURL = "";
72
73 private $chapterCount = 0;
74 private $opf = NULL;
75 private $ncx = NULL;
76 private $isFinalized = FALSE;
77 private $isCoverImageSet = FALSE;
78 private $buildTOC = FALSE;
79 private $tocTitle = NULL;
80 private $tocFileName = NULL;
81 private $tocCSSClass = NULL;
82 private $tocAddReferences = FALSE;
83 private $tocCssFileName = NULL;
84
85 private $fileList = array();
86 private $writingDirection = EPub::DIRECTION_LEFT_TO_RIGHT;
87 private $languageCode = "en";
88
89 /**
90 * Used for building the TOC.
91 * If this list is overwritten it MUST contain at least "text" as an element.
92 */
93 public $referencesOrder = NULL;
94
95 private $dateformat = 'Y-m-d\TH:i:s.000000P'; // ISO 8601 long
96 private $dateformatShort = 'Y-m-d'; // short date format to placate ePubChecker.
97 private $headerDateFormat = "D, d M Y H:i:s T";
98
99 protected $isCurlInstalled;
100 protected $isGdInstalled;
101 protected $isExifInstalled;
102 protected $isFileGetContentsInstalled;
103 protected $isFileGetContentsExtInstalled;
104
105 private $bookRoot = "OEBPS/";
106 private $docRoot = NULL;
107 private $EPubMark = TRUE;
108 private $generator = "";
109
110 private $log = NULL;
111 public $isLogging = TRUE;
112
113 public $encodeHTML = FALSE;
114
115 private $mimetypes = array(
116 "js" => "application/x-javascript", "swf" => "application/x-shockwave-flash", "xht" => "application/xhtml+xml", "xhtml" => "application/xhtml+xml", "zip" => "application/zip",
117 "aif" => "audio/x-aiff", "aifc" => "audio/x-aiff", "aiff" => "audio/x-aiff", "au" => "audio/basic", "kar" => "audio/midi", "m3u" => "audio/x-mpegurl", "mid" => "audio/midi", "midi" => "audio/midi", "mp2" => "audio/mpeg", "mp3" => "audio/mpeg", "mpga" => "audio/mpeg", "oga" => "audio/ogg", "ogg" => "audio/ogg", "ra" => "audio/x-realaudio", "ram" => "audio/x-pn-realaudio", "rm" => "audio/x-pn-realaudio", "rpm" => "audio/x-pn-realaudio-plugin", "snd" => "audio/basic", "wav" => "audio/x-wav",
118 "bmp" => "image/bmp", "djv" => "image/vnd.djvu", "djvu" => "image/vnd.djvu", "gif" => "image/gif", "ief" => "image/ief", "jpe" => "image/jpeg", "jpeg" => "image/jpeg", "jpg" => "image/jpeg", "pbm" => "image/x-portable-bitmap", "pgm" => "image/x-portable-graymap", "png" => "image/png", "pnm" => "image/x-portable-anymap", "ppm" => "image/x-portable-pixmap", "ras" => "image/x-cmu-raster", "rgb" => "image/x-rgb", "tif" => "image/tif", "tiff" => "image/tiff", "wbmp" => "image/vnd.wap.wbmp", "xbm" => "image/x-xbitmap", "xpm" => "image/x-xpixmap", "xwd" => "image/x-windowdump",
119 "asc" => "text/plain", "css" => "text/css", "etx" => "text/x-setext", "htm" => "text/html", "html" => "text/html", "rtf" => "text/rtf", "rtx" => "text/richtext", "sgm" => "text/sgml", "sgml" => "text/sgml", "tsv" => "text/tab-seperated-values", "txt" => "text/plain", "wml" => "text/vnd.wap.wml", "wmls" => "text/vnd.wap.wmlscript", "xml" => "text/xml", "xsl" => "text/xml",
120 "avi" => "video/x-msvideo", "mov" => "video/quicktime", "movie" => "video/x-sgi-movie", "mp4" => "video/mp4", "mpe" => "video/mpeg", "mpeg" => "video/mpeg", "mpg" => "video/mpeg", "mxu" => "video/vnd.mpegurl", "ogv" => "video/ogg", "qt" => "video/quicktime", "webm" => "video/webm");
121
122 // These are the ONLY allowed types in that these are the ones ANY reader must support, any other MUST have the fallback attribute pointing to one of these.
123 private $coreMediaTypes = array("image/gif", "image/jpeg", "image/png", "image/svg+xml", "application/xhtml+xml", "application/x-dtbook+xml", "application/xml", "application/x-dtbncx+xml", "text/css", "text/x-oeb1-css", "text/x-oeb1-document");
124
125 private $opsContentTypes = array("application/xhtml+xml", "application/x-dtbook+xml", "application/xml", "application/x-dtbncx+xml", "text/x-oeb1-document");
126
127 private $forbiddenCharacters = array("?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}", "%");
128
129 private $htmlContentHeader = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n<title></title>\n</head>\n<body>\n";
130 private $htmlContentFooter = "</body>\n</html>\n";
131
132 /**
133 * Class constructor.
134 *
135 * @return void
136 */
137 function __construct($bookVersion = EPub::BOOK_VERSION_EPUB2, $debugInside = FALSE, $languageCode = "en", $writingDirection = EPub::DIRECTION_LEFT_TO_RIGHT) {
138 include_once("Zip.php");
139 include_once("Logger.php");
140
141 if (!$debugInside) {
142 error_reporting(E_ERROR | E_PARSE);
143 }
144
145 $this->bookVersion = $bookVersion;
146 $this->writingDirection = $writingDirection;
147 $this->languageCode = $languageCode;
148
149 $this->log = new Logger("EPub", $this->isLogging);
150
151 /* Prepare Logging. Just in case it's used. later */
152 if ($this->isLogging) {
153 $this->log->logLine("EPub class version....: " . self::VERSION);
154 $this->log->logLine("EPub req. Zip version.: " . self::REQ_ZIP_VERSION);
155 $this->log->logLine("Zip version...........: " . Zip::VERSION);
156 $this->log->dumpInstalledModules();
157 }
158
159 if (!defined("Zip::VERSION") || Zip::VERSION < self::REQ_ZIP_VERSION) {
160 die("<p>EPub version " . self::VERSION . " requires Zip.php at version " . self::REQ_ZIP_VERSION . " or higher.<br />You can obtain the latest version from <a href=\"http://www.phpclasses.org/browse/package/6110.html\">http://www.phpclasses.org/browse/package/6110.html</a>.</p>");
161 }
162
163 include_once("EPubChapterSplitter.php");
164 include_once("EPub.HtmlEntities.php");
165 include_once("EPub.NCX.php");
166 include_once("EPub.OPF.php");
167
168 $this->initialize();
169 }
170
171 /**
172 * Class destructor
173 *
174 * @return void
175 * @TODO make sure elements in the destructor match the current class elements
176 */
177 function __destruct() {
178 unset($this->bookVersion, $this->maxImageWidth, $this->maxImageHeight);
179 unset($this->splitDefaultSize, $this->isGifImagesEnabled, $this->isReferencesAddedToToc);
180 unset($this->zip, $this->title, $this->language, $this->identifier, $this->identifierType);
181 unset($this->description, $this->author, $this->authorSortKey, $this->publisherName);
182 unset($this->publisherURL, $this->date, $this->rights, $this->coverage, $this->relation);
183 unset($this->sourceURL, $this->chapterCount, $this->opf, $this->ncx, $this->isFinalized);
184 unset($this->isCoverImageSet, $this->fileList, $this->writingDirection, $this->languageCode);
185 unset($this->referencesOrder, $this->dateformat, $this->dateformatShort, $this->headerDateFormat);
186 unset($this->isCurlInstalled, $this->isGdInstalled, $this->isExifInstalled);
187 unset($this->isFileGetContentsInstalled, $this->isFileGetContentsExtInstalled, $this->bookRoot);
188 unset($this->docRoot, $this->EPubMark, $this->generator, $this->log, $this->isLogging);
189 unset($this->encodeHTML, $this->mimetypes, $this->coreMediaTypes, $this->opsContentTypes);
190 unset($this->forbiddenCharacters, $this->htmlContentHeader, $this->htmlContentFooter);
191 unset($this->buildTOC, $this->tocTitle, $this->tocCSSClass, $this->tocAddReferences);
192 unset($this->tocFileName, $this->tocCssFileName);
193 }
194
195 /**
196 * initialize defaults.
197 */
198 private function initialize() {
199 $this->referencesOrder = array(
200 Reference::COVER => "Cover Page",
201 Reference::TITLE_PAGE => "Title Page",
202 Reference::ACKNOWLEDGEMENTS => "Acknowledgements",
203 Reference::BIBLIOGRAPHY => "Bibliography",
204 Reference::COLOPHON => "Colophon",
205 Reference::COPYRIGHT_PAGE => "Copyright",
206 Reference::DEDICATION => "Dedication",
207 Reference::EPIGRAPH => "Epigraph",
208 Reference::FOREWORD => "Foreword",
209 Reference::TABLE_OF_CONTENTS => "Table of Contents",
210 Reference::NOTES => "Notes",
211 Reference::PREFACE => "Preface",
212 Reference::TEXT => "First Page",
213 Reference::LIST_OF_ILLUSTRATIONS => "List of Illustrations",
214 Reference::LIST_OF_TABLES => "List of Tables",
215 Reference::GLOSSARY => "Glossary",
216 Reference::INDEX => "Index");
217
218 $this->docRoot = filter_input(INPUT_SERVER, "DOCUMENT_ROOT") . "/";
219
220 $this->isCurlInstalled = extension_loaded('curl') && function_exists('curl_version');
221 $this->isGdInstalled = extension_loaded('gd') && function_exists('gd_info');
222 $this->isExifInstalled = extension_loaded('exif') && function_exists('exif_imagetype');
223 $this->isFileGetContentsInstalled = function_exists('file_get_contents');
224 $this->isFileGetContentsExtInstalled = $this->isFileGetContentsInstalled && ini_get('allow_url_fopen');
225
226 $this->zip = new Zip();
227 $this->zip->setExtraField(FALSE);
228 $this->zip->addFile("application/epub+zip", "mimetype");
229 $this->zip->setExtraField(TRUE);
230 $this->zip->addDirectory("META-INF");
231
232 $this->content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n\t<rootfiles>\n\t\t<rootfile full-path=\"" . $this->bookRoot . "book.opf\" media-type=\"application/oebps-package+xml\" />\n\t</rootfiles>\n</container>\n";
233
234 if (!$this->isEPubVersion2()) {
235 $this->htmlContentHeader = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
236 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
237 . "<head>"
238 . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
239 . "<title></title>\n"
240 . "</head>\n"
241 . "<body>\n";
242 }
243
244 $this->zip->addFile($this->content, "META-INF/container.xml", 0, NULL, FALSE);
245 $this->content = NULL;
246 $this->ncx = new Ncx(NULL, NULL, NULL, $this->languageCode, $this->writingDirection);
247 $this->opf = new Opf();
248 $this->ncx->setVersion($this->bookVersion);
249 $this->opf->setVersion($this->bookVersion);
250 $this->opf->addItem("ncx", "book.ncx", Ncx::MIMETYPE);
251 $this->chapterCount = 0;
252 }
253
254 /**
255 * Add dynamically generated data as a file to the book.
256 *
257 * @param string $fileName Filename to use for the file, must be unique for the book.
258 * @param string $fileId Unique identifier for the file.
259 * @param string $fileData File data
260 * @param string $mimetype file mime type
261 * @return bool $success
262 */
263 function addFile($fileName, $fileId, $fileData, $mimetype) {
264 if ($this->isFinalized || array_key_exists($fileName, $this->fileList)) {
265 return FALSE;
266 }
267
268 $fileName = $this->normalizeFileName($fileName);
269
270 $compress = (strpos($mimetype, "image/") !== 0);
271
272 $this->zip->addFile($fileData, $this->bookRoot.$fileName, 0, NULL, $compress);
273 $this->fileList[$fileName] = $fileName;
274 $this->opf->addItem($fileId, $fileName, $mimetype);
275 return TRUE;
276 }
277
278 /**
279 * Add a large file directly from the filestystem to the book.
280 *
281 * @param string $fileName Filename to use for the file, must be unique for the book.
282 * @param string $fileId Unique identifier for the file.
283 * @param string $filePath File path
284 * @param string $mimetype file mime type
285 * @return bool $success
286 */
287 function addLargeFile($fileName, $fileId, $filePath, $mimetype) {
288 if ($this->isFinalized || array_key_exists($fileName, $this->fileList)) {
289 return FALSE;
290 }
291 $fileName = $this->normalizeFileName($fileName);
292
293 if ($this->zip->addLargeFile($filePath, $this->bookRoot.$fileName)) {
294 $this->fileList[$fileName] = $fileName;
295 $this->opf->addItem($fileId, $fileName, $mimetype);
296 return TRUE;
297 }
298 return FALSE;
299 }
300
301 /**
302 * Add a CSS file to the book.
303 *
304 * @param string $fileName Filename to use for the CSS file, must be unique for the book.
305 * @param string $fileId Unique identifier for the file.
306 * @param string $fileData CSS data
307 * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? See documentation for <code>processCSSExternalReferences</code> for explanation. Default is EPub::EXTERNAL_REF_IGNORE.
308 * @param string $baseDir Default is "", meaning it is pointing to the document root. NOT used if $externalReferences is set to EPub::EXTERNAL_REF_IGNORE.
309 *
310 * @return bool $success
311 */
312 function addCSSFile($fileName, $fileId, $fileData, $externalReferences = EPub::EXTERNAL_REF_IGNORE, $baseDir = "") {
313 if ($this->isFinalized || array_key_exists($fileName, $this->fileList)) {
314 return FALSE;
315 }
316 $fileName = Zip::getRelativePath($fileName);
317 $fileName = preg_replace('#^[/\.]+#i', "", $fileName);
318
319 if ($externalReferences !== EPub::EXTERNAL_REF_IGNORE) {
320 $cssDir = pathinfo($fileName);
321 $cssDir = preg_replace('#^[/\.]+#i', "", $cssDir["dirname"] . "/");
322 if (!empty($cssDir)) {
323 $cssDir = preg_replace('#[^/]+/#i', "../", $cssDir);
324 }
325
326 $this->processCSSExternalReferences($fileData, $externalReferences, $baseDir, $cssDir);
327 }
328
329 $this->addFile($fileName, "css_" . $fileId, $fileData, "text/css");
330
331 return TRUE;
332 }
333
334 /**
335 * Add a chapter to the book, as a chapter should not exceed 250kB, you can parse an array with multiple parts as $chapterData.
336 * These will still only show up as a single chapter in the book TOC.
337 *
338 * @param string $chapterName Name of the chapter, will be use din the TOC
339 * @param string $fileName Filename to use for the chapter, must be unique for the book.
340 * @param string $chapter Chapter text in XHTML or array $chapterData valid XHTML data for the chapter. File should NOT exceed 250kB.
341 * @param bool $autoSplit Should the chapter be split if it exceeds the default split size? Default=FALSE, only used if $chapterData is a string.
342 * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? See documentation for <code>processChapterExternalReferences</code> for explanation. Default is EPub::EXTERNAL_REF_IGNORE.
343 * @param string $baseDir Default is "", meaning it is pointing to the document root. NOT used if $externalReferences is set to EPub::EXTERNAL_REF_IGNORE.
344 * @return mixed $success FALSE if the addition failed, else the new NavPoint.
345 */
346 function addChapter($chapterName, $fileName, $chapterData = NULL, $autoSplit = FALSE, $externalReferences = EPub::EXTERNAL_REF_IGNORE, $baseDir = "") {
347 if ($this->isFinalized) {
348 return FALSE;
349 }
350 $fileName = Zip::getRelativePath($fileName);
351 $fileName = preg_replace('#^[/\.]+#i', "", $fileName);
352 $fileName = $this->sanitizeFileName($fileName);
353
354 $chapter = $chapterData;
355 if ($autoSplit && is_string($chapterData) && mb_strlen($chapterData) > $this->splitDefaultSize) {
356 $splitter = new EPubChapterSplitter();
357
358 $chapterArray = $splitter->splitChapter($chapterData);
359 if (count($chapterArray) > 1) {
360 $chapter = $chapterArray;
361 }
362 }
363
364 if (!empty($chapter) && is_string($chapter)) {
365 if ($externalReferences !== EPub::EXTERNAL_REF_IGNORE) {
366 $htmlDirInfo = pathinfo($fileName);
367 $htmlDir = preg_replace('#^[/\.]+#i', "", $htmlDirInfo["dirname"] . "/");
368 $this->processChapterExternalReferences($chapter, $externalReferences, $baseDir, $htmlDir);
369 }
370
371 if ($this->encodeHTML === TRUE) {
372 $chapter = $this->encodeHtml($chapter);
373 }
374
375 $this->chapterCount++;
376 $this->addFile($fileName, "chapter" . $this->chapterCount, $chapter, "application/xhtml+xml");
377 $this->opf->addItemRef("chapter" . $this->chapterCount);
378
379 $navPoint = new NavPoint($this->decodeHtmlEntities($chapterName), $fileName, "chapter" . $this->chapterCount);
380 $this->ncx->addNavPoint($navPoint);
381 $this->ncx->chapterList[$chapterName] = $navPoint;
382 } else if (is_array($chapter)) {
383 $fileNameParts = pathinfo($fileName);
384 $extension = $fileNameParts['extension'];
385 $name = $fileNameParts['filename'];
386
387 $partCount = 0;
388 $this->chapterCount++;
389
390 $oneChapter = each($chapter);
391 while ($oneChapter) {
392 list($k, $v) = $oneChapter;
393 if ($this->encodeHTML === TRUE) {
394 $v = $this->encodeHtml($v);
395 }
396
397 if ($externalReferences !== EPub::EXTERNAL_REF_IGNORE) {
398 $this->processChapterExternalReferences($v, $externalReferences, $baseDir);
399 }
400 $partCount++;
401 $partName = $name . "_" . $partCount;
402 $this->addFile($partName . "." . $extension, $partName, $v, "application/xhtml+xml");
403 $this->opf->addItemRef($partName);
404
405 $oneChapter = each($chapter);
406 }
407 $partName = $name . "_1." . $extension;
408 $navPoint = new NavPoint($this->decodeHtmlEntities($chapterName), $partName, $partName);
409 $this->ncx->addNavPoint($navPoint);
410
411 $this->ncx->chapterList[$chapterName] = $navPoint;
412 } else if (!isset($chapterData) && strpos($fileName, "#") > 0) {
413 $this->chapterCount++;
414 //$this->opf->addItemRef("chapter" . $this->chapterCount);
415
416 $navPoint = new NavPoint($this->decodeHtmlEntities($chapterName), $fileName, "chapter" . $this->chapterCount);
417 $this->ncx->addNavPoint($navPoint);
418 $this->ncx->chapterList[$chapterName] = $navPoint;
419 } else if (!isset($chapterData) && $fileName=="TOC.xhtml") {
420 $this->chapterCount++;
421 $this->opf->addItemRef("toc");
422
423 $navPoint = new NavPoint($this->decodeHtmlEntities($chapterName), $fileName, "chapter" . $this->chapterCount);
424 $this->ncx->addNavPoint($navPoint);
425 $this->ncx->chapterList[$chapterName] = $navPoint;
426 }
427 return $navPoint;
428 }
429
430 /**
431 * Add one chapter level.
432 *
433 * Subsequent chapters will be added to this level.
434 *
435 * @param string $navTitle
436 * @param string $navId
437 * @param string $navClass
438 * @param int $isNavHidden
439 * @param string $writingDirection
440 * @return NavPoint The new NavPoint for that level.
441 */
442 function subLevel($navTitle = NULL, $navId = NULL, $navClass = NULL, $isNavHidden = FALSE, $writingDirection = NULL) {
443 return $this->ncx->subLevel($this->decodeHtmlEntities($navTitle), $navId, $navClass, $isNavHidden, $writingDirection);
444 }
445
446 /**
447 * Step back one chapter level.
448 *
449 * Subsequent chapters will be added to this chapters parent level.
450 */
451 function backLevel() {
452 $this->ncx->backLevel();
453 }
454
455 /**
456 * Step back to the root level.
457 *
458 * Subsequent chapters will be added to the rooot NavMap.
459 */
460 function rootLevel() {
461 $this->ncx->rootLevel();
462 }
463
464 /**
465 * Step back to the given level.
466 * Useful for returning to a previous level from deep within the structure.
467 * Values below 2 will have the same effect as rootLevel()
468 *
469 * @param int $newLevel
470 */
471 function setCurrentLevel($newLevel) {
472 $this->ncx->setCurrentLevel($newLevel);
473 }
474
475 /**
476 * Get current level count.
477 * The indentation of the current structure point.
478 *
479 * @return current level count;
480 */
481 function getCurrentLevel() {
482 return $this->ncx->getCurrentLevel();
483 }
484
485 /**
486 * Wrap ChapterContent with Head and Footer
487 *
488 * @param $content
489 * @return string $content
490 */
491 private function wrapChapter($content) {
492 return $this->htmlContentHeader . "\n" . $content . "\n" . $this->htmlContentFooter;
493 }
494
495 /**
496 * Reference pages is usually one or two pages for items such as Table of Contents, reference lists, Author notes or Acknowledgements.
497 * These do not show up in the regular navigation list.
498 *
499 * As they are supposed to be short.
500 *
501 * @param string $pageName Name of the chapter, will be use din the TOC
502 * @param string $fileName Filename to use for the chapter, must be unique for the book.
503 * @param string $pageData Page content in XHTML. File should NOT exceed 250kB.
504 * @param string $reference Reference key
505 * @param int $externalReferences How to handle external references. See documentation for <code>processChapterExternalReferences</code> for explanation. Default is EPub::EXTERNAL_REF_IGNORE.
506 * @param string $baseDir Default is "", meaning it is pointing to the document root. NOT used if $externalReferences is set to EPub::EXTERNAL_REF_IGNORE.
507 * @return bool $success
508 */
509 function addReferencePage($pageName, $fileName, $pageData, $reference, $externalReferences = EPub::EXTERNAL_REF_IGNORE, $baseDir = "") {
510 if ($this->isFinalized) {
511 return FALSE;
512 }
513 $fileName = Zip::getRelativePath($fileName);
514 $fileName = preg_replace('#^[/\.]+#i', "", $fileName);
515
516
517 if (!empty($pageData) && is_string($pageData)) {
518 if ($this->encodeHTML === TRUE) {
519 $pageData = $this->encodeHtml($pageData);
520 }
521
522 $this->wrapChapter($pageData);
523
524 if ($externalReferences !== EPub::EXTERNAL_REF_IGNORE) {
525 $htmlDirInfo = pathinfo($fileName);
526 $htmlDir = preg_replace('#^[/\.]+#i', "", $htmlDirInfo["dirname"] . "/");
527 $this->processChapterExternalReferences($pageData, $externalReferences, $baseDir, $htmlDir);
528 }
529
530 $this->addFile($fileName, "ref_" . $reference, $pageData, "application/xhtml+xml");
531
532 if ($reference !== Reference::TABLE_OF_CONTENTS || !isset($this->ncx->referencesList[$reference])) {
533 $this->opf->addItemRef("ref_" . $reference, FALSE);
534 $this->opf->addReference($reference, $pageName, $fileName);
535
536 $this->ncx->referencesList[$reference] = $fileName;
537 $this->ncx->referencesName[$reference] = $pageName;
538 }
539 return TRUE;
540 }
541 return TRUE;
542 }
543
544 /**
545 * Add custom metadata to the book.
546 *
547 * It is up to the builder to make sure there are no collisions. Metadata are just key value pairs.
548 *
549 * @param string $name
550 * @param string $content
551 */
552 function addCustomMetadata($name, $content) {
553 $this->opf->addMeta($name, $content);
554 }
555
556 /**
557 * Add DublinCore metadata to the book
558 *
559 * Use the DublinCore constants included in EPub, ie DublinCore::DATE
560 *
561 * @param string $dublinCore name
562 * @param string $value
563 */
564 function addDublinCoreMetadata($dublinCoreConstant, $value) {
565 if ($this->isFinalized) {
566 return;
567 }
568
569 $this->opf->addDCMeta($dublinCoreConstant, $this->decodeHtmlEntities($value));
570 }
571
572 /**
573 * Add a cover image to the book.
574 * If the $imageData is not set, the function assumes the $fileName is the path to the image file.
575 *
576 * The styling and structure of the generated XHTML is heavily inspired by the XHTML generated by Calibre.
577 *
578 * @param string $fileName Filename to use for the image, must be unique for the book.
579 * @param string $imageData Binary image data
580 * @param string $mimetype Image mimetype, such as "image/jpeg" or "image/png".
581 * @return bool $success
582 */
583 function setCoverImage($fileName, $imageData = NULL, $mimetype = NULL,$bookTitle) {
584 if ($this->isFinalized || $this->isCoverImageSet || array_key_exists("CoverPage.html", $this->fileList)) {
585 return FALSE;
586 }
587
588 if ($imageData == NULL) {
589 // assume $fileName is the valid file path.
590 if (!file_exists($fileName)) {
591 // Attempt to locate the file using the doc root.
592 $rp = realpath($this->docRoot . "/" . $fileName);
593
594 if ($rp !== FALSE) {
595 // only assign the docroot path if it actually exists there.
596 $fileName = $rp;
597 }
598 }
599 $image = $this->getImage($fileName);
600 $imageData = $image['image'];
601 $mimetype = $image['mime'];
602 $fileName = preg_replace("#\.[^\.]+$#", "." . $image['ext'], $fileName);
603 }
604
605
606 $path = pathinfo($fileName);
607 $imgPath = "images/" . $path["basename"];
608
609 if (empty($mimetype) && file_exists($fileName)) {
610 list($width, $height, $type, $attr) = getimagesize($fileName);
611 $mimetype = image_type_to_mime_type($type);
612 }
613 if (empty($mimetype)) {
614 $ext = strtolower($path['extension']);
615 if ($ext == "jpg") {
616 $ext = "jpeg";
617 }
618 $mimetype = "image/" . $ext;
619 }
620
621 $coverPage = "";
622
623 if ($this->isEPubVersion2()) {
624 $coverPage = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
625 . "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n"
626 . " \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"
627 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\" xml:lang=\"en\">\n"
628 . "\t<head>\n"
629 . "\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n"
630 . "\t\t<title>Cover Image</title>\n"
631 . "\t\t<link type=\"text/css\" rel=\"stylesheet\" href=\"Styles/CoverPage.css\" />\n"
632 . "\t</head>\n"
633 . "\t<body>\n"
634 . "\t" . $bookTitle . "\n"
635 . "\t\t<div>\n"
636 . "\t\t\t<img src=\"" . $imgPath . "\" alt=\"Cover image\" style=\"height: 100%\"/>\n"
637 . "\t\t</div>\n"
638 . "\t</body>\n"
639 . "</html>\n";
640 } else {
641 $coverPage = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
642 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
643 . "<head>"
644 . "\t<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
645 . "\t\t<title>Cover Image</title>\n"
646 . "\t\t<link type=\"text/css\" rel=\"stylesheet\" href=\"Styles/CoverPage.css\" />\n"
647 . "\t</head>\n"
648 . "\t<body>\n"
649 . "\t\t<section epub:type=\"cover\">\n"
650 . "\t" . $bookTitle . "\n"
651 . "\t\t\t<img src=\"" . $imgPath . "\" alt=\"Cover image\" style=\"height: 30%\"/>\n"
652 . "\t\t</section>\n"
653 . "\t</body>\n"
654 . "</html>\n";
655 }
656 $coverPageCss = "@page, body, div, img {\n"
657 . "\tpadding: 0pt;\n"
658 . "\tmargin:0pt;\n"
659 . "}\n\nbody {\n"
660 . "\ttext-align: center;\n"
661 . "}\n";
662
663 $this->addCSSFile("Styles/CoverPage.css", "CoverPageCss", $coverPageCss);
664 $this->addFile($imgPath, "CoverImage", $imageData, $mimetype);
665 $this->addReferencePage("CoverPage", "CoverPage.xhtml", $coverPage, "cover");
666 $this->isCoverImageSet = TRUE;
667 return TRUE;
668 }
669
670 /**
671 * Process external references from a HTML to the book. The chapter itself is not stored.
672 * the HTML is scanned for &lt;link..., &lt;style..., and &lt;img tags.
673 * Embedded CSS styles and links will also be processed.
674 * Script tags are not processed, as scripting should be avoided in e-books.
675 *
676 * EPub keeps track of added files, and duplicate files referenced across multiple
677 * chapters, are only added once.
678 *
679 * If the $doc is a string, it is assumed to be the content of an HTML file,
680 * else is it assumes to be a DOMDocument.
681 *
682 * Basedir is the root dir the HTML is supposed to "live" in, used to resolve
683 * relative references such as <code>&lt;img src="../images/image.png"/&gt;</code>
684 *
685 * $externalReferences determines how the function will handle external references.
686 *
687 * @param mixed &$doc (referenced)
688 * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
689 * @param string $baseDir Default is "", meaning it is pointing to the document root.
690 * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
691 *
692 * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
693 */
694 protected function processChapterExternalReferences(&$doc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "") {
695 if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
696 return FALSE;
697 }
698
699 $backPath = preg_replace('#[^/]+/#i', "../", $htmlDir);
700 $isDocAString = is_string($doc);
701 $xmlDoc = NULL;
702
703 if ($isDocAString) {
704 $xmlDoc = new DOMDocument();
705 @$xmlDoc->loadHTML($doc);
706 } else {
707 $xmlDoc = $doc;
708 }
709
710 $this->processChapterStyles($xmlDoc, $externalReferences, $baseDir, $htmlDir);
711 $this->processChapterLinks($xmlDoc, $externalReferences, $baseDir, $htmlDir, $backPath);
712 $this->processChapterImages($xmlDoc, $externalReferences, $baseDir, $htmlDir, $backPath);
713 $this->processChapterSources($xmlDoc, $externalReferences, $baseDir, $htmlDir, $backPath);
714
715 if ($isDocAString) {
716 //$html = $xmlDoc->saveXML();
717
718 $htmlNode = $xmlDoc->getElementsByTagName("html");
719 $headNode = $xmlDoc->getElementsByTagName("head");
720 $bodyNode = $xmlDoc->getElementsByTagName("body");
721
722 $htmlNS = "";
723 for ($index = 0; $index < $htmlNode->item(0)->attributes->length; $index++) {
724 $nodeName = $htmlNode->item(0)->attributes->item($index)->nodeName;
725 $nodeValue = $htmlNode->item(0)->attributes->item($index)->nodeValue;
726
727 if ($nodeName != "xmlns") {
728 $htmlNS .= " $nodeName=\"$nodeValue\"";
729 }
730 }
731
732 $xml = new DOMDocument('1.0', "utf-8");
733 $xml->lookupPrefix("http://www.w3.org/1999/xhtml");
734 $xml->preserveWhiteSpace = FALSE;
735 $xml->formatOutput = TRUE;
736
737 $xml2Doc = new DOMDocument('1.0', "utf-8");
738 $xml2Doc->lookupPrefix("http://www.w3.org/1999/xhtml");
739 $xml2Doc->loadXML("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\"$htmlNS>\n</html>\n");
740 $html = $xml2Doc->getElementsByTagName("html")->item(0);
741 $html->appendChild($xml2Doc->importNode($headNode->item(0), TRUE));
742 $html->appendChild($xml2Doc->importNode($bodyNode->item(0), TRUE));
743
744 // force pretty printing and correct formatting, should not be needed, but it is.
745 $xml->loadXML($xml2Doc->saveXML());
746 $doc = $xml->saveXML();
747
748 if (!$this->isEPubVersion2()) {
749 $doc = preg_replace('#^\s*<!DOCTYPE\ .+?>\s*#im', '', $doc);
750 }
751 }
752 return TRUE;
753 }
754
755 /**
756 * Process images referenced from an CSS file to the book.
757 *
758 * $externalReferences determins how the function will handle external references.
759 *
760 * @param string &$cssFile (referenced)
761 * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
762 * @param string $baseDir Default is "", meaning it is pointing to the document root.
763 * @param string $cssDir The of the CSS file's directory from the root of the archive.
764 *
765 * @return bool FALSE if unsuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
766 */
767 protected function processCSSExternalReferences(&$cssFile, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $cssDir = "") {
768 if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
769 return FALSE;
770 }
771
772 $backPath = preg_replace('#[^/]+/#i', "../", $cssDir);
773 $imgs = null;
774 preg_match_all('#url\s*\([\'\"\s]*(.+?)[\'\"\s]*\)#im', $cssFile, $imgs, PREG_SET_ORDER);
775
776 $itemCount = count($imgs);
777 for ($idx = 0; $idx < $itemCount; $idx++) {
778 $img = $imgs[$idx];
779 if ($externalReferences === EPub::EXTERNAL_REF_REMOVE_IMAGES || $externalReferences === EPub::EXTERNAL_REF_REPLACE_IMAGES) {
780 $cssFile = str_replace($img[0], "", $cssFile);
781 } else {
782 $source = $img[1];
783
784 $pathData = pathinfo($source);
785 $internalSrc = $pathData['basename'];
786 $internalPath = "";
787 $isSourceExternal = FALSE;
788
789 if ($this->resolveImage($source, $internalPath, $internalSrc, $isSourceExternal, $baseDir, $cssDir, $backPath)) {
790 $cssFile = str_replace($img[0], "url('" . $backPath . $internalPath . "')", $cssFile);
791 } else if ($isSourceExternal) {
792 $cssFile = str_replace($img[0], "", $cssFile); // External image is missing
793 } // else do nothing, if the image is local, and missing, assume it's been generated.
794 }
795 }
796 return TRUE;
797 }
798
799 /**
800 * Process style tags in a DOMDocument. Styles will be passed as CSS files and reinserted into the document.
801 *
802 * @param DOMDocument &$xmlDoc (referenced)
803 * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
804 * @param string $baseDir Default is "", meaning it is pointing to the document root.
805 * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
806 *
807 * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
808 */
809 protected function processChapterStyles(&$xmlDoc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "") {
810 if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
811 return FALSE;
812 }
813 // process inlined CSS styles in style tags.
814 $styles = $xmlDoc->getElementsByTagName("style");
815 $styleCount = $styles->length;
816 for ($styleIdx = 0; $styleIdx < $styleCount; $styleIdx++) {
817 $style = $styles->item($styleIdx);
818
819 $styleData = preg_replace('#[/\*\s]*\<\!\[CDATA\[[\s\*/]*#im', "", $style->nodeValue);
820 $styleData = preg_replace('#[/\*\s]*\]\]\>[\s\*/]*#im', "", $styleData);
821
822 $this->processCSSExternalReferences($styleData, $externalReferences, $baseDir, $htmlDir);
823 $style->nodeValue = "\n" . trim($styleData) . "\n";
824 }
825 return TRUE;
826 }
827
828 /**
829 * Process link tags in a DOMDocument. Linked files will be loaded into the archive, and the link src will be rewritten to point to that location.
830 * Link types text/css will be passed as CSS files.
831 *
832 * @param DOMDocument &$xmlDoc (referenced)
833 * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
834 * @param string $baseDir Default is "", meaning it is pointing to the document root.
835 * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
836 * @param string $backPath The path to get back to the root of the archive from $htmlDir.
837 *
838 * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
839 */
840 protected function processChapterLinks(&$xmlDoc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "", $backPath = "") {
841 if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
842 return FALSE;
843 }
844 // process link tags.
845 $links = $xmlDoc->getElementsByTagName("link");
846 $linkCount = $links->length;
847 for ($linkIdx = 0; $linkIdx < $linkCount; $linkIdx++) {
848 $link = $links->item($linkIdx);
849 $source = $link->attributes->getNamedItem("href")->nodeValue;
850 $sourceData = NULL;
851
852 $pathData = pathinfo($source);
853 $internalSrc = $pathData['basename'];
854
855 if (preg_match('#^(http|ftp)s?://#i', $source) == 1) {
856 $urlinfo = parse_url($source);
857
858 if (strpos($urlinfo['path'], $baseDir."/") !== FALSE) {
859 $internalSrc = substr($urlinfo['path'], strpos($urlinfo['path'], $baseDir."/") + strlen($baseDir) + 1);
860 }
861
862 @$sourceData = getFileContents($source);
863 } else if (strpos($source, "/") === 0) {
864 @$sourceData = file_get_contents($this->docRoot . $source);
865 } else {
866 @$sourceData = file_get_contents($this->docRoot . $baseDir . "/" . $source);
867 }
868
869 if (!empty($sourceData)) {
870 if (!array_key_exists($internalSrc, $this->fileList)) {
871 $mime = $link->attributes->getNamedItem("type")->nodeValue;
872 if (empty($mime)) {
873 $mime = "text/plain";
874 }
875 if ($mime == "text/css") {
876 $this->processCSSExternalReferences($sourceData, $externalReferences, $baseDir, $htmlDir);
877 $this->addCSSFile($internalSrc, $internalSrc, $sourceData, EPub::EXTERNAL_REF_IGNORE, $baseDir);
878 $link->setAttribute("href", $backPath . $internalSrc);
879 } else {
880 $this->addFile($internalSrc, $internalSrc, $sourceData, $mime);
881 }
882 $this->fileList[$internalSrc] = $source;
883 } else {
884 $link->setAttribute("href", $backPath . $internalSrc);
885 }
886 } // else do nothing, if the link is local, and missing, assume it's been generated.
887 }
888 return TRUE;
889 }
890
891 /**
892 * Process img tags in a DOMDocument.
893 * $externalReferences will determine what will happen to these images, and the img src will be rewritten accordingly.
894 *
895 * @param DOMDocument &$xmlDoc (referenced)
896 * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
897 * @param string $baseDir Default is "", meaning it is pointing to the document root.
898 * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
899 * @param string $backPath The path to get back to the root of the archive from $htmlDir.
900 *
901 * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
902 */
903 protected function processChapterImages(&$xmlDoc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "", $backPath = "") {
904 if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
905 return FALSE;
906 }
907 // process img tags.
908 $postProcDomElememts = array();
909 $images = $xmlDoc->getElementsByTagName("img");
910 $itemCount = $images->length;
911
912 for ($idx = 0; $idx < $itemCount; $idx++) {
913 $img = $images->item($idx);
914
915 if ($externalReferences === EPub::EXTERNAL_REF_REMOVE_IMAGES) {
916 $postProcDomElememts[] = $img;
917 } else if ($externalReferences === EPub::EXTERNAL_REF_REPLACE_IMAGES) {
918 $altNode = $img->attributes->getNamedItem("alt");
919 $alt = "image";
920 if ($altNode !== NULL && strlen($altNode->nodeValue) > 0) {
921 $alt = $altNode->nodeValue;
922 }
923 $postProcDomElememts[] = array($img, $this->createDomFragment($xmlDoc, "<em>[" . $alt . "]</em>"));
924 } else {
925 $source = $img->attributes->getNamedItem("src")->nodeValue;
926
927 $parsedSource = parse_url($source);
928 $internalSrc = $this->sanitizeFileName(urldecode(pathinfo($parsedSource['path'], PATHINFO_BASENAME)));
929 $internalPath = "";
930 $isSourceExternal = FALSE;
931
932 if ($this->resolveImage($source, $internalPath, $internalSrc, $isSourceExternal, $baseDir, $htmlDir, $backPath)) {
933 $img->setAttribute("src", $backPath . $internalPath);
934 } else if ($isSourceExternal) {
935 $postProcDomElememts[] = $img; // External image is missing
936 } // else do nothing, if the image is local, and missing, assume it's been generated.
937 }
938 }
939
940 foreach ($postProcDomElememts as $target) {
941 if (is_array($target)) {
942 $target[0]->parentNode->replaceChild($target[1], $target[0]);
943 } else {
944 $target->parentNode->removeChild($target);
945 }
946 }
947 return TRUE;
948 }
949
950 /**
951 * Process source tags in a DOMDocument.
952 * $externalReferences will determine what will happen to these images, and the img src will be rewritten accordingly.
953 *
954 * @param DOMDocument &$xmlDoc (referenced)
955 * @param int $externalReferences How to handle external references, EPub::EXTERNAL_REF_IGNORE, EPub::EXTERNAL_REF_ADD or EPub::EXTERNAL_REF_REMOVE_IMAGES? Default is EPub::EXTERNAL_REF_ADD.
956 * @param string $baseDir Default is "", meaning it is pointing to the document root.
957 * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
958 * @param string $backPath The path to get back to the root of the archive from $htmlDir.
959 *
960 * @return bool FALSE if uncuccessful (book is finalized or $externalReferences == EXTERNAL_REF_IGNORE).
961 */
962 protected function processChapterSources(&$xmlDoc, $externalReferences = EPub::EXTERNAL_REF_ADD, $baseDir = "", $htmlDir = "", $backPath = "") {
963 if ($this->isFinalized || $externalReferences === EPub::EXTERNAL_REF_IGNORE) {
964 return FALSE;
965 }
966
967 if ($this->bookVersion !== EPub::BOOK_VERSION_EPUB3) {
968 // ePub 2 does not support multimedia formats, and they must be removed.
969 $externalReferences = EPub::EXTERNAL_REF_REMOVE_IMAGES;
970 }
971
972 $postProcDomElememts = array();
973 $images = $xmlDoc->getElementsByTagName("source");
974 $itemCount = $images->length;
975 for ($idx = 0; $idx < $itemCount; $idx++) {
976 $img = $images->item($idx);
977 if ($externalReferences === EPub::EXTERNAL_REF_REMOVE_IMAGES) {
978 $postProcDomElememts[] = $img;
979 } else if ($externalReferences === EPub::EXTERNAL_REF_REPLACE_IMAGES) {
980 $altNode = $img->attributes->getNamedItem("alt");
981 $alt = "image";
982 if ($altNode !== NULL && strlen($altNode->nodeValue) > 0) {
983 $alt = $altNode->nodeValue;
984 }
985 $postProcDomElememts[] = array($img, $this->createDomFragment($xmlDoc, "[" . $alt . "]"));
986 } else {
987 $source = $img->attributes->getNamedItem("src")->nodeValue;
988
989 $parsedSource = parse_url($source);
990 $internalSrc = $this->sanitizeFileName(urldecode(pathinfo($parsedSource['path'], PATHINFO_BASENAME)));
991 $internalPath = "";
992 $isSourceExternal = FALSE;
993
994 if ($this->resolveMedia($source, $internalPath, $internalSrc, $isSourceExternal, $baseDir, $htmlDir, $backPath)) {
995 $img->setAttribute("src", $backPath . $internalPath);
996 } else if ($isSourceExternal) {
997 $postProcDomElememts[] = $img; // External image is missing
998 } // else do nothing, if the image is local, and missing, assume it's been generated.
999 }
1000 }
1001 }
1002
1003 /**
1004 * Resolve an image src and determine it's target location and add it to the book.
1005 *
1006 * @param string $source Image Source link.
1007 * @param string &$internalPath (referenced) Return value, will be set to the target path and name in the book.
1008 * @param string &$internalSrc (referenced) Return value, will be set to the target name in the book.
1009 * @param string &$isSourceExternal (referenced) Return value, will be set to TRUE if the image originated from a full URL.
1010 * @param string $baseDir Default is "", meaning it is pointing to the document root.
1011 * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
1012 * @param string $backPath The path to get back to the root of the archive from $htmlDir.
1013 */
1014 protected function resolveImage($source, &$internalPath, &$internalSrc, &$isSourceExternal, $baseDir = "", $htmlDir = "", $backPath = "") {
1015 if ($this->isFinalized) {
1016 return FALSE;
1017 }
1018 $imageData = NULL;
1019
1020 if (preg_match('#^(http|ftp)s?://#i', $source) == 1) {
1021 $urlinfo = parse_url($source);
1022 $urlPath = pathinfo($urlinfo['path']);
1023
1024 if (strpos($urlinfo['path'], $baseDir."/") !== FALSE) {
1025 $internalSrc = $this->sanitizeFileName(urldecode(substr($urlinfo['path'], strpos($urlinfo['path'], $baseDir."/") + strlen($baseDir) + 1)));
1026 }
1027 $internalPath = $urlinfo["scheme"] . "/" . $urlinfo["host"] . "/" . pathinfo($urlinfo["path"], PATHINFO_DIRNAME);
1028 $isSourceExternal = TRUE;
1029 $imageData = $this->getImage($source);
1030 } else if (strpos($source, "/") === 0) {
1031 $internalPath = pathinfo($source, PATHINFO_DIRNAME);
1032
1033 $path = $source;
1034 if (!file_exists($path)) {
1035 $path = $this->docRoot . $path;
1036 }
1037
1038 $imageData = $this->getImage($path);
1039 } else {
1040 $internalPath = $htmlDir . "/" . preg_replace('#^[/\.]+#', '', pathinfo($source, PATHINFO_DIRNAME));
1041
1042 $path = $baseDir . "/" . $source;
1043 if (!file_exists($path)) {
1044 $path = $this->docRoot . $path;
1045 }
1046
1047 $imageData = $this->getImage($path);
1048 }
1049 if ($imageData !== FALSE) {
1050 $iSrcInfo = pathinfo($internalSrc);
1051 if (!empty($imageData['ext']) && $imageData['ext'] != $iSrcInfo['extension']) {
1052 $internalSrc = $iSrcInfo['filename'] . "." . $imageData['ext'];
1053 }
1054 $internalPath = Zip::getRelativePath("images/" . $internalPath . "/" . $internalSrc);
1055 if (!array_key_exists($internalPath, $this->fileList)) {
1056 $this->addFile($internalPath, "i_" . $internalSrc, $imageData['image'], $imageData['mime']);
1057 $this->fileList[$internalPath] = $source;
1058 }
1059 return TRUE;
1060 }
1061 return FALSE;
1062 }
1063
1064 /**
1065 * Resolve a media src and determine it's target location and add it to the book.
1066 *
1067 * @param string $source Source link.
1068 * @param string $internalPath (referenced) Return value, will be set to the target path and name in the book.
1069 * @param string $internalSrc (referenced) Return value, will be set to the target name in the book.
1070 * @param string $isSourceExternal (referenced) Return value, will be set to TRUE if the image originated from a full URL.
1071 * @param string $baseDir Default is "", meaning it is pointing to the document root.
1072 * @param string $htmlDir The path to the parent HTML file's directory from the root of the archive.
1073 * @param string $backPath The path to get back to the root of the archive from $htmlDir.
1074 */
1075 protected function resolveMedia($source, &$internalPath, &$internalSrc, &$isSourceExternal, $baseDir = "", $htmlDir = "", $backPath = "") {
1076 if ($this->isFinalized) {
1077 return FALSE;
1078 }
1079 $mediaPath = NULL;
1080 $tmpFile;
1081
1082 if (preg_match('#^(http|ftp)s?://#i', $source) == 1) {
1083 $urlinfo = parse_url($source);
1084
1085 if (strpos($urlinfo['path'], $baseDir."/") !== FALSE) {
1086 $internalSrc = substr($urlinfo['path'], strpos($urlinfo['path'], $baseDir."/") + strlen($baseDir) + 1);
1087 }
1088 $internalPath = $urlinfo["scheme"] . "/" . $urlinfo["host"] . "/" . pathinfo($urlinfo["path"], PATHINFO_DIRNAME);
1089 $isSourceExternal = TRUE;
1090 $mediaPath = $this->getFileContents($source, true);
1091 $tmpFile = $mediaPath;
1092 } else if (strpos($source, "/") === 0) {
1093 $internalPath = pathinfo($source, PATHINFO_DIRNAME);
1094
1095 $mediaPath = $source;
1096 if (!file_exists($mediaPath)) {
1097 $mediaPath = $this->docRoot . $mediaPath;
1098 }
1099 } else {
1100 $internalPath = $htmlDir . "/" . preg_replace('#^[/\.]+#', '', pathinfo($source, PATHINFO_DIRNAME));
1101
1102 $mediaPath = $baseDir . "/" . $source;
1103 if (!file_exists($mediaPath)) {
1104 $mediaPath = $this->docRoot . $mediaPath;
1105 }
1106 }
1107
1108 if ($mediaPath !== FALSE) {
1109 $mime = $this->getMime($source);
1110 $internalPath = Zip::getRelativePath("media/" . $internalPath . "/" . $internalSrc);
1111
1112 if (!array_key_exists($internalPath, $this->fileList) &&
1113 $this->addLargeFile($internalPath, "m_" . $internalSrc, $mediaPath, $mime)) {
1114 $this->fileList[$internalPath] = $source;
1115 }
1116 if (isset($tmpFile)) {
1117 unlink($tmpFile);
1118 }
1119 return TRUE;
1120 }
1121 return FALSE;
1122 }
1123
1124 /**
1125 * Get Book Chapter count.
1126 *
1127 * @access public
1128 * @return number of chapters
1129 */
1130 function getChapterCount() {
1131 return $this->chapterCount;
1132 }
1133
1134 /**
1135 * Book title, mandatory.
1136 *
1137 * Used for the dc:title metadata parameter in the OPF file as well as the DocTitle attribute in the NCX file.
1138 *
1139 * @param string $title
1140 * @access public
1141 * @return bool $success
1142 */
1143 function setTitle($title) {
1144 if ($this->isFinalized) {
1145 return FALSE;
1146 }
1147 $this->title = $title;
1148 return TRUE;
1149 }
1150
1151 /**
1152 * Get Book title.
1153 *
1154 * @access public
1155 * @return $title
1156 */
1157 function getTitle() {
1158 return $this->title;
1159 }
1160
1161 /**
1162 * Book language, mandatory
1163 *
1164 * Use the RFC3066 Language codes, such as "en", "da", "fr" etc.
1165 * Defaults to "en".
1166 *
1167 * Used for the dc:language metadata parameter in the OPF file.
1168 *
1169 * @param string $language
1170 * @access public
1171 * @return bool $success
1172 */
1173 function setLanguage($language) {
1174 if ($this->isFinalized || mb_strlen($language) != 2) {
1175 return FALSE;
1176 }
1177 $this->language = $language;
1178 return TRUE;
1179 }
1180
1181 /**
1182 * Get Book language.
1183 *
1184 * @access public
1185 * @return $language
1186 */
1187 function getLanguage() {
1188 return $this->language;
1189 }
1190
1191 /**
1192 * Unique book identifier, mandatory.
1193 * Use the URI, or ISBN if available.
1194 *
1195 * An unambiguous reference to the resource within a given context.
1196 *
1197 * Recommended best practice is to identify the resource by means of a
1198 * string conforming to a formal identification system.
1199 *
1200 * Used for the dc:identifier metadata parameter in the OPF file, as well
1201 * as dtb:uid in the NCX file.
1202 *
1203 * Identifier type should only be:
1204 * EPub::IDENTIFIER_URI
1205 * EPub::IDENTIFIER_ISBN
1206 * EPub::IDENTIFIER_UUID
1207 *
1208 * @param string $identifier
1209 * @param string $identifierType
1210 * @access public
1211 * @return bool $success
1212 */
1213 function setIdentifier($identifier, $identifierType) {
1214 if ($this->isFinalized || ($identifierType !== EPub::IDENTIFIER_URI && $identifierType !== EPub::IDENTIFIER_ISBN && $identifierType !== EPub::IDENTIFIER_UUID)) {
1215 return FALSE;
1216 }
1217 $this->identifier = $identifier;
1218 $this->identifierType = $identifierType;
1219 return TRUE;
1220 }
1221
1222 /**
1223 * Get Book identifier.
1224 *
1225 * @access public
1226 * @return $identifier
1227 */
1228 function getIdentifier() {
1229 return $this->identifier;
1230 }
1231
1232 /**
1233 * Get Book identifierType.
1234 *
1235 * @access public
1236 * @return $identifierType
1237 */
1238 function getIdentifierType() {
1239 return $this->identifierType;
1240 }
1241
1242 /**
1243 * Book description, optional.
1244 *
1245 * An account of the resource.
1246 *
1247 * Description may include but is not limited to: an abstract, a table of
1248 * contents, a graphical representation, or a free-text account of the
1249 * resource.
1250 *
1251 * Used for the dc:source metadata parameter in the OPF file
1252 *
1253 * @param string $description
1254 * @access public
1255 * @return bool $success
1256 */
1257 function setDescription($description) {
1258 if ($this->isFinalized) {
1259 return FALSE;
1260 }
1261 $this->description = $description;
1262 return TRUE;
1263 }
1264
1265 /**
1266 * Get Book description.
1267 *
1268 * @access public
1269 * @return $description
1270 */
1271 function getDescription() {
1272 return $this->description;
1273 }
1274
1275 /**
1276 * Book author or creator, optional.
1277 * The $authorSortKey is basically how the name is to be sorted, usually
1278 * it's "Lastname, First names" where the $author is the straight
1279 * "Firstnames Lastname"
1280 *
1281 * An entity primarily responsible for making the resource.
1282 *
1283 * Examples of a Creator include a person, an organization, or a service.
1284 * Typically, the name of a Creator should be used to indicate the entity.
1285 *
1286 * Used for the dc:creator metadata parameter in the OPF file and the
1287 * docAuthor attribure in the NCX file.
1288 * The sort key is used for the opf:file-as attribute in dc:creator.
1289 *
1290 * @param string $author
1291 * @param string $authorSortKey
1292 * @access public
1293 * @return bool $success
1294 */
1295 function setAuthor($author, $authorSortKey) {
1296 if ($this->isFinalized) {
1297 return FALSE;
1298 }
1299 $this->author = $author;
1300 $this->authorSortKey = $authorSortKey;
1301 return TRUE;
1302 }
1303
1304 /**
1305 * Get Book author.
1306 *
1307 * @access public
1308 * @return $author
1309 */
1310 function getAuthor() {
1311 return $this->author;
1312 }
1313
1314 /**
1315 * Publisher Information, optional.
1316 *
1317 * An entity responsible for making the resource available.
1318 *
1319 * Examples of a Publisher include a person, an organization, or a service.
1320 * Typically, the name of a Publisher should be used to indicate the entity.
1321 *
1322 * Used for the dc:publisher and dc:relation metadata parameters in the OPF file.
1323 *
1324 * @param string $publisherName
1325 * @param string $publisherURL
1326 * @access public
1327 * @return bool $success
1328 */
1329 function setPublisher($publisherName, $publisherURL) {
1330 if ($this->isFinalized) {
1331 return FALSE;
1332 }
1333 $this->publisherName = $publisherName;
1334 $this->publisherURL = $publisherURL;
1335 return TRUE;
1336 }
1337
1338 /**
1339 * Get Book publisherName.
1340 *
1341 * @access public
1342 * @return $publisherName
1343 */
1344 function getPublisherName() {
1345 return $this->publisherName;
1346 }
1347
1348 /**
1349 * Get Book publisherURL.
1350 *
1351 * @access public
1352 * @return $publisherURL
1353 */
1354 function getPublisherURL() {
1355 return $this->publisherURL;
1356 }
1357
1358 /**
1359 * Release date, optional. If left blank, the time of the finalization will
1360 * be used.
1361 *
1362 * A point or period of time associated with an event in the lifecycle of
1363 * the resource.
1364 *
1365 * Date may be used to express temporal information at any level of
1366 * granularity. Recommended best practice is to use an encoding scheme,
1367 * such as the W3CDTF profile of ISO 8601 [W3CDTF].
1368 *
1369 * Used for the dc:date metadata parameter in the OPF file
1370 *
1371 * @param long $timestamp
1372 * @access public
1373 * @return bool $success
1374 */
1375 function setDate($timestamp) {
1376 if ($this->isFinalized) {
1377 return FALSE;
1378 }
1379 $this->date = $timestamp;
1380 $this->opf->date = $timestamp;
1381 return TRUE;
1382 }
1383
1384 /**
1385 * Get Book date.
1386 *
1387 * @access public
1388 * @return $date
1389 */
1390 function getDate() {
1391 return $this->date;
1392 }
1393
1394 /**
1395 * Book (copy)rights, optional.
1396 *
1397 * Information about rights held in and over the resource.
1398 *
1399 * Typically, rights information includes a statement about various
1400 * property rights associated with the resource, including intellectual
1401 * property rights.
1402 *
1403 * Used for the dc:rights metadata parameter in the OPF file
1404 *
1405 * @param string $rightsText
1406 * @access public
1407 * @return bool $success
1408 */
1409 function setRights($rightsText) {
1410 if ($this->isFinalized) {
1411 return FALSE;
1412 }
1413 $this->rights = $rightsText;
1414 return TRUE;
1415 }
1416
1417 /**
1418 * Get Book rights.
1419 *
1420 * @access public
1421 * @return $rights
1422 */
1423 function getRights() {
1424 return $this->rights;
1425 }
1426
1427 /**
1428 * Add book Subject.
1429 *
1430 * The topic of the resource.
1431 *
1432 * Typically, the subject will be represented using keywords, key phrases,
1433 * or classification codes. Recommended best practice is to use a
1434 * controlled vocabulary. To describe the spatial or temporal topic of the
1435 * resource, use the Coverage element.
1436 *
1437 * @param string $subject
1438 */
1439 function setSubject($subject) {
1440 if ($this->isFinalized) {
1441 return;
1442 }
1443 $this->opf->addDCMeta(DublinCore::SUBJECT, $this->decodeHtmlEntities($subject));
1444 }
1445
1446 /**
1447 * Book source URL, optional.
1448 *
1449 * A related resource from which the described resource is derived.
1450 *
1451 * The described resource may be derived from the related resource in whole
1452 * or in part. Recommended best practice is to identify the related
1453 * resource by means of a string conforming to a formal identification system.
1454 *
1455 * Used for the dc:source metadata parameter in the OPF file
1456 *
1457 * @param string $sourceURL
1458 * @access public
1459 * @return bool $success
1460 */
1461 function setSourceURL($sourceURL) {
1462 if ($this->isFinalized) {
1463 return FALSE;
1464 }
1465 $this->sourceURL = $sourceURL;
1466 return TRUE;
1467 }
1468
1469 /**
1470 * Get Book sourceURL.
1471 *
1472 * @access public
1473 * @return $sourceURL
1474 */
1475 function getSourceURL() {
1476 return $this->sourceURL;
1477 }
1478
1479 /**
1480 * Coverage, optional.
1481 *
1482 * The spatial or temporal topic of the resource, the spatial applicability
1483 * of the resource, or the jurisdiction under which the resource is relevant.
1484 *
1485 * Spatial topic and spatial applicability may be a named place or a location
1486 * specified by its geographic coordinates. Temporal topic may be a named
1487 * period, date, or date range. A jurisdiction may be a named administrative
1488 * entity or a geographic place to which the resource applies. Recommended
1489 * best practice is to use a controlled vocabulary such as the Thesaurus of
1490 * Geographic Names [TGN]. Where appropriate, named places or time periods
1491 * can be used in preference to numeric identifiers such as sets of
1492 * coordinates or date ranges.
1493 *
1494 * Used for the dc:coverage metadata parameter in the OPF file
1495 *
1496 * Same as ->addDublinCoreMetadata(DublinCore::COVERAGE, $coverage);
1497 *
1498 * @param string $coverage
1499 * @access public
1500 * @return bool $success
1501 */
1502 function setCoverage($coverage) {
1503 if ($this->isFinalized) {
1504 return FALSE;
1505 }
1506 $this->coverage = $coverage;
1507 return TRUE;
1508 }
1509
1510 /**
1511 * Get Book coverage.
1512 *
1513 * @access public
1514 * @return $coverage
1515 */
1516 function getCoverage() {
1517 return $this->coverage;
1518 }
1519
1520 /**
1521 * Set book Relation.
1522 *
1523 * A related resource.
1524 *
1525 * Recommended best practice is to identify the related resource by means
1526 * of a string conforming to a formal identification system.
1527 *
1528 * @param string $relation
1529 */
1530 function setRelation($relation) {
1531 if ($this->isFinalized) {
1532 return;
1533 }
1534 $this->relation = $relation;
1535 }
1536
1537 /**
1538 * Get the book relation.
1539 *
1540 * @return string The relation.
1541 */
1542 function getRelation() {
1543 return $this->relation;
1544 }
1545
1546 /**
1547 * Set book Generator.
1548 *
1549 * The generator is a meta tag added to the ncx file, it is not visible
1550 * from within the book, but is a kind of electronic watermark.
1551 *
1552 * @param string $generator
1553 */
1554 function setGenerator($generator) {
1555 if ($this->isFinalized) {
1556 return;
1557 }
1558 $this->generator = $generator;
1559 }
1560
1561 /**
1562 * Get the book relation.
1563 *
1564 * @return string The generator identity string.
1565 */
1566 function getGenerator() {
1567 return $this->generator;
1568 }
1569
1570 /**
1571 * Set ePub date formate to the short yyyy-mm-dd form, for compliance with
1572 * a bug in EpubCheck, prior to its version 1.1.
1573 *
1574 * The latest version of ePubCheck can be obtained here:
1575 * http://code.google.com/p/epubcheck/
1576 *
1577 * @access public
1578 * @return bool $success
1579 */
1580 function setShortDateFormat() {
1581 if ($this->isFinalized) {
1582 return FALSE;
1583 }
1584 $this->dateformat = $this->dateformatShort;
1585 return TRUE;
1586 }
1587
1588 /**
1589 * @Deprecated
1590 */
1591 function setIgnoreEmptyBuffer($ignoreEmptyBuffer = TRUE) {
1592 die ("Function was deprecated, functionality is no longer needed.");
1593 }
1594
1595 /**
1596 * Set the references title for the ePub 3 landmarks section
1597 *
1598 * @param string $referencesTitle
1599 * @param string $referencesId
1600 * @param string $referencesClass
1601 * @return bool
1602 */
1603 function setReferencesTitle($referencesTitle = "Guide", $referencesId = "", $referencesClass = "references") {
1604 if ($this->isFinalized) {
1605 return FALSE;
1606 }
1607 $this->ncx->referencesTitle = is_string($referencesTitle) ? trim($referencesTitle) : "Guide";
1608 $this->ncx->referencesId = is_string($referencesId) ? trim($referencesId) : "references";
1609 $this->ncx->referencesClass = is_string($referencesClass) ? trim($referencesClass) : "references";
1610 return TRUE;
1611 }
1612
1613 /**
1614 * Set the references title for the ePub 3 landmarks section
1615 *
1616 * @param bool $referencesTitle
1617 */
1618 function setisReferencesAddedToToc($isReferencesAddedToToc = TRUE) {
1619 if ($this->isFinalized) {
1620 return FALSE;
1621 }
1622 $this->isReferencesAddedToToc = $isReferencesAddedToToc === TRUE;
1623 return TRUE;
1624 }
1625
1626 /**
1627 * Get Book status.
1628 *
1629 * @access public
1630 * @return bool
1631 */
1632 function isFinalized() {
1633 return $this->isFinalized;
1634 }
1635
1636 /**
1637 * Build the Table of Contents. This is not strictly necessary, as most eReaders will build it from the navigation structure in the .ncx file.
1638 *
1639 * @param string $cssFileName Include a link to this css file in the TOC html.
1640 * @param string $tocCSSClass The TOC is a <div>, if you need special formatting, you can add a css class for that div. Default is "toc".
1641 * @param string $title Title of the Table of contents. Default is "Table of Contents". Use this for ie. languages other than English.
1642 * @param bool $addReferences include reference pages in the TOC, using the $referencesOrder array to determine the order of the pages in the TOC. Default is TRUE.
1643 * @param bool $addToIndex Add the TOC to the NCX index at the current leve/position. Default is FALSE
1644 * @param string $tocFileName Change teh default name of the TOC file. The default is "TOC.xhtml"
1645 */
1646 function buildTOC($cssFileName = NULL, $tocCSSClass = "toc", $title = "Table of Contents", $addReferences = TRUE, $addToIndex = FALSE, $tocFileName = "TOC.xhtml") {
1647 if ($this->isFinalized) {
1648 return FALSE;
1649 }
1650 $this->buildTOC = TRUE;
1651 $this->tocTitle = $title;
1652 $this->tocFileName = $this->normalizeFileName($tocFileName);
1653 if (!empty($cssFileName)) {
1654 $this->tocCSSFileName = $this->normalizeFileName($cssFileName);
1655 }
1656 $this->tocCSSClass = $tocCSSClass;
1657 $this->tocAddReferences = $addReferences;
1658
1659 $this->opf->addItemRef("ref_" . Reference::TABLE_OF_CONTENTS, FALSE);
1660 $this->opf->addReference(Reference::TABLE_OF_CONTENTS, $title, $this->tocFileName);
1661
1662 if ($addToIndex) {
1663 $navPoint = new NavPoint($this->decodeHtmlEntities($title), $this->tocFileName, "ref_" . Reference::TABLE_OF_CONTENTS);
1664 $this->ncx->addNavPoint($navPoint);
1665 } else {
1666 $this->ncx->referencesList[Reference::TABLE_OF_CONTENTS] = $this->tocFileName;
1667 $this->ncx->referencesName[Reference::TABLE_OF_CONTENTS] = $title;
1668 }
1669 }
1670
1671 private function finalizeTOC() {
1672 if (!$this->buildTOC) {
1673 return FALSE;
1674 }
1675
1676 if (empty($this->tocTitle)) {
1677 $this->tocTitle = "Table of Contents";
1678 }
1679
1680 $tocData = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1681
1682 if ($this->isEPubVersion2()) {
1683 $tocData .= "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n"
1684 . " \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"
1685 . "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
1686 . "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n";
1687 } else {
1688 $tocData .= "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
1689 . "<head>\n<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n";
1690 }
1691
1692 if (!empty($this->tocCssFileName)) {
1693 $tocData .= "<link rel=\"stylesheet\" type=\"text/css\" href=\"" . $this->tocCssFileName . "\" />\n";
1694 }
1695
1696 $tocData .= "<title>" . $this->tocTitle . "</title>\n"
1697 . "</head>\n"
1698 . "<body>\n"
1699 . "<h3>" . $this->tocTitle . "</h3>\n<div";
1700
1701 if (!empty($this->tocCSSClass)) {
1702 $tocData .= " class=\"" . $this->tocCSSClass . "\"";
1703 }
1704 $tocData .= ">\n";
1705
1706 while (list($item, $descriptive) = each($this->referencesOrder)) {
1707 if ($item === "text") {
1708 while (list($chapterName, $navPoint) = each($this->ncx->chapterList)) {
1709 $fileName = $navPoint->getContentSrc();
1710 $level = $navPoint->getLevel() -2;
1711 $tocData .= "\t<p>" . str_repeat(" &#160; &#160; &#160;", $level) . "<a href=\"" . $this->sanitizeFileName($fileName) . "\">" . $chapterName . "</a></p>\n";
1712 }
1713 } else if ($this->tocAddReferences === TRUE) {
1714 if (array_key_exists($item, $this->ncx->referencesList)) {
1715 $tocData .= "\t<p><a href=\"" . $this->ncx->referencesList[$item] . "\">" . $descriptive . "</a></p>\n";
1716 } else if ($item === "toc") {
1717 $tocData .= "\t<p><a href=\"TOC.xhtml\">" . $this->tocTitle . "</a></p>\n";
1718 } else if ($item === "cover" && $this->isCoverImageSet) {
1719 $tocData .= "\t<p><a href=\"CoverPage.xhtml\">" . $descriptive . "</a></p>\n";
1720 }
1721 }
1722 }
1723 $tocData .= "</div>\n</body>\n</html>\n";
1724
1725 $this->addReferencePage($this->tocTitle, $this->tocFileName, $tocData, Reference::TABLE_OF_CONTENTS);
1726
1727 }
1728
1729 /**
1730 * @return bool
1731 */
1732 function isEPubVersion2() {
1733 return $this->bookVersion === EPub::BOOK_VERSION_EPUB2;
1734 }
1735
1736 /**
1737 * @param string $cssFileName
1738 * @param string $title
1739 * @return string
1740 */
1741 function buildEPub3TOC($cssFileName = NULL, $title = "Table of Contents") {
1742 $this->ncx->referencesOrder = $this->referencesOrder;
1743 $this->ncx->setDocTitle($this->decodeHtmlEntities($this->title));
1744 return $this->ncx->finalizeEPub3($title, $cssFileName);
1745 }
1746
1747 /**
1748 * @param string $fileName
1749 * @param string $tocData
1750 * @return bool
1751 */
1752 function addEPub3TOC($fileName, $tocData) {
1753 if ($this->isEPubVersion2() || $this->isFinalized || array_key_exists($fileName, $this->fileList)) {
1754 return FALSE;
1755 }
1756 $fileName = Zip::getRelativePath($fileName);
1757 $fileName = preg_replace('#^[/\.]+#i', "", $fileName);
1758
1759 $this->zip->addFile($tocData, $this->bookRoot.$fileName);
1760
1761 $this->fileList[$fileName] = $fileName;
1762 $this->opf->addItem("toc", $fileName, "application/xhtml+xml", "nav");
1763 return TRUE;
1764 }
1765
1766 /**
1767 * Check for mandatory parameters and finalize the e-book.
1768 * Once finalized, the book is locked for further additions.
1769 *
1770 * @return bool $success
1771 */
1772 function finalize() {
1773 if ($this->isFinalized || $this->chapterCount == 0 || empty($this->title) || empty($this->language)) {
1774 return FALSE;
1775 }
1776
1777 if (empty($this->identifier) || empty($this->identifierType)) {
1778 $this->setIdentifier($this->createUUID(4), EPub::IDENTIFIER_UUID);
1779 }
1780
1781 if ($this->date == 0) {
1782 $this->date = time();
1783 }
1784
1785 if (empty($this->sourceURL)) {
1786 $this->sourceURL = $this->getCurrentPageURL();
1787 }
1788
1789 if (empty($this->publisherURL)) {
1790 $this->sourceURL = $this->getCurrentServerURL();
1791 }
1792
1793 // Generate OPF data:
1794 $this->opf->setIdent("BookId");
1795 $this->opf->initialize($this->title, $this->language, $this->identifier, $this->identifierType);
1796
1797 $DCdate = new DublinCore(DublinCore::DATE, gmdate($this->dateformat, $this->date));
1798 $DCdate->addOpfAttr("event", "publication");
1799 $this->opf->metadata->addDublinCore($DCdate);
1800
1801 if (!empty($this->description)) {
1802 $this->opf->addDCMeta(DublinCore::DESCRIPTION, $this->decodeHtmlEntities($this->description));
1803 }
1804
1805 if (!empty($this->publisherName)) {
1806 $this->opf->addDCMeta(DublinCore::PUBLISHER, $this->decodeHtmlEntities($this->publisherName));
1807 }
1808
1809 if (!empty($this->publisherURL)) {
1810 $this->opf->addDCMeta(DublinCore::RELATION, $this->decodeHtmlEntities($this->publisherURL));
1811 }
1812
1813 if (!empty($this->author)) {
1814 $author = $this->decodeHtmlEntities($this->author);
1815 $this->opf->addCreator($author, $this->decodeHtmlEntities($this->authorSortKey), MarcCode::AUTHOR);
1816 $this->ncx->setDocAuthor($author);
1817 }
1818
1819 if (!empty($this->rights)) {
1820 $this->opf->addDCMeta(DublinCore::RIGHTS, $this->decodeHtmlEntities($this->rights));
1821 }
1822
1823 if (!empty($this->coverage)) {
1824 $this->opf->addDCMeta(DublinCore::COVERAGE, $this->decodeHtmlEntities($this->coverage));
1825 }
1826
1827 if (!empty($this->sourceURL)) {
1828 $this->opf->addDCMeta(DublinCore::SOURCE, $this->sourceURL);
1829 }
1830
1831 if (!empty($this->relation)) {
1832 $this->opf->addDCMeta(DublinCore::RELATION, $this->decodeHtmlEntities($this->relation));
1833 }
1834
1835 if ($this->isCoverImageSet) {
1836 $this->opf->addMeta("cover", "coverImage");
1837 }
1838
1839 if (!empty($this->generator)) {
1840 $gen = $this->decodeHtmlEntities($this->generator);
1841 $this->opf->addMeta("generator", $gen);
1842 $this->ncx->addMetaEntry("dtb:generator", $gen);
1843 }
1844
1845 if ($this->EPubMark) {
1846 $this->opf->addMeta("generator", "EPub (Version " . self::VERSION . ") by A. Grandt, http://www.phpclasses.org/package/6115");
1847 }
1848
1849 reset($this->ncx->chapterList);
1850 list($firstChapterName, $firstChapterNavPoint) = each($this->ncx->chapterList);
1851 $firstChapterFileName = $firstChapterNavPoint->getContentSrc();
1852 $this->opf->addReference(Reference::TEXT, $this->decodeHtmlEntities($firstChapterName), $firstChapterFileName);
1853
1854 $this->ncx->setUid($this->identifier);
1855
1856 $this->ncx->setDocTitle($this->decodeHtmlEntities($this->title));
1857
1858 $this->ncx->referencesOrder = $this->referencesOrder;
1859 if ($this->isReferencesAddedToToc) {
1860 $this->ncx->finalizeReferences();
1861 }
1862
1863 $this->finalizeTOC();
1864
1865 if (!$this->isEPubVersion2()) {
1866 $this->addEPub3TOC("epub3toc.xhtml", $this->buildEPub3TOC());
1867 }
1868
1869 $opfFinal = $this->fixEncoding($this->opf->finalize());
1870 $ncxFinal = $this->fixEncoding($this->ncx->finalize());
1871
1872 if (mb_detect_encoding($opfFinal, 'UTF-8', true) === "UTF-8") {
1873 $this->zip->addFile($opfFinal, $this->bookRoot."book.opf");
1874 } else {
1875 $this->zip->addFile(mb_convert_encoding($opfFinal, "UTF-8"), $this->bookRoot."book.opf");
1876 }
1877
1878 if (mb_detect_encoding($ncxFinal, 'UTF-8', true) === "UTF-8") {
1879 $this->zip->addFile($ncxFinal, $this->bookRoot."book.ncx");
1880 } else {
1881 $this->zip->addFile(mb_convert_encoding($ncxFinal, "UTF-8"), $this->bookRoot."book.ncx");
1882 }
1883
1884 $this->opf = NULL;
1885 $this->ncx = NULL;
1886
1887 $this->isFinalized = TRUE;
1888 return TRUE;
1889 }
1890
1891 /**
1892 * Ensure the encoded string is a valid UTF-8 string.
1893 *
1894 * Note, that a mb_detect_encoding on the returned string will still return ASCII if the entire string is comprized of characters in the 1-127 range.
1895 *
1896 * @link: http://snippetdb.com/php/convert-string-to-utf-8-for-mysql
1897 * @param string $in_str
1898 * @return string converted string.
1899 */
1900 function fixEncoding($in_str) {
1901 if (mb_detect_encoding($in_str) == "UTF-8" && mb_check_encoding($in_str,"UTF-8")) {
1902 return $in_str;
1903 } else {
1904 return utf8_encode($in_str);
1905 }
1906 }
1907
1908 /**
1909 * Return the finalized book.
1910 *
1911 * @return string with the book in binary form.
1912 */
1913 function getBook() {
1914 if (!$this->isFinalized) {
1915 $this->finalize();
1916 }
1917
1918 return $this->zip->getZipData();
1919 }
1920
1921 /**
1922 * Remove disallowed characters from string to get a nearly safe filename
1923 *
1924 * @param string $fileName
1925 * @return mixed|string
1926 */
1927 function sanitizeFileName($fileName) {
1928 $fileName1 = str_replace($this->forbiddenCharacters, '', $fileName);
1929 $fileName2 = preg_replace('/[\s-]+/', '-', $fileName1);
1930 return trim($fileName2, '.-_');
1931
1932 }
1933
1934 /**
1935 * Cleanup the filepath, and remove leading . and / characters.
1936 *
1937 * Sometimes, when a path is generated from multiple fragments,
1938 * you can get something like "../data/html/../images/image.jpeg"
1939 * ePub files don't work well with that, this will normalize that
1940 * example path to "data/images/image.jpeg"
1941 *
1942 * @param string $fileName
1943 * @return string normalized filename
1944 */
1945 function normalizeFileName($fileName) {
1946 return preg_replace('#^[/\.]+#i', "", Zip::getRelativePath($fileName));
1947 }
1948
1949 /**
1950 * Save the ePub file to local disk.
1951 *
1952 * @param string $fileName
1953 * @param string $baseDir If empty baseDir is absolute to server path, if omitted it's relative to script path
1954 * @return The sent file name if successfull, FALSE if it failed.
1955 */
1956 function saveBook($fileName, $baseDir = '.') {
1957
1958 // Make fileName safe
1959 $fileName = $this->sanitizeFileName($fileName);
1960
1961 // Finalize book, if it's not done already
1962 if (!$this->isFinalized) {
1963 $this->finalize();
1964 }
1965
1966 if (stripos(strrev($fileName), "bupe.") !== 0) {
1967 $fileName .= ".epub";
1968 }
1969
1970 // Try to open file access
1971 $fh = fopen($baseDir.'/'.$fileName, "w");
1972
1973 if ($fh) {
1974 fputs($fh, $this->getBook());
1975 fclose($fh);
1976
1977 // if file is written return TRUE
1978 return $fileName;
1979 }
1980
1981 // return FALSE by default
1982 return FALSE;
1983 }
1984
1985 /**
1986 * Return the finalized book size.
1987 *
1988 * @return string
1989 */
1990 function getBookSize() {
1991 if (!$this->isFinalized) {
1992 $this->finalize();
1993 }
1994
1995 return $this->zip->getArchiveSize();
1996 }
1997
1998 /**
1999 * Send the book as a zip download
2000 *
2001 * Sending will fail if the output buffer is in use. You can override this limit by
2002 * calling setIgnoreEmptyBuffer(TRUE), though the function will still fail if that
2003 * buffer is not empty.
2004 *
2005 * @param string $fileName The name of the book without the .epub at the end.
2006 * @return The sent file name if successfull, FALSE if it failed.
2007 */
2008 function sendBook($fileName) {
2009 if (!$this->isFinalized) {
2010 $this->finalize();
2011 }
2012
2013 if (stripos(strrev($fileName), "bupe.") !== 0) {
2014 $fileName .= ".epub";
2015 }
2016
2017 if (TRUE === $this->zip->sendZip($fileName, "application/epub+zip")) {
2018 return $fileName;
2019 }
2020 return FALSE;
2021 }
2022
2023 /**
2024 * Generates an UUID.
2025 *
2026 * Default version (4) will generate a random UUID, version 3 will URL based UUID.
2027 *
2028 * Added for convinience
2029 *
2030 * @param int $bookVersion UUID version to retrieve, See lib.uuid.manual.html for details.
2031 * @param string $url
2032 * @return string The formatted uuid
2033 */
2034 function createUUID($bookVersion = 4, $url = NULL) {
2035 include_once("lib.uuid.php");
2036 return UUID::mint($bookVersion, $url, UUID::nsURL);
2037 }
2038
2039 /**
2040 * Get the url of the current page.
2041 * Example use: Default Source URL
2042 *
2043 * $return string Page URL.
2044 */
2045 function getCurrentPageURL() {
2046 $pageURL = $this->getCurrentServerURL() . filter_input(INPUT_SERVER, "REQUEST_URI");
2047 return $pageURL;
2048 }
2049
2050 /**
2051 * Get the url of the server.
2052 * Example use: Default Publisher URL
2053 *
2054 * $return string Server URL.
2055 */
2056 function getCurrentServerURL() {
2057 $serverURL = 'http';
2058 $https = filter_input(INPUT_SERVER, "HTTPS");
2059 $port = filter_input(INPUT_SERVER, "SERVER_PORT");
2060
2061 if ($https === "on") {
2062 $serverURL .= "s";
2063 }
2064 $serverURL .= "://" . filter_input(INPUT_SERVER, "SERVER_NAME");
2065 if ($port != "80") {
2066 $serverURL .= ":" . $port;
2067 }
2068 return $serverURL . '/';
2069 }
2070
2071 /**
2072 * Try to determine the mimetype of the file path.
2073 *
2074 * @param string $source Path
2075 * @return string mimetype, or FALSE.
2076 */
2077 function getMime($source) {
2078 return $this->mimetypes[pathinfo($source, PATHINFO_EXTENSION)];
2079 }
2080
2081 /**
2082 * Get an image from a file or url, return it resized if the image exceeds the $maxImageWidth or $maxImageHeight directives.
2083 *
2084 * The return value is an array.
2085 * ['width'] is the width of the image.
2086 * ['height'] is the height of the image.
2087 * ['mime'] is the mime type of the image. Resized images are always in jpeg format.
2088 * ['image'] is the image data.
2089 * ['ext'] is the extension of the image file.
2090 *
2091 * @param string $source path or url to file.
2092 * $return array
2093 */
2094 function getImage($source) {
2095 $width = -1;
2096 $height = -1;
2097 $mime = "application/octet-stream";
2098 $type = FALSE;
2099 $ext = "";
2100
2101
2102 $image = $this->getFileContents($source);
2103
2104 if ($image !== FALSE && strlen($image) > 0) {
2105 $imageFile = imagecreatefromstring($image);
2106 if ($imageFile !== false) {
2107 $width = ImageSX($imageFile);
2108 $height = ImageSY($imageFile);
2109 }
2110 if ($this->isExifInstalled) {
2111 @$type = exif_imagetype($source);
2112 $mime = image_type_to_mime_type($type);
2113 }
2114 if ($mime === "application/octet-stream") {
2115 $mime = $this->image_file_type_from_binary($image);
2116 }
2117 if ($mime === "application/octet-stream") {
2118 $mime = $this->getMimeTypeFromUrl($source);
2119 }
2120 } else {
2121 return FALSE;
2122 }
2123
2124 if ($width <= 0 || $height <= 0) {
2125 return FALSE;
2126 }
2127
2128 $ratio = 1;
2129
2130 if ($this->isGdInstalled) {
2131 if ($width > $this->maxImageWidth) {
2132 $ratio = $this->maxImageWidth/$width;
2133 }
2134 if ($height*$ratio > $this->maxImageHeight) {
2135 $ratio = $this->maxImageHeight/$height;
2136 }
2137
2138 if ($ratio < 1 || empty($mime) || ($this->isGifImagesEnabled !== FALSE && $mime == "image/gif")) {
2139 $image_o = imagecreatefromstring($image);
2140 $image_p = imagecreatetruecolor($width*$ratio, $height*$ratio);
2141
2142 if ($mime == "image/png") {
2143 imagealphablending($image_p, false);
2144 imagesavealpha($image_p, true);
2145 imagealphablending($image_o, true);
2146
2147 imagecopyresampled($image_p, $image_o, 0, 0, 0, 0, ($width*$ratio), ($height*$ratio), $width, $height);
2148 ob_start();
2149 imagepng($image_p, NULL, 9);
2150 $image = ob_get_contents();
2151 ob_end_clean();
2152
2153 $ext = "png";
2154 } else {
2155 imagecopyresampled($image_p, $image_o, 0, 0, 0, 0, ($width*$ratio), ($height*$ratio), $width, $height);
2156 ob_start();
2157 imagejpeg($image_p, NULL, 80);
2158 $image = ob_get_contents();
2159 ob_end_clean();
2160
2161 $mime = "image/jpeg";
2162 $ext = "jpg";
2163 }
2164 imagedestroy($image_o);
2165 imagedestroy($image_p);
2166 }
2167 }
2168
2169 if ($ext === "") {
2170 static $mimeToExt = array (
2171 'image/jpeg' => 'jpg',
2172 'image/gif' => 'gif',
2173 'image/png' => 'png'
2174 );
2175
2176 if (isset($mimeToExt[$mime])) {
2177 $ext = $mimeToExt[$mime];
2178 }
2179 }
2180
2181 $rv = array();
2182 $rv['width'] = $width*$ratio;
2183 $rv['height'] = $height*$ratio;
2184 $rv['mime'] = $mime;
2185 $rv['image'] = $image;
2186 $rv['ext'] = $ext;
2187
2188 return $rv;
2189 }
2190
2191 /**
2192 * Get file contents, using curl if available, else file_get_contents
2193 *
2194 * @param string $source
2195 * @return bool
2196 */
2197 function getFileContents($source, $toTempFile = FALSE) {
2198 $isExternal = preg_match('#^(http|ftp)s?://#i', $source) == 1;
2199
2200 if ($isExternal && $this->isCurlInstalled) {
2201 $ch = curl_init();
2202 $outFile = NULL;
2203 $fp = NULL;
2204 $res = FALSE;
2205 $info = array('http_code' => 500);
2206
2207 curl_setopt($ch, CURLOPT_HEADER, 0);
2208 curl_setopt($ch, CURLOPT_URL, str_replace(" ","%20",$source));
2209 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
2210 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
2211 curl_setopt($ch, CURLOPT_BUFFERSIZE, 4096);
2212
2213 if ($toTempFile) {
2214 $outFile = tempnam(sys_get_temp_dir(), "EPub_v" . EPub::VERSION . "_");
2215 $fp = fopen($outFile, "w+b");
2216 curl_setopt($ch, CURLOPT_FILE, $fp);
2217
2218 $res = curl_exec($ch);
2219 $info = curl_getinfo($ch);
2220
2221 curl_close($ch);
2222 fclose($fp);
2223 } else {
2224 $res = curl_exec($ch);
2225 $info = curl_getinfo($ch);
2226
2227 curl_close($ch);
2228 }
2229
2230 if ($info['http_code'] == 200 && $res != false) {
2231 if ($toTempFile) {
2232 return $outFile;
2233 }
2234 return $res;
2235 }
2236 return FALSE;
2237 }
2238
2239 if ($this->isFileGetContentsInstalled && (!$isExternal || $this->isFileGetContentsExtInstalled)) {
2240 @$data = file_get_contents($source);
2241 return $data;
2242 }
2243 return FALSE;
2244 }
2245
2246 /**
2247 * get mime type from image data
2248 *
2249 * By fireweasel found on http://stackoverflow.com/questions/2207095/get-image-mimetype-from-resource-in-php-gd
2250 * @staticvar array $type
2251 * @param object $binary
2252 * @return string
2253 */
2254 function image_file_type_from_binary($binary) {
2255 $hits = 0;
2256 if (!preg_match(
2257 '/\A(?:(\xff\xd8\xff)|(GIF8[79]a)|(\x89PNG\x0d\x0a)|(BM)|(\x49\x49(?:\x2a\x00|\x00\x4a))|(FORM.{4}ILBM))/',
2258 $binary, $hits)) {
2259 return 'application/octet-stream';
2260 }
2261 static $type = array (
2262 1 => 'image/jpeg',
2263 2 => 'image/gif',
2264 3 => 'image/png',
2265 4 => 'image/x-windows-bmp',
2266 5 => 'image/tiff',
2267 6 => 'image/x-ilbm',
2268 );
2269 return $type[count($hits) - 1];
2270 }
2271
2272 /**
2273 * @param string $source URL Source
2274 * @return string MimeType
2275 */
2276 function getMimeTypeFromUrl($source) {
2277 $ext = FALSE;
2278
2279 $srev = strrev($source);
2280 $pos = strpos($srev, "?");
2281 if ($pos !== FALSE) {
2282 $srev = substr($srev, $pos+1);
2283 }
2284
2285 $pos = strpos($srev, ".");
2286 if ($pos !== FALSE) {
2287 $ext = strtolower(strrev(substr($srev, 0, $pos)));
2288 }
2289
2290 if ($ext !== FALSE) {
2291 return $this->getMimeTypeFromExtension($ext);
2292 }
2293 return "application/octet-stream";
2294 }
2295
2296 /**
2297 * @param string $ext Extension
2298 * @return string MimeType
2299 */
2300 function getMimeTypeFromExtension($ext) {
2301 switch ($ext) {
2302 case "jpg":
2303 case "jpe":
2304 case "jpeg":
2305 return 'image/jpeg';
2306 case "gif":
2307 return 'image/gif';
2308 case "png":
2309 return 'image/png';
2310 case "bmp":
2311 return 'image/x-windows-bmp';
2312 case "tif":
2313 case "tiff":
2314 case "cpt":
2315 return 'image/tiff';
2316 case "lbm":
2317 case "ilbm":
2318 return 'image/x-ilbm';
2319 default:
2320 return "application/octet-stream";
2321 }
2322 }
2323
2324 /**
2325 * Encode html code to use html entities, safeguarding it from potential character encoding peoblems
2326 * This function is a bit different from the vanilla htmlentities function in that it does not encode html tags.
2327 *
2328 * The regexp is taken from the PHP Manual discussion, it was written by user "busbyjon".
2329 * http://www.php.net/manual/en/function.htmlentities.php#90111
2330 *
2331 * @param string $string string to encode.
2332 */
2333 public function encodeHtml($string) {
2334 $string = strtr($string, $this->html_encoding_characters);
2335
2336 //return preg_replace("/&amp;(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,5};)/", "&\\1", $string);
2337 //return preg_replace("/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,5};)/", "&amp;", $string);
2338 return $string;
2339 }
2340
2341 /**
2342 * Helper function to create a DOM fragment with given markup.
2343 *
2344 * @author Adam Schmalhofer
2345 *
2346 * @param DOMDocument $dom
2347 * @param string $markup
2348 * @return DOMNode fragment in a node.
2349 */
2350 protected function createDomFragment($dom, $markup) {
2351 $node = $dom->createDocumentFragment();
2352 $node->appendXML($markup);
2353 return $node;
2354 }
2355
2356 /**
2357 * Retrieve an array of file names currently added to the book.
2358 * $key is the filename used in the book
2359 * $value is the original filename, will be the same as $key for most entries
2360 *
2361 * @return array file list
2362 */
2363 function getFileList() {
2364 return $this->fileList;
2365 }
2366
2367 /**
2368 * @deprecated Use Zip::getRelativePath($relPath) instead.
2369 */
2370 function relPath($relPath) {
2371 die ("Function was deprecated, use Zip::getRelativePath(\$relPath); instead");
2372 }
2373
2374 /**
2375 * Set default chapter target size.
2376 * Default is 250000 bytes, and minimum is 10240 bytes.
2377 *
2378 * @param int $size segment size in bytes
2379 * @return void
2380 */
2381 function setSplitSize($size) {
2382 $this->splitDefaultSize = (int)$size;
2383 if ($size < 10240) {
2384 $this->splitDefaultSize = 10240; // Making the file smaller than 10k is not a good idea.
2385 }
2386 }
2387
2388 /**
2389 * Get the chapter target size.
2390 *
2391 * @return $size
2392 */
2393 function getSplitSize() {
2394 return $this->splitDefaultSize;
2395 }
2396
2397 /**
2398 * Remove all non essential html tags and entities.
2399 *
2400 * @global type $htmlEntities
2401 * @param string $string
2402 * @return string with the stripped entities.
2403 */
2404 function decodeHtmlEntities($string) {
2405 global $htmlEntities;
2406
2407 $string = preg_replace('~\s*<br\s*/*\s*>\s*~i', "\n", $string);
2408 $string = preg_replace('~\s*</(p|div)\s*>\s*~i', "\n\n", $string);
2409 $string = preg_replace('~<[^>]*>~', '', $string);
2410
2411 $string = strtr($string, $htmlEntities);
2412
2413 $string = str_replace('&', '&amp;', $string);
2414 $string = str_replace('&amp;amp;', '&amp;', $string);
2415 $string = preg_replace('~&amp;(#x*[a-fA-F0-9]+;)~', '&\1', $string);
2416 $string = str_replace('<', '&lt;', $string);
2417 $string = str_replace('>', '&gt;', $string);
2418
2419 return $string;
2420 }
2421
2422 /**
2423 * Simply remove all HTML tags, brute force and no finesse.
2424 *
2425 * @param string $string html
2426 * @return string
2427 */
2428 function html2text($string) {
2429 return preg_replace('~<[^>]*>~', '', $string);
2430 }
2431
2432 /**
2433 * @return string
2434 */
2435 function getLog() {
2436 return $this->log->getLog();
2437 }
2438}
diff --git a/inc/3rdparty/libraries/PHPePub/EPubChapterSplitter.php b/inc/3rdparty/libraries/PHPePub/EPubChapterSplitter.php
new file mode 100644
index 00000000..1d44f238
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/EPubChapterSplitter.php
@@ -0,0 +1,201 @@
1<?php
2/**
3 * Split an HTML file into smaller html files, retaining the formatting and structure for the individual parts.
4 * What this splitter does is using DOM to try and retain any formatting in the file, including rebuilding the DOM tree for subsequent parts.
5 * Split size is considered max target size. The actual size is the result of an even split across the resulting files.
6 *
7 * @author A. Grandt <php@grandt.com>
8 * @copyright 2009-2014 A. Grandt
9 * @license GNU LGPL 2.1
10 * @link http://www.phpclasses.org/package/6115
11 * @link https://github.com/Grandt/PHPePub
12 * @version 3.20
13 */
14class EPubChapterSplitter {
15 const VERSION = 3.20;
16
17 private $splitDefaultSize = 250000;
18 private $bookVersion = EPub::BOOK_VERSION_EPUB2;
19
20 /**
21 *
22 * Enter description here ...
23 *
24 * @param unknown_type $ident
25 */
26 function setVersion($bookVersion) {
27 $this->bookVersion = is_string($bookVersion) ? trim($bookVersion) : EPub::BOOK_VERSION_EPUB2;
28 }
29
30 /**
31 * Set default chapter target size.
32 * Default is 250000 bytes, and minimum is 10240 bytes.
33 *
34 * @param $size segment size in bytes
35 * @return void
36 */
37 function setSplitSize($size) {
38 $this->splitDefaultSize = (int)$size;
39 if ($size < 10240) {
40 $this->splitDefaultSize = 10240; // Making the file smaller than 10k is not a good idea.
41 }
42 }
43
44 /**
45 * Get the chapter target size.
46 *
47 * @return $size
48 */
49 function getSplitSize() {
50 return $this->splitDefaultSize;
51 }
52
53 /**
54 * Split $chapter into multiple parts.
55 *
56 * The search string can either be a regular string or a PHP PECL Regular Expression pattern as defined here: http://www.php.net/manual/en/pcre.pattern.php
57 * If the search string is a regular string, the matching will be for lines in the HTML starting with the string given
58 *
59 * @param String $chapter XHTML file
60 * @param Bool $splitOnSearchString Split on chapter boundaries, Splitting on search strings disables the split size check.
61 * @param String $searchString Chapter string to search for can be fixed text, or a regular expression pattern.
62 *
63 * @return array with 1 or more parts
64 */
65 function splitChapter($chapter, $splitOnSearchString = false, $searchString = '/^Chapter\\ /i') {
66 $chapterData = array();
67 $isSearchRegexp = $splitOnSearchString && (preg_match('#^(\D|\S|\W).+\1[imsxeADSUXJu]*$#m', $searchString) == 1);
68 if ($splitOnSearchString && !$isSearchRegexp) {
69 $searchString = '#^<.+?>' . preg_quote($searchString, '#') . "#";
70 }
71
72 if (!$splitOnSearchString && strlen($chapter) <= $this->splitDefaultSize) {
73 return array($chapter);
74 }
75
76 $xmlDoc = new DOMDocument();
77 @$xmlDoc->loadHTML($chapter);
78
79 $head = $xmlDoc->getElementsByTagName("head");
80 $body = $xmlDoc->getElementsByTagName("body");
81
82 $htmlPos = stripos($chapter, "<html");
83 $htmlEndPos = stripos($chapter, ">", $htmlPos);
84 $newXML = substr($chapter, 0, $htmlEndPos+1) . "\n</html>";
85 if (strpos(trim($newXML), "<?xml ") === FALSE) {
86 $newXML = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" . $newXML;
87 }
88 $headerLength = strlen($newXML);
89
90 $files = array();
91 $chapterNames = array();
92 $domDepth = 0;
93 $domPath = array();
94 $domClonedPath = array();
95
96 $curFile = $xmlDoc->createDocumentFragment();
97 $files[] = $curFile;
98 $curParent = $curFile;
99 $curSize = 0;
100
101 $bodyLen = strlen($xmlDoc->saveXML($body->item(0)));
102 $headLen = strlen($xmlDoc->saveXML($head->item(0))) + $headerLength;
103
104 $partSize = $this->splitDefaultSize - $headLen;
105
106 if ($bodyLen > $partSize) {
107 $parts = ceil($bodyLen / $partSize);
108 $partSize = ($bodyLen / $parts) - $headLen;
109 }
110
111 $node = $body->item(0)->firstChild;
112
113 do {
114 $nodeData = $xmlDoc->saveXML($node);
115 $nodeLen = strlen($nodeData);
116
117 if ($nodeLen > $partSize && $node->hasChildNodes()) {
118 $domPath[] = $node;
119 $domClonedPath[] = $node->cloneNode(false);
120 $domDepth++;
121
122 $node = $node->firstChild;
123 }
124
125 $node2 = $node->nextSibling;
126
127 if ($node != null && $node->nodeName != "#text") {
128 $doSplit = false;
129 if ($splitOnSearchString) {
130 $doSplit = preg_match($searchString, $nodeData) == 1;
131 if ($doSplit) {
132 $chapterNames[] = trim($nodeData);
133 }
134 }
135
136 if ($curSize > 0 && ($doSplit || (!$splitOnSearchString && $curSize + $nodeLen > $partSize))) {
137 $curFile = $xmlDoc->createDocumentFragment();
138 $files[] = $curFile;
139 $curParent = $curFile;
140 if ($domDepth > 0) {
141 reset($domPath);
142 reset($domClonedPath);
143 $oneDomClonedPath = each($domClonedPath);
144 while ($oneDomClonedPath) {
145 list($k, $v) = $oneDomClonedPath;
146 $newParent = $v->cloneNode(false);
147 $curParent->appendChild($newParent);
148 $curParent = $newParent;
149 $oneDomClonedPath = each($domClonedPath);
150 }
151 }
152 $curSize = strlen($xmlDoc->saveXML($curFile));
153 }
154 $curParent->appendChild($node->cloneNode(true));
155 $curSize += $nodeLen;
156 }
157
158 $node = $node2;
159 while ($node == null && $domDepth > 0) {
160 $domDepth--;
161 $node = end($domPath)->nextSibling;
162 array_pop($domPath);
163 array_pop($domClonedPath);
164 $curParent = $curParent->parentNode;
165 }
166 } while ($node != null);
167
168 $curFile = null;
169 $curSize = 0;
170
171 $xml = new DOMDocument('1.0', $xmlDoc->xmlEncoding);
172 $xml->lookupPrefix("http://www.w3.org/1999/xhtml");
173 $xml->preserveWhiteSpace = false;
174 $xml->formatOutput = true;
175
176 for ($idx = 0; $idx < count($files); $idx++) {
177 $xml2Doc = new DOMDocument('1.0', $xmlDoc->xmlEncoding);
178 $xml2Doc->lookupPrefix("http://www.w3.org/1999/xhtml");
179 $xml2Doc->loadXML($newXML);
180 $html = $xml2Doc->getElementsByTagName("html")->item(0);
181 $html->appendChild($xml2Doc->importNode($head->item(0), true));
182 $body = $xml2Doc->createElement("body");
183 $html->appendChild($body);
184 $body->appendChild($xml2Doc->importNode($files[$idx], true));
185
186 // force pretty printing and correct formatting, should not be needed, but it is.
187 $xml->loadXML($xml2Doc->saveXML());
188
189 $doc = $xml->saveXML();
190
191 if ($this->bookVersion === EPub::BOOK_VERSION_EPUB3) {
192 $doc = preg_replace('#^\s*<!DOCTYPE\ .+?>\s*#im', '', $doc);
193 }
194
195 $chapterData[$splitOnSearchString ? $chapterNames[$idx] : $idx] = $doc;
196 }
197
198 return $chapterData;
199 }
200}
201?>
diff --git a/inc/3rdparty/libraries/PHPePub/Logger.php b/inc/3rdparty/libraries/PHPePub/Logger.php
new file mode 100644
index 00000000..314019cb
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/Logger.php
@@ -0,0 +1,92 @@
1<?php
2/**
3 * Simple log line aggregator.
4 *
5 * @author A. Grandt <php@grandt.com>
6 * @copyright 2012-2013 A. Grandt
7 * @license GNU LGPL, Attribution required for commercial implementations, requested for everything else.
8 * @version 1.00
9 */
10class Logger {
11 const VERSION = 1.00;
12
13 private $log = "";
14 private $tStart;
15 private $tLast;
16 private $name = NULL;
17 private $isLogging = FALSE;
18 private $isDebugging = FALSE;
19
20 /**
21 * Class constructor.
22 *
23 * @return void
24 */
25 function __construct($name = NULL, $isLogging = FALSE) {
26 if ($name === NULL) {
27 $this->name = "";
28 } else {
29 $this->name = $name . " : ";
30 }
31 $this->isLogging = $isLogging;
32 $this->start();
33 }
34
35 /**
36 * Class destructor
37 *
38 * @return void
39 * @TODO make sure elements in the destructor match the current class elements
40 */
41 function __destruct() {
42 unset($this->log);
43 }
44
45 function start() {
46 /* Prepare Logging. Just in case it's used. later */
47 if ($this->isLogging) {
48 $this->tStart = gettimeofday();
49 $this->tLast = $this->tStart;
50 $this->log = "<h1>Log: " . $this->name . "</h1>\n<pre>Started: " . gmdate("D, d M Y H:i:s T", $this->tStart['sec']) . "\n &#916; Start ; &#916; Last ;";
51 $this->logLine("Start");
52 }
53 }
54
55 function dumpInstalledModules() {
56 if ($this->isLogging) {
57 $isCurlInstalled = extension_loaded('curl') && function_exists('curl_version');
58 $isGdInstalled = extension_loaded('gd') && function_exists('gd_info');
59 $isExifInstalled = extension_loaded('exif') && function_exists('exif_imagetype');
60 $isFileGetContentsInstalled = function_exists('file_get_contents');
61 $isFileGetContentsExtInstalled = $isFileGetContentsInstalled && ini_get('allow_url_fopen');
62
63 $this->logLine("isCurlInstalled...............: " . ($isCurlInstalled ? "Yes" : "No"));
64 $this->logLine("isGdInstalled.................: " . ($isGdInstalled ? "Yes" : "No"));
65 $this->logLine("isExifInstalled...............: " . ($isExifInstalled ? "Yes" : "No"));
66 $this->logLine("isFileGetContentsInstalled....: " . ($isFileGetContentsInstalled ? "Yes" : "No"));
67 $this->logLine("isFileGetContentsExtInstalled.: " . ($isFileGetContentsExtInstalled ? "Yes" : "No"));
68 }
69 }
70
71 function logLine($line) {
72 if ($this->isLogging) {
73 $tTemp = gettimeofday();
74 $tS = $this->tStart['sec'] + (((int)($this->tStart['usec']/100))/10000);
75 $tL = $this->tLast['sec'] + (((int)($this->tLast['usec']/100))/10000);
76 $tT = $tTemp['sec'] + (((int)($tTemp['usec']/100))/10000);
77
78 $logline = sprintf("\n+%08.04f; +%08.04f; ", ($tT-$tS), ($tT-$tL)) . $this->name . $line;
79 $this->log .= $logline;
80 $this->tLast = $tTemp;
81
82 if ($this->isDebugging) {
83 echo "<pre>" . $logline . "\n</pre>\n";
84 }
85 }
86 }
87
88 function getLog() {
89 return $this->log;
90 }
91}
92?> \ No newline at end of file
diff --git a/inc/3rdparty/libraries/PHPePub/Zip.php b/inc/3rdparty/libraries/PHPePub/Zip.php
new file mode 100644
index 00000000..01e03566
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/Zip.php
@@ -0,0 +1,818 @@
1<?php
2/**
3 * Class to create and manage a Zip file.
4 *
5 * Initially inspired by CreateZipFile by Rochak Chauhan www.rochakchauhan.com (http://www.phpclasses.org/browse/package/2322.html)
6 * and
7 * http://www.pkware.com/documents/casestudies/APPNOTE.TXT Zip file specification.
8 *
9 * License: GNU LGPL, Attribution required for commercial implementations, requested for everything else.
10 *
11 * @author A. Grandt <php@grandt.com>
12 * @copyright 2009-2014 A. Grandt
13 * @license GNU LGPL 2.1
14 * @link http://www.phpclasses.org/package/6110
15 * @link https://github.com/Grandt/PHPZip
16 * @version 1.60
17 */
18class Zip {
19 const VERSION = 1.60;
20
21 const ZIP_LOCAL_FILE_HEADER = "\x50\x4b\x03\x04"; // Local file header signature
22 const ZIP_CENTRAL_FILE_HEADER = "\x50\x4b\x01\x02"; // Central file header signature
23 const ZIP_END_OF_CENTRAL_DIRECTORY = "\x50\x4b\x05\x06\x00\x00\x00\x00"; //end of Central directory record
24
25 const EXT_FILE_ATTR_DIR = 010173200020; // Permission 755 drwxr-xr-x = (((S_IFDIR | 0755) << 16) | S_DOS_D);
26 const EXT_FILE_ATTR_FILE = 020151000040; // Permission 644 -rw-r--r-- = (((S_IFREG | 0644) << 16) | S_DOS_A);
27
28 const ATTR_VERSION_TO_EXTRACT = "\x14\x00"; // Version needed to extract
29 const ATTR_MADE_BY_VERSION = "\x1E\x03"; // Made By Version
30
31 // Unix file types
32 const S_IFIFO = 0010000; // named pipe (fifo)
33 const S_IFCHR = 0020000; // character special
34 const S_IFDIR = 0040000; // directory
35 const S_IFBLK = 0060000; // block special
36 const S_IFREG = 0100000; // regular
37 const S_IFLNK = 0120000; // symbolic link
38 const S_IFSOCK = 0140000; // socket
39
40 // setuid/setgid/sticky bits, the same as for chmod:
41
42 const S_ISUID = 0004000; // set user id on execution
43 const S_ISGID = 0002000; // set group id on execution
44 const S_ISTXT = 0001000; // sticky bit
45
46 // And of course, the other 12 bits are for the permissions, the same as for chmod:
47 // When addding these up, you can also just write the permissions as a simgle octal number
48 // ie. 0755. The leading 0 specifies octal notation.
49 const S_IRWXU = 0000700; // RWX mask for owner
50 const S_IRUSR = 0000400; // R for owner
51 const S_IWUSR = 0000200; // W for owner
52 const S_IXUSR = 0000100; // X for owner
53 const S_IRWXG = 0000070; // RWX mask for group
54 const S_IRGRP = 0000040; // R for group
55 const S_IWGRP = 0000020; // W for group
56 const S_IXGRP = 0000010; // X for group
57 const S_IRWXO = 0000007; // RWX mask for other
58 const S_IROTH = 0000004; // R for other
59 const S_IWOTH = 0000002; // W for other
60 const S_IXOTH = 0000001; // X for other
61 const S_ISVTX = 0001000; // save swapped text even after use
62
63 // Filetype, sticky and permissions are added up, and shifted 16 bits left BEFORE adding the DOS flags.
64
65 // DOS file type flags, we really only use the S_DOS_D flag.
66
67 const S_DOS_A = 0000040; // DOS flag for Archive
68 const S_DOS_D = 0000020; // DOS flag for Directory
69 const S_DOS_V = 0000010; // DOS flag for Volume
70 const S_DOS_S = 0000004; // DOS flag for System
71 const S_DOS_H = 0000002; // DOS flag for Hidden
72 const S_DOS_R = 0000001; // DOS flag for Read Only
73
74 private $zipMemoryThreshold = 1048576; // Autocreate tempfile if the zip data exceeds 1048576 bytes (1 MB)
75
76 private $zipData = NULL;
77 private $zipFile = NULL;
78 private $zipComment = NULL;
79 private $cdRec = array(); // central directory
80 private $offset = 0;
81 private $isFinalized = FALSE;
82 private $addExtraField = TRUE;
83
84 private $streamChunkSize = 65536;
85 private $streamFilePath = NULL;
86 private $streamTimestamp = NULL;
87 private $streamFileComment = NULL;
88 private $streamFile = NULL;
89 private $streamData = NULL;
90 private $streamFileLength = 0;
91 private $streamExtFileAttr = null;
92
93 /**
94 * Constructor.
95 *
96 * @param boolean $useZipFile Write temp zip data to tempFile? Default FALSE
97 */
98 function __construct($useZipFile = FALSE) {
99 if ($useZipFile) {
100 $this->zipFile = tmpfile();
101 } else {
102 $this->zipData = "";
103 }
104 }
105
106 function __destruct() {
107 if (is_resource($this->zipFile)) {
108 fclose($this->zipFile);
109 }
110 $this->zipData = NULL;
111 }
112
113 /**
114 * Extra fields on the Zip directory records are Unix time codes needed for compatibility on the default Mac zip archive tool.
115 * These are enabled as default, as they do no harm elsewhere and only add 26 bytes per file added.
116 *
117 * @param bool $setExtraField TRUE (default) will enable adding of extra fields, anything else will disable it.
118 */
119 function setExtraField($setExtraField = TRUE) {
120 $this->addExtraField = ($setExtraField === TRUE);
121 }
122
123 /**
124 * Set Zip archive comment.
125 *
126 * @param string $newComment New comment. NULL to clear.
127 * @return bool $success
128 */
129 public function setComment($newComment = NULL) {
130 if ($this->isFinalized) {
131 return FALSE;
132 }
133 $this->zipComment = $newComment;
134
135 return TRUE;
136 }
137
138 /**
139 * Set zip file to write zip data to.
140 * This will cause all present and future data written to this class to be written to this file.
141 * This can be used at any time, even after the Zip Archive have been finalized. Any previous file will be closed.
142 * Warning: If the given file already exists, it will be overwritten.
143 *
144 * @param string $fileName
145 * @return bool $success
146 */
147 public function setZipFile($fileName) {
148 if (is_file($fileName)) {
149 unlink($fileName);
150 }
151 $fd=fopen($fileName, "x+b");
152 if (is_resource($this->zipFile)) {
153 rewind($this->zipFile);
154 while (!feof($this->zipFile)) {
155 fwrite($fd, fread($this->zipFile, $this->streamChunkSize));
156 }
157
158 fclose($this->zipFile);
159 } else {
160 fwrite($fd, $this->zipData);
161 $this->zipData = NULL;
162 }
163 $this->zipFile = $fd;
164
165 return TRUE;
166 }
167
168 /**
169 * Add an empty directory entry to the zip archive.
170 * Basically this is only used if an empty directory is added.
171 *
172 * @param string $directoryPath Directory Path and name to be added to the archive.
173 * @param int $timestamp (Optional) Timestamp for the added directory, if omitted or set to 0, the current time will be used.
174 * @param string $fileComment (Optional) Comment to be added to the archive for this directory. To use fileComment, timestamp must be given.
175 * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this.
176 * @return bool $success
177 */
178 public function addDirectory($directoryPath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self::EXT_FILE_ATTR_DIR) {
179 if ($this->isFinalized) {
180 return FALSE;
181 }
182 $directoryPath = str_replace("\\", "/", $directoryPath);
183 $directoryPath = rtrim($directoryPath, "/");
184
185 if (strlen($directoryPath) > 0) {
186 $this->buildZipEntry($directoryPath.'/', $fileComment, "\x00\x00", "\x00\x00", $timestamp, "\x00\x00\x00\x00", 0, 0, $extFileAttr);
187 return TRUE;
188 }
189 return FALSE;
190 }
191
192 /**
193 * Add a file to the archive at the specified location and file name.
194 *
195 * @param string $data File data.
196 * @param string $filePath Filepath and name to be used in the archive.
197 * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
198 * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
199 * @param bool $compress (Optional) Compress file, if set to FALSE the file will only be stored. Default TRUE.
200 * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this.
201 * @return bool $success
202 */
203 public function addFile($data, $filePath, $timestamp = 0, $fileComment = NULL, $compress = TRUE, $extFileAttr = self::EXT_FILE_ATTR_FILE) {
204 if ($this->isFinalized) {
205 return FALSE;
206 }
207
208 if (is_resource($data) && get_resource_type($data) == "stream") {
209 $this->addLargeFile($data, $filePath, $timestamp, $fileComment, $extFileAttr);
210 return FALSE;
211 }
212
213 $gzData = "";
214 $gzType = "\x08\x00"; // Compression type 8 = deflate
215 $gpFlags = "\x00\x00"; // General Purpose bit flags for compression type 8 it is: 0=Normal, 1=Maximum, 2=Fast, 3=super fast compression.
216 $dataLength = strlen($data);
217 $fileCRC32 = pack("V", crc32($data));
218
219 if ($compress) {
220 $gzTmp = gzcompress($data);
221 $gzData = substr(substr($gzTmp, 0, strlen($gzTmp) - 4), 2); // gzcompress adds a 2 byte header and 4 byte CRC we can't use.
222 // The 2 byte header does contain useful data, though in this case the 2 parameters we'd be interrested in will always be 8 for compression type, and 2 for General purpose flag.
223 $gzLength = strlen($gzData);
224 } else {
225 $gzLength = $dataLength;
226 }
227
228 if ($gzLength >= $dataLength) {
229 $gzLength = $dataLength;
230 $gzData = $data;
231 $gzType = "\x00\x00"; // Compression type 0 = stored
232 $gpFlags = "\x00\x00"; // Compression type 0 = stored
233 }
234
235 if (!is_resource($this->zipFile) && ($this->offset + $gzLength) > $this->zipMemoryThreshold) {
236 $this->zipflush();
237 }
238
239 $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr);
240
241 $this->zipwrite($gzData);
242
243 return TRUE;
244 }
245
246 /**
247 * Add the content to a directory.
248 *
249 * @author Adam Schmalhofer <Adam.Schmalhofer@gmx.de>
250 * @author A. Grandt
251 *
252 * @param string $realPath Path on the file system.
253 * @param string $zipPath Filepath and name to be used in the archive.
254 * @param bool $recursive Add content recursively, default is TRUE.
255 * @param bool $followSymlinks Follow and add symbolic links, if they are accessible, default is TRUE.
256 * @param array &$addedFiles Reference to the added files, this is used to prevent duplicates, efault is an empty array.
257 * If you start the function by parsing an array, the array will be populated with the realPath
258 * and zipPath kay/value pairs added to the archive by the function.
259 * @param bool $overrideFilePermissions Force the use of the file/dir permissions set in the $extDirAttr
260 * and $extFileAttr parameters.
261 * @param int $extDirAttr Permissions for directories.
262 * @param int $extFileAttr Permissions for files.
263 */
264 public function addDirectoryContent($realPath, $zipPath, $recursive = TRUE, $followSymlinks = TRUE, &$addedFiles = array(),
265 $overrideFilePermissions = FALSE, $extDirAttr = self::EXT_FILE_ATTR_DIR, $extFileAttr = self::EXT_FILE_ATTR_FILE) {
266 if (file_exists($realPath) && !isset($addedFiles[realpath($realPath)])) {
267 if (is_dir($realPath)) {
268 if ($overrideFilePermissions) {
269 $this->addDirectory($zipPath, 0, null, $extDirAttr);
270 } else {
271 $this->addDirectory($zipPath, 0, null, self::getFileExtAttr($realPath));
272 }
273 }
274
275 $addedFiles[realpath($realPath)] = $zipPath;
276
277 $iter = new DirectoryIterator($realPath);
278 foreach ($iter as $file) {
279 if ($file->isDot()) {
280 continue;
281 }
282 $newRealPath = $file->getPathname();
283 $newZipPath = self::pathJoin($zipPath, $file->getFilename());
284
285 if (file_exists($newRealPath) && ($followSymlinks === TRUE || !is_link($newRealPath))) {
286 if ($file->isFile()) {
287 $addedFiles[realpath($newRealPath)] = $newZipPath;
288 if ($overrideFilePermissions) {
289 $this->addLargeFile($newRealPath, $newZipPath, 0, null, $extFileAttr);
290 } else {
291 $this->addLargeFile($newRealPath, $newZipPath, 0, null, self::getFileExtAttr($newRealPath));
292 }
293 } else if ($recursive === TRUE) {
294 $this->addDirectoryContent($newRealPath, $newZipPath, $recursive, $followSymlinks, $addedFiles, $overrideFilePermissions, $extDirAttr, $extFileAttr);
295 } else {
296 if ($overrideFilePermissions) {
297 $this->addDirectory($zipPath, 0, null, $extDirAttr);
298 } else {
299 $this->addDirectory($zipPath, 0, null, self::getFileExtAttr($newRealPath));
300 }
301 }
302 }
303 }
304 }
305 }
306
307 /**
308 * Add a file to the archive at the specified location and file name.
309 *
310 * @param string $dataFile File name/path.
311 * @param string $filePath Filepath and name to be used in the archive.
312 * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
313 * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
314 * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this.
315 * @return bool $success
316 */
317 public function addLargeFile($dataFile, $filePath, $timestamp = 0, $fileComment = NULL, $extFileAttr = self::EXT_FILE_ATTR_FILE) {
318 if ($this->isFinalized) {
319 return FALSE;
320 }
321
322 if (is_string($dataFile) && is_file($dataFile)) {
323 $this->processFile($dataFile, $filePath, $timestamp, $fileComment, $extFileAttr);
324 } else if (is_resource($dataFile) && get_resource_type($dataFile) == "stream") {
325 $fh = $dataFile;
326 $this->openStream($filePath, $timestamp, $fileComment, $extFileAttr);
327
328 while (!feof($fh)) {
329 $this->addStreamData(fread($fh, $this->streamChunkSize));
330 }
331 $this->closeStream($this->addExtraField);
332 }
333 return TRUE;
334 }
335
336 /**
337 * Create a stream to be used for large entries.
338 *
339 * @param string $filePath Filepath and name to be used in the archive.
340 * @param int $timestamp (Optional) Timestamp for the added file, if omitted or set to 0, the current time will be used.
341 * @param string $fileComment (Optional) Comment to be added to the archive for this file. To use fileComment, timestamp must be given.
342 * @param int $extFileAttr (Optional) The external file reference, use generateExtAttr to generate this.
343 * @return bool $success
344 */
345 public function openStream($filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE) {
346 if (!function_exists('sys_get_temp_dir')) {
347 die ("ERROR: Zip " . self::VERSION . " requires PHP version 5.2.1 or above if large files are used.");
348 }
349
350 if ($this->isFinalized) {
351 return FALSE;
352 }
353
354 $this->zipflush();
355
356 if (strlen($this->streamFilePath) > 0) {
357 $this->closeStream();
358 }
359
360 $this->streamFile = tempnam(sys_get_temp_dir(), 'Zip');
361 $this->streamData = fopen($this->streamFile, "wb");
362 $this->streamFilePath = $filePath;
363 $this->streamTimestamp = $timestamp;
364 $this->streamFileComment = $fileComment;
365 $this->streamFileLength = 0;
366 $this->streamExtFileAttr = $extFileAttr;
367
368 return TRUE;
369 }
370
371 /**
372 * Add data to the open stream.
373 *
374 * @param string $data
375 * @return mixed length in bytes added or FALSE if the archive is finalized or there are no open stream.
376 */
377 public function addStreamData($data) {
378 if ($this->isFinalized || strlen($this->streamFilePath) == 0) {
379 return FALSE;
380 }
381
382 $length = fwrite($this->streamData, $data, strlen($data));
383 if ($length != strlen($data)) {
384 die ("<p>Length mismatch</p>\n");
385 }
386 $this->streamFileLength += $length;
387
388 return $length;
389 }
390
391 /**
392 * Close the current stream.
393 *
394 * @return bool $success
395 */
396 public function closeStream() {
397 if ($this->isFinalized || strlen($this->streamFilePath) == 0) {
398 return FALSE;
399 }
400
401 fflush($this->streamData);
402 fclose($this->streamData);
403
404 $this->processFile($this->streamFile, $this->streamFilePath, $this->streamTimestamp, $this->streamFileComment, $this->streamExtFileAttr);
405
406 $this->streamData = null;
407 $this->streamFilePath = null;
408 $this->streamTimestamp = null;
409 $this->streamFileComment = null;
410 $this->streamFileLength = 0;
411 $this->streamExtFileAttr = null;
412
413 // Windows is a little slow at times, so a millisecond later, we can unlink this.
414 unlink($this->streamFile);
415
416 $this->streamFile = null;
417
418 return TRUE;
419 }
420
421 private function processFile($dataFile, $filePath, $timestamp = 0, $fileComment = null, $extFileAttr = self::EXT_FILE_ATTR_FILE) {
422 if ($this->isFinalized) {
423 return FALSE;
424 }
425
426 $tempzip = tempnam(sys_get_temp_dir(), 'ZipStream');
427
428 $zip = new ZipArchive;
429 if ($zip->open($tempzip) === TRUE) {
430 $zip->addFile($dataFile, 'file');
431 $zip->close();
432 }
433
434 $file_handle = fopen($tempzip, "rb");
435 $stats = fstat($file_handle);
436 $eof = $stats['size']-72;
437
438 fseek($file_handle, 6);
439
440 $gpFlags = fread($file_handle, 2);
441 $gzType = fread($file_handle, 2);
442 fread($file_handle, 4);
443 $fileCRC32 = fread($file_handle, 4);
444 $v = unpack("Vval", fread($file_handle, 4));
445 $gzLength = $v['val'];
446 $v = unpack("Vval", fread($file_handle, 4));
447 $dataLength = $v['val'];
448
449 $this->buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr);
450
451 fseek($file_handle, 34);
452 $pos = 34;
453
454 while (!feof($file_handle) && $pos < $eof) {
455 $datalen = $this->streamChunkSize;
456 if ($pos + $this->streamChunkSize > $eof) {
457 $datalen = $eof-$pos;
458 }
459 $data = fread($file_handle, $datalen);
460 $pos += $datalen;
461
462 $this->zipwrite($data);
463 }
464
465 fclose($file_handle);
466
467 unlink($tempzip);
468 }
469
470 /**
471 * Close the archive.
472 * A closed archive can no longer have new files added to it.
473 *
474 * @return bool $success
475 */
476 public function finalize() {
477 if (!$this->isFinalized) {
478 if (strlen($this->streamFilePath) > 0) {
479 $this->closeStream();
480 }
481 $cd = implode("", $this->cdRec);
482
483 $cdRecSize = pack("v", sizeof($this->cdRec));
484 $cdRec = $cd . self::ZIP_END_OF_CENTRAL_DIRECTORY
485 . $cdRecSize . $cdRecSize
486 . pack("VV", strlen($cd), $this->offset);
487 if (!empty($this->zipComment)) {
488 $cdRec .= pack("v", strlen($this->zipComment)) . $this->zipComment;
489 } else {
490 $cdRec .= "\x00\x00";
491 }
492
493 $this->zipwrite($cdRec);
494
495 $this->isFinalized = TRUE;
496 $this->cdRec = NULL;
497
498 return TRUE;
499 }
500 return FALSE;
501 }
502
503 /**
504 * Get the handle ressource for the archive zip file.
505 * If the zip haven't been finalized yet, this will cause it to become finalized
506 *
507 * @return zip file handle
508 */
509 public function getZipFile() {
510 if (!$this->isFinalized) {
511 $this->finalize();
512 }
513
514 $this->zipflush();
515
516 rewind($this->zipFile);
517
518 return $this->zipFile;
519 }
520
521 /**
522 * Get the zip file contents
523 * If the zip haven't been finalized yet, this will cause it to become finalized
524 *
525 * @return zip data
526 */
527 public function getZipData() {
528 if (!$this->isFinalized) {
529 $this->finalize();
530 }
531 if (!is_resource($this->zipFile)) {
532 return $this->zipData;
533 } else {
534 rewind($this->zipFile);
535 $filestat = fstat($this->zipFile);
536 return fread($this->zipFile, $filestat['size']);
537 }
538 }
539
540 /**
541 * Send the archive as a zip download
542 *
543 * @param String $fileName The name of the Zip archive, in ISO-8859-1 (or ASCII) encoding, ie. "archive.zip". Optional, defaults to NULL, which means that no ISO-8859-1 encoded file name will be specified.
544 * @param String $contentType Content mime type. Optional, defaults to "application/zip".
545 * @param String $utf8FileName The name of the Zip archive, in UTF-8 encoding. Optional, defaults to NULL, which means that no UTF-8 encoded file name will be specified.
546 * @param bool $inline Use Content-Disposition with "inline" instead of "attached". Optional, defaults to FALSE.
547 * @return bool $success
548 */
549 function sendZip($fileName = null, $contentType = "application/zip", $utf8FileName = null, $inline = false) {
550 if (!$this->isFinalized) {
551 $this->finalize();
552 }
553
554 $headerFile = null;
555 $headerLine = null;
556 if (!headers_sent($headerFile, $headerLine) or die("<p><strong>Error:</strong> Unable to send file $fileName. HTML Headers have already been sent from <strong>$headerFile</strong> in line <strong>$headerLine</strong></p>")) {
557 if ((ob_get_contents() === FALSE || ob_get_contents() == '') or die("\n<p><strong>Error:</strong> Unable to send file <strong>$fileName</strong>. Output buffer contains the following text (typically warnings or errors):<br>" . htmlentities(ob_get_contents()) . "</p>")) {
558 if (ini_get('zlib.output_compression')) {
559 ini_set('zlib.output_compression', 'Off');
560 }
561
562 header("Pragma: public");
563 header("Last-Modified: " . gmdate("D, d M Y H:i:s T"));
564 header("Expires: 0");
565 header("Accept-Ranges: bytes");
566 header("Connection: close");
567 header("Content-Type: " . $contentType);
568 $cd = "Content-Disposition: ";
569 if ($inline) {
570 $cd .= "inline";
571 } else{
572 $cd .= "attached";
573 }
574 if ($fileName) {
575 $cd .= '; filename="' . $fileName . '"';
576 }
577 if ($utf8FileName) {
578 $cd .= "; filename*=UTF-8''" . rawurlencode($utf8FileName);
579 }
580 header($cd);
581 header("Content-Length: ". $this->getArchiveSize());
582
583 if (!is_resource($this->zipFile)) {
584 echo $this->zipData;
585 } else {
586 rewind($this->zipFile);
587
588 while (!feof($this->zipFile)) {
589 echo fread($this->zipFile, $this->streamChunkSize);
590 }
591 }
592 }
593 return TRUE;
594 }
595 return FALSE;
596 }
597
598 /**
599 * Return the current size of the archive
600 *
601 * @return $size Size of the archive
602 */
603 public function getArchiveSize() {
604 if (!is_resource($this->zipFile)) {
605 return strlen($this->zipData);
606 }
607 $filestat = fstat($this->zipFile);
608
609 return $filestat['size'];
610 }
611
612 /**
613 * Calculate the 2 byte dostime used in the zip entries.
614 *
615 * @param int $timestamp
616 * @return 2-byte encoded DOS Date
617 */
618 private function getDosTime($timestamp = 0) {
619 $timestamp = (int)$timestamp;
620 $oldTZ = @date_default_timezone_get();
621 date_default_timezone_set('UTC');
622 $date = ($timestamp == 0 ? getdate() : getdate($timestamp));
623 date_default_timezone_set($oldTZ);
624 if ($date["year"] >= 1980) {
625 return pack("V", (($date["mday"] + ($date["mon"] << 5) + (($date["year"]-1980) << 9)) << 16) |
626 (($date["seconds"] >> 1) + ($date["minutes"] << 5) + ($date["hours"] << 11)));
627 }
628 return "\x00\x00\x00\x00";
629 }
630
631 /**
632 * Build the Zip file structures
633 *
634 * @param string $filePath
635 * @param string $fileComment
636 * @param string $gpFlags
637 * @param string $gzType
638 * @param int $timestamp
639 * @param string $fileCRC32
640 * @param int $gzLength
641 * @param int $dataLength
642 * @param int $extFileAttr Use self::EXT_FILE_ATTR_FILE for files, self::EXT_FILE_ATTR_DIR for Directories.
643 */
644 private function buildZipEntry($filePath, $fileComment, $gpFlags, $gzType, $timestamp, $fileCRC32, $gzLength, $dataLength, $extFileAttr) {
645 $filePath = str_replace("\\", "/", $filePath);
646 $fileCommentLength = (empty($fileComment) ? 0 : strlen($fileComment));
647 $timestamp = (int)$timestamp;
648 $timestamp = ($timestamp == 0 ? time() : $timestamp);
649
650 $dosTime = $this->getDosTime($timestamp);
651 $tsPack = pack("V", $timestamp);
652
653 $ux = "\x75\x78\x0B\x00\x01\x04\xE8\x03\x00\x00\x04\x00\x00\x00\x00";
654
655 if (!isset($gpFlags) || strlen($gpFlags) != 2) {
656 $gpFlags = "\x00\x00";
657 }
658
659 $isFileUTF8 = mb_check_encoding($filePath, "UTF-8") && !mb_check_encoding($filePath, "ASCII");
660 $isCommentUTF8 = !empty($fileComment) && mb_check_encoding($fileComment, "UTF-8") && !mb_check_encoding($fileComment, "ASCII");
661 if ($isFileUTF8 || $isCommentUTF8) {
662 $flag = 0;
663 $gpFlagsV = unpack("vflags", $gpFlags);
664 if (isset($gpFlagsV['flags'])) {
665 $flag = $gpFlagsV['flags'];
666 }
667 $gpFlags = pack("v", $flag | (1 << 11));
668 }
669
670 $header = $gpFlags . $gzType . $dosTime. $fileCRC32
671 . pack("VVv", $gzLength, $dataLength, strlen($filePath)); // File name length
672
673 $zipEntry = self::ZIP_LOCAL_FILE_HEADER;
674 $zipEntry .= self::ATTR_VERSION_TO_EXTRACT;
675 $zipEntry .= $header;
676 $zipEntry .= pack("v", ($this->addExtraField ? 28 : 0)); // Extra field length
677 $zipEntry .= $filePath; // FileName
678 // Extra fields
679 if ($this->addExtraField) {
680 $zipEntry .= "\x55\x54\x09\x00\x03" . $tsPack . $tsPack . $ux;
681 }
682 $this->zipwrite($zipEntry);
683
684 $cdEntry = self::ZIP_CENTRAL_FILE_HEADER;
685 $cdEntry .= self::ATTR_MADE_BY_VERSION;
686 $cdEntry .= ($dataLength === 0 ? "\x0A\x00" : self::ATTR_VERSION_TO_EXTRACT);
687 $cdEntry .= $header;
688 $cdEntry .= pack("v", ($this->addExtraField ? 24 : 0)); // Extra field length
689 $cdEntry .= pack("v", $fileCommentLength); // File comment length
690 $cdEntry .= "\x00\x00"; // Disk number start
691 $cdEntry .= "\x00\x00"; // internal file attributes
692 $cdEntry .= pack("V", $extFileAttr); // External file attributes
693 $cdEntry .= pack("V", $this->offset); // Relative offset of local header
694 $cdEntry .= $filePath; // FileName
695 // Extra fields
696 if ($this->addExtraField) {
697 $cdEntry .= "\x55\x54\x05\x00\x03" . $tsPack . $ux;
698 }
699 if (!empty($fileComment)) {
700 $cdEntry .= $fileComment; // Comment
701 }
702
703 $this->cdRec[] = $cdEntry;
704 $this->offset += strlen($zipEntry) + $gzLength;
705 }
706
707 private function zipwrite($data) {
708 if (!is_resource($this->zipFile)) {
709 $this->zipData .= $data;
710 } else {
711 fwrite($this->zipFile, $data);
712 fflush($this->zipFile);
713 }
714 }
715
716 private function zipflush() {
717 if (!is_resource($this->zipFile)) {
718 $this->zipFile = tmpfile();
719 fwrite($this->zipFile, $this->zipData);
720 $this->zipData = NULL;
721 }
722 }
723
724 /**
725 * Join $file to $dir path, and clean up any excess slashes.
726 *
727 * @param string $dir
728 * @param string $file
729 */
730 public static function pathJoin($dir, $file) {
731 if (empty($dir) || empty($file)) {
732 return self::getRelativePath($dir . $file);
733 }
734 return self::getRelativePath($dir . '/' . $file);
735 }
736
737 /**
738 * Clean up a path, removing any unnecessary elements such as /./, // or redundant ../ segments.
739 * If the path starts with a "/", it is deemed an absolute path and any /../ in the beginning is stripped off.
740 * The returned path will not end in a "/".
741 *
742 * Sometimes, when a path is generated from multiple fragments,
743 * you can get something like "../data/html/../images/image.jpeg"
744 * This will normalize that example path to "../data/images/image.jpeg"
745 *
746 * @param string $path The path to clean up
747 * @return string the clean path
748 */
749 public static function getRelativePath($path) {
750 $path = preg_replace("#/+\.?/+#", "/", str_replace("\\", "/", $path));
751 $dirs = explode("/", rtrim(preg_replace('#^(?:\./)+#', '', $path), '/'));
752
753 $offset = 0;
754 $sub = 0;
755 $subOffset = 0;
756 $root = "";
757
758 if (empty($dirs[0])) {
759 $root = "/";
760 $dirs = array_splice($dirs, 1);
761 } else if (preg_match("#[A-Za-z]:#", $dirs[0])) {
762 $root = strtoupper($dirs[0]) . "/";
763 $dirs = array_splice($dirs, 1);
764 }
765
766 $newDirs = array();
767 foreach ($dirs as $dir) {
768 if ($dir !== "..") {
769 $subOffset--;
770 $newDirs[++$offset] = $dir;
771 } else {
772 $subOffset++;
773 if (--$offset < 0) {
774 $offset = 0;
775 if ($subOffset > $sub) {
776 $sub++;
777 }
778 }
779 }
780 }
781
782 if (empty($root)) {
783 $root = str_repeat("../", $sub);
784 }
785 return $root . implode("/", array_slice($newDirs, 0, $offset));
786 }
787
788 /**
789 * Create the file permissions for a file or directory, for use in the extFileAttr parameters.
790 *
791 * @param int $owner Unix permisions for owner (octal from 00 to 07)
792 * @param int $group Unix permisions for group (octal from 00 to 07)
793 * @param int $other Unix permisions for others (octal from 00 to 07)
794 * @param bool $isFile
795 * @return EXTRERNAL_REF field.
796 */
797 public static function generateExtAttr($owner = 07, $group = 05, $other = 05, $isFile = true) {
798 $fp = $isFile ? self::S_IFREG : self::S_IFDIR;
799 $fp |= (($owner & 07) << 6) | (($group & 07) << 3) | ($other & 07);
800
801 return ($fp << 16) | ($isFile ? self::S_DOS_A : self::S_DOS_D);
802 }
803
804 /**
805 * Get the file permissions for a file or directory, for use in the extFileAttr parameters.
806 *
807 * @param string $filename
808 * @return external ref field, or FALSE if the file is not found.
809 */
810 public static function getFileExtAttr($filename) {
811 if (file_exists($filename)) {
812 $fp = fileperms($filename) << 16;
813 return $fp | (is_dir($filename) ? self::S_DOS_D : self::S_DOS_A);
814 }
815 return FALSE;
816 }
817}
818?>
diff --git a/inc/3rdparty/libraries/PHPePub/lib.uuid.LICENCE.txt b/inc/3rdparty/libraries/PHPePub/lib.uuid.LICENCE.txt
new file mode 100644
index 00000000..9424a83e
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/lib.uuid.LICENCE.txt
@@ -0,0 +1,31 @@
1 DrUUID RFC4122 library for PHP5
2 by J. King (http://jkingweb.ca/)
3 Licensed under MIT license
4
5 See http://jkingweb.ca/code/php/lib.uuid/
6 for documentation
7
8 Last revised 2010-02-15
9
10Copyright (c) 2009 J. King
11
12Permission is hereby granted, free of charge, to any person
13obtaining a copy of this software and associated documentation
14files (the "Software"), to deal in the Software without
15restriction, including without limitation the rights to use,
16copy, modify, merge, publish, distribute, sublicense, and/or sell
17copies of the Software, and to permit persons to whom the
18Software is furnished to do so, subject to the following
19conditions:
20
21The above copyright notice and this permission notice shall be
22included in all copies or substantial portions of the Software.
23
24THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
26OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
28HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
29WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
30FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
31OTHER DEALINGS IN THE SOFTWARE.
diff --git a/inc/3rdparty/libraries/PHPePub/lib.uuid.php b/inc/3rdparty/libraries/PHPePub/lib.uuid.php
new file mode 100644
index 00000000..c6a8de52
--- /dev/null
+++ b/inc/3rdparty/libraries/PHPePub/lib.uuid.php
@@ -0,0 +1,314 @@
1<?php
2/*
3 DrUUID RFC4122 library for PHP5
4by J. King (http://jkingweb.ca/)
5Licensed under MIT license
6
7See http://jkingweb.ca/code/php/lib.uuid/
8for documentation
9
10Last revised 2010-02-15
11*/
12
13/*
14 Copyright (c) 2009 J. King
15
16Permission is hereby granted, free of charge, to any person
17obtaining a copy of this software and associated documentation
18files (the "Software"), to deal in the Software without
19restriction, including without limitation the rights to use,
20copy, modify, merge, publish, distribute, sublicense, and/or sell
21copies of the Software, and to permit persons to whom the
22Software is furnished to do so, subject to the following
23conditions:
24
25The above copyright notice and this permission notice shall be
26included in all copies or substantial portions of the Software.
27
28THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
29EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
30OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
32HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
33WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
34FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35OTHER DEALINGS IN THE SOFTWARE.
36*/
37
38
39class UUID {
40 const MD5 = 3;
41 const SHA1 = 5;
42 const clearVer = 15; // 00001111 Clears all bits of version byte with AND
43 const clearVar = 63; // 00111111 Clears all relevant bits of variant byte with AND
44 const varRes = 224; // 11100000 Variant reserved for future use
45 const varMS = 192; // 11000000 Microsft GUID variant
46 const varRFC = 128; // 10000000 The RFC 4122 variant (this variant)
47 const varNCS = 0; // 00000000 The NCS compatibility variant
48 const version1 = 16; // 00010000
49 const version3 = 48; // 00110000
50 const version4 = 64; // 01000000
51 const version5 = 80; // 01010000
52 const interval = 0x01b21dd213814000; // Time (in 100ns steps) between the start of the UTC and Unix epochs
53 const nsDNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
54 const nsURL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
55 const nsOID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8';
56 const nsX500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8';
57 protected static $randomFunc = 'randomTwister';
58 protected static $randomSource = NULL;
59 //instance properties
60 protected $bytes;
61 protected $hex;
62 protected $string;
63 protected $urn;
64 protected $version;
65 protected $variant;
66 protected $node;
67 protected $time;
68
69 public static function mint($ver = 1, $node = NULL, $ns = NULL) {
70 /* Create a new UUID based on provided data. */
71 switch((int) $ver) {
72 case 1:
73 return new self(self::mintTime($node));
74 case 2:
75 // Version 2 is not supported
76 throw new UUIDException("Version 2 is unsupported.");
77 case 3:
78 return new self(self::mintName(self::MD5, $node, $ns));
79 case 4:
80 return new self(self::mintRand());
81 case 5:
82 return new self(self::mintName(self::SHA1, $node, $ns));
83 default:
84 throw new UUIDException("Selected version is invalid or unsupported.");
85 }
86 }
87
88 public static function import($uuid) {
89 /* Import an existing UUID. */
90 return new self(self::makeBin($uuid, 16));
91 }
92
93 public static function compare($a, $b) {
94 /* Compares the binary representations of two UUIDs.
95 The comparison will return true if they are bit-exact,
96 or if neither is valid. */
97 if (self::makeBin($a, 16)==self::makeBin($b, 16)) {
98 return TRUE;
99 } else {
100 return FALSE;
101 }
102 }
103
104 public function __toString() {
105 return $this->string;
106 }
107
108 public function __get($var) {
109 switch($var) {
110 case "bytes":
111 return $this->bytes;
112 case "hex":
113 return bin2hex($this->bytes);
114 case "string":
115 return $this->__toString();
116 case "urn":
117 return "urn:uuid:".$this->__toString();
118 case "version":
119 return ord($this->bytes[6]) >> 4;
120 case "variant":
121 $byte = ord($this->bytes[8]);
122 if ($byte >= self::varRes) {
123 return 3;
124 }
125 if ($byte >= self::varMS) {
126 return 2;
127 }
128 if ($byte >= self::varRFC) {
129 return 1;
130 }
131 return 0;
132 case "node":
133 if (ord($this->bytes[6])>>4==1) {
134 return bin2hex(substr($this->bytes,10));
135 } else {
136 return NULL;
137 }
138 case "time":
139 if (ord($this->bytes[6])>>4==1) {
140 // Restore contiguous big-endian byte order
141 $time = bin2hex($this->bytes[6].$this->bytes[7].$this->bytes[4].$this->bytes[5].$this->bytes[0].$this->bytes[1].$this->bytes[2].$this->bytes[3]);
142 // Clear version flag
143 $time[0] = "0";
144 // Do some reverse arithmetic to get a Unix timestamp
145 $time = (hexdec($time) - self::interval) / 10000000;
146 return $time;
147 } else {
148 return NULL;
149 }
150 default:
151 return NULL;
152 }
153 }
154
155 protected function __construct($uuid) {
156 if (strlen($uuid) != 16) {
157 throw new UUIDException("Input must be a 128-bit integer.");
158 }
159 $this->bytes = $uuid;
160 // Optimize the most common use
161 $this->string =
162 bin2hex(substr($uuid,0,4))."-".
163 bin2hex(substr($uuid,4,2))."-".
164 bin2hex(substr($uuid,6,2))."-".
165 bin2hex(substr($uuid,8,2))."-".
166 bin2hex(substr($uuid,10,6));
167 }
168
169 protected static function mintTime($node = NULL) {
170 /* Generates a Version 1 UUID.
171 These are derived from the time at which they were generated. */
172 // Get time since Gregorian calendar reform in 100ns intervals
173 // This is exceedingly difficult because of PHP's (and pack()'s)
174 // integer size limits.
175 // Note that this will never be more accurate than to the microsecond.
176 $time = microtime(1) * 10000000 + self::interval;
177 // Convert to a string representation
178 $time = sprintf("%F", $time);
179 preg_match("/^\d+/", $time, $time); //strip decimal point
180 // And now to a 64-bit binary representation
181 $time = base_convert($time[0], 10, 16);
182 $time = pack("H*", str_pad($time, 16, "0", STR_PAD_LEFT));
183 // Reorder bytes to their proper locations in the UUID
184 $uuid = $time[4].$time[5].$time[6].$time[7].$time[2].$time[3].$time[0].$time[1];
185 // Generate a random clock sequence
186 $uuid .= self::randomBytes(2);
187 // set variant
188 $uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC);
189 // set version
190 $uuid[6] = chr(ord($uuid[6]) & self::clearVer | self::version1);
191 // Set the final 'node' parameter, a MAC address
192 if ($node) {
193 $node = self::makeBin($node, 6);
194 }
195 if (!$node) {
196 // If no node was provided or if the node was invalid,
197 // generate a random MAC address and set the multicast bit
198 $node = self::randomBytes(6);
199 $node[0] = pack("C", ord($node[0]) | 1);
200 }
201 $uuid .= $node;
202 return $uuid;
203 }
204
205 protected static function mintRand() {
206 /* Generate a Version 4 UUID.
207 These are derived soly from random numbers. */
208 // generate random fields
209 $uuid = self::randomBytes(16);
210 // set variant
211 $uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC);
212 // set version
213 $uuid[6] = chr(ord($uuid[6]) & self::clearVer | self::version4);
214 return $uuid;
215 }
216
217 protected static function mintName($ver, $node, $ns) {
218 /* Generates a Version 3 or Version 5 UUID.
219 These are derived from a hash of a name and its namespace, in binary form. */
220 if (!$node) {
221 throw new UUIDException("A name-string is required for Version 3 or 5 UUIDs.");
222 }
223 // if the namespace UUID isn't binary, make it so
224 $ns = self::makeBin($ns, 16);
225 if (!$ns) {
226 throw new UUIDException("A binary namespace is required for Version 3 or 5 UUIDs.");
227 }
228 $uuid = null;
229 $version = self::version3;
230 switch($ver) {
231 case self::MD5:
232 $version = self::version3;
233 $uuid = md5($ns.$node,1);
234 break;
235 case self::SHA1:
236 $version = self::version5;
237 $uuid = substr(sha1($ns.$node,1),0, 16);
238 break;
239 }
240 // set variant
241 $uuid[8] = chr(ord($uuid[8]) & self::clearVar | self::varRFC);
242 // set version
243 $uuid[6] = chr(ord($uuid[6]) & self::clearVer | $version);
244 return ($uuid);
245 }
246
247 protected static function makeBin($str, $len) {
248 /* Insure that an input string is either binary or hexadecimal.
249 Returns binary representation, or false on failure. */
250 if ($str instanceof self) {
251 return $str->bytes;
252 }
253 if (strlen($str)==$len) {
254 return $str;
255 } else {
256 $str = preg_replace("/^urn:uuid:/is", "", $str); // strip URN scheme and namespace
257 }
258 $str = preg_replace("/[^a-f0-9]/is", "", $str); // strip non-hex characters
259 if (strlen($str) != ($len * 2)) {
260 return FALSE;
261 } else {
262 return pack("H*", $str);
263 }
264 }
265
266 public static function initRandom() {
267 /* Look for a system-provided source of randomness, which is usually crytographically secure.
268 /dev/urandom is tried first simply out of bias for Linux systems. */
269 if (is_readable('/dev/urandom')) {
270 self::$randomSource = fopen('/dev/urandom', 'rb');
271 self::$randomFunc = 'randomFRead';
272 }
273 else if (class_exists('COM', 0)) {
274 try {
275 self::$randomSource = new COM('CAPICOM.Utilities.1'); // See http://msdn.microsoft.com/en-us/library/aa388182(VS.85).aspx
276 self::$randomFunc = 'randomCOM';
277 }
278 catch(Exception $e) {
279 }
280 }
281 return self::$randomFunc;
282 }
283
284 public static function randomBytes($bytes) {
285 return call_user_func(array('self', self::$randomFunc), $bytes);
286 }
287
288 protected static function randomTwister($bytes) {
289 /* Get the specified number of random bytes, using mt_rand().
290 Randomness is returned as a string of bytes. */
291 $rand = "";
292 for ($a = 0; $a < $bytes; $a++) {
293 $rand .= chr(mt_rand(0, 255));
294 }
295 return $rand;
296 }
297
298 protected static function randomFRead($bytes) {
299 /* Get the specified number of random bytes using a file handle
300 previously opened with UUID::initRandom().
301 Randomness is returned as a string of bytes. */
302 return fread(self::$randomSource, $bytes);
303 }
304
305 protected static function randomCOM($bytes) {
306 /* Get the specified number of random bytes using Windows'
307 randomness source via a COM object previously created by UUID::initRandom().
308 Randomness is returned as a string of bytes. */
309 return base64_decode(self::$randomSource->GetRandom($bytes,0)); // straight binary mysteriously doesn't work, hence the base64
310 }
311}
312
313class UUIDException extends Exception {
314}
diff --git a/inc/3rdparty/libraries/content-extractor/ContentExtractor.php b/inc/3rdparty/libraries/content-extractor/ContentExtractor.php
index ddd33bb5..21e693e7 100644
--- a/inc/3rdparty/libraries/content-extractor/ContentExtractor.php
+++ b/inc/3rdparty/libraries/content-extractor/ContentExtractor.php
@@ -1,728 +1,727 @@
1<?php 1<?php
2/** 2/**
3 * Content Extractor 3 * Content Extractor
4 * 4 *
5 * Uses patterns specified in site config files and auto detection (hNews/PHP Readability) 5 * Uses patterns specified in site config files and auto detection (hNews/PHP Readability)
6 * to extract content from HTML files. 6 * to extract content from HTML files.
7 * 7 *
8 * @version 1.0 8 * @version 1.0
9 * @date 2013-02-05 9 * @date 2013-02-05
10 * @author Keyvan Minoukadeh 10 * @author Keyvan Minoukadeh
11 * @copyright 2013 Keyvan Minoukadeh 11 * @copyright 2013 Keyvan Minoukadeh
12 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3 12 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3
13 */ 13 */
14 14
15class ContentExtractor 15class ContentExtractor
16{ 16{
17 protected static $tidy_config = array( 17 protected static $tidy_config = array(
18 'clean' => true, 18 'clean' => true,
19 'output-xhtml' => true, 19 'output-xhtml' => true,
20 'logical-emphasis' => true, 20 'logical-emphasis' => true,
21 'show-body-only' => false, 21 'show-body-only' => false,
22 'new-blocklevel-tags' => 'article, aside, footer, header, hgroup, menu, nav, section, details, datagrid', 22 'new-blocklevel-tags' => 'article, aside, footer, header, hgroup, menu, nav, section, details, datagrid',
23 'new-inline-tags' => 'mark, time, meter, progress, data', 23 'new-inline-tags' => 'mark, time, meter, progress, data',
24 'wrap' => 0, 24 'wrap' => 0,
25 'drop-empty-paras' => true, 25 'drop-empty-paras' => true,
26 'drop-proprietary-attributes' => false, 26 'drop-proprietary-attributes' => false,
27 'enclose-text' => true, 27 'enclose-text' => true,
28 'enclose-block-text' => true, 28 'enclose-block-text' => true,
29 'merge-divs' => true, 29 'merge-divs' => true,
30 'merge-spans' => true, 30 'merge-spans' => true,
31 'char-encoding' => 'utf8', 31 'char-encoding' => 'utf8',
32 'hide-comments' => true 32 'hide-comments' => true
33 ); 33 );
34 protected $html; 34 protected $html;
35 protected $config; 35 protected $config;
36 protected $title; 36 protected $title;
37 protected $author = array(); 37 protected $author = array();
38 protected $language; 38 protected $language;
39 protected $date; 39 protected $date;
40 protected $body; 40 protected $body;
41 protected $success = false; 41 protected $success = false;
42 protected $nextPageUrl; 42 protected $nextPageUrl;
43 public $allowedParsers = array('libxml', 'html5lib'); 43 public $allowedParsers = array('libxml', 'html5lib');
44 public $fingerprints = array(); 44 public $fingerprints = array();
45 public $readability; 45 public $readability;
46 public $debug = false; 46 public $debug = false;
47 public $debugVerbose = false; 47 public $debugVerbose = false;
48 48
49 function __construct($path, $fallback=null) { 49 function __construct($path, $fallback=null) {
50 SiteConfig::set_config_path($path, $fallback); 50 SiteConfig::set_config_path($path, $fallback);
51 } 51 }
52 52
53 protected function debug($msg) { 53 protected function debug($msg) {
54 if ($this->debug) { 54 if ($this->debug) {
55 $mem = round(memory_get_usage()/1024, 2); 55 $mem = round(memory_get_usage()/1024, 2);
56 $memPeak = round(memory_get_peak_usage()/1024, 2); 56 $memPeak = round(memory_get_peak_usage()/1024, 2);
57 echo '* ',$msg; 57 echo '* ',$msg;
58 if ($this->debugVerbose) echo ' - mem used: ',$mem," (peak: $memPeak)"; 58 if ($this->debugVerbose) echo ' - mem used: ',$mem," (peak: $memPeak)";
59 echo "\n"; 59 echo "\n";
60 ob_flush(); 60 ob_flush();
61 flush(); 61 flush();
62 } 62 }
63 } 63 }
64 64
65 public function reset() { 65 public function reset() {
66 $this->html = null; 66 $this->html = null;
67 $this->readability = null; 67 $this->readability = null;
68 $this->config = null; 68 $this->config = null;
69 $this->title = null; 69 $this->title = null;
70 $this->body = null; 70 $this->body = null;
71 $this->author = array(); 71 $this->author = array();
72 $this->language = null; 72 $this->language = null;
73 $this->date = null; 73 $this->date = null;
74 $this->nextPageUrl = null; 74 $this->nextPageUrl = null;
75 $this->success = false; 75 $this->success = false;
76 } 76 }
77 77
78 public function findHostUsingFingerprints($html) { 78 public function findHostUsingFingerprints($html) {
79 $this->debug('Checking fingerprints...'); 79 $this->debug('Checking fingerprints...');
80 $head = substr($html, 0, 8000); 80 $head = substr($html, 0, 8000);
81 foreach ($this->fingerprints as $_fp => $_fphost) { 81 foreach ($this->fingerprints as $_fp => $_fphost) {
82 $lookin = 'html'; 82 $lookin = 'html';
83 if (is_array($_fphost)) { 83 if (is_array($_fphost)) {
84 if (isset($_fphost['head']) && $_fphost['head']) { 84 if (isset($_fphost['head']) && $_fphost['head']) {
85 $lookin = 'head'; 85 $lookin = 'head';
86 } 86 }
87 $_fphost = $_fphost['hostname']; 87 $_fphost = $_fphost['hostname'];
88 } 88 }
89 if (strpos($$lookin, $_fp) !== false) { 89 if (strpos($$lookin, $_fp) !== false) {
90 $this->debug("Found match: $_fphost"); 90 $this->debug("Found match: $_fphost");
91 return $_fphost; 91 return $_fphost;
92 } 92 }
93 } 93 }
94 $this->debug('No fingerprint matches'); 94 $this->debug('No fingerprint matches');
95 return false; 95 return false;
96 } 96 }
97 97
98 // returns SiteConfig instance (joined in order: exact match, wildcard, fingerprint, global, default) 98 // returns SiteConfig instance (joined in order: exact match, wildcard, fingerprint, global, default)
99 public function buildSiteConfig($url, $html='', $add_to_cache=true) { 99 public function buildSiteConfig($url, $html='', $add_to_cache=true) {
100 // extract host name 100 // extract host name
101 $host = @parse_url($url, PHP_URL_HOST); 101 $host = @parse_url($url, PHP_URL_HOST);
102 $host = strtolower($host); 102 $host = strtolower($host);
103 if (substr($host, 0, 4) == 'www.') $host = substr($host, 4); 103 if (substr($host, 0, 4) == 'www.') $host = substr($host, 4);
104 // is merged version already cached? 104 // is merged version already cached?
105 if (SiteConfig::is_cached("$host.merged")) { 105 if (SiteConfig::is_cached("$host.merged")) {
106 $this->debug("Returning cached and merged site config for $host"); 106 $this->debug("Returning cached and merged site config for $host");
107 return SiteConfig::build("$host.merged"); 107 return SiteConfig::build("$host.merged");
108 } 108 }
109 // let's build from site_config/custom/ and standard/ 109 // let's build from site_config/custom/ and standard/
110 $config = SiteConfig::build($host); 110 $config = SiteConfig::build($host);
111 if ($add_to_cache && $config && !SiteConfig::is_cached("$host")) { 111 if ($add_to_cache && $config && !SiteConfig::is_cached("$host")) {
112 SiteConfig::add_to_cache($host, $config); 112 SiteConfig::add_to_cache($host, $config);
113 } 113 }
114 // if no match, use defaults 114 // if no match, use defaults
115 if (!$config) $config = new SiteConfig(); 115 if (!$config) $config = new SiteConfig();
116 // load fingerprint config? 116 // load fingerprint config?
117 if ($config->autodetect_on_failure()) { 117 if ($config->autodetect_on_failure()) {
118 // check HTML for fingerprints 118 // check HTML for fingerprints
119 if (!empty($this->fingerprints) && ($_fphost = $this->findHostUsingFingerprints($html))) { 119 if (!empty($this->fingerprints) && ($_fphost = $this->findHostUsingFingerprints($html))) {
120 if ($config_fingerprint = SiteConfig::build($_fphost)) { 120 if ($config_fingerprint = SiteConfig::build($_fphost)) {
121 $this->debug("Appending site config settings from $_fphost (fingerprint match)"); 121 $this->debug("Appending site config settings from $_fphost (fingerprint match)");
122 $config->append($config_fingerprint); 122 $config->append($config_fingerprint);
123 if ($add_to_cache && !SiteConfig::is_cached($_fphost)) { 123 if ($add_to_cache && !SiteConfig::is_cached($_fphost)) {
124 //$config_fingerprint->cache_in_apc = true; 124 //$config_fingerprint->cache_in_apc = true;
125 SiteConfig::add_to_cache($_fphost, $config_fingerprint); 125 SiteConfig::add_to_cache($_fphost, $config_fingerprint);
126 } 126 }
127 } 127 }
128 } 128 }
129 } 129 }
130 // load global config? 130 // load global config?
131 if ($config->autodetect_on_failure()) { 131 if ($config->autodetect_on_failure()) {
132 if ($config_global = SiteConfig::build('global', true)) { 132 if ($config_global = SiteConfig::build('global', true)) {
133 $this->debug('Appending site config settings from global.txt'); 133 $this->debug('Appending site config settings from global.txt');
134 $config->append($config_global); 134 $config->append($config_global);
135 if ($add_to_cache && !SiteConfig::is_cached('global')) { 135 if ($add_to_cache && !SiteConfig::is_cached('global')) {
136 //$config_global->cache_in_apc = true; 136 //$config_global->cache_in_apc = true;
137 SiteConfig::add_to_cache('global', $config_global); 137 SiteConfig::add_to_cache('global', $config_global);
138 } 138 }
139 } 139 }
140 } 140 }
141 // store copy of merged config 141 // store copy of merged config
142 if ($add_to_cache) { 142 if ($add_to_cache) {
143 // do not store in APC if wildcard match 143 // do not store in APC if wildcard match
144 $use_apc = ($host == $config->cache_key); 144 $use_apc = ($host == $config->cache_key);
145 $config->cache_key = null; 145 $config->cache_key = null;
146 SiteConfig::add_to_cache("$host.merged", $config, $use_apc); 146 SiteConfig::add_to_cache("$host.merged", $config, $use_apc);
147 } 147 }
148 return $config; 148 return $config;
149 } 149 }
150 150
151 // returns true on success, false on failure 151 // returns true on success, false on failure
152 // $smart_tidy indicates that if tidy is used and no results are produced, we will 152 // $smart_tidy indicates that if tidy is used and no results are produced, we will
153 // try again without it. Tidy helps us deal with PHP's patchy HTML parsing most of the time 153 // try again without it. Tidy helps us deal with PHP's patchy HTML parsing most of the time
154 // but it has problems of its own which we try to avoid with this option. 154 // but it has problems of its own which we try to avoid with this option.
155 public function process($html, $url, $smart_tidy=true) { 155 public function process($html, $url, $smart_tidy=true) {
156 $this->reset(); 156 $this->reset();
157 $this->config = $this->buildSiteConfig($url, $html); 157 $this->config = $this->buildSiteConfig($url, $html);
158 158
159 // do string replacements 159 // do string replacements
160 if (!empty($this->config->find_string)) { 160 if (!empty($this->config->find_string)) {
161 if (count($this->config->find_string) == count($this->config->replace_string)) { 161 if (count($this->config->find_string) == count($this->config->replace_string)) {
162 $html = str_replace($this->config->find_string, $this->config->replace_string, $html, $_count); 162 $html = str_replace($this->config->find_string, $this->config->replace_string, $html, $_count);
163 $this->debug("Strings replaced: $_count (find_string and/or replace_string)"); 163 $this->debug("Strings replaced: $_count (find_string and/or replace_string)");
164 } else { 164 } else {
165 $this->debug('Skipped string replacement - incorrect number of find-replace strings in site config'); 165 $this->debug('Skipped string replacement - incorrect number of find-replace strings in site config');
166 } 166 }
167 unset($_count); 167 unset($_count);
168 } 168 }
169 169
170 // use tidy (if it exists)? 170 // use tidy (if it exists)?
171 // This fixes problems with some sites which would otherwise 171 // This fixes problems with some sites which would otherwise
172 // trouble DOMDocument's HTML parsing. (Although sometimes it 172 // trouble DOMDocument's HTML parsing. (Although sometimes it
173 // makes matters worse, which is why you can override it in site config files.) 173 // makes matters worse, which is why you can override it in site config files.)
174 $tidied = false; 174 $tidied = false;
175 if ($this->config->tidy() && function_exists('tidy_parse_string') && $smart_tidy) { 175 if ($this->config->tidy() && function_exists('tidy_parse_string') && $smart_tidy) {
176 $this->debug('Using Tidy'); 176 $this->debug('Using Tidy');
177 $tidy = tidy_parse_string($html, self::$tidy_config, 'UTF8'); 177 $tidy = tidy_parse_string($html, self::$tidy_config, 'UTF8');
178 if (tidy_clean_repair($tidy)) { 178 if (tidy_clean_repair($tidy)) {
179 $original_html = $html; 179 $original_html = $html;
180 $tidied = true; 180 $tidied = true;
181 $html = $tidy->value; 181 $html = $tidy->value;
182 } 182 }
183 unset($tidy); 183 unset($tidy);
184 } 184 }
185 185
186 // load and parse html 186 // load and parse html
187 $_parser = $this->config->parser(); 187 $_parser = $this->config->parser();
188 if (!in_array($_parser, $this->allowedParsers)) { 188 if (!in_array($_parser, $this->allowedParsers)) {
189 $this->debug("HTML parser $_parser not listed, using libxml instead"); 189 $this->debug("HTML parser $_parser not listed, using libxml instead");
190 $_parser = 'libxml'; 190 $_parser = 'libxml';
191 } 191 }
192 $this->debug("Attempting to parse HTML with $_parser"); 192 $this->debug("Attempting to parse HTML with $_parser");
193 $this->readability = new Readability($html, $url, $_parser); 193 $this->readability = new Readability($html, $url, $_parser);
194 194
195 // we use xpath to find elements in the given HTML document 195 // we use xpath to find elements in the given HTML document
196 // see http://en.wikipedia.org/wiki/XPath_1.0 196 // see http://en.wikipedia.org/wiki/XPath_1.0
197 $xpath = new DOMXPath($this->readability->dom); 197 $xpath = new DOMXPath($this->readability->dom);
198 198
199 // try to get next page link 199 // try to get next page link
200 foreach ($this->config->next_page_link as $pattern) { 200 foreach ($this->config->next_page_link as $pattern) {
201 $elems = @$xpath->evaluate($pattern, $this->readability->dom); 201 $elems = @$xpath->evaluate($pattern, $this->readability->dom);
202 if (is_string($elems)) { 202 if (is_string($elems)) {
203 $this->nextPageUrl = trim($elems); 203 $this->nextPageUrl = trim($elems);
204 break; 204 break;
205 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) { 205 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
206 foreach ($elems as $item) { 206 foreach ($elems as $item) {
207 if ($item instanceof DOMElement && $item->hasAttribute('href')) { 207 if ($item instanceof DOMElement && $item->hasAttribute('href')) {
208 $this->nextPageUrl = $item->getAttribute('href'); 208 $this->nextPageUrl = $item->getAttribute('href');
209 break 2; 209 break 2;
210 } elseif ($item instanceof DOMAttr && $item->value) { 210 } elseif ($item instanceof DOMAttr && $item->value) {
211 $this->nextPageUrl = $item->value; 211 $this->nextPageUrl = $item->value;
212 break 2; 212 break 2;
213 } 213 }
214 } 214 }
215 } 215 }
216 } 216 }
217 217
218 // try to get title 218 // try to get title
219 foreach ($this->config->title as $pattern) { 219 foreach ($this->config->title as $pattern) {
220 // $this->debug("Trying $pattern"); 220 // $this->debug("Trying $pattern");
221 $elems = @$xpath->evaluate($pattern, $this->readability->dom); 221 $elems = @$xpath->evaluate($pattern, $this->readability->dom);
222 if (is_string($elems)) { 222 if (is_string($elems)) {
223 $this->title = trim($elems); 223 $this->title = trim($elems);
224 $this->debug('Title expression evaluated as string: '.$this->title); 224 $this->debug('Title expression evaluated as string: '.$this->title);
225 $this->debug("...XPath match: $pattern"); 225 $this->debug("...XPath match: $pattern");
226 break; 226 break;
227 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) { 227 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
228 $this->title = $elems->item(0)->textContent; 228 $this->title = $elems->item(0)->textContent;
229 $this->debug('Title matched: '.$this->title); 229 $this->debug('Title matched: '.$this->title);
230 $this->debug("...XPath match: $pattern"); 230 $this->debug("...XPath match: $pattern");
231 // remove title from document 231 // remove title from document
232 try { 232 try {
233 $elems->item(0)->parentNode->removeChild($elems->item(0)); 233 @$elems->item(0)->parentNode->removeChild($elems->item(0));
234 } catch (DOMException $e) { 234 } catch (DOMException $e) {
235 // do nothing 235 // do nothing
236 } 236 }
237 break; 237 break;
238 } 238 }
239 } 239 }
240 240
241 // try to get author (if it hasn't already been set) 241 // try to get author (if it hasn't already been set)
242 if (empty($this->author)) { 242 if (empty($this->author)) {
243 foreach ($this->config->author as $pattern) { 243 foreach ($this->config->author as $pattern) {
244 $elems = @$xpath->evaluate($pattern, $this->readability->dom); 244 $elems = @$xpath->evaluate($pattern, $this->readability->dom);
245 if (is_string($elems)) { 245 if (is_string($elems)) {
246 if (trim($elems) != '') { 246 if (trim($elems) != '') {
247 $this->author[] = trim($elems); 247 $this->author[] = trim($elems);
248 $this->debug('Author expression evaluated as string: '.trim($elems)); 248 $this->debug('Author expression evaluated as string: '.trim($elems));
249 $this->debug("...XPath match: $pattern"); 249 $this->debug("...XPath match: $pattern");
250 break; 250 break;
251 } 251 }
252 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) { 252 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
253 foreach ($elems as $elem) { 253 foreach ($elems as $elem) {
254 if (!isset($elem->parentNode)) continue; 254 if (!isset($elem->parentNode)) continue;
255 $this->author[] = trim($elem->textContent); 255 $this->author[] = trim($elem->textContent);
256 $this->debug('Author matched: '.trim($elem->textContent)); 256 $this->debug('Author matched: '.trim($elem->textContent));
257 } 257 }
258 if (!empty($this->author)) { 258 if (!empty($this->author)) {
259 $this->debug("...XPath match: $pattern"); 259 $this->debug("...XPath match: $pattern");
260 break; 260 break;
261 } 261 }
262 } 262 }
263 } 263 }
264 } 264 }
265 265
266 // try to get language 266 // try to get language
267 $_lang_xpath = array('//html[@lang]/@lang', '//meta[@name="DC.language"]/@content'); 267 $_lang_xpath = array('//html[@lang]/@lang', '//meta[@name="DC.language"]/@content');
268 foreach ($_lang_xpath as $pattern) { 268 foreach ($_lang_xpath as $pattern) {
269 $elems = @$xpath->evaluate($pattern, $this->readability->dom); 269 $elems = @$xpath->evaluate($pattern, $this->readability->dom);
270 if (is_string($elems)) { 270 if (is_string($elems)) {
271 if (trim($elems) != '') { 271 if (trim($elems) != '') {
272 $this->language = trim($elems); 272 $this->language = trim($elems);
273 $this->debug('Language matched: '.$this->language); 273 $this->debug('Language matched: '.$this->language);
274 break; 274 break;
275 } 275 }
276 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) { 276 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
277 foreach ($elems as $elem) { 277 foreach ($elems as $elem) {
278 if (!isset($elem->parentNode)) continue; 278 if (!isset($elem->parentNode)) continue;
279 $this->language = trim($elem->textContent); 279 $this->language = trim($elem->textContent);
280 $this->debug('Language matched: '.$this->language); 280 $this->debug('Language matched: '.$this->language);
281 } 281 }
282 if ($this->language) break; 282 if ($this->language) break;
283 } 283 }
284 } 284 }
285 285
286 // try to get date 286 // try to get date
287 foreach ($this->config->date as $pattern) { 287 foreach ($this->config->date as $pattern) {
288 $elems = @$xpath->evaluate($pattern, $this->readability->dom); 288 $elems = @$xpath->evaluate($pattern, $this->readability->dom);
289 if (is_string($elems)) { 289 if (is_string($elems)) {
290 $this->date = strtotime(trim($elems, "; \t\n\r\0\x0B")); 290 $this->date = strtotime(trim($elems, "; \t\n\r\0\x0B"));
291 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) { 291 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
292 $this->date = $elems->item(0)->textContent; 292 $this->date = $elems->item(0)->textContent;
293 $this->date = strtotime(trim($this->date, "; \t\n\r\0\x0B")); 293 $this->date = strtotime(trim($this->date, "; \t\n\r\0\x0B"));
294 // remove date from document 294 // remove date from document
295 // $elems->item(0)->parentNode->removeChild($elems->item(0)); 295 // $elems->item(0)->parentNode->removeChild($elems->item(0));
296 } 296 }
297 if (!$this->date) { 297 if (!$this->date) {
298 $this->date = null; 298 $this->date = null;
299 } else { 299 } else {
300 $this->debug('Date matched: '.date('Y-m-d H:i:s', $this->date)); 300 $this->debug('Date matched: '.date('Y-m-d H:i:s', $this->date));
301 $this->debug("...XPath match: $pattern"); 301 $this->debug("...XPath match: $pattern");
302 break; 302 break;
303 } 303 }
304 } 304 }
305 305
306 // strip elements (using xpath expressions) 306 // strip elements (using xpath expressions)
307 foreach ($this->config->strip as $pattern) { 307 foreach ($this->config->strip as $pattern) {
308 $elems = @$xpath->query($pattern, $this->readability->dom); 308 $elems = @$xpath->query($pattern, $this->readability->dom);
309 // check for matches 309 // check for matches
310 if ($elems && $elems->length > 0) { 310 if ($elems && $elems->length > 0) {
311 $this->debug('Stripping '.$elems->length.' elements (strip)'); 311 $this->debug('Stripping '.$elems->length.' elements (strip)');
312 for ($i=$elems->length-1; $i >= 0; $i--) { 312 for ($i=$elems->length-1; $i >= 0; $i--) {
313 $elems->item($i)->parentNode->removeChild($elems->item($i)); 313 $elems->item($i)->parentNode->removeChild($elems->item($i));
314 } 314 }
315 } 315 }
316 } 316 }
317 317
318 // strip elements (using id and class attribute values) 318 // strip elements (using id and class attribute values)
319 foreach ($this->config->strip_id_or_class as $string) { 319 foreach ($this->config->strip_id_or_class as $string) {
320 $string = strtr($string, array("'"=>'', '"'=>'')); 320 $string = strtr($string, array("'"=>'', '"'=>''));
321 $elems = @$xpath->query("//*[contains(@class, '$string') or contains(@id, '$string')]", $this->readability->dom); 321 $elems = @$xpath->query("//*[contains(@class, '$string') or contains(@id, '$string')]", $this->readability->dom);
322 // check for matches 322 // check for matches
323 if ($elems && $elems->length > 0) { 323 if ($elems && $elems->length > 0) {
324 $this->debug('Stripping '.$elems->length.' elements (strip_id_or_class)'); 324 $this->debug('Stripping '.$elems->length.' elements (strip_id_or_class)');
325 for ($i=$elems->length-1; $i >= 0; $i--) { 325 for ($i=$elems->length-1; $i >= 0; $i--) {
326 $elems->item($i)->parentNode->removeChild($elems->item($i)); 326 $elems->item($i)->parentNode->removeChild($elems->item($i));
327 } 327 }
328 } 328 }
329 } 329 }
330 330
331 // strip images (using src attribute values) 331 // strip images (using src attribute values)
332 foreach ($this->config->strip_image_src as $string) { 332 foreach ($this->config->strip_image_src as $string) {
333 $string = strtr($string, array("'"=>'', '"'=>'')); 333 $string = strtr($string, array("'"=>'', '"'=>''));
334 $elems = @$xpath->query("//img[contains(@src, '$string')]", $this->readability->dom); 334 $elems = @$xpath->query("//img[contains(@src, '$string')]", $this->readability->dom);
335 // check for matches 335 // check for matches
336 if ($elems && $elems->length > 0) { 336 if ($elems && $elems->length > 0) {
337 $this->debug('Stripping '.$elems->length.' image elements'); 337 $this->debug('Stripping '.$elems->length.' image elements');
338 for ($i=$elems->length-1; $i >= 0; $i--) { 338 for ($i=$elems->length-1; $i >= 0; $i--) {
339 $elems->item($i)->parentNode->removeChild($elems->item($i)); 339 $elems->item($i)->parentNode->removeChild($elems->item($i));
340 } 340 }
341 } 341 }
342 } 342 }
343 // strip elements using Readability.com and Instapaper.com ignore class names 343 // strip elements using Readability.com and Instapaper.com ignore class names
344 // .entry-unrelated and .instapaper_ignore 344 // .entry-unrelated and .instapaper_ignore
345 // See https://www.readability.com/publishers/guidelines/#view-plainGuidelines 345 // See https://www.readability.com/publishers/guidelines/#view-plainGuidelines
346 // and http://blog.instapaper.com/post/730281947 346 // and http://blog.instapaper.com/post/730281947
347 $elems = @$xpath->query("//*[contains(concat(' ',normalize-space(@class),' '),' entry-unrelated ') or contains(concat(' ',normalize-space(@class),' '),' instapaper_ignore ')]", $this->readability->dom); 347 $elems = @$xpath->query("//*[contains(concat(' ',normalize-space(@class),' '),' entry-unrelated ') or contains(concat(' ',normalize-space(@class),' '),' instapaper_ignore ')]", $this->readability->dom);
348 // check for matches 348 // check for matches
349 if ($elems && $elems->length > 0) { 349 if ($elems && $elems->length > 0) {
350 $this->debug('Stripping '.$elems->length.' .entry-unrelated,.instapaper_ignore elements'); 350 $this->debug('Stripping '.$elems->length.' .entry-unrelated,.instapaper_ignore elements');
351 for ($i=$elems->length-1; $i >= 0; $i--) { 351 for ($i=$elems->length-1; $i >= 0; $i--) {
352 $elems->item($i)->parentNode->removeChild($elems->item($i)); 352 $elems->item($i)->parentNode->removeChild($elems->item($i));
353 } 353 }
354 } 354 }
355 355
356 // strip elements that contain style="display: none;" 356 // strip elements that contain style="display: none;"
357 $elems = @$xpath->query("//*[contains(@style,'display:none')]", $this->readability->dom); 357 $elems = @$xpath->query("//*[contains(@style,'display:none')]", $this->readability->dom);
358 // check for matches 358 // check for matches
359 if ($elems && $elems->length > 0) { 359 if ($elems && $elems->length > 0) {
360 $this->debug('Stripping '.$elems->length.' elements with inline display:none style'); 360 $this->debug('Stripping '.$elems->length.' elements with inline display:none style');
361 for ($i=$elems->length-1; $i >= 0; $i--) { 361 for ($i=$elems->length-1; $i >= 0; $i--) {
362 $elems->item($i)->parentNode->removeChild($elems->item($i)); 362 $elems->item($i)->parentNode->removeChild($elems->item($i));
363 } 363 }
364 } 364 }
365 365
366 // try to get body 366 // try to get body
367 foreach ($this->config->body as $pattern) { 367 foreach ($this->config->body as $pattern) {
368 $elems = @$xpath->query($pattern, $this->readability->dom); 368 $elems = @$xpath->query($pattern, $this->readability->dom);
369 // check for matches 369 // check for matches
370 if ($elems && $elems->length > 0) { 370 if ($elems && $elems->length > 0) {
371 $this->debug('Body matched'); 371 $this->debug('Body matched');
372 $this->debug("...XPath match: $pattern"); 372 $this->debug("...XPath match: $pattern");
373 if ($elems->length == 1) { 373 if ($elems->length == 1) {
374 $this->body = $elems->item(0); 374 $this->body = $elems->item(0);
375 // prune (clean up elements that may not be content) 375 // prune (clean up elements that may not be content)
376 if ($this->config->prune()) { 376 if ($this->config->prune()) {
377 $this->debug('...pruning content'); 377 $this->debug('...pruning content');
378 $this->readability->prepArticle($this->body); 378 $this->readability->prepArticle($this->body);
379 } 379 }
380 break; 380 break;
381 } else { 381 } else {
382 $this->body = $this->readability->dom->createElement('div'); 382 $this->body = $this->readability->dom->createElement('div');
383 $this->debug($elems->length.' body elems found'); 383 $this->debug($elems->length.' body elems found');
384 foreach ($elems as $elem) { 384 foreach ($elems as $elem) {
385 if (!isset($elem->parentNode)) continue; 385 if (!isset($elem->parentNode)) continue;
386 $isDescendant = false; 386 $isDescendant = false;
387 foreach ($this->body->childNodes as $parent) { 387 foreach ($this->body->childNodes as $parent) {
388 if ($this->isDescendant($parent, $elem)) { 388 if ($this->isDescendant($parent, $elem)) {
389 $isDescendant = true; 389 $isDescendant = true;
390 break; 390 break;
391 } 391 }
392 } 392 }
393 if ($isDescendant) { 393 if ($isDescendant) {
394 $this->debug('...element is child of another body element, skipping.'); 394 $this->debug('...element is child of another body element, skipping.');
395 } else { 395 } else {
396 // prune (clean up elements that may not be content) 396 // prune (clean up elements that may not be content)
397 if ($this->config->prune()) { 397 if ($this->config->prune()) {
398 $this->debug('Pruning content'); 398 $this->debug('Pruning content');
399 $this->readability->prepArticle($elem); 399 $this->readability->prepArticle($elem);
400 } 400 }
401 $this->debug('...element added to body'); 401 $this->debug('...element added to body');
402 $this->body->appendChild($elem); 402 $this->body->appendChild($elem);
403 } 403 }
404 } 404 }
405 if ($this->body->hasChildNodes()) break; 405 if ($this->body->hasChildNodes()) break;
406 } 406 }
407 } 407 }
408 } 408 }
409 409
410 // auto detect? 410 // auto detect?
411 $detect_title = $detect_body = $detect_author = $detect_date = false; 411 $detect_title = $detect_body = $detect_author = $detect_date = false;
412 // detect title? 412 // detect title?
413 if (!isset($this->title)) { 413 if (!isset($this->title)) {
414 if (empty($this->config->title) || $this->config->autodetect_on_failure()) { 414 if (empty($this->config->title) || $this->config->autodetect_on_failure()) {
415 $detect_title = true; 415 $detect_title = true;
416 } 416 }
417 } 417 }
418 // detect body? 418 // detect body?
419 if (!isset($this->body)) { 419 if (!isset($this->body)) {
420 if (empty($this->config->body) || $this->config->autodetect_on_failure()) { 420 if (empty($this->config->body) || $this->config->autodetect_on_failure()) {
421 $detect_body = true; 421 $detect_body = true;
422 } 422 }
423 } 423 }
424 // detect author? 424 // detect author?
425 if (empty($this->author)) { 425 if (empty($this->author)) {
426 if (empty($this->config->author) || $this->config->autodetect_on_failure()) { 426 if (empty($this->config->author) || $this->config->autodetect_on_failure()) {
427 $detect_author = true; 427 $detect_author = true;
428 } 428 }
429 } 429 }
430 // detect date? 430 // detect date?
431 if (!isset($this->date)) { 431 if (!isset($this->date)) {
432 if (empty($this->config->date) || $this->config->autodetect_on_failure()) { 432 if (empty($this->config->date) || $this->config->autodetect_on_failure()) {
433 $detect_date = true; 433 $detect_date = true;
434 } 434 }
435 } 435 }
436 436
437 // check for hNews 437 // check for hNews
438 if ($detect_title || $detect_body) { 438 if ($detect_title || $detect_body) {
439 // check for hentry 439 // check for hentry
440 $elems = @$xpath->query("//*[contains(concat(' ',normalize-space(@class),' '),' hentry ')]", $this->readability->dom); 440 $elems = @$xpath->query("//*[contains(concat(' ',normalize-space(@class),' '),' hentry ')]", $this->readability->dom);
441 if ($elems && $elems->length > 0) { 441 if ($elems && $elems->length > 0) {
442 $this->debug('hNews: found hentry'); 442 $this->debug('hNews: found hentry');
443 $hentry = $elems->item(0); 443 $hentry = $elems->item(0);
444 444
445 if ($detect_title) { 445 if ($detect_title) {
446 // check for entry-title 446 // check for entry-title
447 $elems = @$xpath->query(".//*[contains(concat(' ',normalize-space(@class),' '),' entry-title ')]", $hentry); 447 $elems = @$xpath->query(".//*[contains(concat(' ',normalize-space(@class),' '),' entry-title ')]", $hentry);
448 if ($elems && $elems->length > 0) { 448 if ($elems && $elems->length > 0) {
449 $this->title = $elems->item(0)->textContent; 449 $this->title = $elems->item(0)->textContent;
450 $this->debug('hNews: found entry-title: '.$this->title); 450 $this->debug('hNews: found entry-title: '.$this->title);
451 // remove title from document 451 // remove title from document
452 $elems->item(0)->parentNode->removeChild($elems->item(0)); 452 $elems->item(0)->parentNode->removeChild($elems->item(0));
453 $detect_title = false; 453 $detect_title = false;
454 } 454 }
455 } 455 }
456 456
457 if ($detect_date) { 457 if ($detect_date) {
458 // check for time element with pubdate attribute 458 // check for time element with pubdate attribute
459 $elems = @$xpath->query(".//time[@pubdate] | .//abbr[contains(concat(' ',normalize-space(@class),' '),' published ')]", $hentry); 459 $elems = @$xpath->query(".//time[@pubdate] | .//abbr[contains(concat(' ',normalize-space(@class),' '),' published ')]", $hentry);
460 if ($elems && $elems->length > 0) { 460 if ($elems && $elems->length > 0) {
461 $this->date = strtotime(trim($elems->item(0)->textContent)); 461 $this->date = strtotime(trim($elems->item(0)->textContent));
462 // remove date from document 462 // remove date from document
463 //$elems->item(0)->parentNode->removeChild($elems->item(0)); 463 //$elems->item(0)->parentNode->removeChild($elems->item(0));
464 if ($this->date) { 464 if ($this->date) {
465 $this->debug('hNews: found publication date: '.date('Y-m-d H:i:s', $this->date)); 465 $this->debug('hNews: found publication date: '.date('Y-m-d H:i:s', $this->date));
466 $detect_date = false; 466 $detect_date = false;
467 } else { 467 } else {
468 $this->date = null; 468 $this->date = null;
469 } 469 }
470 } 470 }
471 } 471 }
472 472
473 if ($detect_author) { 473 if ($detect_author) {
474 // check for time element with pubdate attribute 474 // check for time element with pubdate attribute
475 $elems = @$xpath->query(".//*[contains(concat(' ',normalize-space(@class),' '),' vcard ') and (contains(concat(' ',normalize-space(@class),' '),' author ') or contains(concat(' ',normalize-space(@class),' '),' byline '))]", $hentry); 475 $elems = @$xpath->query(".//*[contains(concat(' ',normalize-space(@class),' '),' vcard ') and (contains(concat(' ',normalize-space(@class),' '),' author ') or contains(concat(' ',normalize-space(@class),' '),' byline '))]", $hentry);
476 if ($elems && $elems->length > 0) { 476 if ($elems && $elems->length > 0) {
477 $author = $elems->item(0); 477 $author = $elems->item(0);
478 $fn = @$xpath->query(".//*[contains(concat(' ',normalize-space(@class),' '),' fn ')]", $author); 478 $fn = @$xpath->query(".//*[contains(concat(' ',normalize-space(@class),' '),' fn ')]", $author);
479 if ($fn && $fn->length > 0) { 479 if ($fn && $fn->length > 0) {
480 foreach ($fn as $_fn) { 480 foreach ($fn as $_fn) {
481 if (trim($_fn->textContent) != '') { 481 if (trim($_fn->textContent) != '') {
482 $this->author[] = trim($_fn->textContent); 482 $this->author[] = trim($_fn->textContent);
483 $this->debug('hNews: found author: '.trim($_fn->textContent)); 483 $this->debug('hNews: found author: '.trim($_fn->textContent));
484 } 484 }
485 } 485 }
486 } else { 486 } else {
487 if (trim($author->textContent) != '') { 487 if (trim($author->textContent) != '') {
488 $this->author[] = trim($author->textContent); 488 $this->author[] = trim($author->textContent);
489 $this->debug('hNews: found author: '.trim($author->textContent)); 489 $this->debug('hNews: found author: '.trim($author->textContent));
490 } 490 }
491 } 491 }
492 $detect_author = empty($this->author); 492 $detect_author = empty($this->author);
493 } 493 }
494 } 494 }
495 495
496 // check for entry-content. 496 // check for entry-content.
497 // according to hAtom spec, if there are multiple elements marked entry-content, 497 // according to hAtom spec, if there are multiple elements marked entry-content,
498 // we include all of these in the order they appear - see http://microformats.org/wiki/hatom#Entry_Content 498 // we include all of these in the order they appear - see http://microformats.org/wiki/hatom#Entry_Content
499 if ($detect_body) { 499 if ($detect_body) {
500 $elems = @$xpath->query(".//*[contains(concat(' ',normalize-space(@class),' '),' entry-content ')]", $hentry); 500 $elems = @$xpath->query(".//*[contains(concat(' ',normalize-space(@class),' '),' entry-content ')]", $hentry);
501 if ($elems && $elems->length > 0) { 501 if ($elems && $elems->length > 0) {
502 $this->debug('hNews: found entry-content'); 502 $this->debug('hNews: found entry-content');
503 if ($elems->length == 1) { 503 if ($elems->length == 1) {
504 // what if it's empty? (some sites misuse hNews - place their content outside an empty entry-content element) 504 // what if it's empty? (some sites misuse hNews - place their content outside an empty entry-content element)
505 $e = $elems->item(0); 505 $e = $elems->item(0);
506 if (($e->tagName == 'img') || (trim($e->textContent) != '')) { 506 if (($e->tagName == 'img') || (trim($e->textContent) != '')) {
507 $this->body = $elems->item(0); 507 $this->body = $elems->item(0);
508 // prune (clean up elements that may not be content) 508 // prune (clean up elements that may not be content)
509 if ($this->config->prune()) { 509 if ($this->config->prune()) {
510 $this->debug('Pruning content'); 510 $this->debug('Pruning content');
511 $this->readability->prepArticle($this->body); 511 $this->readability->prepArticle($this->body);
512 } 512 }
513 $detect_body = false; 513 $detect_body = false;
514 } else { 514 } else {
515 $this->debug('hNews: skipping entry-content - appears not to contain content'); 515 $this->debug('hNews: skipping entry-content - appears not to contain content');
516 } 516 }
517 unset($e); 517 unset($e);
518 } else { 518 } else {
519 $this->body = $this->readability->dom->createElement('div'); 519 $this->body = $this->readability->dom->createElement('div');
520 $this->debug($elems->length.' entry-content elems found'); 520 $this->debug($elems->length.' entry-content elems found');
521 foreach ($elems as $elem) { 521 foreach ($elems as $elem) {
522 if (!isset($elem->parentNode)) continue; 522 if (!isset($elem->parentNode)) continue;
523 $isDescendant = false; 523 $isDescendant = false;
524 foreach ($this->body->childNodes as $parent) { 524 foreach ($this->body->childNodes as $parent) {
525 if ($this->isDescendant($parent, $elem)) { 525 if ($this->isDescendant($parent, $elem)) {
526 $isDescendant = true; 526 $isDescendant = true;
527 break; 527 break;
528 } 528 }
529 } 529 }
530 if ($isDescendant) { 530 if ($isDescendant) {
531 $this->debug('Element is child of another body element, skipping.'); 531 $this->debug('Element is child of another body element, skipping.');
532 } else { 532 } else {
533 // prune (clean up elements that may not be content) 533 // prune (clean up elements that may not be content)
534 if ($this->config->prune()) { 534 if ($this->config->prune()) {
535 $this->debug('Pruning content'); 535 $this->debug('Pruning content');
536 $this->readability->prepArticle($elem); 536 $this->readability->prepArticle($elem);
537 } 537 }
538 $this->debug('Element added to body'); 538 $this->debug('Element added to body');
539 $this->body->appendChild($elem); 539 $this->body->appendChild($elem);
540 } 540 }
541 } 541 }
542 $detect_body = false; 542 $detect_body = false;
543 } 543 }
544 } 544 }
545 } 545 }
546 } 546 }
547 } 547 }
548 548
549 // check for elements marked with instapaper_title 549 // check for elements marked with instapaper_title
550 if ($detect_title) { 550 if ($detect_title) {
551 // check for instapaper_title 551 // check for instapaper_title
552 $elems = @$xpath->query("//*[contains(concat(' ',normalize-space(@class),' '),' instapaper_title ')]", $this->readability->dom); 552 $elems = @$xpath->query("//*[contains(concat(' ',normalize-space(@class),' '),' instapaper_title ')]", $this->readability->dom);
553 if ($elems && $elems->length > 0) { 553 if ($elems && $elems->length > 0) {
554 $this->title = $elems->item(0)->textContent; 554 $this->title = $elems->item(0)->textContent;
555 $this->debug('Title found (.instapaper_title): '.$this->title); 555 $this->debug('Title found (.instapaper_title): '.$this->title);
556 // remove title from document 556 // remove title from document
557 $elems->item(0)->parentNode->removeChild($elems->item(0)); 557 $elems->item(0)->parentNode->removeChild($elems->item(0));
558 $detect_title = false; 558 $detect_title = false;
559 } 559 }
560 } 560 }
561 // check for elements marked with instapaper_body 561 // check for elements marked with instapaper_body
562 if ($detect_body) { 562 if ($detect_body) {
563 $elems = @$xpath->query("//*[contains(concat(' ',normalize-space(@class),' '),' instapaper_body ')]", $this->readability->dom); 563 $elems = @$xpath->query("//*[contains(concat(' ',normalize-space(@class),' '),' instapaper_body ')]", $this->readability->dom);
564 if ($elems && $elems->length > 0) { 564 if ($elems && $elems->length > 0) {
565 $this->debug('body found (.instapaper_body)'); 565 $this->debug('body found (.instapaper_body)');
566 $this->body = $elems->item(0); 566 $this->body = $elems->item(0);
567 // prune (clean up elements that may not be content) 567 // prune (clean up elements that may not be content)
568 if ($this->config->prune()) { 568 if ($this->config->prune()) {
569 $this->debug('Pruning content'); 569 $this->debug('Pruning content');
570 $this->readability->prepArticle($this->body); 570 $this->readability->prepArticle($this->body);
571 } 571 }
572 $detect_body = false; 572 $detect_body = false;
573 } 573 }
574 } 574 }
575 575
576 // Find author in rel="author" marked element 576 // Find author in rel="author" marked element
577 // We only use this if there's exactly one. 577 // We only use this if there's exactly one.
578 // If there's more than one, it could indicate more than 578 // If there's more than one, it could indicate more than
579 // one author, but it could also indicate that we're processing 579 // one author, but it could also indicate that we're processing
580 // a page listing different articles with different authors. 580 // a page listing different articles with different authors.
581 if ($detect_author) { 581 if ($detect_author) {
582 $elems = @$xpath->query("//a[contains(concat(' ',normalize-space(@rel),' '),' author ')]", $this->readability->dom); 582 $elems = @$xpath->query("//a[contains(concat(' ',normalize-space(@rel),' '),' author ')]", $this->readability->dom);
583 if ($elems && $elems->length == 1) { 583 if ($elems && $elems->length == 1) {
584 $author = trim($elems->item(0)->textContent); 584 $author = trim($elems->item(0)->textContent);
585 if ($author != '') { 585 if ($author != '') {
586 $this->debug("Author found (rel=\"author\"): $author"); 586 $this->debug("Author found (rel=\"author\"): $author");
587 $this->author[] = $author; 587 $this->author[] = $author;
588 $detect_author = false; 588 $detect_author = false;
589 } 589 }
590 } 590 }
591 } 591 }
592 592
593 // Find date in pubdate marked time element 593 // Find date in pubdate marked time element
594 // For the same reason given above, we only use this 594 // For the same reason given above, we only use this
595 // if there's exactly one element. 595 // if there's exactly one element.
596 if ($detect_date) { 596 if ($detect_date) {
597 $elems = @$xpath->query("//time[@pubdate]", $this->readability->dom); 597 $elems = @$xpath->query("//time[@pubdate]", $this->readability->dom);
598 if ($elems && $elems->length == 1) { 598 if ($elems && $elems->length == 1) {
599 $this->date = strtotime(trim($elems->item(0)->textContent)); 599 $this->date = strtotime(trim($elems->item(0)->textContent));
600 // remove date from document 600 // remove date from document
601 //$elems->item(0)->parentNode->removeChild($elems->item(0)); 601 //$elems->item(0)->parentNode->removeChild($elems->item(0));
602 if ($this->date) { 602 if ($this->date) {
603 $this->debug('Date found (pubdate marked time element): '.date('Y-m-d H:i:s', $this->date)); 603 $this->debug('Date found (pubdate marked time element): '.date('Y-m-d H:i:s', $this->date));
604 $detect_date = false; 604 $detect_date = false;
605 } else { 605 } else {
606 $this->date = null; 606 $this->date = null;
607 } 607 }
608 } 608 }
609 } 609 }
610 610
611 // still missing title or body, so we detect using Readability 611 // still missing title or body, so we detect using Readability
612 if ($detect_title || $detect_body) { 612 if ($detect_title || $detect_body) {
613 $this->debug('Using Readability'); 613 $this->debug('Using Readability');
614 // clone body if we're only using Readability for title (otherwise it may interfere with body element) 614 // clone body if we're only using Readability for title (otherwise it may interfere with body element)
615 if (isset($this->body)) $this->body = $this->body->cloneNode(true); 615 if (isset($this->body)) $this->body = $this->body->cloneNode(true);
616 $success = $this->readability->init(); 616 $success = $this->readability->init();
617 } 617 }
618 if ($detect_title) { 618 if ($detect_title) {
619 $this->debug('Detecting title'); 619 $this->debug('Detecting title');
620 $this->title = $this->readability->getTitle()->textContent; 620 $this->title = $this->readability->getTitle()->textContent;
621 } 621 }
622 if ($detect_body && $success) { 622 if ($detect_body && $success) {
623 $this->debug('Detecting body'); 623 $this->debug('Detecting body');
624 $this->body = $this->readability->getContent(); 624 $this->body = $this->readability->getContent();
625 if ($this->body->childNodes->length == 1 && $this->body->firstChild->nodeType === XML_ELEMENT_NODE) { 625 if ($this->body->childNodes->length == 1 && $this->body->firstChild->nodeType === XML_ELEMENT_NODE) {
626 $this->body = $this->body->firstChild; 626 $this->body = $this->body->firstChild;
627 } 627 }
628 // prune (clean up elements that may not be content) 628 // prune (clean up elements that may not be content)
629 if ($this->config->prune()) { 629 if ($this->config->prune()) {
630 $this->debug('Pruning content'); 630 $this->debug('Pruning content');
631 $this->readability->prepArticle($this->body); 631 $this->readability->prepArticle($this->body);
632 } 632 }
633 } 633 }
634 if (isset($this->body)) { 634 if (isset($this->body)) {
635 // remove scripts 635 // remove scripts
636 $this->readability->removeScripts($this->body); 636 $this->readability->removeScripts($this->body);
637 // remove any h1-h6 elements that appear as first thing in the body 637 // remove any h1-h6 elements that appear as first thing in the body
638 // and which match our title 638 // and which match our title
639 if (isset($this->title) && ($this->title != '')) { 639 if (isset($this->title) && ($this->title != '')) {
640 $firstChild = $this->body->firstChild; 640 $firstChild = $this->body->firstChild;
641 while ($firstChild->nodeType && ($firstChild->nodeType !== XML_ELEMENT_NODE)) { 641 while ($firstChild->nodeType && ($firstChild->nodeType !== XML_ELEMENT_NODE)) {
642 $firstChild = $firstChild->nextSibling; 642 $firstChild = $firstChild->nextSibling;
643 } 643 }
644 if (($firstChild->nodeType === XML_ELEMENT_NODE) 644 if (($firstChild->nodeType === XML_ELEMENT_NODE)
645 && in_array(strtolower($firstChild->tagName), array('h1', 'h2', 'h3', 'h4', 'h5', 'h6')) 645 && in_array(strtolower($firstChild->tagName), array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))
646 && (strtolower(trim($firstChild->textContent)) == strtolower(trim($this->title)))) { 646 && (strtolower(trim($firstChild->textContent)) == strtolower(trim($this->title)))) {
647 $this->body->removeChild($firstChild); 647 $this->body->removeChild($firstChild);
648 } 648 }
649 } 649 }
650 // prevent self-closing iframes 650 // prevent self-closing iframes
651 $elems = $this->body->getElementsByTagName('iframe'); 651 $elems = $this->body->getElementsByTagName('iframe');
652 for ($i = $elems->length-1; $i >= 0; $i--) { 652 for ($i = $elems->length-1; $i >= 0; $i--) {
653 $e = $elems->item($i); 653 $e = $elems->item($i);
654 if (!$e->hasChildNodes()) { 654 if (!$e->hasChildNodes()) {
655 $e->appendChild($this->body->ownerDocument->createTextNode('[embedded content]')); 655 $e->appendChild($this->body->ownerDocument->createTextNode('[embedded content]'));
656 } 656 }
657 } 657 }
658 // remove image lazy loading - WordPress plugin http://wordpress.org/extend/plugins/lazy-load/ 658 // remove image lazy loading - WordPress plugin http://wordpress.org/extend/plugins/lazy-load/
659 // the plugin replaces the src attribute to point to a 1x1 gif and puts the original src 659 // the plugin replaces the src attribute to point to a 1x1 gif and puts the original src
660 // inside the data-lazy-src attribute. It also places the original image inside a noscript element 660 // inside the data-lazy-src attribute. It also places the original image inside a noscript element
661 // next to the amended one. 661 // next to the amended one.
662 $elems = @$xpath->query("//img[@data-lazy-src]", $this->body); 662 $elems = @$xpath->query("//img[@data-lazy-src]", $this->body);
663 for ($i = $elems->length-1; $i >= 0; $i--) { 663 for ($i = $elems->length-1; $i >= 0; $i--) {
664 $e = $elems->item($i); 664 $e = $elems->item($i);
665 // let's see if we can grab image from noscript 665 // let's see if we can grab image from noscript
666 if ($e->nextSibling !== null && $e->nextSibling->nodeName === 'noscript') { 666 if ($e->nextSibling !== null && $e->nextSibling->nodeName === 'noscript') {
667 $_new_elem = $e->ownerDocument->createDocumentFragment(); 667 $_new_elem = $e->ownerDocument->createDocumentFragment();
668 @$_new_elem->appendXML($e->nextSibling->innerHTML); 668 @$_new_elem->appendXML($e->nextSibling->innerHTML);
669 $e->nextSibling->parentNode->replaceChild($_new_elem, $e->nextSibling); 669 $e->nextSibling->parentNode->replaceChild($_new_elem, $e->nextSibling);
670 $e->parentNode->removeChild($e); 670 $e->parentNode->removeChild($e);
671 } else { 671 } else {
672 // Use data-lazy-src as src value 672 // Use data-lazy-src as src value
673 $e->setAttribute('src', $e->getAttribute('data-lazy-src')); 673 $e->setAttribute('src', $e->getAttribute('data-lazy-src'));
674 $e->removeAttribute('data-lazy-src'); 674 $e->removeAttribute('data-lazy-src');
675 } 675 }
676 } 676 }
677 677
678 $this->success = true; 678 $this->success = true;
679 } 679 }
680 680
681 // if we've had no success and we've used tidy, there's a chance 681 // if we've had no success and we've used tidy, there's a chance
682 // that tidy has messed up. So let's try again without tidy... 682 // that tidy has messed up. So let's try again without tidy...
683 if (!$this->success && $tidied && $smart_tidy) { 683 if (!$this->success && $tidied && $smart_tidy) {
684 $this->debug('Trying again without tidy'); 684 $this->debug('Trying again without tidy');
685 $this->process($original_html, $url, false); 685 $this->process($original_html, $url, false);
686 } 686 }
687 687
688 return $this->success; 688 return $this->success;
689 } 689 }
690 690
691 private function isDescendant(DOMElement $parent, DOMElement $child) { 691 private function isDescendant(DOMElement $parent, DOMElement $child) {
692 $node = $child->parentNode; 692 $node = $child->parentNode;
693 while ($node != null) { 693 while ($node != null) {
694 if ($node->isSameNode($parent)) return true; 694 if ($node->isSameNode($parent)) return true;
695 $node = $node->parentNode; 695 $node = $node->parentNode;
696 } 696 }
697 return false; 697 return false;
698 } 698 }
699 699
700 public function getContent() { 700 public function getContent() {
701 return $this->body; 701 return $this->body;
702 } 702 }
703 703
704 public function getTitle() { 704 public function getTitle() {
705 return $this->title; 705 return $this->title;
706 } 706 }
707 707
708 public function getAuthors() { 708 public function getAuthors() {
709 return $this->author; 709 return $this->author;
710 } 710 }
711 711
712 public function getLanguage() { 712 public function getLanguage() {
713 return $this->language; 713 return $this->language;
714 } 714 }
715 715
716 public function getDate() { 716 public function getDate() {
717 return $this->date; 717 return $this->date;
718 } 718 }
719 719
720 public function getSiteConfig() { 720 public function getSiteConfig() {
721 return $this->config; 721 return $this->config;
722 } 722 }
723 723
724 public function getNextPageUrl() { 724 public function getNextPageUrl() {
725 return $this->nextPageUrl; 725 return $this->nextPageUrl;
726 } 726 }
727} 727} \ No newline at end of file
728?> \ No newline at end of file
diff --git a/inc/3rdparty/libraries/content-extractor/SiteConfig.php b/inc/3rdparty/libraries/content-extractor/SiteConfig.php
index c5e300d7..1f6a7603 100644
--- a/inc/3rdparty/libraries/content-extractor/SiteConfig.php
+++ b/inc/3rdparty/libraries/content-extractor/SiteConfig.php
@@ -1,338 +1,343 @@
1<?php 1<?php
2/** 2/**
3 * Site Config 3 * Site Config
4 * 4 *
5 * Each instance of this class should hold extraction patterns and other directives 5 * Each instance of this class should hold extraction patterns and other directives
6 * for a website. See ContentExtractor class to see how it's used. 6 * for a website. See ContentExtractor class to see how it's used.
7 * 7 *
8 * @version 0.7 8 * @version 0.8
9 * @date 2012-08-27 9 * @date 2013-04-16
10 * @author Keyvan Minoukadeh 10 * @author Keyvan Minoukadeh
11 * @copyright 2012 Keyvan Minoukadeh 11 * @copyright 2013 Keyvan Minoukadeh
12 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3 12 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3
13 */ 13 */
14 14
15class SiteConfig 15class SiteConfig
16{ 16{
17 // Use first matching element as title (0 or more xpath expressions) 17 // Use first matching element as title (0 or more xpath expressions)
18 public $title = array(); 18 public $title = array();
19 19
20 // Use first matching element as body (0 or more xpath expressions) 20 // Use first matching element as body (0 or more xpath expressions)
21 public $body = array(); 21 public $body = array();
22 22
23 // Use first matching element as author (0 or more xpath expressions) 23 // Use first matching element as author (0 or more xpath expressions)
24 public $author = array(); 24 public $author = array();
25 25
26 // Use first matching element as date (0 or more xpath expressions) 26 // Use first matching element as date (0 or more xpath expressions)
27 public $date = array(); 27 public $date = array();
28 28
29 // Strip elements matching these xpath expressions (0 or more) 29 // Strip elements matching these xpath expressions (0 or more)
30 public $strip = array(); 30 public $strip = array();
31 31
32 // Strip elements which contain these strings (0 or more) in the id or class attribute 32 // Strip elements which contain these strings (0 or more) in the id or class attribute
33 public $strip_id_or_class = array(); 33 public $strip_id_or_class = array();
34 34
35 // Strip images which contain these strings (0 or more) in the src attribute 35 // Strip images which contain these strings (0 or more) in the src attribute
36 public $strip_image_src = array(); 36 public $strip_image_src = array();
37 37
38 // Additional HTTP headers to send 38 // Additional HTTP headers to send
39 // NOT YET USED 39 // NOT YET USED
40 public $http_header = array(); 40 public $http_header = array();
41 41
42 // Process HTML with tidy before creating DOM (bool or null if undeclared) 42 // Process HTML with tidy before creating DOM (bool or null if undeclared)
43 public $tidy = null; 43 public $tidy = null;
44 44
45 protected $default_tidy = true; // used if undeclared 45 protected $default_tidy = true; // used if undeclared
46 46
47 // Autodetect title/body if xpath expressions fail to produce results. 47 // Autodetect title/body if xpath expressions fail to produce results.
48 // Note that this applies to title and body separately, ie. 48 // Note that this applies to title and body separately, ie.
49 // * if we get a body match but no title match, this option will determine whether we autodetect title 49 // * if we get a body match but no title match, this option will determine whether we autodetect title
50 // * if neither match, this determines whether we autodetect title and body. 50 // * if neither match, this determines whether we autodetect title and body.
51 // Also note that this only applies when there is at least one xpath expression in title or body, ie. 51 // Also note that this only applies when there is at least one xpath expression in title or body, ie.
52 // * if title and body are both empty (no xpath expressions), this option has no effect (both title and body will be auto-detected) 52 // * if title and body are both empty (no xpath expressions), this option has no effect (both title and body will be auto-detected)
53 // * if there's an xpath expression for title and none for body, body will be auto-detected and this option will determine whether we auto-detect title if the xpath expression for it fails to produce results. 53 // * if there's an xpath expression for title and none for body, body will be auto-detected and this option will determine whether we auto-detect title if the xpath expression for it fails to produce results.
54 // Usage scenario: you want to extract something specific from a set of URLs, e.g. a table, and if the table is not found, you want to ignore the entry completely. Auto-detection is unlikely to succeed here, so you construct your patterns and set this option to false. Another scenario may be a site where auto-detection has proven to fail (or worse, picked up the wrong content). 54 // Usage scenario: you want to extract something specific from a set of URLs, e.g. a table, and if the table is not found, you want to ignore the entry completely. Auto-detection is unlikely to succeed here, so you construct your patterns and set this option to false. Another scenario may be a site where auto-detection has proven to fail (or worse, picked up the wrong content).
55 // bool or null if undeclared 55 // bool or null if undeclared
56 public $autodetect_on_failure = null; 56 public $autodetect_on_failure = null;
57 protected $default_autodetect_on_failure = true; // used if undeclared 57 protected $default_autodetect_on_failure = true; // used if undeclared
58 58
59 // Clean up content block - attempt to remove elements that appear to be superfluous 59 // Clean up content block - attempt to remove elements that appear to be superfluous
60 // bool or null if undeclared 60 // bool or null if undeclared
61 public $prune = null; 61 public $prune = null;
62 protected $default_prune = true; // used if undeclared 62 protected $default_prune = true; // used if undeclared
63 63
64 // Test URL - if present, can be used to test the config above 64 // Test URL - if present, can be used to test the config above
65 public $test_url = array(); 65 public $test_url = array();
66 66
67 // Single-page link - should identify a link element or URL pointing to the page holding the entire article 67 // Single-page link - should identify a link element or URL pointing to the page holding the entire article
68 // This is useful for sites which split their articles across multiple pages. Links to such pages tend to 68 // This is useful for sites which split their articles across multiple pages. Links to such pages tend to
69 // display the first page with links to the other pages at the bottom. Often there is also a link to a page 69 // display the first page with links to the other pages at the bottom. Often there is also a link to a page
70 // which displays the entire article on one page (e.g. 'print view'). 70 // which displays the entire article on one page (e.g. 'print view').
71 // This should be an XPath expression identifying the link to that page. If present and we find a match, 71 // This should be an XPath expression identifying the link to that page. If present and we find a match,
72 // we will retrieve that page and the rest of the options in this config will be applied to the new page. 72 // we will retrieve that page and the rest of the options in this config will be applied to the new page.
73 public $single_page_link = array(); 73 public $single_page_link = array();
74 74
75 public $next_page_link = array(); 75 public $next_page_link = array();
76 76
77 // Single-page link in feed? - same as above, but patterns applied to item description HTML taken from feed 77 // Single-page link in feed? - same as above, but patterns applied to item description HTML taken from feed
78 public $single_page_link_in_feed = array(); 78 public $single_page_link_in_feed = array();
79 79
80 // Which parser to use for turning raw HTML into a DOMDocument (either 'libxml' or 'html5lib') 80 // Which parser to use for turning raw HTML into a DOMDocument (either 'libxml' or 'html5lib')
81 // string or null if undeclared 81 // string or null if undeclared
82 public $parser = null; 82 public $parser = null;
83 protected $default_parser = 'libxml'; // used if undeclared 83 protected $default_parser = 'libxml'; // used if undeclared
84 84
85 // Strings to search for in HTML before processing begins (used with $replace_string) 85 // Strings to search for in HTML before processing begins (used with $replace_string)
86 public $find_string = array(); 86 public $find_string = array();
87 // Strings to replace those found in $find_string before HTML processing begins 87 // Strings to replace those found in $find_string before HTML processing begins
88 public $replace_string = array(); 88 public $replace_string = array();
89 89
90 // the options below cannot be set in the config files which this class represents 90 // the options below cannot be set in the config files which this class represents
91 91
92 //public $cache_in_apc = false; // used to decide if we should cache in apc or not 92 //public $cache_in_apc = false; // used to decide if we should cache in apc or not
93 public $cache_key = null; 93 public $cache_key = null;
94 public static $debug = false; 94 public static $debug = false;
95 protected static $apc = false; 95 protected static $apc = false;
96 protected static $config_path; 96 protected static $config_path;
97 protected static $config_path_fallback; 97 protected static $config_path_fallback;
98 protected static $config_cache = array(); 98 protected static $config_cache = array();
99 const HOSTNAME_REGEX = '/^(([a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9-]*[A-Za-z0-9])$/'; 99 const HOSTNAME_REGEX = '/^(([a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9-]*[A-Za-z0-9])$/';
100 100
101 protected static function debug($msg) { 101 protected static function debug($msg) {
102 if (self::$debug) { 102 if (self::$debug) {
103 //$mem = round(memory_get_usage()/1024, 2); 103 //$mem = round(memory_get_usage()/1024, 2);
104 //$memPeak = round(memory_get_peak_usage()/1024, 2); 104 //$memPeak = round(memory_get_peak_usage()/1024, 2);
105 echo '* ',$msg; 105 echo '* ',$msg;
106 //echo ' - mem used: ',$mem," (peak: $memPeak)\n"; 106 //echo ' - mem used: ',$mem," (peak: $memPeak)\n";
107 echo "\n"; 107 echo "\n";
108 ob_flush(); 108 ob_flush();
109 flush(); 109 flush();
110 } 110 }
111 } 111 }
112 112
113 // enable APC caching of certain site config files? 113 // enable APC caching of certain site config files?
114 // If enabled the following site config files will be 114 // If enabled the following site config files will be
115 // cached in APC cache (when requested for first time): 115 // cached in APC cache (when requested for first time):
116 // * anything in site_config/custom/ and its corresponding file in site_config/standard/ 116 // * anything in site_config/custom/ and its corresponding file in site_config/standard/
117 // * the site config files associated with HTML fingerprints 117 // * the site config files associated with HTML fingerprints
118 // * the global site config file 118 // * the global site config file
119 // returns true if enabled, false otherwise 119 // returns true if enabled, false otherwise
120 public static function use_apc($apc=true) { 120 public static function use_apc($apc=true) {
121 if (!function_exists('apc_add')) { 121 if (!function_exists('apc_add')) {
122 if ($apc) self::debug('APC will not be used (function apc_add does not exist)'); 122 if ($apc) self::debug('APC will not be used (function apc_add does not exist)');
123 return false; 123 return false;
124 } 124 }
125 self::$apc = $apc; 125 self::$apc = $apc;
126 return $apc; 126 return $apc;
127 } 127 }
128 128
129 // return bool or null 129 // return bool or null
130 public function tidy($use_default=true) { 130 public function tidy($use_default=true) {
131 if ($use_default) return (isset($this->tidy)) ? $this->tidy : $this->default_tidy; 131 if ($use_default) return (isset($this->tidy)) ? $this->tidy : $this->default_tidy;
132 return $this->tidy; 132 return $this->tidy;
133 } 133 }
134 134
135 // return bool or null 135 // return bool or null
136 public function prune($use_default=true) { 136 public function prune($use_default=true) {
137 if ($use_default) return (isset($this->prune)) ? $this->prune : $this->default_prune; 137 if ($use_default) return (isset($this->prune)) ? $this->prune : $this->default_prune;
138 return $this->prune; 138 return $this->prune;
139 } 139 }
140 140
141 // return string or null 141 // return string or null
142 public function parser($use_default=true) { 142 public function parser($use_default=true) {
143 if ($use_default) return (isset($this->parser)) ? $this->parser : $this->default_parser; 143 if ($use_default) return (isset($this->parser)) ? $this->parser : $this->default_parser;
144 return $this->parser; 144 return $this->parser;
145 } 145 }
146 146
147 // return bool or null 147 // return bool or null
148 public function autodetect_on_failure($use_default=true) { 148 public function autodetect_on_failure($use_default=true) {
149 if ($use_default) return (isset($this->autodetect_on_failure)) ? $this->autodetect_on_failure : $this->default_autodetect_on_failure; 149 if ($use_default) return (isset($this->autodetect_on_failure)) ? $this->autodetect_on_failure : $this->default_autodetect_on_failure;
150 return $this->autodetect_on_failure; 150 return $this->autodetect_on_failure;
151 } 151 }
152 152
153 public static function set_config_path($path, $fallback=null) { 153 public static function set_config_path($path, $fallback=null) {
154 self::$config_path = $path; 154 self::$config_path = $path;
155 self::$config_path_fallback = $fallback; 155 self::$config_path_fallback = $fallback;
156 } 156 }
157 157
158 public static function add_to_cache($key, SiteConfig $config, $use_apc=true) { 158 public static function add_to_cache($key, SiteConfig $config, $use_apc=true) {
159 $key = strtolower($key); 159 $key = strtolower($key);
160 if (substr($key, 0, 4) == 'www.') $key = substr($key, 4); 160 if (substr($key, 0, 4) == 'www.') $key = substr($key, 4);
161 if ($config->cache_key) $key = $config->cache_key; 161 if ($config->cache_key) $key = $config->cache_key;
162 self::$config_cache[$key] = $config; 162 self::$config_cache[$key] = $config;
163 if (self::$apc && $use_apc) { 163 if (self::$apc && $use_apc) {
164 self::debug("Adding site config to APC cache with key sc.$key"); 164 self::debug("Adding site config to APC cache with key sc.$key");
165 apc_add("sc.$key", $config); 165 apc_add("sc.$key", $config);
166 } 166 }
167 self::debug("Cached site config with key $key"); 167 self::debug("Cached site config with key $key");
168 } 168 }
169 169
170 public static function is_cached($key) { 170 public static function is_cached($key) {
171 $key = strtolower($key); 171 $key = strtolower($key);
172 if (substr($key, 0, 4) == 'www.') $key = substr($key, 4); 172 if (substr($key, 0, 4) == 'www.') $key = substr($key, 4);
173 if (array_key_exists($key, self::$config_cache)) { 173 if (array_key_exists($key, self::$config_cache)) {
174 return true; 174 return true;
175 } elseif (self::$apc && (bool)apc_fetch("sc.$key")) { 175 } elseif (self::$apc && (bool)apc_fetch("sc.$key")) {
176 return true; 176 return true;
177 } 177 }
178 return false; 178 return false;
179 } 179 }
180 180
181 public function append(SiteConfig $newconfig) { 181 public function append(SiteConfig $newconfig) {
182 // check for commands where we accept multiple statements (no test_url) 182 // check for commands where we accept multiple statements (no test_url)
183 foreach (array('title', 'body', 'author', 'date', 'strip', 'strip_id_or_class', 'strip_image_src', 'single_page_link', 'single_page_link_in_feed', 'next_page_link', 'http_header', 'find_string', 'replace_string') as $var) { 183 foreach (array('title', 'body', 'author', 'date', 'strip', 'strip_id_or_class', 'strip_image_src', 'single_page_link', 'single_page_link_in_feed', 'next_page_link', 'http_header') as $var) {
184 // append array elements for this config variable from $newconfig to this config 184 // append array elements for this config variable from $newconfig to this config
185 //$this->$var = $this->$var + $newconfig->$var; 185 //$this->$var = $this->$var + $newconfig->$var;
186 $this->$var = array_unique(array_merge($this->$var, $newconfig->$var)); 186 $this->$var = array_unique(array_merge($this->$var, $newconfig->$var));
187 } 187 }
188 // check for single statement commands 188 // check for single statement commands
189 // we do not overwrite existing non null values 189 // we do not overwrite existing non null values
190 foreach (array('tidy', 'prune', 'parser', 'autodetect_on_failure') as $var) { 190 foreach (array('tidy', 'prune', 'parser', 'autodetect_on_failure') as $var) {
191 if ($this->$var === null) $this->$var = $newconfig->$var; 191 if ($this->$var === null) $this->$var = $newconfig->$var;
192 } 192 }
193 } 193 // treat find_string and replace_string separately (don't apply array_unique) (thanks fabrizio!)
194 194 foreach (array('find_string', 'replace_string') as $var) {
195 // returns SiteConfig instance if an appropriate one is found, false otherwise 195 // append array elements for this config variable from $newconfig to this config
196 // if $exact_host_match is true, we will not look for wildcard config matches 196 //$this->$var = $this->$var + $newconfig->$var;
197 // by default if host is 'test.example.org' we will look for and load '.example.org.txt' if it exists 197 $this->$var = array_merge($this->$var, $newconfig->$var);
198 public static function build($host, $exact_host_match=false) { 198 }
199 $host = strtolower($host); 199 }
200 if (substr($host, 0, 4) == 'www.') $host = substr($host, 4); 200
201 if (!$host || (strlen($host) > 200) || !preg_match(self::HOSTNAME_REGEX, ltrim($host, '.'))) return false; 201 // returns SiteConfig instance if an appropriate one is found, false otherwise
202 // check for site configuration 202 // if $exact_host_match is true, we will not look for wildcard config matches
203 $try = array($host); 203 // by default if host is 'test.example.org' we will look for and load '.example.org.txt' if it exists
204 // should we look for wildcard matches 204 public static function build($host, $exact_host_match=false) {
205 if (!$exact_host_match) { 205 $host = strtolower($host);
206 $split = explode('.', $host); 206 if (substr($host, 0, 4) == 'www.') $host = substr($host, 4);
207 if (count($split) > 1) { 207 if (!$host || (strlen($host) > 200) || !preg_match(self::HOSTNAME_REGEX, ltrim($host, '.'))) return false;
208 array_shift($split); 208 // check for site configuration
209 $try[] = '.'.implode('.', $split); 209 $try = array($host);
210 } 210 // should we look for wildcard matches
211 } 211 if (!$exact_host_match) {
212 212 $split = explode('.', $host);
213 // look for site config file in primary folder 213 if (count($split) > 1) {
214 self::debug(". looking for site config for $host in primary folder"); 214 array_shift($split);
215 foreach ($try as $h) { 215 $try[] = '.'.implode('.', $split);
216 if (array_key_exists($h, self::$config_cache)) { 216 }
217 self::debug("... site config for $h already loaded in this request"); 217 }
218 return self::$config_cache[$h]; 218
219 } elseif (self::$apc && ($sconfig = apc_fetch("sc.$h"))) { 219 // look for site config file in primary folder
220 self::debug("... site config for $h in APC cache"); 220 self::debug(". looking for site config for $host in primary folder");
221 return $sconfig; 221 foreach ($try as $h) {
222 } elseif (file_exists(self::$config_path."/$h.txt")) { 222 if (array_key_exists($h, self::$config_cache)) {
223 self::debug("... found site config ($h.txt)"); 223 self::debug("... site config for $h already loaded in this request");
224 $file_primary = self::$config_path."/$h.txt"; 224 return self::$config_cache[$h];
225 $matched_name = $h; 225 } elseif (self::$apc && ($sconfig = apc_fetch("sc.$h"))) {
226 break; 226 self::debug("... site config for $h in APC cache");
227 } 227 return $sconfig;
228 } 228 } elseif (file_exists(self::$config_path."/$h.txt")) {
229 229 self::debug("... found site config ($h.txt)");
230 // if we found site config, process it 230 $file_primary = self::$config_path."/$h.txt";
231 if (isset($file_primary)) { 231 $matched_name = $h;
232 $config_lines = file($file_primary, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 232 break;
233 if (!$config_lines || !is_array($config_lines)) return false; 233 }
234 $config = self::build_from_array($config_lines); 234 }
235 // if APC caching is available and enabled, mark this for cache 235
236 //$config->cache_in_apc = true; 236 // if we found site config, process it
237 $config->cache_key = $matched_name; 237 if (isset($file_primary)) {
238 238 $config_lines = file($file_primary, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
239 // if autodetec on failure is off (on by default) we do not need to look 239 if (!$config_lines || !is_array($config_lines)) return false;
240 // in secondary folder 240 $config = self::build_from_array($config_lines);
241 if (!$config->autodetect_on_failure()) { 241 // if APC caching is available and enabled, mark this for cache
242 self::debug('... autodetect on failure is disabled (no other site config files will be loaded)'); 242 //$config->cache_in_apc = true;
243 return $config; 243 $config->cache_key = $matched_name;
244 } 244
245 } 245 // if autodetec on failure is off (on by default) we do not need to look
246 246 // in secondary folder
247 // look for site config file in secondary folder 247 if (!$config->autodetect_on_failure()) {
248 if (isset(self::$config_path_fallback)) { 248 self::debug('... autodetect on failure is disabled (no other site config files will be loaded)');
249 self::debug(". looking for site config for $host in secondary folder"); 249 return $config;
250 foreach ($try as $h) { 250 }
251 if (file_exists(self::$config_path_fallback."/$h.txt")) { 251 }
252 self::debug("... found site config in secondary folder ($h.txt)"); 252
253 $file_secondary = self::$config_path_fallback."/$h.txt"; 253 // look for site config file in secondary folder
254 $matched_name = $h; 254 if (isset(self::$config_path_fallback)) {
255 break; 255 self::debug(". looking for site config for $host in secondary folder");
256 } 256 foreach ($try as $h) {
257 } 257 if (file_exists(self::$config_path_fallback."/$h.txt")) {
258 if (!isset($file_secondary)) { 258 self::debug("... found site config in secondary folder ($h.txt)");
259 self::debug("... no site config match in secondary folder"); 259 $file_secondary = self::$config_path_fallback."/$h.txt";
260 } 260 $matched_name = $h;
261 } 261 break;
262 262 }
263 // return false if no config file found 263 }
264 if (!isset($file_primary) && !isset($file_secondary)) { 264 if (!isset($file_secondary)) {
265 self::debug("... no site config match for $host"); 265 self::debug("... no site config match in secondary folder");
266 return false; 266 }
267 } 267 }
268 268
269 // return primary config if secondary not found 269 // return false if no config file found
270 if (!isset($file_secondary) && isset($config)) { 270 if (!isset($file_primary) && !isset($file_secondary)) {
271 return $config; 271 self::debug("... no site config match for $host");
272 } 272 return false;
273 273 }
274 // process secondary config file 274
275 $config_lines = file($file_secondary, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 275 // return primary config if secondary not found
276 if (!$config_lines || !is_array($config_lines)) { 276 if (!isset($file_secondary) && isset($config)) {
277 // failed to process secondary 277 return $config;
278 if (isset($config)) { 278 }
279 // return primary config 279
280 return $config; 280 // process secondary config file
281 } else { 281 $config_lines = file($file_secondary, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
282 return false; 282 if (!$config_lines || !is_array($config_lines)) {
283 } 283 // failed to process secondary
284 } 284 if (isset($config)) {
285 285 // return primary config
286 // merge with primary and return 286 return $config;
287 if (isset($config)) { 287 } else {
288 self::debug('. merging config files'); 288 return false;
289 $config->append(self::build_from_array($config_lines)); 289 }
290 return $config; 290 }
291 } else { 291
292 // return just secondary 292 // merge with primary and return
293 $config = self::build_from_array($config_lines); 293 if (isset($config)) {
294 // if APC caching is available and enabled, mark this for cache 294 self::debug('. merging config files');
295 //$config->cache_in_apc = true; 295 $config->append(self::build_from_array($config_lines));
296 $config->cache_key = $matched_name; 296 return $config;
297 return $config; 297 } else {
298 } 298 // return just secondary
299 } 299 $config = self::build_from_array($config_lines);
300 300 // if APC caching is available and enabled, mark this for cache
301 public static function build_from_array(array $lines) { 301 //$config->cache_in_apc = true;
302 $config = new SiteConfig(); 302 $config->cache_key = $matched_name;
303 foreach ($lines as $line) { 303 return $config;
304 $line = trim($line); 304 }
305 305 }
306 // skip comments, empty lines 306
307 if ($line == '' || $line[0] == '#') continue; 307 public static function build_from_array(array $lines) {
308 308 $config = new SiteConfig();
309 // get command 309 foreach ($lines as $line) {
310 $command = explode(':', $line, 2); 310 $line = trim($line);
311 // if there's no colon ':', skip this line 311
312 if (count($command) != 2) continue; 312 // skip comments, empty lines
313 $val = trim($command[1]); 313 if ($line == '' || $line[0] == '#') continue;
314 $command = trim($command[0]); 314
315 if ($command == '' || $val == '') continue; 315 // get command
316 316 $command = explode(':', $line, 2);
317 // check for commands where we accept multiple statements 317 // if there's no colon ':', skip this line
318 if (in_array($command, array('title', 'body', 'author', 'date', 'strip', 'strip_id_or_class', 'strip_image_src', 'single_page_link', 'single_page_link_in_feed', 'next_page_link', 'http_header', 'test_url', 'find_string', 'replace_string'))) { 318 if (count($command) != 2) continue;
319 array_push($config->$command, $val); 319 $val = trim($command[1]);
320 // check for single statement commands that evaluate to true or false 320 $command = trim($command[0]);
321 } elseif (in_array($command, array('tidy', 'prune', 'autodetect_on_failure'))) { 321 if ($command == '' || $val == '') continue;
322 $config->$command = ($val == 'yes'); 322
323 // check for single statement commands stored as strings 323 // check for commands where we accept multiple statements
324 } elseif (in_array($command, array('parser'))) { 324 if (in_array($command, array('title', 'body', 'author', 'date', 'strip', 'strip_id_or_class', 'strip_image_src', 'single_page_link', 'single_page_link_in_feed', 'next_page_link', 'http_header', 'test_url', 'find_string', 'replace_string'))) {
325 $config->$command = $val; 325 array_push($config->$command, $val);
326 // check for replace_string(find): replace 326 // check for single statement commands that evaluate to true or false
327 } elseif ((substr($command, -1) == ')') && preg_match('!^([a-z0-9_]+)\((.*?)\)$!i', $command, $match)) { 327 } elseif (in_array($command, array('tidy', 'prune', 'autodetect_on_failure'))) {
328 if (in_array($match[1], array('replace_string'))) { 328 $config->$command = ($val == 'yes');
329 $command = $match[1]; 329 // check for single statement commands stored as strings
330 array_push($config->find_string, $match[2]); 330 } elseif (in_array($command, array('parser'))) {
331 array_push($config->$command, $val); 331 $config->$command = $val;
332 } 332 // check for replace_string(find): replace
333 } 333 } elseif ((substr($command, -1) == ')') && preg_match('!^([a-z0-9_]+)\((.*?)\)$!i', $command, $match)) {
334 } 334 if (in_array($match[1], array('replace_string'))) {
335 return $config; 335 $command = $match[1];
336 } 336 array_push($config->find_string, $match[2]);
337} 337 array_push($config->$command, $val);
338?> \ No newline at end of file 338 }
339 }
340 }
341 return $config;
342 }
343} \ No newline at end of file
diff --git a/inc/3rdparty/libraries/feedwriter/FeedItem.php b/inc/3rdparty/libraries/feedwriter/FeedItem.php
index 9373deeb..40786598 100644..100755
--- a/inc/3rdparty/libraries/feedwriter/FeedItem.php
+++ b/inc/3rdparty/libraries/feedwriter/FeedItem.php
@@ -1,7 +1,7 @@
1<?php 1<?php
2 /** 2 /**
3 * Univarsel Feed Writer 3 * Univarsel Feed Writer
4 * 4 *
5 * FeedItem class - Used as feed element in FeedWriter class 5 * FeedItem class - Used as feed element in FeedWriter class
6 * 6 *
7 * @package UnivarselFeedWriter 7 * @package UnivarselFeedWriter
@@ -10,176 +10,195 @@
10 */ 10 */
11 class FeedItem 11 class FeedItem
12 { 12 {
13 private $elements = array(); //Collection of feed elements 13 private $elements = array(); //Collection of feed elements
14 private $version; 14 private $version;
15 15
16 /** 16 /**
17 * Constructor 17 * Constructor
18 * 18 *
19 * @param contant (RSS1/RSS2/ATOM) RSS2 is default. 19 * @param contant (RSS1/RSS2/ATOM) RSS2 is default.
20 */ 20 */
21 function __construct($version = RSS2) 21 function __construct($version = RSS2)
22 { 22 {
23 $this->version = $version; 23 $this->version = $version;
24 } 24 }
25 25
26 /** 26 /**
27 * Set element (overwrites existing elements with $elementName) 27 * Set element (overwrites existing elements with $elementName)
28 * 28 *
29 * @access public 29 * @access public
30 * @param srting The tag name of an element 30 * @param srting The tag name of an element
31 * @param srting The content of tag 31 * @param srting The content of tag
32 * @param array Attributes(if any) in 'attrName' => 'attrValue' format 32 * @param array Attributes(if any) in 'attrName' => 'attrValue' format
33 * @return void 33 * @return void
34 */ 34 */
35 public function setElement($elementName, $content, $attributes = null) 35 public function setElement($elementName, $content, $attributes = null)
36 { 36 {
37 if (isset($this->elements[$elementName])) { 37 if (isset($this->elements[$elementName])) {
38 unset($this->elements[$elementName]); 38 unset($this->elements[$elementName]);
39 } 39 }
40 $this->addElement($elementName, $content, $attributes); 40 $this->addElement($elementName, $content, $attributes);
41 } 41 }
42 42
43 /** 43 /**
44 * Add an element to elements array 44 * Add an element to elements array
45 * 45 *
46 * @access public 46 * @access public
47 * @param srting The tag name of an element 47 * @param srting The tag name of an element
48 * @param srting The content of tag 48 * @param srting The content of tag
49 * @param array Attributes(if any) in 'attrName' => 'attrValue' format 49 * @param array Attributes(if any) in 'attrName' => 'attrValue' format
50 * @return void 50 * @return void
51 */ 51 */
52 public function addElement($elementName, $content, $attributes = null) 52 public function addElement($elementName, $content, $attributes = null)
53 { 53 {
54 $i = 0; 54 $i = 0;
55 if (isset($this->elements[$elementName])) { 55 if (isset($this->elements[$elementName])) {
56 $i = count($this->elements[$elementName]); 56 $i = count($this->elements[$elementName]);
57 } else { 57 } else {
58 $this->elements[$elementName] = array(); 58 $this->elements[$elementName] = array();
59 } 59 }
60 $this->elements[$elementName][$i]['name'] = $elementName; 60 $this->elements[$elementName][$i]['name'] = $elementName;
61 $this->elements[$elementName][$i]['content'] = $content; 61 $this->elements[$elementName][$i]['content'] = $content;
62 $this->elements[$elementName][$i]['attributes'] = $attributes; 62 $this->elements[$elementName][$i]['attributes'] = $attributes;
63 } 63 }
64 64
65 /** 65 /**
66 * Set multiple feed elements from an array. 66 * Set multiple feed elements from an array.
67 * Elements which have attributes cannot be added by this method 67 * Elements which have attributes cannot be added by this method
68 * 68 *
69 * @access public 69 * @access public
70 * @param array array of elements in 'tagName' => 'tagContent' format. 70 * @param array array of elements in 'tagName' => 'tagContent' format.
71 * @return void 71 * @return void
72 */ 72 */
73 public function addElementArray($elementArray) 73 public function addElementArray($elementArray)
74 { 74 {
75 if(! is_array($elementArray)) return; 75 if(! is_array($elementArray)) return;
76 foreach ($elementArray as $elementName => $content) 76 foreach ($elementArray as $elementName => $content)
77 { 77 {
78 $this->addElement($elementName, $content); 78 $this->addElement($elementName, $content);
79 } 79 }
80 } 80 }
81 81
82 /** 82 /**
83 * Return the collection of elements in this feed item 83 * Return the collection of elements in this feed item
84 * 84 *
85 * @access public 85 * @access public
86 * @return array 86 * @return array
87 */ 87 */
88 public function getElements() 88 public function getElements()
89 { 89 {
90 return $this->elements; 90 return $this->elements;
91 } 91 }
92 92
93 // Wrapper functions ------------------------------------------------------ 93 // Wrapper functions ------------------------------------------------------
94 94
95 /** 95 /**
96 * Set the 'dscription' element of feed item 96 * Set the 'dscription' element of feed item
97 * 97 *
98 * @access public 98 * @access public
99 * @param string The content of 'description' element 99 * @param string The content of 'description' element
100 * @return void 100 * @return void
101 */ 101 */
102 public function setDescription($description) 102 public function setDescription($description)
103 { 103 {
104 $tag = 'description'; 104 $tag = ($this->version == ATOM)? 'summary' : 'description';
105 $this->setElement($tag, $description); 105 $this->setElement($tag, $description);
106 } 106 }
107 107
108 /** 108 /**
109 * @desc Set the 'title' element of feed item 109 * @desc Set the 'title' element of feed item
110 * @access public 110 * @access public
111 * @param string The content of 'title' element 111 * @param string The content of 'title' element
112 * @return void 112 * @return void
113 */ 113 */
114 public function setTitle($title) 114 public function setTitle($title)
115 { 115 {
116 $this->setElement('title', $title); 116 $this->setElement('title', $title);
117 } 117 }
118 118
119 /** 119 /**
120 * Set the 'date' element of feed item 120 * Set the 'date' element of feed item
121 * 121 *
122 * @access public 122 * @access public
123 * @param string The content of 'date' element 123 * @param string The content of 'date' element
124 * @return void 124 * @return void
125 */ 125 */
126 public function setDate($date) 126 public function setDate($date)
127 { 127 {
128 if(! is_numeric($date)) 128 if(! is_numeric($date))
129 { 129 {
130 $date = strtotime($date); 130 $date = strtotime($date);
131 } 131 }
132 132
133 if($this->version == RSS2) 133 if($this->version == ATOM)
134 { 134 {
135 $tag = 'pubDate'; 135 $tag = 'updated';
136 $value = date(DATE_RSS, $date); 136 $value = date(DATE_ATOM, $date);
137 } 137 }
138 else 138 elseif($this->version == RSS2)
139 { 139 {
140 $tag = 'dc:date'; 140 $tag = 'pubDate';
141 $value = date("Y-m-d", $date); 141 $value = date(DATE_RSS, $date);
142 } 142 }
143 143 else
144 $this->setElement($tag, $value); 144 {
145 } 145 $tag = 'dc:date';
146 146 $value = date("Y-m-d", $date);
147 /** 147 }
148 * Set the 'link' element of feed item 148
149 * 149 $this->setElement($tag, $value);
150 * @access public 150 }
151 * @param string The content of 'link' element 151
152 * @return void 152 /**
153 */ 153 * Set the 'link' element of feed item
154 public function setLink($link) 154 *
155 { 155 * @access public
156 if($this->version == RSS2 || $this->version == RSS1) 156 * @param string The content of 'link' element
157 { 157 * @return void
158 $this->setElement('link', $link); 158 */
159 } 159 public function setLink($link)
160 else 160 {
161 { 161 if($this->version == RSS2 || $this->version == RSS1)
162 $this->setElement('link','',array('href'=>$link)); 162 {
163 $this->setElement('id', FeedWriter::uuid($link,'urn:uuid:')); 163 $this->setElement('link', $link);
164 } 164 $this->setElement('guid', $link);
165 165 }
166 } 166 else
167 167 {
168 /** 168 $this->setElement('link','',array('href'=>$link));
169 * Set the 'encloser' element of feed item 169 $this->setElement('id', FeedWriter::uuid($link,'urn:uuid:'));
170 * For RSS 2.0 only 170 }
171 * 171
172 * @access public 172 }
173 * @param string The url attribute of encloser tag 173
174 * @param string The length attribute of encloser tag 174 /**
175 * @param string The type attribute of encloser tag 175 * Set the 'source' element of feed item
176 * @return void 176 *
177 */ 177 * @access public
178 public function setEncloser($url, $length, $type) 178 * @param string The content of 'source' element
179 { 179 * @return void
180 $attributes = array('url'=>$url, 'length'=>$length, 'type'=>$type); 180 */
181 $this->setElement('enclosure','',$attributes); 181 public function setSource($link)
182 } 182 {
183 183 $attributes = array('url'=>$link);
184 $this->setElement('source', "wallabag",$attributes);
185 }
186
187 /**
188 * Set the 'encloser' element of feed item
189 * For RSS 2.0 only
190 *
191 * @access public
192 * @param string The url attribute of encloser tag
193 * @param string The length attribute of encloser tag
194 * @param string The type attribute of encloser tag
195 * @return void
196 */
197 public function setEncloser($url, $length, $type)
198 {
199 $attributes = array('url'=>$url, 'length'=>$length, 'type'=>$type);
200 $this->setElement('enclosure','',$attributes);
201 }
202
184 } // end of class FeedItem 203 } // end of class FeedItem
185?> 204?> \ No newline at end of file
diff --git a/inc/3rdparty/libraries/feedwriter/FeedWriter.php b/inc/3rdparty/libraries/feedwriter/FeedWriter.php
index adb2526c..9446cddf 100644..100755
--- a/inc/3rdparty/libraries/feedwriter/FeedWriter.php
+++ b/inc/3rdparty/libraries/feedwriter/FeedWriter.php
@@ -2,6 +2,7 @@
2define('RSS2', 1, true); 2define('RSS2', 1, true);
3define('JSON', 2, true); 3define('JSON', 2, true);
4define('JSONP', 3, true); 4define('JSONP', 3, true);
5define('ATOM', 4, true);
5 6
6 /** 7 /**
7 * Univarsel Feed Writer class 8 * Univarsel Feed Writer class
@@ -9,433 +10,444 @@ define('JSONP', 3, true);
9 * Genarate RSS2 or JSON (original: RSS 1.0, RSS2.0 and ATOM Feed) 10 * Genarate RSS2 or JSON (original: RSS 1.0, RSS2.0 and ATOM Feed)
10 * 11 *
11 * Modified for FiveFilters.org's Full-Text RSS project 12 * Modified for FiveFilters.org's Full-Text RSS project
12 * to allow for inclusion of hubs, JSON output. 13 * to allow for inclusion of hubs, JSON output.
13 * Stripped RSS1 and ATOM support. 14 * Stripped RSS1 and ATOM support.
14 * 15 *
15 * @package UnivarselFeedWriter 16 * @package UnivarselFeedWriter
16 * @author Anis uddin Ahmad <anisniit@gmail.com> 17 * @author Anis uddin Ahmad <anisniit@gmail.com>
17 * @link http://www.ajaxray.com/projects/rss 18 * @link http://www.ajaxray.com/projects/rss
18 */ 19 */
19 class FeedWriter 20 class FeedWriter
20 { 21 {
21 private $self = null; // self URL - http://feed2.w3.org/docs/warning/MissingAtomSelfLink.html 22 private $self = null; // self URL - http://feed2.w3.org/docs/warning/MissingAtomSelfLink.html
22 private $hubs = array(); // PubSubHubbub hubs 23 private $hubs = array(); // PubSubHubbub hubs
23 private $channels = array(); // Collection of channel elements 24 private $channels = array(); // Collection of channel elements
24 private $items = array(); // Collection of items as object of FeedItem class. 25 private $items = array(); // Collection of items as object of FeedItem class.
25 private $data = array(); // Store some other version wise data 26 private $data = array(); // Store some other version wise data
26 private $CDATAEncoding = array(); // The tag names which have to encoded as CDATA 27 private $CDATAEncoding = array(); // The tag names which have to encoded as CDATA
27 private $xsl = null; // stylesheet to render RSS (used by Chrome) 28 private $xsl = null; // stylesheet to render RSS (used by Chrome)
28 private $json = null; // JSON object 29 private $json = null; // JSON object
29 30
30 private $version = null; 31 private $version = null;
31 32
32 /** 33 /**
33 * Constructor 34 * Constructor
34 * 35 *
35 * @param constant the version constant (RSS2 or JSON). 36 * @param constant the version constant (RSS2 or JSON).
36 */ 37 */
37 function __construct($version = RSS2) 38 function __construct($version = RSS2)
38 { 39 {
39 $this->version = $version; 40 $this->version = $version;
40 41
41 // Setting default value for assential channel elements 42 // Setting default value for assential channel elements
42 $this->channels['title'] = $version . ' Feed'; 43 $this->channels['title'] = $version . ' Feed';
43 $this->channels['link'] = 'http://www.ajaxray.com/blog'; 44 $this->channels['link'] = 'http://www.ajaxray.com/blog';
44 45
45 //Tag names to encode in CDATA 46 //Tag names to encode in CDATA
46 $this->CDATAEncoding = array('description', 'content:encoded', 'content', 'subtitle', 'summary'); 47 $this->CDATAEncoding = array('description', 'content:encoded', 'content', 'subtitle', 'summary');
47 } 48 }
48 49
49 public function setFormat($format) { 50 public function setFormat($format) {
50 $this->version = $format; 51 $this->version = $format;
51 } 52 }
52 53
53 // Start # public functions --------------------------------------------- 54 // Start # public functions ---------------------------------------------
54 55
55 /** 56 /**
56 * Set a channel element 57 * Set a channel element
57 * @access public 58 * @access public
58 * @param srting name of the channel tag 59 * @param srting name of the channel tag
59 * @param string content of the channel tag 60 * @param string content of the channel tag
60 * @return void 61 * @return void
61 */ 62 */
62 public function setChannelElement($elementName, $content) 63 public function setChannelElement($elementName, $content)
63 { 64 {
64 $this->channels[$elementName] = $content ; 65 $this->channels[$elementName] = $content ;
65 } 66 }
66 67
67 /** 68 /**
68 * Set multiple channel elements from an array. Array elements 69 * Set multiple channel elements from an array. Array elements
69 * should be 'channelName' => 'channelContent' format. 70 * should be 'channelName' => 'channelContent' format.
70 * 71 *
71 * @access public 72 * @access public
72 * @param array array of channels 73 * @param array array of channels
73 * @return void 74 * @return void
74 */ 75 */
75 public function setChannelElementsFromArray($elementArray) 76 public function setChannelElementsFromArray($elementArray)
76 { 77 {
77 if(! is_array($elementArray)) return; 78 if(! is_array($elementArray)) return;
78 foreach ($elementArray as $elementName => $content) 79 foreach ($elementArray as $elementName => $content)
79 { 80 {
80 $this->setChannelElement($elementName, $content); 81 $this->setChannelElement($elementName, $content);
81 } 82 }
82 } 83 }
83 84
84 /** 85 /**
85 * Genarate the actual RSS/JSON file 86 * Genarate the actual RSS/JSON file
86 * 87 *
87 * @access public 88 * @access public
88 * @return void 89 * @return void
89 */ 90 */
90 public function genarateFeed() 91 public function genarateFeed($withHeaders = true)
91 { 92 {
92 if ($this->version == RSS2) { 93 if ($withHeaders) {
93 header('Content-type: text/xml; charset=UTF-8'); 94 if ($this->version == RSS2) {
94 // this line prevents Chrome 20 from prompting download 95 header('Content-type: text/xml; charset=UTF-8');
95 // used by Google: https://news.google.com/news/feeds?ned=us&topic=b&output=rss 96 // this line prevents Chrome 20 from prompting download
96 header('X-content-type-options: nosniff'); 97 // used by Google: https://news.google.com/news/feeds?ned=us&topic=b&output=rss
97 } elseif ($this->version == JSON) { 98 header('X-content-type-options: nosniff');
98 header('Content-type: application/json; charset=UTF-8'); 99 } elseif ($this->version == JSON) {
99 $this->json = new stdClass(); 100 header('Content-type: application/json; charset=UTF-8');
100 } elseif ($this->version == JSONP) { 101 } elseif ($this->version == JSONP) {
101 header('Content-type: application/javascript; charset=UTF-8'); 102 header('Content-type: application/javascript; charset=UTF-8');
102 $this->json = new stdClass(); 103 }
103 } 104 }
104 $this->printHead(); 105
105 $this->printChannels(); 106 if ($this->version == JSON || $this->version == JSONP) {
106 $this->printItems(); 107 $this->json = new stdClass();
107 $this->printTale(); 108 }
108 if ($this->version == JSON || $this->version == JSONP) { 109
109 echo json_encode($this->json); 110
110 } 111 $this->printHead();
111 } 112 $this->printChannels();
112 113 $this->printItems();
113 /** 114 $this->printTale();
114 * Create a new FeedItem. 115 if ($this->version == JSON || $this->version == JSONP) {
115 * 116 echo json_encode($this->json);
116 * @access public 117 }
117 * @return object instance of FeedItem class 118 }
118 */ 119
119 public function createNewItem() 120 public function &getItems()
120 { 121 {
121 $Item = new FeedItem($this->version); 122 return $this->items;
122 return $Item; 123 }
123 } 124
124 125 /**
125 /** 126 * Create a new FeedItem.
126 * Add a FeedItem to the main class 127 *
127 * 128 * @access public
128 * @access public 129 * @return object instance of FeedItem class
129 * @param object instance of FeedItem class 130 */
130 * @return void 131 public function createNewItem()
131 */ 132 {
132 public function addItem($feedItem) 133 $Item = new FeedItem($this->version);
133 { 134 return $Item;
134 $this->items[] = $feedItem; 135 }
135 } 136
136 137 /**
137 // Wrapper functions ------------------------------------------------------------------- 138 * Add a FeedItem to the main class
138 139 *
139 /** 140 * @access public
140 * Set the 'title' channel element 141 * @param object instance of FeedItem class
141 * 142 * @return void
142 * @access public 143 */
143 * @param srting value of 'title' channel tag 144 public function addItem($feedItem)
144 * @return void 145 {
145 */ 146 $this->items[] = $feedItem;
146 public function setTitle($title) 147 }
147 { 148
148 $this->setChannelElement('title', $title); 149 // Wrapper functions -------------------------------------------------------------------
149 } 150
150 151 /**
151 /** 152 * Set the 'title' channel element
152 * Add a hub to the channel element 153 *
153 * 154 * @access public
154 * @access public 155 * @param srting value of 'title' channel tag
155 * @param string URL 156 * @return void
156 * @return void 157 */
157 */ 158 public function setTitle($title)
158 public function addHub($hub) 159 {
159 { 160 $this->setChannelElement('title', $title);
160 $this->hubs[] = $hub; 161 }
161 } 162
162 163 /**
163 /** 164 * Add a hub to the channel element
164 * Set XSL URL 165 *
165 * 166 * @access public
166 * @access public 167 * @param string URL
167 * @param string URL 168 * @return void
168 * @return void 169 */
169 */ 170 public function addHub($hub)
170 public function setXsl($xsl) 171 {
171 { 172 $this->hubs[] = $hub;
172 $this->xsl = $xsl; 173 }
173 } 174
174 175 /**
175 /** 176 * Set XSL URL
176 * Set self URL 177 *
177 * 178 * @access public
178 * @access public 179 * @param string URL
179 * @param string URL 180 * @return void
180 * @return void 181 */
181 */ 182 public function setXsl($xsl)
182 public function setSelf($self) 183 {
183 { 184 $this->xsl = $xsl;
184 $this->self = $self; 185 }
185 } 186
186 187 /**
187 /** 188 * Set self URL
188 * Set the 'description' channel element 189 *
189 * 190 * @access public
190 * @access public 191 * @param string URL
191 * @param srting value of 'description' channel tag 192 * @return void
192 * @return void 193 */
193 */ 194 public function setSelf($self)
194 public function setDescription($desciption) 195 {
195 { 196 $this->self = $self;
196 $tag = ($this->version == ATOM)? 'subtitle' : 'description'; 197 }
197 $this->setChannelElement($tag, $desciption); 198
198 } 199 /**
199 200 * Set the 'description' channel element
200 /** 201 *
201 * Set the 'link' channel element 202 * @access public
202 * 203 * @param srting value of 'description' channel tag
203 * @access public 204 * @return void
204 * @param srting value of 'link' channel tag 205 */
205 * @return void 206 public function setDescription($description)
206 */ 207 {
207 public function setLink($link) 208 $tag = ($this->version == ATOM)? 'subtitle' : 'description';
208 { 209 $this->setChannelElement($tag, $description);
209 $this->setChannelElement('link', $link); 210 }
210 } 211
211 212 /**
212 /** 213 * Set the 'link' channel element
213 * Set the 'image' channel element 214 *
214 * 215 * @access public
215 * @access public 216 * @param srting value of 'link' channel tag
216 * @param srting title of image 217 * @return void
217 * @param srting link url of the imahe 218 */
218 * @param srting path url of the image 219 public function setLink($link)
219 * @return void 220 {
220 */ 221 $this->setChannelElement('link', $link);
221 public function setImage($title, $link, $url) 222 }
222 { 223
223 $this->setChannelElement('image', array('title'=>$title, 'link'=>$link, 'url'=>$url)); 224 /**
224 } 225 * Set the 'image' channel element
225 226 *
226 // End # public functions ---------------------------------------------- 227 * @access public
227 228 * @param srting title of image
228 // Start # private functions ---------------------------------------------- 229 * @param srting link url of the imahe
229 230 * @param srting path url of the image
230 /** 231 * @return void
231 * Prints the xml and rss namespace 232 */
232 * 233 public function setImage($title, $link, $url)
233 * @access private 234 {
234 * @return void 235 $this->setChannelElement('image', array('title'=>$title, 'link'=>$link, 'url'=>$url));
235 */ 236 }
236 private function printHead() 237
237 { 238 // End # public functions ----------------------------------------------
238 if ($this->version == RSS2) 239
239 { 240 // Start # private functions ----------------------------------------------
240 $out = '<?xml version="1.0" encoding="utf-8"?>'."\n"; 241
241 if ($this->xsl) $out .= '<?xml-stylesheet type="text/xsl" href="'.htmlspecialchars($this->xsl).'"?>' . PHP_EOL; 242 /**
242 $out .= '<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">' . PHP_EOL; 243 * Prints the xml and rss namespace
243 echo $out; 244 *
244 } 245 * @access private
245 elseif ($this->version == JSON || $this->version == JSONP) 246 * @return void
246 { 247 */
247 $this->json->rss = array('@attributes' => array('version' => '2.0')); 248 private function printHead()
248 } 249 {
249 } 250 if ($this->version == RSS2)
250 251 {
251 /** 252 $out = '<?xml version="1.0" encoding="utf-8"?>'."\n";
252 * Closes the open tags at the end of file 253 if ($this->xsl) $out .= '<?xml-stylesheet type="text/xsl" href="'.htmlspecialchars($this->xsl).'"?>' . PHP_EOL;
253 * 254 $out .= '<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">' . PHP_EOL;
254 * @access private 255 echo $out;
255 * @return void 256 }
256 */ 257 elseif ($this->version == JSON || $this->version == JSONP)
257 private function printTale() 258 {
258 { 259 $this->json->rss = array('@attributes' => array('version' => '2.0'));
259 if ($this->version == RSS2) 260 }
260 { 261 }
261 echo '</channel>',PHP_EOL,'</rss>'; 262
262 } 263 /**
263 // do nothing for JSON 264 * Closes the open tags at the end of file
264 } 265 *
265 266 * @access private
266 /** 267 * @return void
267 * Creates a single node as xml format 268 */
268 * 269 private function printTale()
269 * @access private 270 {
270 * @param string name of the tag 271 if ($this->version == RSS2)
271 * @param mixed tag value as string or array of nested tags in 'tagName' => 'tagValue' format 272 {
272 * @param array Attributes(if any) in 'attrName' => 'attrValue' format 273 echo '</channel>',PHP_EOL,'</rss>';
273 * @return string formatted xml tag 274 }
274 */ 275 // do nothing for JSON
275 private function makeNode($tagName, $tagContent, $attributes = null) 276 }
276 { 277
277 if ($this->version == RSS2) 278 /**
278 { 279 * Creates a single node as xml format
279 $nodeText = ''; 280 *
280 $attrText = ''; 281 * @access private
281 if (is_array($attributes)) 282 * @param string name of the tag
282 { 283 * @param mixed tag value as string or array of nested tags in 'tagName' => 'tagValue' format
283 foreach ($attributes as $key => $value) 284 * @param array Attributes(if any) in 'attrName' => 'attrValue' format
284 { 285 * @return string formatted xml tag
285 $attrText .= " $key=\"$value\" "; 286 */
286 } 287 private function makeNode($tagName, $tagContent, $attributes = null)
287 } 288 {
288 $nodeText .= "<{$tagName}{$attrText}>"; 289 if ($this->version == RSS2)
289 if (is_array($tagContent)) 290 {
290 { 291 $nodeText = '';
291 foreach ($tagContent as $key => $value) 292 $attrText = '';
292 { 293 if (is_array($attributes))
293 $nodeText .= $this->makeNode($key, $value); 294 {
294 } 295 foreach ($attributes as $key => $value)
295 } 296 {
296 else 297 $attrText .= " $key=\"$value\" ";
297 { 298 }
298 //$nodeText .= (in_array($tagName, $this->CDATAEncoding))? $tagContent : htmlentities($tagContent); 299 }
299 $nodeText .= htmlspecialchars($tagContent); 300 $nodeText .= "<{$tagName}{$attrText}>";
300 } 301 if (is_array($tagContent))
301 //$nodeText .= (in_array($tagName, $this->CDATAEncoding))? "]]></$tagName>" : "</$tagName>"; 302 {
302 $nodeText .= "</$tagName>"; 303 foreach ($tagContent as $key => $value)
303 return $nodeText . PHP_EOL; 304 {
304 } 305 $nodeText .= $this->makeNode($key, $value);
305 elseif ($this->version == JSON || $this->version == JSONP) 306 }
306 { 307 }
307 $tagName = (string)$tagName; 308 else
308 $tagName = strtr($tagName, ':', '_'); 309 {
309 $node = null; 310 //$nodeText .= (in_array($tagName, $this->CDATAEncoding))? $tagContent : htmlentities($tagContent);
310 if (!$tagContent && is_array($attributes) && count($attributes)) 311 $nodeText .= htmlspecialchars($tagContent);
311 { 312 }
312 $node = array('@attributes' => $this->json_keys($attributes)); 313 //$nodeText .= (in_array($tagName, $this->CDATAEncoding))? "]]></$tagName>" : "</$tagName>";
313 } else { 314 $nodeText .= "</$tagName>";
314 if (is_array($tagContent)) { 315 return $nodeText . PHP_EOL;
315 $node = $this->json_keys($tagContent); 316 }
316 } else { 317 elseif ($this->version == JSON || $this->version == JSONP)
317 $node = $tagContent; 318 {
318 } 319 $tagName = (string)$tagName;
319 } 320 $tagName = strtr($tagName, ':', '_');
320 return $node; 321 $node = null;
321 } 322 if (!$tagContent && is_array($attributes) && count($attributes))
322 return ''; // should not get here 323 {
323 } 324 $node = array('@attributes' => $this->json_keys($attributes));
324 325 } else {
325 private function json_keys(array $array) { 326 if (is_array($tagContent)) {
326 $new = array(); 327 $node = $this->json_keys($tagContent);
327 foreach ($array as $key => $val) { 328 } else {
328 if (is_string($key)) $key = strtr($key, ':', '_'); 329 $node = $tagContent;
329 if (is_array($val)) { 330 }
330 $new[$key] = $this->json_keys($val); 331 }
331 } else { 332 return $node;
332 $new[$key] = $val; 333 }
333 } 334 return ''; // should not get here
334 } 335 }
335 return $new; 336
336 } 337 private function json_keys(array $array) {
337 338 $new = array();
338 /** 339 foreach ($array as $key => $val) {
339 * @desc Print channels 340 if (is_string($key)) $key = strtr($key, ':', '_');
340 * @access private 341 if (is_array($val)) {
341 * @return void 342 $new[$key] = $this->json_keys($val);
342 */ 343 } else {
343 private function printChannels() 344 $new[$key] = $val;
344 { 345 }
345 //Start channel tag 346 }
346 if ($this->version == RSS2) { 347 return $new;
347 echo '<channel>' . PHP_EOL; 348 }
348 // add hubs 349
349 foreach ($this->hubs as $hub) { 350 /**
350 //echo $this->makeNode('link', '', array('rel'=>'hub', 'href'=>$hub, 'xmlns'=>'http://www.w3.org/2005/Atom')); 351 * @desc Print channels
351 echo '<link rel="hub" href="'.htmlspecialchars($hub).'" xmlns="http://www.w3.org/2005/Atom" />' . PHP_EOL; 352 * @access private
352 } 353 * @return void
353 // add self 354 */
354 if (isset($this->self)) { 355 private function printChannels()
355 //echo $this->makeNode('link', '', array('rel'=>'self', 'href'=>$this->self, 'xmlns'=>'http://www.w3.org/2005/Atom')); 356 {
356 echo '<link rel="self" href="'.htmlspecialchars($this->self).'" xmlns="http://www.w3.org/2005/Atom" />' . PHP_EOL; 357 //Start channel tag
357 } 358 if ($this->version == RSS2) {
358 //Print Items of channel 359 echo '<channel>' . PHP_EOL;
359 foreach ($this->channels as $key => $value) 360 // add hubs
360 { 361 foreach ($this->hubs as $hub) {
361 echo $this->makeNode($key, $value); 362 //echo $this->makeNode('link', '', array('rel'=>'hub', 'href'=>$hub, 'xmlns'=>'http://www.w3.org/2005/Atom'));
362 } 363 echo '<link rel="hub" href="'.htmlspecialchars($hub).'" xmlns="http://www.w3.org/2005/Atom" />' . PHP_EOL;
363 } elseif ($this->version == JSON || $this->version == JSONP) { 364 }
364 $this->json->rss['channel'] = (object)$this->json_keys($this->channels); 365 // add self
365 } 366 if (isset($this->self)) {
366 } 367 //echo $this->makeNode('link', '', array('rel'=>'self', 'href'=>$this->self, 'xmlns'=>'http://www.w3.org/2005/Atom'));
367 368 echo '<link rel="self" href="'.htmlspecialchars($this->self).'" xmlns="http://www.w3.org/2005/Atom" />' . PHP_EOL;
368 /** 369 }
369 * Prints formatted feed items 370 //Print Items of channel
370 * 371 foreach ($this->channels as $key => $value)
371 * @access private 372 {
372 * @return void 373 echo $this->makeNode($key, $value);
373 */ 374 }
374 private function printItems() 375 } elseif ($this->version == JSON || $this->version == JSONP) {
375 { 376 $this->json->rss['channel'] = (object)$this->json_keys($this->channels);
376 foreach ($this->items as $item) { 377 }
377 $itemElements = $item->getElements(); 378 }
378 379
379 echo $this->startItem(); 380 /**
380 381 * Prints formatted feed items
381 if ($this->version == JSON || $this->version == JSONP) { 382 *
382 $json_item = array(); 383 * @access private
383 } 384 * @return void
384 385 */
385 foreach ($itemElements as $thisElement) { 386 private function printItems()
386 foreach ($thisElement as $instance) { 387 {
387 if ($this->version == RSS2) { 388 foreach ($this->items as $item) {
388 echo $this->makeNode($instance['name'], $instance['content'], $instance['attributes']); 389 $itemElements = $item->getElements();
389 } elseif ($this->version == JSON || $this->version == JSONP) { 390
390 $_json_node = $this->makeNode($instance['name'], $instance['content'], $instance['attributes']); 391 echo $this->startItem();
391 if (count($thisElement) > 1) { 392
392 $json_item[strtr($instance['name'], ':', '_')][] = $_json_node; 393 if ($this->version == JSON || $this->version == JSONP) {
393 } else { 394 $json_item = array();
394 $json_item[strtr($instance['name'], ':', '_')] = $_json_node; 395 }
395 } 396
396 } 397 foreach ($itemElements as $thisElement) {
397 } 398 foreach ($thisElement as $instance) {
398 } 399 if ($this->version == RSS2) {
399 echo $this->endItem(); 400 echo $this->makeNode($instance['name'], $instance['content'], $instance['attributes']);
400 if ($this->version == JSON || $this->version == JSONP) { 401 } elseif ($this->version == JSON || $this->version == JSONP) {
401 if (count($this->items) > 1) { 402 $_json_node = $this->makeNode($instance['name'], $instance['content'], $instance['attributes']);
402 $this->json->rss['channel']->item[] = $json_item; 403 if (count($thisElement) > 1) {
403 } else { 404 $json_item[strtr($instance['name'], ':', '_')][] = $_json_node;
404 $this->json->rss['channel']->item = $json_item; 405 } else {
405 } 406 $json_item[strtr($instance['name'], ':', '_')] = $_json_node;
406 } 407 }
407 } 408 }
408 } 409 }
409 410 }
410 /** 411 echo $this->endItem();
411 * Make the starting tag of channels 412 if ($this->version == JSON || $this->version == JSONP) {
412 * 413 if (count($this->items) > 1) {
413 * @access private 414 $this->json->rss['channel']->item[] = $json_item;
414 * @return void 415 } else {
415 */ 416 $this->json->rss['channel']->item = $json_item;
416 private function startItem() 417 }
417 { 418 }
418 if ($this->version == RSS2) 419 }
419 { 420 }
420 echo '<item>' . PHP_EOL; 421
421 } 422 /**
422 // nothing for JSON 423 * Make the starting tag of channels
423 } 424 *
424 425 * @access private
425 /** 426 * @return void
426 * Closes feed item tag 427 */
427 * 428 private function startItem()
428 * @access private 429 {
429 * @return void 430 if ($this->version == RSS2)
430 */ 431 {
431 private function endItem() 432 echo '<item>' . PHP_EOL;
432 { 433 }
433 if ($this->version == RSS2) 434 // nothing for JSON
434 { 435 }
435 echo '</item>' . PHP_EOL; 436
436 } 437 /**
437 // nothing for JSON 438 * Closes feed item tag
438 } 439 *
439 440 * @access private
440 // End # private functions ---------------------------------------------- 441 * @return void
442 */
443 private function endItem()
444 {
445 if ($this->version == RSS2)
446 {
447 echo '</item>' . PHP_EOL;
448 }
449 // nothing for JSON
450 }
451
452 // End # private functions ----------------------------------------------
441 } \ No newline at end of file 453 } \ No newline at end of file
diff --git a/inc/3rdparty/libraries/html5/TreeBuilder.php b/inc/3rdparty/libraries/html5/TreeBuilder.php
index 2f5244f9..c4a48b21 100644
--- a/inc/3rdparty/libraries/html5/TreeBuilder.php
+++ b/inc/3rdparty/libraries/html5/TreeBuilder.php
@@ -134,6 +134,7 @@ class HTML5_TreeBuilder {
134 134
135 // Namespaces for foreign content 135 // Namespaces for foreign content
136 const NS_HTML = null; // to prevent DOM from requiring NS on everything 136 const NS_HTML = null; // to prevent DOM from requiring NS on everything
137 const NS_XHTML = 'http://www.w3.org/1999/xhtml';
137 const NS_MATHML = 'http://www.w3.org/1998/Math/MathML'; 138 const NS_MATHML = 'http://www.w3.org/1998/Math/MathML';
138 const NS_SVG = 'http://www.w3.org/2000/svg'; 139 const NS_SVG = 'http://www.w3.org/2000/svg';
139 const NS_XLINK = 'http://www.w3.org/1999/xlink'; 140 const NS_XLINK = 'http://www.w3.org/1999/xlink';
@@ -3157,11 +3158,19 @@ class HTML5_TreeBuilder {
3157 } 3158 }
3158 3159
3159 private function insertElement($token, $append = true) { 3160 private function insertElement($token, $append = true) {
3160 $el = $this->dom->createElementNS(self::NS_HTML, $token['name']); 3161 //$el = $this->dom->createElementNS(self::NS_HTML, $token['name']);
3162 $namespaceURI = strpos($token['name'], ':') ? self::NS_XHTML : self::NS_HTML;
3163 $el = $this->dom->createElementNS($namespaceURI, $token['name']);
3161 3164
3162 if (!empty($token['attr'])) { 3165 if (!empty($token['attr'])) {
3163 foreach($token['attr'] as $attr) { 3166 foreach($token['attr'] as $attr) {
3164 if(!$el->hasAttribute($attr['name'])) { 3167
3168 // mike@macgirvin.com 2011-11-17, check attribute name for
3169 // validity (ignoring extenders and combiners) as illegal chars in names
3170 // causes everything to abort
3171
3172 $valid = preg_match('/^[a-zA-Z\_\:]([\-a-zA-Z0-9\_\:\.]+$)/',$attr['name']);
3173 if($attr['name'] && (!$el->hasAttribute($attr['name'])) && ($valid)) {
3165 $el->setAttribute($attr['name'], $attr['value']); 3174 $el->setAttribute($attr['name'], $attr['value']);
3166 } 3175 }
3167 } 3176 }
diff --git a/inc/3rdparty/libraries/humble-http-agent/CookieJar.php b/inc/3rdparty/libraries/humble-http-agent/CookieJar.php
index 83e94f14..e4d5f495 100644
--- a/inc/3rdparty/libraries/humble-http-agent/CookieJar.php
+++ b/inc/3rdparty/libraries/humble-http-agent/CookieJar.php
@@ -1,404 +1,403 @@
1<?php 1<?php
2/** 2/**
3 * Cookie Jar 3 * Cookie Jar
4 * 4 *
5 * PHP class for handling cookies, as defined by the Netscape spec: 5 * PHP class for handling cookies, as defined by the Netscape spec:
6 * <http://curl.haxx.se/rfc/cookie_spec.html> 6 * <http://curl.haxx.se/rfc/cookie_spec.html>
7 * 7 *
8 * This class should be used to handle cookies (storing cookies from HTTP response messages, and 8 * This class should be used to handle cookies (storing cookies from HTTP response messages, and
9 * sending out cookies in HTTP request messages). This has been adapted for FiveFilters.org 9 * sending out cookies in HTTP request messages). This has been adapted for FiveFilters.org
10 * from the original version used in HTTP Navigator. See http://www.keyvan.net/code/http-navigator/ 10 * from the original version used in HTTP Navigator. See http://www.keyvan.net/code/http-navigator/
11 * 11 *
12 * This class is mainly based on Cookies.pm <http://search.cpan.org/author/GAAS/libwww-perl-5.65/ 12 * This class is mainly based on Cookies.pm <http://search.cpan.org/author/GAAS/libwww-perl-5.65/
13 * lib/HTTP/Cookies.pm> from the libwww-perl collection <http://www.linpro.no/lwp/>. 13 * lib/HTTP/Cookies.pm> from the libwww-perl collection <http://www.linpro.no/lwp/>.
14 * Unlike Cookies.pm, this class only supports the Netscape cookie spec, not RFC 2965. 14 * Unlike Cookies.pm, this class only supports the Netscape cookie spec, not RFC 2965.
15 * 15 *
16 * @version 0.5 16 * @version 0.5
17 * @date 2011-03-15 17 * @date 2011-03-15
18 * @see http://php.net/HttpRequestPool 18 * @see http://php.net/HttpRequestPool
19 * @author Keyvan Minoukadeh 19 * @author Keyvan Minoukadeh
20 * @copyright 2011 Keyvan Minoukadeh 20 * @copyright 2011 Keyvan Minoukadeh
21 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3 21 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3
22 */ 22 */
23 23
24class CookieJar 24class CookieJar
25{ 25{
26 /** 26 /**
27 * Cookies - array containing all cookies. 27 * Cookies - array containing all cookies.
28 * 28 *
29 * <pre> 29 * <pre>
30 * Cookies are stored like this: 30 * Cookies are stored like this:
31 * [domain][path][name] = array 31 * [domain][path][name] = array
32 * where array is: 32 * where array is:
33 * 0 => value, 1 => secure, 2 => expires 33 * 0 => value, 1 => secure, 2 => expires
34 * </pre> 34 * </pre>
35 * @var array 35 * @var array
36 * @access private 36 * @access private
37 */ 37 */
38 public $cookies = array(); 38 public $cookies = array();
39 public $debug = false; 39 public $debug = false;
40 40
41 /** 41 /**
42 * Constructor 42 * Constructor
43 */ 43 */
44 function __construct() { 44 function __construct() {
45 } 45 }
46 46
47 protected function debug($msg, $file=null, $line=null) { 47 protected function debug($msg, $file=null, $line=null) {
48 if ($this->debug) { 48 if ($this->debug) {
49 $mem = round(memory_get_usage()/1024, 2); 49 $mem = round(memory_get_usage()/1024, 2);
50 $memPeak = round(memory_get_peak_usage()/1024, 2); 50 $memPeak = round(memory_get_peak_usage()/1024, 2);
51 echo '* ',$msg; 51 echo '* ',$msg;
52 if (isset($file, $line)) echo " ($file line $line)"; 52 if (isset($file, $line)) echo " ($file line $line)";
53 echo ' - mem used: ',$mem," (peak: $memPeak)\n"; 53 echo ' - mem used: ',$mem," (peak: $memPeak)\n";
54 ob_flush(); 54 ob_flush();
55 flush(); 55 flush();
56 } 56 }
57 } 57 }
58 58
59 /** 59 /**
60 * Get matching cookies 60 * Get matching cookies
61 * 61 *
62 * Only use this method if you cannot use add_cookie_header(), for example, if you want to use 62 * Only use this method if you cannot use add_cookie_header(), for example, if you want to use
63 * this cookie jar class without using the request class. 63 * this cookie jar class without using the request class.
64 * 64 *
65 * @param array $param associative array containing 'domain', 'path', 'secure' keys 65 * @param array $param associative array containing 'domain', 'path', 'secure' keys
66 * @return string 66 * @return string
67 * @see add_cookie_header() 67 * @see add_cookie_header()
68 */ 68 */
69 public function getMatchingCookies($url) 69 public function getMatchingCookies($url)
70 { 70 {
71 if (($parts = @parse_url($url)) && isset($parts['scheme'], $parts['host'], $parts['path'])) { 71 if (($parts = @parse_url($url)) && isset($parts['scheme'], $parts['host'], $parts['path'])) {
72 $param['domain'] = $parts['host']; 72 $param['domain'] = $parts['host'];
73 $param['path'] = $parts['path']; 73 $param['path'] = $parts['path'];
74 $param['secure'] = (strtolower($parts['scheme']) == 'https'); 74 $param['secure'] = (strtolower($parts['scheme']) == 'https');
75 unset($parts); 75 unset($parts);
76 } else { 76 } else {
77 return false; 77 return false;
78 } 78 }
79 // RFC 2965 notes: 79 // RFC 2965 notes:
80 // If multiple cookies satisfy the criteria above, they are ordered in 80 // If multiple cookies satisfy the criteria above, they are ordered in
81 // the Cookie header such that those with more specific Path attributes 81 // the Cookie header such that those with more specific Path attributes
82 // precede those with less specific. Ordering with respect to other 82 // precede those with less specific. Ordering with respect to other
83 // attributes (e.g., Domain) is unspecified. 83 // attributes (e.g., Domain) is unspecified.
84 $domain = $param['domain']; 84 $domain = $param['domain'];
85 if (strpos($domain, '.') === false) $domain .= '.local'; 85 if (strpos($domain, '.') === false) $domain .= '.local';
86 $request_path = $param['path']; 86 $request_path = $param['path'];
87 if ($request_path == '') $request_path = '/'; 87 if ($request_path == '') $request_path = '/';
88 $request_secure = $param['secure']; 88 $request_secure = $param['secure'];
89 $now = time(); 89 $now = time();
90 $matched_cookies = array(); 90 $matched_cookies = array();
91 // domain - find matching domains 91 // domain - find matching domains
92 $this->debug('Finding matching domains for '.$domain, __FILE__, __LINE__); 92 $this->debug('Finding matching domains for '.$domain, __FILE__, __LINE__);
93 while (strpos($domain, '.') !== false) { 93 while (strpos($domain, '.') !== false) {
94 if (isset($this->cookies[$domain])) { 94 if (isset($this->cookies[$domain])) {
95 $this->debug(' domain match found: '.$domain); 95 $this->debug(' domain match found: '.$domain);
96 $cookies =& $this->cookies[$domain]; 96 $cookies =& $this->cookies[$domain];
97 } else { 97 } else {
98 $domain = $this->_reduce_domain($domain); 98 $domain = $this->_reduce_domain($domain);
99 continue; 99 continue;
100 } 100 }
101 // paths - find matching paths starting from most specific 101 // paths - find matching paths starting from most specific
102 $this->debug(' - Finding matching paths for '.$request_path); 102 $this->debug(' - Finding matching paths for '.$request_path);
103 $paths = array_keys($cookies); 103 $paths = array_keys($cookies);
104 usort($paths, array($this, '_cmp_length')); 104 usort($paths, array($this, '_cmp_length'));
105 foreach ($paths as $path) { 105 foreach ($paths as $path) {
106 // continue to next cookie if request path does not path-match cookie path 106 // continue to next cookie if request path does not path-match cookie path
107 if (!$this->_path_match($request_path, $path)) continue; 107 if (!$this->_path_match($request_path, $path)) continue;
108 // loop through cookie names 108 // loop through cookie names
109 $this->debug(' path match found: '.$path); 109 $this->debug(' path match found: '.$path);
110 foreach ($cookies[$path] as $name => $values) { 110 foreach ($cookies[$path] as $name => $values) {
111 // if this cookie is secure but request isn't, continue to next cookie 111 // if this cookie is secure but request isn't, continue to next cookie
112 if ($values[1] && !$request_secure) continue; 112 if ($values[1] && !$request_secure) continue;
113 // if cookie is not a session cookie and has expired, continue to next cookie 113 // if cookie is not a session cookie and has expired, continue to next cookie
114 if (is_int($values[2]) && ($values[2] < $now)) continue; 114 if (is_int($values[2]) && ($values[2] < $now)) continue;
115 // cookie matches request 115 // cookie matches request
116 $this->debug(' cookie match: '.$name.'='.$values[0]); 116 $this->debug(' cookie match: '.$name.'='.$values[0]);
117 $matched_cookies[] = $name.'='.$values[0]; 117 $matched_cookies[] = $name.'='.$values[0];
118 } 118 }
119 } 119 }
120 $domain = $this->_reduce_domain($domain); 120 $domain = $this->_reduce_domain($domain);
121 } 121 }
122 // return cookies 122 // return cookies
123 return implode('; ', $matched_cookies); 123 return implode('; ', $matched_cookies);
124 } 124 }
125 125
126 /** 126 /**
127 * Parse Set-Cookie values. 127 * Parse Set-Cookie values.
128 * 128 *
129 * Only use this method if you cannot use extract_cookies(), for example, if you want to use 129 * Only use this method if you cannot use extract_cookies(), for example, if you want to use
130 * this cookie jar class without using the response class. 130 * this cookie jar class without using the response class.
131 * 131 *
132 * @param array $set_cookies array holding 1 or more "Set-Cookie" header values 132 * @param array $set_cookies array holding 1 or more "Set-Cookie" header values
133 * @param array $param associative array containing 'host', 'path' keys 133 * @param array $param associative array containing 'host', 'path' keys
134 * @return void 134 * @return void
135 * @see extract_cookies() 135 * @see extract_cookies()
136 */ 136 */
137 public function storeCookies($url, $set_cookies) 137 public function storeCookies($url, $set_cookies)
138 { 138 {
139 if (count($set_cookies) == 0) return; 139 if (count($set_cookies) == 0) return;
140 $param = @parse_url($url); 140 $param = @parse_url($url);
141 if (!is_array($param) || !isset($param['host'])) return; 141 if (!is_array($param) || !isset($param['host'])) return;
142 $request_host = $param['host']; 142 $request_host = $param['host'];
143 if (strpos($request_host, '.') === false) $request_host .= '.local'; 143 if (strpos($request_host, '.') === false) $request_host .= '.local';
144 $request_path = @$param['path']; 144 $request_path = @$param['path'];
145 if ($request_path == '') $request_path = '/'; 145 if ($request_path == '') $request_path = '/';
146 // 146 //
147 // loop through set-cookie headers 147 // loop through set-cookie headers
148 // 148 //
149 foreach ($set_cookies as $set_cookie) { 149 foreach ($set_cookies as $set_cookie) {
150 $this->debug('Parsing: '.$set_cookie); 150 $this->debug('Parsing: '.$set_cookie);
151 // temporary cookie store (before adding to jar) 151 // temporary cookie store (before adding to jar)
152 $tmp_cookie = array(); 152 $tmp_cookie = array();
153 $param = explode(';', $set_cookie); 153 $param = explode(';', $set_cookie);
154 // loop through params 154 // loop through params
155 for ($x=0; $x<count($param); $x++) { 155 for ($x=0; $x<count($param); $x++) {
156 $key_val = explode('=', $param[$x], 2); 156 $key_val = explode('=', $param[$x], 2);
157 if (count($key_val) != 2) { 157 if (count($key_val) != 2) {
158 // if the first param isn't a name=value pair, continue to the next set-cookie 158 // if the first param isn't a name=value pair, continue to the next set-cookie
159 // header 159 // header
160 if ($x == 0) continue 2; 160 if ($x == 0) continue 2;
161 // check for secure flag 161 // check for secure flag
162 if (strtolower(trim($key_val[0])) == 'secure') $tmp_cookie['secure'] = true; 162 if (strtolower(trim($key_val[0])) == 'secure') $tmp_cookie['secure'] = true;
163 // continue to next param 163 // continue to next param
164 continue; 164 continue;
165 } 165 }
166 list($key, $val) = array_map('trim', $key_val); 166 list($key, $val) = array_map('trim', $key_val);
167 // first name=value pair is the cookie name and value 167 // first name=value pair is the cookie name and value
168 // the name and value are stored under 'name' and 'value' to avoid conflicts 168 // the name and value are stored under 'name' and 'value' to avoid conflicts
169 // with later parameters. 169 // with later parameters.
170 if ($x == 0) { 170 if ($x == 0) {
171 $tmp_cookie = array('name'=>$key, 'value'=>$val); 171 $tmp_cookie = array('name'=>$key, 'value'=>$val);
172 continue; 172 continue;
173 } 173 }
174 $key = strtolower($key); 174 $key = strtolower($key);
175 if (in_array($key, array('expires', 'path', 'domain', 'secure'))) { 175 if (in_array($key, array('expires', 'path', 'domain', 'secure'))) {
176 $tmp_cookie[$key] = $val; 176 $tmp_cookie[$key] = $val;
177 } 177 }
178 } 178 }
179 // 179 //
180 // set cookie 180 // set cookie
181 // 181 //
182 // check domain 182 // check domain
183 if (isset($tmp_cookie['domain']) && ($tmp_cookie['domain'] != $request_host) && 183 if (isset($tmp_cookie['domain']) && ($tmp_cookie['domain'] != $request_host) &&
184 ($tmp_cookie['domain'] != ".$request_host")) { 184 ($tmp_cookie['domain'] != ".$request_host")) {
185 $domain = $tmp_cookie['domain']; 185 $domain = $tmp_cookie['domain'];
186 if ((strpos($domain, '.') === false) && ($domain != 'local')) { 186 if ((strpos($domain, '.') === false) && ($domain != 'local')) {
187 $this->debug(' - domain "'.$domain.'" has no dot and is not a local domain'); 187 $this->debug(' - domain "'.$domain.'" has no dot and is not a local domain');
188 continue; 188 continue;
189 } 189 }
190 if (preg_match('/\.[0-9]+$/', $domain)) { 190 if (preg_match('/\.[0-9]+$/', $domain)) {
191 $this->debug(' - domain "'.$domain.'" appears to be an ip address'); 191 $this->debug(' - domain "'.$domain.'" appears to be an ip address');
192 continue; 192 continue;
193 } 193 }
194 if (substr($domain, 0, 1) != '.') $domain = ".$domain"; 194 if (substr($domain, 0, 1) != '.') $domain = ".$domain";
195 if (!$this->_domain_match($request_host, $domain)) { 195 if (!$this->_domain_match($request_host, $domain)) {
196 $this->debug(' - request host "'.$request_host.'" does not domain-match "'.$domain.'"'); 196 $this->debug(' - request host "'.$request_host.'" does not domain-match "'.$domain.'"');
197 continue; 197 continue;
198 } 198 }
199 } else { 199 } else {
200 // if domain is not specified in the set-cookie header, domain will default to 200 // if domain is not specified in the set-cookie header, domain will default to
201 // the request host 201 // the request host
202 $domain = $request_host; 202 $domain = $request_host;
203 } 203 }
204 // check path 204 // check path
205 if (isset($tmp_cookie['path']) && ($tmp_cookie['path'] != '')) { 205 if (isset($tmp_cookie['path']) && ($tmp_cookie['path'] != '')) {
206 $path = urldecode($tmp_cookie['path']); 206 $path = urldecode($tmp_cookie['path']);
207 if (!$this->_path_match($request_path, $path)) { 207 if (!$this->_path_match($request_path, $path)) {
208 $this->debug(' - request path "'.$request_path.'" does not path-match "'.$path.'"'); 208 $this->debug(' - request path "'.$request_path.'" does not path-match "'.$path.'"');
209 continue; 209 continue;
210 } 210 }
211 } else { 211 } else {
212 $path = $request_path; 212 $path = $request_path;
213 $path = substr($path, 0, strrpos($path, '/')); 213 $path = substr($path, 0, strrpos($path, '/'));
214 if ($path == '') $path = '/'; 214 if ($path == '') $path = '/';
215 } 215 }
216 // check if secure 216 // check if secure
217 $secure = (isset($tmp_cookie['secure'])) ? true : false; 217 $secure = (isset($tmp_cookie['secure'])) ? true : false;
218 // check expiry 218 // check expiry
219 if (isset($tmp_cookie['expires'])) { 219 if (isset($tmp_cookie['expires'])) {
220 if (($expires = strtotime($tmp_cookie['expires'])) < 0) { 220 if (($expires = strtotime($tmp_cookie['expires'])) < 0) {
221 $expires = null; 221 $expires = null;
222 } 222 }
223 } else { 223 } else {
224 $expires = null; 224 $expires = null;
225 } 225 }
226 // set cookie 226 // set cookie
227 $this->set_cookie($domain, $path, $tmp_cookie['name'], $tmp_cookie['value'], $secure, $expires); 227 $this->set_cookie($domain, $path, $tmp_cookie['name'], $tmp_cookie['value'], $secure, $expires);
228 } 228 }
229 } 229 }
230 230
231 // return array of set-cookie values extracted from HTTP response headers (string $h) 231 // return array of set-cookie values extracted from HTTP response headers (string $h)
232 public function extractCookies($h) { 232 public function extractCookies($h) {
233 $x = 0; 233 $x = 0;
234 $lines = 0; 234 $lines = 0;
235 $headers = array(); 235 $headers = array();
236 $last_match = false; 236 $last_match = false;
237 $h = explode("\n", $h); 237 $h = explode("\n", $h);
238 foreach ($h as $line) { 238 foreach ($h as $line) {
239 $line = rtrim($line); 239 $line = rtrim($line);
240 $lines++; 240 $lines++;
241 241
242 $trimmed_line = trim($line); 242 $trimmed_line = trim($line);
243 if (isset($line_last)) { 243 if (isset($line_last)) {
244 // check if we have \r\n\r\n (indicating the end of headers) 244 // check if we have \r\n\r\n (indicating the end of headers)
245 // some servers will not use CRLF (\r\n), so we make CR (\r) optional. 245 // some servers will not use CRLF (\r\n), so we make CR (\r) optional.
246 // if (preg_match('/\015?\012\015?\012/', $line_last.$line)) { 246 // if (preg_match('/\015?\012\015?\012/', $line_last.$line)) {
247 // break; 247 // break;
248 // } 248 // }
249 // As an alternative, we can check if the current trimmed line is empty 249 // As an alternative, we can check if the current trimmed line is empty
250 if ($trimmed_line == '') { 250 if ($trimmed_line == '') {
251 break; 251 break;
252 } 252 }
253 253
254 // check for continuation line... 254 // check for continuation line...
255 // RFC 2616 Section 2.2 "Basic Rules": 255 // RFC 2616 Section 2.2 "Basic Rules":
256 // HTTP/1.1 header field values can be folded onto multiple lines if the 256 // HTTP/1.1 header field values can be folded onto multiple lines if the
257 // continuation line begins with a space or horizontal tab. All linear 257 // continuation line begins with a space or horizontal tab. All linear
258 // white space, including folding, has the same semantics as SP. A 258 // white space, including folding, has the same semantics as SP. A
259 // recipient MAY replace any linear white space with a single SP before 259 // recipient MAY replace any linear white space with a single SP before
260 // interpreting the field value or forwarding the message downstream. 260 // interpreting the field value or forwarding the message downstream.
261 if ($last_match && preg_match('/^\s+(.*)/', $line, $match)) { 261 if ($last_match && preg_match('/^\s+(.*)/', $line, $match)) {
262 // append to previous header value 262 // append to previous header value
263 $headers[$x-1] .= ' '.rtrim($match[1]); 263 $headers[$x-1] .= ' '.rtrim($match[1]);
264 continue; 264 continue;
265 } 265 }
266 } 266 }
267 $line_last = $line; 267 $line_last = $line;
268 268
269 // split header name and value 269 // split header name and value
270 if (preg_match('/^Set-Cookie\s*:\s*(.*)/i', $line, $match)) { 270 if (preg_match('/^Set-Cookie\s*:\s*(.*)/i', $line, $match)) {
271 $headers[$x++] = rtrim($match[1]); 271 $headers[$x++] = rtrim($match[1]);
272 $last_match = true; 272 $last_match = true;
273 } else { 273 } else {
274 $last_match = false; 274 $last_match = false;
275 } 275 }
276 } 276 }
277 return $headers; 277 return $headers;
278 } 278 }
279 279
280 /** 280 /**
281 * Set Cookie 281 * Set Cookie
282 * @param string $domain 282 * @param string $domain
283 * @param string $path 283 * @param string $path
284 * @param string $name cookie name 284 * @param string $name cookie name
285 * @param string $value cookie value 285 * @param string $value cookie value
286 * @param bool $secure 286 * @param bool $secure
287 * @param int $expires expiry time (null if session cookie, <= 0 will delete cookie) 287 * @param int $expires expiry time (null if session cookie, <= 0 will delete cookie)
288 * @return void 288 * @return void
289 */ 289 */
290 function set_cookie($domain, $path, $name, $value, $secure=false, $expires=null) 290 function set_cookie($domain, $path, $name, $value, $secure=false, $expires=null)
291 { 291 {
292 if ($domain == '') return; 292 if ($domain == '') return;
293 if ($path == '') return; 293 if ($path == '') return;
294 if ($name == '') return; 294 if ($name == '') return;
295 // check if cookie needs to go 295 // check if cookie needs to go
296 if (isset($expires) && ($expires <= 0)) { 296 if (isset($expires) && ($expires <= 0)) {
297 if (isset($this->cookies[$domain][$path][$name])) unset($this->cookies[$domain][$path][$name]); 297 if (isset($this->cookies[$domain][$path][$name])) unset($this->cookies[$domain][$path][$name]);
298 return; 298 return;
299 } 299 }
300 if ($value == '') return; 300 if ($value == '') return;
301 $this->cookies[$domain][$path][$name] = array($value, $secure, $expires); 301 $this->cookies[$domain][$path][$name] = array($value, $secure, $expires);
302 return; 302 return;
303 } 303 }
304 304
305 /** 305 /**
306 * Clear cookies - [domain [,path [,name]]] - call method with no arguments to clear all cookies. 306 * Clear cookies - [domain [,path [,name]]] - call method with no arguments to clear all cookies.
307 * @param string $domain 307 * @param string $domain
308 * @param string $path 308 * @param string $path
309 * @param string $name 309 * @param string $name
310 * @return void 310 * @return void
311 */ 311 */
312 function clear($domain=null, $path=null, $name=null) 312 function clear($domain=null, $path=null, $name=null)
313 { 313 {
314 if (!isset($domain)) { 314 if (!isset($domain)) {
315 $this->cookies = array(); 315 $this->cookies = array();
316 } elseif (!isset($path)) { 316 } elseif (!isset($path)) {
317 if (isset($this->cookies[$domain])) unset($this->cookies[$domain]); 317 if (isset($this->cookies[$domain])) unset($this->cookies[$domain]);
318 } elseif (!isset($name)) { 318 } elseif (!isset($name)) {
319 if (isset($this->cookies[$domain][$path])) unset($this->cookies[$domain][$path]); 319 if (isset($this->cookies[$domain][$path])) unset($this->cookies[$domain][$path]);
320 } elseif (isset($name)) { 320 } elseif (isset($name)) {
321 if (isset($this->cookies[$domain][$path][$name])) unset($this->cookies[$domain][$path][$name]); 321 if (isset($this->cookies[$domain][$path][$name])) unset($this->cookies[$domain][$path][$name]);
322 } 322 }
323 } 323 }
324 324
325 /** 325 /**
326 * Compare string length - used for sorting 326 * Compare string length - used for sorting
327 * @access private 327 * @access private
328 * @return int 328 * @return int
329 */ 329 */
330 function _cmp_length($a, $b) 330 function _cmp_length($a, $b)
331 { 331 {
332 $la = strlen($a); $lb = strlen($b); 332 $la = strlen($a); $lb = strlen($b);
333 if ($la == $lb) return 0; 333 if ($la == $lb) return 0;
334 return ($la > $lb) ? -1 : 1; 334 return ($la > $lb) ? -1 : 1;
335 } 335 }
336 336
337 /** 337 /**
338 * Reduce domain 338 * Reduce domain
339 * @param string $domain 339 * @param string $domain
340 * @return string 340 * @return string
341 * @access private 341 * @access private
342 */ 342 */
343 function _reduce_domain($domain) 343 function _reduce_domain($domain)
344 { 344 {
345 if ($domain == '') return ''; 345 if ($domain == '') return '';
346 if (substr($domain, 0, 1) == '.') return substr($domain, 1); 346 if (substr($domain, 0, 1) == '.') return substr($domain, 1);
347 return substr($domain, strpos($domain, '.')); 347 return substr($domain, strpos($domain, '.'));
348 } 348 }
349 349
350 /** 350 /**
351 * Path match - check if path1 path-matches path2 351 * Path match - check if path1 path-matches path2
352 * 352 *
353 * From RFC 2965: 353 * From RFC 2965:
354 * <i>For two strings that represent paths, P1 and P2, P1 path-matches P2 354 * <i>For two strings that represent paths, P1 and P2, P1 path-matches P2
355 * if P2 is a prefix of P1 (including the case where P1 and P2 string- 355 * if P2 is a prefix of P1 (including the case where P1 and P2 string-
356 * compare equal). Thus, the string /tec/waldo path-matches /tec.</i> 356 * compare equal). Thus, the string /tec/waldo path-matches /tec.</i>
357 * @param string $path1 357 * @param string $path1
358 * @param string $path2 358 * @param string $path2
359 * @return bool 359 * @return bool
360 * @access private 360 * @access private
361 */ 361 */
362 function _path_match($path1, $path2) 362 function _path_match($path1, $path2)
363 { 363 {
364 return (substr($path1, 0, strlen($path2)) == $path2); 364 return (substr($path1, 0, strlen($path2)) == $path2);
365 } 365 }
366 366
367 /** 367 /**
368 * Domain match - check if domain1 domain-matches domain2 368 * Domain match - check if domain1 domain-matches domain2
369 * 369 *
370 * A few extracts from RFC 2965: 370 * A few extracts from RFC 2965:
371 * - A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com 371 * - A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com
372 * would be rejected, because H is y.x and contains a dot. 372 * would be rejected, because H is y.x and contains a dot.
373 * 373 *
374 * - A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com 374 * - A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com
375 * would be accepted. 375 * would be accepted.
376 * 376 *
377 * - A Set-Cookie2 with Domain=.com or Domain=.com., will always be 377 * - A Set-Cookie2 with Domain=.com or Domain=.com., will always be
378 * rejected, because there is no embedded dot. 378 * rejected, because there is no embedded dot.
379 * 379 *
380 * - A Set-Cookie2 from request-host example for Domain=.local will 380 * - A Set-Cookie2 from request-host example for Domain=.local will
381 * be accepted, because the effective host name for the request- 381 * be accepted, because the effective host name for the request-
382 * host is example.local, and example.local domain-matches .local. 382 * host is example.local, and example.local domain-matches .local.
383 * 383 *
384 * I'm ignoring the first point for now (must check to see how other browsers handle 384 * I'm ignoring the first point for now (must check to see how other browsers handle
385 * this rule for Set-Cookie headers) 385 * this rule for Set-Cookie headers)
386 * 386 *
387 * @param string $domain1 387 * @param string $domain1
388 * @param string $domain2 388 * @param string $domain2
389 * @return bool 389 * @return bool
390 * @access private 390 * @access private
391 */ 391 */
392 function _domain_match($domain1, $domain2) 392 function _domain_match($domain1, $domain2)
393 { 393 {
394 $domain1 = strtolower($domain1); 394 $domain1 = strtolower($domain1);
395 $domain2 = strtolower($domain2); 395 $domain2 = strtolower($domain2);
396 while (strpos($domain1, '.') !== false) { 396 while (strpos($domain1, '.') !== false) {
397 if ($domain1 == $domain2) return true; 397 if ($domain1 == $domain2) return true;
398 $domain1 = $this->_reduce_domain($domain1); 398 $domain1 = $this->_reduce_domain($domain1);
399 continue; 399 continue;
400 } 400 }
401 return false; 401 return false;
402 } 402 }
403} 403} \ No newline at end of file
404?> \ No newline at end of file
diff --git a/inc/3rdparty/libraries/humble-http-agent/HumbleHttpAgent.php b/inc/3rdparty/libraries/humble-http-agent/HumbleHttpAgent.php
index e4f1b3b3..963f0c05 100644
--- a/inc/3rdparty/libraries/humble-http-agent/HumbleHttpAgent.php
+++ b/inc/3rdparty/libraries/humble-http-agent/HumbleHttpAgent.php
@@ -1,779 +1,810 @@
1<?php 1<?php
2/** 2/**
3 * Humble HTTP Agent 3 * Humble HTTP Agent
4 * 4 *
5 * This class is designed to take advantage of parallel HTTP requests 5 * This class is designed to take advantage of parallel HTTP requests
6 * offered by PHP's PECL HTTP extension or the curl_multi_* functions. 6 * offered by PHP's PECL HTTP extension or the curl_multi_* functions.
7 * For environments which do not have these options, it reverts to standard sequential 7 * For environments which do not have these options, it reverts to standard sequential
8 * requests (using file_get_contents()) 8 * requests (using file_get_contents())
9 * 9 *
10 * @version 1.1 10 * @version 1.4
11 * @date 2012-08-20 11 * @date 2013-05-10
12 * @see http://php.net/HttpRequestPool 12 * @see http://php.net/HttpRequestPool
13 * @author Keyvan Minoukadeh 13 * @author Keyvan Minoukadeh
14 * @copyright 2011-2012 Keyvan Minoukadeh 14 * @copyright 2011-2013 Keyvan Minoukadeh
15 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3 15 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3
16 */ 16 */
17 17
18class HumbleHttpAgent 18class HumbleHttpAgent
19{ 19{
20 const METHOD_REQUEST_POOL = 1; 20 const METHOD_REQUEST_POOL = 1;
21 const METHOD_CURL_MULTI = 2; 21 const METHOD_CURL_MULTI = 2;
22 const METHOD_FILE_GET_CONTENTS = 4; 22 const METHOD_FILE_GET_CONTENTS = 4;
23 //const UA_BROWSER = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1'; 23 //const UA_BROWSER = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1';
24 const UA_BROWSER = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.92 Safari/535.2'; 24 const UA_BROWSER = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.92 Safari/535.2';
25 const UA_PHP = 'PHP/5.2'; 25 const UA_PHP = 'PHP/5.4';
26 const REF_GOOGLE = 'http://www.google.co.uk/url?sa=t&source=web&cd=1'; 26 const REF_GOOGLE = 'http://www.google.co.uk/url?sa=t&source=web&cd=1';
27 27
28 protected $requests = array(); 28 protected $requests = array();
29 protected $redirectQueue = array(); 29 protected $redirectQueue = array();
30 protected $requestOptions; 30 protected $requestOptions;
31 protected $maxParallelRequests = 5; 31 protected $maxParallelRequests = 5;
32 protected $cache = null; //TODO 32 protected $cache = null; //TODO
33 protected $httpContext; 33 protected $httpContext;
34 protected $minimiseMemoryUse = false; //TODO 34 protected $minimiseMemoryUse = false; //TODO
35 protected $method; 35 protected $method;
36 protected $cookieJar; 36 protected $cookieJar;
37 public $debug = false; 37 public $debug = false;
38 public $debugVerbose = false; 38 public $debugVerbose = false;
39 public $rewriteHashbangFragment = true; // see http://code.google.com/web/ajaxcrawling/docs/specification.html 39 public $rewriteHashbangFragment = true; // see http://code.google.com/web/ajaxcrawling/docs/specification.html
40 public $maxRedirects = 5; 40 public $maxRedirects = 5;
41 public $userAgentMap = array(); 41 public $userAgentMap = array();
42 public $rewriteUrls = array(); 42 public $rewriteUrls = array();
43 public $userAgentDefault; 43 public $userAgentDefault;
44 public $referer; 44 public $referer;
45 //public $userAgent = 'Mozilla/5.0'; 45 //public $userAgent = 'Mozilla/5.0';
46 46
47 // Prevent certain file/mime types 47 // Prevent certain file/mime types
48 // HTTP responses which match these content types will 48 // HTTP responses which match these content types will
49 // be returned without body. 49 // be returned without body.
50 public $headerOnlyTypes = array(); 50 public $headerOnlyTypes = array();
51 // URLs ending with one of these extensions will 51 // URLs ending with one of these extensions will
52 // prompt Humble HTTP Agent to send a HEAD request first 52 // prompt Humble HTTP Agent to send a HEAD request first
53 // to see if returned content type matches $headerOnlyTypes. 53 // to see if returned content type matches $headerOnlyTypes.
54 public $headerOnlyClues = array('pdf','mp3','zip','exe','gif','gzip','gz','jpeg','jpg','mpg','mpeg','png','ppt','mov'); 54 public $headerOnlyClues = array('pdf','mp3','zip','exe','gif','gzip','gz','jpeg','jpg','mpg','mpeg','png','ppt','mov');
55 // AJAX triggers to search for. 55 // AJAX triggers to search for.
56 // for AJAX sites, e.g. Blogger with its dynamic views templates. 56 // for AJAX sites, e.g. Blogger with its dynamic views templates.
57 public $ajaxTriggers = array("<meta name='fragment' content='!'",'<meta name="fragment" content="!"',"<meta content='!' name='fragment'",'<meta content="!" name="fragment"'); 57 public $ajaxTriggers = array("<meta name='fragment' content='!'",'<meta name="fragment" content="!"',"<meta content='!' name='fragment'",'<meta content="!" name="fragment"');
58 58
59 //TODO: set max file size 59 //TODO: set max file size
60 //TODO: normalise headers 60 //TODO: normalise headers
61 61
62 function __construct($requestOptions=null, $method=null) { 62 function __construct($requestOptions=null, $method=null) {
63 $this->userAgentDefault = self::UA_BROWSER; 63 $this->userAgentDefault = self::UA_BROWSER;
64 $this->referer = self::REF_GOOGLE; 64 $this->referer = self::REF_GOOGLE;
65 // set the request method 65 // set the request method
66 if (in_array($method, array(1,2,4))) { 66 if (in_array($method, array(1,2,4))) {
67 $this->method = $method; 67 $this->method = $method;
68 } else { 68 } else {
69 if (class_exists('HttpRequestPool')) { 69 if (class_exists('HttpRequestPool')) {
70 $this->method = self::METHOD_REQUEST_POOL; 70 $this->method = self::METHOD_REQUEST_POOL;
71 } elseif (function_exists('curl_multi_init')) { 71 } elseif (function_exists('curl_multi_init')) {
72 $this->method = self::METHOD_CURL_MULTI; 72 $this->method = self::METHOD_CURL_MULTI;
73 } else { 73 } else {
74 $this->method = self::METHOD_FILE_GET_CONTENTS; 74 $this->method = self::METHOD_FILE_GET_CONTENTS;
75 } 75 }
76 } 76 }
77 if ($this->method == self::METHOD_CURL_MULTI) { 77 if ($this->method == self::METHOD_CURL_MULTI) {
78 require_once(dirname(__FILE__).'/RollingCurl.php'); 78 require_once(dirname(__FILE__).'/RollingCurl.php');
79 } 79 }
80 // create cookie jar 80 // create cookie jar
81 $this->cookieJar = new CookieJar(); 81 $this->cookieJar = new CookieJar();
82 // set request options (redirect must be 0) 82 // set request options (redirect must be 0)
83 $this->requestOptions = array( 83 $this->requestOptions = array(
84 'timeout' => 15, 84 'timeout' => 15,
85 'redirect' => 0 // we handle redirects manually so we can rewrite the new hashbang URLs that are creeping up over the web 85 'connecttimeout' => 15,
86 // TODO: test onprogress? 86 'dns_cache_timeout' => 300,
87 ); 87 'redirect' => 0 // we handle redirects manually so we can rewrite the new hashbang URLs that are creeping up over the web
88 if (is_array($requestOptions)) { 88 // TODO: test onprogress?
89 $this->requestOptions = array_merge($this->requestOptions, $requestOptions); 89 );
90 } 90 if (is_array($requestOptions)) {
91 $this->httpContext = array( 91 $this->requestOptions = array_merge($this->requestOptions, $requestOptions);
92 'http' => array( 92 }
93 'ignore_errors' => true, 93 $this->httpContext = array(
94 'timeout' => $this->requestOptions['timeout'], 94 'http' => array(
95 'max_redirects' => $this->requestOptions['redirect'], 95 'ignore_errors' => true,
96 'header' => "Accept: */*\r\n" 96 'timeout' => $this->requestOptions['timeout'],
97 ) 97 'max_redirects' => $this->requestOptions['redirect'],
98 ); 98 'header' => "Accept: */*\r\n"
99 } 99 )
100 100 );
101 protected function debug($msg) { 101 }
102 if ($this->debug) { 102
103 $mem = round(memory_get_usage()/1024, 2); 103 protected function debug($msg) {
104 $memPeak = round(memory_get_peak_usage()/1024, 2); 104 if ($this->debug) {
105 echo '* ',$msg; 105 $mem = round(memory_get_usage()/1024, 2);
106 if ($this->debugVerbose) echo ' - mem used: ',$mem," (peak: $memPeak)"; 106 $memPeak = round(memory_get_peak_usage()/1024, 2);
107 echo "\n"; 107 echo '* ',$msg;
108 ob_flush(); 108 if ($this->debugVerbose) echo ' - mem used: ',$mem," (peak: $memPeak)";
109 flush(); 109 echo "\n";
110 } 110 ob_flush();
111 } 111 flush();
112 112 }
113 protected function getUserAgent($url, $asArray=false) { 113 }
114 $host = @parse_url($url, PHP_URL_HOST); 114
115 if (strtolower(substr($host, 0, 4)) == 'www.') { 115 protected function getUserAgent($url, $asArray=false) {
116 $host = substr($host, 4); 116 $host = @parse_url($url, PHP_URL_HOST);
117 } 117 if (strtolower(substr($host, 0, 4)) == 'www.') {
118 if ($host) { 118 $host = substr($host, 4);
119 $try = array($host); 119 }
120 $split = explode('.', $host); 120 if ($host) {
121 if (count($split) > 1) { 121 $try = array($host);
122 array_shift($split); 122 $split = explode('.', $host);
123 $try[] = '.'.implode('.', $split); 123 if (count($split) > 1) {
124 } 124 array_shift($split);
125 foreach ($try as $h) { 125 $try[] = '.'.implode('.', $split);
126 if (isset($this->userAgentMap[$h])) { 126 }
127 $ua = $this->userAgentMap[$h]; 127 foreach ($try as $h) {
128 break; 128 if (isset($this->userAgentMap[$h])) {
129 } 129 $ua = $this->userAgentMap[$h];
130 } 130 break;
131 } 131 }
132 if (!isset($ua)) $ua = $this->userAgentDefault; 132 }
133 if ($asArray) { 133 }
134 return array('User-Agent' => $ua); 134 if (!isset($ua)) $ua = $this->userAgentDefault;
135 } else { 135 if ($asArray) {
136 return 'User-Agent: '.$ua; 136 return array('User-Agent' => $ua);
137 } 137 } else {
138 } 138 return 'User-Agent: '.$ua;
139 139 }
140 public function rewriteHashbangFragment($url) { 140 }
141 // return $url if there's no '#!' 141
142 if (strpos($url, '#!') === false) return $url; 142 public function rewriteHashbangFragment($url) {
143 // split $url and rewrite 143 // return $url if there's no '#!'
144 // TODO: is SimplePie_IRI included? 144 if (strpos($url, '#!') === false) return $url;
145 $iri = new SimplePie_IRI($url); 145 // split $url and rewrite
146 $fragment = substr($iri->fragment, 1); // strip '!' 146 // TODO: is SimplePie_IRI included?
147 $iri->fragment = null; 147 $iri = new SimplePie_IRI($url);
148 if (isset($iri->query)) { 148 $fragment = substr($iri->fragment, 1); // strip '!'
149 parse_str($iri->query, $query); 149 $iri->fragment = null;
150 } else { 150 if (isset($iri->query)) {
151 $query = array(); 151 parse_str($iri->query, $query);
152 } 152 } else {
153 $query['_escaped_fragment_'] = (string)$fragment; 153 $query = array();
154 $iri->query = str_replace('%2F', '/', http_build_query($query)); // needed for some sites 154 }
155 return $iri->get_iri(); 155 $query['_escaped_fragment_'] = (string)$fragment;
156 } 156 $iri->query = str_replace('%2F', '/', http_build_query($query)); // needed for some sites
157 157 return $iri->get_iri();
158 public function getUglyURL($url, $html) { 158 }
159 if ($html == '') return false; 159
160 $found = false; 160 public function getRedirectURLfromHTML($url, $html) {
161 foreach ($this->ajaxTriggers as $string) { 161 $redirect_url = $this->getMetaRefreshURL($url, $html);
162 if (stripos($html, $string)) { 162 if (!$redirect_url) {
163 $found = true; 163 $redirect_url = $this->getUglyURL($url, $html);
164 break; 164 }
165 } 165 return $redirect_url;
166 } 166 }
167 if (!$found) return false; 167
168 $iri = new SimplePie_IRI($url); 168 public function getMetaRefreshURL($url, $html) {
169 if (isset($iri->query)) { 169 if ($html == '') return false;
170 parse_str($iri->query, $query); 170 // <meta HTTP-EQUIV="REFRESH" content="0; url=http://www.bernama.com/bernama/v6/newsindex.php?id=943513">
171 } else { 171 if (!preg_match('!<meta http-equiv=["\']?refresh["\']? content=["\']?[0-9];\s*url=["\']?([^"\'>]+)["\']*>!i', $html, $match)) {
172 $query = array(); 172 return false;
173 } 173 }
174 $query['_escaped_fragment_'] = ''; 174 $redirect_url = $match[1];
175 $iri->query = str_replace('%2F', '/', http_build_query($query)); // needed for some sites 175 if (preg_match('!^https?://!i', $redirect_url)) {
176 return $iri->get_iri(); 176 // already absolute
177 } 177 $this->debug('Meta refresh redirect found (http-equiv="refresh"), new URL: '.$redirect_url);
178 178 return $redirect_url;
179 public function removeFragment($url) { 179 }
180 $pos = strpos($url, '#'); 180 // absolutize redirect URL
181 if ($pos === false) { 181 $base = new SimplePie_IRI($url);
182 return $url; 182 // remove '//' in URL path (causes URLs not to resolve properly)
183 } else { 183 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
184 return substr($url, 0, $pos); 184 if ($absolute = SimplePie_IRI::absolutize($base, $redirect_url)) {
185 } 185 $this->debug('Meta refresh redirect found (http-equiv="refresh"), new URL: '.$absolute);
186 } 186 return $absolute;
187 187 }
188 public function rewriteUrls($url) { 188 return false;
189 foreach ($this->rewriteUrls as $find => $action) { 189 }
190 if (strpos($url, $find) !== false) { 190
191 if (is_array($action)) { 191 public function getUglyURL($url, $html) {
192 return strtr($url, $action); 192 if ($html == '') return false;
193 } 193 $found = false;
194 } 194 foreach ($this->ajaxTriggers as $string) {
195 } 195 if (stripos($html, $string)) {
196 return $url; 196 $found = true;
197 } 197 break;
198 198 }
199 public function enableDebug($bool=true) { 199 }
200 $this->debug = (bool)$bool; 200 if (!$found) return false;
201 } 201 $iri = new SimplePie_IRI($url);
202 202 if (isset($iri->query)) {
203 public function minimiseMemoryUse($bool = true) { 203 parse_str($iri->query, $query);
204 $this->minimiseMemoryUse = $bool; 204 } else {
205 } 205 $query = array();
206 206 }
207 public function setMaxParallelRequests($max) { 207 $query['_escaped_fragment_'] = '';
208 $this->maxParallelRequests = $max; 208 $iri->query = str_replace('%2F', '/', http_build_query($query)); // needed for some sites
209 } 209 $ugly_url = $iri->get_iri();
210 210 $this->debug('AJAX trigger (meta name="fragment" content="!") found, new URL: '.$ugly_url);
211 public function validateUrl($url) { 211 return $ugly_url;
212 $url = filter_var($url, FILTER_SANITIZE_URL); 212 }
213 $test = filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED); 213
214 // deal with bug http://bugs.php.net/51192 (present in PHP 5.2.13 and PHP 5.3.2) 214 public function removeFragment($url) {
215 if ($test === false) { 215 $pos = strpos($url, '#');
216 $test = filter_var(strtr($url, '-', '_'), FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED); 216 if ($pos === false) {
217 } 217 return $url;
218 if ($test !== false && $test !== null && preg_match('!^https?://!', $url)) { 218 } else {
219 return $url; 219 return substr($url, 0, $pos);
220 } else { 220 }
221 return false; 221 }
222 } 222
223 } 223 public function rewriteUrls($url) {
224 224 foreach ($this->rewriteUrls as $find => $action) {
225 public function fetchAll(array $urls) { 225 if (strpos($url, $find) !== false) {
226 $this->fetchAllOnce($urls, $isRedirect=false); 226 if (is_array($action)) {
227 $redirects = 0; 227 return strtr($url, $action);
228 while (!empty($this->redirectQueue) && ++$redirects <= $this->maxRedirects) { 228 }
229 $this->debug("Following redirects #$redirects..."); 229 }
230 $this->fetchAllOnce($this->redirectQueue, $isRedirect=true); 230 }
231 } 231 return $url;
232 } 232 }
233 233
234 // fetch all URLs without following redirects 234 public function enableDebug($bool=true) {
235 public function fetchAllOnce(array $urls, $isRedirect=false) { 235 $this->debug = (bool)$bool;
236 if (!$isRedirect) $urls = array_unique($urls); 236 }
237 if (empty($urls)) return; 237
238 238 public function minimiseMemoryUse($bool = true) {
239 ////////////////////////////////////////////////////// 239 $this->minimiseMemoryUse = $bool;
240 // parallel (HttpRequestPool) 240 }
241 if ($this->method == self::METHOD_REQUEST_POOL) { 241
242 $this->debug('Starting parallel fetch (HttpRequestPool)'); 242 public function setMaxParallelRequests($max) {
243 try { 243 $this->maxParallelRequests = $max;
244 while (count($urls) > 0) { 244 }
245 $this->debug('Processing set of '.min($this->maxParallelRequests, count($urls))); 245
246 $subset = array_splice($urls, 0, $this->maxParallelRequests); 246 public function validateUrl($url) {
247 $pool = new HttpRequestPool(); 247 $url = filter_var($url, FILTER_SANITIZE_URL);
248 foreach ($subset as $orig => $url) { 248 $test = filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED);
249 if (!$isRedirect) $orig = $url; 249 // deal with bug http://bugs.php.net/51192 (present in PHP 5.2.13 and PHP 5.3.2)
250 unset($this->redirectQueue[$orig]); 250 if ($test === false) {
251 $this->debug("...$url"); 251 $test = filter_var(strtr($url, '-', '_'), FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED);
252 if (!$isRedirect && isset($this->requests[$url])) { 252 }
253 $this->debug("......in memory"); 253 if ($test !== false && $test !== null && preg_match('!^https?://!', $url)) {
254 /* 254 return $url;
255 } elseif ($this->isCached($url)) { 255 } else {
256 $this->debug("......is cached"); 256 return false;
257 if (!$this->minimiseMemoryUse) { 257 }
258 $this->requests[$url] = $this->getCached($url); 258 }
259 } 259
260 */ 260 public function fetchAll(array $urls) {
261 } else { 261 $this->fetchAllOnce($urls, $isRedirect=false);
262 $this->debug("......adding to pool"); 262 $redirects = 0;
263 $req_url = $this->rewriteUrls($url); 263 while (!empty($this->redirectQueue) && ++$redirects <= $this->maxRedirects) {
264 $req_url = ($this->rewriteHashbangFragment) ? $this->rewriteHashbangFragment($req_url) : $req_url; 264 $this->debug("Following redirects #$redirects...");
265 $req_url = $this->removeFragment($req_url); 265 $this->fetchAllOnce($this->redirectQueue, $isRedirect=true);
266 if (!empty($this->headerOnlyTypes) && !isset($this->requests[$orig]['wrongGuess']) && $this->possibleUnsupportedType($req_url)) { 266 }
267 $_meth = HttpRequest::METH_HEAD; 267 }
268 } else { 268
269 $_meth = HttpRequest::METH_GET; 269 // fetch all URLs without following redirects
270 unset($this->requests[$orig]['wrongGuess']); 270 public function fetchAllOnce(array $urls, $isRedirect=false) {
271 } 271 if (!$isRedirect) $urls = array_unique($urls);
272 $httpRequest = new HttpRequest($req_url, $_meth, $this->requestOptions); 272 if (empty($urls)) return;
273 // send cookies, if we have any 273
274 if ($cookies = $this->cookieJar->getMatchingCookies($req_url)) { 274 //////////////////////////////////////////////////////
275 $this->debug("......sending cookies: $cookies"); 275 // parallel (HttpRequestPool)
276 $httpRequest->addHeaders(array('Cookie' => $cookies)); 276 if ($this->method == self::METHOD_REQUEST_POOL) {
277 } 277 $this->debug('Starting parallel fetch (HttpRequestPool)');
278 //$httpRequest->addHeaders(array('User-Agent' => $this->userAgent)); 278 try {
279 $httpRequest->addHeaders($this->getUserAgent($req_url, true)); 279 while (count($urls) > 0) {
280 // add referer for picky sites 280 $this->debug('Processing set of '.min($this->maxParallelRequests, count($urls)));
281 $httpRequest->addheaders(array('Referer' => $this->referer)); 281 $subset = array_splice($urls, 0, $this->maxParallelRequests);
282 $this->requests[$orig] = array('headers'=>null, 'body'=>null, 'httpRequest'=>$httpRequest); 282 $pool = new HttpRequestPool();
283 $this->requests[$orig]['original_url'] = $orig; 283 foreach ($subset as $orig => $url) {
284 $pool->attach($httpRequest); 284 if (!$isRedirect) $orig = $url;
285 } 285 unset($this->redirectQueue[$orig]);
286 } 286 $this->debug("...$url");
287 // did we get anything into the pool? 287 if (!$isRedirect && isset($this->requests[$url])) {
288 if (count($pool) > 0) { 288 $this->debug("......in memory");
289 $this->debug('Sending request...'); 289 /*
290 try { 290 } elseif ($this->isCached($url)) {
291 $pool->send(); 291 $this->debug("......is cached");
292 } catch (HttpRequestPoolException $e) { 292 if (!$this->minimiseMemoryUse) {
293 // do nothing 293 $this->requests[$url] = $this->getCached($url);
294 } 294 }
295 $this->debug('Received responses'); 295 */
296 foreach($subset as $orig => $url) { 296 } else {
297 if (!$isRedirect) $orig = $url; 297 $this->debug("......adding to pool");
298 $request = $this->requests[$orig]['httpRequest']; 298 $req_url = $this->rewriteUrls($url);
299 //$this->requests[$orig]['headers'] = $this->headersToString($request->getResponseHeader()); 299 $req_url = ($this->rewriteHashbangFragment) ? $this->rewriteHashbangFragment($req_url) : $req_url;
300 // getResponseHeader() doesn't return status line, so, for consistency... 300 $req_url = $this->removeFragment($req_url);
301 $this->requests[$orig]['headers'] = substr($request->getRawResponseMessage(), 0, $request->getResponseInfo('header_size')); 301 if (!empty($this->headerOnlyTypes) && !isset($this->requests[$orig]['wrongGuess']) && $this->possibleUnsupportedType($req_url)) {
302 // check content type 302 $_meth = HttpRequest::METH_HEAD;
303 // TODO: use getResponseHeader('content-type') or getResponseInfo() 303 } else {
304 if ($this->headerOnlyType($this->requests[$orig]['headers'])) { 304 $_meth = HttpRequest::METH_GET;
305 $this->requests[$orig]['body'] = ''; 305 unset($this->requests[$orig]['wrongGuess']);
306 $_header_only_type = true; 306 }
307 $this->debug('Header only type returned'); 307 $httpRequest = new HttpRequest($req_url, $_meth, $this->requestOptions);
308 } else { 308 // send cookies, if we have any
309 $this->requests[$orig]['body'] = $request->getResponseBody(); 309 if ($cookies = $this->cookieJar->getMatchingCookies($req_url)) {
310 $_header_only_type = false; 310 $this->debug("......sending cookies: $cookies");
311 } 311 $httpRequest->addHeaders(array('Cookie' => $cookies));
312 $this->requests[$orig]['effective_url'] = $request->getResponseInfo('effective_url'); 312 }
313 $this->requests[$orig]['status_code'] = $status_code = $request->getResponseCode(); 313 //$httpRequest->addHeaders(array('User-Agent' => $this->userAgent));
314 // is redirect? 314 $httpRequest->addHeaders($this->getUserAgent($req_url, true));
315 if ((in_array($status_code, array(300, 301, 302, 303, 307)) || $status_code > 307 && $status_code < 400) && $request->getResponseHeader('location')) { 315 // add referer for picky sites
316 $redirectURL = $request->getResponseHeader('location'); 316 $httpRequest->addheaders(array('Referer' => $this->referer));
317 if (!preg_match('!^https?://!i', $redirectURL)) { 317 $this->requests[$orig] = array('headers'=>null, 'body'=>null, 'httpRequest'=>$httpRequest);
318 $redirectURL = SimplePie_Misc::absolutize_url($redirectURL, $url); 318 $this->requests[$orig]['original_url'] = $orig;
319 } 319 $pool->attach($httpRequest);
320 if ($this->validateURL($redirectURL)) { 320 }
321 $this->debug('Redirect detected. Valid URL: '.$redirectURL); 321 }
322 // store any cookies 322 // did we get anything into the pool?
323 $cookies = $request->getResponseHeader('set-cookie'); 323 if (count($pool) > 0) {
324 if ($cookies && !is_array($cookies)) $cookies = array($cookies); 324 $this->debug('Sending request...');
325 if ($cookies) $this->cookieJar->storeCookies($url, $cookies); 325 try {
326 $this->redirectQueue[$orig] = $redirectURL; 326 $pool->send();
327 } else { 327 } catch (HttpRequestPoolException $e) {
328 $this->debug('Redirect detected. Invalid URL: '.$redirectURL); 328 // do nothing
329 } 329 }
330 } elseif (!$_header_only_type && $request->getMethod() === HttpRequest::METH_HEAD) { 330 $this->debug('Received responses');
331 // the response content-type did not match our 'header only' types, 331 foreach($subset as $orig => $url) {
332 // but we'd issues a HEAD request because we assumed it would. So 332 if (!$isRedirect) $orig = $url;
333 // let's queue a proper GET request for this item... 333 $request = $this->requests[$orig]['httpRequest'];
334 $this->debug('Wrong guess at content-type, queing GET request'); 334 //$this->requests[$orig]['headers'] = $this->headersToString($request->getResponseHeader());
335 $this->requests[$orig]['wrongGuess'] = true; 335 // getResponseHeader() doesn't return status line, so, for consistency...
336 $this->redirectQueue[$orig] = $this->requests[$orig]['effective_url']; 336 $this->requests[$orig]['headers'] = substr($request->getRawResponseMessage(), 0, $request->getResponseInfo('header_size'));
337 } elseif (strpos($this->requests[$orig]['effective_url'], '_escaped_fragment_') === false) { 337 // check content type
338 // check for <meta name='fragment' content='!'/> 338 // TODO: use getResponseHeader('content-type') or getResponseInfo()
339 // for AJAX sites, e.g. Blogger with its dynamic views templates. 339 if ($this->headerOnlyType($this->requests[$orig]['headers'])) {
340 // Based on Google's spec: https://developers.google.com/webmasters/ajax-crawling/docs/specification 340 $this->requests[$orig]['body'] = '';
341 if (isset($this->requests[$orig]['body'])) { 341 $_header_only_type = true;
342 $redirectURL = $this->getUglyURL($this->requests[$orig]['effective_url'], substr($this->requests[$orig]['body'], 0, 4000)); 342 $this->debug('Header only type returned');
343 if ($redirectURL) { 343 } else {
344 $this->debug('AJAX trigger (meta name="fragment" content="!") found. Queueing '.$redirectURL); 344 $this->requests[$orig]['body'] = $request->getResponseBody();
345 $this->redirectQueue[$orig] = $redirectURL; 345 $_header_only_type = false;
346 } 346 }
347 } 347 $this->requests[$orig]['effective_url'] = $request->getResponseInfo('effective_url');
348 } 348 $this->requests[$orig]['status_code'] = $status_code = $request->getResponseCode();
349 //die($url.' -multi- '.$request->getResponseInfo('effective_url')); 349 // is redirect?
350 $pool->detach($request); 350 if ((in_array($status_code, array(300, 301, 302, 303, 307)) || $status_code > 307 && $status_code < 400) && $request->getResponseHeader('location')) {
351 unset($this->requests[$orig]['httpRequest'], $request); 351 $redirectURL = $request->getResponseHeader('location');
352 /* 352 if (!preg_match('!^https?://!i', $redirectURL)) {
353 if ($this->minimiseMemoryUse) { 353 $redirectURL = SimplePie_Misc::absolutize_url($redirectURL, $url);
354 if ($this->cache($url)) { 354 }
355 unset($this->requests[$url]); 355 if ($this->validateURL($redirectURL)) {
356 } 356 $this->debug('Redirect detected. Valid URL: '.$redirectURL);
357 } 357 // store any cookies
358 */ 358 $cookies = $request->getResponseHeader('set-cookie');
359 } 359 if ($cookies && !is_array($cookies)) $cookies = array($cookies);
360 } 360 if ($cookies) $this->cookieJar->storeCookies($url, $cookies);
361 } 361 $this->redirectQueue[$orig] = $redirectURL;
362 } catch (HttpException $e) { 362 } else {
363 $this->debug($e); 363 $this->debug('Redirect detected. Invalid URL: '.$redirectURL);
364 return false; 364 }
365 } 365 } elseif (!$_header_only_type && $request->getMethod() === HttpRequest::METH_HEAD) {
366 } 366 // the response content-type did not match our 'header only' types,
367 367 // but we'd issues a HEAD request because we assumed it would. So
368 ////////////////////////////////////////////////////////// 368 // let's queue a proper GET request for this item...
369 // parallel (curl_multi_*) 369 $this->debug('Wrong guess at content-type, queing GET request');
370 elseif ($this->method == self::METHOD_CURL_MULTI) { 370 $this->requests[$orig]['wrongGuess'] = true;
371 $this->debug('Starting parallel fetch (curl_multi_*)'); 371 $this->redirectQueue[$orig] = $this->requests[$orig]['effective_url'];
372 while (count($urls) > 0) { 372 } elseif (strpos($this->requests[$orig]['effective_url'], '_escaped_fragment_') === false) {
373 $this->debug('Processing set of '.min($this->maxParallelRequests, count($urls))); 373 // check for <meta name='fragment' content='!'/>
374 $subset = array_splice($urls, 0, $this->maxParallelRequests); 374 // for AJAX sites, e.g. Blogger with its dynamic views templates.
375 $pool = new RollingCurl(array($this, 'handleCurlResponse')); 375 // Based on Google's spec: https://developers.google.com/webmasters/ajax-crawling/docs/specification
376 $pool->window_size = count($subset); 376 if (isset($this->requests[$orig]['body'])) {
377 377 $redirectURL = $this->getRedirectURLfromHTML($this->requests[$orig]['effective_url'], substr($this->requests[$orig]['body'], 0, 4000));
378 foreach ($subset as $orig => $url) { 378 if ($redirectURL) {
379 if (!$isRedirect) $orig = $url; 379 $this->redirectQueue[$orig] = $redirectURL;
380 unset($this->redirectQueue[$orig]); 380 }
381 $this->debug("...$url"); 381 }
382 if (!$isRedirect && isset($this->requests[$url])) { 382 }
383 $this->debug("......in memory"); 383 //die($url.' -multi- '.$request->getResponseInfo('effective_url'));
384 /* 384 $pool->detach($request);
385 } elseif ($this->isCached($url)) { 385 unset($this->requests[$orig]['httpRequest'], $request);
386 $this->debug("......is cached"); 386 /*
387 if (!$this->minimiseMemoryUse) { 387 if ($this->minimiseMemoryUse) {
388 $this->requests[$url] = $this->getCached($url); 388 if ($this->cache($url)) {
389 } 389 unset($this->requests[$url]);
390 */ 390 }
391 } else { 391 }
392 $this->debug("......adding to pool"); 392 */
393 $req_url = $this->rewriteUrls($url); 393 }
394 $req_url = ($this->rewriteHashbangFragment) ? $this->rewriteHashbangFragment($req_url) : $req_url; 394 }
395 $req_url = $this->removeFragment($req_url); 395 }
396 if (!empty($this->headerOnlyTypes) && !isset($this->requests[$orig]['wrongGuess']) && $this->possibleUnsupportedType($req_url)) { 396 } catch (HttpException $e) {
397 $_meth = 'HEAD'; 397 $this->debug($e);
398 } else { 398 return false;
399 $_meth = 'GET'; 399 }
400 unset($this->requests[$orig]['wrongGuess']); 400 }
401 } 401
402 $headers = array(); 402 //////////////////////////////////////////////////////////
403 //$headers[] = 'User-Agent: '.$this->userAgent; 403 // parallel (curl_multi_*)
404 $headers[] = $this->getUserAgent($req_url); 404 elseif ($this->method == self::METHOD_CURL_MULTI) {
405 // add referer for picky sites 405 $this->debug('Starting parallel fetch (curl_multi_*)');
406 $headers[] = 'Referer: '.$this->referer; 406 while (count($urls) > 0) {
407 // send cookies, if we have any 407 $this->debug('Processing set of '.min($this->maxParallelRequests, count($urls)));
408 if ($cookies = $this->cookieJar->getMatchingCookies($req_url)) { 408 $subset = array_splice($urls, 0, $this->maxParallelRequests);
409 $this->debug("......sending cookies: $cookies"); 409 $pool = new RollingCurl(array($this, 'handleCurlResponse'));
410 $headers[] = 'Cookie: '.$cookies; 410 $pool->window_size = count($subset);
411 } 411
412 $httpRequest = new RollingCurlRequest($req_url, $_meth, null, $headers, array( 412 foreach ($subset as $orig => $url) {
413 CURLOPT_CONNECTTIMEOUT => $this->requestOptions['timeout'], 413 if (!$isRedirect) $orig = $url;
414 CURLOPT_TIMEOUT => $this->requestOptions['timeout'] 414 unset($this->redirectQueue[$orig]);
415 )); 415 $this->debug("...$url");
416 $httpRequest->set_original_url($orig); 416 if (!$isRedirect && isset($this->requests[$url])) {
417 $this->requests[$orig] = array('headers'=>null, 'body'=>null, 'httpRequest'=>$httpRequest); 417 $this->debug("......in memory");
418 $this->requests[$orig]['original_url'] = $orig; // TODO: is this needed anymore? 418 /*
419 $pool->add($httpRequest); 419 } elseif ($this->isCached($url)) {
420 } 420 $this->debug("......is cached");
421 } 421 if (!$this->minimiseMemoryUse) {
422 // did we get anything into the pool? 422 $this->requests[$url] = $this->getCached($url);
423 if (count($pool) > 0) { 423 }
424 $this->debug('Sending request...'); 424 */
425 $pool->execute(); // this will call handleCurlResponse() and populate $this->requests[$orig] 425 } else {
426 $this->debug('Received responses'); 426 $this->debug("......adding to pool");
427 foreach($subset as $orig => $url) { 427 $req_url = $this->rewriteUrls($url);
428 if (!$isRedirect) $orig = $url; 428 $req_url = ($this->rewriteHashbangFragment) ? $this->rewriteHashbangFragment($req_url) : $req_url;
429 // $this->requests[$orig]['headers'] 429 $req_url = $this->removeFragment($req_url);
430 // $this->requests[$orig]['body'] 430 if (!empty($this->headerOnlyTypes) && !isset($this->requests[$orig]['wrongGuess']) && $this->possibleUnsupportedType($req_url)) {
431 // $this->requests[$orig]['effective_url'] 431 $_meth = 'HEAD';
432 // check content type 432 } else {
433 if ($this->headerOnlyType($this->requests[$orig]['headers'])) { 433 $_meth = 'GET';
434 $this->requests[$orig]['body'] = ''; 434 unset($this->requests[$orig]['wrongGuess']);
435 $_header_only_type = true; 435 }
436 $this->debug('Header only type returned'); 436 $headers = array();
437 } else { 437 //$headers[] = 'User-Agent: '.$this->userAgent;
438 $_header_only_type = false; 438 $headers[] = $this->getUserAgent($req_url);
439 } 439 // add referer for picky sites
440 $status_code = $this->requests[$orig]['status_code']; 440 $headers[] = 'Referer: '.$this->referer;
441 if ((in_array($status_code, array(300, 301, 302, 303, 307)) || $status_code > 307 && $status_code < 400) && isset($this->requests[$orig]['location'])) { 441 // send cookies, if we have any
442 $redirectURL = $this->requests[$orig]['location']; 442 if ($cookies = $this->cookieJar->getMatchingCookies($req_url)) {
443 if (!preg_match('!^https?://!i', $redirectURL)) { 443 $this->debug("......sending cookies: $cookies");
444 $redirectURL = SimplePie_Misc::absolutize_url($redirectURL, $url); 444 $headers[] = 'Cookie: '.$cookies;
445 } 445 }
446 if ($this->validateURL($redirectURL)) { 446 $httpRequest = new RollingCurlRequest($req_url, $_meth, null, $headers, array(
447 $this->debug('Redirect detected. Valid URL: '.$redirectURL); 447 CURLOPT_CONNECTTIMEOUT => $this->requestOptions['timeout'],
448 // store any cookies 448 CURLOPT_TIMEOUT => $this->requestOptions['timeout']
449 $cookies = $this->cookieJar->extractCookies($this->requests[$orig]['headers']); 449 ));
450 if (!empty($cookies)) $this->cookieJar->storeCookies($url, $cookies); 450 $httpRequest->set_original_url($orig);
451 $this->redirectQueue[$orig] = $redirectURL; 451 $this->requests[$orig] = array('headers'=>null, 'body'=>null, 'httpRequest'=>$httpRequest);
452 } else { 452 $this->requests[$orig]['original_url'] = $orig; // TODO: is this needed anymore?
453 $this->debug('Redirect detected. Invalid URL: '.$redirectURL); 453 $pool->add($httpRequest);
454 } 454 }
455 } elseif (!$_header_only_type && $this->requests[$orig]['method'] == 'HEAD') { 455 }
456 // the response content-type did not match our 'header only' types, 456 // did we get anything into the pool?
457 // but we'd issues a HEAD request because we assumed it would. So 457 if (count($pool) > 0) {
458 // let's queue a proper GET request for this item... 458 $this->debug('Sending request...');
459 $this->debug('Wrong guess at content-type, queing GET request'); 459 $pool->execute(); // this will call handleCurlResponse() and populate $this->requests[$orig]
460 $this->requests[$orig]['wrongGuess'] = true; 460 $this->debug('Received responses');
461 $this->redirectQueue[$orig] = $this->requests[$orig]['effective_url']; 461 foreach($subset as $orig => $url) {
462 } elseif (strpos($this->requests[$orig]['effective_url'], '_escaped_fragment_') === false) { 462 if (!$isRedirect) $orig = $url;
463 // check for <meta name='fragment' content='!'/> 463 // $this->requests[$orig]['headers']
464 // for AJAX sites, e.g. Blogger with its dynamic views templates. 464 // $this->requests[$orig]['body']
465 // Based on Google's spec: https://developers.google.com/webmasters/ajax-crawling/docs/specification 465 // $this->requests[$orig]['effective_url']
466 if (isset($this->requests[$orig]['body'])) { 466 // check content type
467 $redirectURL = $this->getUglyURL($this->requests[$orig]['effective_url'], substr($this->requests[$orig]['body'], 0, 4000)); 467 if ($this->headerOnlyType($this->requests[$orig]['headers'])) {
468 if ($redirectURL) { 468 $this->requests[$orig]['body'] = '';
469 $this->debug('AJAX trigger (meta name="fragment" content="!") found. Queueing '.$redirectURL); 469 $_header_only_type = true;
470 $this->redirectQueue[$orig] = $redirectURL; 470 $this->debug('Header only type returned');
471 } 471 } else {
472 } 472 $_header_only_type = false;
473 } 473 }
474 // die($url.' -multi- '.$request->getResponseInfo('effective_url')); 474 $status_code = $this->requests[$orig]['status_code'];
475 unset($this->requests[$orig]['httpRequest'], $this->requests[$orig]['method']); 475 if ((in_array($status_code, array(300, 301, 302, 303, 307)) || $status_code > 307 && $status_code < 400) && isset($this->requests[$orig]['location'])) {
476 } 476 $redirectURL = $this->requests[$orig]['location'];
477 } 477 if (!preg_match('!^https?://!i', $redirectURL)) {
478 } 478 $redirectURL = SimplePie_Misc::absolutize_url($redirectURL, $url);
479 } 479 }
480 480 if ($this->validateURL($redirectURL)) {
481 ////////////////////////////////////////////////////// 481 $this->debug('Redirect detected. Valid URL: '.$redirectURL);
482 // sequential (file_get_contents) 482 // store any cookies
483 else { 483 $cookies = $this->cookieJar->extractCookies($this->requests[$orig]['headers']);
484 $this->debug('Starting sequential fetch (file_get_contents)'); 484 if (!empty($cookies)) $this->cookieJar->storeCookies($url, $cookies);
485 $this->debug('Processing set of '.count($urls)); 485 $this->redirectQueue[$orig] = $redirectURL;
486 foreach ($urls as $orig => $url) { 486 } else {
487 if (!$isRedirect) $orig = $url; 487 $this->debug('Redirect detected. Invalid URL: '.$redirectURL);
488 unset($this->redirectQueue[$orig]); 488 }
489 $this->debug("...$url"); 489 } elseif (!$_header_only_type && $this->requests[$orig]['method'] == 'HEAD') {
490 if (!$isRedirect && isset($this->requests[$url])) { 490 // the response content-type did not match our 'header only' types,
491 $this->debug("......in memory"); 491 // but we'd issues a HEAD request because we assumed it would. So
492 /* 492 // let's queue a proper GET request for this item...
493 } elseif ($this->isCached($url)) { 493 $this->debug('Wrong guess at content-type, queing GET request');
494 $this->debug("......is cached"); 494 $this->requests[$orig]['wrongGuess'] = true;
495 if (!$this->minimiseMemoryUse) { 495 $this->redirectQueue[$orig] = $this->requests[$orig]['effective_url'];
496 $this->requests[$url] = $this->getCached($url); 496 } elseif (strpos($this->requests[$orig]['effective_url'], '_escaped_fragment_') === false) {
497 } 497 // check for <meta name='fragment' content='!'/>
498 */ 498 // for AJAX sites, e.g. Blogger with its dynamic views templates.
499 } else { 499 // Based on Google's spec: https://developers.google.com/webmasters/ajax-crawling/docs/specification
500 $this->debug("Sending request for $url"); 500 if (isset($this->requests[$orig]['body'])) {
501 $this->requests[$orig]['original_url'] = $orig; 501 $redirectURL = $this->getRedirectURLfromHTML($this->requests[$orig]['effective_url'], substr($this->requests[$orig]['body'], 0, 4000));
502 $req_url = $this->rewriteUrls($url); 502 if ($redirectURL) {
503 $req_url = ($this->rewriteHashbangFragment) ? $this->rewriteHashbangFragment($req_url) : $req_url; 503 $this->redirectQueue[$orig] = $redirectURL;
504 $req_url = $this->removeFragment($req_url); 504 }
505 // send cookies, if we have any 505 }
506 $httpContext = $this->httpContext; 506 }
507 $httpContext['http']['header'] .= $this->getUserAgent($req_url)."\r\n"; 507 // die($url.' -multi- '.$request->getResponseInfo('effective_url'));
508 // add referer for picky sites 508 unset($this->requests[$orig]['httpRequest'], $this->requests[$orig]['method']);
509 $httpContext['http']['header'] .= 'Referer: '.$this->referer."\r\n"; 509 }
510 if ($cookies = $this->cookieJar->getMatchingCookies($req_url)) { 510 }
511 $this->debug("......sending cookies: $cookies"); 511 }
512 $httpContext['http']['header'] .= 'Cookie: '.$cookies."\r\n"; 512 }
513 } 513
514 if (false !== ($html = @file_get_contents($req_url, false, stream_context_create($httpContext)))) { 514 //////////////////////////////////////////////////////
515 $this->debug('Received response'); 515 // sequential (file_get_contents)
516 // get status code 516 else {
517 if (!isset($http_response_header[0]) || !preg_match('!^HTTP/\d+\.\d+\s+(\d+)!', trim($http_response_header[0]), $match)) { 517 $this->debug('Starting sequential fetch (file_get_contents)');
518 $this->debug('Error: no status code found'); 518 $this->debug('Processing set of '.count($urls));
519 // TODO: handle error - no status code 519 foreach ($urls as $orig => $url) {
520 } else { 520 if (!$isRedirect) $orig = $url;
521 $this->requests[$orig]['headers'] = $this->headersToString($http_response_header, false); 521 unset($this->redirectQueue[$orig]);
522 // check content type 522 $this->debug("...$url");
523 if ($this->headerOnlyType($this->requests[$orig]['headers'])) { 523 if (!$isRedirect && isset($this->requests[$url])) {
524 $this->requests[$orig]['body'] = ''; 524 $this->debug("......in memory");
525 } else { 525 /*
526 $this->requests[$orig]['body'] = $html; 526 } elseif ($this->isCached($url)) {
527 } 527 $this->debug("......is cached");
528 $this->requests[$orig]['effective_url'] = $req_url; 528 if (!$this->minimiseMemoryUse) {
529 $this->requests[$orig]['status_code'] = $status_code = (int)$match[1]; 529 $this->requests[$url] = $this->getCached($url);
530 unset($match); 530 }
531 // handle redirect 531 */
532 if (preg_match('/^Location:(.*?)$/mi', $this->requests[$orig]['headers'], $match)) { 532 } else {
533 $this->requests[$orig]['location'] = trim($match[1]); 533 $this->debug("Sending request for $url");
534 } 534 $this->requests[$orig]['original_url'] = $orig;
535 if ((in_array($status_code, array(300, 301, 302, 303, 307)) || $status_code > 307 && $status_code < 400) && isset($this->requests[$orig]['location'])) { 535 $req_url = $this->rewriteUrls($url);
536 $redirectURL = $this->requests[$orig]['location']; 536 $req_url = ($this->rewriteHashbangFragment) ? $this->rewriteHashbangFragment($req_url) : $req_url;
537 if (!preg_match('!^https?://!i', $redirectURL)) { 537 $req_url = $this->removeFragment($req_url);
538 $redirectURL = SimplePie_Misc::absolutize_url($redirectURL, $url); 538 // send cookies, if we have any
539 } 539 $httpContext = $this->httpContext;
540 if ($this->validateURL($redirectURL)) { 540 $httpContext['http']['header'] .= $this->getUserAgent($req_url)."\r\n";
541 $this->debug('Redirect detected. Valid URL: '.$redirectURL); 541 // add referer for picky sites
542 // store any cookies 542 $httpContext['http']['header'] .= 'Referer: '.$this->referer."\r\n";
543 $cookies = $this->cookieJar->extractCookies($this->requests[$orig]['headers']); 543 if ($cookies = $this->cookieJar->getMatchingCookies($req_url)) {
544 if (!empty($cookies)) $this->cookieJar->storeCookies($url, $cookies); 544 $this->debug("......sending cookies: $cookies");
545 $this->redirectQueue[$orig] = $redirectURL; 545 $httpContext['http']['header'] .= 'Cookie: '.$cookies."\r\n";
546 } else { 546 }
547 $this->debug('Redirect detected. Invalid URL: '.$redirectURL); 547 if (false !== ($html = @file_get_contents($req_url, false, stream_context_create($httpContext)))) {
548 } 548 $this->debug('Received response');
549 } elseif (strpos($this->requests[$orig]['effective_url'], '_escaped_fragment_') === false) { 549 // get status code
550 // check for <meta name='fragment' content='!'/> 550 if (!isset($http_response_header[0]) || !preg_match('!^HTTP/\d+\.\d+\s+(\d+)!', trim($http_response_header[0]), $match)) {
551 // for AJAX sites, e.g. Blogger with its dynamic views templates. 551 $this->debug('Error: no status code found');
552 // Based on Google's spec: https://developers.google.com/webmasters/ajax-crawling/docs/specification 552 // TODO: handle error - no status code
553 if (isset($this->requests[$orig]['body'])) { 553 } else {
554 $redirectURL = $this->getUglyURL($this->requests[$orig]['effective_url'], substr($this->requests[$orig]['body'], 0, 4000)); 554 $this->requests[$orig]['headers'] = $this->headersToString($http_response_header, false);
555 if ($redirectURL) { 555 // check content type
556 $this->debug('AJAX trigger (meta name="fragment" content="!") found. Queueing '.$redirectURL); 556 if ($this->headerOnlyType($this->requests[$orig]['headers'])) {
557 $this->redirectQueue[$orig] = $redirectURL; 557 $this->requests[$orig]['body'] = '';
558 } 558 } else {
559 } 559 $this->requests[$orig]['body'] = $html;
560 } 560 }
561 } 561 $this->requests[$orig]['effective_url'] = $req_url;
562 } else { 562 $this->requests[$orig]['status_code'] = $status_code = (int)$match[1];
563 $this->debug('Error retrieving URL'); 563 unset($match);
564 //print_r($req_url); 564 // handle redirect
565 //print_r($http_response_header); 565 if (preg_match('/^Location:(.*?)$/mi', $this->requests[$orig]['headers'], $match)) {
566 //print_r($html); 566 $this->requests[$orig]['location'] = trim($match[1]);
567 567 }
568 // TODO: handle error - failed to retrieve URL 568 if ((in_array($status_code, array(300, 301, 302, 303, 307)) || $status_code > 307 && $status_code < 400) && isset($this->requests[$orig]['location'])) {
569 } 569 $redirectURL = $this->requests[$orig]['location'];
570 } 570 if (!preg_match('!^https?://!i', $redirectURL)) {
571 } 571 $redirectURL = SimplePie_Misc::absolutize_url($redirectURL, $url);
572 } 572 }
573 } 573 if ($this->validateURL($redirectURL)) {
574 574 $this->debug('Redirect detected. Valid URL: '.$redirectURL);
575 public function handleCurlResponse($response, $info, $request) { 575 // store any cookies
576 $orig = $request->url_original; 576 $cookies = $this->cookieJar->extractCookies($this->requests[$orig]['headers']);
577 $this->requests[$orig]['headers'] = substr($response, 0, $info['header_size']); 577 if (!empty($cookies)) $this->cookieJar->storeCookies($url, $cookies);
578 $this->requests[$orig]['body'] = substr($response, $info['header_size']); 578 $this->redirectQueue[$orig] = $redirectURL;
579 $this->requests[$orig]['method'] = $request->method; 579 } else {
580 $this->requests[$orig]['effective_url'] = $info['url']; 580 $this->debug('Redirect detected. Invalid URL: '.$redirectURL);
581 $this->requests[$orig]['status_code'] = (int)$info['http_code']; 581 }
582 if (preg_match('/^Location:(.*?)$/mi', $this->requests[$orig]['headers'], $match)) { 582 } elseif (strpos($this->requests[$orig]['effective_url'], '_escaped_fragment_') === false) {
583 $this->requests[$orig]['location'] = trim($match[1]); 583 // check for <meta name='fragment' content='!'/>
584 } 584 // for AJAX sites, e.g. Blogger with its dynamic views templates.
585 } 585 // Based on Google's spec: https://developers.google.com/webmasters/ajax-crawling/docs/specification
586 586 if (isset($this->requests[$orig]['body'])) {
587 protected function headersToString(array $headers, $associative=true) { 587 $redirectURL = $this->getRedirectURLfromHTML($this->requests[$orig]['effective_url'], substr($this->requests[$orig]['body'], 0, 4000));
588 if (!$associative) { 588 if ($redirectURL) {
589 return implode("\n", $headers); 589 $this->redirectQueue[$orig] = $redirectURL;
590 } else { 590 }
591 $str = ''; 591 }
592 foreach ($headers as $key => $val) { 592 }
593 if (is_array($val)) { 593 }
594 foreach ($val as $v) $str .= "$key: $v\n"; 594 } else {
595 } else { 595 $this->debug('Error retrieving URL');
596 $str .= "$key: $val\n"; 596 //print_r($req_url);
597 } 597 //print_r($http_response_header);
598 } 598 //print_r($html);
599 return rtrim($str); 599
600 } 600 // TODO: handle error - failed to retrieve URL
601 } 601 }
602 602 }
603 public function get($url, $remove=false, $gzdecode=true) { 603 }
604 $url = "$url"; 604 }
605 if (isset($this->requests[$url]) && isset($this->requests[$url]['body'])) { 605 }
606 $this->debug("URL already fetched - in memory ($url, effective: {$this->requests[$url]['effective_url']})"); 606
607 $response = $this->requests[$url]; 607 public function handleCurlResponse($response, $info, $request) {
608 /* 608 $orig = $request->url_original;
609 } elseif ($this->isCached($url)) { 609 $this->requests[$orig]['headers'] = substr($response, 0, $info['header_size']);
610 $this->debug("URL already fetched - in disk cache ($url)"); 610 $this->requests[$orig]['body'] = substr($response, $info['header_size']);
611 $response = $this->getCached($url); 611 $this->requests[$orig]['method'] = $request->method;
612 $this->requests[$url] = $response; 612 $this->requests[$orig]['effective_url'] = $info['url'];
613 */ 613 $this->requests[$orig]['status_code'] = (int)$info['http_code'];
614 } else { 614 if (preg_match('/^Location:(.*?)$/mi', $this->requests[$orig]['headers'], $match)) {
615 $this->debug("Fetching URL ($url)"); 615 $this->requests[$orig]['location'] = trim($match[1]);
616 $this->fetchAll(array($url)); 616 }
617 if (isset($this->requests[$url]) && isset($this->requests[$url]['body'])) { 617 }
618 $response = $this->requests[$url]; 618
619 } else { 619 protected function headersToString(array $headers, $associative=true) {
620 $this->debug("Request failed"); 620 if (!$associative) {
621 $response = false; 621 return implode("\n", $headers);
622 } 622 } else {
623 } 623 $str = '';
624 /* 624 foreach ($headers as $key => $val) {
625 if ($this->minimiseMemoryUse && $response) { 625 if (is_array($val)) {
626 $this->cache($url); 626 foreach ($val as $v) $str .= "$key: $v\n";
627 unset($this->requests[$url]); 627 } else {
628 } 628 $str .= "$key: $val\n";
629 */ 629 }
630 if ($remove && $response) unset($this->requests[$url]); 630 }
631 if ($gzdecode && stripos($response['headers'], 'Content-Encoding: gzip')) { 631 return rtrim($str);
632 if ($html = gzdecode($response['body'])) { 632 }
633 $response['body'] = $html; 633 }
634 } 634
635 } 635 public function get($url, $remove=false, $gzdecode=true) {
636 return $response; 636 $url = "$url";
637 } 637 if (isset($this->requests[$url]) && isset($this->requests[$url]['body'])) {
638 638 $this->debug("URL already fetched - in memory ($url, effective: {$this->requests[$url]['effective_url']})");
639 public function parallelSupport() { 639 $response = $this->requests[$url];
640 return class_exists('HttpRequestPool') || function_exists('curl_multi_init'); 640 /*
641 } 641 } elseif ($this->isCached($url)) {
642 642 $this->debug("URL already fetched - in disk cache ($url)");
643 private function headerOnlyType($headers) { 643 $response = $this->getCached($url);
644 if (preg_match('!^Content-Type:\s*(([a-z-]+)/([^;\r\n ]+))!im', $headers, $match)) { 644 $this->requests[$url] = $response;
645 // look for full mime type (e.g. image/jpeg) or just type (e.g. image) 645 */
646 $match[1] = strtolower(trim($match[1])); 646 } else {
647 $match[2] = strtolower(trim($match[2])); 647 $this->debug("Fetching URL ($url)");
648 foreach (array($match[1], $match[2]) as $mime) { 648 $this->fetchAll(array($url));
649 if (in_array($mime, $this->headerOnlyTypes)) return true; 649 if (isset($this->requests[$url]) && isset($this->requests[$url]['body'])) {
650 } 650 $response = $this->requests[$url];
651 } 651 } else {
652 return false; 652 $this->debug("Request failed");
653 } 653 $response = false;
654 654 }
655 private function possibleUnsupportedType($url) { 655 }
656 $path = @parse_url($url, PHP_URL_PATH); 656 /*
657 if ($path && strpos($path, '.') !== false) { 657 if ($this->minimiseMemoryUse && $response) {
658 $ext = strtolower(trim(pathinfo($path, PATHINFO_EXTENSION))); 658 $this->cache($url);
659 return in_array($ext, $this->headerOnlyClues); 659 unset($this->requests[$url]);
660 } 660 }
661 return false; 661 */
662 } 662 if ($remove && $response) unset($this->requests[$url]);
663} 663 if ($gzdecode && stripos($response['headers'], 'Content-Encoding: gzip')) {
664 664 if ($html = gzdecode($response['body'])) {
665// gzdecode from http://www.php.net/manual/en/function.gzdecode.php#82930 665 $response['body'] = $html;
666if (!function_exists('gzdecode')) { 666 }
667 function gzdecode($data,&$filename='',&$error='',$maxlength=null) 667 }
668 { 668 return $response;
669 $len = strlen($data); 669 }
670 if ($len < 18 || strcmp(substr($data,0,2),"\x1f\x8b")) { 670
671 $error = "Not in GZIP format."; 671 public function parallelSupport() {
672 return null; // Not GZIP format (See RFC 1952) 672 return class_exists('HttpRequestPool') || function_exists('curl_multi_init');
673 } 673 }
674 $method = ord(substr($data,2,1)); // Compression method 674
675 $flags = ord(substr($data,3,1)); // Flags 675 private function headerOnlyType($headers) {
676 if ($flags & 31 != $flags) { 676 if (preg_match('!^Content-Type:\s*(([a-z-]+)/([^;\r\n ]+))!im', $headers, $match)) {
677 $error = "Reserved bits not allowed."; 677 // look for full mime type (e.g. image/jpeg) or just type (e.g. image)
678 return null; 678 $match[1] = strtolower(trim($match[1]));
679 } 679 $match[2] = strtolower(trim($match[2]));
680 // NOTE: $mtime may be negative (PHP integer limitations) 680 foreach (array($match[1], $match[2]) as $mime) {
681 $mtime = unpack("V", substr($data,4,4)); 681 if (in_array($mime, $this->headerOnlyTypes)) return true;
682 $mtime = $mtime[1]; 682 }
683 $xfl = substr($data,8,1); 683 }
684 $os = substr($data,8,1); 684 return false;
685 $headerlen = 10; 685 }
686 $extralen = 0; 686
687 $extra = ""; 687 private function possibleUnsupportedType($url) {
688 if ($flags & 4) { 688 $path = @parse_url($url, PHP_URL_PATH);
689 // 2-byte length prefixed EXTRA data in header 689 if ($path && strpos($path, '.') !== false) {
690 if ($len - $headerlen - 2 < 8) { 690 $ext = strtolower(trim(pathinfo($path, PATHINFO_EXTENSION)));
691 return false; // invalid 691 return in_array($ext, $this->headerOnlyClues);
692 } 692 }
693 $extralen = unpack("v",substr($data,8,2)); 693 return false;
694 $extralen = $extralen[1]; 694 }
695 if ($len - $headerlen - 2 - $extralen < 8) { 695}
696 return false; // invalid 696
697 } 697// gzdecode from http://www.php.net/manual/en/function.gzdecode.php#82930
698 $extra = substr($data,10,$extralen); 698if (!function_exists('gzdecode')) {
699 $headerlen += 2 + $extralen; 699 function gzdecode($data,&$filename='',&$error='',$maxlength=null)
700 } 700 {
701 $filenamelen = 0; 701 $len = strlen($data);
702 $filename = ""; 702 if ($len < 18 || strcmp(substr($data,0,2),"\x1f\x8b")) {
703 if ($flags & 8) { 703 $error = "Not in GZIP format.";
704 // C-style string 704 return null; // Not GZIP format (See RFC 1952)
705 if ($len - $headerlen - 1 < 8) { 705 }
706 return false; // invalid 706 $method = ord(substr($data,2,1)); // Compression method
707 } 707 $flags = ord(substr($data,3,1)); // Flags
708 $filenamelen = strpos(substr($data,$headerlen),chr(0)); 708 if ($flags & 31 != $flags) {
709 if ($filenamelen === false || $len - $headerlen - $filenamelen - 1 < 8) { 709 $error = "Reserved bits not allowed.";
710 return false; // invalid 710 return null;
711 } 711 }
712 $filename = substr($data,$headerlen,$filenamelen); 712 // NOTE: $mtime may be negative (PHP integer limitations)
713 $headerlen += $filenamelen + 1; 713 $mtime = unpack("V", substr($data,4,4));
714 } 714 $mtime = $mtime[1];
715 $commentlen = 0; 715 $xfl = substr($data,8,1);
716 $comment = ""; 716 $os = substr($data,8,1);
717 if ($flags & 16) { 717 $headerlen = 10;
718 // C-style string COMMENT data in header 718 $extralen = 0;
719 if ($len - $headerlen - 1 < 8) { 719 $extra = "";
720 return false; // invalid 720 if ($flags & 4) {
721 } 721 // 2-byte length prefixed EXTRA data in header
722 $commentlen = strpos(substr($data,$headerlen),chr(0)); 722 if ($len - $headerlen - 2 < 8) {
723 if ($commentlen === false || $len - $headerlen - $commentlen - 1 < 8) { 723 return false; // invalid
724 return false; // Invalid header format 724 }
725 } 725 $extralen = unpack("v",substr($data,8,2));
726 $comment = substr($data,$headerlen,$commentlen); 726 $extralen = $extralen[1];
727 $headerlen += $commentlen + 1; 727 if ($len - $headerlen - 2 - $extralen < 8) {
728 } 728 return false; // invalid
729 $headercrc = ""; 729 }
730 if ($flags & 2) { 730 $extra = substr($data,10,$extralen);
731 // 2-bytes (lowest order) of CRC32 on header present 731 $headerlen += 2 + $extralen;
732 if ($len - $headerlen - 2 < 8) { 732 }
733 return false; // invalid 733 $filenamelen = 0;
734 } 734 $filename = "";
735 $calccrc = crc32(substr($data,0,$headerlen)) & 0xffff; 735 if ($flags & 8) {
736 $headercrc = unpack("v", substr($data,$headerlen,2)); 736 // C-style string
737 $headercrc = $headercrc[1]; 737 if ($len - $headerlen - 1 < 8) {
738 if ($headercrc != $calccrc) { 738 return false; // invalid
739 $error = "Header checksum failed."; 739 }
740 return false; // Bad header CRC 740 $filenamelen = strpos(substr($data,$headerlen),chr(0));
741 } 741 if ($filenamelen === false || $len - $headerlen - $filenamelen - 1 < 8) {
742 $headerlen += 2; 742 return false; // invalid
743 } 743 }
744 // GZIP FOOTER 744 $filename = substr($data,$headerlen,$filenamelen);
745 $datacrc = unpack("V",substr($data,-8,4)); 745 $headerlen += $filenamelen + 1;
746 $datacrc = sprintf('%u',$datacrc[1] & 0xFFFFFFFF); 746 }
747 $isize = unpack("V",substr($data,-4)); 747 $commentlen = 0;
748 $isize = $isize[1]; 748 $comment = "";
749 // decompression: 749 if ($flags & 16) {
750 $bodylen = $len-$headerlen-8; 750 // C-style string COMMENT data in header
751 if ($bodylen < 1) { 751 if ($len - $headerlen - 1 < 8) {
752 // IMPLEMENTATION BUG! 752 return false; // invalid
753 return null; 753 }
754 } 754 $commentlen = strpos(substr($data,$headerlen),chr(0));
755 $body = substr($data,$headerlen,$bodylen); 755 if ($commentlen === false || $len - $headerlen - $commentlen - 1 < 8) {
756 $data = ""; 756 return false; // Invalid header format
757 if ($bodylen > 0) { 757 }
758 switch ($method) { 758 $comment = substr($data,$headerlen,$commentlen);
759 case 8: 759 $headerlen += $commentlen + 1;
760 // Currently the only supported compression method: 760 }
761 $data = gzinflate($body,$maxlength); 761 $headercrc = "";
762 break; 762 if ($flags & 2) {
763 default: 763 // 2-bytes (lowest order) of CRC32 on header present
764 $error = "Unknown compression method."; 764 if ($len - $headerlen - 2 < 8) {
765 return false; 765 return false; // invalid
766 } 766 }
767 } // zero-byte body content is allowed 767 $calccrc = crc32(substr($data,0,$headerlen)) & 0xffff;
768 // Verifiy CRC32 768 $headercrc = unpack("v", substr($data,$headerlen,2));
769 $crc = sprintf("%u",crc32($data)); 769 $headercrc = $headercrc[1];
770 $crcOK = $crc == $datacrc; 770 if ($headercrc != $calccrc) {
771 $lenOK = $isize == strlen($data); 771 $error = "Header checksum failed.";
772 if (!$lenOK || !$crcOK) { 772 return false; // Bad header CRC
773 $error = ( $lenOK ? '' : 'Length check FAILED. ') . ( $crcOK ? '' : 'Checksum FAILED.'); 773 }
774 return false; 774 $headerlen += 2;
775 } 775 }
776 return $data; 776 // GZIP FOOTER
777 } 777 $datacrc = unpack("V",substr($data,-8,4));
778} 778 $datacrc = sprintf('%u',$datacrc[1] & 0xFFFFFFFF);
779?> \ No newline at end of file 779 $isize = unpack("V",substr($data,-4));
780 $isize = $isize[1];
781 // decompression:
782 $bodylen = $len-$headerlen-8;
783 if ($bodylen < 1) {
784 // IMPLEMENTATION BUG!
785 return null;
786 }
787 $body = substr($data,$headerlen,$bodylen);
788 $data = "";
789 if ($bodylen > 0) {
790 switch ($method) {
791 case 8:
792 // Currently the only supported compression method:
793 $data = gzinflate($body,$maxlength);
794 break;
795 default:
796 $error = "Unknown compression method.";
797 return false;
798 }
799 } // zero-byte body content is allowed
800 // Verifiy CRC32
801 $crc = sprintf("%u",crc32($data));
802 $crcOK = $crc == $datacrc;
803 $lenOK = $isize == strlen($data);
804 if (!$lenOK || !$crcOK) {
805 $error = ( $lenOK ? '' : 'Length check FAILED. ') . ( $crcOK ? '' : 'Checksum FAILED.');
806 return false;
807 }
808 return $data;
809 }
810} \ No newline at end of file
diff --git a/inc/3rdparty/libraries/humble-http-agent/SimplePie_HumbleHttpAgent.php b/inc/3rdparty/libraries/humble-http-agent/SimplePie_HumbleHttpAgent.php
index ecd46d5f..c524a1ee 100644
--- a/inc/3rdparty/libraries/humble-http-agent/SimplePie_HumbleHttpAgent.php
+++ b/inc/3rdparty/libraries/humble-http-agent/SimplePie_HumbleHttpAgent.php
@@ -1,79 +1,78 @@
1<?php 1<?php
2/** 2/**
3 * Humble HTTP Agent extension for SimplePie_File 3 * Humble HTTP Agent extension for SimplePie_File
4 * 4 *
5 * This class is designed to extend and override SimplePie_File 5 * This class is designed to extend and override SimplePie_File
6 * in order to prevent duplicate HTTP requests being sent out. 6 * in order to prevent duplicate HTTP requests being sent out.
7 * The idea is to initialise an instance of Humble HTTP Agent 7 * The idea is to initialise an instance of Humble HTTP Agent
8 * and attach it, to a static class variable, of this class. 8 * and attach it, to a static class variable, of this class.
9 * SimplePie will then automatically initialise this class 9 * SimplePie will then automatically initialise this class
10 * 10 *
11 * @date 2011-02-28 11 * @date 2011-02-28
12 */ 12 */
13 13
14class SimplePie_HumbleHttpAgent extends SimplePie_File 14class SimplePie_HumbleHttpAgent extends SimplePie_File
15{ 15{
16 protected static $agent; 16 protected static $agent;
17 var $url; 17 var $url;
18 var $useragent; 18 var $useragent;
19 var $success = true; 19 var $success = true;
20 var $headers = array(); 20 var $headers = array();
21 var $body; 21 var $body;
22 var $status_code; 22 var $status_code;
23 var $redirects = 0; 23 var $redirects = 0;
24 var $error; 24 var $error;
25 var $method = SIMPLEPIE_FILE_SOURCE_NONE; 25 var $method = SIMPLEPIE_FILE_SOURCE_NONE;
26 26
27 public static function set_agent(HumbleHttpAgent $agent) { 27 public static function set_agent(HumbleHttpAgent $agent) {
28 self::$agent = $agent; 28 self::$agent = $agent;
29 } 29 }
30 30
31 public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false) { 31 public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false) {
32 if (class_exists('idna_convert')) 32 if (class_exists('idna_convert'))
33 { 33 {
34 $idn = new idna_convert(); 34 $idn = new idna_convert();
35 $parsed = SimplePie_Misc::parse_url($url); 35 $parsed = SimplePie_Misc::parse_url($url);
36 $url = SimplePie_Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']); 36 $url = SimplePie_Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']);
37 } 37 }
38 $this->url = $url; 38 $this->url = $url;
39 $this->useragent = $useragent; 39 $this->useragent = $useragent;
40 if (preg_match('/^http(s)?:\/\//i', $url)) 40 if (preg_match('/^http(s)?:\/\//i', $url))
41 { 41 {
42 if (!is_array($headers)) 42 if (!is_array($headers))
43 { 43 {
44 $headers = array(); 44 $headers = array();
45 } 45 }
46 $this->method = SIMPLEPIE_FILE_SOURCE_REMOTE | SIMPLEPIE_FILE_SOURCE_CURL; 46 $this->method = SIMPLEPIE_FILE_SOURCE_REMOTE | SIMPLEPIE_FILE_SOURCE_CURL;
47 $headers2 = array(); 47 $headers2 = array();
48 foreach ($headers as $key => $value) { 48 foreach ($headers as $key => $value) {
49 $headers2[] = "$key: $value"; 49 $headers2[] = "$key: $value";
50 } 50 }
51 //TODO: allow for HTTP headers 51 //TODO: allow for HTTP headers
52 // curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2); 52 // curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2);
53 53
54 $response = self::$agent->get($url); 54 $response = self::$agent->get($url);
55 55
56 if ($response === false || !isset($response['status_code'])) { 56 if ($response === false || !isset($response['status_code'])) {
57 $this->error = 'failed to fetch URL'; 57 $this->error = 'failed to fetch URL';
58 $this->success = false; 58 $this->success = false;
59 } else { 59 } else {
60 // The extra lines at the end are there to satisfy SimplePie's HTTP parser. 60 // The extra lines at the end are there to satisfy SimplePie's HTTP parser.
61 // The class expects a full HTTP message, whereas we're giving it only 61 // The class expects a full HTTP message, whereas we're giving it only
62 // headers - the new lines indicate the start of the body. 62 // headers - the new lines indicate the start of the body.
63 $parser = new SimplePie_HTTP_Parser($response['headers']."\r\n\r\n"); 63 $parser = new SimplePie_HTTP_Parser($response['headers']."\r\n\r\n");
64 if ($parser->parse()) { 64 if ($parser->parse()) {
65 $this->headers = $parser->headers; 65 $this->headers = $parser->headers;
66 //$this->body = $parser->body; 66 //$this->body = $parser->body;
67 $this->body = $response['body']; 67 $this->body = $response['body'];
68 $this->status_code = $parser->status_code; 68 $this->status_code = $parser->status_code;
69 } 69 }
70 } 70 }
71 } 71 }
72 else 72 else
73 { 73 {
74 $this->error = 'invalid URL'; 74 $this->error = 'invalid URL';
75 $this->success = false; 75 $this->success = false;
76 } 76 }
77 } 77 }
78} 78} \ No newline at end of file
79?> \ No newline at end of file
diff --git a/inc/3rdparty/libraries/language-detect/LanguageDetect.php b/inc/3rdparty/libraries/language-detect/LanguageDetect.php
index 09b11546..382d869c 100644
--- a/inc/3rdparty/libraries/language-detect/LanguageDetect.php
+++ b/inc/3rdparty/libraries/language-detect/LanguageDetect.php
@@ -6,23 +6,24 @@
6 * Attempts to detect the language of a sample of text by correlating ranked 6 * Attempts to detect the language of a sample of text by correlating ranked
7 * 3-gram frequencies to a table of 3-gram frequencies of known languages. 7 * 3-gram frequencies to a table of 3-gram frequencies of known languages.
8 * 8 *
9 * Implements a version of a technique originally proposed by Cavnar & Trenkle 9 * Implements a version of a technique originally proposed by Cavnar & Trenkle
10 * (1994): "N-Gram-Based Text Categorization" 10 * (1994): "N-Gram-Based Text Categorization"
11 * 11 *
12 * PHP versions 4 and 5 12 * PHP version 5
13 * 13 *
14 * @category Text 14 * @category Text
15 * @package Text_LanguageDetect 15 * @package Text_LanguageDetect
16 * @author Nicholas Pisarro <infinityminusnine+pear@gmail.com> 16 * @author Nicholas Pisarro <infinityminusnine+pear@gmail.com>
17 * @copyright 2005-2006 Nicholas Pisarro 17 * @copyright 2005-2006 Nicholas Pisarro
18 * @license http://www.debian.org/misc/bsd.license BSD 18 * @license http://www.debian.org/misc/bsd.license BSD
19 * @version CVS: $Id: LanguageDetect.php,v 1.20 2008/07/01 02:09:15 taak Exp $ 19 * @version SVN: $Id: LanguageDetect.php 322353 2012-01-16 08:41:43Z cweiske $
20 * @link http://pear.php.net/package/Text_LanguageDetect/ 20 * @link http://pear.php.net/package/Text_LanguageDetect/
21 * @link http://langdetect.blogspot.com/ 21 * @link http://langdetect.blogspot.com/
22 */ 22 */
23 23
24//require_once 'PEAR.php'; 24require_once 'LanguageDetect/Exception.php';
25require_once 'Parser.php'; 25require_once 'LanguageDetect/Parser.php';
26require_once 'LanguageDetect/ISO639.php';
26 27
27/** 28/**
28 * Language detection class 29 * Language detection class
@@ -41,9 +42,10 @@ require_once 'Parser.php';
41 * 42 *
42 * echo "Supported languages:\n"; 43 * echo "Supported languages:\n";
43 * 44 *
44 * $langs = $l->getLanguages(); 45 * try {
45 * if (PEAR::isError($langs)) { 46 * $langs = $l->getLanguages();
46 * die($langs->getMessage()); 47 * } catch (Text_LanguageDetect_Exception $e) {
48 * die($e->getMessage());
47 * } 49 * }
48 * 50 *
49 * sort($langs); 51 * sort($langs);
@@ -54,38 +56,38 @@ require_once 'Parser.php';
54 * } 56 * }
55 * </code> 57 * </code>
56 * 58 *
57 * @category Text 59 * @category Text
58 * @package Text_LanguageDetect 60 * @package Text_LanguageDetect
59 * @author Nicholas Pisarro <infinityminusnine+pear@gmail.com> 61 * @author Nicholas Pisarro <infinityminusnine+pear@gmail.com>
60 * @copyright 2005 Nicholas Pisarro 62 * @copyright 2005 Nicholas Pisarro
61 * @license http://www.debian.org/misc/bsd.license BSD 63 * @license http://www.debian.org/misc/bsd.license BSD
62 * @version Release: @package_version@ 64 * @version Release: @package_version@
63 * @todo allow users to generate their own language models 65 * @link http://pear.php.net/package/Text_LanguageDetect/
66 * @todo allow users to generate their own language models
64 */ 67 */
65
66class Text_LanguageDetect 68class Text_LanguageDetect
67{ 69{
68 /** 70 /**
69 * The filename that stores the trigram data for the detector 71 * The filename that stores the trigram data for the detector
70 * 72 *
71 * If this value starts with a slash (/) or a dot (.) the value of 73 * If this value starts with a slash (/) or a dot (.) the value of
72 * $this->_data_dir will be ignored 74 * $this->_data_dir will be ignored
73 * 75 *
74 * @var string 76 * @var string
75 * @access private 77 * @access private
76 */ 78 */
77 var $_db_filename = './lang.dat'; 79 var $_db_filename = 'lang.dat';
78 80
79 /** 81 /**
80 * The filename that stores the unicode block definitions 82 * The filename that stores the unicode block definitions
81 * 83 *
82 * If this value starts with a slash (/) or a dot (.) the value of 84 * If this value starts with a slash (/) or a dot (.) the value of
83 * $this->_data_dir will be ignored 85 * $this->_data_dir will be ignored
84 * 86 *
85 * @var string 87 * @var string
86 * @access private 88 * @access private
87 */ 89 */
88 var $_unicode_db_filename = './unicode_blocks.dat'; 90 var $_unicode_db_filename = 'unicode_blocks.dat';
89 91
90 /** 92 /**
91 * The data directory 93 * The data directory
@@ -99,11 +101,8 @@ class Text_LanguageDetect
99 101
100 /** 102 /**
101 * The trigram data for comparison 103 * The trigram data for comparison
102 *
103 * Will be loaded on start from $this->_db_filename
104 * 104 *
105 * May be set to a PEAR_Error object if there is an error during its 105 * Will be loaded on start from $this->_db_filename
106 * initialization
107 * 106 *
108 * @var array 107 * @var array
109 * @access private 108 * @access private
@@ -120,7 +119,7 @@ class Text_LanguageDetect
120 119
121 /** 120 /**
122 * The size of the trigram data arrays 121 * The size of the trigram data arrays
123 * 122 *
124 * @var int 123 * @var int
125 * @access private 124 * @access private
126 */ 125 */
@@ -140,7 +139,7 @@ class Text_LanguageDetect
140 139
141 /** 140 /**
142 * Whether or not to simulate perl's Language::Guess exactly 141 * Whether or not to simulate perl's Language::Guess exactly
143 * 142 *
144 * @access private 143 * @access private
145 * @var bool 144 * @var bool
146 * @see setPerlCompatible() 145 * @see setPerlCompatible()
@@ -165,18 +164,24 @@ class Text_LanguageDetect
165 var $_clusters; 164 var $_clusters;
166 165
167 /** 166 /**
167 * Which type of "language names" are accepted and returned:
168 *
169 * 0 - language name ("english")
170 * 2 - 2-letter ISO 639-1 code ("en")
171 * 3 - 3-letter ISO 639-2 code ("eng")
172 */
173 var $_name_mode = 0;
174
175 /**
168 * Constructor 176 * Constructor
169 * 177 *
170 * Will attempt to load the language database. If it fails, you will get 178 * Will attempt to load the language database. If it fails, you will get
171 * a PEAR_Error object returned when you try to use detect() 179 * an exception.
172 *
173 */ 180 */
174 function Text_LanguageDetect($db=null, $unicode_db=null) 181 function __construct()
175 { 182 {
176 if (isset($db)) $this->_db_filename = $db;
177 if (isset($unicode_db)) $this->_unicode_db_filename = $unicode_db;
178
179 $data = $this->_readdb($this->_db_filename); 183 $data = $this->_readdb($this->_db_filename);
184 $this->_checkTrigram($data['trigram']);
180 $this->_lang_db = $data['trigram']; 185 $this->_lang_db = $data['trigram'];
181 186
182 if (isset($data['trigram-unicodemap'])) { 187 if (isset($data['trigram-unicodemap'])) {
@@ -186,29 +191,32 @@ class Text_LanguageDetect
186 // Not yet implemented: 191 // Not yet implemented:
187 if (isset($data['trigram-clusters'])) { 192 if (isset($data['trigram-clusters'])) {
188 $this->_clusters = $data['trigram-clusters']; 193 $this->_clusters = $data['trigram-clusters'];
189 } 194 }
190 } 195 }
191 196
192 /** 197 /**
193 * Returns the path to the location of the database 198 * Returns the path to the location of the database
194 * 199 *
195 * @access private 200 * @param string $fname File name to load
196 * @return string expected path to the language model database 201 *
202 * @return string expected path to the language model database
203 * @access private
197 */ 204 */
198 function _get_data_loc($fname) 205 function _get_data_loc($fname)
199 { 206 {
200 return $fname; 207 return dirname(__FILE__).'/'.$fname;
201 } 208 }
202 209
203 /** 210 /**
204 * Loads the language trigram database from filename 211 * Loads the language trigram database from filename
205 * 212 *
206 * Trigram datbase should be a serialize()'d array 213 * Trigram datbase should be a serialize()'d array
207 * 214 *
208 * @access private 215 * @param string $fname the filename where the data is stored
209 * @param string $fname the filename where the data is stored 216 *
210 * @return array the language model data 217 * @return array the language model data
211 * @throws PEAR_Error 218 * @throws Text_LanguageDetect_Exception
219 * @access private
212 */ 220 */
213 function _readdb($fname) 221 function _readdb($fname)
214 { 222 {
@@ -217,79 +225,74 @@ class Text_LanguageDetect
217 225
218 // input check 226 // input check
219 if (!file_exists($fname)) { 227 if (!file_exists($fname)) {
220 throw new Exception('Language database does not exist.'); 228 throw new Text_LanguageDetect_Exception(
229 'Language database does not exist: ' . $fname,
230 Text_LanguageDetect_Exception::DB_NOT_FOUND
231 );
221 } elseif (!is_readable($fname)) { 232 } elseif (!is_readable($fname)) {
222 throw new Exception('Language database is not readable.'); 233 throw new Text_LanguageDetect_Exception(
234 'Language database is not readable: ' . $fname,
235 Text_LanguageDetect_Exception::DB_NOT_READABLE
236 );
223 } 237 }
224 238
225 if (function_exists('file_get_contents')) { 239 return unserialize(file_get_contents($fname));
226 return unserialize(file_get_contents($fname));
227 } else {
228 // if you don't have file_get_contents(),
229 // then this is the next fastest way
230 ob_start();
231 readfile($fname);
232 $contents = ob_get_contents();
233 ob_end_clean();
234 return unserialize($contents);
235 }
236 } 240 }
237 241
238 242
239 /** 243 /**
240 * Checks if this object is ready to detect languages 244 * Checks if this object is ready to detect languages
241 * 245 *
242 * @access private 246 * @param array $trigram Trigram data from database
243 * @param mixed &$err error object to be returned by reference, if any 247 *
244 * @return bool true if no errors 248 * @return void
249 * @access private
245 */ 250 */
246 function _setup_ok(&$err) 251 function _checkTrigram($trigram)
247 { 252 {
248 if (!is_array($this->_lang_db)) { 253 if (!is_array($trigram)) {
249 if (ini_get('magic_quotes_runtime')) { 254 if (ini_get('magic_quotes_runtime')) {
250 throw new Exception('Error loading database. Try turning magic_quotes_runtime off.'); 255 throw new Text_LanguageDetect_Exception(
251 } else { 256 'Error loading database. Try turning magic_quotes_runtime off.',
252 throw new Exception('Language database is not an array.'); 257 Text_LanguageDetect_Exception::MAGIC_QUOTES
258 );
253 } 259 }
254 return false; 260 throw new Text_LanguageDetect_Exception(
255 261 'Language database is not an array.',
256 } elseif (empty($this->_lang_db)) { 262 Text_LanguageDetect_Exception::DB_NOT_ARRAY
257 throw new Exception('Language database has no elements.'); 263 );
258 return false; 264 } elseif (empty($trigram)) {
259 265 throw new Text_LanguageDetect_Exception(
260 } else { 266 'Language database has no elements.',
261 return true; 267 Text_LanguageDetect_Exception::DB_EMPTY
268 );
262 } 269 }
263 } 270 }
264 271
265 /** 272 /**
266 * Omits languages 273 * Omits languages
267 * 274 *
268 * Pass this function the name of or an array of names of 275 * Pass this function the name of or an array of names of
269 * languages that you don't want considered 276 * languages that you don't want considered
270 * 277 *
271 * If you're only expecting a limited set of languages, this can greatly 278 * If you're only expecting a limited set of languages, this can greatly
272 * speed up processing 279 * speed up processing
273 * 280 *
274 * @access public 281 * @param mixed $omit_list language name or array of names to omit
275 * @param mixed $omit_list language name or array of names to omit 282 * @param bool $include_only if true will include (rather than
276 * @param bool $include_only if true will include (rather than 283 * exclude) only those in the list
277 * exclude) only those in the list 284 *
278 * @return int number of languages successfully deleted 285 * @return int number of languages successfully deleted
279 * @throws PEAR_Error 286 * @throws Text_LanguageDetect_Exception
280 */ 287 */
281 function omitLanguages($omit_list, $include_only = false) 288 public function omitLanguages($omit_list, $include_only = false)
282 { 289 {
283
284 // setup check
285 if (!$this->_setup_ok($err)) {
286 return $err;
287 }
288
289 $deleted = 0; 290 $deleted = 0;
290 291
291 // deleting the given languages 292 $omit_list = $this->_convertFromNameMode($omit_list);
293
292 if (!$include_only) { 294 if (!$include_only) {
295 // deleting the given languages
293 if (!is_array($omit_list)) { 296 if (!is_array($omit_list)) {
294 $omit_list = strtolower($omit_list); // case desensitize 297 $omit_list = strtolower($omit_list); // case desensitize
295 if (isset($this->_lang_db[$omit_list])) { 298 if (isset($this->_lang_db[$omit_list])) {
@@ -301,12 +304,12 @@ class Text_LanguageDetect
301 if (isset($this->_lang_db[$omit_lang])) { 304 if (isset($this->_lang_db[$omit_lang])) {
302 unset($this->_lang_db[$omit_lang]); 305 unset($this->_lang_db[$omit_lang]);
303 $deleted++; 306 $deleted++;
304 } 307 }
305 } 308 }
306 } 309 }
307 310
308 // deleting all except the given languages
309 } else { 311 } else {
312 // deleting all except the given languages
310 if (!is_array($omit_list)) { 313 if (!is_array($omit_list)) {
311 $omit_list = array($omit_list); 314 $omit_list = array($omit_list);
312 } 315 }
@@ -327,7 +330,7 @@ class Text_LanguageDetect
327 // reset the cluster cache if the number of languages changes 330 // reset the cluster cache if the number of languages changes
328 // this will then have to be recalculated 331 // this will then have to be recalculated
329 if (isset($this->_clusters) && $deleted > 0) { 332 if (isset($this->_clusters) && $deleted > 0) {
330 unset($this->_clusters); 333 $this->_clusters = null;
331 } 334 }
332 335
333 return $deleted; 336 return $deleted;
@@ -339,49 +342,40 @@ class Text_LanguageDetect
339 * 342 *
340 * @access public 343 * @access public
341 * @return int the number of languages 344 * @return int the number of languages
342 * @throws PEAR_Error 345 * @throws Text_LanguageDetect_Exception
343 */ 346 */
344 function getLanguageCount() 347 function getLanguageCount()
345 { 348 {
346 if (!$this->_setup_ok($err)) { 349 return count($this->_lang_db);
347 return $err;
348 } else {
349 return count($this->_lang_db);
350 }
351 } 350 }
352 351
353 /** 352 /**
354 * Returns true if a given language exists 353 * Checks if the language with the given name exists in the database
355 * 354 *
356 * If passed an array of names, will return true only if all exist 355 * @param mixed $lang Language name or array of language names
357 * 356 *
358 * @access public 357 * @return bool true if language model exists
359 * @param mixed $lang language name or array of language names
360 * @return bool true if language model exists
361 * @throws PEAR_Error
362 */ 358 */
363 function languageExists($lang) 359 public function languageExists($lang)
364 { 360 {
365 if (!$this->_setup_ok($err)) { 361 $lang = $this->_convertFromNameMode($lang);
366 return $err;
367 } else {
368 // string
369 if (is_string($lang)) {
370 return isset($this->_lang_db[strtolower($lang)]);
371
372 // array
373 } elseif (is_array($lang)) {
374 foreach ($lang as $test_lang) {
375 if (!isset($this->_lang_db[strtolower($test_lang)])) {
376 return false;
377 }
378 }
379 return true;
380 362
381 // other (error) 363 if (is_string($lang)) {
382 } else { 364 return isset($this->_lang_db[strtolower($lang)]);
383 throw new Exception('Unknown type passed to languageExists()'); 365
366 } elseif (is_array($lang)) {
367 foreach ($lang as $test_lang) {
368 if (!isset($this->_lang_db[strtolower($test_lang)])) {
369 return false;
370 }
384 } 371 }
372 return true;
373
374 } else {
375 throw new Text_LanguageDetect_Exception(
376 'Unsupported parameter type passed to languageExists()',
377 Text_LanguageDetect_Exception::PARAM_TYPE
378 );
385 } 379 }
386 } 380 }
387 381
@@ -389,25 +383,24 @@ class Text_LanguageDetect
389 * Returns the list of detectable languages 383 * Returns the list of detectable languages
390 * 384 *
391 * @access public 385 * @access public
392 * @return array the names of the languages known to this object 386 * @return array the names of the languages known to this object<<<<<<<
393 * @throws PEAR_Error 387 * @throws Text_LanguageDetect_Exception
394 */ 388 */
395 function getLanguages() 389 function getLanguages()
396 { 390 {
397 if (!$this->_setup_ok($err)) { 391 return $this->_convertToNameMode(
398 return $err; 392 array_keys($this->_lang_db)
399 } else { 393 );
400 return array_keys($this->_lang_db);
401 }
402 } 394 }
403 395
404 /** 396 /**
405 * Make this object behave like Language::Guess 397 * Make this object behave like Language::Guess
406 * 398 *
407 * @access public 399 * @param bool $setting false to turn off perl compatibility
408 * @param bool $setting false to turn off perl compatibility 400 *
401 * @return void
409 */ 402 */
410 function setPerlCompatible($setting = true) 403 public function setPerlCompatible($setting = true)
411 { 404 {
412 if (is_bool($setting)) { // input check 405 if (is_bool($setting)) { // input check
413 $this->_perl_compatible = $setting; 406 $this->_perl_compatible = $setting;
@@ -422,6 +415,21 @@ class Text_LanguageDetect
422 } 415 }
423 416
424 /** 417 /**
418 * Sets the way how language names are accepted and returned.
419 *
420 * @param integer $name_mode One of the following modes:
421 * 0 - language name ("english")
422 * 2 - 2-letter ISO 639-1 code ("en")
423 * 3 - 3-letter ISO 639-2 code ("eng")
424 *
425 * @return void
426 */
427 function setNameMode($name_mode)
428 {
429 $this->_name_mode = $name_mode;
430 }
431
432 /**
425 * Whether to use unicode block ranges in detection 433 * Whether to use unicode block ranges in detection
426 * 434 *
427 * Should speed up most detections if turned on (detault is on). In some 435 * Should speed up most detections if turned on (detault is on). In some
@@ -429,10 +437,11 @@ class Text_LanguageDetect
429 * in languages that use latin scripts. In other cases it should speed up 437 * in languages that use latin scripts. In other cases it should speed up
430 * detection noticeably. 438 * detection noticeably.
431 * 439 *
432 * @access public 440 * @param bool $setting false to turn off
433 * @param bool $setting false to turn off 441 *
442 * @return void
434 */ 443 */
435 function useUnicodeBlocks($setting = true) 444 public function useUnicodeBlocks($setting = true)
436 { 445 {
437 if (is_bool($setting)) { 446 if (is_bool($setting)) {
438 $this->_use_unicode_narrowing = $setting; 447 $this->_use_unicode_narrowing = $setting;
@@ -442,15 +451,15 @@ class Text_LanguageDetect
442 /** 451 /**
443 * Converts a piece of text into trigrams 452 * Converts a piece of text into trigrams
444 * 453 *
445 * Superceded by the Text_LanguageDetect_Parser class 454 * @param string $text text to convert
446 * 455 *
447 * @access private 456 * @return array array of trigram frequencies
448 * @param string $text text to convert 457 * @access private
449 * @return array array of trigram frequencies 458 * @deprecated Superceded by the Text_LanguageDetect_Parser class
450 */ 459 */
451 function _trigram($text) 460 function _trigram($text)
452 { 461 {
453 $s = new Text_LanguageDetect_Parser($text, $this->_db_filename, $this->_unicode_db_filename); 462 $s = new Text_LanguageDetect_Parser($text);
454 $s->prepareTrigram(); 463 $s->prepareTrigram();
455 $s->prepareUnicode(false); 464 $s->prepareUnicode(false);
456 $s->setPadStart(!$this->_perl_compatible); 465 $s->setPadStart(!$this->_perl_compatible);
@@ -463,11 +472,12 @@ class Text_LanguageDetect
463 * 472 *
464 * Thresholds (cuts off) the list at $this->_threshold 473 * Thresholds (cuts off) the list at $this->_threshold
465 * 474 *
466 * @access protected 475 * @param array $arr array of trigram
467 * @param array $arr array of trgram 476 *
468 * @return array ranks of trigrams 477 * @return array ranks of trigrams
478 * @access protected
469 */ 479 */
470 function _arr_rank(&$arr) 480 function _arr_rank($arr)
471 { 481 {
472 482
473 // sorts alphabetically first as a standard way of breaking rank ties 483 // sorts alphabetically first as a standard way of breaking rank ties
@@ -494,14 +504,17 @@ class Text_LanguageDetect
494 504
495 /** 505 /**
496 * Sorts an array by value breaking ties alphabetically 506 * Sorts an array by value breaking ties alphabetically
497 * 507 *
498 * @access private 508 * @param array &$arr the array to sort
499 * @param array &$arr the array to sort 509 *
510 * @return void
511 * @access private
500 */ 512 */
501 function _bub_sort(&$arr) 513 function _bub_sort(&$arr)
502 { 514 {
503 // should do the same as this perl statement: 515 // should do the same as this perl statement:
504 // sort { $trigrams{$b} == $trigrams{$a} ? $a cmp $b : $trigrams{$b} <=> $trigrams{$a} } 516 // sort { $trigrams{$b} == $trigrams{$a}
517 // ? $a cmp $b : $trigrams{$b} <=> $trigrams{$a} }
505 518
506 // needs to sort by both key and value at once 519 // needs to sort by both key and value at once
507 // using the key to break ties for the value 520 // using the key to break ties for the value
@@ -528,13 +541,14 @@ class Text_LanguageDetect
528 /** 541 /**
529 * Sort function used by bubble sort 542 * Sort function used by bubble sort
530 * 543 *
531 * Callback function for usort(). 544 * Callback function for usort().
532 * 545 *
533 * @access private 546 * @param array $a first param passed by usort()
534 * @param array first param passed by usort() 547 * @param array $b second param passed by usort()
535 * @param array second param passed by usort() 548 *
536 * @return int 1 if $a is greater, -1 if not 549 * @return int 1 if $a is greater, -1 if not
537 * @see _bub_sort() 550 * @see _bub_sort()
551 * @access private
538 */ 552 */
539 function _sort_func($a, $b) 553 function _sort_func($a, $b)
540 { 554 {
@@ -542,12 +556,12 @@ class Text_LanguageDetect
542 list($a_key, $a_value) = $a; 556 list($a_key, $a_value) = $a;
543 list($b_key, $b_value) = $b; 557 list($b_key, $b_value) = $b;
544 558
545 // if the values are the same, break ties using the key
546 if ($a_value == $b_value) { 559 if ($a_value == $b_value) {
560 // if the values are the same, break ties using the key
547 return strcmp($a_key, $b_key); 561 return strcmp($a_key, $b_key);
548 562
549 // if not, just sort normally
550 } else { 563 } else {
564 // if not, just sort normally
551 if ($a_value > $b_value) { 565 if ($a_value > $b_value) {
552 return -1; 566 return -1;
553 } else { 567 } else {
@@ -559,23 +573,24 @@ class Text_LanguageDetect
559 } 573 }
560 574
561 /** 575 /**
562 * Calculates a linear rank-order distance statistic between two sets of 576 * Calculates a linear rank-order distance statistic between two sets of
563 * ranked trigrams 577 * ranked trigrams
564 * 578 *
565 * Sums the differences in rank for each trigram. If the trigram does not 579 * Sums the differences in rank for each trigram. If the trigram does not
566 * appear in both, consider it a difference of $this->_threshold. 580 * appear in both, consider it a difference of $this->_threshold.
567 * 581 *
568 * This distance measure was proposed by Cavnar & Trenkle (1994). Despite 582 * This distance measure was proposed by Cavnar & Trenkle (1994). Despite
569 * its simplicity it has been shown to be highly accurate for language 583 * its simplicity it has been shown to be highly accurate for language
570 * identification tasks. 584 * identification tasks.
571 * 585 *
572 * @access private 586 * @param array $arr1 the reference set of trigram ranks
573 * @param array $arr1 the reference set of trigram ranks 587 * @param array $arr2 the target set of trigram ranks
574 * @param array $arr2 the target set of trigram ranks 588 *
575 * @return int the sum of the differences between the ranks of 589 * @return int the sum of the differences between the ranks of
576 * the two trigram sets 590 * the two trigram sets
591 * @access private
577 */ 592 */
578 function _distance(&$arr1, &$arr2) 593 function _distance($arr1, $arr2)
579 { 594 {
580 $sumdist = 0; 595 $sumdist = 0;
581 596
@@ -598,14 +613,15 @@ class Text_LanguageDetect
598 613
599 /** 614 /**
600 * Normalizes the score returned by _distance() 615 * Normalizes the score returned by _distance()
601 * 616 *
602 * Different if perl compatible or not 617 * Different if perl compatible or not
603 * 618 *
604 * @access private 619 * @param int $score the score from _distance()
605 * @param int $score the score from _distance() 620 * @param int $base_count the number of trigrams being considered
606 * @param int $base_count the number of trigrams being considered 621 *
607 * @return float the normalized score 622 * @return float the normalized score
608 * @see _distance() 623 * @see _distance()
624 * @access private
609 */ 625 */
610 function _normalize_score($score, $base_count = null) 626 function _normalize_score($score, $base_count = null)
611 { 627 {
@@ -630,29 +646,24 @@ class Text_LanguageDetect
630 * 646 *
631 * If perl compatible, the score is 300-0, 0 being most similar. 647 * If perl compatible, the score is 300-0, 0 being most similar.
632 * Otherwise, it's 0-1 with 1 being most similar. 648 * Otherwise, it's 0-1 with 1 being most similar.
633 * 649 *
634 * The $sample text should be at least a few sentences in length; 650 * The $sample text should be at least a few sentences in length;
635 * should be ascii-7 or utf8 encoded, if another and the mbstring extension 651 * should be ascii-7 or utf8 encoded, if another and the mbstring extension
636 * is present it will try to detect and convert. However, experience has 652 * is present it will try to detect and convert. However, experience has
637 * shown that mb_detect_encoding() *does not work very well* with at least 653 * shown that mb_detect_encoding() *does not work very well* with at least
638 * some types of encoding. 654 * some types of encoding.
639 * 655 *
640 * @access public 656 * @param string $sample a sample of text to compare.
641 * @param string $sample a sample of text to compare. 657 * @param int $limit if specified, return an array of the most likely
642 * @param int $limit if specified, return an array of the most likely 658 * $limit languages and their scores.
643 * $limit languages and their scores. 659 *
644 * @return mixed sorted array of language scores, blank array if no 660 * @return mixed sorted array of language scores, blank array if no
645 * useable text was found, or PEAR_Error if error 661 * useable text was found
646 * with the object setup 662 * @see _distance()
647 * @see _distance() 663 * @throws Text_LanguageDetect_Exception
648 * @throws PEAR_Error
649 */ 664 */
650 function detect($sample, $limit = 0) 665 public function detect($sample, $limit = 0)
651 { 666 {
652 if (!$this->_setup_ok($err)) {
653 return $err;
654 }
655
656 // input check 667 // input check
657 if (!Text_LanguageDetect_Parser::validateString($sample)) { 668 if (!Text_LanguageDetect_Parser::validateString($sample)) {
658 return array(); 669 return array();
@@ -660,36 +671,27 @@ class Text_LanguageDetect
660 671
661 // check char encoding 672 // check char encoding
662 // (only if mbstring extension is compiled and PHP > 4.0.6) 673 // (only if mbstring extension is compiled and PHP > 4.0.6)
663 if (function_exists('mb_detect_encoding') 674 if (function_exists('mb_detect_encoding')
664 && function_exists('mb_convert_encoding')) { 675 && function_exists('mb_convert_encoding')
665 676 ) {
666 // mb_detect_encoding isn't very reliable, to say the least 677 // mb_detect_encoding isn't very reliable, to say the least
667 // detection should still work with a sufficient sample of ascii characters 678 // detection should still work with a sufficient sample
679 // of ascii characters
668 $encoding = mb_detect_encoding($sample); 680 $encoding = mb_detect_encoding($sample);
669 681
670 // mb_detect_encoding() will return FALSE if detection fails 682 // mb_detect_encoding() will return FALSE if detection fails
671 // don't attempt conversion if that's the case 683 // don't attempt conversion if that's the case
672 if ($encoding != 'ASCII' && $encoding != 'UTF-8' && $encoding !== false) { 684 if ($encoding != 'ASCII' && $encoding != 'UTF-8'
673 685 && $encoding !== false
674 if (function_exists('mb_list_encodings')) { 686 ) {
675 687 // verify the encoding exists in mb_list_encodings
676 // verify the encoding exists in mb_list_encodings 688 if (in_array($encoding, mb_list_encodings())) {
677 if (in_array($encoding, mb_list_encodings())) { 689 $sample = mb_convert_encoding($sample, 'UTF-8', $encoding);
678 $sample = mb_convert_encoding($sample, 'UTF-8', $encoding);
679 }
680
681 // if the previous condition failed:
682 // somehow we detected an encoding that also we don't support
683
684 } else {
685 // php 4 doesnt have mb_list_encodings()
686 // so attempt with error suppression
687 $sample = @mb_convert_encoding($sample, 'UTF-8', $encoding);
688 } 690 }
689 } 691 }
690 } 692 }
691 693
692 $sample_obj = new Text_LanguageDetect_Parser($sample, $this->_db_filename, $this->_unicode_db_filename); 694 $sample_obj = new Text_LanguageDetect_Parser($sample);
693 $sample_obj->prepareTrigram(); 695 $sample_obj->prepareTrigram();
694 if ($this->_use_unicode_narrowing) { 696 if ($this->_use_unicode_narrowing) {
695 $sample_obj->prepareUnicode(); 697 $sample_obj->prepareUnicode();
@@ -713,7 +715,10 @@ class Text_LanguageDetect
713 if (is_array($blocks)) { 715 if (is_array($blocks)) {
714 $present_blocks = array_keys($blocks); 716 $present_blocks = array_keys($blocks);
715 } else { 717 } else {
716 throw new Exception('Error during block detection'); 718 throw new Text_LanguageDetect_Exception(
719 'Error during block detection',
720 Text_LanguageDetect_Exception::BLOCK_DETECTION
721 );
717 } 722 }
718 723
719 $possible_langs = array(); 724 $possible_langs = array();
@@ -731,30 +736,30 @@ class Text_LanguageDetect
731 } 736 }
732 737
733 // could also try an intersect operation rather than a union 738 // could also try an intersect operation rather than a union
734 // in other words, choose languages whose trigrams contain 739 // in other words, choose languages whose trigrams contain
735 // ALL of the unicode blocks found in this sample 740 // ALL of the unicode blocks found in this sample
736 // would improve speed but would be completely thrown off by an 741 // would improve speed but would be completely thrown off by an
737 // unexpected character, like an umlaut appearing in english text 742 // unexpected character, like an umlaut appearing in english text
738 743
739 $possible_langs = array_intersect( 744 $possible_langs = array_intersect(
740 array_keys($this->_lang_db), 745 array_keys($this->_lang_db),
741 array_unique($possible_langs) 746 array_unique($possible_langs)
742 ); 747 );
743 748
744 // needs to intersect it with the keys of _lang_db in case 749 // needs to intersect it with the keys of _lang_db in case
745 // languages have been omitted 750 // languages have been omitted
746 751
747 // or just try 'em all
748 } else { 752 } else {
753 // or just try 'em all
749 $possible_langs = array_keys($this->_lang_db); 754 $possible_langs = array_keys($this->_lang_db);
750 } 755 }
751 756
752 757
753 foreach ($possible_langs as $lang) { 758 foreach ($possible_langs as $lang) {
754 $scores[$lang] = 759 $scores[$lang] = $this->_normalize_score(
755 $this->_normalize_score( 760 $this->_distance($this->_lang_db[$lang], $trigram_freqs),
756 $this->_distance($this->_lang_db[$lang], $trigram_freqs), 761 $trigram_count
757 $trigram_count); 762 );
758 } 763 }
759 764
760 unset($sample_obj); 765 unset($sample_obj);
@@ -772,7 +777,6 @@ class Text_LanguageDetect
772 $limited_scores = array(); 777 $limited_scores = array();
773 778
774 $i = 0; 779 $i = 0;
775
776 foreach ($scores as $key => $value) { 780 foreach ($scores as $key => $value) {
777 if ($i++ >= $limit) { 781 if ($i++ >= $limit) {
778 break; 782 break;
@@ -781,9 +785,9 @@ class Text_LanguageDetect
781 $limited_scores[$key] = $value; 785 $limited_scores[$key] = $value;
782 } 786 }
783 787
784 return $limited_scores; 788 return $this->_convertToNameMode($limited_scores, true);
785 } else { 789 } else {
786 return $scores; 790 return $this->_convertToNameMode($scores, true);
787 } 791 }
788 } 792 }
789 793
@@ -791,35 +795,33 @@ class Text_LanguageDetect
791 * Returns only the most similar language to the text sample 795 * Returns only the most similar language to the text sample
792 * 796 *
793 * Calls $this->detect() and returns only the top result 797 * Calls $this->detect() and returns only the top result
794 * 798 *
795 * @access public 799 * @param string $sample text to detect the language of
796 * @param string $sample text to detect the language of 800 *
797 * @return string the name of the most likely language 801 * @return string the name of the most likely language
798 * or null if no language is similar 802 * or null if no language is similar
799 * @see detect() 803 * @see detect()
800 * @throws PEAR_Error 804 * @throws Text_LanguageDetect_Exception
801 */ 805 */
802 function detectSimple($sample) 806 public function detectSimple($sample)
803 { 807 {
804 $scores = $this->detect($sample, 1); 808 $scores = $this->detect($sample, 1);
805 809
806 // if top language has the maximum possible score, 810 // if top language has the maximum possible score,
807 // then the top score will have been picked at random 811 // then the top score will have been picked at random
808 if ( !is_array($scores) 812 if (!is_array($scores) || empty($scores)
809 || empty($scores) 813 || current($scores) == $this->_max_score
810 || current($scores) == $this->_max_score) { 814 ) {
811
812 return null; 815 return null;
813
814 } else { 816 } else {
815 return ucfirst(key($scores)); 817 return key($scores);
816 } 818 }
817 } 819 }
818 820
819 /** 821 /**
820 * Returns an array containing the most similar language and a confidence 822 * Returns an array containing the most similar language and a confidence
821 * rating 823 * rating
822 * 824 *
823 * Confidence is a simple measure calculated from the similarity score 825 * Confidence is a simple measure calculated from the similarity score
824 * minus the similarity score from the next most similar language 826 * minus the similarity score from the next most similar language
825 * divided by the highest possible score. Languages that have closely 827 * divided by the highest possible score. Languages that have closely
@@ -827,46 +829,43 @@ class Text_LanguageDetect
827 * confidence scores. 829 * confidence scores.
828 * 830 *
829 * The similarity score answers the question "How likely is the text the 831 * The similarity score answers the question "How likely is the text the
830 * returned language regardless of the other languages considered?" The 832 * returned language regardless of the other languages considered?" The
831 * confidence score is one way of answering the question "how likely is the 833 * confidence score is one way of answering the question "how likely is the
832 * text the detected language relative to the rest of the language model 834 * text the detected language relative to the rest of the language model
833 * set?" 835 * set?"
834 * 836 *
835 * To see how similar languages are a priori, see languageSimilarity() 837 * To see how similar languages are a priori, see languageSimilarity()
836 * 838 *
837 * @access public 839 * @param string $sample text for which language will be detected
838 * @param string $sample text for which language will be detected 840 *
839 * @return array most similar language, score and confidence rating 841 * @return array most similar language, score and confidence rating
840 * or null if no language is similar 842 * or null if no language is similar
841 * @see detect() 843 * @see detect()
842 * @throws PEAR_Error 844 * @throws Text_LanguageDetect_Exception
843 */ 845 */
844 function detectConfidence($sample) 846 public function detectConfidence($sample)
845 { 847 {
846 $scores = $this->detect($sample, 2); 848 $scores = $this->detect($sample, 2);
847 849
848 // if most similar language has the max score, it 850 // if most similar language has the max score, it
849 // will have been picked at random 851 // will have been picked at random
850 if ( !is_array($scores) 852 if (!is_array($scores) || empty($scores)
851 || empty($scores) 853 || current($scores) == $this->_max_score
852 || current($scores) == $this->_max_score) { 854 ) {
853
854 return null; 855 return null;
855 } 856 }
856 857
857 $arr['language'] = ucfirst(key($scores)); 858 $arr['language'] = key($scores);
858 $arr['similarity'] = current($scores); 859 $arr['similarity'] = current($scores);
859 if (next($scores) !== false) { // if false then no next element 860 if (next($scores) !== false) { // if false then no next element
860 // the goal is to return a higher value if the distance between 861 // the goal is to return a higher value if the distance between
861 // the similarity of the first score and the second score is high 862 // the similarity of the first score and the second score is high
862 863
863 if ($this->_perl_compatible) { 864 if ($this->_perl_compatible) {
864 865 $arr['confidence'] = (current($scores) - $arr['similarity'])
865 $arr['confidence'] = 866 / $this->_max_score;
866 (current($scores) - $arr['similarity']) / $this->_max_score;
867 867
868 } else { 868 } else {
869
870 $arr['confidence'] = $arr['similarity'] - current($scores); 869 $arr['confidence'] = $arr['similarity'] - current($scores);
871 870
872 } 871 }
@@ -882,32 +881,26 @@ class Text_LanguageDetect
882 * Returns the distribution of unicode blocks in a given utf8 string 881 * Returns the distribution of unicode blocks in a given utf8 string
883 * 882 *
884 * For the block name of a single char, use unicodeBlockName() 883 * For the block name of a single char, use unicodeBlockName()
885 * 884 *
886 * @access public 885 * @param string $str input string. Must be ascii or utf8
887 * @param string $str input string. Must be ascii or utf8 886 * @param bool $skip_symbols if true, skip ascii digits, symbols and
888 * @param bool $skip_symbols if true, skip ascii digits, symbols and 887 * non-printing characters. Includes spaces,
889 * non-printing characters. Includes spaces, 888 * newlines and common punctutation characters.
890 * newlines and common punctutation characters. 889 *
891 * @return array 890 * @return array
892 * @throws PEAR_Error 891 * @throws Text_LanguageDetect_Exception
893 */ 892 */
894 function detectUnicodeBlocks($str, $skip_symbols) 893 public function detectUnicodeBlocks($str, $skip_symbols)
895 { 894 {
896 // input check 895 $skip_symbols = (bool)$skip_symbols;
897 if (!is_bool($skip_symbols)) { 896 $str = (string)$str;
898 throw new Exception('Second parameter must be boolean');
899 }
900
901 if (!is_string($str)) {
902 throw new Exception('First parameter was not a string');
903 }
904 897
905 $sample_obj = new Text_LanguageDetect_Parser($str, $this->_db_filename, $this->_unicode_db_filename); 898 $sample_obj = new Text_LanguageDetect_Parser($str);
906 $sample_obj->prepareUnicode(); 899 $sample_obj->prepareUnicode();
907 $sample_obj->prepareTrigram(false); 900 $sample_obj->prepareTrigram(false);
908 $sample_obj->setUnicodeSkipSymbols($skip_symbols); 901 $sample_obj->setUnicodeSkipSymbols($skip_symbols);
909 $sample_obj->analyze(); 902 $sample_obj->analyze();
910 $blocks =& $sample_obj->getUnicodeBlocks(); 903 $blocks = $sample_obj->getUnicodeBlocks();
911 unset($sample_obj); 904 unset($sample_obj);
912 return $blocks; 905 return $blocks;
913 } 906 }
@@ -915,38 +908,37 @@ class Text_LanguageDetect
915 /** 908 /**
916 * Returns the block name for a given unicode value 909 * Returns the block name for a given unicode value
917 * 910 *
918 * If passed a string, will assume it is being passed a UTF8-formatted 911 * If passed a string, will assume it is being passed a UTF8-formatted
919 * character and will automatically convert. Otherwise it will assume it 912 * character and will automatically convert. Otherwise it will assume it
920 * is being passed a numeric unicode value. 913 * is being passed a numeric unicode value.
921 * 914 *
922 * Make sure input is of the correct type! 915 * Make sure input is of the correct type!
923 * 916 *
924 * @access public
925 * @param mixed $unicode unicode value or utf8 char 917 * @param mixed $unicode unicode value or utf8 char
918 *
926 * @return mixed the block name string or false if not found 919 * @return mixed the block name string or false if not found
927 * @throws PEAR_Error 920 * @throws Text_LanguageDetect_Exception
928 */ 921 */
929 function unicodeBlockName($unicode) { 922 public function unicodeBlockName($unicode)
923 {
930 if (is_string($unicode)) { 924 if (is_string($unicode)) {
931 // assume it is being passed a utf8 char, so convert it 925 // assume it is being passed a utf8 char, so convert it
932 926 if (self::utf8strlen($unicode) > 1) {
933 // input check 927 throw new Text_LanguageDetect_Exception(
934 if ($this->utf8strlen($unicode) > 1) { 928 'Pass a single char only to this method',
935 throw new Exception('Pass this function only a single char'); 929 Text_LanguageDetect_Exception::PARAM_TYPE
930 );
936 } 931 }
937
938 $unicode = $this->_utf8char2unicode($unicode); 932 $unicode = $this->_utf8char2unicode($unicode);
939 933
940 if ($unicode == -1) {
941 throw new Exception('Malformatted char');
942 }
943
944 // input check
945 } elseif (!is_int($unicode)) { 934 } elseif (!is_int($unicode)) {
946 throw new Exception('Input must be of type string or int.'); 935 throw new Text_LanguageDetect_Exception(
936 'Input must be of type string or int.',
937 Text_LanguageDetect_Exception::PARAM_TYPE
938 );
947 } 939 }
948 940
949 $blocks =& $this->_read_unicode_block_db(); 941 $blocks = $this->_read_unicode_block_db();
950 942
951 $result = $this->_unicode_block_name($unicode, $blocks); 943 $result = $this->_unicode_block_name($unicode, $blocks);
952 944
@@ -964,14 +956,17 @@ class Text_LanguageDetect
964 * the public interface for this function, which does input checks which 956 * the public interface for this function, which does input checks which
965 * this function omits for speed. 957 * this function omits for speed.
966 * 958 *
967 * @access protected 959 * @param int $unicode the unicode value
968 * @param int $unicode the unicode value 960 * @param array $blocks the block database
969 * @param array &$blocks the block database 961 * @param int $block_count the number of defined blocks in the database
970 * @param int $block_count the number of defined blocks in the database 962 *
971 * @see unicodeBlockName() 963 * @return mixed Block name, -1 if it failed
964 * @see unicodeBlockName()
965 * @access protected
972 */ 966 */
973 function _unicode_block_name($unicode, &$blocks, $block_count = -1) { 967 function _unicode_block_name($unicode, $blocks, $block_count = -1)
974 // for a reference, see 968 {
969 // for a reference, see
975 // http://www.unicode.org/Public/UNIDATA/Blocks.txt 970 // http://www.unicode.org/Public/UNIDATA/Blocks.txt
976 971
977 // assume that ascii characters are the most common 972 // assume that ascii characters are the most common
@@ -994,35 +989,36 @@ class Text_LanguageDetect
994 while ($low <= $high) { 989 while ($low <= $high) {
995 $mid = floor(($low + $high) / 2); 990 $mid = floor(($low + $high) / 2);
996 991
997 // if it's lower than the lower bound
998 if ($unicode < $blocks[$mid][0]) { 992 if ($unicode < $blocks[$mid][0]) {
993 // if it's lower than the lower bound
999 $high = $mid - 1; 994 $high = $mid - 1;
1000 995
1001 // if it's higher than the upper bound
1002 } elseif ($unicode > $blocks[$mid][1]) { 996 } elseif ($unicode > $blocks[$mid][1]) {
997 // if it's higher than the upper bound
1003 $low = $mid + 1; 998 $low = $mid + 1;
1004 999
1005 // found it
1006 } else { 1000 } else {
1001 // found it
1007 return $blocks[$mid]; 1002 return $blocks[$mid];
1008 } 1003 }
1009 } 1004 }
1010 1005
1011 // failed to find the block 1006 // failed to find the block
1012 return -1; 1007 return -1;
1013 1008
1014 // todo: differentiate when it's out of range or when it falls 1009 // todo: differentiate when it's out of range or when it falls
1015 // into an unassigned range? 1010 // into an unassigned range?
1016 } 1011 }
1017 1012
1018 /** 1013 /**
1019 * Brings up the unicode block database 1014 * Brings up the unicode block database
1020 * 1015 *
1021 * @access protected
1022 * @return array the database of unicode block definitions 1016 * @return array the database of unicode block definitions
1023 * @throws PEAR_Error 1017 * @throws Text_LanguageDetect_Exception
1018 * @access protected
1024 */ 1019 */
1025 function &_read_unicode_block_db() { 1020 function _read_unicode_block_db()
1021 {
1026 // since the unicode definitions are always going to be the same, 1022 // since the unicode definitions are always going to be the same,
1027 // might as well share the memory for the db with all other instances 1023 // might as well share the memory for the db with all other instances
1028 // of this class 1024 // of this class
@@ -1037,29 +1033,27 @@ class Text_LanguageDetect
1037 1033
1038 /** 1034 /**
1039 * Calculate the similarities between the language models 1035 * Calculate the similarities between the language models
1040 * 1036 *
1041 * Use this function to see how similar languages are to each other. 1037 * Use this function to see how similar languages are to each other.
1042 * 1038 *
1043 * If passed 2 language names, will return just those languages compared. 1039 * If passed 2 language names, will return just those languages compared.
1044 * If passed 1 language name, will return that language compared to 1040 * If passed 1 language name, will return that language compared to
1045 * all others. 1041 * all others.
1046 * If passed none, will return an array of every language model compared 1042 * If passed none, will return an array of every language model compared
1047 * to every other one. 1043 * to every other one.
1048 * 1044 *
1049 * @access public 1045 * @param string $lang1 the name of the first language to be compared
1050 * @param string $lang1 the name of the first language to be compared 1046 * @param string $lang2 the name of the second language to be compared
1051 * @param string $lang2 the name of the second language to be compared 1047 *
1052 * @return array scores of every language compared 1048 * @return array scores of every language compared
1053 * or the score of just the provided languages 1049 * or the score of just the provided languages
1054 * or null if one of the supplied languages does not exist 1050 * or null if one of the supplied languages does not exist
1055 * @throws PEAR_Error 1051 * @throws Text_LanguageDetect_Exception
1056 */ 1052 */
1057 function languageSimilarity($lang1 = null, $lang2 = null) 1053 public function languageSimilarity($lang1 = null, $lang2 = null)
1058 { 1054 {
1059 if (!$this->_setup_ok($err)) { 1055 $lang1 = $this->_convertFromNameMode($lang1);
1060 return $err; 1056 $lang2 = $this->_convertFromNameMode($lang2);
1061 }
1062
1063 if ($lang1 != null) { 1057 if ($lang1 != null) {
1064 $lang1 = strtolower($lang1); 1058 $lang1 = strtolower($lang1);
1065 1059
@@ -1069,12 +1063,8 @@ class Text_LanguageDetect
1069 } 1063 }
1070 1064
1071 if ($lang2 != null) { 1065 if ($lang2 != null) {
1072 1066 if (!isset($this->_lang_db[$lang2])) {
1073 // can't only set the second param 1067 // check if language model exists
1074 if ($lang1 == null) {
1075 return null;
1076 // check if language model exists
1077 } elseif (!isset($this->_lang_db[$lang2])) {
1078 return null; 1068 return null;
1079 } 1069 }
1080 1070
@@ -1088,14 +1078,15 @@ class Text_LanguageDetect
1088 ) 1078 )
1089 ); 1079 );
1090 1080
1091
1092 // compare just $lang1 to all languages
1093 } else { 1081 } else {
1082 // compare just $lang1 to all languages
1094 $return_arr = array(); 1083 $return_arr = array();
1095 foreach ($this->_lang_db as $key => $value) { 1084 foreach ($this->_lang_db as $key => $value) {
1096 if ($key != $lang1) { // don't compare a language to itself 1085 if ($key != $lang1) {
1086 // don't compare a language to itself
1097 $return_arr[$key] = $this->_normalize_score( 1087 $return_arr[$key] = $this->_normalize_score(
1098 $this->_distance($this->_lang_db[$lang1], $value)); 1088 $this->_distance($this->_lang_db[$lang1], $value)
1089 );
1099 } 1090 }
1100 } 1091 }
1101 asort($return_arr); 1092 asort($return_arr);
@@ -1104,30 +1095,27 @@ class Text_LanguageDetect
1104 } 1095 }
1105 1096
1106 1097
1107 // compare all languages to each other
1108 } else { 1098 } else {
1099 // compare all languages to each other
1109 $return_arr = array(); 1100 $return_arr = array();
1110 foreach (array_keys($this->_lang_db) as $lang1) { 1101 foreach (array_keys($this->_lang_db) as $lang1) {
1111 foreach (array_keys($this->_lang_db) as $lang2) { 1102 foreach (array_keys($this->_lang_db) as $lang2) {
1112
1113 // skip comparing languages to themselves 1103 // skip comparing languages to themselves
1114 if ($lang1 != $lang2) { 1104 if ($lang1 != $lang2) {
1115
1116 // don't re-calculate what's already been done
1117 if (isset($return_arr[$lang2][$lang1])) {
1118 1105
1119 $return_arr[$lang1][$lang2] = 1106 if (isset($return_arr[$lang2][$lang1])) {
1120 $return_arr[$lang2][$lang1]; 1107 // don't re-calculate what's already been done
1108 $return_arr[$lang1][$lang2]
1109 = $return_arr[$lang2][$lang1];
1121 1110
1122 // calculate
1123 } else { 1111 } else {
1124 1112 // calculate
1125 $return_arr[$lang1][$lang2] = 1113 $return_arr[$lang1][$lang2]
1126 $this->_normalize_score( 1114 = $this->_normalize_score(
1127 $this->_distance( 1115 $this->_distance(
1128 $this->_lang_db[$lang1], 1116 $this->_lang_db[$lang1],
1129 $this->_lang_db[$lang2] 1117 $this->_lang_db[$lang2]
1130 ) 1118 )
1131 ); 1119 );
1132 1120
1133 } 1121 }
@@ -1150,20 +1138,14 @@ class Text_LanguageDetect
1150 * 1138 *
1151 * @access public 1139 * @access public
1152 * @return array language cluster data 1140 * @return array language cluster data
1153 * @throws PEAR_Error 1141 * @throws Text_LanguageDetect_Exception
1154 * @see languageSimilarity() 1142 * @see languageSimilarity()
1155 * @deprecated this function will eventually be removed and placed into 1143 * @deprecated this function will eventually be removed and placed into
1156 * the model generation class 1144 * the model generation class
1157 */ 1145 */
1158 function clusterLanguages() 1146 function clusterLanguages()
1159 { 1147 {
1160 // todo: set the maximum number of clusters 1148 // todo: set the maximum number of clusters
1161
1162 // setup check
1163 if (!$this->_setup_ok($err)) {
1164 return $err;
1165 }
1166
1167 // return cached result, if any 1149 // return cached result, if any
1168 if (isset($this->_clusters)) { 1150 if (isset($this->_clusters)) {
1169 return $this->_clusters; 1151 return $this->_clusters;
@@ -1177,7 +1159,10 @@ class Text_LanguageDetect
1177 1159
1178 foreach ($langs as $lang) { 1160 foreach ($langs as $lang) {
1179 if (!isset($this->_lang_db[$lang])) { 1161 if (!isset($this->_lang_db[$lang])) {
1180 throw new Exception("missing $lang!\n"); 1162 throw new Text_LanguageDetect_Exception(
1163 "missing $lang!",
1164 Text_LanguageDetect_Exception::UNKNOWN_LANGUAGE
1165 );
1181 } 1166 }
1182 } 1167 }
1183 1168
@@ -1186,7 +1171,9 @@ class Text_LanguageDetect
1186 $langs[$lang1] = $lang1; 1171 $langs[$lang1] = $lang1;
1187 unset($langs[$old_key]); 1172 unset($langs[$old_key]);
1188 } 1173 }
1189 1174
1175 $result_data = $really_map = array();
1176
1190 $i = 0; 1177 $i = 0;
1191 while (count($langs) > 2 && $i++ < 200) { 1178 while (count($langs) > 2 && $i++ < 200) {
1192 $highest_score = -1; 1179 $highest_score = -1;
@@ -1194,18 +1181,22 @@ class Text_LanguageDetect
1194 $highest_key2 = ''; 1181 $highest_key2 = '';
1195 foreach ($langs as $lang1) { 1182 foreach ($langs as $lang1) {
1196 foreach ($langs as $lang2) { 1183 foreach ($langs as $lang2) {
1197 if ( $lang1 != $lang2 1184 if ($lang1 != $lang2
1198 && $arr[$lang1][$lang2] > $highest_score) { 1185 && $arr[$lang1][$lang2] > $highest_score
1186 ) {
1199 $highest_score = $arr[$lang1][$lang2]; 1187 $highest_score = $arr[$lang1][$lang2];
1200 $highest_key1 = $lang1; 1188 $highest_key1 = $lang1;
1201 $highest_key2 = $lang2; 1189 $highest_key2 = $lang2;
1202 } 1190 }
1203 } 1191 }
1204 } 1192 }
1205 1193
1206 if (!$highest_key1) { 1194 if (!$highest_key1) {
1207 // should not ever happen 1195 // should not ever happen
1208 throw new Exception("no highest key? (step: $i)"); 1196 throw new Text_LanguageDetect_Exception(
1197 "no highest key? (step: $i)",
1198 Text_LanguageDetect_Exception::NO_HIGHEST_KEY
1199 );
1209 } 1200 }
1210 1201
1211 if ($highest_score == 0) { 1202 if ($highest_score == 0) {
@@ -1217,7 +1208,7 @@ class Text_LanguageDetect
1217 $sum1 = array_sum($arr[$highest_key1]); 1208 $sum1 = array_sum($arr[$highest_key1]);
1218 $sum2 = array_sum($arr[$highest_key2]); 1209 $sum2 = array_sum($arr[$highest_key2]);
1219 1210
1220 // use the score for the one that is most similar to the rest of 1211 // use the score for the one that is most similar to the rest of
1221 // the field as the score for the group 1212 // the field as the score for the group
1222 // todo: could try averaging or "centroid" method instead 1213 // todo: could try averaging or "centroid" method instead
1223 // seems like that might make more sense 1214 // seems like that might make more sense
@@ -1248,7 +1239,7 @@ class Text_LanguageDetect
1248 $really_lang = $replaceme; 1239 $really_lang = $replaceme;
1249 while (isset($really_map[$really_lang])) { 1240 while (isset($really_map[$really_lang])) {
1250 $really_lang = $really_map[$really_lang]; 1241 $really_lang = $really_map[$really_lang];
1251 } 1242 }
1252 $really_map[$newkey] = $really_lang; 1243 $really_map[$newkey] = $really_lang;
1253 1244
1254 1245
@@ -1259,8 +1250,8 @@ class Text_LanguageDetect
1259 $arr[$key1][$newkey] = $arr[$key1][$key2]; 1250 $arr[$key1][$newkey] = $arr[$key1][$key2];
1260 unset($arr[$key1][$key2]); 1251 unset($arr[$key1][$key2]);
1261 // replacing $arr[$key1][$key2] with $arr[$key1][$newkey] 1252 // replacing $arr[$key1][$key2] with $arr[$key1][$newkey]
1262 } 1253 }
1263 1254
1264 if ($key1 == $replaceme) { 1255 if ($key1 == $replaceme) {
1265 $arr[$newkey][$key2] = $arr[$key1][$key2]; 1256 $arr[$newkey][$key2] = $arr[$key1][$key2];
1266 unset($arr[$key1][$key2]); 1257 unset($arr[$key1][$key2]);
@@ -1273,7 +1264,7 @@ class Text_LanguageDetect
1273 } 1264 }
1274 } 1265 }
1275 } 1266 }
1276 1267
1277 1268
1278 unset($langs[$highest_key1]); 1269 unset($langs[$highest_key1]);
1279 unset($langs[$highest_key2]); 1270 unset($langs[$highest_key2]);
@@ -1293,7 +1284,7 @@ class Text_LanguageDetect
1293 } 1284 }
1294 1285
1295 $return_val = array( 1286 $return_val = array(
1296 'open_forks' => $langs, 1287 'open_forks' => $langs,
1297 // the top level of clusters 1288 // the top level of clusters
1298 // clusters that are mutually exclusive 1289 // clusters that are mutually exclusive
1299 // or specified by a specific maximum 1290 // or specified by a specific maximum
@@ -1323,11 +1314,11 @@ class Text_LanguageDetect
1323 * use, and it may disappear or its functionality may change in future 1314 * use, and it may disappear or its functionality may change in future
1324 * releases without notice. 1315 * releases without notice.
1325 * 1316 *
1326 * This compares the sample text to top the top level of clusters. If the 1317 * This compares the sample text to top the top level of clusters. If the
1327 * sample is similar to the cluster it will drop down and compare it to the 1318 * sample is similar to the cluster it will drop down and compare it to the
1328 * languages in the cluster, and so on until it hits a leaf node. 1319 * languages in the cluster, and so on until it hits a leaf node.
1329 * 1320 *
1330 * this should find the language in considerably fewer compares 1321 * this should find the language in considerably fewer compares
1331 * (the equivalent of a binary search), however clusterLanguages() is costly 1322 * (the equivalent of a binary search), however clusterLanguages() is costly
1332 * and the loss of accuracy from this technique is significant. 1323 * and the loss of accuracy from this technique is significant.
1333 * 1324 *
@@ -1337,15 +1328,14 @@ class Text_LanguageDetect
1337 * was very large, however in such cases some method of Bayesian inference 1328 * was very large, however in such cases some method of Bayesian inference
1338 * might be more helpful. 1329 * might be more helpful.
1339 * 1330 *
1340 * @see clusterLanguages() 1331 * @param string $str input string
1341 * @access public 1332 *
1342 * @param string $str input string 1333 * @return array language scores (only those compared)
1343 * @return array language scores (only those compared) 1334 * @throws Text_LanguageDetect_Exception
1344 * @throws PEAR_Error 1335 * @see clusterLanguages()
1345 */ 1336 */
1346 function clusteredSearch($str) 1337 public function clusteredSearch($str)
1347 { 1338 {
1348
1349 // input check 1339 // input check
1350 if (!Text_LanguageDetect_Parser::validateString($str)) { 1340 if (!Text_LanguageDetect_Parser::validateString($str)) {
1351 return array(); 1341 return array();
@@ -1359,7 +1349,7 @@ class Text_LanguageDetect
1359 $dendogram_data = $result['fork_data']; 1349 $dendogram_data = $result['fork_data'];
1360 $dendogram_alias = $result['name_map']; 1350 $dendogram_alias = $result['name_map'];
1361 1351
1362 $sample_obj = new Text_LanguageDetect_Parser($str, $this->_db_filename, $this->_unicode_db_filename); 1352 $sample_obj = new Text_LanguageDetect_Parser($str);
1363 $sample_obj->prepareTrigram(); 1353 $sample_obj->prepareTrigram();
1364 $sample_obj->setPadStart(!$this->_perl_compatible); 1354 $sample_obj->setPadStart(!$this->_perl_compatible);
1365 $sample_obj->analyze(); 1355 $sample_obj->analyze();
@@ -1372,7 +1362,7 @@ class Text_LanguageDetect
1372 } 1362 }
1373 1363
1374 $i = 0; // counts the number of steps 1364 $i = 0; // counts the number of steps
1375 1365
1376 foreach ($dendogram_start as $lang) { 1366 foreach ($dendogram_start as $lang) {
1377 if (isset($dendogram_alias[$lang])) { 1367 if (isset($dendogram_alias[$lang])) {
1378 $lang_key = $dendogram_alias[$lang]; 1368 $lang_key = $dendogram_alias[$lang];
@@ -1382,7 +1372,8 @@ class Text_LanguageDetect
1382 1372
1383 $scores[$lang] = $this->_normalize_score( 1373 $scores[$lang] = $this->_normalize_score(
1384 $this->_distance($this->_lang_db[$lang_key], $sample_result), 1374 $this->_distance($this->_lang_db[$lang_key], $sample_result),
1385 $sample_count); 1375 $sample_count
1376 );
1386 1377
1387 $i++; 1378 $i++;
1388 } 1379 }
@@ -1411,7 +1402,8 @@ class Text_LanguageDetect
1411 1402
1412 $scores[$lang] = $this->_normalize_score( 1403 $scores[$lang] = $this->_normalize_score(
1413 $this->_distance($this->_lang_db[$lang_key], $sample_result), 1404 $this->_distance($this->_lang_db[$lang_key], $sample_result),
1414 $sample_count); 1405 $sample_count
1406 );
1415 1407
1416 //todo: does not need to do same comparison again 1408 //todo: does not need to do same comparison again
1417 } 1409 }
@@ -1428,8 +1420,8 @@ class Text_LanguageDetect
1428 1420
1429 $diff = $scores[$cur_key] - $scores[$loser_key]; 1421 $diff = $scores[$cur_key] - $scores[$loser_key];
1430 1422
1431 // $cur_key ({$dendogram_alias[$cur_key]}) wins 1423 // $cur_key ({$dendogram_alias[$cur_key]}) wins
1432 // over $loser_key ({$dendogram_alias[$loser_key]}) 1424 // over $loser_key ({$dendogram_alias[$loser_key]})
1433 // with a difference of $diff 1425 // with a difference of $diff
1434 } 1426 }
1435 1427
@@ -1439,9 +1431,9 @@ class Text_LanguageDetect
1439 // which paths the algorithm decided to take along the tree 1431 // which paths the algorithm decided to take along the tree
1440 1432
1441 // but sometimes the last item is only the second highest 1433 // but sometimes the last item is only the second highest
1442 if ( ($this->_perl_compatible && (end($scores) > prev($scores))) 1434 if (($this->_perl_compatible && (end($scores) > prev($scores)))
1443 || (!$this->_perl_compatible && (end($scores) < prev($scores)))) { 1435 || (!$this->_perl_compatible && (end($scores) < prev($scores)))
1444 1436 ) {
1445 $real_last_score = current($scores); 1437 $real_last_score = current($scores);
1446 $real_last_key = key($scores); 1438 $real_last_key = key($scores);
1447 1439
@@ -1449,7 +1441,7 @@ class Text_LanguageDetect
1449 unset($scores[$real_last_key]); 1441 unset($scores[$real_last_key]);
1450 $scores[$real_last_key] = $real_last_score; 1442 $scores[$real_last_key] = $real_last_score;
1451 } 1443 }
1452 1444
1453 1445
1454 if (!$this->_perl_compatible) { 1446 if (!$this->_perl_compatible) {
1455 $scores = array_reverse($scores, true); 1447 $scores = array_reverse($scores, true);
@@ -1464,12 +1456,11 @@ class Text_LanguageDetect
1464 * 1456 *
1465 * Returns the numbers of characters (not bytes) in a utf8 string 1457 * Returns the numbers of characters (not bytes) in a utf8 string
1466 * 1458 *
1467 * @static 1459 * @param string $str string to get the length of
1468 * @access public 1460 *
1469 * @param string $str string to get the length of 1461 * @return int number of chars
1470 * @return int number of chars
1471 */ 1462 */
1472 function utf8strlen($str) 1463 public static function utf8strlen($str)
1473 { 1464 {
1474 // utf8_decode() will convert unknown chars to '?', which is actually 1465 // utf8_decode() will convert unknown chars to '?', which is actually
1475 // ideal for counting. 1466 // ideal for counting.
@@ -1482,53 +1473,45 @@ class Text_LanguageDetect
1482 /** 1473 /**
1483 * Returns the unicode value of a utf8 char 1474 * Returns the unicode value of a utf8 char
1484 * 1475 *
1485 * @access protected 1476 * @param string $char a utf8 (possibly multi-byte) char
1486 * @param string $char a utf8 (possibly multi-byte) char 1477 *
1487 * @return int unicode value or -1 if malformatted 1478 * @return int unicode value
1479 * @access protected
1480 * @link http://en.wikipedia.org/wiki/UTF-8
1488 */ 1481 */
1489 function _utf8char2unicode($char) { 1482 function _utf8char2unicode($char)
1490 1483 {
1491 // strlen() here will actually get the binary length of a single char 1484 // strlen() here will actually get the binary length of a single char
1492 switch (strlen($char)) { 1485 switch (strlen($char)) {
1493 1486 case 1:
1494 // for a reference, see http://en.wikipedia.org/wiki/UTF-8 1487 // normal ASCII-7 byte
1495 1488 // 0xxxxxxx --> 0xxxxxxx
1496 case 1: 1489 return ord($char{0});
1497 // normal ASCII-7 byte 1490
1498 // 0xxxxxxx --> 0xxxxxxx 1491 case 2:
1499 return ord($char{0}); 1492 // 2 byte unicode
1500 1493 // 110zzzzx 10xxxxxx --> 00000zzz zxxxxxxx
1501 case 2: 1494 $z = (ord($char{0}) & 0x000001F) << 6;
1502 // 2 byte unicode 1495 $x = (ord($char{1}) & 0x0000003F);
1503 // 110zzzzx 10xxxxxx --> 00000zzz zxxxxxxx 1496 return ($z | $x);
1504 $z = (ord($char{0}) & 0x000001F) << 6; 1497
1505 $x = (ord($char{1}) & 0x0000003F); 1498 case 3:
1506 1499 // 3 byte unicode
1507 return ($z | $x); 1500 // 1110zzzz 10zxxxxx 10xxxxxx --> zzzzzxxx xxxxxxxx
1508 1501 $z = (ord($char{0}) & 0x0000000F) << 12;
1509 case 3: 1502 $x1 = (ord($char{1}) & 0x0000003F) << 6;
1510 // 3 byte unicode 1503 $x2 = (ord($char{2}) & 0x0000003F);
1511 // 1110zzzz 10zxxxxx 10xxxxxx --> zzzzzxxx xxxxxxxx 1504 return ($z | $x1 | $x2);
1512 $z = (ord($char{0}) & 0x0000000F) << 12; 1505
1513 $x1 = (ord($char{1}) & 0x0000003F) << 6; 1506 case 4:
1514 $x2 = (ord($char{2}) & 0x0000003F); 1507 // 4 byte unicode
1515 1508 // 11110zzz 10zzxxxx 10xxxxxx 10xxxxxx -->
1516 return ($z | $x1 | $x2); 1509 // 000zzzzz xxxxxxxx xxxxxxxx
1517 1510 $z1 = (ord($char{0}) & 0x00000007) << 18;
1518 case 4: 1511 $z2 = (ord($char{1}) & 0x0000003F) << 12;
1519 // 4 byte unicode 1512 $x1 = (ord($char{2}) & 0x0000003F) << 6;
1520 // 11110zzz 10zzxxxx 10xxxxxx 10xxxxxx --> 1513 $x2 = (ord($char{3}) & 0x0000003F);
1521 // 000zzzzz xxxxxxxx xxxxxxxx 1514 return ($z1 | $z2 | $x1 | $x2);
1522 $z1 = (ord($char{0}) & 0x00000007) << 18;
1523 $z2 = (ord($char{1}) & 0x0000003F) << 12;
1524 $x1 = (ord($char{2}) & 0x0000003F) << 6;
1525 $x2 = (ord($char{3}) & 0x0000003F);
1526
1527 return ($z1 | $z2 | $x1 | $x2);
1528
1529 default:
1530 // error: malformatted char?
1531 return -1;
1532 } 1515 }
1533 } 1516 }
1534 1517
@@ -1536,18 +1519,18 @@ class Text_LanguageDetect
1536 * utf8-safe fast character iterator 1519 * utf8-safe fast character iterator
1537 * 1520 *
1538 * Will get the next character starting from $counter, which will then be 1521 * Will get the next character starting from $counter, which will then be
1539 * incremented. If a multi-byte char the bytes will be concatenated and 1522 * incremented. If a multi-byte char the bytes will be concatenated and
1540 * $counter will be incremeted by the number of bytes in the char. 1523 * $counter will be incremeted by the number of bytes in the char.
1541 * 1524 *
1542 * @access private 1525 * @param string $str the string being iterated over
1543 * @param string &$str the string being iterated over 1526 * @param int &$counter the iterator, will increment by reference
1544 * @param int &$counter the iterator, will increment by reference 1527 * @param bool $special_convert whether to do special conversions
1545 * @param bool $special_convert whether to do special conversions 1528 *
1546 * @return char the next (possibly multi-byte) char from $counter 1529 * @return char the next (possibly multi-byte) char from $counter
1530 * @access private
1547 */ 1531 */
1548 function _next_char(&$str, &$counter, $special_convert = false) 1532 static function _next_char($str, &$counter, $special_convert = false)
1549 { 1533 {
1550
1551 $char = $str{$counter++}; 1534 $char = $str{$counter++};
1552 $ord = ord($char); 1535 $ord = ord($char);
1553 1536
@@ -1556,7 +1539,6 @@ class Text_LanguageDetect
1556 1539
1557 // normal ascii one byte char 1540 // normal ascii one byte char
1558 if ($ord <= 127) { 1541 if ($ord <= 127) {
1559
1560 // special conversions needed for this package 1542 // special conversions needed for this package
1561 // (that only apply to regular ascii characters) 1543 // (that only apply to regular ascii characters)
1562 // lower case, and convert all non-alphanumeric characters 1544 // lower case, and convert all non-alphanumeric characters
@@ -1571,8 +1553,8 @@ class Text_LanguageDetect
1571 1553
1572 return $char; 1554 return $char;
1573 1555
1574 // multi-byte chars
1575 } elseif ($ord >> 5 == 6) { // two-byte char 1556 } elseif ($ord >> 5 == 6) { // two-byte char
1557 // multi-byte chars
1576 $nextchar = $str{$counter++}; // get next byte 1558 $nextchar = $str{$counter++}; // get next byte
1577 1559
1578 // lower-casing of non-ascii characters is still incomplete 1560 // lower-casing of non-ascii characters is still incomplete
@@ -1582,27 +1564,27 @@ class Text_LanguageDetect
1582 if ($ord == 195) { 1564 if ($ord == 195) {
1583 $nextord = ord($nextchar); 1565 $nextord = ord($nextchar);
1584 $nextord_adj = $nextord + 64; 1566 $nextord_adj = $nextord + 64;
1585 // for a reference, see 1567 // for a reference, see
1586 // http://www.ramsch.org/martin/uni/fmi-hp/iso8859-1.html 1568 // http://www.ramsch.org/martin/uni/fmi-hp/iso8859-1.html
1587 1569
1588 // &Agrave; - &THORN; but not &times; 1570 // &Agrave; - &THORN; but not &times;
1589 if ( $nextord_adj >= 192 1571 if ($nextord_adj >= 192
1590 && $nextord_adj <= 222 1572 && $nextord_adj <= 222
1591 && $nextord_adj != 215) { 1573 && $nextord_adj != 215
1592 1574 ) {
1593 $nextchar = chr($nextord + 32); 1575 $nextchar = chr($nextord + 32);
1594 } 1576 }
1595 1577
1596 // lower case cyrillic alphabet
1597 } elseif ($ord == 208) { 1578 } elseif ($ord == 208) {
1579 // lower case cyrillic alphabet
1598 $nextord = ord($nextchar); 1580 $nextord = ord($nextchar);
1599 // if A - Pe 1581 // if A - Pe
1600 if ($nextord >= 144 && $nextord <= 159) { 1582 if ($nextord >= 144 && $nextord <= 159) {
1601 // lower case 1583 // lower case
1602 $nextchar = chr($nextord + 32); 1584 $nextchar = chr($nextord + 32);
1603 1585
1604 // if Er - Ya
1605 } elseif ($nextord >= 160 && $nextord <= 175) { 1586 } elseif ($nextord >= 160 && $nextord <= 175) {
1587 // if Er - Ya
1606 // lower case 1588 // lower case
1607 $char = chr(209); // == $ord++ 1589 $char = chr(209); // == $ord++
1608 $nextchar = chr($nextord - 32); 1590 $nextchar = chr($nextord - 32);
@@ -1611,12 +1593,11 @@ class Text_LanguageDetect
1611 } 1593 }
1612 1594
1613 // tag on next byte 1595 // tag on next byte
1614 return $char . $nextchar; 1596 return $char . $nextchar;
1615
1616 } elseif ($ord >> 4 == 14) { // three-byte char 1597 } elseif ($ord >> 4 == 14) { // three-byte char
1617 1598
1618 // tag on next 2 bytes 1599 // tag on next 2 bytes
1619 return $char . $str{$counter++} . $str{$counter++}; 1600 return $char . $str{$counter++} . $str{$counter++};
1620 1601
1621 } elseif ($ord >> 3 == 30) { // four-byte char 1602 } elseif ($ord >> 3 == 30) { // four-byte char
1622 1603
@@ -1628,8 +1609,85 @@ class Text_LanguageDetect
1628 } 1609 }
1629 } 1610 }
1630 1611
1631} 1612 /**
1613 * Converts an $language input parameter from the configured mode
1614 * to the language name that is used internally.
1615 *
1616 * Works for strings and arrays.
1617 *
1618 * @param string|array $lang A language description ("english"/"en"/"eng")
1619 * @param boolean $convertKey If $lang is an array, setting $key
1620 * converts the keys to the language name.
1621 *
1622 * @return string|array Language name
1623 */
1624 function _convertFromNameMode($lang, $convertKey = false)
1625 {
1626 if ($this->_name_mode == 0) {
1627 return $lang;
1628 }
1629
1630 if ($this->_name_mode == 2) {
1631 $method = 'code2ToName';
1632 } else {
1633 $method = 'code3ToName';
1634 }
1635
1636 if (is_string($lang)) {
1637 return (string)Text_LanguageDetect_ISO639::$method($lang);
1638 }
1639
1640 $newlang = array();
1641 foreach ($lang as $key => $val) {
1642 if ($convertKey) {
1643 $newkey = (string)Text_LanguageDetect_ISO639::$method($key);
1644 $newlang[$newkey] = $val;
1645 } else {
1646 $newlang[$key] = (string)Text_LanguageDetect_ISO639::$method($val);
1647 }
1648 }
1649 return $newlang;
1650 }
1632 1651
1633/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 1652 /**
1653 * Converts an $language output parameter from the language name that is
1654 * used internally to the configured mode.
1655 *
1656 * Works for strings and arrays.
1657 *
1658 * @param string|array $lang A language description ("english"/"en"/"eng")
1659 * @param boolean $convertKey If $lang is an array, setting $key
1660 * converts the keys to the language name.
1661 *
1662 * @return string|array Language name
1663 */
1664 function _convertToNameMode($lang, $convertKey = false)
1665 {
1666 if ($this->_name_mode == 0) {
1667 return $lang;
1668 }
1669
1670 if ($this->_name_mode == 2) {
1671 $method = 'nameToCode2';
1672 } else {
1673 $method = 'nameToCode3';
1674 }
1675
1676 if (is_string($lang)) {
1677 return Text_LanguageDetect_ISO639::$method($lang);
1678 }
1679
1680 $newlang = array();
1681 foreach ($lang as $key => $val) {
1682 if ($convertKey) {
1683 $newkey = Text_LanguageDetect_ISO639::$method($key);
1684 $newlang[$newkey] = $val;
1685 } else {
1686 $newlang[$key] = Text_LanguageDetect_ISO639::$method($val);
1687 }
1688 }
1689 return $newlang;
1690 }
1691}
1634 1692
1635?> 1693/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ \ No newline at end of file
diff --git a/inc/3rdparty/libraries/language-detect/LanguageDetect/Exception.php b/inc/3rdparty/libraries/language-detect/LanguageDetect/Exception.php
new file mode 100644
index 00000000..196d994f
--- /dev/null
+++ b/inc/3rdparty/libraries/language-detect/LanguageDetect/Exception.php
@@ -0,0 +1,57 @@
1<?php
2class Text_LanguageDetect_Exception extends Exception
3{
4 /**
5 * Database file could not be found
6 */
7 const DB_NOT_FOUND = 10;
8
9 /**
10 * Database file found, but not readable
11 */
12 const DB_NOT_READABLE = 11;
13
14 /**
15 * Database file is empty
16 */
17 const DB_EMPTY = 12;
18
19 /**
20 * Database contents is not a PHP array
21 */
22 const DB_NOT_ARRAY = 13;
23
24 /**
25 * Magic quotes are activated
26 */
27 const MAGIC_QUOTES = 14;
28
29
30 /**
31 * Parameter of invalid type passed to method
32 */
33 const PARAM_TYPE = 20;
34
35 /**
36 * Character in parameter is invalid
37 */
38 const INVALID_CHAR = 21;
39
40
41 /**
42 * Language is not in the database
43 */
44 const UNKNOWN_LANGUAGE = 30;
45
46
47 /**
48 * Error during block detection
49 */
50 const BLOCK_DETECTION = 40;
51
52
53 /**
54 * Error while clustering languages
55 */
56 const NO_HIGHEST_KEY = 50;
57}
diff --git a/inc/3rdparty/libraries/language-detect/LanguageDetect/ISO639.php b/inc/3rdparty/libraries/language-detect/LanguageDetect/ISO639.php
new file mode 100644
index 00000000..05b0590d
--- /dev/null
+++ b/inc/3rdparty/libraries/language-detect/LanguageDetect/ISO639.php
@@ -0,0 +1,339 @@
1<?php
2/**
3 * Part of Text_LanguageDetect
4 *
5 * PHP version 5
6 *
7 * @category Text
8 * @package Text_LanguageDetect
9 * @author Christian Weiske <cweiske@php.net>
10 * @copyright 2011 Christian Weiske <cweiske@php.net>
11 * @license http://www.debian.org/misc/bsd.license BSD
12 * @version SVN: $Id$
13 * @link http://pear.php.net/package/Text_LanguageDetect/
14 */
15
16/**
17 * Provides a mapping between the languages from lang.dat and the
18 * ISO 639-1 and ISO-639-2 codes.
19 *
20 * Note that this class contains only languages that exist in lang.dat.
21 *
22 * @category Text
23 * @package Text_LanguageDetect
24 * @author Christian Weiske <cweiske@php.net>
25 * @copyright 2011 Christian Weiske <cweiske@php.net>
26 * @license http://www.debian.org/misc/bsd.license BSD
27 * @link http://www.loc.gov/standards/iso639-2/php/code_list.php
28 */
29class Text_LanguageDetect_ISO639
30{
31 /**
32 * Maps all language names from the language database to the
33 * ISO 639-1 2-letter language code.
34 *
35 * NULL indicates that there is no 2-letter code.
36 *
37 * @var array
38 */
39 public static $nameToCode2 = array(
40 'albanian' => 'sq',
41 'arabic' => 'ar',
42 'azeri' => 'az',
43 'bengali' => 'bn',
44 'bulgarian' => 'bg',
45 'cebuano' => null,
46 'croatian' => 'hr',
47 'czech' => 'cs',
48 'danish' => 'da',
49 'dutch' => 'nl',
50 'english' => 'en',
51 'estonian' => 'et',
52 'farsi' => 'fa',
53 'finnish' => 'fi',
54 'french' => 'fr',
55 'german' => 'de',
56 'hausa' => 'ha',
57 'hawaiian' => null,
58 'hindi' => 'hi',
59 'hungarian' => 'hu',
60 'icelandic' => 'is',
61 'indonesian' => 'id',
62 'italian' => 'it',
63 'kazakh' => 'kk',
64 'kyrgyz' => 'ky',
65 'latin' => 'la',
66 'latvian' => 'lv',
67 'lithuanian' => 'lt',
68 'macedonian' => 'mk',
69 'mongolian' => 'mn',
70 'nepali' => 'ne',
71 'norwegian' => 'no',
72 'pashto' => 'ps',
73 'pidgin' => null,
74 'polish' => 'pl',
75 'portuguese' => 'pt',
76 'romanian' => 'ro',
77 'russian' => 'ru',
78 'serbian' => 'sr',
79 'slovak' => 'sk',
80 'slovene' => 'sl',
81 'somali' => 'so',
82 'spanish' => 'es',
83 'swahili' => 'sw',
84 'swedish' => 'sv',
85 'tagalog' => 'tl',
86 'turkish' => 'tr',
87 'ukrainian' => 'uk',
88 'urdu' => 'ur',
89 'uzbek' => 'uz',
90 'vietnamese' => 'vi',
91 'welsh' => 'cy',
92 );
93
94 /**
95 * Maps all language names from the language database to the
96 * ISO 639-2 3-letter language code.
97 *
98 * @var array
99 */
100 public static $nameToCode3 = array(
101 'albanian' => 'sqi',
102 'arabic' => 'ara',
103 'azeri' => 'aze',
104 'bengali' => 'ben',
105 'bulgarian' => 'bul',
106 'cebuano' => 'ceb',
107 'croatian' => 'hrv',
108 'czech' => 'ces',
109 'danish' => 'dan',
110 'dutch' => 'nld',
111 'english' => 'eng',
112 'estonian' => 'est',
113 'farsi' => 'fas',
114 'finnish' => 'fin',
115 'french' => 'fra',
116 'german' => 'deu',
117 'hausa' => 'hau',
118 'hawaiian' => 'haw',
119 'hindi' => 'hin',
120 'hungarian' => 'hun',
121 'icelandic' => 'isl',
122 'indonesian' => 'ind',
123 'italian' => 'ita',
124 'kazakh' => 'kaz',
125 'kyrgyz' => 'kir',
126 'latin' => 'lat',
127 'latvian' => 'lav',
128 'lithuanian' => 'lit',
129 'macedonian' => 'mkd',
130 'mongolian' => 'mon',
131 'nepali' => 'nep',
132 'norwegian' => 'nor',
133 'pashto' => 'pus',
134 'pidgin' => 'crp',
135 'polish' => 'pol',
136 'portuguese' => 'por',
137 'romanian' => 'ron',
138 'russian' => 'rus',
139 'serbian' => 'srp',
140 'slovak' => 'slk',
141 'slovene' => 'slv',
142 'somali' => 'som',
143 'spanish' => 'spa',
144 'swahili' => 'swa',
145 'swedish' => 'swe',
146 'tagalog' => 'tgl',
147 'turkish' => 'tur',
148 'ukrainian' => 'ukr',
149 'urdu' => 'urd',
150 'uzbek' => 'uzb',
151 'vietnamese' => 'vie',
152 'welsh' => 'cym',
153 );
154
155 /**
156 * Maps ISO 639-1 2-letter language codes to the language names
157 * in the language database
158 *
159 * Not all languages have a 2 letter code, so some are missing
160 *
161 * @var array
162 */
163 public static $code2ToName = array(
164 'ar' => 'arabic',
165 'az' => 'azeri',
166 'bg' => 'bulgarian',
167 'bn' => 'bengali',
168 'cs' => 'czech',
169 'cy' => 'welsh',
170 'da' => 'danish',
171 'de' => 'german',
172 'en' => 'english',
173 'es' => 'spanish',
174 'et' => 'estonian',
175 'fa' => 'farsi',
176 'fi' => 'finnish',
177 'fr' => 'french',
178 'ha' => 'hausa',
179 'hi' => 'hindi',
180 'hr' => 'croatian',
181 'hu' => 'hungarian',
182 'id' => 'indonesian',
183 'is' => 'icelandic',
184 'it' => 'italian',
185 'kk' => 'kazakh',
186 'ky' => 'kyrgyz',
187 'la' => 'latin',
188 'lt' => 'lithuanian',
189 'lv' => 'latvian',
190 'mk' => 'macedonian',
191 'mn' => 'mongolian',
192 'ne' => 'nepali',
193 'nl' => 'dutch',
194 'no' => 'norwegian',
195 'pl' => 'polish',
196 'ps' => 'pashto',
197 'pt' => 'portuguese',
198 'ro' => 'romanian',
199 'ru' => 'russian',
200 'sk' => 'slovak',
201 'sl' => 'slovene',
202 'so' => 'somali',
203 'sq' => 'albanian',
204 'sr' => 'serbian',
205 'sv' => 'swedish',
206 'sw' => 'swahili',
207 'tl' => 'tagalog',
208 'tr' => 'turkish',
209 'uk' => 'ukrainian',
210 'ur' => 'urdu',
211 'uz' => 'uzbek',
212 'vi' => 'vietnamese',
213 );
214
215 /**
216 * Maps ISO 639-2 3-letter language codes to the language names
217 * in the language database.
218 *
219 * @var array
220 */
221 public static $code3ToName = array(
222 'ara' => 'arabic',
223 'aze' => 'azeri',
224 'ben' => 'bengali',
225 'bul' => 'bulgarian',
226 'ceb' => 'cebuano',
227 'ces' => 'czech',
228 'crp' => 'pidgin',
229 'cym' => 'welsh',
230 'dan' => 'danish',
231 'deu' => 'german',
232 'eng' => 'english',
233 'est' => 'estonian',
234 'fas' => 'farsi',
235 'fin' => 'finnish',
236 'fra' => 'french',
237 'hau' => 'hausa',
238 'haw' => 'hawaiian',
239 'hin' => 'hindi',
240 'hrv' => 'croatian',
241 'hun' => 'hungarian',
242 'ind' => 'indonesian',
243 'isl' => 'icelandic',
244 'ita' => 'italian',
245 'kaz' => 'kazakh',
246 'kir' => 'kyrgyz',
247 'lat' => 'latin',
248 'lav' => 'latvian',
249 'lit' => 'lithuanian',
250 'mkd' => 'macedonian',
251 'mon' => 'mongolian',
252 'nep' => 'nepali',
253 'nld' => 'dutch',
254 'nor' => 'norwegian',
255 'pol' => 'polish',
256 'por' => 'portuguese',
257 'pus' => 'pashto',
258 'rom' => 'romanian',
259 'rus' => 'russian',
260 'slk' => 'slovak',
261 'slv' => 'slovene',
262 'som' => 'somali',
263 'spa' => 'spanish',
264 'sqi' => 'albanian',
265 'srp' => 'serbian',
266 'swa' => 'swahili',
267 'swe' => 'swedish',
268 'tgl' => 'tagalog',
269 'tur' => 'turkish',
270 'ukr' => 'ukrainian',
271 'urd' => 'urdu',
272 'uzb' => 'uzbek',
273 'vie' => 'vietnamese',
274 );
275
276 /**
277 * Returns the 2-letter ISO 639-1 code for the given language name.
278 *
279 * @param string $lang English language name like "swedish"
280 *
281 * @return string Two-letter language code (e.g. "sv") or NULL if not found
282 */
283 public static function nameToCode2($lang)
284 {
285 $lang = strtolower($lang);
286 if (!isset(self::$nameToCode2[$lang])) {
287 return null;
288 }
289 return self::$nameToCode2[$lang];
290 }
291
292 /**
293 * Returns the 3-letter ISO 639-2 code for the given language name.
294 *
295 * @param string $lang English language name like "swedish"
296 *
297 * @return string Three-letter language code (e.g. "swe") or NULL if not found
298 */
299 public static function nameToCode3($lang)
300 {
301 $lang = strtolower($lang);
302 if (!isset(self::$nameToCode3[$lang])) {
303 return null;
304 }
305 return self::$nameToCode3[$lang];
306 }
307
308 /**
309 * Returns the language name for the given 2-letter ISO 639-1 code.
310 *
311 * @param string $code Two-letter language code (e.g. "sv")
312 *
313 * @return string English language name like "swedish"
314 */
315 public static function code2ToName($code)
316 {
317 $lang = strtolower($code);
318 if (!isset(self::$code2ToName[$code])) {
319 return null;
320 }
321 return self::$code2ToName[$code];
322 }
323
324 /**
325 * Returns the language name for the given 3-letter ISO 639-2 code.
326 *
327 * @param string $code Three-letter language code (e.g. "swe")
328 *
329 * @return string English language name like "swedish"
330 */
331 public static function code3ToName($code)
332 {
333 $lang = strtolower($code);
334 if (!isset(self::$code3ToName[$code])) {
335 return null;
336 }
337 return self::$code3ToName[$code];
338 }
339} \ No newline at end of file
diff --git a/inc/3rdparty/libraries/language-detect/Parser.php b/inc/3rdparty/libraries/language-detect/LanguageDetect/Parser.php
index 7f15fa98..fb0e1e20 100644
--- a/inc/3rdparty/libraries/language-detect/Parser.php
+++ b/inc/3rdparty/libraries/language-detect/LanguageDetect/Parser.php
@@ -8,7 +8,7 @@
8 * @author Nicholas Pisarro 8 * @author Nicholas Pisarro
9 * @copyright 2006 9 * @copyright 2006
10 * @license BSD 10 * @license BSD
11 * @version CVS: $Id: Parser.php,v 1.5 2006/03/11 05:45:05 taak Exp $ 11 * @version CVS: $Id: Parser.php 322327 2012-01-15 17:55:59Z cweiske $
12 * @link http://pear.php.net/package/Text_LanguageDetect/ 12 * @link http://pear.php.net/package/Text_LanguageDetect/
13 * @link http://langdetect.blogspot.com/ 13 * @link http://langdetect.blogspot.com/
14 */ 14 */
@@ -28,7 +28,7 @@
28 * @author Nicholas Pisarro 28 * @author Nicholas Pisarro
29 * @copyright 2006 29 * @copyright 2006
30 * @license BSD 30 * @license BSD
31 * @version release: 0.2.3 31 * @version release: 0.3.0
32 */ 32 */
33class Text_LanguageDetect_Parser extends Text_LanguageDetect 33class Text_LanguageDetect_Parser extends Text_LanguageDetect
34{ 34{
@@ -102,21 +102,17 @@ class Text_LanguageDetect_Parser extends Text_LanguageDetect
102 * @access private 102 * @access private
103 * @param string $string string to be parsed 103 * @param string $string string to be parsed
104 */ 104 */
105 function Text_LanguageDetect_Parser($string, $db=null, $unicode_db=null) { 105 function Text_LanguageDetect_Parser($string) {
106 if (isset($db)) $this->_db_filename = $db;
107 if (isset($unicode_db)) $this->_unicode_db_filename = $unicode_db;
108 $this->_string = $string; 106 $this->_string = $string;
109 } 107 }
110 108
111 /** 109 /**
112 * Returns true if a string is suitable for parsing 110 * Returns true if a string is suitable for parsing
113 * 111 *
114 * @static
115 * @access public
116 * @param string $str input string to test 112 * @param string $str input string to test
117 * @return bool true if acceptable, false if not 113 * @return bool true if acceptable, false if not
118 */ 114 */
119 function validateString($str) { 115 public static function validateString($str) {
120 if (!empty($str) && strlen($str) > 3 && preg_match('/\S/', $str)) { 116 if (!empty($str) && strlen($str) > 3 && preg_match('/\S/', $str)) {
121 return true; 117 return true;
122 } else { 118 } else {
@@ -222,8 +218,7 @@ class Text_LanguageDetect_Parser extends Text_LanguageDetect
222 218
223 // unicode startup 219 // unicode startup
224 if ($this->_compile_unicode) { 220 if ($this->_compile_unicode) {
225 $blocks =& $this->_read_unicode_block_db(); 221 $blocks = $this->_read_unicode_block_db();
226
227 $block_count = count($blocks); 222 $block_count = count($blocks);
228 223
229 $skipped_count = 0; 224 $skipped_count = 0;
@@ -349,6 +344,4 @@ class Text_LanguageDetect_Parser extends Text_LanguageDetect
349 } 344 }
350} 345}
351 346
352/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 347/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ \ No newline at end of file
353
354?>
diff --git a/inc/3rdparty/libraries/readability/Readability.php b/inc/3rdparty/libraries/readability/Readability.php
index 2e8991cc..4fa3ba63 100644..100755
--- a/inc/3rdparty/libraries/readability/Readability.php
+++ b/inc/3rdparty/libraries/readability/Readability.php
@@ -1,1138 +1,1151 @@
1<?php 1<?php
2/** 2/**
3* Arc90's Readability ported to PHP for FiveFilters.org 3* Arc90's Readability ported to PHP for FiveFilters.org
4* Based on readability.js version 1.7.1 (without multi-page support) 4* Based on readability.js version 1.7.1 (without multi-page support)
5* Updated to allow HTML5 parsing with html5lib 5* Updated to allow HTML5 parsing with html5lib
6* Updated with lightClean mode to preserve more images and youtube/vimeo/viddler embeds 6* Updated with lightClean mode to preserve more images and youtube/vimeo/viddler embeds
7* ------------------------------------------------------ 7* ------------------------------------------------------
8* Original URL: http://lab.arc90.com/experiments/readability/js/readability.js 8* Original URL: http://lab.arc90.com/experiments/readability/js/readability.js
9* Arc90's project URL: http://lab.arc90.com/experiments/readability/ 9* Arc90's project URL: http://lab.arc90.com/experiments/readability/
10* JS Source: http://code.google.com/p/arc90labs-readability 10* JS Source: http://code.google.com/p/arc90labs-readability
11* Ported by: Keyvan Minoukadeh, http://www.keyvan.net 11* Ported by: Keyvan Minoukadeh, http://www.keyvan.net
12* More information: http://fivefilters.org/content-only/ 12* More information: http://fivefilters.org/content-only/
13* License: Apache License, Version 2.0 13* License: Apache License, Version 2.0
14* Requires: PHP5 14* Requires: PHP5
15* Date: 2012-09-19 15* Date: 2012-09-19
16* 16*
17* Differences between the PHP port and the original 17* Differences between the PHP port and the original
18* ------------------------------------------------------ 18* ------------------------------------------------------
19* Arc90's Readability is designed to run in the browser. It works on the DOM 19* Arc90's Readability is designed to run in the browser. It works on the DOM
20* tree (the parsed HTML) after the page's CSS styles have been applied and 20* tree (the parsed HTML) after the page's CSS styles have been applied and
21* Javascript code executed. This PHP port does not run inside a browser. 21* Javascript code executed. This PHP port does not run inside a browser.
22* We use PHP's ability to parse HTML to build our DOM tree, but we cannot 22* We use PHP's ability to parse HTML to build our DOM tree, but we cannot
23* rely on CSS or Javascript support. As such, the results will not always 23* rely on CSS or Javascript support. As such, the results will not always
24* match Arc90's Readability. (For example, if a web page contains CSS style 24* match Arc90's Readability. (For example, if a web page contains CSS style
25* rules or Javascript code which hide certain HTML elements from display, 25* rules or Javascript code which hide certain HTML elements from display,
26* Arc90's Readability will dismiss those from consideration but our PHP port, 26* Arc90's Readability will dismiss those from consideration but our PHP port,
27* unable to understand CSS or Javascript, will not know any better.) 27* unable to understand CSS or Javascript, will not know any better.)
28* 28*
29* Another significant difference is that the aim of Arc90's Readability is 29* Another significant difference is that the aim of Arc90's Readability is
30* to re-present the main content block of a given web page so users can 30* to re-present the main content block of a given web page so users can
31* read it more easily in their browsers. Correct identification, clean up, 31* read it more easily in their browsers. Correct identification, clean up,
32* and separation of the content block is only a part of this process. 32* and separation of the content block is only a part of this process.
33* This PHP port is only concerned with this part, it does not include code 33* This PHP port is only concerned with this part, it does not include code
34* that relates to presentation in the browser - Arc90 already do 34* that relates to presentation in the browser - Arc90 already do
35* that extremely well, and for PDF output there's FiveFilters.org's 35* that extremely well, and for PDF output there's FiveFilters.org's
36* PDF Newspaper: http://fivefilters.org/pdf-newspaper/. 36* PDF Newspaper: http://fivefilters.org/pdf-newspaper/.
37* 37*
38* Finally, this class contains methods that might be useful for developers 38* Finally, this class contains methods that might be useful for developers
39* working on HTML document fragments. So without deviating too much from 39* working on HTML document fragments. So without deviating too much from
40* the original code (which I don't want to do because it makes debugging 40* the original code (which I don't want to do because it makes debugging
41* and updating more difficult), I've tried to make it a little more 41* and updating more difficult), I've tried to make it a little more
42* developer friendly. You should be able to use the methods here on 42* developer friendly. You should be able to use the methods here on
43* existing DOMElement objects without passing an entire HTML document to 43* existing DOMElement objects without passing an entire HTML document to
44* be parsed. 44* be parsed.
45*/ 45*/
46 46
47// This class allows us to do JavaScript like assignements to innerHTML 47// This class allows us to do JavaScript like assignements to innerHTML
48require_once(dirname(__FILE__).'/JSLikeHTMLElement.php'); 48require_once(dirname(__FILE__).'/JSLikeHTMLElement.php');
49 49
50// Alternative usage (for testing only!) 50// Alternative usage (for testing only!)
51// uncomment the lines below and call Readability.php in your browser 51// uncomment the lines below and call Readability.php in your browser
52// passing it the URL of the page you'd like content from, e.g.: 52// passing it the URL of the page you'd like content from, e.g.:
53// Readability.php?url=http://medialens.org/alerts/09/090615_the_guardian_climate.php 53// Readability.php?url=http://medialens.org/alerts/09/090615_the_guardian_climate.php
54 54
55/* 55/*
56if (!isset($_GET['url']) || $_GET['url'] == '') { 56if (!isset($_GET['url']) || $_GET['url'] == '') {
57 die('Please pass a URL to the script. E.g. Readability.php?url=bla.com/story.html'); 57 die('Please pass a URL to the script. E.g. Readability.php?url=bla.com/story.html');
58} 58}
59$url = $_GET['url']; 59$url = $_GET['url'];
60if (!preg_match('!^https?://!i', $url)) $url = 'http://'.$url; 60if (!preg_match('!^https?://!i', $url)) $url = 'http://'.$url;
61$html = file_get_contents($url); 61$html = file_get_contents($url);
62$r = new Readability($html, $url); 62$r = new Readability($html, $url);
63$r->init(); 63$r->init();
64echo $r->articleContent->innerHTML; 64echo $r->articleContent->innerHTML;
65*/ 65*/
66 66
67class Readability 67class Readability
68{ 68{
69 public $version = '1.7.1-without-multi-page'; 69 public $version = '1.7.1-without-multi-page';
70 public $convertLinksToFootnotes = false; 70 public $convertLinksToFootnotes = false;
71 public $revertForcedParagraphElements = true; 71 public $revertForcedParagraphElements = true;
72 public $articleTitle; 72 public $articleTitle;
73 public $articleContent; 73 public $articleContent;
74 public $dom; 74 public $dom;
75 public $url = null; // optional - URL where HTML was retrieved 75 public $url = null; // optional - URL where HTML was retrieved
76 public $debug = false; 76 public $debug = false;
77 public $lightClean = true; // preserves more content (experimental) added 2012-09-19 77 public $lightClean = true; // preserves more content (experimental) added 2012-09-19
78 protected $body = null; // 78 protected $body = null; //
79 protected $bodyCache = null; // Cache the body HTML in case we need to re-use it later 79 protected $bodyCache = null; // Cache the body HTML in case we need to re-use it later
80 protected $flags = 7; // 1 | 2 | 4; // Start with all flags set. 80 protected $flags = 7; // 1 | 2 | 4; // Start with all flags set.
81 protected $success = false; // indicates whether we were able to extract or not 81 protected $success = false; // indicates whether we were able to extract or not
82 82
83 /**
84 * All of the regular expressions in use within readability.
85 * Defined up here so we don't instantiate them repeatedly in loops.
86 **/
87 public $regexps = array(
88 'unlikelyCandidates' => '/combx|comment|community|disqus|extra|foot|header|menu|remark|rss|shoutbox|sidebar|sponsor|ad-break|agegate|pagination|pager|popup/i',
89 'okMaybeItsACandidate' => '/and|article|body|column|main|shadow/i',
90 'positive' => '/article|body|content|entry|hentry|main|page|attachment|pagination|post|text|blog|story/i',
91 'negative' => '/combx|comment|com-|contact|foot|footer|_nav|footnote|masthead|media|meta|outbrain|promo|related|scroll|shoutbox|sidebar|sponsor|shopping|tags|tool|widget/i',
92 'divToPElements' => '/<(a|blockquote|dl|div|img|ol|p|pre|table|ul)/i',
93 'replaceBrs' => '/(<br[^>]*>[ \n\r\t]*){2,}/i',
94 'replaceFonts' => '/<(\/?)font[^>]*>/i',
95 // 'trimRe' => '/^\s+|\s+$/g', // PHP has trim()
96 'normalize' => '/\s{2,}/',
97 'killBreaks' => '/(<br\s*\/?>(\s|&nbsp;?)*){1,}/',
98 'video' => '!//(player\.|www\.)?(youtube|vimeo|viddler)\.com!i',
99 'skipFootnoteLink' => '/^\s*(\[?[a-z0-9]{1,2}\]?|^|edit|citation needed)\s*$/i'
100 );
101
102 /* constants */
103 const FLAG_STRIP_UNLIKELYS = 1;
104 const FLAG_WEIGHT_CLASSES = 2;
105 const FLAG_CLEAN_CONDITIONALLY = 4;
106
107 /**
108 * Create instance of Readability
109 * @param string UTF-8 encoded string
110 * @param string (optional) URL associated with HTML (used for footnotes)
111 * @param string which parser to use for turning raw HTML into a DOMDocument (either 'libxml' or 'html5lib')
112 */
113 function __construct($html, $url=null, $parser='libxml')
114 {
115 $this->url = $url;
116 /* Turn all double br's into p's */
117 $html = preg_replace($this->regexps['replaceBrs'], '</p><p>', $html);
118 $html = preg_replace($this->regexps['replaceFonts'], '<$1span>', $html);
119 $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8");
120 if (trim($html) == '') $html = '<html></html>';
121 if ($parser=='html5lib' && ($this->dom = HTML5_Parser::parse($html))) {
122 // all good
123 } else {
124 $this->dom = new DOMDocument();
125 $this->dom->preserveWhiteSpace = false;
126 @$this->dom->loadHTML($html);
127 }
128 $this->dom->registerNodeClass('DOMElement', 'JSLikeHTMLElement');
129 }
130
131 /**
132 * Get article title element
133 * @return DOMElement
134 */
135 public function getTitle() {
136 return $this->articleTitle;
137 }
138
139 /**
140 * Get article content element
141 * @return DOMElement
142 */
143 public function getContent() {
144 return $this->articleContent;
145 }
146
147 /**
148 * Runs readability.
149 *
150 * Workflow:
151 * 1. Prep the document by removing script tags, css, etc.
152 * 2. Build readability's DOM tree.
153 * 3. Grab the article content from the current dom tree.
154 * 4. Replace the current DOM tree with the new one.
155 * 5. Read peacefully.
156 *
157 * @return boolean true if we found content, false otherwise
158 **/
159 public function init()
160 {
161 if (!isset($this->dom->documentElement)) return false;
162 $this->removeScripts($this->dom);
163 //die($this->getInnerHTML($this->dom->documentElement));
164
165 // Assume successful outcome
166 $this->success = true;
167
168 $bodyElems = $this->dom->getElementsByTagName('body');
169 if ($bodyElems->length > 0) {
170 if ($this->bodyCache == null) {
171 $this->bodyCache = $bodyElems->item(0)->innerHTML;
172 }
173 if ($this->body == null) {
174 $this->body = $bodyElems->item(0);
175 }
176 }
177
178 $this->prepDocument();
179
180 //die($this->dom->documentElement->parentNode->nodeType);
181 //$this->setInnerHTML($this->dom->documentElement, $this->getInnerHTML($this->dom->documentElement));
182 //die($this->getInnerHTML($this->dom->documentElement));
183
184 /* Build readability's DOM tree */
185 $overlay = $this->dom->createElement('div');
186 $innerDiv = $this->dom->createElement('div');
187 $articleTitle = $this->getArticleTitle();
188 $articleContent = $this->grabArticle();
189
190 if (!$articleContent) {
191 $this->success = false;
192 $articleContent = $this->dom->createElement('div');
193 $articleContent->setAttribute('id', 'readability-content');
194 $articleContent->innerHTML = '<p>Sorry, Readability was unable to parse this page for content.</p>';
195 }
196
197 $overlay->setAttribute('id', 'readOverlay');
198 $innerDiv->setAttribute('id', 'readInner');
199
200 /* Glue the structure of our document together. */
201 $innerDiv->appendChild($articleTitle);
202 $innerDiv->appendChild($articleContent);
203 $overlay->appendChild($innerDiv);
204
205 /* Clear the old HTML, insert the new content. */
206 $this->body->innerHTML = '';
207 $this->body->appendChild($overlay);
208 //document.body.insertBefore(overlay, document.body.firstChild);
209 $this->body->removeAttribute('style');
210
211 $this->postProcessContent($articleContent);
212
213 // Set title and content instance variables
214 $this->articleTitle = $articleTitle;
215 $this->articleContent = $articleContent;
216
217 return $this->success;
218 }
219
220 /**
221 * Debug
222 */
223 protected function dbg($msg) {
224 if ($this->debug) echo '* ',$msg, "\n";
225 }
226
227 /**
228 * Run any post-process modifications to article content as necessary.
229 *
230 * @param DOMElement
231 * @return void
232 */
233 public function postProcessContent($articleContent) {
234 if ($this->convertLinksToFootnotes && !preg_match('/wikipedia\.org/', @$this->url)) {
235 $this->addFootnotes($articleContent);
236 }
237 }
238
239 /**
240 * Get the article title as an H1.
241 *
242 * @return DOMElement
243 */
244 protected function getArticleTitle() {
245 $curTitle = '';
246 $origTitle = '';
247
248 try {
249 $curTitle = $origTitle = $this->getInnerText($this->dom->getElementsByTagName('title')->item(0));
250 } catch(Exception $e) {}
251
252 if (preg_match('/ [\|\-] /', $curTitle))
253 {
254 $curTitle = preg_replace('/(.*)[\|\-] .*/i', '$1', $origTitle);
255
256 if (count(explode(' ', $curTitle)) < 3) {
257 $curTitle = preg_replace('/[^\|\-]*[\|\-](.*)/i', '$1', $origTitle);
258 }
259 }
260 else if (strpos($curTitle, ': ') !== false)
261 {
262 $curTitle = preg_replace('/.*:(.*)/i', '$1', $origTitle);
263
264 if (count(explode(' ', $curTitle)) < 3) {
265 $curTitle = preg_replace('/[^:]*[:](.*)/i','$1', $origTitle);
266 }
267 }
268 else if(strlen($curTitle) > 150 || strlen($curTitle) < 15)
269 {
270 $hOnes = $this->dom->getElementsByTagName('h1');
271 if($hOnes->length == 1)
272 {
273 $curTitle = $this->getInnerText($hOnes->item(0));
274 }
275 }
276
277 $curTitle = trim($curTitle);
278
279 if (count(explode(' ', $curTitle)) <= 4) {
280 $curTitle = $origTitle;
281 }
282
283 $articleTitle = $this->dom->createElement('h1');
284 $articleTitle->innerHTML = $curTitle;
285
286 return $articleTitle;
287 }
288
289 /**
290 * Prepare the HTML document for readability to scrape it.
291 * This includes things like stripping javascript, CSS, and handling terrible markup.
292 *
293 * @return void
294 **/
295 protected function prepDocument() {
296 /**
297 * In some cases a body element can't be found (if the HTML is totally hosed for example)
298 * so we create a new body node and append it to the document.
299 */
300 if ($this->body == null)
301 {
302 $this->body = $this->dom->createElement('body');
303 $this->dom->documentElement->appendChild($this->body);
304 }
305 $this->body->setAttribute('id', 'readabilityBody');
306
307 /* Remove all style tags in head */
308 $styleTags = $this->dom->getElementsByTagName('style');
309 for ($i = $styleTags->length-1; $i >= 0; $i--)
310 {
311 $styleTags->item($i)->parentNode->removeChild($styleTags->item($i));
312 }
313
314 /* Turn all double br's into p's */
315 /* Note, this is pretty costly as far as processing goes. Maybe optimize later. */
316 //document.body.innerHTML = document.body.innerHTML.replace(readability.regexps.replaceBrs, '</p><p>').replace(readability.regexps.replaceFonts, '<$1span>');
317 // We do this in the constructor for PHP as that's when we have raw HTML - before parsing it into a DOM tree.
318 // Manipulating innerHTML as it's done in JS is not possible in PHP.
319 }
320
321 /**
322 * For easier reading, convert this document to have footnotes at the bottom rather than inline links.
323 * @see http://www.roughtype.com/archives/2010/05/experiments_in.php
324 *
325 * @return void
326 **/
327 public function addFootnotes($articleContent) {
328 $footnotesWrapper = $this->dom->createElement('div');
329 $footnotesWrapper->setAttribute('id', 'readability-footnotes');
330 $footnotesWrapper->innerHTML = '<h3>References</h3>';
331
332 $articleFootnotes = $this->dom->createElement('ol');
333 $articleFootnotes->setAttribute('id', 'readability-footnotes-list');
334 $footnotesWrapper->appendChild($articleFootnotes);
335
336 $articleLinks = $articleContent->getElementsByTagName('a');
337
338 $linkCount = 0;
339 for ($i = 0; $i < $articleLinks->length; $i++)
340 {
341 $articleLink = $articleLinks->item($i);
342 $footnoteLink = $articleLink->cloneNode(true);
343 $refLink = $this->dom->createElement('a');
344 $footnote = $this->dom->createElement('li');
345 $linkDomain = @parse_url($footnoteLink->getAttribute('href'), PHP_URL_HOST);
346 if (!$linkDomain && isset($this->url)) $linkDomain = @parse_url($this->url, PHP_URL_HOST);
347 //linkDomain = footnoteLink.host ? footnoteLink.host : document.location.host,
348 $linkText = $this->getInnerText($articleLink);
349
350 if ((strpos($articleLink->getAttribute('class'), 'readability-DoNotFootnote') !== false) || preg_match($this->regexps['skipFootnoteLink'], $linkText)) {
351 continue;
352 }
353
354 $linkCount++;
355
356 /** Add a superscript reference after the article link */
357 $refLink->setAttribute('href', '#readabilityFootnoteLink-' . $linkCount);
358 $refLink->innerHTML = '<small><sup>[' . $linkCount . ']</sup></small>';
359 $refLink->setAttribute('class', 'readability-DoNotFootnote');
360 $refLink->setAttribute('style', 'color: inherit;');
361
362 //TODO: does this work or should we use DOMNode.isSameNode()?
363 if ($articleLink->parentNode->lastChild == $articleLink) {
364 $articleLink->parentNode->appendChild($refLink);
365 } else {
366 $articleLink->parentNode->insertBefore($refLink, $articleLink->nextSibling);
367 }
368
369 $articleLink->setAttribute('style', 'color: inherit; text-decoration: none;');
370 $articleLink->setAttribute('name', 'readabilityLink-' . $linkCount);
371
372 $footnote->innerHTML = '<small><sup><a href="#readabilityLink-' . $linkCount . '" title="Jump to Link in Article">^</a></sup></small> ';
373
374 $footnoteLink->innerHTML = ($footnoteLink->getAttribute('title') != '' ? $footnoteLink->getAttribute('title') : $linkText);
375 $footnoteLink->setAttribute('name', 'readabilityFootnoteLink-' . $linkCount);
376
377 $footnote->appendChild($footnoteLink);
378 if ($linkDomain) $footnote->innerHTML = $footnote->innerHTML . '<small> (' . $linkDomain . ')</small>';
379
380 $articleFootnotes->appendChild($footnote);
381 }
382
383 if ($linkCount > 0) {
384 $articleContent->appendChild($footnotesWrapper);
385 }
386 }
387
388 /**
389 * Reverts P elements with class 'readability-styled'
390 * to text nodes - which is what they were before.
391 *
392 * @param DOMElement
393 * @return void
394 */
395 function revertReadabilityStyledElements($articleContent) {
396 $xpath = new DOMXPath($articleContent->ownerDocument);
397 $elems = $xpath->query('.//p[@class="readability-styled"]', $articleContent);
398 //$elems = $articleContent->getElementsByTagName('p');
399 for ($i = $elems->length-1; $i >= 0; $i--) {
400 $e = $elems->item($i);
401 $e->parentNode->replaceChild($articleContent->ownerDocument->createTextNode($e->textContent), $e);
402 //if ($e->hasAttribute('class') && $e->getAttribute('class') == 'readability-styled') {
403 // $e->parentNode->replaceChild($this->dom->createTextNode($e->textContent), $e);
404 //}
405 }
406 }
407
408 /**
409 * Prepare the article node for display. Clean out any inline styles,
410 * iframes, forms, strip extraneous <p> tags, etc.
411 *
412 * @param DOMElement
413 * @return void
414 */
415 function prepArticle($articleContent) {
416 $this->cleanStyles($articleContent);
417 $this->killBreaks($articleContent);
418 if ($this->revertForcedParagraphElements) {
419 $this->revertReadabilityStyledElements($articleContent);
420 }
421
422 /* Clean out junk from the article content */
423 $this->cleanConditionally($articleContent, 'form');
424 $this->clean($articleContent, 'object');
425 $this->clean($articleContent, 'h1');
426
427 /**
428 * If there is only one h2, they are probably using it
429 * as a header and not a subheader, so remove it since we already have a header.
430 ***/
431 if (!$this->lightClean && ($articleContent->getElementsByTagName('h2')->length == 1)) {
432 $this->clean($articleContent, 'h2');
433 }
434 $this->clean($articleContent, 'iframe');
435
436 $this->cleanHeaders($articleContent);
437
438 /* Do these last as the previous stuff may have removed junk that will affect these */
439 $this->cleanConditionally($articleContent, 'table');
440 $this->cleanConditionally($articleContent, 'ul');
441 $this->cleanConditionally($articleContent, 'div');
442
443 /* Remove extra paragraphs */
444 $articleParagraphs = $articleContent->getElementsByTagName('p');
445 for ($i = $articleParagraphs->length-1; $i >= 0; $i--)
446 {
447 $imgCount = $articleParagraphs->item($i)->getElementsByTagName('img')->length;
448 $embedCount = $articleParagraphs->item($i)->getElementsByTagName('embed')->length;
449 $objectCount = $articleParagraphs->item($i)->getElementsByTagName('object')->length;
450 $iframeCount = $articleParagraphs->item($i)->getElementsByTagName('iframe')->length;
451
452 if ($imgCount === 0 && $embedCount === 0 && $objectCount === 0 && $iframeCount === 0 && $this->getInnerText($articleParagraphs->item($i), false) == '')
453 {
454 $articleParagraphs->item($i)->parentNode->removeChild($articleParagraphs->item($i));
455 }
456 }
457
458 try {
459 $articleContent->innerHTML = preg_replace('/<br[^>]*>\s*<p/i', '<p', $articleContent->innerHTML);
460 //articleContent.innerHTML = articleContent.innerHTML.replace(/<br[^>]*>\s*<p/gi, '<p');
461 }
462 catch (Exception $e) {
463 $this->dbg("Cleaning innerHTML of breaks failed. This is an IE strict-block-elements bug. Ignoring.: " . $e);
464 }
465 }
466
467 /**
468 * Initialize a node with the readability object. Also checks the
469 * className/id for special names to add to its score.
470 *
471 * @param Element
472 * @return void
473 **/
474 protected function initializeNode($node) {
475 $readability = $this->dom->createAttribute('readability');
476 $readability->value = 0; // this is our contentScore
477 $node->setAttributeNode($readability);
478
479 switch (strtoupper($node->tagName)) { // unsure if strtoupper is needed, but using it just in case
480 case 'DIV':
481 $readability->value += 5;
482 break;
483
484 case 'PRE':
485 case 'TD':
486 case 'BLOCKQUOTE':
487 $readability->value += 3;
488 break;
489
490 case 'ADDRESS':
491 case 'OL':
492 case 'UL':
493 case 'DL':
494 case 'DD':
495 case 'DT':
496 case 'LI':
497 case 'FORM':
498 $readability->value -= 3;
499 break;
500
501 case 'H1':
502 case 'H2':
503 case 'H3':
504 case 'H4':
505 case 'H5':
506 case 'H6':
507 case 'TH':
508 $readability->value -= 5;
509 break;
510 }
511 $readability->value += $this->getClassWeight($node);
512 }
513
514 /***
515 * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is
516 * most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
517 *
518 * @return DOMElement
519 **/
520 protected function grabArticle($page=null) {
521 $stripUnlikelyCandidates = $this->flagIsActive(self::FLAG_STRIP_UNLIKELYS);
522 if (!$page) $page = $this->dom;
523 $allElements = $page->getElementsByTagName('*');
524 /**
525 * First, node prepping. Trash nodes that look cruddy (like ones with the class name "comment", etc), and turn divs
526 * into P tags where they have been used inappropriately (as in, where they contain no other block level elements.)
527 *
528 * Note: Assignment from index for performance. See http://www.peachpit.com/articles/article.aspx?p=31567&seqNum=5
529 * TODO: Shouldn't this be a reverse traversal?
530 **/
531 $node = null;
532 $nodesToScore = array();
533 for ($nodeIndex = 0; ($node = $allElements->item($nodeIndex)); $nodeIndex++) {
534 //for ($nodeIndex=$targetList->length-1; $nodeIndex >= 0; $nodeIndex--) {
535 //$node = $targetList->item($nodeIndex);
536 $tagName = strtoupper($node->tagName);
537 /* Remove unlikely candidates */
538 if ($stripUnlikelyCandidates) {
539 $unlikelyMatchString = $node->getAttribute('class') . $node->getAttribute('id');
540 if (
541 preg_match($this->regexps['unlikelyCandidates'], $unlikelyMatchString) &&
542 !preg_match($this->regexps['okMaybeItsACandidate'], $unlikelyMatchString) &&
543 $tagName != 'BODY'
544 )
545 {
546 $this->dbg('Removing unlikely candidate - ' . $unlikelyMatchString);
547 //$nodesToRemove[] = $node;
548 $node->parentNode->removeChild($node);
549 $nodeIndex--;
550 continue;
551 }
552 }
553
554 if ($tagName == 'P' || $tagName == 'TD' || $tagName == 'PRE') {
555 $nodesToScore[] = $node;
556 }
557
558 /* Turn all divs that don't have children block level elements into p's */
559 if ($tagName == 'DIV') {
560 if (!preg_match($this->regexps['divToPElements'], $node->innerHTML)) {
561 //$this->dbg('Altering div to p');
562 $newNode = $this->dom->createElement('p');
563 try {
564 $newNode->innerHTML = $node->innerHTML;
565 //$nodesToReplace[] = array('new'=>$newNode, 'old'=>$node);
566 $node->parentNode->replaceChild($newNode, $node);
567 $nodeIndex--;
568 $nodesToScore[] = $node; // or $newNode?
569 }
570 catch(Exception $e) {
571 $this->dbg('Could not alter div to p, reverting back to div.: ' . $e);
572 }
573 }
574 else
575 {
576 /* EXPERIMENTAL */
577 // TODO: change these p elements back to text nodes after processing
578 for ($i = 0, $il = $node->childNodes->length; $i < $il; $i++) {
579 $childNode = $node->childNodes->item($i);
580 if ($childNode->nodeType == 3) { // XML_TEXT_NODE
581 //$this->dbg('replacing text node with a p tag with the same content.');
582 $p = $this->dom->createElement('p');
583 $p->innerHTML = $childNode->nodeValue;
584 $p->setAttribute('style', 'display: inline;');
585 $p->setAttribute('class', 'readability-styled');
586 $childNode->parentNode->replaceChild($p, $childNode);
587 }
588 }
589 }
590 }
591 }
592
593 /**
594 * Loop through all paragraphs, and assign a score to them based on how content-y they look.
595 * Then add their score to their parent node.
596 *
597 * A score is determined by things like number of commas, class names, etc. Maybe eventually link density.
598 **/
599 $candidates = array();
600 for ($pt=0; $pt < count($nodesToScore); $pt++) {
601 $parentNode = $nodesToScore[$pt]->parentNode;
602 // $grandParentNode = $parentNode ? $parentNode->parentNode : null;
603 $grandParentNode = !$parentNode ? null : (($parentNode->parentNode instanceof DOMElement) ? $parentNode->parentNode : null);
604 $innerText = $this->getInnerText($nodesToScore[$pt]);
605
606 if (!$parentNode || !isset($parentNode->tagName)) {
607 continue;
608 }
609
610 /* If this paragraph is less than 25 characters, don't even count it. */
611 if(strlen($innerText) < 25) {
612 continue;
613 }
614
615 /* Initialize readability data for the parent. */
616 if (!$parentNode->hasAttribute('readability'))
617 {
618 $this->initializeNode($parentNode);
619 $candidates[] = $parentNode;
620 }
621
622 /* Initialize readability data for the grandparent. */
623 if ($grandParentNode && !$grandParentNode->hasAttribute('readability') && isset($grandParentNode->tagName))
624 {
625 $this->initializeNode($grandParentNode);
626 $candidates[] = $grandParentNode;
627 }
628
629 $contentScore = 0;
630
631 /* Add a point for the paragraph itself as a base. */
632 $contentScore++;
633
634 /* Add points for any commas within this paragraph */
635 $contentScore += count(explode(',', $innerText));
636
637 /* For every 100 characters in this paragraph, add another point. Up to 3 points. */
638 $contentScore += min(floor(strlen($innerText) / 100), 3);
639
640 /* Add the score to the parent. The grandparent gets half. */
641 $parentNode->getAttributeNode('readability')->value += $contentScore;
642
643 if ($grandParentNode) {
644 $grandParentNode->getAttributeNode('readability')->value += $contentScore/2;
645 }
646 }
647
648 /**
649 * After we've calculated scores, loop through all of the possible candidate nodes we found
650 * and find the one with the highest score.
651 **/
652 $topCandidate = null;
653 for ($c=0, $cl=count($candidates); $c < $cl; $c++)
654 {
655 /**
656 * Scale the final candidates score based on link density. Good content should have a
657 * relatively small link density (5% or less) and be mostly unaffected by this operation.
658 **/
659 $readability = $candidates[$c]->getAttributeNode('readability');
660 $readability->value = $readability->value * (1-$this->getLinkDensity($candidates[$c]));
661
662 $this->dbg('Candidate: ' . $candidates[$c]->tagName . ' (' . $candidates[$c]->getAttribute('class') . ':' . $candidates[$c]->getAttribute('id') . ') with score ' . $readability->value);
663
664 if (!$topCandidate || $readability->value > (int)$topCandidate->getAttribute('readability')) {
665 $topCandidate = $candidates[$c];
666 }
667 }
668
669 /**
670 * If we still have no top candidate, just use the body as a last resort.
671 * We also have to copy the body node so it is something we can modify.
672 **/
673 if ($topCandidate === null || strtoupper($topCandidate->tagName) == 'BODY')
674 {
675 $topCandidate = $this->dom->createElement('div');
676 if ($page instanceof DOMDocument) {
677 if (!isset($page->documentElement)) {
678 // we don't have a body either? what a mess! :)
679 } else {
680 $topCandidate->innerHTML = $page->documentElement->innerHTML;
681 $page->documentElement->innerHTML = '';
682 $this->reinitBody();
683 $page->documentElement->appendChild($topCandidate);
684 }
685 } else {
686 $topCandidate->innerHTML = $page->innerHTML;
687 $page->innerHTML = '';
688 $page->appendChild($topCandidate);
689 }
690 $this->initializeNode($topCandidate);
691 }
692
693 /**
694 * Now that we have the top candidate, look through its siblings for content that might also be related.
695 * Things like preambles, content split by ads that we removed, etc.
696 **/
697 $articleContent = $this->dom->createElement('div');
698 $articleContent->setAttribute('id', 'readability-content');
699 $siblingScoreThreshold = max(10, ((int)$topCandidate->getAttribute('readability')) * 0.2);
700 $siblingNodes = $topCandidate->parentNode->childNodes;
701 if (!isset($siblingNodes)) {
702 $siblingNodes = new stdClass;
703 $siblingNodes->length = 0;
704 }
705
706 for ($s=0, $sl=$siblingNodes->length; $s < $sl; $s++)
707 {
708 $siblingNode = $siblingNodes->item($s);
709 $append = false;
710
711 $this->dbg('Looking at sibling node: ' . $siblingNode->nodeName . (($siblingNode->nodeType === XML_ELEMENT_NODE && $siblingNode->hasAttribute('readability')) ? (' with score ' . $siblingNode->getAttribute('readability')) : ''));
712
713 //dbg('Sibling has score ' . ($siblingNode->readability ? siblingNode.readability.contentScore : 'Unknown'));
714
715 if ($siblingNode === $topCandidate)
716 // or if ($siblingNode->isSameNode($topCandidate))
717 {
718 $append = true;
719 }
720
721 $contentBonus = 0;
722 /* Give a bonus if sibling nodes and top candidates have the example same classname */
723 if ($siblingNode->nodeType === XML_ELEMENT_NODE && $siblingNode->getAttribute('class') == $topCandidate->getAttribute('class') && $topCandidate->getAttribute('class') != '') {
724 $contentBonus += ((int)$topCandidate->getAttribute('readability')) * 0.2;
725 }
726
727 if ($siblingNode->nodeType === XML_ELEMENT_NODE && $siblingNode->hasAttribute('readability') && (((int)$siblingNode->getAttribute('readability')) + $contentBonus) >= $siblingScoreThreshold)
728 {
729 $append = true;
730 }
731
732 if (strtoupper($siblingNode->nodeName) == 'P') {
733 $linkDensity = $this->getLinkDensity($siblingNode);
734 $nodeContent = $this->getInnerText($siblingNode);
735 $nodeLength = strlen($nodeContent);
736
737 if ($nodeLength > 80 && $linkDensity < 0.25)
738 {
739 $append = true;
740 }
741 else if ($nodeLength < 80 && $linkDensity === 0 && preg_match('/\.( |$)/', $nodeContent))
742 {
743 $append = true;
744 }
745 }
746
747 if ($append)
748 {
749 $this->dbg('Appending node: ' . $siblingNode->nodeName);
750
751 $nodeToAppend = null;
752 $sibNodeName = strtoupper($siblingNode->nodeName);
753 if ($sibNodeName != 'DIV' && $sibNodeName != 'P') {
754 /* We have a node that isn't a common block level element, like a form or td tag. Turn it into a div so it doesn't get filtered out later by accident. */
755
756 $this->dbg('Altering siblingNode of ' . $sibNodeName . ' to div.');
757 $nodeToAppend = $this->dom->createElement('div');
758 try {
759 $nodeToAppend->setAttribute('id', $siblingNode->getAttribute('id'));
760 $nodeToAppend->innerHTML = $siblingNode->innerHTML;
761 }
762 catch(Exception $e)
763 {
764 $this->dbg('Could not alter siblingNode to div, reverting back to original.');
765 $nodeToAppend = $siblingNode;
766 $s--;
767 $sl--;
768 }
769 } else {
770 $nodeToAppend = $siblingNode;
771 $s--;
772 $sl--;
773 }
774
775 /* To ensure a node does not interfere with readability styles, remove its classnames */
776 $nodeToAppend->removeAttribute('class');
777
778 /* Append sibling and subtract from our list because it removes the node when you append to another node */
779 $articleContent->appendChild($nodeToAppend);
780 }
781 }
782
783 /**
784 * So we have all of the content that we need. Now we clean it up for presentation.
785 **/
786 $this->prepArticle($articleContent);
787
788 /**
789 * Now that we've gone through the full algorithm, check to see if we got any meaningful content.
790 * If we didn't, we may need to re-run grabArticle with different flags set. This gives us a higher
791 * likelihood of finding the content, and the sieve approach gives us a higher likelihood of
792 * finding the -right- content.
793 **/
794 if (strlen($this->getInnerText($articleContent, false)) < 250)
795 {
796 // TODO: find out why element disappears sometimes, e.g. for this URL http://www.businessinsider.com/6-hedge-fund-etfs-for-average-investors-2011-7
797 // in the meantime, we check and create an empty element if it's not there.
798 $this->reinitBody();
799
800 if ($this->flagIsActive(self::FLAG_STRIP_UNLIKELYS)) {
801 $this->removeFlag(self::FLAG_STRIP_UNLIKELYS);
802 return $this->grabArticle($this->body);
803 }
804 else if ($this->flagIsActive(self::FLAG_WEIGHT_CLASSES)) {
805 $this->removeFlag(self::FLAG_WEIGHT_CLASSES);
806 return $this->grabArticle($this->body);
807 }
808 else if ($this->flagIsActive(self::FLAG_CLEAN_CONDITIONALLY)) {
809 $this->removeFlag(self::FLAG_CLEAN_CONDITIONALLY);
810 return $this->grabArticle($this->body);
811 }
812 else {
813 return false;
814 }
815 }
816 return $articleContent;
817 }
818
819 /**
820 * Remove script tags from document
821 *
822 * @param DOMElement
823 * @return void
824 */
825 public function removeScripts($doc) {
826 $scripts = $doc->getElementsByTagName('script');
827 for($i = $scripts->length-1; $i >= 0; $i--)
828 {
829 $scripts->item($i)->parentNode->removeChild($scripts->item($i));
830 }
831 }
832
833 /**
834 * Get the inner text of a node.
835 * This also strips out any excess whitespace to be found.
836 *
837 * @param DOMElement $
838 * @param boolean $normalizeSpaces (default: true)
839 * @return string
840 **/
841 public function getInnerText($e, $normalizeSpaces=true) {
842 $textContent = '';
843
844 if (!isset($e->textContent) || $e->textContent == '') {
845 return '';
846 }
847
848 $textContent = trim($e->textContent);
849
850 if ($normalizeSpaces) {
851 return preg_replace($this->regexps['normalize'], ' ', $textContent);
852 } else {
853 return $textContent;
854 }
855 }
856
857 /**
858 * Get the number of times a string $s appears in the node $e.
859 *
860 * @param DOMElement $e
861 * @param string - what to count. Default is ","
862 * @return number (integer)
863 **/
864 public function getCharCount($e, $s=',') {
865 return substr_count($this->getInnerText($e), $s);
866 }
867
868 /**
869 * Remove the style attribute on every $e and under.
870 *
871 * @param DOMElement $e
872 * @return void
873 */
874 public function cleanStyles($e) {
875 if (!is_object($e)) return;
876 $elems = $e->getElementsByTagName('*');
877 foreach ($elems as $elem) {
878 $elem->removeAttribute('style');
879 }
880 }
881
882 /**
883 * Get the density of links as a percentage of the content
884 * This is the amount of text that is inside a link divided by the total text in the node.
885 *
886 * @param DOMElement $e
887 * @return number (float)
888 */
889 public function getLinkDensity($e) {
890 $links = $e->getElementsByTagName('a');
891 $textLength = strlen($this->getInnerText($e));
892 $linkLength = 0;
893 for ($i=0, $il=$links->length; $i < $il; $i++)
894 {
895 $linkLength += strlen($this->getInnerText($links->item($i)));
896 }
897 if ($textLength > 0) {
898 return $linkLength / $textLength;
899 } else {
900 return 0;
901 }
902 }
903
904 /**
905 * Get an elements class/id weight. Uses regular expressions to tell if this
906 * element looks good or bad.
907 *
908 * @param DOMElement $e
909 * @return number (Integer)
910 */
911 public function getClassWeight($e) {
912 if(!$this->flagIsActive(self::FLAG_WEIGHT_CLASSES)) {
913 return 0;
914 }
915
916 $weight = 0;
917
918 /* Look for a special classname */
919 if ($e->hasAttribute('class') && $e->getAttribute('class') != '')
920 {
921 if (preg_match($this->regexps['negative'], $e->getAttribute('class'))) {
922 $weight -= 25;
923 }
924 if (preg_match($this->regexps['positive'], $e->getAttribute('class'))) {
925 $weight += 25;
926 }
927 }
928
929 /* Look for a special ID */
930 if ($e->hasAttribute('id') && $e->getAttribute('id') != '')
931 {
932 if (preg_match($this->regexps['negative'], $e->getAttribute('id'))) {
933 $weight -= 25;
934 }
935 if (preg_match($this->regexps['positive'], $e->getAttribute('id'))) {
936 $weight += 25;
937 }
938 }
939 return $weight;
940 }
941
942 /**
943 * Remove extraneous break tags from a node.
944 *
945 * @param DOMElement $node
946 * @return void
947 */
948 public function killBreaks($node) {
949 $html = $node->innerHTML;
950 $html = preg_replace($this->regexps['killBreaks'], '<br />', $html);
951 $node->innerHTML = $html;
952 }
953
954 /**
955 * Clean a node of all elements of type "tag".
956 * (Unless it's a youtube/vimeo video. People love movies.)
957 *
958 * Updated 2012-09-18 to preserve youtube/vimeo iframes
959 *
960 * @param DOMElement $e
961 * @param string $tag
962 * @return void
963 */
964 public function clean($e, $tag) {
965 $targetList = $e->getElementsByTagName($tag);
966 $isEmbed = ($tag == 'iframe' || $tag == 'object' || $tag == 'embed');
967
968 for ($y=$targetList->length-1; $y >= 0; $y--) {
969 /* Allow youtube and vimeo videos through as people usually want to see those. */
970 if ($isEmbed) {
971 $attributeValues = '';
972 for ($i=0, $il=$targetList->item($y)->attributes->length; $i < $il; $i++) {
973 $attributeValues .= $targetList->item($y)->attributes->item($i)->value . '|'; // DOMAttr? (TODO: test)
974 }
975
976 /* First, check the elements attributes to see if any of them contain youtube or vimeo */
977 if (preg_match($this->regexps['video'], $attributeValues)) {
978 continue;
979 }
980
981 /* Then check the elements inside this element for the same. */
982 if (preg_match($this->regexps['video'], $targetList->item($y)->innerHTML)) {
983 continue;
984 }
985 }
986 $targetList->item($y)->parentNode->removeChild($targetList->item($y));
987 }
988 }
989
990 /**
991 * Clean an element of all tags of type "tag" if they look fishy.
992 * "Fishy" is an algorithm based on content length, classnames,
993 * link density, number of images & embeds, etc.
994 *
995 * @param DOMElement $e
996 * @param string $tag
997 * @return void
998 */
999 public function cleanConditionally($e, $tag) {
1000 if (!$this->flagIsActive(self::FLAG_CLEAN_CONDITIONALLY)) {
1001 return;
1002 }
1003
1004 $tagsList = $e->getElementsByTagName($tag);
1005 $curTagsLength = $tagsList->length;
1006
1007 /**
1008 * Gather counts for other typical elements embedded within.
1009 * Traverse backwards so we can remove nodes at the same time without effecting the traversal.
1010 *
1011 * TODO: Consider taking into account original contentScore here.
1012 */
1013 for ($i=$curTagsLength-1; $i >= 0; $i--) {
1014 $weight = $this->getClassWeight($tagsList->item($i));
1015 $contentScore = ($tagsList->item($i)->hasAttribute('readability')) ? (int)$tagsList->item($i)->getAttribute('readability') : 0;
1016
1017 $this->dbg('Cleaning Conditionally ' . $tagsList->item($i)->tagName . ' (' . $tagsList->item($i)->getAttribute('class') . ':' . $tagsList->item($i)->getAttribute('id') . ')' . (($tagsList->item($i)->hasAttribute('readability')) ? (' with score ' . $tagsList->item($i)->getAttribute('readability')) : ''));
1018
1019 if ($weight + $contentScore < 0) {
1020 $tagsList->item($i)->parentNode->removeChild($tagsList->item($i));
1021 }
1022 else if ( $this->getCharCount($tagsList->item($i), ',') < 10) {
1023 /**
1024 * If there are not very many commas, and the number of
1025 * non-paragraph elements is more than paragraphs or other ominous signs, remove the element.
1026 **/
1027 $p = $tagsList->item($i)->getElementsByTagName('p')->length;
1028 $img = $tagsList->item($i)->getElementsByTagName('img')->length;
1029 $li = $tagsList->item($i)->getElementsByTagName('li')->length-100;
1030 $input = $tagsList->item($i)->getElementsByTagName('input')->length;
1031 $a = $tagsList->item($i)->getElementsByTagName('a')->length;
1032
1033 $embedCount = 0;
1034 $embeds = $tagsList->item($i)->getElementsByTagName('embed');
1035 for ($ei=0, $il=$embeds->length; $ei < $il; $ei++) {
1036 if (preg_match($this->regexps['video'], $embeds->item($ei)->getAttribute('src'))) {
1037 $embedCount++;
1038 }
1039 }
1040 $embeds = $tagsList->item($i)->getElementsByTagName('iframe');
1041 for ($ei=0, $il=$embeds->length; $ei < $il; $ei++) {
1042 if (preg_match($this->regexps['video'], $embeds->item($ei)->getAttribute('src'))) {
1043 $embedCount++;
1044 }
1045 }
1046
1047 $linkDensity = $this->getLinkDensity($tagsList->item($i));
1048 $contentLength = strlen($this->getInnerText($tagsList->item($i)));
1049 $toRemove = false;
1050
1051 if ($this->lightClean) {
1052 $this->dbg('Light clean...');
1053 if ( ($img > $p) && ($img > 4) ) {
1054 $this->dbg(' more than 4 images and more image elements than paragraph elements');
1055 $toRemove = true;
1056 } else if ($li > $p && $tag != 'ul' && $tag != 'ol') {
1057 $this->dbg(' too many <li> elements, and parent is not <ul> or <ol>');
1058 $toRemove = true;
1059 } else if ( $input > floor($p/3) ) {
1060 $this->dbg(' too many <input> elements');
1061 $toRemove = true;
1062 } else if ($contentLength < 10 && ($embedCount === 0 && ($img === 0 || $img > 2))) {
1063 $this->dbg(' content length less than 10 chars, 0 embeds and either 0 images or more than 2 images');
1064 $toRemove = true;
1065 } else if($weight < 25 && $linkDensity > 0.2) {
1066 $this->dbg(' weight smaller than 25 and link density above 0.2');
1067 $toRemove = true;
1068 } else if($a > 2 && ($weight >= 25 && $linkDensity > 0.5)) {
1069 $this->dbg(' more than 2 links and weight above 25 but link density greater than 0.5');
1070 $toRemove = true;
1071 } else if($embedCount > 3) {
1072 $this->dbg(' more than 3 embeds');
1073 $toRemove = true;
1074 }
1075 } else {
1076 $this->dbg('Standard clean...');
1077 if ( $img > $p ) {
1078 $this->dbg(' more image elements than paragraph elements');
1079 $toRemove = true;
1080 } else if ($li > $p && $tag != 'ul' && $tag != 'ol') {
1081 $this->dbg(' too many <li> elements, and parent is not <ul> or <ol>');
1082 $toRemove = true;
1083 } else if ( $input > floor($p/3) ) {
1084 $this->dbg(' too many <input> elements');
1085 $toRemove = true;
1086 } else if ($contentLength < 25 && ($img === 0 || $img > 2) ) {
1087 $this->dbg(' content length less than 25 chars and 0 images, or more than 2 images');
1088 $toRemove = true;
1089 } else if($weight < 25 && $linkDensity > 0.2) {
1090 $this->dbg(' weight smaller than 25 and link density above 0.2');
1091 $toRemove = true;
1092 } else if($weight >= 25 && $linkDensity > 0.5) {
1093 $this->dbg(' weight above 25 but link density greater than 0.5');
1094 $toRemove = true;
1095 } else if(($embedCount == 1 && $contentLength < 75) || $embedCount > 1) {
1096 $this->dbg(' 1 embed and content length smaller than 75 chars, or more than one embed');
1097 $toRemove = true;
1098 }
1099 }
1100
1101 if ($toRemove) {
1102 //$this->dbg('Removing: '.$tagsList->item($i)->innerHTML);
1103 $tagsList->item($i)->parentNode->removeChild($tagsList->item($i));
1104 }
1105 }
1106 }
1107 }
1108
1109 /**
1110 * Clean out spurious headers from an Element. Checks things like classnames and link density.
1111 *
1112 * @param DOMElement $e
1113 * @return void
1114 */
1115 public function cleanHeaders($e) {
1116 for ($headerIndex = 1; $headerIndex < 3; $headerIndex++) {
1117 $headers = $e->getElementsByTagName('h' . $headerIndex);
1118 for ($i=$headers->length-1; $i >=0; $i--) {
1119 if ($this->getClassWeight($headers->item($i)) < 0 || $this->getLinkDensity($headers->item($i)) > 0.33) {
1120 $headers->item($i)->parentNode->removeChild($headers->item($i));
1121 }
1122 }
1123 }
1124 }
1125
1126 public function flagIsActive($flag) {
1127 return ($this->flags & $flag) > 0;
1128 }
1129
1130 public function addFlag($flag) {
1131 $this->flags = $this->flags | $flag;
1132 }
1133
1134 public function removeFlag($flag) {
1135 $this->flags = $this->flags & ~$flag;
1136 }
1137
83 /** 1138 /**
84 * All of the regular expressions in use within readability. 1139 * Will recreate previously deleted body property
85 * Defined up here so we don't instantiate them repeatedly in loops. 1140 *
86 **/ 1141 * @return void
87 public $regexps = array( 1142 */
88 'unlikelyCandidates' => '/combx|comment|community|disqus|extra|foot|header|menu|remark|rss|shoutbox|sidebar|sponsor|ad-break|agegate|pagination|pager|popup/i', 1143 protected function reinitBody() {
89 'okMaybeItsACandidate' => '/and|article|body|column|main|shadow/i', 1144 if (!isset($this->body->childNodes)) {
90 'positive' => '/article|body|content|entry|hentry|main|page|attachment|pagination|post|text|blog|story/i', 1145 $this->body = $this->dom->createElement('body');
91 'negative' => '/combx|comment|com-|contact|foot|footer|_nav|footnote|masthead|media|meta|outbrain|promo|related|scroll|shoutbox|sidebar|sponsor|shopping|tags|tool|widget/i',
92 'divToPElements' => '/<(a|blockquote|dl|div|img|ol|p|pre|table|ul)/i',
93 'replaceBrs' => '/(<br[^>]*>[ \n\r\t]*){2,}/i',
94 'replaceFonts' => '/<(\/?)font[^>]*>/i',
95 // 'trimRe' => '/^\s+|\s+$/g', // PHP has trim()
96 'normalize' => '/\s{2,}/',
97 'killBreaks' => '/(<br\s*\/?>(\s|&nbsp;?)*){1,}/',
98 'video' => '!//(player\.|www\.)?(youtube|vimeo|viddler)\.com!i',
99 'skipFootnoteLink' => '/^\s*(\[?[a-z0-9]{1,2}\]?|^|edit|citation needed)\s*$/i'
100 );
101
102 /* constants */
103 const FLAG_STRIP_UNLIKELYS = 1;
104 const FLAG_WEIGHT_CLASSES = 2;
105 const FLAG_CLEAN_CONDITIONALLY = 4;
106
107 /**
108 * Create instance of Readability
109 * @param string UTF-8 encoded string
110 * @param string (optional) URL associated with HTML (used for footnotes)
111 * @param string which parser to use for turning raw HTML into a DOMDocument (either 'libxml' or 'html5lib')
112 */
113 function __construct($html, $url=null, $parser='libxml')
114 {
115 $this->url = $url;
116 /* Turn all double br's into p's */
117 $html = preg_replace($this->regexps['replaceBrs'], '</p><p>', $html);
118 $html = preg_replace($this->regexps['replaceFonts'], '<$1span>', $html);
119 $html = mb_convert_encoding($html, 'HTML-ENTITIES', "UTF-8");
120 if (trim($html) == '') $html = '<html></html>';
121 if ($parser=='html5lib' && ($this->dom = HTML5_Parser::parse($html))) {
122 // all good
123 } else {
124 $this->dom = new DOMDocument();
125 $this->dom->preserveWhiteSpace = false;
126 @$this->dom->loadHTML($html);
127 }
128 $this->dom->registerNodeClass('DOMElement', 'JSLikeHTMLElement');
129 }
130
131 /**
132 * Get article title element
133 * @return DOMElement
134 */
135 public function getTitle() {
136 return $this->articleTitle;
137 }
138
139 /**
140 * Get article content element
141 * @return DOMElement
142 */
143 public function getContent() {
144 return $this->articleContent;
145 }
146
147 /**
148 * Runs readability.
149 *
150 * Workflow:
151 * 1. Prep the document by removing script tags, css, etc.
152 * 2. Build readability's DOM tree.
153 * 3. Grab the article content from the current dom tree.
154 * 4. Replace the current DOM tree with the new one.
155 * 5. Read peacefully.
156 *
157 * @return boolean true if we found content, false otherwise
158 **/
159 public function init()
160 {
161 if (!isset($this->dom->documentElement)) return false;
162 $this->removeScripts($this->dom);
163 //die($this->getInnerHTML($this->dom->documentElement));
164
165 // Assume successful outcome
166 $this->success = true;
167
168 $bodyElems = $this->dom->getElementsByTagName('body');
169 if ($bodyElems->length > 0) {
170 if ($this->bodyCache == null) {
171 $this->bodyCache = $bodyElems->item(0)->innerHTML;
172 }
173 if ($this->body == null) {
174 $this->body = $bodyElems->item(0);
175 }
176 }
177
178 $this->prepDocument();
179
180 //die($this->dom->documentElement->parentNode->nodeType);
181 //$this->setInnerHTML($this->dom->documentElement, $this->getInnerHTML($this->dom->documentElement));
182 //die($this->getInnerHTML($this->dom->documentElement));
183
184 /* Build readability's DOM tree */
185 $overlay = $this->dom->createElement('div');
186 $innerDiv = $this->dom->createElement('div');
187 $articleTitle = $this->getArticleTitle();
188 $articleContent = $this->grabArticle();
189
190 if (!$articleContent) {
191 $this->success = false;
192 $articleContent = $this->dom->createElement('div');
193 $articleContent->setAttribute('id', 'readability-content');
194 $articleContent->innerHTML = '<p>Sorry, Readability was unable to parse this page for content.</p>';
195 }
196
197 $overlay->setAttribute('id', 'readOverlay');
198 $innerDiv->setAttribute('id', 'readInner');
199
200 /* Glue the structure of our document together. */
201 $innerDiv->appendChild($articleTitle);
202 $innerDiv->appendChild($articleContent);
203 $overlay->appendChild($innerDiv);
204
205 /* Clear the old HTML, insert the new content. */
206 $this->body->innerHTML = '';
207 $this->body->appendChild($overlay);
208 //document.body.insertBefore(overlay, document.body.firstChild);
209 $this->body->removeAttribute('style');
210
211 $this->postProcessContent($articleContent);
212
213 // Set title and content instance variables
214 $this->articleTitle = $articleTitle;
215 $this->articleContent = $articleContent;
216
217 return $this->success;
218 }
219
220 /**
221 * Debug
222 */
223 protected function dbg($msg) {
224 if ($this->debug) echo '* ',$msg, "\n";
225 }
226
227 /**
228 * Run any post-process modifications to article content as necessary.
229 *
230 * @param DOMElement
231 * @return void
232 */
233 public function postProcessContent($articleContent) {
234 if ($this->convertLinksToFootnotes && !preg_match('/wikipedia\.org/', @$this->url)) {
235 $this->addFootnotes($articleContent);
236 }
237 }
238
239 /**
240 * Get the article title as an H1.
241 *
242 * @return DOMElement
243 */
244 protected function getArticleTitle() {
245 $curTitle = '';
246 $origTitle = '';
247
248 try {
249 $curTitle = $origTitle = $this->getInnerText($this->dom->getElementsByTagName('title')->item(0));
250 } catch(Exception $e) {}
251
252 if (preg_match('/ [\|\-] /', $curTitle))
253 {
254 $curTitle = preg_replace('/(.*)[\|\-] .*/i', '$1', $origTitle);
255
256 if (count(explode(' ', $curTitle)) < 3) {
257 $curTitle = preg_replace('/[^\|\-]*[\|\-](.*)/i', '$1', $origTitle);
258 }
259 }
260 else if (strpos($curTitle, ': ') !== false)
261 {
262 $curTitle = preg_replace('/.*:(.*)/i', '$1', $origTitle);
263
264 if (count(explode(' ', $curTitle)) < 3) {
265 $curTitle = preg_replace('/[^:]*[:](.*)/i','$1', $origTitle);
266 }
267 }
268 else if(strlen($curTitle) > 150 || strlen($curTitle) < 15)
269 {
270 $hOnes = $this->dom->getElementsByTagName('h1');
271 if($hOnes->length == 1)
272 {
273 $curTitle = $this->getInnerText($hOnes->item(0));
274 }
275 }
276
277 $curTitle = trim($curTitle);
278
279 if (count(explode(' ', $curTitle)) <= 4) {
280 $curTitle = $origTitle;
281 }
282
283 $articleTitle = $this->dom->createElement('h1');
284 $articleTitle->innerHTML = $curTitle;
285
286 return $articleTitle;
287 }
288
289 /**
290 * Prepare the HTML document for readability to scrape it.
291 * This includes things like stripping javascript, CSS, and handling terrible markup.
292 *
293 * @return void
294 **/
295 protected function prepDocument() {
296 /**
297 * In some cases a body element can't be found (if the HTML is totally hosed for example)
298 * so we create a new body node and append it to the document.
299 */
300 if ($this->body == null)
301 {
302 $this->body = $this->dom->createElement('body');
303 $this->dom->documentElement->appendChild($this->body);
304 }
305 $this->body->setAttribute('id', 'readabilityBody');
306
307 /* Remove all style tags in head */
308 $styleTags = $this->dom->getElementsByTagName('style');
309 for ($i = $styleTags->length-1; $i >= 0; $i--)
310 {
311 $styleTags->item($i)->parentNode->removeChild($styleTags->item($i));
312 }
313
314 /* Turn all double br's into p's */
315 /* Note, this is pretty costly as far as processing goes. Maybe optimize later. */
316 //document.body.innerHTML = document.body.innerHTML.replace(readability.regexps.replaceBrs, '</p><p>').replace(readability.regexps.replaceFonts, '<$1span>');
317 // We do this in the constructor for PHP as that's when we have raw HTML - before parsing it into a DOM tree.
318 // Manipulating innerHTML as it's done in JS is not possible in PHP.
319 }
320
321 /**
322 * For easier reading, convert this document to have footnotes at the bottom rather than inline links.
323 * @see http://www.roughtype.com/archives/2010/05/experiments_in.php
324 *
325 * @return void
326 **/
327 public function addFootnotes($articleContent) {
328 $footnotesWrapper = $this->dom->createElement('div');
329 $footnotesWrapper->setAttribute('id', 'readability-footnotes');
330 $footnotesWrapper->innerHTML = '<h3>References</h3>';
331
332 $articleFootnotes = $this->dom->createElement('ol');
333 $articleFootnotes->setAttribute('id', 'readability-footnotes-list');
334 $footnotesWrapper->appendChild($articleFootnotes);
335
336 $articleLinks = $articleContent->getElementsByTagName('a');
337
338 $linkCount = 0;
339 for ($i = 0; $i < $articleLinks->length; $i++)
340 {
341 $articleLink = $articleLinks->item($i);
342 $footnoteLink = $articleLink->cloneNode(true);
343 $refLink = $this->dom->createElement('a');
344 $footnote = $this->dom->createElement('li');
345 $linkDomain = @parse_url($footnoteLink->getAttribute('href'), PHP_URL_HOST);
346 if (!$linkDomain && isset($this->url)) $linkDomain = @parse_url($this->url, PHP_URL_HOST);
347 //linkDomain = footnoteLink.host ? footnoteLink.host : document.location.host,
348 $linkText = $this->getInnerText($articleLink);
349
350 if ((strpos($articleLink->getAttribute('class'), 'readability-DoNotFootnote') !== false) || preg_match($this->regexps['skipFootnoteLink'], $linkText)) {
351 continue;
352 }
353
354 $linkCount++;
355
356 /** Add a superscript reference after the article link */
357 $refLink->setAttribute('href', '#readabilityFootnoteLink-' . $linkCount);
358 $refLink->innerHTML = '<small><sup>[' . $linkCount . ']</sup></small>';
359 $refLink->setAttribute('class', 'readability-DoNotFootnote');
360 $refLink->setAttribute('style', 'color: inherit;');
361
362 //TODO: does this work or should we use DOMNode.isSameNode()?
363 if ($articleLink->parentNode->lastChild == $articleLink) {
364 $articleLink->parentNode->appendChild($refLink);
365 } else {
366 $articleLink->parentNode->insertBefore($refLink, $articleLink->nextSibling);
367 }
368
369 $articleLink->setAttribute('style', 'color: inherit; text-decoration: none;');
370 $articleLink->setAttribute('name', 'readabilityLink-' . $linkCount);
371
372 $footnote->innerHTML = '<small><sup><a href="#readabilityLink-' . $linkCount . '" title="Jump to Link in Article">^</a></sup></small> ';
373
374 $footnoteLink->innerHTML = ($footnoteLink->getAttribute('title') != '' ? $footnoteLink->getAttribute('title') : $linkText);
375 $footnoteLink->setAttribute('name', 'readabilityFootnoteLink-' . $linkCount);
376
377 $footnote->appendChild($footnoteLink);
378 if ($linkDomain) $footnote->innerHTML = $footnote->innerHTML . '<small> (' . $linkDomain . ')</small>';
379
380 $articleFootnotes->appendChild($footnote);
381 }
382
383 if ($linkCount > 0) {
384 $articleContent->appendChild($footnotesWrapper);
385 }
386 }
387
388 /**
389 * Reverts P elements with class 'readability-styled'
390 * to text nodes - which is what they were before.
391 *
392 * @param DOMElement
393 * @return void
394 */
395 function revertReadabilityStyledElements($articleContent) {
396 $xpath = new DOMXPath($articleContent->ownerDocument);
397 $elems = $xpath->query('.//p[@class="readability-styled"]', $articleContent);
398 //$elems = $articleContent->getElementsByTagName('p');
399 for ($i = $elems->length-1; $i >= 0; $i--) {
400 $e = $elems->item($i);
401 $e->parentNode->replaceChild($articleContent->ownerDocument->createTextNode($e->textContent), $e);
402 //if ($e->hasAttribute('class') && $e->getAttribute('class') == 'readability-styled') {
403 // $e->parentNode->replaceChild($this->dom->createTextNode($e->textContent), $e);
404 //}
405 }
406 }
407
408 /**
409 * Prepare the article node for display. Clean out any inline styles,
410 * iframes, forms, strip extraneous <p> tags, etc.
411 *
412 * @param DOMElement
413 * @return void
414 */
415 function prepArticle($articleContent) {
416 $this->cleanStyles($articleContent);
417 $this->killBreaks($articleContent);
418 if ($this->revertForcedParagraphElements) {
419 $this->revertReadabilityStyledElements($articleContent);
420 }
421
422 /* Clean out junk from the article content */
423 $this->cleanConditionally($articleContent, 'form');
424 $this->clean($articleContent, 'object');
425 $this->clean($articleContent, 'h1');
426
427 /**
428 * If there is only one h2, they are probably using it
429 * as a header and not a subheader, so remove it since we already have a header.
430 ***/
431 if (!$this->lightClean && ($articleContent->getElementsByTagName('h2')->length == 1)) {
432 $this->clean($articleContent, 'h2');
433 }
434 $this->clean($articleContent, 'iframe');
435
436 $this->cleanHeaders($articleContent);
437
438 /* Do these last as the previous stuff may have removed junk that will affect these */
439 $this->cleanConditionally($articleContent, 'table');
440 $this->cleanConditionally($articleContent, 'ul');
441 $this->cleanConditionally($articleContent, 'div');
442
443 /* Remove extra paragraphs */
444 $articleParagraphs = $articleContent->getElementsByTagName('p');
445 for ($i = $articleParagraphs->length-1; $i >= 0; $i--)
446 {
447 $imgCount = $articleParagraphs->item($i)->getElementsByTagName('img')->length;
448 $embedCount = $articleParagraphs->item($i)->getElementsByTagName('embed')->length;
449 $objectCount = $articleParagraphs->item($i)->getElementsByTagName('object')->length;
450 $iframeCount = $articleParagraphs->item($i)->getElementsByTagName('iframe')->length;
451
452 if ($imgCount === 0 && $embedCount === 0 && $objectCount === 0 && $iframeCount === 0 && $this->getInnerText($articleParagraphs->item($i), false) == '')
453 {
454 $articleParagraphs->item($i)->parentNode->removeChild($articleParagraphs->item($i));
455 }
456 }
457
458 try {
459 $articleContent->innerHTML = preg_replace('/<br[^>]*>\s*<p/i', '<p', $articleContent->innerHTML);
460 //articleContent.innerHTML = articleContent.innerHTML.replace(/<br[^>]*>\s*<p/gi, '<p');
461 }
462 catch (Exception $e) {
463 $this->dbg("Cleaning innerHTML of breaks failed. This is an IE strict-block-elements bug. Ignoring.: " . $e);
464 }
465 }
466
467 /**
468 * Initialize a node with the readability object. Also checks the
469 * className/id for special names to add to its score.
470 *
471 * @param Element
472 * @return void
473 **/
474 protected function initializeNode($node) {
475 $readability = $this->dom->createAttribute('readability');
476 $readability->value = 0; // this is our contentScore
477 $node->setAttributeNode($readability);
478
479 switch (strtoupper($node->tagName)) { // unsure if strtoupper is needed, but using it just in case
480 case 'DIV':
481 $readability->value += 5;
482 break;
483
484 case 'PRE':
485 case 'TD':
486 case 'BLOCKQUOTE':
487 $readability->value += 3;
488 break;
489
490 case 'ADDRESS':
491 case 'OL':
492 case 'UL':
493 case 'DL':
494 case 'DD':
495 case 'DT':
496 case 'LI':
497 case 'FORM':
498 $readability->value -= 3;
499 break;
500
501 case 'H1':
502 case 'H2':
503 case 'H3':
504 case 'H4':
505 case 'H5':
506 case 'H6':
507 case 'TH':
508 $readability->value -= 5;
509 break;
510 }
511 $readability->value += $this->getClassWeight($node);
512 }
513
514 /***
515 * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is
516 * most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
517 *
518 * @return DOMElement
519 **/
520 protected function grabArticle($page=null) {
521 $stripUnlikelyCandidates = $this->flagIsActive(self::FLAG_STRIP_UNLIKELYS);
522 if (!$page) $page = $this->dom;
523 $allElements = $page->getElementsByTagName('*');
524 /**
525 * First, node prepping. Trash nodes that look cruddy (like ones with the class name "comment", etc), and turn divs
526 * into P tags where they have been used inappropriately (as in, where they contain no other block level elements.)
527 *
528 * Note: Assignment from index for performance. See http://www.peachpit.com/articles/article.aspx?p=31567&seqNum=5
529 * TODO: Shouldn't this be a reverse traversal?
530 **/
531 $node = null;
532 $nodesToScore = array();
533 for ($nodeIndex = 0; ($node = $allElements->item($nodeIndex)); $nodeIndex++) {
534 //for ($nodeIndex=$targetList->length-1; $nodeIndex >= 0; $nodeIndex--) {
535 //$node = $targetList->item($nodeIndex);
536 $tagName = strtoupper($node->tagName);
537 /* Remove unlikely candidates */
538 if ($stripUnlikelyCandidates) {
539 $unlikelyMatchString = $node->getAttribute('class') . $node->getAttribute('id');
540 if (
541 preg_match($this->regexps['unlikelyCandidates'], $unlikelyMatchString) &&
542 !preg_match($this->regexps['okMaybeItsACandidate'], $unlikelyMatchString) &&
543 $tagName != 'BODY'
544 )
545 {
546 $this->dbg('Removing unlikely candidate - ' . $unlikelyMatchString);
547 //$nodesToRemove[] = $node;
548 $node->parentNode->removeChild($node);
549 $nodeIndex--;
550 continue;
551 }
552 }
553
554 if ($tagName == 'P' || $tagName == 'TD' || $tagName == 'PRE') {
555 $nodesToScore[] = $node;
556 }
557
558 /* Turn all divs that don't have children block level elements into p's */
559 if ($tagName == 'DIV') {
560 if (!preg_match($this->regexps['divToPElements'], $node->innerHTML)) {
561 //$this->dbg('Altering div to p');
562 $newNode = $this->dom->createElement('p');
563 try {
564 $newNode->innerHTML = $node->innerHTML;
565 //$nodesToReplace[] = array('new'=>$newNode, 'old'=>$node);
566 $node->parentNode->replaceChild($newNode, $node);
567 $nodeIndex--;
568 $nodesToScore[] = $node; // or $newNode?
569 }
570 catch(Exception $e) {
571 $this->dbg('Could not alter div to p, reverting back to div.: ' . $e);
572 }
573 }
574 else
575 {
576 /* EXPERIMENTAL */
577 // TODO: change these p elements back to text nodes after processing
578 for ($i = 0, $il = $node->childNodes->length; $i < $il; $i++) {
579 $childNode = $node->childNodes->item($i);
580 if ($childNode->nodeType == 3) { // XML_TEXT_NODE
581 //$this->dbg('replacing text node with a p tag with the same content.');
582 $p = $this->dom->createElement('p');
583 $p->innerHTML = $childNode->nodeValue;
584 $p->setAttribute('style', 'display: inline;');
585 $p->setAttribute('class', 'readability-styled');
586 $childNode->parentNode->replaceChild($p, $childNode);
587 }
588 }
589 }
590 }
591 }
592
593 /**
594 * Loop through all paragraphs, and assign a score to them based on how content-y they look.
595 * Then add their score to their parent node.
596 *
597 * A score is determined by things like number of commas, class names, etc. Maybe eventually link density.
598 **/
599 $candidates = array();
600 for ($pt=0; $pt < count($nodesToScore); $pt++) {
601 $parentNode = $nodesToScore[$pt]->parentNode;
602 // $grandParentNode = $parentNode ? $parentNode->parentNode : null;
603 $grandParentNode = !$parentNode ? null : (($parentNode->parentNode instanceof DOMElement) ? $parentNode->parentNode : null);
604 $innerText = $this->getInnerText($nodesToScore[$pt]);
605
606 if (!$parentNode || !isset($parentNode->tagName)) {
607 continue;
608 }
609
610 /* If this paragraph is less than 25 characters, don't even count it. */
611 if(strlen($innerText) < 25) {
612 continue;
613 }
614
615 /* Initialize readability data for the parent. */
616 if (!$parentNode->hasAttribute('readability'))
617 {
618 $this->initializeNode($parentNode);
619 $candidates[] = $parentNode;
620 }
621
622 /* Initialize readability data for the grandparent. */
623 if ($grandParentNode && !$grandParentNode->hasAttribute('readability') && isset($grandParentNode->tagName))
624 {
625 $this->initializeNode($grandParentNode);
626 $candidates[] = $grandParentNode;
627 }
628
629 $contentScore = 0;
630
631 /* Add a point for the paragraph itself as a base. */
632 $contentScore++;
633
634 /* Add points for any commas within this paragraph */
635 $contentScore += count(explode(',', $innerText));
636
637 /* For every 100 characters in this paragraph, add another point. Up to 3 points. */
638 $contentScore += min(floor(strlen($innerText) / 100), 3);
639
640 /* Add the score to the parent. The grandparent gets half. */
641 $parentNode->getAttributeNode('readability')->value += $contentScore;
642
643 if ($grandParentNode) {
644 $grandParentNode->getAttributeNode('readability')->value += $contentScore/2;
645 }
646 }
647
648 /**
649 * After we've calculated scores, loop through all of the possible candidate nodes we found
650 * and find the one with the highest score.
651 **/
652 $topCandidate = null;
653 for ($c=0, $cl=count($candidates); $c < $cl; $c++)
654 {
655 /**
656 * Scale the final candidates score based on link density. Good content should have a
657 * relatively small link density (5% or less) and be mostly unaffected by this operation.
658 **/
659 $readability = $candidates[$c]->getAttributeNode('readability');
660 $readability->value = $readability->value * (1-$this->getLinkDensity($candidates[$c]));
661
662 $this->dbg('Candidate: ' . $candidates[$c]->tagName . ' (' . $candidates[$c]->getAttribute('class') . ':' . $candidates[$c]->getAttribute('id') . ') with score ' . $readability->value);
663
664 if (!$topCandidate || $readability->value > (int)$topCandidate->getAttribute('readability')) {
665 $topCandidate = $candidates[$c];
666 }
667 }
668
669 /**
670 * If we still have no top candidate, just use the body as a last resort.
671 * We also have to copy the body node so it is something we can modify.
672 **/
673 if ($topCandidate === null || strtoupper($topCandidate->tagName) == 'BODY')
674 {
675 $topCandidate = $this->dom->createElement('div');
676 if ($page instanceof DOMDocument) {
677 if (!isset($page->documentElement)) {
678 // we don't have a body either? what a mess! :)
679 } else {
680 $topCandidate->innerHTML = $page->documentElement->innerHTML;
681 $page->documentElement->innerHTML = '';
682 $page->documentElement->appendChild($topCandidate);
683 }
684 } else {
685 $topCandidate->innerHTML = $page->innerHTML;
686 $page->innerHTML = '';
687 $page->appendChild($topCandidate);
688 }
689 $this->initializeNode($topCandidate);
690 }
691
692 /**
693 * Now that we have the top candidate, look through its siblings for content that might also be related.
694 * Things like preambles, content split by ads that we removed, etc.
695 **/
696 $articleContent = $this->dom->createElement('div');
697 $articleContent->setAttribute('id', 'readability-content');
698 $siblingScoreThreshold = max(10, ((int)$topCandidate->getAttribute('readability')) * 0.2);
699 $siblingNodes = $topCandidate->parentNode->childNodes;
700 if (!isset($siblingNodes)) {
701 $siblingNodes = new stdClass;
702 $siblingNodes->length = 0;
703 }
704
705 for ($s=0, $sl=$siblingNodes->length; $s < $sl; $s++)
706 {
707 $siblingNode = $siblingNodes->item($s);
708 $append = false;
709
710 $this->dbg('Looking at sibling node: ' . $siblingNode->nodeName . (($siblingNode->nodeType === XML_ELEMENT_NODE && $siblingNode->hasAttribute('readability')) ? (' with score ' . $siblingNode->getAttribute('readability')) : ''));
711
712 //dbg('Sibling has score ' . ($siblingNode->readability ? siblingNode.readability.contentScore : 'Unknown'));
713
714 if ($siblingNode === $topCandidate)
715 // or if ($siblingNode->isSameNode($topCandidate))
716 {
717 $append = true;
718 }
719
720 $contentBonus = 0;
721 /* Give a bonus if sibling nodes and top candidates have the example same classname */
722 if ($siblingNode->nodeType === XML_ELEMENT_NODE && $siblingNode->getAttribute('class') == $topCandidate->getAttribute('class') && $topCandidate->getAttribute('class') != '') {
723 $contentBonus += ((int)$topCandidate->getAttribute('readability')) * 0.2;
724 }
725
726 if ($siblingNode->nodeType === XML_ELEMENT_NODE && $siblingNode->hasAttribute('readability') && (((int)$siblingNode->getAttribute('readability')) + $contentBonus) >= $siblingScoreThreshold)
727 {
728 $append = true;
729 }
730
731 if (strtoupper($siblingNode->nodeName) == 'P') {
732 $linkDensity = $this->getLinkDensity($siblingNode);
733 $nodeContent = $this->getInnerText($siblingNode);
734 $nodeLength = strlen($nodeContent);
735
736 if ($nodeLength > 80 && $linkDensity < 0.25)
737 {
738 $append = true;
739 }
740 else if ($nodeLength < 80 && $linkDensity === 0 && preg_match('/\.( |$)/', $nodeContent))
741 {
742 $append = true;
743 }
744 }
745
746 if ($append)
747 {
748 $this->dbg('Appending node: ' . $siblingNode->nodeName);
749
750 $nodeToAppend = null;
751 $sibNodeName = strtoupper($siblingNode->nodeName);
752 if ($sibNodeName != 'DIV' && $sibNodeName != 'P') {
753 /* We have a node that isn't a common block level element, like a form or td tag. Turn it into a div so it doesn't get filtered out later by accident. */
754
755 $this->dbg('Altering siblingNode of ' . $sibNodeName . ' to div.');
756 $nodeToAppend = $this->dom->createElement('div');
757 try {
758 $nodeToAppend->setAttribute('id', $siblingNode->getAttribute('id'));
759 $nodeToAppend->innerHTML = $siblingNode->innerHTML;
760 }
761 catch(Exception $e)
762 {
763 $this->dbg('Could not alter siblingNode to div, reverting back to original.');
764 $nodeToAppend = $siblingNode;
765 $s--;
766 $sl--;
767 }
768 } else {
769 $nodeToAppend = $siblingNode;
770 $s--;
771 $sl--;
772 }
773
774 /* To ensure a node does not interfere with readability styles, remove its classnames */
775 $nodeToAppend->removeAttribute('class');
776
777 /* Append sibling and subtract from our list because it removes the node when you append to another node */
778 $articleContent->appendChild($nodeToAppend);
779 }
780 }
781
782 /**
783 * So we have all of the content that we need. Now we clean it up for presentation.
784 **/
785 $this->prepArticle($articleContent);
786
787 /**
788 * Now that we've gone through the full algorithm, check to see if we got any meaningful content.
789 * If we didn't, we may need to re-run grabArticle with different flags set. This gives us a higher
790 * likelihood of finding the content, and the sieve approach gives us a higher likelihood of
791 * finding the -right- content.
792 **/
793 if (strlen($this->getInnerText($articleContent, false)) < 250)
794 {
795 // TODO: find out why element disappears sometimes, e.g. for this URL http://www.businessinsider.com/6-hedge-fund-etfs-for-average-investors-2011-7
796 // in the meantime, we check and create an empty element if it's not there.
797 if (!isset($this->body->childNodes)) $this->body = $this->dom->createElement('body');
798 $this->body->innerHTML = $this->bodyCache; 1146 $this->body->innerHTML = $this->bodyCache;
799
800 if ($this->flagIsActive(self::FLAG_STRIP_UNLIKELYS)) {
801 $this->removeFlag(self::FLAG_STRIP_UNLIKELYS);
802 return $this->grabArticle($this->body);
803 }
804 else if ($this->flagIsActive(self::FLAG_WEIGHT_CLASSES)) {
805 $this->removeFlag(self::FLAG_WEIGHT_CLASSES);
806 return $this->grabArticle($this->body);
807 }
808 else if ($this->flagIsActive(self::FLAG_CLEAN_CONDITIONALLY)) {
809 $this->removeFlag(self::FLAG_CLEAN_CONDITIONALLY);
810 return $this->grabArticle($this->body);
811 }
812 else {
813 return false;
814 }
815 }
816 return $articleContent;
817 }
818
819 /**
820 * Remove script tags from document
821 *
822 * @param DOMElement
823 * @return void
824 */
825 public function removeScripts($doc) {
826 $scripts = $doc->getElementsByTagName('script');
827 for($i = $scripts->length-1; $i >= 0; $i--)
828 {
829 $scripts->item($i)->parentNode->removeChild($scripts->item($i));
830 }
831 }
832
833 /**
834 * Get the inner text of a node.
835 * This also strips out any excess whitespace to be found.
836 *
837 * @param DOMElement $
838 * @param boolean $normalizeSpaces (default: true)
839 * @return string
840 **/
841 public function getInnerText($e, $normalizeSpaces=true) {
842 $textContent = '';
843
844 if (!isset($e->textContent) || $e->textContent == '') {
845 return '';
846 }
847
848 $textContent = trim($e->textContent);
849
850 if ($normalizeSpaces) {
851 return preg_replace($this->regexps['normalize'], ' ', $textContent);
852 } else {
853 return $textContent;
854 }
855 }
856
857 /**
858 * Get the number of times a string $s appears in the node $e.
859 *
860 * @param DOMElement $e
861 * @param string - what to count. Default is ","
862 * @return number (integer)
863 **/
864 public function getCharCount($e, $s=',') {
865 return substr_count($this->getInnerText($e), $s);
866 }
867
868 /**
869 * Remove the style attribute on every $e and under.
870 *
871 * @param DOMElement $e
872 * @return void
873 */
874 public function cleanStyles($e) {
875 if (!is_object($e)) return;
876 $elems = $e->getElementsByTagName('*');
877 foreach ($elems as $elem) {
878 $elem->removeAttribute('style');
879 }
880 }
881
882 /**
883 * Get the density of links as a percentage of the content
884 * This is the amount of text that is inside a link divided by the total text in the node.
885 *
886 * @param DOMElement $e
887 * @return number (float)
888 */
889 public function getLinkDensity($e) {
890 $links = $e->getElementsByTagName('a');
891 $textLength = strlen($this->getInnerText($e));
892 $linkLength = 0;
893 for ($i=0, $il=$links->length; $i < $il; $i++)
894 {
895 $linkLength += strlen($this->getInnerText($links->item($i)));
896 }
897 if ($textLength > 0) {
898 return $linkLength / $textLength;
899 } else {
900 return 0;
901 }
902 }
903
904 /**
905 * Get an elements class/id weight. Uses regular expressions to tell if this
906 * element looks good or bad.
907 *
908 * @param DOMElement $e
909 * @return number (Integer)
910 */
911 public function getClassWeight($e) {
912 if(!$this->flagIsActive(self::FLAG_WEIGHT_CLASSES)) {
913 return 0;
914 }
915
916 $weight = 0;
917
918 /* Look for a special classname */
919 if ($e->hasAttribute('class') && $e->getAttribute('class') != '')
920 {
921 if (preg_match($this->regexps['negative'], $e->getAttribute('class'))) {
922 $weight -= 25;
923 }
924 if (preg_match($this->regexps['positive'], $e->getAttribute('class'))) {
925 $weight += 25;
926 }
927 }
928
929 /* Look for a special ID */
930 if ($e->hasAttribute('id') && $e->getAttribute('id') != '')
931 {
932 if (preg_match($this->regexps['negative'], $e->getAttribute('id'))) {
933 $weight -= 25;
934 }
935 if (preg_match($this->regexps['positive'], $e->getAttribute('id'))) {
936 $weight += 25;
937 }
938 }
939 return $weight;
940 }
941
942 /**
943 * Remove extraneous break tags from a node.
944 *
945 * @param DOMElement $node
946 * @return void
947 */
948 public function killBreaks($node) {
949 $html = $node->innerHTML;
950 $html = preg_replace($this->regexps['killBreaks'], '<br />', $html);
951 $node->innerHTML = $html;
952 }
953
954 /**
955 * Clean a node of all elements of type "tag".
956 * (Unless it's a youtube/vimeo video. People love movies.)
957 *
958 * Updated 2012-09-18 to preserve youtube/vimeo iframes
959 *
960 * @param DOMElement $e
961 * @param string $tag
962 * @return void
963 */
964 public function clean($e, $tag) {
965 $targetList = $e->getElementsByTagName($tag);
966 $isEmbed = ($tag == 'iframe' || $tag == 'object' || $tag == 'embed');
967
968 for ($y=$targetList->length-1; $y >= 0; $y--) {
969 /* Allow youtube and vimeo videos through as people usually want to see those. */
970 if ($isEmbed) {
971 $attributeValues = '';
972 for ($i=0, $il=$targetList->item($y)->attributes->length; $i < $il; $i++) {
973 $attributeValues .= $targetList->item($y)->attributes->item($i)->value . '|'; // DOMAttr? (TODO: test)
974 }
975
976 /* First, check the elements attributes to see if any of them contain youtube or vimeo */
977 if (preg_match($this->regexps['video'], $attributeValues)) {
978 continue;
979 }
980
981 /* Then check the elements inside this element for the same. */
982 if (preg_match($this->regexps['video'], $targetList->item($y)->innerHTML)) {
983 continue;
984 }
985 }
986 $targetList->item($y)->parentNode->removeChild($targetList->item($y));
987 }
988 }
989
990 /**
991 * Clean an element of all tags of type "tag" if they look fishy.
992 * "Fishy" is an algorithm based on content length, classnames,
993 * link density, number of images & embeds, etc.
994 *
995 * @param DOMElement $e
996 * @param string $tag
997 * @return void
998 */
999 public function cleanConditionally($e, $tag) {
1000 if (!$this->flagIsActive(self::FLAG_CLEAN_CONDITIONALLY)) {
1001 return;
1002 }
1003
1004 $tagsList = $e->getElementsByTagName($tag);
1005 $curTagsLength = $tagsList->length;
1006
1007 /**
1008 * Gather counts for other typical elements embedded within.
1009 * Traverse backwards so we can remove nodes at the same time without effecting the traversal.
1010 *
1011 * TODO: Consider taking into account original contentScore here.
1012 */
1013 for ($i=$curTagsLength-1; $i >= 0; $i--) {
1014 $weight = $this->getClassWeight($tagsList->item($i));
1015 $contentScore = ($tagsList->item($i)->hasAttribute('readability')) ? (int)$tagsList->item($i)->getAttribute('readability') : 0;
1016
1017 $this->dbg('Cleaning Conditionally ' . $tagsList->item($i)->tagName . ' (' . $tagsList->item($i)->getAttribute('class') . ':' . $tagsList->item($i)->getAttribute('id') . ')' . (($tagsList->item($i)->hasAttribute('readability')) ? (' with score ' . $tagsList->item($i)->getAttribute('readability')) : ''));
1018
1019 if ($weight + $contentScore < 0) {
1020 $tagsList->item($i)->parentNode->removeChild($tagsList->item($i));
1021 }
1022 else if ( $this->getCharCount($tagsList->item($i), ',') < 10) {
1023 /**
1024 * If there are not very many commas, and the number of
1025 * non-paragraph elements is more than paragraphs or other ominous signs, remove the element.
1026 **/
1027 $p = $tagsList->item($i)->getElementsByTagName('p')->length;
1028 $img = $tagsList->item($i)->getElementsByTagName('img')->length;
1029 $li = $tagsList->item($i)->getElementsByTagName('li')->length-100;
1030 $input = $tagsList->item($i)->getElementsByTagName('input')->length;
1031 $a = $tagsList->item($i)->getElementsByTagName('a')->length;
1032
1033 $embedCount = 0;
1034 $embeds = $tagsList->item($i)->getElementsByTagName('embed');
1035 for ($ei=0, $il=$embeds->length; $ei < $il; $ei++) {
1036 if (preg_match($this->regexps['video'], $embeds->item($ei)->getAttribute('src'))) {
1037 $embedCount++;
1038 }
1039 }
1040 $embeds = $tagsList->item($i)->getElementsByTagName('iframe');
1041 for ($ei=0, $il=$embeds->length; $ei < $il; $ei++) {
1042 if (preg_match($this->regexps['video'], $embeds->item($ei)->getAttribute('src'))) {
1043 $embedCount++;
1044 }
1045 }
1046
1047 $linkDensity = $this->getLinkDensity($tagsList->item($i));
1048 $contentLength = strlen($this->getInnerText($tagsList->item($i)));
1049 $toRemove = false;
1050
1051 if ($this->lightClean) {
1052 $this->dbg('Light clean...');
1053 if ( ($img > $p) && ($img > 4) ) {
1054 $this->dbg(' more than 4 images and more image elements than paragraph elements');
1055 $toRemove = true;
1056 } else if ($li > $p && $tag != 'ul' && $tag != 'ol') {
1057 $this->dbg(' too many <li> elements, and parent is not <ul> or <ol>');
1058 $toRemove = true;
1059 } else if ( $input > floor($p/3) ) {
1060 $this->dbg(' too many <input> elements');
1061 $toRemove = true;
1062 } else if ($contentLength < 25 && ($embedCount === 0 && ($img === 0 || $img > 2))) {
1063 $this->dbg(' content length less than 25 chars, 0 embeds and either 0 images or more than 2 images');
1064 $toRemove = true;
1065 } else if($weight < 25 && $linkDensity > 0.2) {
1066 $this->dbg(' weight smaller than 25 and link density above 0.2');
1067 $toRemove = true;
1068 } else if($a > 2 && ($weight >= 25 && $linkDensity > 0.5)) {
1069 $this->dbg(' more than 2 links and weight above 25 but link density greater than 0.5');
1070 $toRemove = true;
1071 } else if($embedCount > 3) {
1072 $this->dbg(' more than 3 embeds');
1073 $toRemove = true;
1074 }
1075 } else {
1076 $this->dbg('Standard clean...');
1077 if ( $img > $p ) {
1078 $this->dbg(' more image elements than paragraph elements');
1079 $toRemove = true;
1080 } else if ($li > $p && $tag != 'ul' && $tag != 'ol') {
1081 $this->dbg(' too many <li> elements, and parent is not <ul> or <ol>');
1082 $toRemove = true;
1083 } else if ( $input > floor($p/3) ) {
1084 $this->dbg(' too many <input> elements');
1085 $toRemove = true;
1086 } else if ($contentLength < 25 && ($img === 0 || $img > 2) ) {
1087 $this->dbg(' content length less than 25 chars and 0 images, or more than 2 images');
1088 $toRemove = true;
1089 } else if($weight < 25 && $linkDensity > 0.2) {
1090 $this->dbg(' weight smaller than 25 and link density above 0.2');
1091 $toRemove = true;
1092 } else if($weight >= 25 && $linkDensity > 0.5) {
1093 $this->dbg(' weight above 25 but link density greater than 0.5');
1094 $toRemove = true;
1095 } else if(($embedCount == 1 && $contentLength < 75) || $embedCount > 1) {
1096 $this->dbg(' 1 embed and content length smaller than 75 chars, or more than one embed');
1097 $toRemove = true;
1098 }
1099 }
1100
1101 if ($toRemove) {
1102 //$this->dbg('Removing: '.$tagsList->item($i)->innerHTML);
1103 $tagsList->item($i)->parentNode->removeChild($tagsList->item($i));
1104 }
1105 }
1106 }
1107 }
1108
1109 /**
1110 * Clean out spurious headers from an Element. Checks things like classnames and link density.
1111 *
1112 * @param DOMElement $e
1113 * @return void
1114 */
1115 public function cleanHeaders($e) {
1116 for ($headerIndex = 1; $headerIndex < 3; $headerIndex++) {
1117 $headers = $e->getElementsByTagName('h' . $headerIndex);
1118 for ($i=$headers->length-1; $i >=0; $i--) {
1119 if ($this->getClassWeight($headers->item($i)) < 0 || $this->getLinkDensity($headers->item($i)) > 0.33) {
1120 $headers->item($i)->parentNode->removeChild($headers->item($i));
1121 }
1122 }
1123 } 1147 }
1124 } 1148 }
1125 1149
1126 public function flagIsActive($flag) { 1150}
1127 return ($this->flags & $flag) > 0;
1128 }
1129
1130 public function addFlag($flag) {
1131 $this->flags = $this->flags | $flag;
1132 }
1133
1134 public function removeFlag($flag) {
1135 $this->flags = $this->flags & ~$flag;
1136 }
1137}
1138?> \ No newline at end of file 1151?> \ No newline at end of file
diff --git a/inc/3rdparty/makefulltextfeed.php b/inc/3rdparty/makefulltextfeed.php
index 2852c4c2..a081f88b 100755
--- a/inc/3rdparty/makefulltextfeed.php
+++ b/inc/3rdparty/makefulltextfeed.php
@@ -3,8 +3,8 @@
3// Author: Keyvan Minoukadeh 3// Author: Keyvan Minoukadeh
4// Copyright (c) 2013 Keyvan Minoukadeh 4// Copyright (c) 2013 Keyvan Minoukadeh
5// License: AGPLv3 5// License: AGPLv3
6// Version: 3.1 6// Version: 3.2
7// Date: 2013-03-05 7// Date: 2013-05-13
8// More info: http://fivefilters.org/content-only/ 8// More info: http://fivefilters.org/content-only/
9// Help: http://help.fivefilters.org 9// Help: http://help.fivefilters.org
10 10
@@ -25,14 +25,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26// Usage 26// Usage
27// ----- 27// -----
28// Request this file passing it your feed in the querystring: makefulltextfeed.php?url=mysite.org 28// Request this file passing it a web page or feed URL in the querystring: makefulltextfeed.php?url=example.org/article
29// The following options can be passed in the querystring: 29// For more request parameters, see http://help.fivefilters.org/customer/portal/articles/226660-usage
30// * URL: url=[feed or website url] (required, should be URL-encoded - in php: urlencode($url))
31// * URL points to HTML (not feed): html=true (optional, by default it's automatically detected)
32// * API key: key=[api key] (optional, refer to config.php)
33// * Max entries to process: max=[max number of items] (optional)
34 30
35error_reporting(E_ALL ^ E_NOTICE); 31//error_reporting(E_ALL ^ E_NOTICE);
36ini_set("display_errors", 1); 32ini_set("display_errors", 1);
37@set_time_limit(120); 33@set_time_limit(120);
38 34
@@ -55,42 +51,8 @@ if (get_magic_quotes_gpc()) {
55 51
56// set include path 52// set include path
57set_include_path(realpath(dirname(__FILE__).'/libraries').PATH_SEPARATOR.get_include_path()); 53set_include_path(realpath(dirname(__FILE__).'/libraries').PATH_SEPARATOR.get_include_path());
58// Autoloading of classes allows us to include files only when they're 54
59// needed. If we've got a cached copy, for example, only Zend_Cache is loaded. 55require_once dirname(__FILE__).'/makefulltextfeedHelpers.php';
60function autoload($class_name) {
61 static $dir = null;
62 if ($dir === null) $dir = dirname(__FILE__).'/libraries/';
63 static $mapping = array(
64 // Include FeedCreator for RSS/Atom creation
65 'FeedWriter' => 'feedwriter/FeedWriter.php',
66 'FeedItem' => 'feedwriter/FeedItem.php',
67 // Include ContentExtractor and Readability for identifying and extracting content from URLs
68 'ContentExtractor' => 'content-extractor/ContentExtractor.php',
69 'SiteConfig' => 'content-extractor/SiteConfig.php',
70 'Readability' => 'readability/Readability.php',
71 // Include Humble HTTP Agent to allow parallel requests and response caching
72 'HumbleHttpAgent' => 'humble-http-agent/HumbleHttpAgent.php',
73 'SimplePie_HumbleHttpAgent' => 'humble-http-agent/SimplePie_HumbleHttpAgent.php',
74 'CookieJar' => 'humble-http-agent/CookieJar.php',
75 // Include Zend Cache to improve performance (cache results)
76 'Zend_Cache' => 'Zend/Cache.php',
77 // Language detect
78 'Text_LanguageDetect' => 'language-detect/LanguageDetect.php',
79 // HTML5 Lib
80 'HTML5_Parser' => 'html5/Parser.php',
81 // htmLawed - used if XSS filter is enabled (xss_filter)
82 'htmLawed' => 'htmLawed/htmLawed.php'
83 );
84 if (isset($mapping[$class_name])) {
85 debug("** Loading class $class_name ({$mapping[$class_name]})");
86 require $dir.$mapping[$class_name];
87 return true;
88 } else {
89 return false;
90 }
91}
92spl_autoload_register('autoload');
93require dirname(__FILE__).'/libraries/simplepie/autoloader.php';
94 56
95//////////////////////////////// 57////////////////////////////////
96// Load config file 58// Load config file
@@ -110,8 +72,8 @@ header('X-Robots-Tag: noindex, nofollow');
110//////////////////////////////// 72////////////////////////////////
111// Check if service is enabled 73// Check if service is enabled
112//////////////////////////////// 74////////////////////////////////
113if (!$options->enabled) { 75if (!$options->enabled) {
114 die('The full-text RSS service is currently disabled'); 76 die('The full-text RSS service is currently disabled');
115} 77}
116 78
117//////////////////////////////// 79////////////////////////////////
@@ -155,8 +117,8 @@ $options->smart_cache = $options->smart_cache && function_exists('apc_inc');
155//////////////////////////////// 117////////////////////////////////
156// Check for feed URL 118// Check for feed URL
157//////////////////////////////// 119////////////////////////////////
158if (!isset($_GET['url'])) { 120if (!isset($_GET['url'])) {
159 die('No URL supplied'); 121 die('No URL supplied');
160} 122}
161$url = trim($_GET['url']); 123$url = trim($_GET['url']);
162if (strtolower(substr($url, 0, 7)) == 'feed://') { 124if (strtolower(substr($url, 0, 7)) == 'feed://') {
@@ -195,10 +157,12 @@ if (isset($_GET['key']) && ($key_index = array_search($_GET['key'], $options->ap
195 if (isset($_GET['links'])) $redirect .= '&links='.urlencode($_GET['links']); 157 if (isset($_GET['links'])) $redirect .= '&links='.urlencode($_GET['links']);
196 if (isset($_GET['exc'])) $redirect .= '&exc='.urlencode($_GET['exc']); 158 if (isset($_GET['exc'])) $redirect .= '&exc='.urlencode($_GET['exc']);
197 if (isset($_GET['format'])) $redirect .= '&format='.urlencode($_GET['format']); 159 if (isset($_GET['format'])) $redirect .= '&format='.urlencode($_GET['format']);
198 if (isset($_GET['callback'])) $redirect .= '&callback='.urlencode($_GET['callback']); 160 if (isset($_GET['callback'])) $redirect .= '&callback='.urlencode($_GET['callback']);
199 if (isset($_GET['l'])) $redirect .= '&l='.urlencode($_GET['l']); 161 if (isset($_GET['l'])) $redirect .= '&l='.urlencode($_GET['l']);
200 if (isset($_GET['xss'])) $redirect .= '&xss'; 162 if (isset($_GET['xss'])) $redirect .= '&xss';
201 if (isset($_GET['use_extracted_title'])) $redirect .= '&use_extracted_title'; 163 if (isset($_GET['use_extracted_title'])) $redirect .= '&use_extracted_title';
164 if (isset($_GET['content'])) $redirect .= '&content='.urlencode($_GET['content']);
165 if (isset($_GET['summary'])) $redirect .= '&summary='.urlencode($_GET['summary']);
202 if (isset($_GET['debug'])) $redirect .= '&debug'; 166 if (isset($_GET['debug'])) $redirect .= '&debug';
203 if ($debug_mode) { 167 if ($debug_mode) {
204 debug('Redirecting to hide access key, follow URL below to continue'); 168 debug('Redirecting to hide access key, follow URL below to continue');
@@ -211,7 +175,7 @@ if (isset($_GET['key']) && ($key_index = array_search($_GET['key'], $options->ap
211 175
212/////////////////////////////////////////////// 176///////////////////////////////////////////////
213// Set timezone. 177// Set timezone.
214// Prevents warnings, but needs more testing - 178// Prevents warnings, but needs more testing -
215// perhaps if timezone is set in php.ini we 179// perhaps if timezone is set in php.ini we
216// don't need to set it at all... 180// don't need to set it at all...
217/////////////////////////////////////////////// 181///////////////////////////////////////////////
@@ -233,7 +197,7 @@ if (isset($_GET['key']) && isset($_GET['hash']) && isset($options->api_keys[(int
233} 197}
234$key_index = ($valid_key) ? (int)$_GET['key'] : 0; 198$key_index = ($valid_key) ? (int)$_GET['key'] : 0;
235if (!$valid_key && $options->key_required) { 199if (!$valid_key && $options->key_required) {
236 die('A valid key must be supplied'); 200 die('A valid key must be supplied');
237} 201}
238if (!$valid_key && isset($_GET['key']) && $_GET['key'] != '') { 202if (!$valid_key && isset($_GET['key']) && $_GET['key'] != '') {
239 die('The entered key is invalid'); 203 die('The entered key is invalid');
@@ -285,6 +249,28 @@ if ($options->favour_feed_titles == 'user') {
285} 249}
286 250
287/////////////////////////////////////////////// 251///////////////////////////////////////////////
252// Include full content in output?
253///////////////////////////////////////////////
254if ($options->content === 'user') {
255 if (isset($_GET['content']) && $_GET['content'] === '0') {
256 $options->content = false;
257 } else {
258 $options->content = true;
259 }
260}
261
262///////////////////////////////////////////////
263// Include summaries in output?
264///////////////////////////////////////////////
265if ($options->summary === 'user') {
266 if (isset($_GET['summary']) && $_GET['summary'] === '1') {
267 $options->summary = true;
268 } else {
269 $options->summary = false;
270 }
271}
272
273///////////////////////////////////////////////
288// Exclude items if extraction fails 274// Exclude items if extraction fails
289/////////////////////////////////////////////// 275///////////////////////////////////////////////
290if ($options->exclude_items_on_fail === 'user') { 276if ($options->exclude_items_on_fail === 'user') {
@@ -306,15 +292,6 @@ if ($options->detect_language === 'user') {
306 $detect_language = $options->detect_language; 292 $detect_language = $options->detect_language;
307} 293}
308 294
309if ($detect_language >= 2) {
310 $language_codes = array('albanian' => 'sq','arabic' => 'ar','azeri' => 'az','bengali' => 'bn','bulgarian' => 'bg',
311 'cebuano' => 'ceb', // ISO 639-2
312 'croatian' => 'hr','czech' => 'cs','danish' => 'da','dutch' => 'nl','english' => 'en','estonian' => 'et','farsi' => 'fa','finnish' => 'fi','french' => 'fr','german' => 'de','hausa' => 'ha',
313 'hawaiian' => 'haw', // ISO 639-2
314 'hindi' => 'hi','hungarian' => 'hu','icelandic' => 'is','indonesian' => 'id','italian' => 'it','kazakh' => 'kk','kyrgyz' => 'ky','latin' => 'la','latvian' => 'lv','lithuanian' => 'lt','macedonian' => 'mk','mongolian' => 'mn','nepali' => 'ne','norwegian' => 'no','pashto' => 'ps',
315 'pidgin' => 'cpe', // ISO 639-2
316 'polish' => 'pl','portuguese' => 'pt','romanian' => 'ro','russian' => 'ru','serbian' => 'sr','slovak' => 'sk','slovene' => 'sl','somali' => 'so','spanish' => 'es','swahili' => 'sw','swedish' => 'sv','tagalog' => 'tl','turkish' => 'tr','ukrainian' => 'uk','urdu' => 'ur','uzbek' => 'uz','vietnamese' => 'vi','welsh' => 'cy');
317}
318$use_cld = extension_loaded('cld') && (version_compare(PHP_VERSION, '5.3.0') >= 0); 295$use_cld = extension_loaded('cld') && (version_compare(PHP_VERSION, '5.3.0') >= 0);
319 296
320///////////////////////////////////// 297/////////////////////////////////////
@@ -364,7 +341,7 @@ if ($options->cors) header('Access-Control-Allow-Origin: *');
364////////////////////////////////// 341//////////////////////////////////
365if ($options->caching) { 342if ($options->caching) {
366 debug('Caching is enabled...'); 343 debug('Caching is enabled...');
367 $cache_id = md5($max.$url.$valid_key.$links.$favour_feed_titles.$xss_filter.$exclude_on_fail.$format.$detect_language.(int)isset($_GET['pubsub'])); 344 $cache_id = md5($max.$url.(int)$valid_key.$links.(int)$favour_feed_titles.(int)$options->content.(int)$options->summary.(int)$xss_filter.(int)$exclude_on_fail.$format.$detect_language.(int)isset($_GET['pubsub']));
368 $check_cache = true; 345 $check_cache = true;
369 if ($options->apc && $options->smart_cache) { 346 if ($options->apc && $options->smart_cache) {
370 apc_add("cache.$cache_id", 0, 10*60); 347 apc_add("cache.$cache_id", 0, 10*60);
@@ -415,6 +392,7 @@ if (!$debug_mode) {
415////////////////////////////////// 392//////////////////////////////////
416// Set up HTTP agent 393// Set up HTTP agent
417////////////////////////////////// 394//////////////////////////////////
395global $http;
418$http = new HumbleHttpAgent(); 396$http = new HumbleHttpAgent();
419$http->debug = $debug_mode; 397$http->debug = $debug_mode;
420$http->userAgentMap = $options->user_agents; 398$http->userAgentMap = $options->user_agents;
@@ -478,29 +456,6 @@ if ($html_only || !$result) {
478 $isDummyFeed = true; 456 $isDummyFeed = true;
479 unset($feed, $result); 457 unset($feed, $result);
480 // create single item dummy feed object 458 // create single item dummy feed object
481 class DummySingleItemFeed {
482 public $item;
483 function __construct($url) { $this->item = new DummySingleItem($url); }
484 public function get_title() { return ''; }
485 public function get_description() { return 'Content extracted from '.$this->item->url; }
486 public function get_link() { return $this->item->url; }
487 public function get_language() { return false; }
488 public function get_image_url() { return false; }
489 public function get_items($start=0, $max=1) { return array(0=>$this->item); }
490 }
491 class DummySingleItem {
492 public $url;
493 function __construct($url) { $this->url = $url; }
494 public function get_permalink() { return $this->url; }
495 public function get_title() { return null; }
496 public function get_date($format='') { return false; }
497 public function get_author($key=0) { return null; }
498 public function get_authors() { return null; }
499 public function get_description() { return ''; }
500 public function get_enclosure($key=0, $prefer=null) { return null; }
501 public function get_enclosures() { return null; }
502 public function get_categories() { return null; }
503 }
504 $feed = new DummySingleItemFeed($url); 459 $feed = new DummySingleItemFeed($url);
505} 460}
506 461
@@ -524,7 +479,7 @@ if ($img_url = $feed->get_image_url()) {
524//////////////////////////////////////////// 479////////////////////////////////////////////
525// Loop through feed items 480// Loop through feed items
526//////////////////////////////////////////// 481////////////////////////////////////////////
527$items = $feed->get_items(0, $max); 482$items = $feed->get_items(0, $max);
528// Request all feed items in parallel (if supported) 483// Request all feed items in parallel (if supported)
529$urls_sanitized = array(); 484$urls_sanitized = array();
530$urls = array(); 485$urls = array();
@@ -606,24 +561,43 @@ foreach ($items as $key => $item) {
606 $is_single_page = false; 561 $is_single_page = false;
607 if ($single_page_response = getSinglePage($item, $html, $effective_url)) { 562 if ($single_page_response = getSinglePage($item, $html, $effective_url)) {
608 $is_single_page = true; 563 $is_single_page = true;
609 $html = $single_page_response['body'];
610 // remove strange things
611 $html = str_replace('</[>', '', $html);
612 $html = convert_to_utf8($html, $single_page_response['headers']);
613 $effective_url = $single_page_response['effective_url']; 564 $effective_url = $single_page_response['effective_url'];
614 debug("Retrieved single-page view from $effective_url"); 565 // check if action defined for returned Content-Type
566 $mime_info = get_mime_action_info($single_page_response['headers']);
567 if (isset($mime_info['action'])) {
568 if ($mime_info['action'] == 'exclude') {
569 continue; // skip this feed item entry
570 } elseif ($mime_info['action'] == 'link') {
571 if ($mime_info['type'] == 'image') {
572 $html = "<a href=\"$effective_url\"><img src=\"$effective_url\" alt=\"{$mime_info['name']}\" /></a>";
573 } else {
574 $html = "<a href=\"$effective_url\">Download {$mime_info['name']}</a>";
575 }
576 $extracted_title = $mime_info['name'];
577 $do_content_extraction = false;
578 }
579 }
580 if ($do_content_extraction) {
581 $html = $single_page_response['body'];
582 // remove strange things
583 $html = str_replace('</[>', '', $html);
584 $html = convert_to_utf8($html, $single_page_response['headers']);
585 debug("Retrieved single-page view from $effective_url");
586 }
615 unset($single_page_response); 587 unset($single_page_response);
616 } 588 }
589 }
590 if ($do_content_extraction) {
617 debug('--------'); 591 debug('--------');
618 debug('Attempting to extract content'); 592 debug('Attempting to extract content');
619 $extract_result = $extractor->process($html, $effective_url); 593 $extract_result = $extractor->process($html, $effective_url);
620 $readability = $extractor->readability; 594 $readability = $extractor->readability;
621 $content_block = ($extract_result) ? $extractor->getContent() : null; 595 $content_block = ($extract_result) ? $extractor->getContent() : null;
622 $extracted_title = ($extract_result) ? $extractor->getTitle() : ''; 596 $extracted_title = ($extract_result) ? $extractor->getTitle() : '';
623 // Deal with multi-page articles 597 // Deal with multi-page articles
624 //die('Next: '.$extractor->getNextPageUrl()); 598 //die('Next: '.$extractor->getNextPageUrl());
625 $is_multi_page = (!$is_single_page && $extract_result && $extractor->getNextPageUrl()); 599 $is_multi_page = (!$is_single_page && $extract_result && $extractor->getNextPageUrl());
626 if ($options->multipage && $is_multi_page) { 600 if ($options->multipage && $is_multi_page && $options->content) {
627 debug('--------'); 601 debug('--------');
628 debug('Attempting to process multi-page article'); 602 debug('Attempting to process multi-page article');
629 $multi_page_urls = array(); 603 $multi_page_urls = array();
@@ -636,7 +610,7 @@ foreach ($items as $key => $item) {
636 // check it's not what we have already! 610 // check it's not what we have already!
637 if (!in_array($next_page_url, $multi_page_urls)) { 611 if (!in_array($next_page_url, $multi_page_urls)) {
638 // it's not, so let's attempt to fetch it 612 // it's not, so let's attempt to fetch it
639 $multi_page_urls[] = $next_page_url; 613 $multi_page_urls[] = $next_page_url;
640 $_prev_ref = $http->referer; 614 $_prev_ref = $http->referer;
641 if (($response = $http->get($next_page_url, true)) && $response['status_code'] < 300) { 615 if (($response = $http->get($next_page_url, true)) && $response['status_code'] < 300) {
642 // make sure mime type is not something with a different action associated 616 // make sure mime type is not something with a different action associated
@@ -661,13 +635,15 @@ foreach ($items as $key => $item) {
661 // did we successfully deal with this multi-page article? 635 // did we successfully deal with this multi-page article?
662 if (empty($multi_page_content)) { 636 if (empty($multi_page_content)) {
663 debug('Failed to extract all parts of multi-page article, so not going to include them'); 637 debug('Failed to extract all parts of multi-page article, so not going to include them');
664 $multi_page_content[] = $readability->dom->createElement('p')->innerHTML = '<em>This article appears to continue on subsequent pages which we could not extract</em>'; 638 $_page = $readability->dom->createElement('p');
639 $_page->innerHTML = '<em>This article appears to continue on subsequent pages which we could not extract</em>';
640 $multi_page_content[] = $_page;
665 } 641 }
666 foreach ($multi_page_content as $_page) { 642 foreach ($multi_page_content as $_page) {
667 $_page = $content_block->ownerDocument->importNode($_page, true); 643 $_page = $content_block->ownerDocument->importNode($_page, true);
668 $content_block->appendChild($_page); 644 $content_block->appendChild($_page);
669 } 645 }
670 unset($multi_page_urls, $multi_page_content, $page_mime_info, $next_page_url); 646 unset($multi_page_urls, $multi_page_content, $page_mime_info, $next_page_url, $_page);
671 } 647 }
672 } 648 }
673 // use extracted title for both feed and item title if we're using single-item dummy feed 649 // use extracted title for both feed and item title if we're using single-item dummy feed
@@ -695,7 +671,11 @@ foreach ($items as $key => $item) {
695 $html .= $item->get_description(); 671 $html .= $item->get_description();
696 } else { 672 } else {
697 $readability->clean($content_block, 'select'); 673 $readability->clean($content_block, 'select');
698 if ($options->rewrite_relative_urls) makeAbsolute($effective_url, $content_block); 674 // get base URL
675 $base_url = get_base_url($readability->dom);
676 if (!$base_url) $base_url = $effective_url;
677 // rewrite URLs
678 if ($options->rewrite_relative_urls) makeAbsolute($base_url, $content_block);
699 // footnotes 679 // footnotes
700 if (($links == 'footnotes') && (strpos($effective_url, 'wikipedia.org') === false)) { 680 if (($links == 'footnotes') && (strpos($effective_url, 'wikipedia.org') === false)) {
701 $readability->addFootnotes($content_block); 681 $readability->addFootnotes($content_block);
@@ -714,7 +694,7 @@ foreach ($items as $key => $item) {
714 } else { 694 } else {
715 $html = $content_block->ownerDocument->saveXML($content_block); // essentially outerHTML 695 $html = $content_block->ownerDocument->saveXML($content_block); // essentially outerHTML
716 } 696 }
717 unset($content_block); 697 //unset($content_block);
718 // post-processing cleanup 698 // post-processing cleanup
719 $html = preg_replace('!<p>[\s\h\v]*</p>!u', '', $html); 699 $html = preg_replace('!<p>[\s\h\v]*</p>!u', '', $html);
720 if ($links == 'remove') { 700 if ($links == 'remove') {
@@ -727,130 +707,155 @@ foreach ($items as $key => $item) {
727 } 707 }
728 } 708 }
729 709
730 if ($valid_key && isset($_GET['pubsub'])) { // used only on fivefilters.org at the moment 710 if ($valid_key && isset($_GET['pubsub'])) { // used only on fivefilters.org at the moment
731 $newitem->addElement('guid', 'http://fivefilters.org/content-only/redirect.php?url='.urlencode($item->get_permalink()), array('isPermaLink'=>'false')); 711 $newitem->addElement('guid', 'http://fivefilters.org/content-only/redirect.php?url='.urlencode($item->get_permalink()), array('isPermaLink'=>'false'));
712 } else {
713 $newitem->addElement('guid', $item->get_permalink(), array('isPermaLink'=>'true'));
714 }
715 // filter xss?
716 if ($xss_filter) {
717 debug('Filtering HTML to remove XSS');
718 $html = htmLawed::hl($html, array('safe'=>1, 'deny_attribute'=>'style', 'comment'=>1, 'cdata'=>1));
719 }
720
721 // add content
722 if ($options->summary === true) {
723 // get summary
724 $summary = '';
725 if (!$do_content_extraction) {
726 $summary = $html;
732 } else { 727 } else {
733 $newitem->addElement('guid', $item->get_permalink(), array('isPermaLink'=>'true')); 728 // Try to get first few paragraphs
734 } 729 if (isset($content_block) && ($content_block instanceof DOMElement)) {
735 // filter xss? 730 $_paras = $content_block->getElementsByTagName('p');
736 if ($xss_filter) { 731 foreach ($_paras as $_para) {
737 debug('Filtering HTML to remove XSS'); 732 $summary .= preg_replace("/[\n\r\t ]+/", ' ', $_para->textContent).' ';
738 $html = htmLawed::hl($html, array('safe'=>1, 'deny_attribute'=>'style', 'comment'=>1, 'cdata'=>1)); 733 if (strlen($summary) > 200) break;
739 }
740 $newitem->setDescription($html);
741
742 // set date
743 if ((int)$item->get_date('U') > 0) {
744 $newitem->setDate((int)$item->get_date('U'));
745 } elseif ($extractor->getDate()) {
746 $newitem->setDate($extractor->getDate());
747 }
748
749 // add authors
750 if ($authors = $item->get_authors()) {
751 foreach ($authors as $author) {
752 // for some feeds, SimplePie stores author's name as email, e.g. http://feeds.feedburner.com/nymag/intel
753 if ($author->get_name() !== null) {
754 $newitem->addElement('dc:creator', $author->get_name());
755 } elseif ($author->get_email() !== null) {
756 $newitem->addElement('dc:creator', $author->get_email());
757 } 734 }
735 } else {
736 $summary = $html;
758 } 737 }
759 } elseif ($authors = $extractor->getAuthors()) { 738 }
760 //TODO: make sure the list size is reasonable 739 unset($_paras, $_para);
761 foreach ($authors as $author) { 740 $summary = get_excerpt($summary);
762 // TODO: xpath often selects authors from other articles linked from the page. 741 $newitem->setDescription($summary);
763 // for now choose first item 742 if ($options->content) $newitem->setElement('content:encoded', $html);
764 $newitem->addElement('dc:creator', $author); 743 } else {
765 break; 744 if ($options->content) $newitem->setDescription($html);
745 }
746
747 // set date
748 if ((int)$item->get_date('U') > 0) {
749 $newitem->setDate((int)$item->get_date('U'));
750 } elseif ($extractor->getDate()) {
751 $newitem->setDate($extractor->getDate());
752 }
753
754 // add authors
755 if ($authors = $item->get_authors()) {
756 foreach ($authors as $author) {
757 // for some feeds, SimplePie stores author's name as email, e.g. http://feeds.feedburner.com/nymag/intel
758 if ($author->get_name() !== null) {
759 $newitem->addElement('dc:creator', $author->get_name());
760 } elseif ($author->get_email() !== null) {
761 $newitem->addElement('dc:creator', $author->get_email());
766 } 762 }
767 } 763 }
768 764 } elseif ($authors = $extractor->getAuthors()) {
769 // add language 765 //TODO: make sure the list size is reasonable
770 if ($detect_language) { 766 foreach ($authors as $author) {
771 $language = $extractor->getLanguage(); 767 // TODO: xpath often selects authors from other articles linked from the page.
772 if (!$language) $language = $feed->get_language(); 768 // for now choose first item
773 if (($detect_language == 3 || (!$language && $detect_language == 2)) && $text_sample) { 769 $newitem->addElement('dc:creator', $author);
774 try { 770 break;
775 if ($use_cld) { 771 }
776 // Use PHP-CLD extension 772 }
777 $php_cld = 'CLD\detect'; // in quotes to prevent PHP 5.2 parse error 773
778 $res = $php_cld($text_sample); 774 // add language
779 if (is_array($res) && count($res) > 0) { 775 if ($detect_language) {
780 $language = $res[0]['code']; 776 $language = $extractor->getLanguage();
781 } 777 if (!$language) $language = $feed->get_language();
782 } else { 778 if (($detect_language == 3 || (!$language && $detect_language == 2)) && $text_sample) {
783 //die('what'); 779 try {
784 // Use PEAR's Text_LanguageDetect 780 if ($use_cld) {
785 if (!isset($l)) { 781 // Use PHP-CLD extension
786 $l = new Text_LanguageDetect('libraries/language-detect/lang.dat', 'libraries/language-detect/unicode_blocks.dat'); 782 $php_cld = 'CLD\detect'; // in quotes to prevent PHP 5.2 parse error
787 } 783 $res = $php_cld($text_sample);
788 $l_result = $l->detect($text_sample, 1); 784 if (is_array($res) && count($res) > 0) {
789 if (count($l_result) > 0) { 785 $language = $res[0]['code'];
790 $language = $language_codes[key($l_result)]; 786 }
791 } 787 } else {
788 //die('what');
789 // Use PEAR's Text_LanguageDetect
790 if (!isset($l)) {
791 $l = new Text_LanguageDetect();
792 $l->setNameMode(2); // return ISO 639-1 codes (e.g. "en")
793 }
794 $l_result = $l->detect($text_sample, 1);
795 if (count($l_result) > 0) {
796 $language = key($l_result);
792 } 797 }
793 } catch (Exception $e) {
794 //die('error: '.$e);
795 // do nothing
796 } 798 }
797 } 799 } catch (Exception $e) {
798 if ($language && (strlen($language) < 7)) { 800 //die('error: '.$e);
799 $newitem->addElement('dc:language', $language); 801 // do nothing
800 } 802 }
801 } 803 }
802 804 if ($language && (strlen($language) < 7)) {
803 // add MIME type (if it appeared in our exclusions lists) 805 $newitem->addElement('dc:language', $language);
804 if (isset($mime_info['mime'])) $newitem->addElement('dc:format', $mime_info['mime']);
805 // add effective URL (URL after redirects)
806 if (isset($effective_url)) {
807 //TODO: ensure $effective_url is valid witout - sometimes it causes problems, e.g.
808 //http://www.siasat.pk/forum/showthread.php?108883-Pakistan-Chowk-by-Rana-Mubashir--25th-March-2012-Special-Program-from-Liari-(Karachi)
809 //temporary measure: use utf8_encode()
810 $newitem->addElement('dc:identifier', remove_url_cruft(utf8_encode($effective_url)));
811 } else {
812 $newitem->addElement('dc:identifier', remove_url_cruft($item->get_permalink()));
813 } 806 }
814 807 }
815 // add categories 808
816 if ($categories = $item->get_categories()) { 809 // add MIME type (if it appeared in our exclusions lists)
817 foreach ($categories as $category) { 810 if (isset($mime_info['mime'])) $newitem->addElement('dc:format', $mime_info['mime']);
818 if ($category->get_label() !== null) { 811 // add effective URL (URL after redirects)
819 $newitem->addElement('category', $category->get_label()); 812 if (isset($effective_url)) {
820 } 813 //TODO: ensure $effective_url is valid witout - sometimes it causes problems, e.g.
814 //http://www.siasat.pk/forum/showthread.php?108883-Pakistan-Chowk-by-Rana-Mubashir-�-25th-March-2012-Special-Program-from-Liari-(Karachi)
815 //temporary measure: use utf8_encode()
816 $newitem->addElement('dc:identifier', remove_url_cruft(utf8_encode($effective_url)));
817 } else {
818 $newitem->addElement('dc:identifier', remove_url_cruft($item->get_permalink()));
819 }
820
821 // add categories
822 if ($categories = $item->get_categories()) {
823 foreach ($categories as $category) {
824 if ($category->get_label() !== null) {
825 $newitem->addElement('category', $category->get_label());
821 } 826 }
822 } 827 }
823 828 }
824 // check for enclosures 829
825 if ($options->keep_enclosures) { 830 // check for enclosures
826 if ($enclosures = $item->get_enclosures()) { 831 if ($options->keep_enclosures) {
827 foreach ($enclosures as $enclosure) { 832 if ($enclosures = $item->get_enclosures()) {
828 // thumbnails 833 foreach ($enclosures as $enclosure) {
829 foreach ((array)$enclosure->get_thumbnails() as $thumbnail) { 834 // thumbnails
830 $newitem->addElement('media:thumbnail', '', array('url'=>$thumbnail)); 835 foreach ((array)$enclosure->get_thumbnails() as $thumbnail) {
831 } 836 $newitem->addElement('media:thumbnail', '', array('url'=>$thumbnail));
832 if (!$enclosure->get_link()) continue;
833 $enc = array();
834 // Media RSS spec ($enc): http://search.yahoo.com/mrss
835 // SimplePie methods ($enclosure): http://simplepie.org/wiki/reference/start#methods4
836 $enc['url'] = $enclosure->get_link();
837 if ($enclosure->get_length()) $enc['fileSize'] = $enclosure->get_length();
838 if ($enclosure->get_type()) $enc['type'] = $enclosure->get_type();
839 if ($enclosure->get_medium()) $enc['medium'] = $enclosure->get_medium();
840 if ($enclosure->get_expression()) $enc['expression'] = $enclosure->get_expression();
841 if ($enclosure->get_bitrate()) $enc['bitrate'] = $enclosure->get_bitrate();
842 if ($enclosure->get_framerate()) $enc['framerate'] = $enclosure->get_framerate();
843 if ($enclosure->get_sampling_rate()) $enc['samplingrate'] = $enclosure->get_sampling_rate();
844 if ($enclosure->get_channels()) $enc['channels'] = $enclosure->get_channels();
845 if ($enclosure->get_duration()) $enc['duration'] = $enclosure->get_duration();
846 if ($enclosure->get_height()) $enc['height'] = $enclosure->get_height();
847 if ($enclosure->get_width()) $enc['width'] = $enclosure->get_width();
848 if ($enclosure->get_language()) $enc['lang'] = $enclosure->get_language();
849 $newitem->addElement('media:content', '', $enc);
850 } 837 }
838 if (!$enclosure->get_link()) continue;
839 $enc = array();
840 // Media RSS spec ($enc): http://search.yahoo.com/mrss
841 // SimplePie methods ($enclosure): http://simplepie.org/wiki/reference/start#methods4
842 $enc['url'] = $enclosure->get_link();
843 if ($enclosure->get_length()) $enc['fileSize'] = $enclosure->get_length();
844 if ($enclosure->get_type()) $enc['type'] = $enclosure->get_type();
845 if ($enclosure->get_medium()) $enc['medium'] = $enclosure->get_medium();
846 if ($enclosure->get_expression()) $enc['expression'] = $enclosure->get_expression();
847 if ($enclosure->get_bitrate()) $enc['bitrate'] = $enclosure->get_bitrate();
848 if ($enclosure->get_framerate()) $enc['framerate'] = $enclosure->get_framerate();
849 if ($enclosure->get_sampling_rate()) $enc['samplingrate'] = $enclosure->get_sampling_rate();
850 if ($enclosure->get_channels()) $enc['channels'] = $enclosure->get_channels();
851 if ($enclosure->get_duration()) $enc['duration'] = $enclosure->get_duration();
852 if ($enclosure->get_height()) $enc['height'] = $enclosure->get_height();
853 if ($enclosure->get_width()) $enc['width'] = $enclosure->get_width();
854 if ($enclosure->get_language()) $enc['lang'] = $enclosure->get_language();
855 $newitem->addElement('media:content', '', $enc);
851 } 856 }
852 } 857 }
853 /* } */ 858 }
854 $output->addItem($newitem); 859 $output->addItem($newitem);
855 unset($html); 860 unset($html);
856 $item_count++; 861 $item_count++;
@@ -887,7 +892,7 @@ if (!$debug_mode) {
887 } 892 }
888 if ($add_to_cache) { 893 if ($add_to_cache) {
889 ob_start(); 894 ob_start();
890 $output->genarateFeed(); 895 $output->genarateFeed(false);
891 $output = ob_get_contents(); 896 $output = ob_get_contents();
892 ob_end_clean(); 897 ob_end_clean();
893 if ($html_only && $item_count == 0) { 898 if ($html_only && $item_count == 0) {
@@ -898,299 +903,8 @@ if (!$debug_mode) {
898 } 903 }
899 echo $output; 904 echo $output;
900 } else { 905 } else {
901 $output->genarateFeed(); 906 $output->genarateFeed(false);
902 } 907 }
903 if ($callback) echo ');'; 908 if ($callback) echo ');';
904} 909}
905 910
906///////////////////////////////
907// HELPER FUNCTIONS
908///////////////////////////////
909
910function url_allowed($url) {
911 global $options;
912 if (!empty($options->allowed_urls)) {
913 $allowed = false;
914 foreach ($options->allowed_urls as $allowurl) {
915 if (stristr($url, $allowurl) !== false) {
916 $allowed = true;
917 break;
918 }
919 }
920 if (!$allowed) return false;
921 } else {
922 foreach ($options->blocked_urls as $blockurl) {
923 if (stristr($url, $blockurl) !== false) {
924 return false;
925 }
926 }
927 }
928 return true;
929}
930
931//////////////////////////////////////////////
932// Convert $html to UTF8
933// (uses HTTP headers and HTML to find encoding)
934// adapted from http://stackoverflow.com/questions/910793/php-detect-encoding-and-make-everything-utf-8
935//////////////////////////////////////////////
936function convert_to_utf8($html, $header=null)
937{
938 $encoding = null;
939 if ($html || $header) {
940 if (is_array($header)) $header = implode("\n", $header);
941 if (!$header || !preg_match_all('/^Content-Type:\s+([^;]+)(?:;\s*charset=["\']?([^;"\'\n]*))?/im', $header, $match, PREG_SET_ORDER)) {
942 // error parsing the response
943 debug('Could not find Content-Type header in HTTP response');
944 } else {
945 $match = end($match); // get last matched element (in case of redirects)
946 if (isset($match[2])) $encoding = trim($match[2], "\"' \r\n\0\x0B\t");
947 }
948 // TODO: check to see if encoding is supported (can we convert it?)
949 // If it's not, result will be empty string.
950 // For now we'll check for invalid encoding types returned by some sites, e.g. 'none'
951 // Problem URL: http://facta.co.jp/blog/archives/20111026001026.html
952 if (!$encoding || $encoding == 'none') {
953 // search for encoding in HTML - only look at the first 50000 characters
954 // Why 50000? See, for example, http://www.lemonde.fr/festival-de-cannes/article/2012/05/23/deux-cretes-en-goguette-sur-la-croisette_1705732_766360.html
955 // TODO: improve this so it looks at smaller chunks first
956 $html_head = substr($html, 0, 50000);
957 if (preg_match('/^<\?xml\s+version=(?:"[^"]*"|\'[^\']*\')\s+encoding=("[^"]*"|\'[^\']*\')/s', $html_head, $match)) {
958 $encoding = trim($match[1], '"\'');
959 } elseif (preg_match('/<meta\s+http-equiv=["\']?Content-Type["\']? content=["\'][^;]+;\s*charset=["\']?([^;"\'>]+)/i', $html_head, $match)) {
960 $encoding = trim($match[1]);
961 } elseif (preg_match_all('/<meta\s+([^>]+)>/i', $html_head, $match)) {
962 foreach ($match[1] as $_test) {
963 if (preg_match('/charset=["\']?([^"\']+)/i', $_test, $_m)) {
964 $encoding = trim($_m[1]);
965 break;
966 }
967 }
968 }
969 }
970 if (isset($encoding)) $encoding = trim($encoding);
971 // trim is important here!
972 if (!$encoding || (strtolower($encoding) == 'iso-8859-1')) {
973 // replace MS Word smart qutoes
974 $trans = array();
975 $trans[chr(130)] = '&sbquo;'; // Single Low-9 Quotation Mark
976 $trans[chr(131)] = '&fnof;'; // Latin Small Letter F With Hook
977 $trans[chr(132)] = '&bdquo;'; // Double Low-9 Quotation Mark
978 $trans[chr(133)] = '&hellip;'; // Horizontal Ellipsis
979 $trans[chr(134)] = '&dagger;'; // Dagger
980 $trans[chr(135)] = '&Dagger;'; // Double Dagger
981 $trans[chr(136)] = '&circ;'; // Modifier Letter Circumflex Accent
982 $trans[chr(137)] = '&permil;'; // Per Mille Sign
983 $trans[chr(138)] = '&Scaron;'; // Latin Capital Letter S With Caron
984 $trans[chr(139)] = '&lsaquo;'; // Single Left-Pointing Angle Quotation Mark
985 $trans[chr(140)] = '&OElig;'; // Latin Capital Ligature OE
986 $trans[chr(145)] = '&lsquo;'; // Left Single Quotation Mark
987 $trans[chr(146)] = '&rsquo;'; // Right Single Quotation Mark
988 $trans[chr(147)] = '&ldquo;'; // Left Double Quotation Mark
989 $trans[chr(148)] = '&rdquo;'; // Right Double Quotation Mark
990 $trans[chr(149)] = '&bull;'; // Bullet
991 $trans[chr(150)] = '&ndash;'; // En Dash
992 $trans[chr(151)] = '&mdash;'; // Em Dash
993 $trans[chr(152)] = '&tilde;'; // Small Tilde
994 $trans[chr(153)] = '&trade;'; // Trade Mark Sign
995 $trans[chr(154)] = '&scaron;'; // Latin Small Letter S With Caron
996 $trans[chr(155)] = '&rsaquo;'; // Single Right-Pointing Angle Quotation Mark
997 $trans[chr(156)] = '&oelig;'; // Latin Small Ligature OE
998 $trans[chr(159)] = '&Yuml;'; // Latin Capital Letter Y With Diaeresis
999 $html = strtr($html, $trans);
1000 }
1001 if (!$encoding) {
1002 debug('No character encoding found, so treating as UTF-8');
1003 $encoding = 'utf-8';
1004 } else {
1005 debug('Character encoding: '.$encoding);
1006 if (strtolower($encoding) != 'utf-8') {
1007 debug('Converting to UTF-8');
1008 $html = SimplePie_Misc::change_encoding($html, $encoding, 'utf-8');
1009 /*
1010 if (function_exists('iconv')) {
1011 // iconv appears to handle certain character encodings better than mb_convert_encoding
1012 $html = iconv($encoding, 'utf-8', $html);
1013 } else {
1014 $html = mb_convert_encoding($html, 'utf-8', $encoding);
1015 }
1016 */
1017 }
1018 }
1019 }
1020 return $html;
1021}
1022
1023function makeAbsolute($base, $elem) {
1024 $base = new SimplePie_IRI($base);
1025 // remove '//' in URL path (used to prevent URLs from resolving properly)
1026 // TODO: check if this is still the case
1027 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
1028 foreach(array('a'=>'href', 'img'=>'src') as $tag => $attr) {
1029 $elems = $elem->getElementsByTagName($tag);
1030 for ($i = $elems->length-1; $i >= 0; $i--) {
1031 $e = $elems->item($i);
1032 //$e->parentNode->replaceChild($articleContent->ownerDocument->createTextNode($e->textContent), $e);
1033 makeAbsoluteAttr($base, $e, $attr);
1034 }
1035 if (strtolower($elem->tagName) == $tag) makeAbsoluteAttr($base, $elem, $attr);
1036 }
1037}
1038function makeAbsoluteAttr($base, $e, $attr) {
1039 if ($e->hasAttribute($attr)) {
1040 // Trim leading and trailing white space. I don't really like this but
1041 // unfortunately it does appear on some sites. e.g. <img src=" /path/to/image.jpg" />
1042 $url = trim(str_replace('%20', ' ', $e->getAttribute($attr)));
1043 $url = str_replace(' ', '%20', $url);
1044 if (!preg_match('!https?://!i', $url)) {
1045 if ($absolute = SimplePie_IRI::absolutize($base, $url)) {
1046 $e->setAttribute($attr, $absolute);
1047 }
1048 }
1049 }
1050}
1051function makeAbsoluteStr($base, $url) {
1052 $base = new SimplePie_IRI($base);
1053 // remove '//' in URL path (causes URLs not to resolve properly)
1054 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
1055 if (preg_match('!^https?://!i', $url)) {
1056 // already absolute
1057 return $url;
1058 } else {
1059 if ($absolute = SimplePie_IRI::absolutize($base, $url)) {
1060 return $absolute;
1061 }
1062 return false;
1063 }
1064}
1065// returns single page response, or false if not found
1066function getSinglePage($item, $html, $url) {
1067 global $http, $extractor;
1068 debug('Looking for site config files to see if single page link exists');
1069 $site_config = $extractor->buildSiteConfig($url, $html);
1070 $splink = null;
1071 if (!empty($site_config->single_page_link)) {
1072 $splink = $site_config->single_page_link;
1073 } elseif (!empty($site_config->single_page_link_in_feed)) {
1074 // single page link xpath is targeted at feed
1075 $splink = $site_config->single_page_link_in_feed;
1076 // so let's replace HTML with feed item description
1077 $html = $item->get_description();
1078 }
1079 if (isset($splink)) {
1080 // Build DOM tree from HTML
1081 $readability = new Readability($html, $url);
1082 $xpath = new DOMXPath($readability->dom);
1083 // Loop through single_page_link xpath expressions
1084 $single_page_url = null;
1085 foreach ($splink as $pattern) {
1086 $elems = @$xpath->evaluate($pattern, $readability->dom);
1087 if (is_string($elems)) {
1088 $single_page_url = trim($elems);
1089 break;
1090 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
1091 foreach ($elems as $item) {
1092 if ($item instanceof DOMElement && $item->hasAttribute('href')) {
1093 $single_page_url = $item->getAttribute('href');
1094 break 2;
1095 } elseif ($item instanceof DOMAttr && $item->value) {
1096 $single_page_url = $item->value;
1097 break 2;
1098 }
1099 }
1100 }
1101 }
1102 // If we've got URL, resolve against $url
1103 if (isset($single_page_url) && ($single_page_url = makeAbsoluteStr($url, $single_page_url))) {
1104 // check it's not what we have already!
1105 if ($single_page_url != $url) {
1106 // it's not, so let's try to fetch it...
1107 $_prev_ref = $http->referer;
1108 $http->referer = $single_page_url;
1109 if (($response = $http->get($single_page_url, true)) && $response['status_code'] < 300) {
1110 $http->referer = $_prev_ref;
1111 return $response;
1112 }
1113 $http->referer = $_prev_ref;
1114 }
1115 }
1116 }
1117 return false;
1118}
1119
1120// based on content-type http header, decide what to do
1121// param: HTTP headers string
1122// return: array with keys: 'mime', 'type', 'subtype', 'action', 'name'
1123// e.g. array('mime'=>'image/jpeg', 'type'=>'image', 'subtype'=>'jpeg', 'action'=>'link', 'name'=>'Image')
1124function get_mime_action_info($headers) {
1125 global $options;
1126 // check if action defined for returned Content-Type
1127 $info = array();
1128 if (preg_match('!^Content-Type:\s*(([-\w]+)/([-\w\+]+))!im', $headers, $match)) {
1129 // look for full mime type (e.g. image/jpeg) or just type (e.g. image)
1130 // match[1] = full mime type, e.g. image/jpeg
1131 // match[2] = first part, e.g. image
1132 // match[3] = last part, e.g. jpeg
1133 $info['mime'] = strtolower(trim($match[1]));
1134 $info['type'] = strtolower(trim($match[2]));
1135 $info['subtype'] = strtolower(trim($match[3]));
1136 foreach (array($info['mime'], $info['type']) as $_mime) {
1137 if (isset($options->content_type_exc[$_mime])) {
1138 $info['action'] = $options->content_type_exc[$_mime]['action'];
1139 $info['name'] = $options->content_type_exc[$_mime]['name'];
1140 break;
1141 }
1142 }
1143 }
1144 return $info;
1145}
1146
1147function remove_url_cruft($url) {
1148 // remove google analytics for the time being
1149 // regex adapted from http://navitronic.co.uk/2010/12/removing-google-analytics-cruft-from-urls/
1150 // https://gist.github.com/758177
1151 return preg_replace('/(\?|\&)utm_[a-z]+=[^\&]+/', '', $url);
1152}
1153
1154function make_substitutions($string) {
1155 if ($string == '') return $string;
1156 global $item, $effective_url;
1157 $string = str_replace('{url}', htmlspecialchars($item->get_permalink()), $string);
1158 $string = str_replace('{effective-url}', htmlspecialchars($effective_url), $string);
1159 return $string;
1160}
1161
1162function get_cache() {
1163 global $options, $valid_key;
1164 static $cache = null;
1165 if ($cache === null) {
1166 $frontendOptions = array(
1167 'lifetime' => 10*60, // cache lifetime of 10 minutes
1168 'automatic_serialization' => false,
1169 'write_control' => false,
1170 'automatic_cleaning_factor' => $options->cache_cleanup,
1171 'ignore_user_abort' => false
1172 );
1173 $backendOptions = array(
1174 'cache_dir' => ($valid_key) ? $options->cache_dir.'/rss-with-key/' : $options->cache_dir.'/rss/', // directory where to put the cache files
1175 'file_locking' => false,
1176 'read_control' => true,
1177 'read_control_type' => 'strlen',
1178 'hashed_directory_level' => $options->cache_directory_level,
1179 'hashed_directory_perm' => 0777,
1180 'cache_file_perm' => 0664,
1181 'file_name_prefix' => 'ff'
1182 );
1183 // getting a Zend_Cache_Core object
1184 $cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
1185 }
1186 return $cache;
1187}
1188
1189function debug($msg) {
1190 global $debug_mode;
1191 if ($debug_mode) {
1192 echo '* ',$msg,"\n";
1193 ob_flush();
1194 flush();
1195 }
1196} \ No newline at end of file
diff --git a/inc/3rdparty/makefulltextfeedHelpers.php b/inc/3rdparty/makefulltextfeedHelpers.php
new file mode 100755
index 00000000..ac872ab8
--- /dev/null
+++ b/inc/3rdparty/makefulltextfeedHelpers.php
@@ -0,0 +1,389 @@
1<?php
2
3// Autoloading of classes allows us to include files only when they're
4// needed. If we've got a cached copy, for example, only Zend_Cache is loaded.
5function autoload($class_name) {
6 static $dir = null;
7 if ($dir === null) $dir = dirname(__FILE__).'/libraries/';
8 static $mapping = array(
9 // Include FeedCreator for RSS/Atom creation
10 'FeedWriter' => 'feedwriter/FeedWriter.php',
11 'FeedItem' => 'feedwriter/FeedItem.php',
12 // Include ContentExtractor and Readability for identifying and extracting content from URLs
13 'ContentExtractor' => 'content-extractor/ContentExtractor.php',
14 'SiteConfig' => 'content-extractor/SiteConfig.php',
15 'Readability' => 'readability/Readability.php',
16 // Include Humble HTTP Agent to allow parallel requests and response caching
17 'HumbleHttpAgent' => 'humble-http-agent/HumbleHttpAgent.php',
18 'SimplePie_HumbleHttpAgent' => 'humble-http-agent/SimplePie_HumbleHttpAgent.php',
19 'CookieJar' => 'humble-http-agent/CookieJar.php',
20 // Include Zend Cache to improve performance (cache results)
21 'Zend_Cache' => 'Zend/Cache.php',
22 // Language detect
23 'Text_LanguageDetect' => 'language-detect/LanguageDetect.php',
24 // HTML5 Lib
25 'HTML5_Parser' => 'html5/Parser.php',
26 // htmLawed - used if XSS filter is enabled (xss_filter)
27 'htmLawed' => 'htmLawed/htmLawed.php'
28 );
29 if (isset($mapping[$class_name])) {
30 debug("** Loading class $class_name ({$mapping[$class_name]})");
31 require $dir.$mapping[$class_name];
32 return true;
33 } else {
34 return false;
35 }
36}
37spl_autoload_register('autoload');
38require dirname(__FILE__).'/libraries/simplepie/autoloader.php';
39
40
41class DummySingleItemFeed {
42 public $item;
43 function __construct($url) { $this->item = new DummySingleItem($url); }
44 public function get_title() { return ''; }
45 public function get_description() { return 'Content extracted from '.$this->item->url; }
46 public function get_link() { return $this->item->url; }
47 public function get_language() { return false; }
48 public function get_image_url() { return false; }
49 public function get_items($start=0, $max=1) { return array(0=>$this->item); }
50}
51class DummySingleItem {
52 public $url;
53 function __construct($url) { $this->url = $url; }
54 public function get_permalink() { return $this->url; }
55 public function get_title() { return null; }
56 public function get_date($format='') { return false; }
57 public function get_author($key=0) { return null; }
58 public function get_authors() { return null; }
59 public function get_description() { return ''; }
60 public function get_enclosure($key=0, $prefer=null) { return null; }
61 public function get_enclosures() { return null; }
62 public function get_categories() { return null; }
63}
64
65///////////////////////////////
66// HELPER FUNCTIONS
67///////////////////////////////
68
69// Adapted from WordPress
70// http://core.trac.wordpress.org/browser/tags/3.5.1/wp-includes/formatting.php#L2173
71function get_excerpt($text, $num_words=55, $more=null) {
72 if (null === $more) $more = '&hellip;';
73 $text = strip_tags($text);
74 //TODO: Check if word count is based on single characters (East Asian characters)
75 /*
76 if (1==2) {
77 $text = trim(preg_replace("/[\n\r\t ]+/", ' ', $text), ' ');
78 preg_match_all('/./u', $text, $words_array);
79 $words_array = array_slice($words_array[0], 0, $num_words + 1);
80 $sep = '';
81 } else {
82 $words_array = preg_split("/[\n\r\t ]+/", $text, $num_words + 1, PREG_SPLIT_NO_EMPTY);
83 $sep = ' ';
84 }
85 */
86 $words_array = preg_split("/[\n\r\t ]+/", $text, $num_words + 1, PREG_SPLIT_NO_EMPTY);
87 $sep = ' ';
88 if (count($words_array) > $num_words) {
89 array_pop($words_array);
90 $text = implode($sep, $words_array);
91 $text = $text.$more;
92 } else {
93 $text = implode($sep, $words_array);
94 }
95 // trim whitespace at beginning or end of string
96 // See: http://stackoverflow.com/questions/4166896/trim-unicode-whitespace-in-php-5-2
97 $text = preg_replace('/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $text);
98 return $text;
99}
100
101function url_allowed($url) {
102 global $options;
103 if (!empty($options->allowed_urls)) {
104 $allowed = false;
105 foreach ($options->allowed_urls as $allowurl) {
106 if (stristr($url, $allowurl) !== false) {
107 $allowed = true;
108 break;
109 }
110 }
111 if (!$allowed) return false;
112 } else {
113 foreach ($options->blocked_urls as $blockurl) {
114 if (stristr($url, $blockurl) !== false) {
115 return false;
116 }
117 }
118 }
119 return true;
120}
121
122//////////////////////////////////////////////
123// Convert $html to UTF8
124// (uses HTTP headers and HTML to find encoding)
125// adapted from http://stackoverflow.com/questions/910793/php-detect-encoding-and-make-everything-utf-8
126//////////////////////////////////////////////
127function convert_to_utf8($html, $header=null)
128{
129 $encoding = null;
130 if ($html || $header) {
131 if (is_array($header)) $header = implode("\n", $header);
132 if (!$header || !preg_match_all('/^Content-Type:\s+([^;]+)(?:;\s*charset=["\']?([^;"\'\n]*))?/im', $header, $match, PREG_SET_ORDER)) {
133 // error parsing the response
134 debug('Could not find Content-Type header in HTTP response');
135 } else {
136 $match = end($match); // get last matched element (in case of redirects)
137 if (isset($match[2])) $encoding = trim($match[2], "\"' \r\n\0\x0B\t");
138 }
139 // TODO: check to see if encoding is supported (can we convert it?)
140 // If it's not, result will be empty string.
141 // For now we'll check for invalid encoding types returned by some sites, e.g. 'none'
142 // Problem URL: http://facta.co.jp/blog/archives/20111026001026.html
143 if (!$encoding || $encoding == 'none') {
144 // search for encoding in HTML - only look at the first 50000 characters
145 // Why 50000? See, for example, http://www.lemonde.fr/festival-de-cannes/article/2012/05/23/deux-cretes-en-goguette-sur-la-croisette_1705732_766360.html
146 // TODO: improve this so it looks at smaller chunks first
147 $html_head = substr($html, 0, 50000);
148 if (preg_match('/^<\?xml\s+version=(?:"[^"]*"|\'[^\']*\')\s+encoding=("[^"]*"|\'[^\']*\')/s', $html_head, $match)) {
149 $encoding = trim($match[1], '"\'');
150 } elseif (preg_match('/<meta\s+http-equiv=["\']?Content-Type["\']? content=["\'][^;]+;\s*charset=["\']?([^;"\'>]+)/i', $html_head, $match)) {
151 $encoding = trim($match[1]);
152 } elseif (preg_match_all('/<meta\s+([^>]+)>/i', $html_head, $match)) {
153 foreach ($match[1] as $_test) {
154 if (preg_match('/charset=["\']?([^"\']+)/i', $_test, $_m)) {
155 $encoding = trim($_m[1]);
156 break;
157 }
158 }
159 }
160 }
161 if (isset($encoding)) $encoding = trim($encoding);
162 // trim is important here!
163 if (!$encoding || (strtolower($encoding) == 'iso-8859-1')) {
164 // replace MS Word smart qutoes
165 $trans = array();
166 $trans[chr(130)] = '&sbquo;'; // Single Low-9 Quotation Mark
167 $trans[chr(131)] = '&fnof;'; // Latin Small Letter F With Hook
168 $trans[chr(132)] = '&bdquo;'; // Double Low-9 Quotation Mark
169 $trans[chr(133)] = '&hellip;'; // Horizontal Ellipsis
170 $trans[chr(134)] = '&dagger;'; // Dagger
171 $trans[chr(135)] = '&Dagger;'; // Double Dagger
172 $trans[chr(136)] = '&circ;'; // Modifier Letter Circumflex Accent
173 $trans[chr(137)] = '&permil;'; // Per Mille Sign
174 $trans[chr(138)] = '&Scaron;'; // Latin Capital Letter S With Caron
175 $trans[chr(139)] = '&lsaquo;'; // Single Left-Pointing Angle Quotation Mark
176 $trans[chr(140)] = '&OElig;'; // Latin Capital Ligature OE
177 $trans[chr(145)] = '&lsquo;'; // Left Single Quotation Mark
178 $trans[chr(146)] = '&rsquo;'; // Right Single Quotation Mark
179 $trans[chr(147)] = '&ldquo;'; // Left Double Quotation Mark
180 $trans[chr(148)] = '&rdquo;'; // Right Double Quotation Mark
181 $trans[chr(149)] = '&bull;'; // Bullet
182 $trans[chr(150)] = '&ndash;'; // En Dash
183 $trans[chr(151)] = '&mdash;'; // Em Dash
184 $trans[chr(152)] = '&tilde;'; // Small Tilde
185 $trans[chr(153)] = '&trade;'; // Trade Mark Sign
186 $trans[chr(154)] = '&scaron;'; // Latin Small Letter S With Caron
187 $trans[chr(155)] = '&rsaquo;'; // Single Right-Pointing Angle Quotation Mark
188 $trans[chr(156)] = '&oelig;'; // Latin Small Ligature OE
189 $trans[chr(159)] = '&Yuml;'; // Latin Capital Letter Y With Diaeresis
190 $html = strtr($html, $trans);
191 }
192 if (!$encoding) {
193 debug('No character encoding found, so treating as UTF-8');
194 $encoding = 'utf-8';
195 } else {
196 debug('Character encoding: '.$encoding);
197 if (strtolower($encoding) != 'utf-8') {
198 debug('Converting to UTF-8');
199 $html = SimplePie_Misc::change_encoding($html, $encoding, 'utf-8');
200 }
201 }
202 }
203 return $html;
204}
205
206function makeAbsolute($base, $elem) {
207 $base = new SimplePie_IRI($base);
208 // remove '//' in URL path (used to prevent URLs from resolving properly)
209 // TODO: check if this is still the case
210 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
211 foreach(array('a'=>'href', 'img'=>'src') as $tag => $attr) {
212 $elems = $elem->getElementsByTagName($tag);
213 for ($i = $elems->length-1; $i >= 0; $i--) {
214 $e = $elems->item($i);
215 //$e->parentNode->replaceChild($articleContent->ownerDocument->createTextNode($e->textContent), $e);
216 makeAbsoluteAttr($base, $e, $attr);
217 }
218 if (strtolower($elem->tagName) == $tag) makeAbsoluteAttr($base, $elem, $attr);
219 }
220}
221function makeAbsoluteAttr($base, $e, $attr) {
222 if ($e->hasAttribute($attr)) {
223 // Trim leading and trailing white space. I don't really like this but
224 // unfortunately it does appear on some sites. e.g. <img src=" /path/to/image.jpg" />
225 $url = trim(str_replace('%20', ' ', $e->getAttribute($attr)));
226 $url = str_replace(' ', '%20', $url);
227 if (!preg_match('!https?://!i', $url)) {
228 if ($absolute = SimplePie_IRI::absolutize($base, $url)) {
229 $e->setAttribute($attr, $absolute);
230 }
231 }
232 }
233}
234function makeAbsoluteStr($base, $url) {
235 $base = new SimplePie_IRI($base);
236 // remove '//' in URL path (causes URLs not to resolve properly)
237 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
238 if (preg_match('!^https?://!i', $url)) {
239 // already absolute
240 return $url;
241 } else {
242 if ($absolute = SimplePie_IRI::absolutize($base, $url)) {
243 return $absolute;
244 }
245 return false;
246 }
247}
248// returns single page response, or false if not found
249function getSinglePage($item, $html, $url) {
250 global $http, $extractor;
251 debug('Looking for site config files to see if single page link exists');
252 $site_config = $extractor->buildSiteConfig($url, $html);
253 $splink = null;
254 if (!empty($site_config->single_page_link)) {
255 $splink = $site_config->single_page_link;
256 } elseif (!empty($site_config->single_page_link_in_feed)) {
257 // single page link xpath is targeted at feed
258 $splink = $site_config->single_page_link_in_feed;
259 // so let's replace HTML with feed item description
260 $html = $item->get_description();
261 }
262 if (isset($splink)) {
263 // Build DOM tree from HTML
264 $readability = new Readability($html, $url);
265 $xpath = new DOMXPath($readability->dom);
266 // Loop through single_page_link xpath expressions
267 $single_page_url = null;
268 foreach ($splink as $pattern) {
269 $elems = @$xpath->evaluate($pattern, $readability->dom);
270 if (is_string($elems)) {
271 $single_page_url = trim($elems);
272 break;
273 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
274 foreach ($elems as $item) {
275 if ($item instanceof DOMElement && $item->hasAttribute('href')) {
276 $single_page_url = $item->getAttribute('href');
277 break 2;
278 } elseif ($item instanceof DOMAttr && $item->value) {
279 $single_page_url = $item->value;
280 break 2;
281 }
282 }
283 }
284 }
285 // If we've got URL, resolve against $url
286 if (isset($single_page_url) && ($single_page_url = makeAbsoluteStr($url, $single_page_url))) {
287 // check it's not what we have already!
288 if ($single_page_url != $url) {
289 // it's not, so let's try to fetch it...
290 $_prev_ref = $http->referer;
291 $http->referer = $single_page_url;
292 if (($response = $http->get($single_page_url, true)) && $response['status_code'] < 300) {
293 $http->referer = $_prev_ref;
294 return $response;
295 }
296 $http->referer = $_prev_ref;
297 }
298 }
299 }
300 return false;
301}
302
303// based on content-type http header, decide what to do
304// param: HTTP headers string
305// return: array with keys: 'mime', 'type', 'subtype', 'action', 'name'
306// e.g. array('mime'=>'image/jpeg', 'type'=>'image', 'subtype'=>'jpeg', 'action'=>'link', 'name'=>'Image')
307function get_mime_action_info($headers) {
308 global $options;
309 // check if action defined for returned Content-Type
310 $info = array();
311 if (preg_match('!^Content-Type:\s*(([-\w]+)/([-\w\+]+))!im', $headers, $match)) {
312 // look for full mime type (e.g. image/jpeg) or just type (e.g. image)
313 // match[1] = full mime type, e.g. image/jpeg
314 // match[2] = first part, e.g. image
315 // match[3] = last part, e.g. jpeg
316 $info['mime'] = strtolower(trim($match[1]));
317 $info['type'] = strtolower(trim($match[2]));
318 $info['subtype'] = strtolower(trim($match[3]));
319 foreach (array($info['mime'], $info['type']) as $_mime) {
320 if (isset($options->content_type_exc[$_mime])) {
321 $info['action'] = $options->content_type_exc[$_mime]['action'];
322 $info['name'] = $options->content_type_exc[$_mime]['name'];
323 break;
324 }
325 }
326 }
327 return $info;
328}
329
330function remove_url_cruft($url) {
331 // remove google analytics for the time being
332 // regex adapted from http://navitronic.co.uk/2010/12/removing-google-analytics-cruft-from-urls/
333 // https://gist.github.com/758177
334 return preg_replace('/(\?|\&)utm_[a-z]+=[^\&]+/', '', $url);
335}
336
337function make_substitutions($string) {
338 if ($string == '') return $string;
339 global $item, $effective_url;
340 $string = str_replace('{url}', htmlspecialchars($item->get_permalink()), $string);
341 $string = str_replace('{effective-url}', htmlspecialchars($effective_url), $string);
342 return $string;
343}
344
345function get_cache() {
346 global $options, $valid_key;
347 static $cache = null;
348 if ($cache === null) {
349 $frontendOptions = array(
350 'lifetime' => 10*60, // cache lifetime of 10 minutes
351 'automatic_serialization' => false,
352 'write_control' => false,
353 'automatic_cleaning_factor' => $options->cache_cleanup,
354 'ignore_user_abort' => false
355 );
356 $backendOptions = array(
357 'cache_dir' => ($valid_key) ? $options->cache_dir.'/rss-with-key/' : $options->cache_dir.'/rss/', // directory where to put the cache files
358 'file_locking' => false,
359 'read_control' => true,
360 'read_control_type' => 'strlen',
361 'hashed_directory_level' => $options->cache_directory_level,
362 'hashed_directory_perm' => 0777,
363 'cache_file_perm' => 0664,
364 'file_name_prefix' => 'ff'
365 );
366 // getting a Zend_Cache_Core object
367 $cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
368 }
369 return $cache;
370}
371
372function debug($msg) {
373 global $debug_mode;
374 if ($debug_mode) {
375 echo '* ',$msg,"\n";
376 ob_flush();
377 flush();
378 }
379}
380
381function get_base_url($dom) {
382 $xpath = new DOMXPath($dom);
383 $base_url = @$xpath->evaluate('string(//head/base/@href)', $dom);
384 if ($base_url !== '') {
385 return $base_url;
386 } else {
387 return false;
388 }
389}
diff --git a/inc/3rdparty/simple_html_dom.php b/inc/3rdparty/simple_html_dom.php
index 43b94e57..9b73b105 100644..100755
--- a/inc/3rdparty/simple_html_dom.php
+++ b/inc/3rdparty/simple_html_dom.php
@@ -34,7 +34,7 @@
34 * @author S.C. Chen <me578022@gmail.com> 34 * @author S.C. Chen <me578022@gmail.com>
35 * @author John Schlick 35 * @author John Schlick
36 * @author Rus Carroll 36 * @author Rus Carroll
37 * @version 1.5 ($Rev: 202 $) 37 * @version 1.5 ($Rev: 210 $)
38 * @package PlaceLocalInclude 38 * @package PlaceLocalInclude
39 * @subpackage simple_html_dom 39 * @subpackage simple_html_dom
40 */ 40 */
@@ -269,7 +269,10 @@ class simple_html_dom_node
269 { 269 {
270 return $this->children; 270 return $this->children;
271 } 271 }
272 if (isset($this->children[$idx])) return $this->children[$idx]; 272 if (isset($this->children[$idx]))
273 {
274 return $this->children[$idx];
275 }
273 return null; 276 return null;
274 } 277 }
275 278
@@ -330,14 +333,14 @@ class simple_html_dom_node
330 function find_ancestor_tag($tag) 333 function find_ancestor_tag($tag)
331 { 334 {
332 global $debug_object; 335 global $debug_object;
333 if (is_object($debug_object)) { $debug_object->debugLogEntry(1); } 336 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
334 337
335 // Start by including ourselves in the comparison. 338 // Start by including ourselves in the comparison.
336 $returnDom = $this; 339 $returnDom = $this;
337 340
338 while (!is_null($returnDom)) 341 while (!is_null($returnDom))
339 { 342 {
340 if (is_object($debug_object)) { $debug_object->debugLog(2, "Current tag is: " . $returnDom->tag); } 343 if (is_object($debug_object)) { $debug_object->debug_log(2, "Current tag is: " . $returnDom->tag); }
341 344
342 if ($returnDom->tag == $tag) 345 if ($returnDom->tag == $tag)
343 { 346 {
@@ -374,7 +377,7 @@ class simple_html_dom_node
374 $text = " with text: " . $this->text; 377 $text = " with text: " . $this->text;
375 } 378 }
376 } 379 }
377 $debug_object->debugLog(1, 'Innertext of tag: ' . $this->tag . $text); 380 $debug_object->debug_log(1, 'Innertext of tag: ' . $this->tag . $text);
378 } 381 }
379 382
380 if ($this->tag==='root') return $this->innertext(); 383 if ($this->tag==='root') return $this->innertext();
@@ -532,7 +535,9 @@ class simple_html_dom_node
532 foreach ($head as $k=>$v) 535 foreach ($head as $k=>$v)
533 { 536 {
534 if (!isset($found_keys[$k])) 537 if (!isset($found_keys[$k]))
538 {
535 $found_keys[$k] = 1; 539 $found_keys[$k] = 1;
540 }
536 } 541 }
537 } 542 }
538 543
@@ -554,7 +559,7 @@ class simple_html_dom_node
554 protected function seek($selector, &$ret, $lowercase=false) 559 protected function seek($selector, &$ret, $lowercase=false)
555 { 560 {
556 global $debug_object; 561 global $debug_object;
557 if (is_object($debug_object)) { $debug_object->debugLogEntry(1); } 562 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
558 563
559 list($tag, $key, $val, $exp, $no_key) = $selector; 564 list($tag, $key, $val, $exp, $no_key) = $selector;
560 565
@@ -615,7 +620,7 @@ class simple_html_dom_node
615 // this is a normal search, we want the value of that attribute of the tag. 620 // this is a normal search, we want the value of that attribute of the tag.
616 $nodeKeyValue = $node->attr[$key]; 621 $nodeKeyValue = $node->attr[$key];
617 } 622 }
618 if (is_object($debug_object)) {$debug_object->debugLog(2, "testing node: " . $node->tag . " for attribute: " . $key . $exp . $val . " where nodes value is: " . $nodeKeyValue);} 623 if (is_object($debug_object)) {$debug_object->debug_log(2, "testing node: " . $node->tag . " for attribute: " . $key . $exp . $val . " where nodes value is: " . $nodeKeyValue);}
619 624
620 //PaperG - If lowercase is set, do a case insensitive test of the value of the selector. 625 //PaperG - If lowercase is set, do a case insensitive test of the value of the selector.
621 if ($lowercase) { 626 if ($lowercase) {
@@ -623,7 +628,7 @@ class simple_html_dom_node
623 } else { 628 } else {
624 $check = $this->match($exp, $val, $nodeKeyValue); 629 $check = $this->match($exp, $val, $nodeKeyValue);
625 } 630 }
626 if (is_object($debug_object)) {$debug_object->debugLog(2, "after match: " . ($check ? "true" : "false"));} 631 if (is_object($debug_object)) {$debug_object->debug_log(2, "after match: " . ($check ? "true" : "false"));}
627 632
628 // handle multiple class 633 // handle multiple class
629 if (!$check && strcasecmp($key, 'class')===0) { 634 if (!$check && strcasecmp($key, 'class')===0) {
@@ -645,12 +650,12 @@ class simple_html_dom_node
645 unset($node); 650 unset($node);
646 } 651 }
647 // It's passed by reference so this is actually what this function returns. 652 // It's passed by reference so this is actually what this function returns.
648 if (is_object($debug_object)) {$debug_object->debugLog(1, "EXIT - ret: ", $ret);} 653 if (is_object($debug_object)) {$debug_object->debug_log(1, "EXIT - ret: ", $ret);}
649 } 654 }
650 655
651 protected function match($exp, $pattern, $value) { 656 protected function match($exp, $pattern, $value) {
652 global $debug_object; 657 global $debug_object;
653 if (is_object($debug_object)) {$debug_object->debugLogEntry(1);} 658 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
654 659
655 switch ($exp) { 660 switch ($exp) {
656 case '=': 661 case '=':
@@ -672,7 +677,7 @@ class simple_html_dom_node
672 677
673 protected function parse_selector($selector_string) { 678 protected function parse_selector($selector_string) {
674 global $debug_object; 679 global $debug_object;
675 if (is_object($debug_object)) {$debug_object->debugLogEntry(1);} 680 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
676 681
677 // pattern of CSS selectors, modified from mootools 682 // pattern of CSS selectors, modified from mootools
678 // Paperg: Add the colon to the attrbute, so that it properly finds <tag attr:ibute="something" > like google does. 683 // Paperg: Add the colon to the attrbute, so that it properly finds <tag attr:ibute="something" > like google does.
@@ -683,7 +688,7 @@ class simple_html_dom_node
683// $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is"; 688// $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
684 $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is"; 689 $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
685 preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER); 690 preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER);
686 if (is_object($debug_object)) {$debug_object->debugLog(2, "Matches Array: ", $matches);} 691 if (is_object($debug_object)) {$debug_object->debug_log(2, "Matches Array: ", $matches);}
687 692
688 $selectors = array(); 693 $selectors = array();
689 $result = array(); 694 $result = array();
@@ -718,12 +723,14 @@ class simple_html_dom_node
718 return $selectors; 723 return $selectors;
719 } 724 }
720 725
721 function __get($name) { 726 function __get($name)
727 {
722 if (isset($this->attr[$name])) 728 if (isset($this->attr[$name]))
723 { 729 {
724 return $this->convert_text($this->attr[$name]); 730 return $this->convert_text($this->attr[$name]);
725 } 731 }
726 switch ($name) { 732 switch ($name)
733 {
727 case 'outertext': return $this->outertext(); 734 case 'outertext': return $this->outertext();
728 case 'innertext': return $this->innertext(); 735 case 'innertext': return $this->innertext();
729 case 'plaintext': return $this->text(); 736 case 'plaintext': return $this->text();
@@ -732,22 +739,30 @@ class simple_html_dom_node
732 } 739 }
733 } 740 }
734 741
735 function __set($name, $value) { 742 function __set($name, $value)
736 switch ($name) { 743 {
744 global $debug_object;
745 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
746
747 switch ($name)
748 {
737 case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value; 749 case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value;
738 case 'innertext': 750 case 'innertext':
739 if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value; 751 if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value;
740 return $this->_[HDOM_INFO_INNER] = $value; 752 return $this->_[HDOM_INFO_INNER] = $value;
741 } 753 }
742 if (!isset($this->attr[$name])) { 754 if (!isset($this->attr[$name]))
755 {
743 $this->_[HDOM_INFO_SPACE][] = array(' ', '', ''); 756 $this->_[HDOM_INFO_SPACE][] = array(' ', '', '');
744 $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE; 757 $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE;
745 } 758 }
746 $this->attr[$name] = $value; 759 $this->attr[$name] = $value;
747 } 760 }
748 761
749 function __isset($name) { 762 function __isset($name)
750 switch ($name) { 763 {
764 switch ($name)
765 {
751 case 'outertext': return true; 766 case 'outertext': return true;
752 case 'innertext': return true; 767 case 'innertext': return true;
753 case 'plaintext': return true; 768 case 'plaintext': return true;
@@ -765,7 +780,7 @@ class simple_html_dom_node
765 function convert_text($text) 780 function convert_text($text)
766 { 781 {
767 global $debug_object; 782 global $debug_object;
768 if (is_object($debug_object)) {$debug_object->debugLogEntry(1);} 783 if (is_object($debug_object)) {$debug_object->debug_log_entry(1);}
769 784
770 $converted_text = $text; 785 $converted_text = $text;
771 786
@@ -777,7 +792,7 @@ class simple_html_dom_node
777 $sourceCharset = strtoupper($this->dom->_charset); 792 $sourceCharset = strtoupper($this->dom->_charset);
778 $targetCharset = strtoupper($this->dom->_target_charset); 793 $targetCharset = strtoupper($this->dom->_target_charset);
779 } 794 }
780 if (is_object($debug_object)) {$debug_object->debugLog(3, "source charset: " . $sourceCharset . " target charaset: " . $targetCharset);} 795 if (is_object($debug_object)) {$debug_object->debug_log(3, "source charset: " . $sourceCharset . " target charaset: " . $targetCharset);}
781 796
782 if (!empty($sourceCharset) && !empty($targetCharset) && (strcasecmp($sourceCharset, $targetCharset) != 0)) 797 if (!empty($sourceCharset) && !empty($targetCharset) && (strcasecmp($sourceCharset, $targetCharset) != 0))
783 { 798 {
@@ -1045,10 +1060,10 @@ class simple_html_dom
1045 1060
1046 // prepare 1061 // prepare
1047 $this->prepare($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText); 1062 $this->prepare($str, $lowercase, $stripRN, $defaultBRText, $defaultSpanText);
1048 // strip out comments
1049 $this->remove_noise("'<!--(.*?)-->'is");
1050 // strip out cdata 1063 // strip out cdata
1051 $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is", true); 1064 $this->remove_noise("'<!\[CDATA\[(.*?)\]\]>'is", true);
1065 // strip out comments
1066 $this->remove_noise("'<!--(.*?)-->'is");
1052 // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037 1067 // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037
1053 // Script tags removal now preceeds style tag removal. 1068 // Script tags removal now preceeds style tag removal.
1054 // strip out <script> tags 1069 // strip out <script> tags
@@ -1078,10 +1093,15 @@ class simple_html_dom
1078 // load html from file 1093 // load html from file
1079 function load_file() 1094 function load_file()
1080 { 1095 {
1096 //external error: NOT related to dom loading
1097 $extError=error_get_last();
1098
1081 $args = func_get_args(); 1099 $args = func_get_args();
1082 $this->load(call_user_func_array('file_get_contents', $args), true); 1100 $this->load(call_user_func_array('file_get_contents', $args), true);
1101
1083 // Throw an error if we can't properly load the dom. 1102 // Throw an error if we can't properly load the dom.
1084 if (($error=error_get_last())!==null) { 1103 $error=error_get_last();
1104 if ($error!==$extError) {
1085 $this->clear(); 1105 $this->clear();
1086 return false; 1106 return false;
1087 } 1107 }
@@ -1198,22 +1218,22 @@ class simple_html_dom
1198 if ($success) 1218 if ($success)
1199 { 1219 {
1200 $charset = $matches[1]; 1220 $charset = $matches[1];
1201 if (is_object($debug_object)) {$debug_object->debugLog(2, 'header content-type found charset of: ' . $charset);} 1221 if (is_object($debug_object)) {$debug_object->debug_log(2, 'header content-type found charset of: ' . $charset);}
1202 } 1222 }
1203 1223
1204 } 1224 }
1205 1225
1206 if (empty($charset)) 1226 if (empty($charset))
1207 { 1227 {
1208 $el = $this->root->find('meta[http-equiv=Content-Type]',0); 1228 $el = $this->root->find('meta[http-equiv=Content-Type]',0, true);
1209 if (!empty($el)) 1229 if (!empty($el))
1210 { 1230 {
1211 $fullvalue = $el->content; 1231 $fullvalue = $el->content;
1212 if (is_object($debug_object)) {$debug_object->debugLog(2, 'meta content-type tag found' . $fullvalue);} 1232 if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag found' . $fullvalue);}
1213 1233
1214 if (!empty($fullvalue)) 1234 if (!empty($fullvalue))
1215 { 1235 {
1216 $success = preg_match('/charset=(.+)/', $fullvalue, $matches); 1236 $success = preg_match('/charset=(.+)/i', $fullvalue, $matches);
1217 if ($success) 1237 if ($success)
1218 { 1238 {
1219 $charset = $matches[1]; 1239 $charset = $matches[1];
@@ -1221,7 +1241,7 @@ class simple_html_dom
1221 else 1241 else
1222 { 1242 {
1223 // If there is a meta tag, and they don't specify the character set, research says that it's typically ISO-8859-1 1243 // If there is a meta tag, and they don't specify the character set, research says that it's typically ISO-8859-1
1224 if (is_object($debug_object)) {$debug_object->debugLog(2, 'meta content-type tag couldn\'t be parsed. using iso-8859 default.');} 1244 if (is_object($debug_object)) {$debug_object->debug_log(2, 'meta content-type tag couldn\'t be parsed. using iso-8859 default.');}
1225 $charset = 'ISO-8859-1'; 1245 $charset = 'ISO-8859-1';
1226 } 1246 }
1227 } 1247 }
@@ -1231,14 +1251,19 @@ class simple_html_dom
1231 // If we couldn't find a charset above, then lets try to detect one based on the text we got... 1251 // If we couldn't find a charset above, then lets try to detect one based on the text we got...
1232 if (empty($charset)) 1252 if (empty($charset))
1233 { 1253 {
1234 // Have php try to detect the encoding from the text given to us. 1254 // Use this in case mb_detect_charset isn't installed/loaded on this machine.
1235 $charset = mb_detect_encoding($this->root->plaintext . "ascii", $encoding_list = array( "UTF-8", "CP1252" ) ); 1255 $charset = false;
1236 if (is_object($debug_object)) {$debug_object->debugLog(2, 'mb_detect found: ' . $charset);} 1256 if (function_exists('mb_detect_encoding'))
1257 {
1258 // Have php try to detect the encoding from the text given to us.
1259 $charset = mb_detect_encoding($this->root->plaintext . "ascii", $encoding_list = array( "UTF-8", "CP1252" ) );
1260 if (is_object($debug_object)) {$debug_object->debug_log(2, 'mb_detect found: ' . $charset);}
1261 }
1237 1262
1238 // and if this doesn't work... then we need to just wrongheadedly assume it's UTF-8 so that we can move on - cause this will usually give us most of what we need... 1263 // and if this doesn't work... then we need to just wrongheadedly assume it's UTF-8 so that we can move on - cause this will usually give us most of what we need...
1239 if ($charset === false) 1264 if ($charset === false)
1240 { 1265 {
1241 if (is_object($debug_object)) {$debug_object->debugLog(2, 'since mb_detect failed - using default of utf-8');} 1266 if (is_object($debug_object)) {$debug_object->debug_log(2, 'since mb_detect failed - using default of utf-8');}
1242 $charset = 'UTF-8'; 1267 $charset = 'UTF-8';
1243 } 1268 }
1244 } 1269 }
@@ -1246,11 +1271,11 @@ class simple_html_dom
1246 // Since CP1252 is a superset, if we get one of it's subsets, we want it instead. 1271 // Since CP1252 is a superset, if we get one of it's subsets, we want it instead.
1247 if ((strtolower($charset) == strtolower('ISO-8859-1')) || (strtolower($charset) == strtolower('Latin1')) || (strtolower($charset) == strtolower('Latin-1'))) 1272 if ((strtolower($charset) == strtolower('ISO-8859-1')) || (strtolower($charset) == strtolower('Latin1')) || (strtolower($charset) == strtolower('Latin-1')))
1248 { 1273 {
1249 if (is_object($debug_object)) {$debug_object->debugLog(2, 'replacing ' . $charset . ' with CP1252 as its a superset');} 1274 if (is_object($debug_object)) {$debug_object->debug_log(2, 'replacing ' . $charset . ' with CP1252 as its a superset');}
1250 $charset = 'CP1252'; 1275 $charset = 'CP1252';
1251 } 1276 }
1252 1277
1253 if (is_object($debug_object)) {$debug_object->debugLog(1, 'EXIT - ' . $charset);} 1278 if (is_object($debug_object)) {$debug_object->debug_log(1, 'EXIT - ' . $charset);}
1254 1279
1255 return $this->_charset = $charset; 1280 return $this->_charset = $charset;
1256 } 1281 }
@@ -1616,14 +1641,14 @@ class simple_html_dom
1616 protected function remove_noise($pattern, $remove_tag=false) 1641 protected function remove_noise($pattern, $remove_tag=false)
1617 { 1642 {
1618 global $debug_object; 1643 global $debug_object;
1619 if (is_object($debug_object)) { $debug_object->debugLogEntry(1); } 1644 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1620 1645
1621 $count = preg_match_all($pattern, $this->doc, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE); 1646 $count = preg_match_all($pattern, $this->doc, $matches, PREG_SET_ORDER|PREG_OFFSET_CAPTURE);
1622 1647
1623 for ($i=$count-1; $i>-1; --$i) 1648 for ($i=$count-1; $i>-1; --$i)
1624 { 1649 {
1625 $key = '___noise___'.sprintf('% 5d', count($this->noise)+1000); 1650 $key = '___noise___'.sprintf('% 5d', count($this->noise)+1000);
1626 if (is_object($debug_object)) { $debug_object->debugLog(2, 'key is: ' . $key); } 1651 if (is_object($debug_object)) { $debug_object->debug_log(2, 'key is: ' . $key); }
1627 $idx = ($remove_tag) ? 0 : 1; 1652 $idx = ($remove_tag) ? 0 : 1;
1628 $this->noise[$key] = $matches[$i][$idx][0]; 1653 $this->noise[$key] = $matches[$i][$idx][0];
1629 $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0])); 1654 $this->doc = substr_replace($this->doc, $key, $matches[$i][$idx][1], strlen($matches[$i][$idx][0]));
@@ -1641,7 +1666,7 @@ class simple_html_dom
1641 function restore_noise($text) 1666 function restore_noise($text)
1642 { 1667 {
1643 global $debug_object; 1668 global $debug_object;
1644 if (is_object($debug_object)) { $debug_object->debugLogEntry(1); } 1669 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1645 1670
1646 while (($pos=strpos($text, '___noise___'))!==false) 1671 while (($pos=strpos($text, '___noise___'))!==false)
1647 { 1672 {
@@ -1649,7 +1674,7 @@ class simple_html_dom
1649 if (strlen($text) > $pos+15) 1674 if (strlen($text) > $pos+15)
1650 { 1675 {
1651 $key = '___noise___'.$text[$pos+11].$text[$pos+12].$text[$pos+13].$text[$pos+14].$text[$pos+15]; 1676 $key = '___noise___'.$text[$pos+11].$text[$pos+12].$text[$pos+13].$text[$pos+14].$text[$pos+15];
1652 if (is_object($debug_object)) { $debug_object->debugLog(2, 'located key of: ' . $key); } 1677 if (is_object($debug_object)) { $debug_object->debug_log(2, 'located key of: ' . $key); }
1653 1678
1654 if (isset($this->noise[$key])) 1679 if (isset($this->noise[$key]))
1655 { 1680 {
@@ -1674,7 +1699,7 @@ class simple_html_dom
1674 function search_noise($text) 1699 function search_noise($text)
1675 { 1700 {
1676 global $debug_object; 1701 global $debug_object;
1677 if (is_object($debug_object)) { $debug_object->debugLogEntry(1); } 1702 if (is_object($debug_object)) { $debug_object->debug_log_entry(1); }
1678 1703
1679 foreach($this->noise as $noiseElement) 1704 foreach($this->noise as $noiseElement)
1680 { 1705 {
diff --git a/inc/3rdparty/site_config/custom/dailymotion.com.txt b/inc/3rdparty/site_config/custom/dailymotion.com.txt
new file mode 100755
index 00000000..0cad808f
--- /dev/null
+++ b/inc/3rdparty/site_config/custom/dailymotion.com.txt
@@ -0,0 +1,12 @@
1title: //title
2body: //iframe
3
4replace_string(<![CDATA[): _
5replace_string(]]>): _
6
7single_page_link: //link[@type='application/xml+oembed']
8
9prune: no
10tidy: no
11
12http://www.dailymotion.com/video/x1vk5oh_before-they-were-on-game-of-thrones_people
diff --git a/inc/3rdparty/site_config/custom/index.php b/inc/3rdparty/site_config/custom/index.php
new file mode 100644
index 00000000..a3d5f739
--- /dev/null
+++ b/inc/3rdparty/site_config/custom/index.php
@@ -0,0 +1,3 @@
1<?php
2// this is here to prevent directory listing over the web
3?> \ No newline at end of file
diff --git a/inc/3rdparty/site_config/custom/mobile.lemondeinformatique.fr.txt b/inc/3rdparty/site_config/custom/mobile.lemondeinformatique.fr.txt
new file mode 100644
index 00000000..24aec5c3
--- /dev/null
+++ b/inc/3rdparty/site_config/custom/mobile.lemondeinformatique.fr.txt
@@ -0,0 +1,6 @@
1title: //h2
2body: div[@id='illustration'] | //p
3prune: no
4tidy: no
5
6test_url: http://mobile.lemondeinformatique.fr/actualites/lire-les-datacenters-d-apple-google-et-facebook-eco-responsables-selon-greenpeace-le-monde-informatique-57122.html
diff --git a/inc/3rdparty/site_config/custom/ted.com.txt b/inc/3rdparty/site_config/custom/ted.com.txt
new file mode 100755
index 00000000..4940d2bc
--- /dev/null
+++ b/inc/3rdparty/site_config/custom/ted.com.txt
@@ -0,0 +1,11 @@
1title: //title
2body: //div[@class='talk-article__body talk-transcript__body'] | //div[@class='media__image media__image--thumb talk-link__image']
3
4strip_id_or_class: talk-transcript__para__time
5
6single_page_link: //a[@id='hero-transcript-link']
7
8#prune: no
9tidy: no
10
11test_url: http://www.ted.com/talks/andrew_solomon_how_the_worst_moments_in_our_lives_make_us_who_we_are
diff --git a/inc/3rdparty/site_config/index.php b/inc/3rdparty/site_config/index.php
index a1b767fd..76ca8b3c 100644
--- a/inc/3rdparty/site_config/index.php
+++ b/inc/3rdparty/site_config/index.php
@@ -1,3 +1,2 @@
1<?php 1<?php
2// this is here to prevent directory listing over the web 2// this is here to prevent directory listing over the web \ No newline at end of file
3?> \ No newline at end of file
diff --git a/inc/3rdparty/site_config/standard/.about.com.txt b/inc/3rdparty/site_config/standard/.about.com.txt
new file mode 100644
index 00000000..e1ebaee3
--- /dev/null
+++ b/inc/3rdparty/site_config/standard/.about.com.txt
@@ -0,0 +1,14 @@
1body: //div[@id='articlebody']
2title: //h1
3author: //p[@id='by']//a
4
5next_page_link: //span[@class='next']/a
6# Not the same as below!
7
8prune: yes
9tidy: no
10
11# Annoying 'next' links plainly inside the article body
12strip: //*[text()[contains(.,'Next: ')]]
13
14test_url: http://psychology.about.com/od/theoriesofpersonality/ss/defensemech.htm
diff --git a/inc/3rdparty/site_config/standard/moo.nac.uci.edu.txt b/inc/3rdparty/site_config/standard/moo.nac.uci.edu.txt
new file mode 100644
index 00000000..24c949e9
--- /dev/null
+++ b/inc/3rdparty/site_config/standard/moo.nac.uci.edu.txt
@@ -0,0 +1,9 @@
1title: //div[@id='header']//h1[1]
2
3body: //div[@id='content']
4
5strip_id_or_class: toc
6
7prune: no
8
9test_url: http://moo.nac.uci.edu/~hjm/HOWTO_move_data.html
diff --git a/inc/3rdparty/site_config/standard/politico.com.txt b/inc/3rdparty/site_config/standard/politico.com.txt
index 121fd5b9..c5302d1b 100644..100755
--- a/inc/3rdparty/site_config/standard/politico.com.txt
+++ b/inc/3rdparty/site_config/standard/politico.com.txt
@@ -4,10 +4,14 @@ body://div[contains(@class,"story-text")]
4# Why doesn't this work? next_page_link://ul[contains(@class,"pagination")]/li/a[@rel="next"] 4# Why doesn't this work? next_page_link://ul[contains(@class,"pagination")]/li/a[@rel="next"]
5 5
6next_page_link://ul[contains(@class,"pagination")]/li[contains(@class, "current")]/following-sibling::node()/a 6next_page_link://ul[contains(@class,"pagination")]/li[contains(@class, "current")]/following-sibling::node()/a
7next_page_link://div[contains(@class,"pagination")]/ol/li[contains(@class, "current")]/following-sibling::node()/a
7date://meta[@name="publish_date"]/@content 8date://meta[@name="publish_date"]/@content
8 9
9strip://div[contains(@class, "breadcrumbs")] 10strip://div[contains(@class, "breadcrumbs")]
10strip://a[contains(@class, "hidden")] 11strip://a[contains(@class, "hidden")]
11strip://div[contains(@class, "story-embed")] 12strip://div[contains(@class, "story-embed")]
12strip://div[contains(@class, "story-text")]//p/a[contains(text(), "Also on POLITICO:")]/.. 13strip://div[contains(@class, "story-text")]//p/a[contains(text(), "Also on POLITICO:")]/..
14strip://div[contains(@class, "story-interrupt")]
15strip://footer[contains(@class, "author-bio")]
16
13test_url: http://www.politico.com/news/stories/0712/78105.html \ No newline at end of file 17test_url: http://www.politico.com/news/stories/0712/78105.html \ No newline at end of file
diff --git a/inc/3rdparty/site_config/standard/version.txt b/inc/3rdparty/site_config/standard/version.txt
index bf0d87ab..eaf01ebd 100644
--- a/inc/3rdparty/site_config/standard/version.txt
+++ b/inc/3rdparty/site_config/standard/version.txt
@@ -1 +1 @@
4 \ No newline at end of file 2013-05-12T22:53:07Z \ No newline at end of file
diff --git a/inc/poche/Database.class.php b/inc/poche/Database.class.php
index c998fe14..11cccb72 100755
--- a/inc/poche/Database.class.php
+++ b/inc/poche/Database.class.php
@@ -18,7 +18,7 @@ class Database {
18 'default' => 'ORDER BY entries.id' 18 'default' => 'ORDER BY entries.id'
19 ); 19 );
20 20
21 function __construct() 21 function __construct()
22 { 22 {
23 switch (STORAGE) { 23 switch (STORAGE) {
24 case 'sqlite': 24 case 'sqlite':
@@ -27,12 +27,14 @@ class Database {
27 break; 27 break;
28 case 'mysql': 28 case 'mysql':
29 $db_path = 'mysql:host=' . STORAGE_SERVER . ';dbname=' . STORAGE_DB; 29 $db_path = 'mysql:host=' . STORAGE_SERVER . ';dbname=' . STORAGE_DB;
30 $this->handle = new PDO($db_path, STORAGE_USER, STORAGE_PASSWORD); 30 $this->handle = new PDO($db_path, STORAGE_USER, STORAGE_PASSWORD);
31 break; 31 break;
32 case 'postgres': 32 case 'postgres':
33 $db_path = 'pgsql:host=' . STORAGE_SERVER . ';dbname=' . STORAGE_DB; 33 $db_path = 'pgsql:host=' . STORAGE_SERVER . ';dbname=' . STORAGE_DB;
34 $this->handle = new PDO($db_path, STORAGE_USER, STORAGE_PASSWORD); 34 $this->handle = new PDO($db_path, STORAGE_USER, STORAGE_PASSWORD);
35 break; 35 break;
36 default:
37 die(STORAGE . ' is not a recognised database system !');
36 } 38 }
37 39
38 $this->handle->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 40 $this->handle->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
@@ -51,7 +53,7 @@ class Database {
51 } 53 }
52 $hasAdmin = count($query->fetchAll()); 54 $hasAdmin = count($query->fetchAll());
53 55
54 if ($hasAdmin == 0) 56 if ($hasAdmin == 0)
55 return false; 57 return false;
56 58
57 return true; 59 return true;
@@ -77,7 +79,7 @@ class Database {
77 } 79 }
78 else { 80 else {
79 $sql = ' 81 $sql = '
80 CREATE TABLE tags ( 82 CREATE TABLE IF NOT EXISTS tags (
81 id bigserial primary key, 83 id bigserial primary key,
82 value varchar(255) NOT NULL 84 value varchar(255) NOT NULL
83 ); 85 );
@@ -110,7 +112,7 @@ class Database {
110 } 112 }
111 else { 113 else {
112 $sql = ' 114 $sql = '
113 CREATE TABLE tags_entries ( 115 CREATE TABLE IF NOT EXISTS tags_entries (
114 id bigserial primary key, 116 id bigserial primary key,
115 entry_id integer NOT NULL, 117 entry_id integer NOT NULL,
116 tag_id integer NOT NULL 118 tag_id integer NOT NULL
@@ -140,7 +142,7 @@ class Database {
140 $sql = 'INSERT INTO users_config ( user_id, name, value ) VALUES (?, ?, ?)'; 142 $sql = 'INSERT INTO users_config ( user_id, name, value ) VALUES (?, ?, ?)';
141 $params = array($id_user, 'language', LANG); 143 $params = array($id_user, 'language', LANG);
142 $query = $this->executeQuery($sql, $params); 144 $query = $this->executeQuery($sql, $params);
143 145
144 $sql = 'INSERT INTO users_config ( user_id, name, value ) VALUES (?, ?, ?)'; 146 $sql = 'INSERT INTO users_config ( user_id, name, value ) VALUES (?, ?, ?)';
145 $params = array($id_user, 'theme', DEFAULT_THEME); 147 $params = array($id_user, 'theme', DEFAULT_THEME);
146 $query = $this->executeQuery($sql, $params); 148 $query = $this->executeQuery($sql, $params);
@@ -153,7 +155,7 @@ class Database {
153 $query = $this->executeQuery($sql, array($id)); 155 $query = $this->executeQuery($sql, array($id));
154 $result = $query->fetchAll(); 156 $result = $query->fetchAll();
155 $user_config = array(); 157 $user_config = array();
156 158
157 foreach ($result as $key => $value) { 159 foreach ($result as $key => $value) {
158 $user_config[$value['name']] = $value['value']; 160 $user_config[$value['name']] = $value['value'];
159 } 161 }
@@ -201,10 +203,10 @@ class Database {
201 $params_update = array($password, $userId); 203 $params_update = array($password, $userId);
202 $query = $this->executeQuery($sql_update, $params_update); 204 $query = $this->executeQuery($sql_update, $params_update);
203 } 205 }
204 206
205 public function updateUserConfig($userId, $key, $value) { 207 public function updateUserConfig($userId, $key, $value) {
206 $config = $this->getConfigUser($userId); 208 $config = $this->getConfigUser($userId);
207 209
208 if (! isset($config[$key])) { 210 if (! isset($config[$key])) {
209 $sql = "INSERT INTO users_config (value, user_id, name) VALUES (?, ?, ?)"; 211 $sql = "INSERT INTO users_config (value, user_id, name) VALUES (?, ?, ?)";
210 } 212 }
@@ -229,6 +231,73 @@ class Database {
229 return FALSE; 231 return FALSE;
230 } 232 }
231 } 233 }
234
235 public function listUsers($username=null) {
236 $sql = 'SELECT count(*) FROM users'.( $username ? ' WHERE username=?' : '');
237 $query = $this->executeQuery($sql, ( $username ? array($username) : array()));
238 list($count) = $query->fetch();
239 return $count;
240 }
241
242 public function getUserPassword($userID) {
243 $sql = "SELECT * FROM users WHERE id=?";
244 $query = $this->executeQuery($sql, array($userID));
245 $password = $query->fetchAll();
246 return isset($password[0]['password']) ? $password[0]['password'] : null;
247 }
248
249 public function deleteUserConfig($userID) {
250 $sql_action = 'DELETE from users_config WHERE user_id=?';
251 $params_action = array($userID);
252 $query = $this->executeQuery($sql_action, $params_action);
253 return $query;
254 }
255
256 public function deleteTagsEntriesAndEntries($userID) {
257 $entries = $this->retrieveAll($userID);
258 foreach($entries as $entryid) {
259 $tags = $this->retrieveTagsByEntry($entryid);
260 foreach($tags as $tag) {
261 $this->removeTagForEntry($entryid,$tags);
262 }
263 $this->deleteById($entryid,$userID);
264 }
265 }
266
267 public function deleteUser($userID) {
268 $sql_action = 'DELETE from users WHERE id=?';
269 $params_action = array($userID);
270 $query = $this->executeQuery($sql_action, $params_action);
271 }
272
273 public function updateContentAndTitle($id, $title, $body, $user_id) {
274 $sql_action = 'UPDATE entries SET content = ?, title = ? WHERE id=? AND user_id=?';
275 $params_action = array($body, $title, $id, $user_id);
276 $query = $this->executeQuery($sql_action, $params_action);
277 return $query;
278 }
279
280 public function retrieveUnfetchedEntries($user_id, $limit) {
281
282 $sql_limit = "LIMIT 0,".$limit;
283 if (STORAGE == 'postgres') {
284 $sql_limit = "LIMIT ".$limit." OFFSET 0";
285 }
286
287 $sql = "SELECT * FROM entries WHERE (content = '' OR content IS NULL) AND title LIKE 'Untitled - Import%' AND user_id=? ORDER BY id " . $sql_limit;
288 $query = $this->executeQuery($sql, array($user_id));
289 $entries = $query->fetchAll();
290
291 return $entries;
292 }
293
294 public function retrieveUnfetchedEntriesCount($user_id) {
295 $sql = "SELECT count(*) FROM entries WHERE (content = '' OR content IS NULL) AND title LIKE 'Untitled - Import%' AND user_id=?";
296 $query = $this->executeQuery($sql, array($user_id));
297 list($count) = $query->fetch();
298
299 return $count;
300 }
232 301
233 public function retrieveAll($user_id) { 302 public function retrieveAll($user_id) {
234 $sql = "SELECT * FROM entries WHERE user_id=? ORDER BY id"; 303 $sql = "SELECT * FROM entries WHERE user_id=? ORDER BY id";
@@ -294,24 +363,24 @@ class Database {
294 return $entries; 363 return $entries;
295 } 364 }
296 365
297 public function getEntriesByViewCount($view, $user_id, $tag_id = 0) { 366 public function getEntriesByViewCount($view, $user_id, $tag_id = 0) {
298 switch ($view) { 367 switch ($view) {
299 case 'archive': 368 case 'archive':
300 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? "; 369 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? ";
301 $params = array($user_id, 1); 370 $params = array($user_id, 1);
302 break; 371 break;
303 case 'fav' : 372 case 'fav' :
304 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_fav=? "; 373 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_fav=? ";
305 $params = array($user_id, 1); 374 $params = array($user_id, 1);
306 break; 375 break;
307 case 'tag' : 376 case 'tag' :
308 $sql = "SELECT count(*) FROM entries 377 $sql = "SELECT count(*) FROM entries
309 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id 378 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id
310 WHERE entries.user_id=? AND tags_entries.tag_id = ? "; 379 WHERE entries.user_id=? AND tags_entries.tag_id = ? ";
311 $params = array($user_id, $tag_id); 380 $params = array($user_id, $tag_id);
312 break; 381 break;
313 default: 382 default:
314 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? "; 383 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? ";
315 $params = array($user_id, 0); 384 $params = array($user_id, 0);
316 break; 385 break;
317 } 386 }
@@ -319,7 +388,7 @@ class Database {
319 $query = $this->executeQuery($sql, $params); 388 $query = $this->executeQuery($sql, $params);
320 list($count) = $query->fetch(); 389 list($count) = $query->fetch();
321 390
322 return $count; 391 return $count;
323 } 392 }
324 393
325 public function updateContent($id, $content, $user_id) { 394 public function updateContent($id, $content, $user_id) {
@@ -329,11 +398,25 @@ class Database {
329 return $query; 398 return $query;
330 } 399 }
331 400
332 public function add($url, $title, $content, $user_id) { 401 /**
333 $sql_action = 'INSERT INTO entries ( url, title, content, user_id ) VALUES (?, ?, ?, ?)'; 402 *
334 $params_action = array($url, $title, $content, $user_id); 403 * @param string $url
335 $query = $this->executeQuery($sql_action, $params_action); 404 * @param string $title
336 return $query; 405 * @param string $content
406 * @param integer $user_id
407 * @return integer $id of inserted record
408 */
409 public function add($url, $title, $content, $user_id, $isFavorite=0, $isRead=0) {
410 $sql_action = 'INSERT INTO entries ( url, title, content, user_id, is_fav, is_read ) VALUES (?, ?, ?, ?, ?, ?)';
411 $params_action = array($url, $title, $content, $user_id, $isFavorite, $isRead);
412
413 if ( !$this->executeQuery($sql_action, $params_action) ) {
414 $id = null;
415 }
416 else {
417 $id = intval($this->getLastId( (STORAGE == 'postgres') ? 'entries_id_seq' : '') );
418 }
419 return $id;
337 } 420 }
338 421
339 public function deleteById($id, $user_id) { 422 public function deleteById($id, $user_id) {
@@ -365,12 +448,24 @@ class Database {
365 return $this->getHandle()->lastInsertId($column); 448 return $this->getHandle()->lastInsertId($column);
366 } 449 }
367 450
368 public function retrieveAllTags($user_id) { 451 public function search($term, $user_id, $limit = '') {
369 $sql = "SELECT DISTINCT tags.* FROM tags 452 $search = '%'.$term.'%';
453 $sql_action = "SELECT * FROM entries WHERE user_id=? AND (content LIKE ? OR title LIKE ? OR url LIKE ?) "; //searches in content, title and URL
454 $sql_action .= $this->getEntriesOrder().' ' . $limit;
455 $params_action = array($user_id, $search, $search, $search);
456 $query = $this->executeQuery($sql_action, $params_action);
457 return $query->fetchAll();
458 }
459
460 public function retrieveAllTags($user_id, $term = null) {
461 $sql = "SELECT DISTINCT tags.*, count(entries.id) AS entriescount FROM tags
370 LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id 462 LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id
371 LEFT JOIN entries ON tags_entries.entry_id=entries.id 463 LEFT JOIN entries ON tags_entries.entry_id=entries.id
372 WHERE entries.user_id=?"; 464 WHERE entries.user_id=?
373 $query = $this->executeQuery($sql, array($user_id)); 465 ". (($term) ? "AND lower(tags.value) LIKE ?" : '') ."
466 GROUP BY tags.id, tags.value
467 ORDER BY tags.value";
468 $query = $this->executeQuery($sql, (($term)? array($user_id, strtolower('%'.$term.'%')) : array($user_id) ));
374 $tags = $query->fetchAll(); 469 $tags = $query->fetchAll();
375 470
376 return $tags; 471 return $tags;
@@ -390,10 +485,10 @@ class Database {
390 } 485 }
391 486
392 public function retrieveEntriesByTag($tag_id, $user_id) { 487 public function retrieveEntriesByTag($tag_id, $user_id) {
393 $sql = 488 $sql =
394 "SELECT entries.* FROM entries 489 "SELECT entries.* FROM entries
395 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id 490 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id
396 WHERE tags_entries.tag_id = ? AND entries.user_id=?"; 491 WHERE tags_entries.tag_id = ? AND entries.user_id=? ORDER by entries.id DESC";
397 $query = $this->executeQuery($sql, array($tag_id, $user_id)); 492 $query = $this->executeQuery($sql, array($tag_id, $user_id));
398 $entries = $query->fetchAll(); 493 $entries = $query->fetchAll();
399 494
@@ -401,7 +496,7 @@ class Database {
401 } 496 }
402 497
403 public function retrieveTagsByEntry($entry_id) { 498 public function retrieveTagsByEntry($entry_id) {
404 $sql = 499 $sql =
405 "SELECT tags.* FROM tags 500 "SELECT tags.* FROM tags
406 LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id 501 LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id
407 WHERE tags_entries.entry_id = ?"; 502 WHERE tags_entries.entry_id = ?";
@@ -417,6 +512,25 @@ class Database {
417 $query = $this->executeQuery($sql_action, $params_action); 512 $query = $this->executeQuery($sql_action, $params_action);
418 return $query; 513 return $query;
419 } 514 }
515
516 public function cleanUnusedTag($tag_id) {
517 $sql_action = "SELECT tags.* FROM tags JOIN tags_entries ON tags_entries.tag_id=tags.id WHERE tags.id=?";
518 $query = $this->executeQuery($sql_action,array($tag_id));
519 $tagstokeep = $query->fetchAll();
520 $sql_action = "SELECT tags.* FROM tags LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id WHERE tags.id=?";
521 $query = $this->executeQuery($sql_action,array($tag_id));
522 $alltags = $query->fetchAll();
523
524 foreach ($alltags as $tag) {
525 if ($tag && !in_array($tag,$tagstokeep)) {
526 $sql_action = "DELETE FROM tags WHERE id=?";
527 $params_action = array($tag[0]);
528 $this->executeQuery($sql_action, $params_action);
529 return true;
530 }
531 }
532
533 }
420 534
421 public function retrieveTagByValue($value) { 535 public function retrieveTagByValue($value) {
422 $tag = NULL; 536 $tag = NULL;
diff --git a/inc/poche/Poche.class.php b/inc/poche/Poche.class.php
index 8a9de488..09a9f5ff 100755
--- a/inc/poche/Poche.class.php
+++ b/inc/poche/Poche.class.php
@@ -18,7 +18,7 @@ class Poche
18 public $tpl; 18 public $tpl;
19 public $messages; 19 public $messages;
20 public $pagination; 20 public $pagination;
21 21
22 private $currentTheme = ''; 22 private $currentTheme = '';
23 private $currentLanguage = ''; 23 private $currentLanguage = '';
24 private $notInstalledMessage = array(); 24 private $notInstalledMessage = array();
@@ -32,20 +32,21 @@ class Poche
32 'fr_FR.utf8' => 'Français', 32 'fr_FR.utf8' => 'Français',
33 'it_IT.utf8' => 'Italiano', 33 'it_IT.utf8' => 'Italiano',
34 'pl_PL.utf8' => 'Polski', 34 'pl_PL.utf8' => 'Polski',
35 'pt_BR.utf8' => 'Português (Brasil)',
35 'ru_RU.utf8' => 'Pусский', 36 'ru_RU.utf8' => 'Pусский',
36 'sl_SI.utf8' => 'Slovenščina', 37 'sl_SI.utf8' => 'Slovenščina',
37 'uk_UA.utf8' => 'Українськй', 38 'uk_UA.utf8' => 'Українськ',
38 ); 39 );
39 public function __construct() 40 public function __construct()
40 { 41 {
41 if ($this->configFileIsAvailable()) { 42 if ($this->configFileIsAvailable()) {
42 $this->init(); 43 $this->init();
43 } 44 }
44 45
45 if ($this->themeIsInstalled()) { 46 if ($this->themeIsInstalled()) {
46 $this->initTpl(); 47 $this->initTpl();
47 } 48 }
48 49
49 if ($this->systemIsInstalled()) { 50 if ($this->systemIsInstalled()) {
50 $this->store = new Database(); 51 $this->store = new Database();
51 $this->messages = new Messages(); 52 $this->messages = new Messages();
@@ -56,12 +57,10 @@ class Poche
56 $this->store->checkTags(); 57 $this->store->checkTags();
57 } 58 }
58 } 59 }
59 60
60 private function init() 61 private function init()
61 { 62 {
62 Tools::initPhp(); 63 Tools::initPhp();
63 Session::$sessionName = 'poche';
64 Session::init();
65 64
66 if (isset($_SESSION['poche_user']) && $_SESSION['poche_user'] != array()) { 65 if (isset($_SESSION['poche_user']) && $_SESSION['poche_user'] != array()) {
67 $this->user = $_SESSION['poche_user']; 66 $this->user = $_SESSION['poche_user'];
@@ -73,43 +72,43 @@ class Poche
73 72
74 # l10n 73 # l10n
75 $language = $this->user->getConfigValue('language'); 74 $language = $this->user->getConfigValue('language');
76 putenv('LC_ALL=' . $language); 75 @putenv('LC_ALL=' . $language);
77 setlocale(LC_ALL, $language); 76 setlocale(LC_ALL, $language);
78 bindtextdomain($language, LOCALE); 77 bindtextdomain($language, LOCALE);
79 textdomain($language); 78 textdomain($language);
80 79
81 # Pagination 80 # Pagination
82 $this->pagination = new Paginator($this->user->getConfigValue('pager'), 'p'); 81 $this->pagination = new Paginator($this->user->getConfigValue('pager'), 'p');
83 82
84 # Set up theme 83 # Set up theme
85 $themeDirectory = $this->user->getConfigValue('theme'); 84 $themeDirectory = $this->user->getConfigValue('theme');
86 85
87 if ($themeDirectory === false) { 86 if ($themeDirectory === false) {
88 $themeDirectory = DEFAULT_THEME; 87 $themeDirectory = DEFAULT_THEME;
89 } 88 }
90 89
91 $this->currentTheme = $themeDirectory; 90 $this->currentTheme = $themeDirectory;
92 91
93 # Set up language 92 # Set up language
94 $languageDirectory = $this->user->getConfigValue('language'); 93 $languageDirectory = $this->user->getConfigValue('language');
95 94
96 if ($languageDirectory === false) { 95 if ($languageDirectory === false) {
97 $languageDirectory = DEFAULT_THEME; 96 $languageDirectory = DEFAULT_THEME;
98 } 97 }
99 98
100 $this->currentLanguage = $languageDirectory; 99 $this->currentLanguage = $languageDirectory;
101 } 100 }
102 101
103 public function configFileIsAvailable() { 102 public function configFileIsAvailable() {
104 if (! self::$configFileAvailable) { 103 if (! self::$configFileAvailable) {
105 $this->notInstalledMessage[] = 'You have to rename inc/poche/config.inc.php.new to inc/poche/config.inc.php.'; 104 $this->notInstalledMessage[] = 'You have to copy (don\'t just rename!) inc/poche/config.inc.default.php to inc/poche/config.inc.php.';
106 105
107 return false; 106 return false;
108 } 107 }
109 108
110 return true; 109 return true;
111 } 110 }
112 111
113 public function themeIsInstalled() { 112 public function themeIsInstalled() {
114 $passTheme = TRUE; 113 $passTheme = TRUE;
115 # Twig is an absolute requirement for Poche to function. Abort immediately if the Composer installer hasn't been run yet 114 # Twig is an absolute requirement for Poche to function. Abort immediately if the Composer installer hasn't been run yet
@@ -124,27 +123,27 @@ class Poche
124 self::$canRenderTemplates = false; 123 self::$canRenderTemplates = false;
125 124
126 $passTheme = FALSE; 125 $passTheme = FALSE;
127 } 126 }
128 127
129 # Check if the selected theme and its requirements are present 128 # Check if the selected theme and its requirements are present
130 $theme = $this->getTheme(); 129 $theme = $this->getTheme();
131 130
132 if ($theme != '' && ! is_dir(THEME . '/' . $theme)) { 131 if ($theme != '' && ! is_dir(THEME . '/' . $theme)) {
133 $this->notInstalledMessage[] = 'The currently selected theme (' . $theme . ') does not seem to be properly installed (Missing directory: ' . THEME . '/' . $theme . ')'; 132 $this->notInstalledMessage[] = 'The currently selected theme (' . $theme . ') does not seem to be properly installed (Missing directory: ' . THEME . '/' . $theme . ')';
134 133
135 self::$canRenderTemplates = false; 134 self::$canRenderTemplates = false;
136 135
137 $passTheme = FALSE; 136 $passTheme = FALSE;
138 } 137 }
139 138
140 $themeInfo = $this->getThemeInfo($theme); 139 $themeInfo = $this->getThemeInfo($theme);
141 if (isset($themeInfo['requirements']) && is_array($themeInfo['requirements'])) { 140 if (isset($themeInfo['requirements']) && is_array($themeInfo['requirements'])) {
142 foreach ($themeInfo['requirements'] as $requiredTheme) { 141 foreach ($themeInfo['requirements'] as $requiredTheme) {
143 if (! is_dir(THEME . '/' . $requiredTheme)) { 142 if (! is_dir(THEME . '/' . $requiredTheme)) {
144 $this->notInstalledMessage[] = 'The required "' . $requiredTheme . '" theme is missing for the current theme (' . $theme . ')'; 143 $this->notInstalledMessage[] = 'The required "' . $requiredTheme . '" theme is missing for the current theme (' . $theme . ')';
145 144
146 self::$canRenderTemplates = false; 145 self::$canRenderTemplates = false;
147 146
148 $passTheme = FALSE; 147 $passTheme = FALSE;
149 } 148 }
150 } 149 }
@@ -154,21 +153,21 @@ class Poche
154 return FALSE; 153 return FALSE;
155 } 154 }
156 155
157 156
158 return true; 157 return true;
159 } 158 }
160 159
161 /** 160 /**
162 * all checks before installation. 161 * all checks before installation.
163 * @todo move HTML to template 162 * @todo move HTML to template
164 * @return boolean 163 * @return boolean
165 */ 164 */
166 public function systemIsInstalled() 165 public function systemIsInstalled()
167 { 166 {
168 $msg = TRUE; 167 $msg = TRUE;
169 168
170 $configSalt = defined('SALT') ? constant('SALT') : ''; 169 $configSalt = defined('SALT') ? constant('SALT') : '';
171 170
172 if (empty($configSalt)) { 171 if (empty($configSalt)) {
173 $this->notInstalledMessage[] = 'You have not yet filled in the SALT value in the config.inc.php file.'; 172 $this->notInstalledMessage[] = 'You have not yet filled in the SALT value in the config.inc.php file.';
174 $msg = FALSE; 173 $msg = FALSE;
@@ -194,7 +193,7 @@ class Poche
194 193
195 return true; 194 return true;
196 } 195 }
197 196
198 public function getNotInstalledMessage() { 197 public function getNotInstalledMessage() {
199 return $this->notInstalledMessage; 198 return $this->notInstalledMessage;
200 } 199 }
@@ -203,7 +202,7 @@ class Poche
203 { 202 {
204 $loaderChain = new Twig_Loader_Chain(); 203 $loaderChain = new Twig_Loader_Chain();
205 $theme = $this->getTheme(); 204 $theme = $this->getTheme();
206 205
207 # add the current theme as first to the loader chain so Twig will look there first for overridden template files 206 # add the current theme as first to the loader chain so Twig will look there first for overridden template files
208 try { 207 try {
209 $loaderChain->addLoader(new Twig_Loader_Filesystem(THEME . '/' . $theme)); 208 $loaderChain->addLoader(new Twig_Loader_Filesystem(THEME . '/' . $theme));
@@ -211,7 +210,7 @@ class Poche
211 # @todo isInstalled() should catch this, inject Twig later 210 # @todo isInstalled() should catch this, inject Twig later
212 die('The currently selected theme (' . $theme . ') does not seem to be properly installed (' . THEME . '/' . $theme .' is missing)'); 211 die('The currently selected theme (' . $theme . ') does not seem to be properly installed (' . THEME . '/' . $theme .' is missing)');
213 } 212 }
214 213
215 # add all required themes to the loader chain 214 # add all required themes to the loader chain
216 $themeInfo = $this->getThemeInfo($theme); 215 $themeInfo = $this->getThemeInfo($theme);
217 if (isset($themeInfo['requirements']) && is_array($themeInfo['requirements'])) { 216 if (isset($themeInfo['requirements']) && is_array($themeInfo['requirements'])) {
@@ -224,16 +223,16 @@ class Poche
224 } 223 }
225 } 224 }
226 } 225 }
227 226
228 if (DEBUG_POCHE) { 227 if (DEBUG_POCHE) {
229 $twigParams = array(); 228 $twigParams = array();
230 } else { 229 } else {
231 $twigParams = array('cache' => CACHE); 230 $twigParams = array('cache' => CACHE);
232 } 231 }
233 232
234 $this->tpl = new Twig_Environment($loaderChain, $twigParams); 233 $this->tpl = new Twig_Environment($loaderChain, $twigParams);
235 $this->tpl->addExtension(new Twig_Extensions_Extension_I18n()); 234 $this->tpl->addExtension(new Twig_Extensions_Extension_I18n());
236 235
237 # filter to display domain name of an url 236 # filter to display domain name of an url
238 $filter = new Twig_SimpleFilter('getDomain', 'Tools::getDomain'); 237 $filter = new Twig_SimpleFilter('getDomain', 'Tools::getDomain');
239 $this->tpl->addFilter($filter); 238 $this->tpl->addFilter($filter);
@@ -243,6 +242,58 @@ class Poche
243 $this->tpl->addFilter($filter); 242 $this->tpl->addFilter($filter);
244 } 243 }
245 244
245 public function createNewUser() {
246 if (isset($_GET['newuser'])){
247 if ($_POST['newusername'] != "" && $_POST['password4newuser'] != ""){
248 $newusername = filter_var($_POST['newusername'], FILTER_SANITIZE_STRING);
249 if (!$this->store->userExists($newusername)){
250 if ($this->store->install($newusername, Tools::encodeString($_POST['password4newuser'] . $newusername))) {
251 Tools::logm('The new user '.$newusername.' has been installed');
252 $this->messages->add('s', sprintf(_('The new user %s has been installed. Do you want to <a href="?logout">logout ?</a>'),$newusername));
253 Tools::redirect();
254 }
255 else {
256 Tools::logm('error during adding new user');
257 Tools::redirect();
258 }
259 }
260 else {
261 $this->messages->add('e', sprintf(_('Error : An user with the name %s already exists !'),$newusername));
262 Tools::logm('An user with the name '.$newusername.' already exists !');
263 Tools::redirect();
264 }
265 }
266 }
267 }
268
269 public function deleteUser(){
270 if (isset($_GET['deluser'])){
271 if ($this->store->listUsers() > 1) {
272 if (Tools::encodeString($_POST['password4deletinguser'].$this->user->getUsername()) == $this->store->getUserPassword($this->user->getId())) {
273 $username = $this->user->getUsername();
274 $this->store->deleteUserConfig($this->user->getId());
275 Tools::logm('The configuration for user '. $username .' has been deleted !');
276 $this->store->deleteTagsEntriesAndEntries($this->user->getId());
277 Tools::logm('The entries for user '. $username .' has been deleted !');
278 $this->store->deleteUser($this->user->getId());
279 Tools::logm('User '. $username .' has been completely deleted !');
280 Session::logout();
281 Tools::logm('logout');
282 Tools::redirect();
283 $this->messages->add('s', sprintf(_('User %s has been successfully deleted !'),$newusername));
284 }
285 else {
286 Tools::logm('Bad password !');
287 $this->messages->add('e', _('Error : The password is wrong !'));
288 }
289 }
290 else {
291 Tools::logm('Only user !');
292 $this->messages->add('e', _('Error : You are the only user, you cannot delete your account !'));
293 }
294 }
295 }
296
246 private function install() 297 private function install()
247 { 298 {
248 Tools::logm('poche still not installed'); 299 Tools::logm('poche still not installed');
@@ -252,7 +303,7 @@ class Poche
252 'poche_url' => Tools::getPocheUrl() 303 'poche_url' => Tools::getPocheUrl()
253 )); 304 ));
254 if (isset($_GET['install'])) { 305 if (isset($_GET['install'])) {
255 if (($_POST['password'] == $_POST['password_repeat']) 306 if (($_POST['password'] == $_POST['password_repeat'])
256 && $_POST['password'] != "" && $_POST['login'] != "") { 307 && $_POST['password'] != "" && $_POST['login'] != "") {
257 # let's rock, install poche baby ! 308 # let's rock, install poche baby !
258 if ($this->store->install($_POST['login'], Tools::encodeString($_POST['password'] . $_POST['login']))) 309 if ($this->store->install($_POST['login'], Tools::encodeString($_POST['password'] . $_POST['login'])))
@@ -269,7 +320,7 @@ class Poche
269 } 320 }
270 exit(); 321 exit();
271 } 322 }
272 323
273 public function getTheme() { 324 public function getTheme() {
274 return $this->currentTheme; 325 return $this->currentTheme;
275 } 326 }
@@ -294,7 +345,7 @@ class Poche
294 if (is_file($themeIniFile) && is_readable($themeIniFile)) { 345 if (is_file($themeIniFile) && is_readable($themeIniFile)) {
295 $themeInfo = parse_ini_file($themeIniFile); 346 $themeInfo = parse_ini_file($themeIniFile);
296 } 347 }
297 348
298 if ($themeInfo === false) { 349 if ($themeInfo === false) {
299 $themeInfo = array(); 350 $themeInfo = array();
300 } 351 }
@@ -305,7 +356,7 @@ class Poche
305 356
306 return $themeInfo; 357 return $themeInfo;
307 } 358 }
308 359
309 public function getInstalledThemes() { 360 public function getInstalledThemes() {
310 $handle = opendir(THEME); 361 $handle = opendir(THEME);
311 $themes = array(); 362 $themes = array();
@@ -332,28 +383,28 @@ class Poche
332 public function getInstalledLanguages() { 383 public function getInstalledLanguages() {
333 $handle = opendir(LOCALE); 384 $handle = opendir(LOCALE);
334 $languages = array(); 385 $languages = array();
335 386
336 while (($language = readdir($handle)) !== false) { 387 while (($language = readdir($handle)) !== false) {
337 # Languages are stored in a directory, so all directory names are languages 388 # Languages are stored in a directory, so all directory names are languages
338 # @todo move language installation data to database 389 # @todo move language installation data to database
339 if (! is_dir(LOCALE . '/' . $language) || in_array($language, array('..', '.'))) { 390 if (! is_dir(LOCALE . '/' . $language) || in_array($language, array('..', '.', 'tools'))) {
340 continue; 391 continue;
341 } 392 }
342 393
343 $current = false; 394 $current = false;
344 395
345 if ($language === $this->getLanguage()) { 396 if ($language === $this->getLanguage()) {
346 $current = true; 397 $current = true;
347 } 398 }
348 399
349 $languages[] = array('name' => $this->language_names[$language], 'value' => $language, 'current' => $current); 400 $languages[] = array('name' => (isset($this->language_names[$language]) ? $this->language_names[$language] : $language), 'value' => $language, 'current' => $current);
350 } 401 }
351 402
352 return $languages; 403 return $languages;
353 } 404 }
354 405
355 public function getDefaultConfig() 406 public function getDefaultConfig()
356 { 407 {
357 return array( 408 return array(
358 'pager' => PAGINATION, 409 'pager' => PAGINATION,
359 'language' => LANG, 410 'language' => LANG,
@@ -361,60 +412,6 @@ class Poche
361 ); 412 );
362 } 413 }
363 414
364 protected function getPageContent(Url $url)
365 {
366 // Saving and clearing context
367 $REAL = array();
368 foreach( $GLOBALS as $key => $value ) {
369 if( $key != "GLOBALS" && $key != "_SESSION" ) {
370 $GLOBALS[$key] = array();
371 $REAL[$key] = $value;
372 }
373 }
374 // Saving and clearing session
375 $REAL_SESSION = array();
376 foreach( $_SESSION as $key => $value ) {
377 $REAL_SESSION[$key] = $value;
378 unset($_SESSION[$key]);
379 }
380
381 // Running code in different context
382 $scope = function() {
383 extract( func_get_arg(1) );
384 $_GET = $_REQUEST = array(
385 "url" => $url->getUrl(),
386 "max" => 5,
387 "links" => "preserve",
388 "exc" => "",
389 "format" => "json",
390 "submit" => "Create Feed"
391 );
392 ob_start();
393 require func_get_arg(0);
394 $json = ob_get_flush();
395 return $json;
396 };
397 $json = $scope( "inc/3rdparty/makefulltextfeed.php", array("url" => $url) );
398
399 // Clearing and restoring context
400 foreach( $GLOBALS as $key => $value ) {
401 if( $key != "GLOBALS" && $key != "_SESSION" ) {
402 unset($GLOBALS[$key]);
403 }
404 }
405 foreach( $REAL as $key => $value ) {
406 $GLOBALS[$key] = $value;
407 }
408 // Clearing and restoring session
409 foreach( $_SESSION as $key => $value ) {
410 unset($_SESSION[$key]);
411 }
412 foreach( $REAL_SESSION as $key => $value ) {
413 $_SESSION[$key] = $value;
414 }
415 return json_decode($json, true);
416 }
417
418 /** 415 /**
419 * Call action (mark as fav, archive, delete, etc.) 416 * Call action (mark as fav, archive, delete, etc.)
420 */ 417 */
@@ -423,28 +420,22 @@ class Poche
423 switch ($action) 420 switch ($action)
424 { 421 {
425 case 'add': 422 case 'add':
426 $content = $this->getPageContent($url); 423 $content = Tools::getPageContent($url);
427 $title = ($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'); 424 $title = ($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled');
428 $body = $content['rss']['channel']['item']['description']; 425 $body = $content['rss']['channel']['item']['description'];
429 426
430 // clean content from prevent xss attack 427 // clean content from prevent xss attack
431 $config = HTMLPurifier_Config::createDefault(); 428 $purifier = $this->getPurifier();
432 $purifier = new HTMLPurifier($config);
433 $title = $purifier->purify($title); 429 $title = $purifier->purify($title);
434 $body = $purifier->purify($body); 430 $body = $purifier->purify($body);
435 431
436 //search for possible duplicate if not in import mode 432 //search for possible duplicate
437 if (!$import) { 433 $duplicate = NULL;
438 $duplicate = $this->store->retrieveOneByURL($url->getUrl(), $this->user->getId()); 434 $duplicate = $this->store->retrieveOneByURL($url->getUrl(), $this->user->getId());
439 }
440 435
441 if ($this->store->add($url->getUrl(), $title, $body, $this->user->getId())) { 436 $last_id = $this->store->add($url->getUrl(), $title, $body, $this->user->getId());
437 if ( $last_id ) {
442 Tools::logm('add link ' . $url->getUrl()); 438 Tools::logm('add link ' . $url->getUrl());
443 $sequence = '';
444 if (STORAGE == 'postgres') {
445 $sequence = 'entries_id_seq';
446 }
447 $last_id = $this->store->getLastId($sequence);
448 if (DOWNLOAD_PICTURES) { 439 if (DOWNLOAD_PICTURES) {
449 $content = filtre_picture($body, $url->getUrl(), $last_id); 440 $content = filtre_picture($body, $url->getUrl(), $last_id);
450 Tools::logm('updating content article'); 441 Tools::logm('updating content article');
@@ -464,23 +455,17 @@ class Poche
464 } 455 }
465 } 456 }
466 457
467 if (!$import) { 458 $this->messages->add('s', _('the link has been added successfully'));
468 $this->messages->add('s', _('the link has been added successfully'));
469 }
470 } 459 }
471 else { 460 else {
472 if (!$import) { 461 $this->messages->add('e', _('error during insertion : the link wasn\'t added'));
473 $this->messages->add('e', _('error during insertion : the link wasn\'t added')); 462 Tools::logm('error during insertion : the link wasn\'t added ' . $url->getUrl());
474 Tools::logm('error during insertion : the link wasn\'t added ' . $url->getUrl());
475 }
476 } 463 }
477 464
478 if (!$import) { 465 if ($autoclose == TRUE) {
479 if ($autoclose == TRUE) { 466 Tools::redirect('?view=home');
480 Tools::redirect('?view=home'); 467 } else {
481 } else { 468 Tools::redirect('?view=home&closewin=true');
482 Tools::redirect('?view=home&closewin=true');
483 }
484 } 469 }
485 break; 470 break;
486 case 'delete': 471 case 'delete':
@@ -501,62 +486,81 @@ class Poche
501 case 'toggle_fav' : 486 case 'toggle_fav' :
502 $this->store->favoriteById($id, $this->user->getId()); 487 $this->store->favoriteById($id, $this->user->getId());
503 Tools::logm('mark as favorite link #' . $id); 488 Tools::logm('mark as favorite link #' . $id);
504 if (!$import) { 489 if ( Tools::isAjaxRequest() ) {
505 Tools::redirect(); 490 echo 1;
491 exit;
492 }
493 else {
494 Tools::redirect();
506 } 495 }
507 break; 496 break;
508 case 'toggle_archive' : 497 case 'toggle_archive' :
509 $this->store->archiveById($id, $this->user->getId()); 498 $this->store->archiveById($id, $this->user->getId());
510 Tools::logm('archive link #' . $id); 499 Tools::logm('archive link #' . $id);
511 if (!$import) { 500 if ( Tools::isAjaxRequest() ) {
512 Tools::redirect(); 501 echo 1;
502 exit;
503 }
504 else {
505 Tools::redirect();
513 } 506 }
514 break; 507 break;
515 case 'archive_all' : 508 case 'archive_all' :
516 $this->store->archiveAll($this->user->getId()); 509 $this->store->archiveAll($this->user->getId());
517 Tools::logm('archive all links'); 510 Tools::logm('archive all links');
518 if (!$import) { 511 Tools::redirect();
519 Tools::redirect();
520 }
521 break; 512 break;
522 case 'add_tag' : 513 case 'add_tag' :
523 if($import){ 514 if (isset($_GET['search'])) {
524 $entry_id = $id; 515 //when we want to apply a tag to a search
525 $tags = explode(',', $tags); 516 $tags = array($_GET['search']);
526 } 517 $allentry_ids = $this->store->search($tags[0], $this->user->getId());
527 else{ 518 $entry_ids = array();
519 foreach ($allentry_ids as $eachentry) {
520 $entry_ids[] = $eachentry[0];
521 }
522 } else { //add a tag to a single article
528 $tags = explode(',', $_POST['value']); 523 $tags = explode(',', $_POST['value']);
529 $entry_id = $_POST['entry_id']; 524 $entry_ids = array($_POST['entry_id']);
530 } 525 }
531 $entry = $this->store->retrieveOneById($entry_id, $this->user->getId()); 526 foreach($entry_ids as $entry_id) {
532 if (!$entry) { 527 $entry = $this->store->retrieveOneById($entry_id, $this->user->getId());
533 $this->messages->add('e', _('Article not found!')); 528 if (!$entry) {
534 Tools::logm('error : article not found'); 529 $this->messages->add('e', _('Article not found!'));
535 Tools::redirect(); 530 Tools::logm('error : article not found');
536 } 531 Tools::redirect();
537 foreach($tags as $key => $tag_value) {
538 $value = trim($tag_value);
539 $tag = $this->store->retrieveTagByValue($value);
540
541 if (is_null($tag)) {
542 # we create the tag
543 $tag = $this->store->createTag($value);
544 $sequence = '';
545 if (STORAGE == 'postgres') {
546 $sequence = 'tags_id_seq';
547 }
548 $tag_id = $this->store->getLastId($sequence);
549 } 532 }
550 else { 533 //get all already set tags to preven duplicates
551 $tag_id = $tag['id']; 534 $already_set_tags = array();
535 $entry_tags = $this->store->retrieveTagsByEntry($entry_id);
536 foreach ($entry_tags as $tag) {
537 $already_set_tags[] = $tag['value'];
538 }
539 foreach($tags as $key => $tag_value) {
540 $value = trim($tag_value);
541 if ($value && !in_array($value, $already_set_tags)) {
542 $tag = $this->store->retrieveTagByValue($value);
543 if (is_null($tag)) {
544 # we create the tag
545 $tag = $this->store->createTag($value);
546 $sequence = '';
547 if (STORAGE == 'postgres') {
548 $sequence = 'tags_id_seq';
549 }
550 $tag_id = $this->store->getLastId($sequence);
551 }
552 else {
553 $tag_id = $tag['id'];
554 }
555
556 # we assign the tag to the article
557 $this->store->setTagToEntry($tag_id, $entry_id);
558 }
552 } 559 }
553
554 # we assign the tag to the article
555 $this->store->setTagToEntry($tag_id, $entry_id);
556 }
557 if(!$import) {
558 Tools::redirect();
559 } 560 }
561 $this->messages->add('s', _('The tag has been applied successfully'));
562 Tools::logm('The tag has been applied successfully');
563 Tools::redirect();
560 break; 564 break;
561 case 'remove_tag' : 565 case 'remove_tag' :
562 $tag_id = $_GET['tag_id']; 566 $tag_id = $_GET['tag_id'];
@@ -567,6 +571,11 @@ class Poche
567 Tools::redirect(); 571 Tools::redirect();
568 } 572 }
569 $this->store->removeTagForEntry($id, $tag_id); 573 $this->store->removeTagForEntry($id, $tag_id);
574 Tools::logm('tag entry deleted');
575 if ($this->store->cleanUnusedTag($tag_id)) {
576 Tools::logm('tag deleted');
577 }
578 $this->messages->add('s', _('The tag has been successfully deleted'));
570 Tools::redirect(); 579 Tools::redirect();
571 break; 580 break;
572 default: 581 default:
@@ -581,24 +590,32 @@ class Poche
581 switch ($view) 590 switch ($view)
582 { 591 {
583 case 'config': 592 case 'config':
584 $dev = trim($this->getPocheVersion('dev')); 593 $dev_infos = $this->getPocheVersion('dev');
585 $prod = trim($this->getPocheVersion('prod')); 594 $dev = trim($dev_infos[0]);
595 $check_time_dev = date('d-M-Y H:i', $dev_infos[1]);
596 $prod_infos = $this->getPocheVersion('prod');
597 $prod = trim($prod_infos[0]);
598 $check_time_prod = date('d-M-Y H:i', $prod_infos[1]);
586 $compare_dev = version_compare(POCHE, $dev); 599 $compare_dev = version_compare(POCHE, $dev);
587 $compare_prod = version_compare(POCHE, $prod); 600 $compare_prod = version_compare(POCHE, $prod);
588 $themes = $this->getInstalledThemes(); 601 $themes = $this->getInstalledThemes();
589 $languages = $this->getInstalledLanguages(); 602 $languages = $this->getInstalledLanguages();
590 $token = $this->user->getConfigValue('token'); 603 $token = $this->user->getConfigValue('token');
591 $http_auth = (isset($_SERVER['PHP_AUTH_USER']) || isset($_SERVER['REMOTE_USER'])) ? true : false; 604 $http_auth = (isset($_SERVER['PHP_AUTH_USER']) || isset($_SERVER['REMOTE_USER'])) ? true : false;
605 $only_user = ($this->store->listUsers() > 1) ? false : true;
592 $tpl_vars = array( 606 $tpl_vars = array(
593 'themes' => $themes, 607 'themes' => $themes,
594 'languages' => $languages, 608 'languages' => $languages,
595 'dev' => $dev, 609 'dev' => $dev,
596 'prod' => $prod, 610 'prod' => $prod,
611 'check_time_dev' => $check_time_dev,
612 'check_time_prod' => $check_time_prod,
597 'compare_dev' => $compare_dev, 613 'compare_dev' => $compare_dev,
598 'compare_prod' => $compare_prod, 614 'compare_prod' => $compare_prod,
599 'token' => $token, 615 'token' => $token,
600 'user_id' => $this->user->getId(), 616 'user_id' => $this->user->getId(),
601 'http_auth' => $http_auth, 617 'http_auth' => $http_auth,
618 'only_user' => $only_user
602 ); 619 );
603 Tools::logm('config view'); 620 Tools::logm('config view');
604 break; 621 break;
@@ -619,13 +636,36 @@ class Poche
619 break; 636 break;
620 case 'tags': 637 case 'tags':
621 $token = $this->user->getConfigValue('token'); 638 $token = $this->user->getConfigValue('token');
622 $tags = $this->store->retrieveAllTags($this->user->getId()); 639 //if term is set - search tags for this term
640 $term = Tools::checkVar('term');
641 $tags = $this->store->retrieveAllTags($this->user->getId(), $term);
642 if (Tools::isAjaxRequest()) {
643 $result = array();
644 foreach ($tags as $tag) {
645 $result[] = $tag['value'];
646 }
647 echo json_encode($result);
648 exit;
649 }
623 $tpl_vars = array( 650 $tpl_vars = array(
624 'token' => $token, 651 'token' => $token,
625 'user_id' => $this->user->getId(), 652 'user_id' => $this->user->getId(),
626 'tags' => $tags, 653 'tags' => $tags,
627 ); 654 );
628 break; 655 break;
656 case 'search':
657 if (isset($_GET['search'])) {
658 $search = filter_var($_GET['search'], FILTER_SANITIZE_STRING);
659 $tpl_vars['entries'] = $this->store->search($search, $this->user->getId());
660 $count = count($tpl_vars['entries']);
661 $this->pagination->set_total($count);
662 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
663 $this->pagination->page_links('?view=' . $view . '?search=' . $search . '&sort=' . $_SESSION['sort'] . '&' ));
664 $tpl_vars['page_links'] = $page_links;
665 $tpl_vars['nb_results'] = $count;
666 $tpl_vars['search_term'] = $search;
667 }
668 break;
629 case 'view': 669 case 'view':
630 $entry = $this->store->retrieveOneById($id, $this->user->getId()); 670 $entry = $this->store->retrieveOneById($id, $this->user->getId());
631 if ($entry != NULL) { 671 if ($entry != NULL) {
@@ -660,8 +700,9 @@ class Poche
660 'entries' => '', 700 'entries' => '',
661 'page_links' => '', 701 'page_links' => '',
662 'nb_results' => '', 702 'nb_results' => '',
703 'listmode' => (isset($_COOKIE['listmode']) ? true : false),
663 ); 704 );
664 705
665 //if id is given - we retrive entries by tag: id is tag id 706 //if id is given - we retrive entries by tag: id is tag id
666 if ($id) { 707 if ($id) {
667 $tpl_vars['tag'] = $this->store->retrieveTag($id, $this->user->getId()); 708 $tpl_vars['tag'] = $this->store->retrieveTag($id, $this->user->getId());
@@ -686,8 +727,8 @@ class Poche
686 } 727 }
687 728
688 /** 729 /**
689 * update the password of the current user. 730 * update the password of the current user.
690 * if MODE_DEMO is TRUE, the password can't be updated. 731 * if MODE_DEMO is TRUE, the password can't be updated.
691 * @todo add the return value 732 * @todo add the return value
692 * @todo set the new password in function header like this updatePassword($newPassword) 733 * @todo set the new password in function header like this updatePassword($newPassword)
693 * @return boolean 734 * @return boolean
@@ -715,42 +756,44 @@ class Poche
715 } 756 }
716 } 757 }
717 } 758 }
718 759
719 public function updateTheme() 760 public function updateTheme()
720 { 761 {
721 # no data 762 # no data
722 if (empty($_POST['theme'])) { 763 if (empty($_POST['theme'])) {
723 } 764 }
724 765
725 # we are not going to change it to the current theme... 766 # we are not going to change it to the current theme...
726 if ($_POST['theme'] == $this->getTheme()) { 767 if ($_POST['theme'] == $this->getTheme()) {
727 $this->messages->add('w', _('still using the "' . $this->getTheme() . '" theme!')); 768 $this->messages->add('w', _('still using the "' . $this->getTheme() . '" theme!'));
728 Tools::redirect('?view=config'); 769 Tools::redirect('?view=config');
729 } 770 }
730 771
731 $themes = $this->getInstalledThemes(); 772 $themes = $this->getInstalledThemes();
732 $actualTheme = false; 773 $actualTheme = false;
733 774
734 foreach (array_keys($themes) as $theme) { 775 foreach (array_keys($themes) as $theme) {
735 if ($theme == $_POST['theme']) { 776 if ($theme == $_POST['theme']) {
736 $actualTheme = true; 777 $actualTheme = true;
737 break; 778 break;
738 } 779 }
739 } 780 }
740 781
741 if (! $actualTheme) { 782 if (! $actualTheme) {
742 $this->messages->add('e', _('that theme does not seem to be installed')); 783 $this->messages->add('e', _('that theme does not seem to be installed'));
743 Tools::redirect('?view=config'); 784 Tools::redirect('?view=config');
744 } 785 }
745 786
746 $this->store->updateUserConfig($this->user->getId(), 'theme', $_POST['theme']); 787 $this->store->updateUserConfig($this->user->getId(), 'theme', $_POST['theme']);
747 $this->messages->add('s', _('you have changed your theme preferences')); 788 $this->messages->add('s', _('you have changed your theme preferences'));
748 789
749 $currentConfig = $_SESSION['poche_user']->config; 790 $currentConfig = $_SESSION['poche_user']->config;
750 $currentConfig['theme'] = $_POST['theme']; 791 $currentConfig['theme'] = $_POST['theme'];
751 792
752 $_SESSION['poche_user']->setConfig($currentConfig); 793 $_SESSION['poche_user']->setConfig($currentConfig);
753 794
795 $this->emptyCache();
796
754 Tools::redirect('?view=config'); 797 Tools::redirect('?view=config');
755 } 798 }
756 799
@@ -759,39 +802,40 @@ class Poche
759 # no data 802 # no data
760 if (empty($_POST['language'])) { 803 if (empty($_POST['language'])) {
761 } 804 }
762 805
763 # we are not going to change it to the current language... 806 # we are not going to change it to the current language...
764 if ($_POST['language'] == $this->getLanguage()) { 807 if ($_POST['language'] == $this->getLanguage()) {
765 $this->messages->add('w', _('still using the "' . $this->getLanguage() . '" language!')); 808 $this->messages->add('w', _('still using the "' . $this->getLanguage() . '" language!'));
766 Tools::redirect('?view=config'); 809 Tools::redirect('?view=config');
767 } 810 }
768 811
769 $languages = $this->getInstalledLanguages(); 812 $languages = $this->getInstalledLanguages();
770 $actualLanguage = false; 813 $actualLanguage = false;
771 814
772 foreach ($languages as $language) { 815 foreach ($languages as $language) {
773 if ($language['value'] == $_POST['language']) { 816 if ($language['value'] == $_POST['language']) {
774 $actualLanguage = true; 817 $actualLanguage = true;
775 break; 818 break;
776 } 819 }
777 } 820 }
778 821
779 if (! $actualLanguage) { 822 if (! $actualLanguage) {
780 $this->messages->add('e', _('that language does not seem to be installed')); 823 $this->messages->add('e', _('that language does not seem to be installed'));
781 Tools::redirect('?view=config'); 824 Tools::redirect('?view=config');
782 } 825 }
783 826
784 $this->store->updateUserConfig($this->user->getId(), 'language', $_POST['language']); 827 $this->store->updateUserConfig($this->user->getId(), 'language', $_POST['language']);
785 $this->messages->add('s', _('you have changed your language preferences')); 828 $this->messages->add('s', _('you have changed your language preferences'));
786 829
787 $currentConfig = $_SESSION['poche_user']->config; 830 $currentConfig = $_SESSION['poche_user']->config;
788 $currentConfig['language'] = $_POST['language']; 831 $currentConfig['language'] = $_POST['language'];
789 832
790 $_SESSION['poche_user']->setConfig($currentConfig); 833 $_SESSION['poche_user']->setConfig($currentConfig);
791 834
835 $this->emptyCache();
836
792 Tools::redirect('?view=config'); 837 Tools::redirect('?view=config');
793 } 838 }
794
795 /** 839 /**
796 * get credentials from differents sources 840 * get credentials from differents sources
797 * it redirects the user to the $referer link 841 * it redirects the user to the $referer link
@@ -846,7 +890,7 @@ class Poche
846 /** 890 /**
847 * log out the poche user. It cleans the session. 891 * log out the poche user. It cleans the session.
848 * @todo add the return value 892 * @todo add the return value
849 * @return boolean 893 * @return boolean
850 */ 894 */
851 public function logout() 895 public function logout()
852 { 896 {
@@ -857,238 +901,137 @@ class Poche
857 } 901 }
858 902
859 /** 903 /**
860 * import from Instapaper. poche needs a ./instapaper-export.html file 904 * import datas into your poche
861 * @todo add the return value
862 * @param string $targetFile the file used for importing
863 * @return boolean 905 * @return boolean
864 */ 906 */
865 private function importFromInstapaper($targetFile) 907 public function import() {
866 { 908
867 # TODO gestion des articles favs 909 if ( isset($_FILES['file']) ) {
868 $html = new simple_html_dom(); 910 Tools::logm('Import stated: parsing file');
869 $html->load_file($targetFile); 911
870 Tools::logm('starting import from instapaper'); 912 // assume, that file is in json format
871 913 $str_data = file_get_contents($_FILES['file']['tmp_name']);
872 $read = 0; 914 $data = json_decode($str_data, true);
873 $errors = array(); 915
874 foreach($html->find('ol') as $ul) 916 if ( $data === null ) {
875 { 917 //not json - assume html
876 foreach($ul->find('li') as $li) 918 $html = new simple_html_dom();
877 { 919 $html->load_file($_FILES['file']['tmp_name']);
878 $a = $li->find('a'); 920 $data = array();
879 $url = new Url(base64_encode($a[0]->href)); 921 $read = 0;
880 $this->action('add', $url, 0, TRUE); 922 foreach (array('ol','ul') as $list) {
881 if ($read == '1') { 923 foreach ($html->find($list) as $ul) {
882 $sequence = ''; 924 foreach ($ul->find('li') as $li) {
883 if (STORAGE == 'postgres') { 925 $tmpEntry = array();
884 $sequence = 'entries_id_seq'; 926 $a = $li->find('a');
885 } 927 $tmpEntry['url'] = $a[0]->href;
886 $last_id = $this->store->getLastId($sequence); 928 $tmpEntry['tags'] = $a[0]->tags;
887 $this->action('toggle_archive', $url, $last_id, TRUE); 929 $tmpEntry['is_read'] = $read;
888 } 930 if ($tmpEntry['url']) {
931 $data[] = $tmpEntry;
932 }
933 }
934 # the second <ol/ul> is for read links
935 $read = ((sizeof($data) && $read)?0:1);
889 } 936 }
890 937 }
891 # the second <ol> is for read links
892 $read = 1;
893 } 938 }
894 $this->messages->add('s', _('import from instapaper completed'));
895 Tools::logm('import from instapaper completed');
896 Tools::redirect();
897 }
898 939
899 /** 940 //for readability structure
900 * import from Pocket. poche needs a ./ril_export.html file 941 foreach ($data as $record) {
901 * @todo add the return value 942 if (is_array($record)) {
902 * @param string $targetFile the file used for importing 943 $data[] = $record;
903 * @return boolean 944 foreach ($record as $record2) {
904 */ 945 if (is_array($record2)) {
905 private function importFromPocket($targetFile) 946 $data[] = $record2;
906 { 947 }
907 # TODO gestion des articles favs 948 }
908 $html = new simple_html_dom(); 949 }
909 $html->load_file($targetFile); 950 }
910 Tools::logm('starting import from pocket'); 951
911 952 $urlsInserted = array(); //urls of articles inserted
912 $read = 0; 953 foreach ($data as $record) {
913 $errors = array(); 954 $url = trim( isset($record['article__url']) ? $record['article__url'] : (isset($record['url']) ? $record['url'] : '') );
914 foreach($html->find('ul') as $ul) 955 if ( $url and !in_array($url, $urlsInserted) ) {
915 { 956 $title = (isset($record['title']) ? $record['title'] : _('Untitled - Import - ').'</a> <a href="./?import">'._('click to finish import').'</a><a>');
916 foreach($ul->find('li') as $li) 957 $body = (isset($record['content']) ? $record['content'] : '');
917 { 958 $isRead = (isset($record['is_read']) ? intval($record['is_read']) : (isset($record['archive'])?intval($record['archive']):0));
918 $a = $li->find('a'); 959 $isFavorite = (isset($record['is_fav']) ? intval($record['is_fav']) : (isset($record['favorite'])?intval($record['favorite']):0) );
919 $url = new Url(base64_encode($a[0]->href)); 960 //insert new record
920 $this->action('add', $url, 0, TRUE); 961 $id = $this->store->add($url, $title, $body, $this->user->getId(), $isFavorite, $isRead);
921 $sequence = ''; 962 if ( $id ) {
922 if (STORAGE == 'postgres') { 963 $urlsInserted[] = $url; //add
923 $sequence = 'entries_id_seq'; 964
924 } 965 if ( isset($record['tags']) && trim($record['tags']) ) {
925 $last_id = $this->store->getLastId($sequence); 966 //@TODO: set tags
926 if ($read == '1') { 967
927 $this->action('toggle_archive', $url, $last_id, TRUE); 968 }
928 }
929 $tags = $a[0]->tags;
930 if(!empty($tags)) {
931 $this->action('add_tag',$url,$last_id,true,false,$tags);
932 }
933 } 969 }
934 970 }
935 # the second <ul> is for read links
936 $read = 1;
937 } 971 }
938 $this->messages->add('s', _('import from pocket completed'));
939 Tools::logm('import from pocket completed');
940 Tools::redirect();
941 }
942
943 /**
944 * import from Readability. poche needs a ./readability file
945 * @todo add the return value
946 * @param string $targetFile the file used for importing
947 * @return boolean
948 */
949 private function importFromReadability($targetFile)
950 {
951 # TODO gestion des articles lus / favs
952 $str_data = file_get_contents($targetFile);
953 $data = json_decode($str_data,true);
954 Tools::logm('starting import from Readability');
955 $count = 0;
956 foreach ($data as $key => $value) {
957 $url = NULL;
958 $favorite = FALSE;
959 $archive = FALSE;
960 foreach ($value as $item) {
961 foreach ($item as $attr => $value) {
962 if ($attr == 'article__url') {
963 $url = new Url(base64_encode($value));
964 }
965 $sequence = '';
966 if (STORAGE == 'postgres') {
967 $sequence = 'entries_id_seq';
968 }
969 if ($value == 'true') {
970 if ($attr == 'favorite') {
971 $favorite = TRUE;
972 }
973 if ($attr == 'archive') {
974 $archive = TRUE;
975 }
976 }
977 }
978 972
979 # we can add the url 973 $i = sizeof($urlsInserted);
980 if (!is_null($url) && $url->isCorrect()) { 974 if ( $i > 0 ) {
981 $this->action('add', $url, 0, TRUE); 975 $this->messages->add('s', _('Articles inserted: ').$i._('. Please note, that some may be marked as "read".'));
982 $count++;
983 if ($favorite) {
984 $last_id = $this->store->getLastId($sequence);
985 $this->action('toggle_fav', $url, $last_id, TRUE);
986 }
987 if ($archive) {
988 $last_id = $this->store->getLastId($sequence);
989 $this->action('toggle_archive', $url, $last_id, TRUE);
990 }
991 }
992 }
993 } 976 }
994 $this->messages->add('s', _('import from Readability completed. ' . $count . ' new links.')); 977 Tools::logm('Import of articles finished: '.$i.' articles added (w/o content if not provided).');
995 Tools::logm('import from Readability completed'); 978 }
979 //file parsing finished here
980
981 //now download article contents if any
982
983 //check if we need to download any content
984 $recordsDownloadRequired = $this->store->retrieveUnfetchedEntriesCount($this->user->getId());
985 if ( $recordsDownloadRequired == 0 ) {
986 //nothing to download
987 $this->messages->add('s', _('Import finished.'));
988 Tools::logm('Import finished completely');
996 Tools::redirect(); 989 Tools::redirect();
997 } 990 }
991 else {
992 //if just inserted - don't download anything, download will start in next reload
993 if ( !isset($_FILES['file']) ) {
994 //download next batch
995 Tools::logm('Fetching next batch of articles...');
996 $items = $this->store->retrieveUnfetchedEntries($this->user->getId(), IMPORT_LIMIT);
998 997
999 /** 998 $purifier = $this->getPurifier();
1000 * import from Poche exported file
1001 * @param string $targetFile the file used for importing
1002 * @return boolean
1003 */
1004 private function importFromPoche($targetFile)
1005 {
1006 $str_data = file_get_contents($targetFile);
1007 $data = json_decode($str_data,true);
1008 Tools::logm('starting import from Poche');
1009 999
1000 foreach ($items as $item) {
1001 $url = new Url(base64_encode($item['url']));
1002 Tools::logm('Fetching article '.$item['id']);
1003 $content = Tools::getPageContent($url);
1010 1004
1011 $sequence = ''; 1005 $title = (($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'));
1012 if (STORAGE == 'postgres') { 1006 $body = (($content['rss']['channel']['item']['description'] != '') ? $content['rss']['channel']['item']['description'] : _('Undefined'));
1013 $sequence = 'entries_id_seq';
1014 }
1015 1007
1016 $count = 0; 1008 //clean content to prevent xss attack
1017 foreach ($data as $value) { 1009 $title = $purifier->purify($title);
1018 1010 $body = $purifier->purify($body);
1019 $url = new Url(base64_encode($value['url'])); 1011
1020 $favorite = ($value['is_fav'] == -1); 1012 $this->store->updateContentAndTitle($item['id'], $title, $body, $this->user->getId());
1021 $archive = ($value['is_read'] == -1); 1013 Tools::logm('Article '.$item['id'].' updated.');
1022 1014 }
1023 # we can add the url
1024 if (!is_null($url) && $url->isCorrect()) {
1025
1026 $this->action('add', $url, 0, TRUE);
1027
1028 $count++;
1029 if ($favorite) {
1030 $last_id = $this->store->getLastId($sequence);
1031 $this->action('toggle_fav', $url, $last_id, TRUE);
1032 }
1033 if ($archive) {
1034 $last_id = $this->store->getLastId($sequence);
1035 $this->action('toggle_archive', $url, $last_id, TRUE);
1036 }
1037 }
1038
1039 }
1040 $this->messages->add('s', _('import from Poche completed. ' . $count . ' new links.'));
1041 Tools::logm('import from Poche completed');
1042 Tools::redirect();
1043 }
1044 1015
1045 /**
1046 * import datas into your poche
1047 * @param string $from name of the service to import : pocket, instapaper or readability
1048 * @todo add the return value
1049 * @return boolean
1050 */
1051 public function import($from)
1052 {
1053 $providers = array(
1054 'pocket' => 'importFromPocket',
1055 'readability' => 'importFromReadability',
1056 'instapaper' => 'importFromInstapaper',
1057 'poche' => 'importFromPoche',
1058 );
1059
1060 if (! isset($providers[$from])) {
1061 $this->messages->add('e', _('Unknown import provider.'));
1062 Tools::redirect();
1063 }
1064
1065 $targetDefinition = 'IMPORT_' . strtoupper($from) . '_FILE';
1066 $targetFile = constant($targetDefinition);
1067
1068 if (! defined($targetDefinition)) {
1069 $this->messages->add('e', _('Incomplete inc/poche/define.inc.php file, please define "' . $targetDefinition . '".'));
1070 Tools::redirect();
1071 }
1072
1073 if (! file_exists($targetFile)) {
1074 $this->messages->add('e', _('Could not find required "' . $targetFile . '" import file.'));
1075 Tools::redirect();
1076 } 1016 }
1077 1017 }
1078 $this->$providers[$from]($targetFile); 1018
1019 return array('includeImport'=>true, 'import'=>array('recordsDownloadRequired'=>$recordsDownloadRequired, 'recordsUnderDownload'=> IMPORT_LIMIT, 'delay'=> IMPORT_DELAY * 1000) );
1079 } 1020 }
1080 1021
1081 /** 1022 /**
1082 * export poche entries in json 1023 * export poche entries in json
1083 * @return json all poche entries 1024 * @return json all poche entries
1084 */ 1025 */
1085 public function export() 1026 public function export() {
1086 { 1027 $filename = "wallabag-export-".$this->user->getId()."-".date("Y-m-d").".json";
1087 $entries = $this->store->retrieveAll($this->user->getId()); 1028 header('Content-Disposition: attachment; filename='.$filename);
1088 echo $this->tpl->render('export.twig', array( 1029
1089 'export' => Tools::renderJson($entries), 1030 $entries = $this->store->retrieveAll($this->user->getId());
1090 )); 1031 echo $this->tpl->render('export.twig', array(
1091 Tools::logm('export view'); 1032 'export' => Tools::renderJson($entries),
1033 ));
1034 Tools::logm('export view');
1092 } 1035 }
1093 1036
1094 /** 1037 /**
@@ -1096,35 +1039,42 @@ class Poche
1096 * @param string $which 'prod' or 'dev' 1039 * @param string $which 'prod' or 'dev'
1097 * @return string latest $which version 1040 * @return string latest $which version
1098 */ 1041 */
1099 private function getPocheVersion($which = 'prod') 1042 private function getPocheVersion($which = 'prod') {
1100 { 1043 $cache_file = CACHE . '/' . $which;
1101 $cache_file = CACHE . '/' . $which; 1044 $check_time = time();
1102 1045
1103 # checks if the cached version file exists 1046 # checks if the cached version file exists
1104 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) { 1047 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
1105 $version = file_get_contents($cache_file); 1048 $version = file_get_contents($cache_file);
1106 } else { 1049 $check_time = filemtime($cache_file);
1107 $version = file_get_contents('http://static.wallabag.org/versions/' . $which); 1050 } else {
1108 file_put_contents($cache_file, $version, LOCK_EX); 1051 $version = file_get_contents('http://static.wallabag.org/versions/' . $which);
1109 } 1052 file_put_contents($cache_file, $version, LOCK_EX);
1110 return $version; 1053 }
1054 return array($version, $check_time);
1111 } 1055 }
1112 1056
1113 public function generateToken() 1057 public function generateToken()
1114 { 1058 {
1115 if (ini_get('open_basedir') === '') { 1059 if (ini_get('open_basedir') === '') {
1116 $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15); 1060 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1117 } 1061 echo 'This is a server using Windows!';
1118 else { 1062 // alternative to /dev/urandom for Windows
1119 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20); 1063 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1120 } 1064 } else {
1121 1065 $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
1122 $token = str_replace('+', '', $token); 1066 }
1123 $this->store->updateUserConfig($this->user->getId(), 'token', $token); 1067 }
1124 $currentConfig = $_SESSION['poche_user']->config; 1068 else {
1125 $currentConfig['token'] = $token; 1069 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1126 $_SESSION['poche_user']->setConfig($currentConfig); 1070 }
1127 Tools::redirect(); 1071
1072 $token = str_replace('+', '', $token);
1073 $this->store->updateUserConfig($this->user->getId(), 'token', $token);
1074 $currentConfig = $_SESSION['poche_user']->config;
1075 $currentConfig['token'] = $token;
1076 $_SESSION['poche_user']->setConfig($currentConfig);
1077 Tools::redirect();
1128 } 1078 }
1129 1079
1130 public function generateFeeds($token, $user_id, $tag_id, $type = 'home') 1080 public function generateFeeds($token, $user_id, $tag_id, $type = 'home')
@@ -1132,8 +1082,11 @@ class Poche
1132 $allowed_types = array('home', 'fav', 'archive', 'tag'); 1082 $allowed_types = array('home', 'fav', 'archive', 'tag');
1133 $config = $this->store->getConfigUser($user_id); 1083 $config = $this->store->getConfigUser($user_id);
1134 1084
1135 if (!in_array($type, $allowed_types) || 1085 if ($config == null) {
1136 $token != $config['token']) { 1086 die(sprintf(_('User with this id (%d) does not exist.'), $user_id));
1087 }
1088
1089 if (!in_array($type, $allowed_types) || $token != $config['token']) {
1137 die(_('Uh, there is a problem while generating feeds.')); 1090 die(_('Uh, there is a problem while generating feeds.'));
1138 } 1091 }
1139 // Check the token 1092 // Check the token
@@ -1141,8 +1094,9 @@ class Poche
1141 $feed = new FeedWriter(RSS2); 1094 $feed = new FeedWriter(RSS2);
1142 $feed->setTitle('wallabag — ' . $type . ' feed'); 1095 $feed->setTitle('wallabag — ' . $type . ' feed');
1143 $feed->setLink(Tools::getPocheUrl()); 1096 $feed->setLink(Tools::getPocheUrl());
1144 $feed->setChannelElement('updated', date(DATE_RSS , time())); 1097 $feed->setChannelElement('pubDate', date(DATE_RSS , time()));
1145 $feed->setChannelElement('author', 'wallabag'); 1098 $feed->setChannelElement('generator', 'wallabag');
1099 $feed->setDescription('wallabag ' . $type . ' elements');
1146 1100
1147 if ($type == 'tag') { 1101 if ($type == 'tag') {
1148 $entries = $this->store->retrieveEntriesByTag($tag_id, $user_id); 1102 $entries = $this->store->retrieveEntriesByTag($tag_id, $user_id);
@@ -1155,7 +1109,8 @@ class Poche
1155 foreach ($entries as $entry) { 1109 foreach ($entries as $entry) {
1156 $newItem = $feed->createNewItem(); 1110 $newItem = $feed->createNewItem();
1157 $newItem->setTitle($entry['title']); 1111 $newItem->setTitle($entry['title']);
1158 $newItem->setLink(Tools::getPocheUrl() . '?view=view&amp;id=' . $entry['id']); 1112 $newItem->setSource(Tools::getPocheUrl() . '?view=view&amp;id=' . $entry['id']);
1113 $newItem->setLink($entry['url']);
1159 $newItem->setDate(time()); 1114 $newItem->setDate(time());
1160 $newItem->setDescription($entry['content']); 1115 $newItem->setDescription($entry['content']);
1161 $feed->addItem($newItem); 1116 $feed->addItem($newItem);
@@ -1181,4 +1136,132 @@ class Poche
1181 $this->messages->add('s', _('Cache deleted.')); 1136 $this->messages->add('s', _('Cache deleted.'));
1182 Tools::redirect(); 1137 Tools::redirect();
1183 } 1138 }
1139
1140 /**
1141 * return new purifier object with actual config
1142 */
1143 protected function getPurifier() {
1144 $config = HTMLPurifier_Config::createDefault();
1145 $config->set('Cache.SerializerPath', CACHE);
1146 $config->set('HTML.SafeIframe', true);
1147
1148 //allow YouTube, Vimeo and dailymotion videos
1149 $config->set('URI.SafeIframeRegexp', '%^(https?:)?//(www\.youtube(?:-nocookie)?\.com/embed/|player\.vimeo\.com/video/|www\.dailymotion\.com/embed/video/)%');
1150
1151 return new HTMLPurifier($config);
1152 }
1153
1154 /**
1155 * handle epub
1156 */
1157 public function createEpub() {
1158
1159 switch ($_GET['method']) {
1160 case 'id':
1161 $entryID = filter_var($_GET['id'],FILTER_SANITIZE_NUMBER_INT);
1162 $entry = $this->store->retrieveOneById($entryID, $this->user->getId());
1163 $entries = array($entry);
1164 $bookTitle = $entry['title'];
1165 $bookFileName = substr($bookTitle, 0, 200);
1166 break;
1167 case 'all':
1168 $entries = $this->store->retrieveAll($this->user->getId());
1169 $bookTitle = sprintf(_('All my articles on '), date(_('d.m.y'))); #translatable because each country has it's own date format system
1170 $bookFileName = _('Allarticles') . date(_('dmY'));
1171 break;
1172 case 'tag':
1173 $tag = filter_var($_GET['tag'],FILTER_SANITIZE_STRING);
1174 $tags_id = $this->store->retrieveAllTags($this->user->getId(),$tag);
1175 $tag_id = $tags_id[0]["id"]; // we take the first result, which is supposed to match perfectly. There must be a workaround.
1176 $entries = $this->store->retrieveEntriesByTag($tag_id,$this->user->getId());
1177 $bookTitle = sprintf(_('Articles tagged %s'),$tag);
1178 $bookFileName = substr(sprintf(_('Tag %s'),$tag), 0, 200);
1179 break;
1180 case 'category':
1181 $category = filter_var($_GET['category'],FILTER_SANITIZE_STRING);
1182 $entries = $this->store->getEntriesByView($category,$this->user->getId());
1183 $bookTitle = sprintf(_('All articles in category %s'), $category);
1184 $bookFileName = substr(sprintf(_('Category %s'),$category), 0, 200);
1185 break;
1186 case 'search':
1187 $search = filter_var($_GET['search'],FILTER_SANITIZE_STRING);
1188 $entries = $this->store->search($search,$this->user->getId());
1189 $bookTitle = sprintf(_('All articles for search %s'), $search);
1190 $bookFileName = substr(sprintf(_('Search %s'), $search), 0, 200);
1191 break;
1192 case 'default':
1193 die(_('Uh, there is a problem while generating epub.'));
1194
1195 }
1196
1197 $content_start =
1198 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1199 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
1200 . "<head>"
1201 . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
1202 . "<title>wallabag articles book</title>\n"
1203 . "</head>\n"
1204 . "<body>\n";
1205
1206 $bookEnd = "</body>\n</html>\n";
1207
1208 $log = new Logger("wallabag", TRUE);
1209 $fileDir = CACHE;
1210
1211 $book = new EPub(EPub::BOOK_VERSION_EPUB3, DEBUG_POCHE);
1212 $log->logLine("new EPub()");
1213 $log->logLine("EPub class version: " . EPub::VERSION);
1214 $log->logLine("EPub Req. Zip version: " . EPub::REQ_ZIP_VERSION);
1215 $log->logLine("Zip version: " . Zip::VERSION);
1216 $log->logLine("getCurrentServerURL: " . $book->getCurrentServerURL());
1217 $log->logLine("getCurrentPageURL..: " . $book->getCurrentPageURL());
1218
1219 $book->setTitle(_('wallabag\'s articles'));
1220 $book->setIdentifier("http://$_SERVER[HTTP_HOST]", EPub::IDENTIFIER_URI); // Could also be the ISBN number, prefered for published books, or a UUID.
1221 //$book->setLanguage("en"); // Not needed, but included for the example, Language is mandatory, but EPub defaults to "en". Use RFC3066 Language codes, such as "en", "da", "fr" etc.
1222 $book->setDescription(_("Some articles saved on my wallabag"));
1223 $book->setAuthor("wallabag","wallabag");
1224 $book->setPublisher("wallabag","wallabag"); // I hope this is a non existant address :)
1225 $book->setDate(time()); // Strictly not needed as the book date defaults to time().
1226 //$book->setRights("Copyright and licence information specific for the book."); // As this is generated, this _could_ contain the name or licence information of the user who purchased the book, if needed. If this is used that way, the identifier must also be made unique for the book.
1227 $book->setSourceURL("http://$_SERVER[HTTP_HOST]");
1228
1229 $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "PHP");
1230 $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "wallabag");
1231
1232 $cssData = "body {\n margin-left: .5em;\n margin-right: .5em;\n text-align: justify;\n}\n\np {\n font-family: serif;\n font-size: 10pt;\n text-align: justify;\n text-indent: 1em;\n margin-top: 0px;\n margin-bottom: 1ex;\n}\n\nh1, h2 {\n font-family: sans-serif;\n font-style: italic;\n text-align: center;\n background-color: #6b879c;\n color: white;\n width: 100%;\n}\n\nh1 {\n margin-bottom: 2px;\n}\n\nh2 {\n margin-top: -2px;\n margin-bottom: 2px;\n}\n";
1233
1234 $log->logLine("Add Cover");
1235
1236 $fullTitle = "<h1> " . $bookTitle . "</h1>\n";
1237
1238 $book->setCoverImage("Cover.png", file_get_contents("themes/baggy/img/apple-touch-icon-152.png"), "image/png", $fullTitle);
1239
1240 $cover = $content_start . '<div style="text-align:center;"><p>' . _('Produced by wallabag with PHPePub') . '</p><p>'. _('Please open <a href="https://github.com/wallabag/wallabag/issues" >an issue</a> if you have trouble with the display of this E-Book on your device.') . '</p></div>' . $bookEnd;
1241
1242 //$book->addChapter("Table of Contents", "TOC.xhtml", NULL, false, EPub::EXTERNAL_REF_IGNORE);
1243 $book->addChapter("Notices", "Cover2.html", $cover);
1244
1245 $book->buildTOC();
1246
1247 foreach ($entries as $entry) { //set tags as subjects
1248 $tags = $this->store->retrieveTagsByEntry($entry['id']);
1249 foreach ($tags as $tag) {
1250 $book->setSubject($tag['value']);
1251 }
1252
1253 $log->logLine("Set up parameters");
1254
1255 $chapter = $content_start . $entry['content'] . $bookEnd;
1256 $book->addChapter($entry['title'], htmlspecialchars($entry['title']) . ".html", $chapter, true, EPub::EXTERNAL_REF_ADD);
1257 $log->logLine("Added chapter " . $entry['title']);
1258 }
1259
1260 if (DEBUG_POCHE) {
1261 $epuplog = $book->getLog();
1262 $book->addChapter("Log", "Log.html", $content_start . $log->getLog() . "\n</pre>" . $bookEnd); // log generation
1263 }
1264 $book->finalize();
1265 $zipData = $book->sendBook($bookFileName);
1266 }
1184} 1267}
diff --git a/inc/poche/Tools.class.php b/inc/poche/Tools.class.php
index 4ed28ed1..cc01f403 100644..100755
--- a/inc/poche/Tools.class.php
+++ b/inc/poche/Tools.class.php
@@ -7,7 +7,7 @@
7 * @copyright 2013 7 * @copyright 2013
8 * @license http://www.wtfpl.net/ see COPYING file 8 * @license http://www.wtfpl.net/ see COPYING file
9 */ 9 */
10 10
11class Tools 11class Tools
12{ 12{
13 public static function initPhp() 13 public static function initPhp()
@@ -18,8 +18,6 @@ class Tools
18 die(_('Oops, it seems you don\'t have PHP 5.')); 18 die(_('Oops, it seems you don\'t have PHP 5.'));
19 } 19 }
20 20
21 error_reporting(E_ALL);
22
23 function stripslashesDeep($value) { 21 function stripslashesDeep($value) {
24 return is_array($value) 22 return is_array($value)
25 ? array_map('stripslashesDeep', $value) 23 ? array_map('stripslashesDeep', $value)
@@ -42,7 +40,7 @@ class Tools
42 && (strtolower($_SERVER['HTTPS']) == 'on')) 40 && (strtolower($_SERVER['HTTPS']) == 'on'))
43 || (isset($_SERVER["SERVER_PORT"]) 41 || (isset($_SERVER["SERVER_PORT"])
44 && $_SERVER["SERVER_PORT"] == '443') // HTTPS detection. 42 && $_SERVER["SERVER_PORT"] == '443') // HTTPS detection.
45 || (isset($_SERVER["SERVER_PORT"]) //Custom HTTPS port detection 43 || (isset($_SERVER["SERVER_PORT"]) //Custom HTTPS port detection
46 && $_SERVER["SERVER_PORT"] == SSL_PORT) 44 && $_SERVER["SERVER_PORT"] == SSL_PORT)
47 || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) 45 || (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
48 && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'); 46 && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https');
@@ -59,8 +57,14 @@ class Tools
59 return $scriptname; 57 return $scriptname;
60 } 58 }
61 59
60 $host = (isset($_SERVER['HTTP_X_FORWARDED_HOST']) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']));
61
62 if (strpos($host, ':') !== false) {
63 $serverport = '';
64 }
65
62 return 'http' . ($https ? 's' : '') . '://' 66 return 'http' . ($https ? 's' : '') . '://'
63 . $_SERVER["HTTP_HOST"] . $serverport . $scriptname; 67 . $host . $serverport . $scriptname;
64 } 68 }
65 69
66 public static function redirect($url = '') 70 public static function redirect($url = '')
@@ -148,7 +152,7 @@ class Tools
148 ); 152 );
149 153
150 # only download page lesser than 4MB 154 # only download page lesser than 4MB
151 $data = @file_get_contents($url, false, $context, -1, 4000000); 155 $data = @file_get_contents($url, false, $context, -1, 4000000);
152 156
153 if (isset($http_response_header) and isset($http_response_header[0])) { 157 if (isset($http_response_header) and isset($http_response_header[0])) {
154 $httpcodeOK = isset($http_response_header) and isset($http_response_header[0]) and ((strpos($http_response_header[0], '200 OK') !== FALSE) or (strpos($http_response_header[0], '301 Moved Permanently') !== FALSE)); 158 $httpcodeOK = isset($http_response_header) and isset($http_response_header[0]) and ((strpos($http_response_header[0], '200 OK') !== FALSE) or (strpos($http_response_header[0], '301 Moved Permanently') !== FALSE));
@@ -193,14 +197,14 @@ class Tools
193 197
194 public static function logm($message) 198 public static function logm($message)
195 { 199 {
196 if (DEBUG_POCHE) { 200 if (DEBUG_POCHE && php_sapi_name() != 'cli') {
197 $t = strval(date('Y/m/d_H:i:s')) . ' - ' . $_SERVER["REMOTE_ADDR"] . ' - ' . strval($message) . "\n"; 201 $t = strval(date('Y/m/d_H:i:s')) . ' - ' . $_SERVER["REMOTE_ADDR"] . ' - ' . strval($message) . "\n";
198 file_put_contents(CACHE . '/log.txt', $t, FILE_APPEND); 202 file_put_contents(CACHE . '/log.txt', $t, FILE_APPEND);
199 error_log('DEBUG POCHE : ' . $message); 203 error_log('DEBUG POCHE : ' . $message);
200 } 204 }
201 } 205 }
202 206
203 public static function encodeString($string) 207 public static function encodeString($string)
204 { 208 {
205 return sha1($string . SALT); 209 return sha1($string . SALT);
206 } 210 }
@@ -212,7 +216,7 @@ class Tools
212 216
213 public static function getDomain($url) 217 public static function getDomain($url)
214 { 218 {
215 return parse_url($url, PHP_URL_HOST); 219 return parse_url($url, PHP_URL_HOST);
216 } 220 }
217 221
218 public static function getReadingTime($text) { 222 public static function getReadingTime($text) {
@@ -241,7 +245,6 @@ class Tools
241 } 245 }
242 } 246 }
243 247
244
245 public static function download_db() { 248 public static function download_db() {
246 header('Content-Disposition: attachment; filename="poche.sqlite.gz"'); 249 header('Content-Disposition: attachment; filename="poche.sqlite.gz"');
247 self::status(200); 250 self::status(200);
@@ -252,4 +255,74 @@ class Tools
252 255
253 exit; 256 exit;
254 } 257 }
258
259 public static function getPageContent(Url $url)
260 {
261 // Saving and clearing context
262 $REAL = array();
263 foreach( $GLOBALS as $key => $value ) {
264 if( $key != 'GLOBALS' && $key != '_SESSION' && $key != 'HTTP_SESSION_VARS' ) {
265 $GLOBALS[$key] = array();
266 $REAL[$key] = $value;
267 }
268 }
269 // Saving and clearing session
270 if ( isset($_SESSION) ) {
271 $REAL_SESSION = array();
272 foreach( $_SESSION as $key => $value ) {
273 $REAL_SESSION[$key] = $value;
274 unset($_SESSION[$key]);
275 }
276 }
277
278 // Running code in different context
279 $scope = function() {
280 extract( func_get_arg(1) );
281 $_GET = $_REQUEST = array(
282 "url" => $url->getUrl(),
283 "max" => 5,
284 "links" => "preserve",
285 "exc" => "",
286 "format" => "json",
287 "submit" => "Create Feed"
288 );
289 ob_start();
290 require func_get_arg(0);
291 $json = ob_get_contents();
292 ob_end_clean();
293 return $json;
294 };
295 $json = $scope( "inc/3rdparty/makefulltextfeed.php", array("url" => $url) );
296
297 // Clearing and restoring context
298 foreach( $GLOBALS as $key => $value ) {
299 if( $key != "GLOBALS" && $key != "_SESSION" ) {
300 unset($GLOBALS[$key]);
301 }
302 }
303 foreach( $REAL as $key => $value ) {
304 $GLOBALS[$key] = $value;
305 }
306 // Clearing and restoring session
307 if ( isset($REAL_SESSION) ) {
308 foreach( $_SESSION as $key => $value ) {
309 unset($_SESSION[$key]);
310 }
311 foreach( $REAL_SESSION as $key => $value ) {
312 $_SESSION[$key] = $value;
313 }
314 }
315
316 return json_decode($json, true);
317 }
318
319 /**
320 * Returns whether we handle an AJAX (XMLHttpRequest) request.
321 * @return boolean whether we handle an AJAX (XMLHttpRequest) request.
322 */
323 public static function isAjaxRequest()
324 {
325 return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest';
326 }
327
255} 328}
diff --git a/inc/poche/config.inc.default.php b/inc/poche/config.inc.default.php
new file mode 100755
index 00000000..95f727c6
--- /dev/null
+++ b/inc/poche/config.inc.default.php
@@ -0,0 +1,68 @@
1<?php
2/**
3 * wallabag, self hostable application allowing you to not miss any content anymore
4 *
5 * @category wallabag
6 * @author Nicolas Lœuillet <nicolas@loeuillet.org>
7 * @copyright 2013
8 * @license http://www.wtfpl.net/ see COPYING file
9 */
10
11@define ('SALT', ''); # put a strong string here
12@define ('LANG', 'en_EN.utf8');
13
14@define ('STORAGE', 'sqlite'); # postgres, mysql or sqlite
15
16@define ('STORAGE_SQLITE', ROOT . '/db/poche.sqlite'); # if you are using sqlite, where the database file is located
17
18# only for postgres & mysql
19@define ('STORAGE_SERVER', 'localhost');
20@define ('STORAGE_DB', 'poche');
21@define ('STORAGE_USER', 'poche');
22@define ('STORAGE_PASSWORD', 'poche');
23
24#################################################################################
25# Do not trespass unless you know what you are doing
26#################################################################################
27
28// Change this if not using the standart port for SSL - i.e you server is behind sslh
29@define ('SSL_PORT', 443);
30
31@define ('MODE_DEMO', FALSE);
32@define ('DEBUG_POCHE', FALSE);
33
34//default level of error reporting in application. Developers should override it in their config.inc.php: set to E_ALL.
35@define ('ERROR_REPORTING', E_ALL & ~E_NOTICE);
36
37@define ('DOWNLOAD_PICTURES', FALSE); # This can slow down the process of adding articles
38@define ('REGENERATE_PICTURES_QUALITY', 75);
39@define ('CONVERT_LINKS_FOOTNOTES', FALSE);
40@define ('REVERT_FORCED_PARAGRAPH_ELEMENTS', FALSE);
41@define ('SHARE_TWITTER', TRUE);
42@define ('SHARE_MAIL', TRUE);
43@define ('SHARE_SHAARLI', FALSE);
44@define ('SHAARLI_URL', 'http://myshaarliurl.com');
45@define ('FLATTR', TRUE);
46@define ('FLATTR_API', 'https://api.flattr.com/rest/v2/things/lookup/?url=');
47@define ('NOT_FLATTRABLE', '0');
48@define ('FLATTRABLE', '1');
49@define ('FLATTRED', '2');
50// display or not print link in article view
51@define ('SHOW_PRINTLINK', '1');
52// display or not percent of read in article view. Affects only default theme.
53@define ('SHOW_READPERCENT', '1');
54@define ('ABS_PATH', 'assets/');
55
56@define ('DEFAULT_THEME', 'baggy');
57
58@define ('THEME', ROOT . '/themes');
59@define ('LOCALE', ROOT . '/locale');
60@define ('CACHE', ROOT . '/cache');
61
62@define ('PAGINATION', '10');
63
64//limit for download of articles during import
65@define ('IMPORT_LIMIT', 5);
66//delay between downloads (in sec)
67@define ('IMPORT_DELAY', 5);
68
diff --git a/inc/poche/config.inc.php.new b/inc/poche/config.inc.php.new
deleted file mode 100755
index 8d52497b..00000000
--- a/inc/poche/config.inc.php.new
+++ /dev/null
@@ -1,63 +0,0 @@
1<?php
2/**
3 * wallabag, self hostable application allowing you to not miss any content anymore
4 *
5 * @category wallabag
6 * @author Nicolas Lœuillet <nicolas@loeuillet.org>
7 * @copyright 2013
8 * @license http://www.wtfpl.net/ see COPYING file
9 */
10
11define ('SALT', ''); # put a strong string here
12define ('LANG', 'en_EN.utf8');
13
14define ('STORAGE', 'sqlite'); # postgres, mysql or sqlite
15
16define ('STORAGE_SQLITE', ROOT . '/db/poche.sqlite'); # if you are using sqlite, where the database file is located
17
18# only for postgres & mysql
19define ('STORAGE_SERVER', 'localhost');
20define ('STORAGE_DB', 'poche');
21define ('STORAGE_USER', 'poche');
22define ('STORAGE_PASSWORD', 'poche');
23
24#################################################################################
25# Do not trespass unless you know what you are doing
26#################################################################################
27
28// Change this if not using the standart port for SSL - i.e you server is behind sslh
29define ('SSL_PORT', 443);
30
31define ('MODE_DEMO', FALSE);
32define ('DEBUG_POCHE', FALSE);
33define ('DOWNLOAD_PICTURES', FALSE);
34define ('CONVERT_LINKS_FOOTNOTES', FALSE);
35define ('REVERT_FORCED_PARAGRAPH_ELEMENTS', FALSE);
36define ('SHARE_TWITTER', TRUE);
37define ('SHARE_MAIL', TRUE);
38define ('SHARE_SHAARLI', FALSE);
39define ('SHAARLI_URL', 'http://myshaarliurl.com');
40define ('FLATTR', TRUE);
41define ('FLATTR_API', 'https://api.flattr.com/rest/v2/things/lookup/?url=');
42define ('NOT_FLATTRABLE', '0');
43define ('FLATTRABLE', '1');
44define ('FLATTRED', '2');
45define ('ABS_PATH', 'assets/');
46
47define ('DEFAULT_THEME', 'baggy');
48
49define ('THEME', ROOT . '/themes');
50define ('LOCALE', ROOT . '/locale');
51define ('CACHE', ROOT . '/cache');
52
53define ('PAGINATION', '10');
54
55define ('POCKET_FILE', '/ril_export.html');
56define ('READABILITY_FILE', '/readability');
57define ('INSTAPAPER_FILE', '/instapaper-export.html');
58define ('POCHE_FILE', '/poche-export');
59
60define ('IMPORT_POCKET_FILE', ROOT . POCKET_FILE);
61define ('IMPORT_READABILITY_FILE', ROOT . READABILITY_FILE);
62define ('IMPORT_INSTAPAPER_FILE', ROOT . INSTAPAPER_FILE);
63define ('IMPORT_POCHE_FILE', ROOT . POCHE_FILE); \ No newline at end of file
diff --git a/inc/poche/global.inc.php b/inc/poche/global.inc.php
index d22b0588..8cf86d03 100644..100755
--- a/inc/poche/global.inc.php
+++ b/inc/poche/global.inc.php
@@ -31,6 +31,11 @@ require_once INCLUDES . '/3rdparty/FlattrItem.class.php';
31 31
32require_once INCLUDES . '/3rdparty/htmlpurifier/HTMLPurifier.auto.php'; 32require_once INCLUDES . '/3rdparty/htmlpurifier/HTMLPurifier.auto.php';
33 33
34# epub library
35require_once INCLUDES . '/3rdparty/libraries/PHPePub/Logger.php';
36require_once INCLUDES . '/3rdparty/libraries/PHPePub/EPub.php';
37require_once INCLUDES . '/3rdparty/libraries/PHPePub/EPubChapterSplitter.php';
38
34# Composer its autoloader for automatically loading Twig 39# Composer its autoloader for automatically loading Twig
35if (! file_exists(ROOT . '/vendor/autoload.php')) { 40if (! file_exists(ROOT . '/vendor/autoload.php')) {
36 Poche::$canRenderTemplates = false; 41 Poche::$canRenderTemplates = false;
@@ -38,11 +43,12 @@ if (! file_exists(ROOT . '/vendor/autoload.php')) {
38 require_once ROOT . '/vendor/autoload.php'; 43 require_once ROOT . '/vendor/autoload.php';
39} 44}
40 45
41# system configuration; database credentials et cetera 46# system configuration; database credentials et caetera
42if (! file_exists(INCLUDES . '/poche/config.inc.php')) { 47if (! file_exists(INCLUDES . '/poche/config.inc.php')) {
43 Poche::$configFileAvailable = false; 48 Poche::$configFileAvailable = false;
44} else { 49} else {
45 require_once INCLUDES . '/poche/config.inc.php'; 50 require_once INCLUDES . '/poche/config.inc.php';
51 require_once INCLUDES . '/poche/config.inc.default.php';
46} 52}
47 53
48if (Poche::$configFileAvailable && DOWNLOAD_PICTURES) { 54if (Poche::$configFileAvailable && DOWNLOAD_PICTURES) {
diff --git a/inc/poche/pochePictures.php b/inc/poche/pochePictures.php
index e4b0b160..7c319a85 100644
--- a/inc/poche/pochePictures.php
+++ b/inc/poche/pochePictures.php
@@ -14,6 +14,7 @@
14function filtre_picture($content, $url, $id) 14function filtre_picture($content, $url, $id)
15{ 15{
16 $matches = array(); 16 $matches = array();
17 $processing_pictures = array(); // list of processing image to avoid processing the same pictures twice
17 preg_match_all('#<\s*(img)[^>]+src="([^"]*)"[^>]*>#Si', $content, $matches, PREG_SET_ORDER); 18 preg_match_all('#<\s*(img)[^>]+src="([^"]*)"[^>]*>#Si', $content, $matches, PREG_SET_ORDER);
18 foreach($matches as $i => $link) { 19 foreach($matches as $i => $link) {
19 $link[1] = trim($link[1]); 20 $link[1] = trim($link[1]);
@@ -22,8 +23,17 @@ function filtre_picture($content, $url, $id)
22 $filename = basename(parse_url($absolute_path, PHP_URL_PATH)); 23 $filename = basename(parse_url($absolute_path, PHP_URL_PATH));
23 $directory = create_assets_directory($id); 24 $directory = create_assets_directory($id);
24 $fullpath = $directory . '/' . $filename; 25 $fullpath = $directory . '/' . $filename;
25 download_pictures($absolute_path, $fullpath); 26
26 $content = str_replace($matches[$i][2], $fullpath, $content); 27 if (in_array($absolute_path, $processing_pictures) === true) {
28 // replace picture's URL only if processing is OK : already processing -> go to next picture
29 continue;
30 }
31
32 if (download_pictures($absolute_path, $fullpath) === true) {
33 $content = str_replace($matches[$i][2], $fullpath, $content);
34 }
35
36 $processing_pictures[] = $absolute_path;
27 } 37 }
28 38
29 } 39 }
@@ -64,17 +74,55 @@ function get_absolute_link($relative_link, $url) {
64 74
65/** 75/**
66 * Téléchargement des images 76 * Téléchargement des images
77 *
78 * @return bool true if the download and processing is OK, false else
67 */ 79 */
68function download_pictures($absolute_path, $fullpath) 80function download_pictures($absolute_path, $fullpath)
69{ 81{
70 $rawdata = Tools::getFile($absolute_path); 82 $rawdata = Tools::getFile($absolute_path);
83 $fullpath = urldecode($fullpath);
71 84
72 if(file_exists($fullpath)) { 85 if(file_exists($fullpath)) {
73 unlink($fullpath); 86 unlink($fullpath);
74 } 87 }
75 $fp = fopen($fullpath, 'x'); 88
76 fwrite($fp, $rawdata); 89 // check extension
77 fclose($fp); 90 $file_ext = strrchr($fullpath, '.');
91 $whitelist = array(".jpg",".jpeg",".gif",".png");
92 if (!(in_array($file_ext, $whitelist))) {
93 Tools::logm('processed image with not allowed extension. Skipping ' . $fullpath);
94 return false;
95 }
96
97 // check headers
98 $imageinfo = getimagesize($absolute_path);
99 if ($imageinfo['mime'] != 'image/gif' && $imageinfo['mime'] != 'image/jpeg'&& $imageinfo['mime'] != 'image/jpg'&& $imageinfo['mime'] != 'image/png') {
100 Tools::logm('processed image with bad header. Skipping ' . $fullpath);
101 return false;
102 }
103
104 // regenerate image
105 $im = imagecreatefromstring($rawdata);
106 if ($im === false) {
107 Tools::logm('error while regenerating image ' . $fullpath);
108 return false;
109 }
110
111 switch ($imageinfo['mime']) {
112 case 'image/gif':
113 $result = imagegif($im, $fullpath);
114 break;
115 case 'image/jpeg':
116 case 'image/jpg':
117 $result = imagejpeg($im, $fullpath, REGENERATE_PICTURES_QUALITY);
118 break;
119 case 'image/png':
120 $result = imagepng($im, $fullpath, ceil(REGENERATE_PICTURES_QUALITY / 100 * 9));
121 break;
122 }
123 imagedestroy($im);
124
125 return $result;
78} 126}
79 127
80/** 128/**
diff --git a/index.php b/index.php
index a26c42e1..481841ec 100644..100755
--- a/index.php
+++ b/index.php
@@ -8,10 +8,18 @@
8 * @license http://www.wtfpl.net/ see COPYING file 8 * @license http://www.wtfpl.net/ see COPYING file
9 */ 9 */
10 10
11define ('POCHE', '1.5.2'); 11define ('POCHE', '1.7.1');
12require 'check_setup.php'; 12require 'check_setup.php';
13require_once 'inc/poche/global.inc.php'; 13require_once 'inc/poche/global.inc.php';
14session_start(); 14
15# Set error reporting level
16if (defined('ERROR_REPORTING')) {
17 error_reporting(ERROR_REPORTING);
18}
19
20# Start session
21Session::$sessionName = 'poche';
22Session::init();
15 23
16# Start Poche 24# Start Poche
17$poche = new Poche(); 25$poche = new Poche();
@@ -30,14 +38,14 @@ $tpl_vars = array(
30 'referer' => $referer, 38 'referer' => $referer,
31 'view' => $view, 39 'view' => $view,
32 'poche_url' => Tools::getPocheUrl(), 40 'poche_url' => Tools::getPocheUrl(),
33 'title' => _('poche, a read it later open source system'), 41 'title' => _('wallabag, a read it later open source system'),
34 'token' => Session::getToken(), 42 'token' => Session::getToken(),
35 'theme' => $poche->getTheme() 43 'theme' => $poche->getTheme()
36); 44);
37 45
38if (! empty($notInstalledMessage)) { 46if (! empty($notInstalledMessage)) {
39 if (! Poche::$canRenderTemplates || ! Poche::$configFileAvailable) { 47 if (! Poche::$canRenderTemplates || ! Poche::$configFileAvailable) {
40 # We cannot use Twig to display the error message 48 # We cannot use Twig to display the error message
41 echo '<h1>Errors</h1><ol>'; 49 echo '<h1>Errors</h1><ol>';
42 foreach ($notInstalledMessage as $message) { 50 foreach ($notInstalledMessage as $message) {
43 echo '<li>' . $message . '</li>'; 51 echo '<li>' . $message . '</li>';
@@ -63,8 +71,15 @@ if (isset($_GET['login'])) {
63} elseif (isset($_GET['config'])) { 71} elseif (isset($_GET['config'])) {
64 # Update password 72 # Update password
65 $poche->updatePassword(); 73 $poche->updatePassword();
74} elseif (isset($_GET['newuser'])) {
75 $poche->createNewUser();
76} elseif (isset($_GET['deluser'])) {
77 $poche->deleteUser();
78} elseif (isset($_GET['epub'])) {
79 $poche->createEpub();
66} elseif (isset($_GET['import'])) { 80} elseif (isset($_GET['import'])) {
67 $import = $poche->import($_GET['from']); 81 $import = $poche->import();
82 $tpl_vars = array_merge($tpl_vars, $import);
68} elseif (isset($_GET['download'])) { 83} elseif (isset($_GET['download'])) {
69 Tools::download_db(); 84 Tools::download_db();
70} elseif (isset($_GET['empty-cache'])) { 85} elseif (isset($_GET['empty-cache'])) {
@@ -75,13 +90,15 @@ if (isset($_GET['login'])) {
75 $poche->updateTheme(); 90 $poche->updateTheme();
76} elseif (isset($_GET['updatelanguage'])) { 91} elseif (isset($_GET['updatelanguage'])) {
77 $poche->updateLanguage(); 92 $poche->updateLanguage();
93} elseif (isset($_GET['uploadfile'])) {
94 $poche->uploadFile();
78} elseif (isset($_GET['feed'])) { 95} elseif (isset($_GET['feed'])) {
79 if (isset($_GET['action']) && $_GET['action'] == 'generate') { 96 if (isset($_GET['action']) && $_GET['action'] == 'generate') {
80 $poche->generateToken(); 97 $poche->generateToken();
81 } 98 }
82 else { 99 else {
83 $tag_id = (isset($_GET['tag_id']) ? intval($_GET['tag_id']) : 0); 100 $tag_id = (isset($_GET['tag_id']) ? intval($_GET['tag_id']) : 0);
84 $poche->generateFeeds($_GET['token'], $_GET['user_id'], $tag_id, $_GET['type']); 101 $poche->generateFeeds($_GET['token'], filter_var($_GET['user_id'],FILTER_SANITIZE_NUMBER_INT), $tag_id, $_GET['type']);
85 } 102 }
86} 103}
87 104
@@ -115,6 +132,7 @@ if (Session::isLogged()) {
115} else { 132} else {
116 $tpl_file = Tools::getTplFile('login'); 133 $tpl_file = Tools::getTplFile('login');
117 $tpl_vars['http_auth'] = 0; 134 $tpl_vars['http_auth'] = 0;
135 Session::logout();
118} 136}
119 137
120# because messages can be added in $poche->action(), we have to add this entry now (we can add it before) 138# because messages can be added in $poche->action(), we have to add this entry now (we can add it before)
diff --git a/install/index.php b/install/index.php
index 975b997f..e702891b 100644..100755
--- a/install/index.php
+++ b/install/index.php
@@ -1,9 +1,30 @@
1<?php 1<?php
2$errors = array(); 2$errors = array();
3$successes = array(); 3$successes = array();
4if ($_POST['download']) { 4
5/* Function taken from at http://php.net/manual/en/function.rmdir.php#110489
6 * Idea : nbari at dalmp dot com
7 * Rights unknown
8 * Here in case of .gitignore files
9 */
10function delTree($dir) {
11 $files = array_diff(scandir($dir), array('.','..'));
12 foreach ($files as $file) {
13 (is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file");
14 }
15 return rmdir($dir);
16 }
17
18if (isset($_GET['clean'])) {
19 if (is_dir('install')){
20 delTree('install');
21 header('Location: index.php');
22 }
23}
24
25if (isset($_POST['download'])) {
5 if (!file_put_contents("cache/vendor.zip", fopen("http://static.wallabag.org/files/vendor.zip", 'r'))) { 26 if (!file_put_contents("cache/vendor.zip", fopen("http://static.wallabag.org/files/vendor.zip", 'r'))) {
6 $errors[] = 'Impossible to download vendor.zip. Please <a href="http://wllbg.org/vendor">download it manually<∕a> and unzip it in your wallabag folder.'; 27 $errors[] = 'Impossible to download vendor.zip. Please <a href="http://wllbg.org/vendor">download it manually</a> and unzip it in your wallabag folder.';
7 } 28 }
8 else { 29 else {
9 if (extension_loaded('zip')) { 30 if (extension_loaded('zip')) {
@@ -25,14 +46,14 @@ if ($_POST['download']) {
25 } 46 }
26 } 47 }
27} 48}
28else if ($_POST['install']) { 49else if (isset($_POST['install'])) {
29 if (!is_dir('vendor')) { 50 if (!is_dir('vendor')) {
30 $errors[] = 'You must install twig before.'; 51 $errors[] = 'You must install twig before.';
31 } 52 }
32 else { 53 else {
33 $continue = true; 54 $continue = true;
34 // Create config.inc.php 55 // Create config.inc.php
35 if (!copy('inc/poche/config.inc.php.new', 'inc/poche/config.inc.php')) { 56 if (!copy('inc/poche/config.inc.default.php', 'inc/poche/config.inc.php')) {
36 $errors[] = 'Installation aborted, impossible to create inc/poche/config.inc.php file. Maybe you don\'t have write access to create it.'; 57 $errors[] = 'Installation aborted, impossible to create inc/poche/config.inc.php file. Maybe you don\'t have write access to create it.';
37 $continue = false; 58 $continue = false;
38 } 59 }
@@ -64,6 +85,7 @@ else if ($_POST['install']) {
64 else { 85 else {
65 $db_path = 'sqlite:' . realpath('') . '/db/poche.sqlite'; 86 $db_path = 'sqlite:' . realpath('') . '/db/poche.sqlite';
66 $handle = new PDO($db_path); 87 $handle = new PDO($db_path);
88 $sql_structure = "";
67 } 89 }
68 } 90 }
69 else { 91 else {
@@ -79,7 +101,7 @@ else if ($_POST['install']) {
79 101
80 $sql_structure = file_get_contents('install/mysql.sql'); 102 $sql_structure = file_get_contents('install/mysql.sql');
81 } 103 }
82 else if ($_POST['db_engine'] == 'postgresql') { 104 else if ($_POST['db_engine'] == 'postgres') {
83 $db_path = 'pgsql:host=' . $_POST['pg_server'] . ';dbname=' . $_POST['pg_database']; 105 $db_path = 'pgsql:host=' . $_POST['pg_server'] . ';dbname=' . $_POST['pg_database'];
84 $content = str_replace("define ('STORAGE_SERVER', 'localhost');", "define ('STORAGE_SERVER', '".$_POST['pg_server']."');", $content); 106 $content = str_replace("define ('STORAGE_SERVER', 'localhost');", "define ('STORAGE_SERVER', '".$_POST['pg_server']."');", $content);
85 $content = str_replace("define ('STORAGE_DB', 'poche');", "define ('STORAGE_DB', '".$_POST['pg_database']."');", $content); 107 $content = str_replace("define ('STORAGE_DB', 'poche');", "define ('STORAGE_DB', '".$_POST['pg_database']."');", $content);
@@ -129,7 +151,7 @@ else if ($_POST['install']) {
129 $params = array($id_user, 'language', 'en_EN.UTF8'); 151 $params = array($id_user, 'language', 'en_EN.UTF8');
130 $query = executeQuery($handle, $sql, $params); 152 $query = executeQuery($handle, $sql, $params);
131 153
132 $successes[] = 'wallabag is now installed. Don\'t forget to delete install folder. Then, <a href="index.php">reload this page</a>.'; 154 $successes[] = 'wallabag is now installed. You can now <a href="index.php?clean=0">access it !</a>';
133 } 155 }
134 } 156 }
135 } 157 }
@@ -143,7 +165,7 @@ else if ($_POST['install']) {
143 <!--[if IE]> 165 <!--[if IE]>
144 <meta http-equiv="X-UA-Compatible" content="IE=10"> 166 <meta http-equiv="X-UA-Compatible" content="IE=10">
145 <![endif]--> 167 <![endif]-->
146 <title>wallabag installation</title> 168 <title>wallabag - installation</title>
147 <link rel="shortcut icon" type="image/x-icon" href="themes/baggy/img/favicon.ico" /> 169 <link rel="shortcut icon" type="image/x-icon" href="themes/baggy/img/favicon.ico" />
148 <link rel="apple-touch-icon-precomposed" sizes="144x144" href="themes/baggy/img/apple-touch-icon-144x144-precomposed.png"> 170 <link rel="apple-touch-icon-precomposed" sizes="144x144" href="themes/baggy/img/apple-touch-icon-144x144-precomposed.png">
149 <link rel="apple-touch-icon-precomposed" sizes="72x72" href="themes/baggy/img/apple-touch-icon-72x72-precomposed.png"> 171 <link rel="apple-touch-icon-precomposed" sizes="72x72" href="themes/baggy/img/apple-touch-icon-72x72-precomposed.png">
@@ -154,7 +176,7 @@ else if ($_POST['install']) {
154 <link rel="stylesheet" href="themes/baggy/css/main.css" media="all"> 176 <link rel="stylesheet" href="themes/baggy/css/main.css" media="all">
155 <link rel="stylesheet" href="themes/baggy/css/messages.css" media="all"> 177 <link rel="stylesheet" href="themes/baggy/css/messages.css" media="all">
156 <link rel="stylesheet" href="themes/baggy/css/print.css" media="print"> 178 <link rel="stylesheet" href="themes/baggy/css/print.css" media="print">
157 <script src="themes/baggy/js/jquery-2.0.3.min.js"></script> 179 <script src="themes/default/js/jquery-2.0.3.min.js"></script>
158 <script src="themes/baggy/js/init.js"></script> 180 <script src="themes/baggy/js/init.js"></script>
159 </head> 181 </head>
160 <body> 182 <body>
@@ -198,18 +220,18 @@ else if ($_POST['install']) {
198 <?php if (file_exists('inc/poche/config.inc.php') && is_dir('vendor')) : ?> 220 <?php if (file_exists('inc/poche/config.inc.php') && is_dir('vendor')) : ?>
199 <div class='messages success install'> 221 <div class='messages success install'>
200 <p> 222 <p>
201 wallabag seems already installed. If you want to update it, you only have to delete install folder. 223 wallabag seems already installed. If you want to update it, you only have to delete install folder, then <a href="index.php">reload this page</a>.
202 </p> 224 </p>
203 </div> 225 </div>
204 <?php endif; ?> 226 <?php endif; ?>
205 <?php endif; ?> 227 <?php endif; ?>
206 <p>To install wallabag, you just have to fill the following fields. That's all.</p> 228 <p>To install wallabag, you just have to fill the following fields. That's all.</p>
207 <p>Don't forget to check your server compatibility <a href="wallabag_compatibility_test.php">here</a>.</p> 229 <p>Don't forget to check your server compatibility <a href="wallabag_compatibility_test.php?from=install">here</a>.</p>
208 <form method="post"> 230 <form method="post">
209 <fieldset> 231 <fieldset>
210 <legend><strong>Technical settings</strong></legend> 232 <legend><strong>Technical settings</strong></legend>
211 <?php if (!is_dir('vendor')) : ?> 233 <?php if (!is_dir('vendor')) : ?>
212 <div class='messages notice install'>wallabag needs twig, a template engine (<a href="http://twig.sensiolabs.org/">?</a>). Two ways to install it: 234 <div class='messages notice install'>wallabag needs twig, a template engine (<a href="http://twig.sensiolabs.org/">?</a>). Two ways to install it:<br />
213 <ul> 235 <ul>
214 <li>automatically download and extract vendor.zip into your wallabag folder. 236 <li>automatically download and extract vendor.zip into your wallabag folder.
215 <p><input type="submit" name="download" value="Download vendor.zip" /></p> 237 <p><input type="submit" name="download" value="Download vendor.zip" /></p>
@@ -225,7 +247,11 @@ php composer.phar install</code></pre></li>
225 <p> 247 <p>
226 Database engine: 248 Database engine:
227 <ul> 249 <ul>
228 <li><label for="sqlite">SQLite</label> <input name="db_engine" type="radio" checked="" id="sqlite" value="sqlite" /></li> 250 <li><label for="sqlite">SQLite</label> <input name="db_engine" type="radio" checked="" id="sqlite" value="sqlite" />
251 <div id="pdo_sqlite" class='messages error install'>
252 <p>You have to enable <a href="http://php.net/manual/ref.pdo-sqlite.php">pdo_sqlite extension</a>.</p>
253 </div>
254 </li>
229 <li> 255 <li>
230 <label for="mysql">MySQL</label> <input name="db_engine" type="radio" id="mysql" value="mysql" /> 256 <label for="mysql">MySQL</label> <input name="db_engine" type="radio" id="mysql" value="mysql" />
231 <ul id="mysql_infos"> 257 <ul id="mysql_infos">
@@ -236,12 +262,12 @@ php composer.phar install</code></pre></li>
236 </ul> 262 </ul>
237 </li> 263 </li>
238 <li> 264 <li>
239 <label for="postgresql">PostgreSQL</label> <input name="db_engine" type="radio" id="postgresql" value="postgresql" /> 265 <label for="postgres">PostgreSQL</label> <input name="db_engine" type="radio" id="postgres" value="postgres" />
240 <ul id="pg_infos"> 266 <ul id="pg_infos">
241 <li><label for="pg_server">Server</label> <input type="text" placeholder="localhost" id="pg_server" name="pg_server" /></li> 267 <li><label for="pg_server">Server</label> <input type="text" placeholder="localhost" id="pg_server" name="pg_server" /></li>
242 <li><label for="pg_database">Database</label> <input type="text" placeholder="wallabag" id="pg_database" name="pg_database" /></li> 268 <li><label for="pg_database">Database</label> <input type="text" placeholder="wallabag" id="pg_database" name="pg_database" /></li>
243 <li><label for="pg_user">User</label> <input type="text" placeholder="user" id="pg_user" name="pg_user" /></li> 269 <li><label for="pg_user">User</label> <input type="text" placeholder="user" id="pg_user" name="pg_user" /></li>
244 id <li><label for="pg_password">Password</label> <input type="text" placeholder="p4ssw0rd" id="pg_password" name="pg_password" /></li> 270 <li><label for="pg_password">Password</label> <input type="text" placeholder="p4ssw0rd" id="pg_password" name="pg_password" /></li>
245 </ul> 271 </ul>
246 </li> 272 </li>
247 </ul> 273 </ul>
@@ -263,26 +289,49 @@ php composer.phar install</code></pre></li>
263 </p> 289 </p>
264 </fieldset> 290 </fieldset>
265 291
266 <input type="submit" value="Install wallabag" name="install" /> 292 <input type="submit" id="install_button" value="Install wallabag" name="install" />
267 </form> 293 </form>
268 </div> 294 </div>
269 <script> 295 <script>
270 $("#mysql_infos").hide(); 296 $("#mysql_infos").hide();
271 $("#pg_infos").hide(); 297 $("#pg_infos").hide();
298
299 <?php
300 if (!extension_loaded('pdo_sqlite')) : ?>
301 $("#install_button").hide();
302 <?php
303 else :
304 ?>
305 $("#pdo_sqlite").hide();
306 <?php
307 endif;
308 ?>
309
272 $("input[name=db_engine]").click(function() 310 $("input[name=db_engine]").click(function()
273 { 311 {
274 if ( $("#mysql").prop('checked')) { 312 if ( $("#mysql").prop('checked')) {
275 $("#mysql_infos").show(); 313 $("#mysql_infos").show();
276 $("#pg_infos").hide(); 314 $("#pg_infos").hide();
315 $("#pdo_sqlite").hide();
316 $("#install_button").show();
277 } 317 }
278 else { 318 else {
279 if ( $("#postgresql").prop('checked')) { 319 if ( $("#postgres").prop('checked')) {
280 $("#mysql_infos").hide(); 320 $("#mysql_infos").hide();
281 $("#pg_infos").show(); 321 $("#pg_infos").show();
322 $("#pdo_sqlite").hide();
323 $("#install_button").show();
282 } 324 }
283 else { 325 else {
284 $("#mysql_infos").hide(); 326 $("#mysql_infos").hide();
285 $("#pg_infos").hide(); 327 $("#pg_infos").hide();
328 <?php
329 if (!extension_loaded('pdo_sqlite')) : ?>
330 $("#pdo_sqlite").show();
331 $("#install_button").hide();
332 <?php
333 endif;
334 ?>
286 } 335 }
287 } 336 }
288 }); 337 });
diff --git a/install/poche.sqlite b/install/poche.sqlite
index f2b79b68..f2b79b68 100755..100644
--- a/install/poche.sqlite
+++ b/install/poche.sqlite
Binary files differ
diff --git a/install/postgres.sql b/install/postgres.sql
index fe8f559c..1d73dfcb 100644
--- a/install/postgres.sql
+++ b/install/postgres.sql
@@ -1,10 +1,10 @@
1CREATE TABLE config ( 1CREATE TABLE IF NOT EXISTS config (
2 id bigserial primary key, 2 id bigserial primary key,
3 name varchar(255) NOT NULL, 3 name varchar(255) NOT NULL,
4 value varchar(255) NOT NULL 4 value varchar(255) NOT NULL
5); 5);
6 6
7CREATE TABLE entries ( 7CREATE TABLE IF NOT EXISTS entries (
8 id bigserial primary key, 8 id bigserial primary key,
9 title varchar(255) NOT NULL, 9 title varchar(255) NOT NULL,
10 url varchar(255) NOT NULL, 10 url varchar(255) NOT NULL,
@@ -14,7 +14,7 @@ CREATE TABLE entries (
14 user_id integer NOT NULL 14 user_id integer NOT NULL
15); 15);
16 16
17CREATE TABLE users ( 17CREATE TABLE IF NOT EXISTS users (
18 id bigserial primary key, 18 id bigserial primary key,
19 username varchar(255) NOT NULL, 19 username varchar(255) NOT NULL,
20 password varchar(255) NOT NULL, 20 password varchar(255) NOT NULL,
@@ -22,20 +22,20 @@ CREATE TABLE users (
22 email varchar(255) NOT NULL 22 email varchar(255) NOT NULL
23); 23);
24 24
25CREATE TABLE users_config ( 25CREATE TABLE IF NOT EXISTS users_config (
26 id bigserial primary key, 26 id bigserial primary key,
27 user_id integer NOT NULL, 27 user_id integer NOT NULL,
28 name varchar(255) NOT NULL, 28 name varchar(255) NOT NULL,
29 value varchar(255) NOT NULL 29 value varchar(255) NOT NULL
30); 30);
31 31
32CREATE TABLE tags ( 32CREATE TABLE IF NOT EXISTS tags (
33 id bigserial primary key, 33 id bigserial primary key,
34 value varchar(255) NOT NULL 34 value varchar(255) NOT NULL
35); 35);
36 36
37CREATE TABLE tags_entries ( 37CREATE TABLE IF NOT EXISTS tags_entries (
38 id bigserial primary key, 38 id bigserial primary key,
39 entry_id integer NOT NULL, 39 entry_id integer NOT NULL,
40 tag_id integer NOT NULL 40 tag_id integer NOT NULL
41) \ No newline at end of file 41)
diff --git a/locale/cs_CZ.utf8/LC_MESSAGES/cs_CZ.utf8.po b/locale/cs_CZ.utf8/LC_MESSAGES/cs_CZ.utf8.po
index 8209a9cd..cf727432 100644
--- a/locale/cs_CZ.utf8/LC_MESSAGES/cs_CZ.utf8.po
+++ b/locale/cs_CZ.utf8/LC_MESSAGES/cs_CZ.utf8.po
@@ -4,54 +4,137 @@
4msgid "" 4msgid ""
5msgstr "" 5msgstr ""
6"Project-Id-Version: poche\n" 6"Project-Id-Version: poche\n"
7"POT-Creation-Date: \n" 7"Report-Msgid-Bugs-To: \n"
8"PO-Revision-Date: 2013-10-08 13:25+0100\n" 8"POT-Creation-Date: 2014-02-25 15:28+0300\n"
9"Last-Translator: Nicolas Lœuillet <nicolas.loeuillet@gmail.com>\n" 9"PO-Revision-Date: 2014-02-25 15:29+0300\n"
10"Language-Team: Czech (http://www.transifex.com/projects/p/poche/language/" 10"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n"
11"cs/)\n" 11"Language-Team: Czech (http://www.transifex.com/projects/p/poche/language/cs/)\n"
12"Language: cs\n"
12"MIME-Version: 1.0\n" 13"MIME-Version: 1.0\n"
13"Content-Type: text/plain; charset=UTF-8\n" 14"Content-Type: text/plain; charset=UTF-8\n"
14"Content-Transfer-Encoding: 8bit\n" 15"Content-Transfer-Encoding: 8bit\n"
15"Language: cs\n"
16"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" 16"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
17"X-Generator: Poedit 1.5.4\n" 17"X-Generator: Poedit 1.5.4\n"
18"X-Poedit-Language: Czech\n"
19"X-Poedit-Basepath: .\n"
20"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag-master-testing\n"
21
22msgid "wallabag, a read it later open source system"
23msgstr ""
24
25msgid "login failed: user doesn't exist"
26msgstr ""
27
28msgid "return home"
29msgstr ""
18 30
19msgid "config" 31msgid "config"
20msgstr "nastavení" 32msgstr "nastavení"
21 33
22msgid "Poching a link" 34msgid "Saving articles"
23msgstr "Odkaz se ukládá" 35msgstr ""
36
37msgid "There are several ways to save an article:"
38msgstr ""
24 39
25msgid "read the documentation" 40msgid "read the documentation"
26msgstr "číst dokumentaci" 41msgstr "číst dokumentaci"
27 42
28msgid "by filling this field" 43msgid "download the extension"
44msgstr ""
45
46msgid "via F-Droid"
47msgstr ""
48
49msgid " or "
50msgstr ""
51
52msgid "via Google Play"
53msgstr ""
54
55msgid "download the application"
56msgstr ""
57
58#, fuzzy
59msgid "By filling this field"
29msgstr "vyplněním tohoto pole" 60msgstr "vyplněním tohoto pole"
30 61
31msgid "poche it!" 62msgid "bag it!"
32msgstr "uložit!" 63msgstr ""
33 64
34msgid "Updating poche" 65msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
35msgstr "Poche se aktualizuje" 66msgstr ""
36 67
37msgid "your version" 68msgid "Upgrading wallabag"
38msgstr "vaše verze" 69msgstr ""
39 70
40msgid "latest stable version" 71#, fuzzy
72msgid "Installed version"
41msgstr "poslední stabilní verze" 73msgstr "poslední stabilní verze"
42 74
43msgid "a more recent stable version is available." 75#, fuzzy
76msgid "Latest stable version"
77msgstr "poslední stabilní verze"
78
79#, fuzzy
80msgid "A more recent stable version is available."
44msgstr "je k dispozici novější stabilní verze." 81msgstr "je k dispozici novější stabilní verze."
45 82
46msgid "you are up to date." 83#, fuzzy
84msgid "You are up to date."
47msgstr "je aktuální" 85msgstr "je aktuální"
48 86
49msgid "latest dev version" 87#, fuzzy
88msgid "Latest dev version"
50msgstr "poslední vývojová verze" 89msgstr "poslední vývojová verze"
51 90
52msgid "a more recent development version is available." 91#, fuzzy
92msgid "A more recent development version is available."
53msgstr "je k dispozici novější vývojová verze." 93msgstr "je k dispozici novější vývojová verze."
54 94
95msgid "Feeds"
96msgstr ""
97
98msgid "Your feed token is currently empty and must first be generated to enable feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
99msgstr ""
100
101msgid "Unread feed"
102msgstr ""
103
104#, fuzzy
105msgid "Favorites feed"
106msgstr "oblíbené"
107
108#, fuzzy
109msgid "Archive feed"
110msgstr "archív"
111
112msgid "Your token:"
113msgstr ""
114
115msgid "Your user id:"
116msgstr ""
117
118msgid "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>."
119msgstr ""
120
121#, fuzzy
122msgid "Change your theme"
123msgstr "Změnit heslo"
124
125msgid "Theme:"
126msgstr ""
127
128msgid "Update"
129msgstr "Aktualizovat"
130
131#, fuzzy
132msgid "Change your language"
133msgstr "Změnit heslo"
134
135msgid "Language:"
136msgstr ""
137
55msgid "Change your password" 138msgid "Change your password"
56msgstr "Změnit heslo" 139msgstr "Změnit heslo"
57 140
@@ -64,65 +147,68 @@ msgstr "Heslo"
64msgid "Repeat your new password:" 147msgid "Repeat your new password:"
65msgstr "Znovu nové heslo:" 148msgstr "Znovu nové heslo:"
66 149
67msgid "Update"
68msgstr "Aktualizovat"
69
70msgid "Import" 150msgid "Import"
71msgstr "Importovat" 151msgstr "Importovat"
72 152
73msgid "Please execute the import script locally, it can take a very long time." 153#, fuzzy
154msgid "Please execute the import script locally as it can take a very long time."
74msgstr "Spusťte importní skript lokálně, může to dlouho trvat." 155msgstr "Spusťte importní skript lokálně, může to dlouho trvat."
75 156
76msgid "More info in the official doc:" 157#, fuzzy
158msgid "More info in the official documentation:"
77msgstr "Více informací v oficiální dokumentaci:" 159msgstr "Více informací v oficiální dokumentaci:"
78 160
79msgid "import from Pocket" 161#, fuzzy
162msgid "Import from Pocket"
80msgstr "importovat z Pocket" 163msgstr "importovat z Pocket"
81 164
82msgid "import from Readability" 165#, php-format
166msgid "(you must have a %s file on your server)"
167msgstr ""
168
169#, fuzzy
170msgid "Import from Readability"
83msgstr "importovat z Readability" 171msgstr "importovat z Readability"
84 172
85msgid "import from Instapaper" 173#, fuzzy
174msgid "Import from Instapaper"
86msgstr "importovat z Instapaper" 175msgstr "importovat z Instapaper"
87 176
88msgid "Export your poche data" 177#, fuzzy
178msgid "Import from wallabag"
179msgstr "importovat z Readability"
180
181#, fuzzy
182msgid "Export your wallabag data"
89msgstr "Export dat" 183msgstr "Export dat"
90 184
91msgid "Click here" 185msgid "Click here"
92msgstr "Klikněte zde" 186msgstr "Klikněte zde"
93 187
94msgid "to export your poche data." 188msgid "to download your database."
95msgstr "pro export vašich dat." 189msgstr ""
96
97msgid "back to home"
98msgstr "zpět na úvod"
99 190
100msgid "installation" 191#, fuzzy
101msgstr "instalace" 192msgid "to export your wallabag data."
193msgstr "pro export vašich dat."
102 194
103msgid "install your poche" 195msgid "Cache"
104msgstr "instalovat" 196msgstr ""
105 197
106msgid "" 198msgid "to delete cache."
107"poche is still not installed. Please fill the below form to install it. "
108"Don't hesitate to <a href='http://doc.inthepoche.com'>read the documentation "
109"on poche website</a>."
110msgstr "" 199msgstr ""
111"poche ještě není nainstalováno. Pro instalaci vyplňte níže uvedený formulář. "
112"Nezapomeňte <a href='http://doc.inthepoche.com'>si přečíst dokumentaci</a> "
113"na stránkách programu."
114 200
115msgid "Login" 201msgid "You can enter multiple tags, separated by commas."
116msgstr "Jméno" 202msgstr ""
117 203
118msgid "Repeat your password" 204msgid "return to article"
119msgstr "Zopakujte heslo" 205msgstr ""
120 206
121msgid "Install" 207msgid "plop"
122msgstr "Instalovat" 208msgstr ""
123 209
124msgid "back to top" 210msgid "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>."
125msgstr "zpět na začátek" 211msgstr ""
126 212
127msgid "favoris" 213msgid "favoris"
128msgstr "oblíbené" 214msgstr "oblíbené"
@@ -151,10 +237,14 @@ msgstr "podle nadpisu"
151msgid "by title desc" 237msgid "by title desc"
152msgstr "podle nadpisu sestupně" 238msgstr "podle nadpisu sestupně"
153 239
154msgid "No link available here!" 240msgid "Tag"
155msgstr "Není k dispozici žádný odkaz!" 241msgstr ""
156 242
157msgid "toggle mark as read" 243msgid "No articles found."
244msgstr ""
245
246#, fuzzy
247msgid "Toggle mark as read"
158msgstr "označit jako přečtené" 248msgstr "označit jako přečtené"
159 249
160msgid "toggle favorite" 250msgid "toggle favorite"
@@ -166,13 +256,95 @@ msgstr "smazat"
166msgid "original" 256msgid "original"
167msgstr "originál" 257msgstr "originál"
168 258
259msgid "estimated reading time:"
260msgstr ""
261
262msgid "mark all the entries as read"
263msgstr ""
264
169msgid "results" 265msgid "results"
170msgstr "výsledky" 266msgstr "výsledky"
171 267
172msgid "tweet" 268msgid "installation"
269msgstr "instalace"
270
271#, fuzzy
272msgid "install your wallabag"
273msgstr "instalovat"
274
275#, fuzzy
276msgid "wallabag is still not installed. Please fill the below form to install it. Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation on wallabag website</a>."
277msgstr "poche ještě není nainstalováno. Pro instalaci vyplňte níže uvedený formulář. Nezapomeňte <a href='http://doc.inthepoche.com'>si přečíst dokumentaci</a> na stránkách programu."
278
279msgid "Login"
280msgstr "Jméno"
281
282msgid "Repeat your password"
283msgstr "Zopakujte heslo"
284
285msgid "Install"
286msgstr "Instalovat"
287
288#, fuzzy
289msgid "login to your wallabag"
290msgstr "přihlásit se k poche"
291
292msgid "Login to wallabag"
293msgstr ""
294
295msgid "you are in demo mode, some features may be disabled."
296msgstr "používáte ukázkový mód, některé funkce jsou zakázány."
297
298msgid "Username"
299msgstr ""
300
301msgid "Stay signed in"
302msgstr "Zůstat přihlášen(a)"
303
304msgid "(Do not check on public computers)"
305msgstr "(Nezaškrtávejte na veřejně dostupných počítačích)"
306
307msgid "Sign in"
308msgstr "Přihlásit se"
309
310msgid "favorites"
311msgstr "oblíbené"
312
313msgid "estimated reading time :"
314msgstr ""
315
316msgid "Mark all the entries as read"
317msgstr ""
318
319msgid "Return home"
320msgstr ""
321
322#, fuzzy
323msgid "Back to top"
324msgstr "zpět na začátek"
325
326#, fuzzy
327msgid "Mark as read"
328msgstr "označit jako přečtené"
329
330#, fuzzy
331msgid "Favorite"
332msgstr "oblíbené"
333
334#, fuzzy
335msgid "Toggle favorite"
336msgstr "označit jako oblíbené"
337
338#, fuzzy
339msgid "Delete"
340msgstr "smazat"
341
342#, fuzzy
343msgid "Tweet"
173msgstr "tweetnout" 344msgstr "tweetnout"
174 345
175msgid "email" 346#, fuzzy
347msgid "Email"
176msgstr "email" 348msgstr "email"
177 349
178msgid "shaarli" 350msgid "shaarli"
@@ -181,26 +353,24 @@ msgstr "shaarli"
181msgid "flattr" 353msgid "flattr"
182msgstr "flattr" 354msgstr "flattr"
183 355
184msgid "this article appears wrong?" 356#, fuzzy
357msgid "Does this article appear wrong?"
185msgstr "vypadá tento článek špatně?" 358msgstr "vypadá tento článek špatně?"
186 359
187msgid "create an issue" 360msgid "tags:"
188msgstr "odeslat požadavek" 361msgstr ""
189
190msgid "or"
191msgstr "nebo"
192 362
193msgid "contact us by mail" 363msgid "Edit tags"
194msgstr "kontaktovat e-mailem" 364msgstr ""
195 365
196msgid "plop" 366msgid "save link!"
197msgstr "" 367msgstr ""
198 368
199msgid "home" 369msgid "home"
200msgstr "domů" 370msgstr "domů"
201 371
202msgid "favorites" 372msgid "tags"
203msgstr "oblíbené" 373msgstr ""
204 374
205msgid "logout" 375msgid "logout"
206msgstr "odhlásit se" 376msgstr "odhlásit se"
@@ -211,23 +381,187 @@ msgstr "běží na"
211msgid "debug mode is on so cache is off." 381msgid "debug mode is on so cache is off."
212msgstr "je zapnut ladicí mód, proto je keš vypnuta." 382msgstr "je zapnut ladicí mód, proto je keš vypnuta."
213 383
214msgid "your poche version:" 384#, fuzzy
215msgstr "verze:" 385msgid "your wallabag version:"
386msgstr "vaše verze"
216 387
217msgid "storage:" 388msgid "storage:"
218msgstr "úložiště:" 389msgstr "úložiště:"
219 390
220msgid "login to your poche" 391msgid "save a link"
221msgstr "přihlásit se k poche" 392msgstr ""
222 393
223msgid "you are in demo mode, some features may be disabled." 394msgid "back to home"
224msgstr "používáte ukázkový mód, některé funkce jsou zakzány." 395msgstr "zpět na vod"
225 396
226msgid "Stay signed in" 397msgid "toggle mark as read"
227msgstr "Zůstat přihlášen(a)" 398msgstr "označit jako přečtené"
228 399
229msgid "(Do not check on public computers)" 400msgid "tweet"
230msgstr "(Nezaškrtávejte na veřejně dostupných počítačích)" 401msgstr "tweetnout"
231 402
232msgid "Sign in" 403msgid "email"
233msgstr "Přihlásit se" 404msgstr "email"
405
406msgid "this article appears wrong?"
407msgstr "vypadá tento článek špatně?"
408
409msgid "No link available here!"
410msgstr "Není k dispozici žádný odkaz!"
411
412msgid "Poching a link"
413msgstr "Odkaz se ukládá"
414
415msgid "by filling this field"
416msgstr "vyplněním tohoto pole"
417
418msgid "bookmarklet: drag & drop this link to your bookmarks bar"
419msgstr ""
420
421msgid "your version"
422msgstr "vaše verze"
423
424msgid "latest stable version"
425msgstr "poslední stabilní verze"
426
427msgid "a more recent stable version is available."
428msgstr "je k dispozici novější stabilní verze."
429
430msgid "you are up to date."
431msgstr "je aktuální"
432
433msgid "latest dev version"
434msgstr "poslední vývojová verze"
435
436msgid "a more recent development version is available."
437msgstr "je k dispozici novější vývojová verze."
438
439msgid "Please execute the import script locally, it can take a very long time."
440msgstr "Spusťte importní skript lokálně, může to dlouho trvat."
441
442#, fuzzy
443msgid "More infos in the official doc:"
444msgstr "Více informací v oficiální dokumentaci:"
445
446msgid "import from Pocket"
447msgstr "importovat z Pocket"
448
449msgid "import from Readability"
450msgstr "importovat z Readability"
451
452msgid "import from Instapaper"
453msgstr "importovat z Instapaper"
454
455msgid "Tags"
456msgstr ""
457
458#, fuzzy
459msgid "Untitled"
460msgstr "podle nadpisu"
461
462msgid "the link has been added successfully"
463msgstr ""
464
465msgid "error during insertion : the link wasn't added"
466msgstr ""
467
468msgid "the link has been deleted successfully"
469msgstr ""
470
471msgid "the link wasn't deleted"
472msgstr ""
473
474msgid "Article not found!"
475msgstr ""
476
477msgid "previous"
478msgstr ""
479
480msgid "next"
481msgstr ""
482
483msgid "in demo mode, you can't update your password"
484msgstr ""
485
486msgid "your password has been updated"
487msgstr ""
488
489msgid "the two fields have to be filled & the password must be the same in the two fields"
490msgstr ""
491
492msgid "still using the \""
493msgstr ""
494
495msgid "that theme does not seem to be installed"
496msgstr ""
497
498msgid "you have changed your theme preferences"
499msgstr ""
500
501msgid "that language does not seem to be installed"
502msgstr ""
503
504msgid "you have changed your language preferences"
505msgstr ""
506
507msgid "login failed: you have to fill all fields"
508msgstr ""
509
510msgid "welcome to your wallabag"
511msgstr ""
512
513msgid "login failed: bad login or password"
514msgstr ""
515
516#, fuzzy
517msgid "import from instapaper completed"
518msgstr "importovat z Instapaper"
519
520#, fuzzy
521msgid "import from pocket completed"
522msgstr "importovat z Pocket"
523
524#, fuzzy
525msgid "import from Readability completed. "
526msgstr "importovat z Readability"
527
528#, fuzzy
529msgid "import from Poche completed. "
530msgstr "importovat z Pocket"
531
532msgid "Unknown import provider."
533msgstr ""
534
535msgid "Incomplete inc/poche/define.inc.php file, please define \""
536msgstr ""
537
538msgid "Could not find required \""
539msgstr ""
540
541msgid "Uh, there is a problem while generating feeds."
542msgstr ""
543
544#, fuzzy
545msgid "Cache deleted."
546msgstr "smazat"
547
548msgid "Oops, it seems you don't have PHP 5."
549msgstr ""
550
551#~ msgid "poche it!"
552#~ msgstr "uložit!"
553
554#~ msgid "Updating poche"
555#~ msgstr "Poche se aktualizuje"
556
557#~ msgid "create an issue"
558#~ msgstr "odeslat požadavek"
559
560#~ msgid "or"
561#~ msgstr "nebo"
562
563#~ msgid "contact us by mail"
564#~ msgstr "kontaktovat e-mailem"
565
566#~ msgid "your poche version:"
567#~ msgstr "verze:"
diff --git a/locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.mo b/locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.mo
index 375e923f..bd18817f 100644
--- a/locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.mo
+++ b/locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.mo
Binary files differ
diff --git a/locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.po b/locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.po
index 5b30d3d7..8b82721d 100644
--- a/locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.po
+++ b/locale/de_DE.utf8/LC_MESSAGES/de_DE.utf8.po
@@ -1,51 +1,130 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: \n" 3"Project-Id-Version: Wallabag\n"
4"POT-Creation-Date: \n" 4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2014-03-27 13:41+0100\n"
5"PO-Revision-Date: \n" 6"PO-Revision-Date: \n"
6"Last-Translator: Square252\n" 7"Last-Translator: Kevin Meyer <wallabag@kevin-meyer.de>\n"
7"Language-Team: \n" 8"Language-Team: \n"
9"Language: de\n"
8"MIME-Version: 1.0\n" 10"MIME-Version: 1.0\n"
9"Content-Type: text/plain; charset=UTF-8\n" 11"Content-Type: text/plain; charset=UTF-8\n"
10"Content-Transfer-Encoding: 8bit\n" 12"Content-Transfer-Encoding: 8bit\n"
11"X-Generator: Poedit 1.5.7\n" 13"X-Generator: Poedit 1.6.4\n"
14"X-Poedit-Basepath: .\n"
15"X-Poedit-SearchPath-0: /Users/kevinmeyer/Dropbox/dev_web/wallabag-dev\n"
12 16
13msgid "config" 17msgid "config"
14msgstr "Konfiguration" 18msgstr "Konfiguration"
15 19
16msgid "Poching a link" 20msgid "Saving articles"
17msgstr "Poche einen Link" 21msgstr "Artikel speichern"
22
23msgid "There are several ways to save an article:"
24msgstr "Es gibt viele Methoden um Artikel zu speichern:"
18 25
19msgid "read the documentation" 26msgid "read the documentation"
20msgstr "Die Dokumentation lesen" 27msgstr "Die Dokumentation lesen"
21 28
22msgid "by filling this field" 29msgid "download the extension"
23msgstr "durch das ausfüllen dieses Feldes:" 30msgstr "installiere die Erweiterung"
31
32msgid "via F-Droid"
33msgstr "via F-Droid"
34
35msgid " or "
36msgstr " oder "
37
38msgid "via Google Play"
39msgstr "via Google Play"
40
41msgid "download the application"
42msgstr "lade die App"
43
44msgid "By filling this field"
45msgstr "Durch Ausfüllen dieses Feldes"
24 46
25msgid "poche it!" 47msgid "bag it!"
26msgstr "Poche es!" 48msgstr "bag it!"
27 49
28msgid "Updating poche" 50msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
29msgstr "Poche aktualisieren" 51msgstr "Bookmarklet: Ziehe diesen Link in deine Lesezeichen-Leiste"
30 52
31msgid "your version" 53msgid "Upgrading wallabag"
32msgstr "Deine Version" 54msgstr "wallabag aktualisieren"
33 55
34msgid "latest stable version" 56msgid "Installed version"
57msgstr "Installierte Version"
58
59msgid "Latest stable version"
35msgstr "Neuste stabile Version" 60msgstr "Neuste stabile Version"
36 61
37msgid "a more recent stable version is available." 62msgid "A more recent stable version is available."
38msgstr "Eine neuere stabile Version ist verfügbar." 63msgstr "Eine neuere stabile Version ist verfügbar."
39 64
40msgid "you are up to date." 65msgid "You are up to date."
41msgstr "Du bist auf den neuesten Stand." 66msgstr "Du bist auf den neuesten Stand."
42 67
43msgid "latest dev version" 68msgid "Last check:"
69msgstr "Zuletzt geprüft:"
70
71msgid "Latest dev version"
44msgstr "Neuste Entwicklungsversion" 72msgstr "Neuste Entwicklungsversion"
45 73
46msgid "a more recent development version is available." 74msgid "A more recent development version is available."
47msgstr "Eine neuere Entwicklungsversion ist verfügbar." 75msgstr "Eine neuere Entwicklungsversion ist verfügbar."
48 76
77msgid "You can clear cache to check the latest release."
78msgstr "Leere den Cache um die neueste Version zu prüfen."
79
80msgid "Feeds"
81msgstr "Feeds"
82
83msgid ""
84"Your feed token is currently empty and must first be generated to enable "
85"feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
86msgstr ""
87"Dein Feed Token ist noch nicht vorhanden und muss zunächst generiert werden, "
88"um deine Feeds zu aktivieren. Klicke <a href='?feed&amp;"
89"action=generate'>hier um ihn zu generieren</a>."
90
91msgid "Unread feed"
92msgstr "Ungelesen Feed"
93
94msgid "Favorites feed"
95msgstr "Favoriten Feed"
96
97msgid "Archive feed"
98msgstr "Archiv Feed"
99
100msgid "Your token:"
101msgstr "Dein Token:"
102
103msgid "Your user id:"
104msgstr "Deine User ID:"
105
106msgid ""
107"You can regenerate your token: <a href='?feed&amp;action=generate'>generate!"
108"</a>."
109msgstr ""
110"Hier kannst du dein Token erzeugen: <a href='?feed&amp;"
111"action=generate'>Generieren!</a>."
112
113msgid "Change your theme"
114msgstr "Theme ändern"
115
116msgid "Theme:"
117msgstr "Theme:"
118
119msgid "Update"
120msgstr "Aktualisieren"
121
122msgid "Change your language"
123msgstr "Sprache ändern"
124
125msgid "Language:"
126msgstr "Sprache:"
127
49msgid "Change your password" 128msgid "Change your password"
50msgstr "Passwort ändern" 129msgstr "Passwort ändern"
51 130
@@ -58,75 +137,86 @@ msgstr "Passwort"
58msgid "Repeat your new password:" 137msgid "Repeat your new password:"
59msgstr "Neues Passwort wiederholen:" 138msgstr "Neues Passwort wiederholen:"
60 139
61msgid "Update"
62msgstr "Aktualisieren"
63
64msgid "Import" 140msgid "Import"
65msgstr "Import" 141msgstr "Import"
66 142
67msgid "Please execute the import script locally, it can take a very long time." 143msgid ""
68msgstr "Bitte führe das Import Script lokal aus, dies kann eine Weile dauern." 144"Importing from other services can be quite long, and webservers default "
145"configuration often prevents long scripts execution time, so it must be done "
146"in multiple parts."
147msgstr ""
148"Der Import von anderen Diensten kann sehr lange dauern. Deswegen bricht der "
149"Webserver diesen in vielen Konfigurationen ab. Daher muss der Import in "
150"mehrere Teile aufgeteilt werden."
151
152msgid "First, select the export file on your computer and upload it."
153msgstr "Wähle eine Datei von deinem Computer aus und lade sie hoch."
154
155msgid "File:"
156msgstr "Datei:"
157
158msgid "Upload"
159msgstr "Hochladen"
69 160
70msgid "More info in the official doc:" 161msgid "Then, click on the right link below."
71msgstr "Mehr Informationen in der offiziellen Dokumentation:" 162msgstr "Klicke dann unten auf den entsprechenden Link."
72 163
73msgid "import from Pocket" 164msgid "Import from Pocket"
74msgstr "Import aus Pocket" 165msgstr "Import aus Pocket"
75 166
76msgid "import from Readability" 167#, php-format
168msgid "(after uploaded %s file)"
169msgstr "(nach Upload der Datei %s)"
170
171msgid "Import from Readability"
77msgstr "Import aus Readability" 172msgstr "Import aus Readability"
78 173
79msgid "import from Instapaper" 174msgid "Import from Instapaper"
80msgstr "Import aus Instapaper" 175msgstr "Import aus Instapaper"
81 176
82msgid "Export your poche data" 177msgid "Import from wallabag"
83msgstr "Exportieren Sie Ihre Poche Daten." 178msgstr "Import aus Readability"
84
85msgid "Click here"
86msgstr "Klicke hier"
87 179
88msgid "to export your poche data." 180msgid ""
89msgstr "um deine Daten aus Poche zu exportieren." 181"3. Your feed token is currently empty and must first be generated to fetch "
182"content. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
183msgstr ""
184"3. Dein Feed Token ist noch nicht vorhanden und muss zunächst generiert "
185"werden, um Inhalt abrufen zu können. Klicke <a href='?feed&amp;"
186"action=generate'>hier um ihn zu generieren</a>."
90 187
91msgid "back to home" 188msgid "Finally, you have to fetch content for imported items."
92msgstr "züruck zur Hauptseite" 189msgstr "Jetzt musst du den Inhalt der importierten Artikel abrufen."
93 190
94msgid "installation" 191msgid "Click here"
95msgstr "Installieren" 192msgstr "Klicke hier"
96 193
97msgid "install your poche" 194msgid "to fetch content for 10 articles"
98msgstr "Installiere dein Poche" 195msgstr "um den Inhalt von 10 Artikeln abzurufen"
99 196
100msgid "" 197msgid ""
101"poche is still not installed. Please fill the below form to install it. " 198"If you have console access to your server, you can also create a cron task:"
102"Don't hesitate to <a href='http://doc.inthepoche.com'>read the documentation "
103"on poche website</a>."
104msgstr "" 199msgstr ""
105"Poche ist noch nicht installiert. Bitte fülle die Felder unten aus, um die " 200"Wenn du Konsolenzugang zu deinem Server hast kannst du auch einen cron "
106"Installation durchzuführen. Zögere nicht, <a href='http://inthepoche.com/" 201"erstellen:"
107"doc'>die Dokumentation auf der Website von Poche zu lesen falls du Probleme "
108"haben solltest."
109 202
110msgid "Login" 203msgid "Export your wallabag data"
111msgstr "Benutzername" 204msgstr "Exportieren deine wallabag Daten"
112 205
113msgid "Repeat your password" 206msgid "to download your database."
114msgstr "Wiederhole dein Passwort" 207msgstr "um deine Datenbank herunterzuladen"
115 208
116msgid "Install" 209msgid "to export your wallabag data."
117msgstr "Installieren" 210msgstr "um deine Daten aus wallabag zu exportieren."
118 211
119msgid "back to top" 212msgid "Cache"
120msgstr "Nach Oben" 213msgstr "Cache"
121 214
122msgid "favoris" 215msgid "to delete cache."
123msgstr "" 216msgstr "um den Cache zu löschen."
124
125msgid "archive"
126msgstr "Archiv"
127 217
128msgid "unread" 218msgid "Tags"
129msgstr "ungelesen" 219msgstr "Tags"
130 220
131msgid "by date asc" 221msgid "by date asc"
132msgstr "nach Datum aufsteigend" 222msgstr "nach Datum aufsteigend"
@@ -146,10 +236,72 @@ msgstr "nach Titel"
146msgid "by title desc" 236msgid "by title desc"
147msgstr "nach Titel absteigend" 237msgstr "nach Titel absteigend"
148 238
149msgid "No link available here!" 239#, fuzzy
150msgstr "Kein Link verfügbar!" 240msgid "toggle view mode"
241msgstr "Favorit"
242
243msgid "home"
244msgstr "Start"
245
246msgid "favorites"
247msgstr "Favoriten"
248
249msgid "archive"
250msgstr "Archiv"
251
252msgid "tags"
253msgstr "Tags"
254
255msgid "save a link"
256msgstr "Speichere einen Link"
257
258msgid "search"
259msgstr "Suche"
260
261msgid "logout"
262msgstr "Logout"
263
264msgid "return home"
265msgstr "Zurück zum Start"
266
267#, fuzzy
268msgid "Search"
269msgstr "Archiv"
270
271msgid "powered by"
272msgstr "bereitgestellt von"
273
274msgid "debug mode is on so cache is off."
275msgstr "Debug Modus ist aktiviert, das Caching ist somit deaktiviert"
276
277msgid "your wallabag version:"
278msgstr "Deine wallabag Version"
279
280msgid "storage:"
281msgstr "Speicher:"
282
283msgid "Save a link"
284msgstr "Speichere einen Link"
285
286msgid "save link!"
287msgstr "Link speichern!"
288
289msgid "unread"
290msgstr "ungelesen"
291
292msgid "Tag"
293msgstr "Tag"
294
295msgid "No articles found."
296msgstr "Keine Artikel gefunden."
151 297
152msgid "toggle mark as read" 298msgid "estimated reading time:"
299msgstr "geschätzte Lesezeit:"
300
301msgid "estimated reading time :"
302msgstr "geschätzte Lesezeit:"
303
304msgid "Toggle mark as read"
153msgstr "Als gelesen markieren" 305msgstr "Als gelesen markieren"
154 306
155msgid "toggle favorite" 307msgid "toggle favorite"
@@ -161,69 +313,346 @@ msgstr "Löschen"
161msgid "original" 313msgid "original"
162msgstr "Original" 314msgstr "Original"
163 315
316msgid "Mark all the entries as read"
317msgstr "Markiere alle als gelesen"
318
164msgid "results" 319msgid "results"
165msgstr "Ergebnisse" 320msgstr "Ergebnisse"
166 321
167msgid "tweet" 322msgid "Uh, there is a problem with the cron."
168msgstr "Twittern" 323msgstr "Oh, es gab ein Problem mit dem cron."
169 324
170msgid "email" 325msgid "Untitled"
171msgstr "senden per E-Mail" 326msgstr "Ohne Titel"
172 327
173msgid "shaarli" 328msgid "the link has been added successfully"
174msgstr "Shaarli" 329msgstr "Speichern des Links erfolgreich"
175 330
176msgid "flattr" 331msgid "error during insertion : the link wasn't added"
177msgstr "flattr" 332msgstr "Fehler beim Einfügen: Der Link wurde nicht hinzugefügt"
178 333
179msgid "this article appears wrong?" 334msgid "the link has been deleted successfully"
180msgstr "dieser Artikel erscheint falsch?" 335msgstr "Löschen des Links erfolgreich"
181 336
182msgid "create an issue" 337msgid "the link wasn't deleted"
183msgstr "ein Ticket erstellen" 338msgstr "Der Link wurde nicht entfernt"
184 339
185msgid "or" 340msgid "Article not found!"
186msgstr "oder" 341msgstr "Artikel nicht gefunden!"
187 342
188msgid "contact us by mail" 343msgid "previous"
189msgstr "kontaktieren Sie uns per E-Mail" 344msgstr "vorherige"
190 345
191msgid "plop" 346msgid "next"
192msgstr "plop" 347msgstr "nächste"
193 348
194msgid "home" 349msgid "in demo mode, you can't update your password"
195msgstr "Start" 350msgstr "im Demo-Modus kann das Passwort nicht geändert werden"
196 351
197msgid "favorites" 352msgid "your password has been updated"
198msgstr "Favoriten" 353msgstr "Dein Passwort wurde geändert"
199 354
200msgid "logout" 355msgid ""
201msgstr "Logout" 356"the two fields have to be filled & the password must be the same in the two "
357"fields"
358msgstr "Beide Felder müssen mit selbem Inhalt ausgefüllt sein"
202 359
203msgid "powered by" 360msgid "still using the \""
204msgstr "bereitgestellt von" 361msgstr "nutze immernoch die \""
205 362
206msgid "debug mode is on so cache is off." 363msgid "that theme does not seem to be installed"
207msgstr "Debug Modus ist aktiviert, das Caching ist somit deaktiviert" 364msgstr "dieses Theme scheint nicht installiert zu sein"
208 365
209msgid "your poche version:" 366msgid "you have changed your theme preferences"
210msgstr "Deine Poche Version" 367msgstr "Du hast deine Theme Einstellungen geändert"
211 368
212msgid "storage:" 369msgid "that language does not seem to be installed"
213msgstr "Speicher:" 370msgstr "Diese Sprache scheint nicht installiert zu sein"
371
372msgid "you have changed your language preferences"
373msgstr "Du hast deine Spracheinstellungen geändert"
374
375msgid "login failed: you have to fill all fields"
376msgstr "Anmeldung fehlgeschlagen: Alle Felder müssen ausgefüllt werden"
377
378msgid "welcome to your wallabag"
379msgstr "Willkommen bei deiner wallabag"
380
381msgid "login failed: bad login or password"
382msgstr "Anmeldung fehlgeschlagen: Falscher Benutzername oder Passwort"
383
384msgid ""
385"import from instapaper completed. You have to execute the cron to fetch "
386"content."
387msgstr ""
388"Import aus Instapaper vollständig. Führe den cronjob aus um den Inhalt "
389"abzurufen."
390
391msgid ""
392"import from pocket completed. You have to execute the cron to fetch content."
393msgstr ""
394"Import aus Pocket vollständig. Führe den cronjob aus um den Inhalt abzurufen."
214 395
215msgid "login to your poche" 396msgid ""
216msgstr "Bei Poche anmelden" 397"import from Readability completed. You have to execute the cron to fetch "
398"content."
399msgstr ""
400"Import aus Readability vollständig. Führe den cronjob aus um den Inhalt "
401"abzurufen."
217 402
218msgid "you are in demo mode, some features may be disabled." 403msgid ""
404"import from Poche completed. You have to execute the cron to fetch content."
219msgstr "" 405msgstr ""
220"Du befindest dich im Demomodus, einige Funktionen könnten deaktiviert sein." 406"Import aus Poche vollständig. Führe den cronjob aus um den Inhalt abzurufen."
407
408msgid "Unknown import provider."
409msgstr "Unbekannter Import Anbieter."
410
411msgid "Could not find required \""
412msgstr "Nicht gefunden: \""
413
414msgid "File uploaded. You can now execute import."
415msgstr "Datei hochgeladen. Du kannst nun importieren."
416
417msgid "Error while importing file. Do you have access to upload it?"
418msgstr "Fehler beim Importieren. Hast du das Recht zum Hochladen?"
419
420msgid "User with this id ("
421msgstr "Nutzer mit dieser id ("
422
423msgid "Uh, there is a problem while generating feeds."
424msgstr "Oh, es gab ein Problem beim Erstellen des Feeds."
425
426msgid "Cache deleted."
427msgstr "Cache gelöscht"
428
429msgid "Oops, it seems you don't have PHP 5."
430msgstr "Oops, es scheint als würde PHP 5 fehlen."
431
432msgid "wallabag, a read it later open source system"
433msgstr "wallabag, ein Später-Lesen Open Source System"
434
435msgid "login failed: user doesn't exist"
436msgstr "Anmeldung fehlgeschlagen: Benutzer existiert nicht"
437
438#~ msgid "You can enter multiple tags, separated by commas."
439#~ msgstr "Du kannst mehrere Tags, durch Kommata getrennt, eingeben."
440
441#~ msgid "return to article"
442#~ msgstr "zurück zum Artikel"
443
444#, fuzzy
445#~ msgid "favoris"
446#~ msgstr "Favoriten"
447
448#~ msgid "mark all the entries as read"
449#~ msgstr "Markiere alle als gelesen"
450
451#~ msgid "Back to top"
452#~ msgstr "Nach Oben"
453
454#~ msgid "Mark as read"
455#~ msgstr "Als gelesen markieren"
456
457#~ msgid "Favorite"
458#~ msgstr "Favoriten"
459
460#~ msgid "Toggle favorite"
461#~ msgstr "Favorit"
462
463#~ msgid "Delete"
464#~ msgstr "Löschen"
465
466#~ msgid "Tweet"
467#~ msgstr "Twittern"
468
469#~ msgid "Email"
470#~ msgstr "per E-Mail senden"
471
472#~ msgid "shaarli"
473#~ msgstr "Shaarli"
474
475#~ msgid "flattr"
476#~ msgstr "flattr"
477
478#~ msgid "Does this article appear wrong?"
479#~ msgstr "Erscheint dieser Artikel falsch?"
480
481#~ msgid "Edit tags"
482#~ msgstr "Tags bearbeiten"
483
484#~ msgid "Start typing for auto complete."
485#~ msgstr "Beginne zu tippen für die Autovervollständigung."
486
487#~ msgid "Return home"
488#~ msgstr "Zurück zum Start"
489
490#~ msgid "tags:"
491#~ msgstr "Tags:"
492
493#~ msgid "login to your wallabag"
494#~ msgstr "Bei wallabag anmelden"
495
496#~ msgid "you are in demo mode, some features may be disabled."
497#~ msgstr ""
498#~ "Du befindest dich im Demomodus, einige Funktionen könnten deaktiviert "
499#~ "sein."
500
501#~ msgid "Login"
502#~ msgstr "Benutzername"
503
504#~ msgid "Stay signed in"
505#~ msgstr "Angemeldet bleiben"
506
507#~ msgid "(Do not check on public computers)"
508#~ msgstr "(nicht auf einem öffentlichen Computer anhaken)"
509
510#~ msgid "plop"
511#~ msgstr "plop"
512
513#~ msgid "Login to wallabag"
514#~ msgstr "Bei wallabag anmelden"
515
516#~ msgid "Username"
517#~ msgstr "Benutzername"
518
519#~ msgid "Sign in"
520#~ msgstr "Einloggen"
521
522#~ msgid "Enter your search here"
523#~ msgstr "Gib hier deine Suchanfrage ein"
524
525#~ msgid "installation"
526#~ msgstr "Installieren"
527
528#~ msgid "install your wallabag"
529#~ msgstr "Installiere deine wallabag"
530
531#~ msgid ""
532#~ "wallabag is still not installed. Please fill the below form to install "
533#~ "it. Don't hesitate to <a href='http://doc.wallabag.org/'>read the "
534#~ "documentation on wallabag website</a>."
535#~ msgstr ""
536#~ "wallabag ist noch nicht installiert. Bitte fülle die Felder unten aus, um "
537#~ "die Installation durchzuführen. Zögere nicht, <a href='http://doc."
538#~ "wallabag.org/'>die Dokumentation auf der Website von wallabag zu lesen, "
539#~ "falls du Probleme haben solltest."
540
541#~ msgid "Repeat your password"
542#~ msgstr "Wiederhole dein Passwort"
543
544#~ msgid "Install"
545#~ msgstr "Installieren"
546
547#~ msgid "No link available here!"
548#~ msgstr "Kein Link verfügbar!"
549
550#~ msgid "toggle mark as read"
551#~ msgstr "Als gelesen markieren"
552
553#~ msgid ""
554#~ "You can <a href='wallabag_compatibility_test.php'>check your "
555#~ "configuration here</a>."
556#~ msgstr ""
557#~ "Du kannst deine Konfiguration <a href='wallabag_compatibility_test."
558#~ "php'>hier testen</a>."
559
560#~ msgid "back to home"
561#~ msgstr "züruck zur Hauptseite"
562
563#~ msgid "tweet"
564#~ msgstr "Twittern"
565
566#~ msgid "email"
567#~ msgstr "senden per E-Mail"
568
569#~ msgid "this article appears wrong?"
570#~ msgstr "dieser Artikel erscheint falsch?"
571
572#~ msgid "Poching a link"
573#~ msgstr "Poche einen Link"
574
575#~ msgid "by filling this field"
576#~ msgstr "durch das ausfüllen dieses Feldes:"
577
578#~ msgid "bookmarklet: drag & drop this link to your bookmarks bar"
579#~ msgstr "Bookmarklet: Ziehe diesen Link in deine Lesezeichen-Leiste"
580
581#~ msgid "your version"
582#~ msgstr "Deine Version"
583
584#~ msgid "latest stable version"
585#~ msgstr "Neuste stabile Version"
586
587#~ msgid "a more recent stable version is available."
588#~ msgstr "Eine neuere stabile Version ist verfügbar."
589
590#~ msgid "you are up to date."
591#~ msgstr "Du bist auf den neuesten Stand."
592
593#~ msgid "latest dev version"
594#~ msgstr "Neuste Entwicklungsversion"
595
596#~ msgid "a more recent development version is available."
597#~ msgstr "Eine neuere Entwicklungsversion ist verfügbar."
598
599#~ msgid ""
600#~ "Please execute the import script locally, it can take a very long time."
601#~ msgstr ""
602#~ "Bitte führe das Import Script lokal aus, dies kann eine Weile dauern."
603
604#~ msgid "More infos in the official doc:"
605#~ msgstr "Mehr Informationen in der offiziellen Dokumentation:"
606
607#~ msgid "import from Pocket"
608#~ msgstr "Import aus Pocket"
609
610#~ msgid "(you must have a %s file on your server)"
611#~ msgstr "(du brauchst eine %s Datei auf deinem Server)"
612
613#~ msgid "import from Readability"
614#~ msgstr "Import aus Readability"
615
616#~ msgid "import from Instapaper"
617#~ msgstr "Import aus Instapaper"
618
619#~ msgid "You can also create a cron task:"
620#~ msgstr "Du kannst auch einen cronjob anlegen:"
621
622#~ msgid ""
623#~ "Please execute the import script locally as it can take a very long time."
624#~ msgstr ""
625#~ "Bitte führe das Import Script lokal aus, da dies eine Weile dauern kann."
626
627#~ msgid "More info in the official documentation:"
628#~ msgstr "Mehr Informationen in der offiziellen Dokumentation:"
629
630#~ msgid "import from instapaper completed"
631#~ msgstr "Import aus Instapaper erfolgreich"
632
633#~ msgid "import from pocket completed"
634#~ msgstr "Import aus Pocket erfolgreich"
635
636#~ msgid "import from Poche completed. "
637#~ msgstr "Import aus Poche erfolgreich"
638
639#~ msgid "Incomplete inc/poche/define.inc.php file, please define \""
640#~ msgstr "Unvollständige inc/poche/define.inc.php Datei, bitte setze \""
641
642#~ msgid "poche it!"
643#~ msgstr "Poche es!"
644
645#~ msgid "Updating poche"
646#~ msgstr "Poche aktualisieren"
647
648#~ msgid "create an issue"
649#~ msgstr "ein Ticket erstellen"
221 650
222msgid "Stay signed in" 651#~ msgid "or"
223msgstr "Angemeldet bleiben" 652#~ msgstr "oder"
224 653
225msgid "(Do not check on public computers)" 654#~ msgid "contact us by mail"
226msgstr "(nicht auf einem öffentlichen Computer anhaken)" 655#~ msgstr "kontaktieren Sie uns per E-Mail"
227 656
228msgid "Sign in" 657#~ msgid "your poche version:"
229msgstr "Einloggen" 658#~ msgstr "Deine Poche Version"
diff --git a/locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.mo b/locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.mo
index 83d0a85f..bf5f69e7 100644
--- a/locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.mo
+++ b/locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.mo
Binary files differ
diff --git a/locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.po b/locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.po
index b78759f5..119fb060 100644
--- a/locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.po
+++ b/locale/en_EN.utf8/LC_MESSAGES/en_EN.utf8.po
@@ -1,50 +1,124 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: \n" 3"Project-Id-Version: \n"
4"POT-Creation-Date: \n" 4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2014-02-25 15:17+0300\n"
5"PO-Revision-Date: \n" 6"PO-Revision-Date: \n"
6"Last-Translator: Nicolas Lœuillet <nicolas.loeuillet@gmail.com>\n" 7"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n"
7"Language-Team: \n" 8"Language-Team: \n"
9"Language: \n"
8"MIME-Version: 1.0\n" 10"MIME-Version: 1.0\n"
9"Content-Type: text/plain; charset=UTF-8\n" 11"Content-Type: text/plain; charset=UTF-8\n"
10"Content-Transfer-Encoding: 8bit\n" 12"Content-Transfer-Encoding: 8bit\n"
11"X-Generator: Poedit 1.5.4\n" 13"X-Generator: Poedit 1.5.4\n"
14"X-Poedit-Language: English\n"
15"X-Poedit-Basepath: .\n"
16"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag-master-testing\n"
17
18msgid "wallabag, a read it later open source system"
19msgstr "wallabag, a read it later open source system"
20
21msgid "login failed: user doesn't exist"
22msgstr "login failed: user doesn't exist"
23
24msgid "return home"
25msgstr "return home"
12 26
13msgid "config" 27msgid "config"
14msgstr "config" 28msgstr "config"
15 29
16msgid "Poching a link" 30msgid "Saving articles"
17msgstr "Poching a link" 31msgstr "Saving articles"
32
33msgid "There are several ways to save an article:"
34msgstr "There are several ways to save an article:"
18 35
19msgid "read the documentation" 36msgid "read the documentation"
20msgstr "read the documentation" 37msgstr "read the documentation"
21 38
22msgid "by filling this field" 39msgid "download the extension"
23msgstr "by filling this field" 40msgstr "download the extension"
24 41
25msgid "poche it!" 42msgid "via F-Droid"
26msgstr "poche it!" 43msgstr "via F-Droid"
27 44
28msgid "Updating poche" 45msgid " or "
29msgstr "Updating poche" 46msgstr " or "
30 47
31msgid "your version" 48msgid "via Google Play"
32msgstr "your version" 49msgstr "via Google Play"
33 50
34msgid "latest stable version" 51msgid "download the application"
35msgstr "latest stable version" 52msgstr "download the application"
36 53
37msgid "a more recent stable version is available." 54msgid "By filling this field"
38msgstr "a more recent stable version is available." 55msgstr "By filling this field"
39 56
40msgid "you are up to date." 57msgid "bag it!"
41msgstr "you are up to date." 58msgstr "bag it!"
42 59
43msgid "latest dev version" 60msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
44msgstr "latest dev version" 61msgstr "Bookmarklet: drag & drop this link to your bookmarks bar"
45 62
46msgid "a more recent development version is available." 63msgid "Upgrading wallabag"
47msgstr "a more recent development version is available." 64msgstr "Upgrading wallabag"
65
66msgid "Installed version"
67msgstr "Installed version"
68
69msgid "Latest stable version"
70msgstr "Latest stable version"
71
72msgid "A more recent stable version is available."
73msgstr "A more recent stable version is available."
74
75msgid "You are up to date."
76msgstr "You are up to date."
77
78msgid "Latest dev version"
79msgstr "Latest dev version"
80
81msgid "A more recent development version is available."
82msgstr "A more recent development version is available."
83
84msgid "Feeds"
85msgstr "Feeds"
86
87msgid "Your feed token is currently empty and must first be generated to enable feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
88msgstr "Your feed token is currently empty and must first be generated to enable feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
89
90msgid "Unread feed"
91msgstr "Unread feed"
92
93msgid "Favorites feed"
94msgstr "Favorites feed"
95
96msgid "Archive feed"
97msgstr "Archive feed"
98
99msgid "Your token:"
100msgstr "Your token:"
101
102msgid "Your user id:"
103msgstr "Your user id:"
104
105msgid "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>."
106msgstr "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>."
107
108msgid "Change your theme"
109msgstr "Change your theme"
110
111msgid "Theme:"
112msgstr "Theme:"
113
114msgid "Update"
115msgstr "Update"
116
117msgid "Change your language"
118msgstr "Change your language"
119
120msgid "Language:"
121msgstr "Language:"
48 122
49msgid "Change your password" 123msgid "Change your password"
50msgstr "Change your password" 124msgstr "Change your password"
@@ -58,66 +132,60 @@ msgstr "Password"
58msgid "Repeat your new password:" 132msgid "Repeat your new password:"
59msgstr "Repeat your new password:" 133msgstr "Repeat your new password:"
60 134
61msgid "Update"
62msgstr "Update"
63
64msgid "Import" 135msgid "Import"
65msgstr "Import" 136msgstr "Import"
66 137
67msgid "Please execute the import script locally, it can take a very long time." 138msgid "Please execute the import script locally as it can take a very long time."
68msgstr "" 139msgstr "Please execute the import script locally as it can take a very long time."
69"Please execute the import script locally, it can take a very long time."
70 140
71msgid "More info in the official doc:" 141msgid "More info in the official documentation:"
72msgstr "More info in the official doc:" 142msgstr "More info in the official documentation:"
73 143
74msgid "import from Pocket" 144msgid "Import from Pocket"
75msgstr "import from Pocket" 145msgstr "Import from Pocket"
76 146
77msgid "import from Readability" 147#, php-format
78msgstr "import from Readability" 148msgid "(you must have a %s file on your server)"
149msgstr "(you must have a %s file on your server)"
79 150
80msgid "import from Instapaper" 151msgid "Import from Readability"
81msgstr "import from Instapaper" 152msgstr "Import from Readability"
153
154msgid "Import from Instapaper"
155msgstr "Import from Instapaper"
82 156
83msgid "Export your poche data" 157msgid "Import from wallabag"
84msgstr "Export your poche data" 158msgstr "Import from wallabag"
159
160msgid "Export your wallabag data"
161msgstr "Export your wallabag data"
85 162
86msgid "Click here" 163msgid "Click here"
87msgstr "Click here" 164msgstr "Click here"
88 165
89msgid "to export your poche data." 166msgid "to download your database."
90msgstr "to export your poche data." 167msgstr "to download your database."
91 168
92msgid "back to home" 169msgid "to export your wallabag data."
93msgstr "back to home" 170msgstr "to export your wallabag data."
94 171
95msgid "installation" 172msgid "Cache"
96msgstr "installation" 173msgstr "Cache"
97 174
98msgid "install your poche" 175msgid "to delete cache."
99msgstr "install your poche" 176msgstr "to delete cache."
100 177
101msgid "" 178msgid "You can enter multiple tags, separated by commas."
102"poche is still not installed. Please fill the below form to install it. " 179msgstr "You can enter multiple tags, separated by commas."
103"Don't hesitate to <a href='http://doc.inthepoche.com'>read the documentation "
104"on poche website</a>."
105msgstr ""
106"poche is still not installed. Please fill the below form to install it. "
107"Don't hesitate to <a href='http://doc.inthepoche.com'>read the documentation "
108"on poche website</a>."
109
110msgid "Login"
111msgstr "Login"
112 180
113msgid "Repeat your password" 181msgid "return to article"
114msgstr "Repeat your password" 182msgstr "return to article"
115 183
116msgid "Install" 184msgid "plop"
117msgstr "Install" 185msgstr "plop"
118 186
119msgid "back to top" 187msgid "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>."
120msgstr "back to top" 188msgstr "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>."
121 189
122msgid "favoris" 190msgid "favoris"
123msgstr "favoris" 191msgstr "favoris"
@@ -146,11 +214,14 @@ msgstr "by title"
146msgid "by title desc" 214msgid "by title desc"
147msgstr "by title desc" 215msgstr "by title desc"
148 216
149msgid "No link available here!" 217msgid "Tag"
150msgstr "No link available here!" 218msgstr "Tag"
151 219
152msgid "toggle mark as read" 220msgid "No articles found."
153msgstr "toggle mark as read" 221msgstr "No articles found."
222
223msgid "Toggle mark as read"
224msgstr "Toggle mark as read"
154 225
155msgid "toggle favorite" 226msgid "toggle favorite"
156msgstr "toggle favorite" 227msgstr "toggle favorite"
@@ -161,14 +232,86 @@ msgstr "delete"
161msgid "original" 232msgid "original"
162msgstr "original" 233msgstr "original"
163 234
235msgid "estimated reading time:"
236msgstr "estimated reading time:"
237
238msgid "mark all the entries as read"
239msgstr "mark all the entries as read"
240
164msgid "results" 241msgid "results"
165msgstr "results" 242msgstr "results"
166 243
167msgid "tweet" 244msgid "installation"
168msgstr "tweet" 245msgstr "installation"
169 246
170msgid "email" 247msgid "install your wallabag"
171msgstr "email" 248msgstr "install your wallabag"
249
250msgid "wallabag is still not installed. Please fill the below form to install it. Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation on wallabag website</a>."
251msgstr "wallabag is still not installed. Please fill the below form to install it. Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation on wallabag website</a>."
252
253msgid "Login"
254msgstr "Login"
255
256msgid "Repeat your password"
257msgstr "Repeat your password"
258
259msgid "Install"
260msgstr "Install"
261
262msgid "login to your wallabag"
263msgstr "login to your wallabag"
264
265msgid "Login to wallabag"
266msgstr "Login to wallabag"
267
268msgid "you are in demo mode, some features may be disabled."
269msgstr "you are in demo mode, some features may be disabled."
270
271msgid "Username"
272msgstr "Username"
273
274msgid "Stay signed in"
275msgstr "Stay signed in"
276
277msgid "(Do not check on public computers)"
278msgstr "(Do not check on public computers)"
279
280msgid "Sign in"
281msgstr "Sign in"
282
283msgid "favorites"
284msgstr "favorites"
285
286msgid "estimated reading time :"
287msgstr "estimated reading time :"
288
289msgid "Mark all the entries as read"
290msgstr "Mark all the entries as read"
291
292msgid "Return home"
293msgstr "Return home"
294
295msgid "Back to top"
296msgstr "Back to top"
297
298msgid "Mark as read"
299msgstr "Mark as read"
300
301msgid "Favorite"
302msgstr "Favorite"
303
304msgid "Toggle favorite"
305msgstr "Toggle favorite"
306
307msgid "Delete"
308msgstr "Delete"
309
310msgid "Tweet"
311msgstr "Tweet"
312
313msgid "Email"
314msgstr "Email"
172 315
173msgid "shaarli" 316msgid "shaarli"
174msgstr "shaarli" 317msgstr "shaarli"
@@ -176,26 +319,23 @@ msgstr "shaarli"
176msgid "flattr" 319msgid "flattr"
177msgstr "flattr" 320msgstr "flattr"
178 321
179msgid "this article appears wrong?" 322msgid "Does this article appear wrong?"
180msgstr "this article appears wrong?" 323msgstr "Does this article appear wrong?"
181 324
182msgid "create an issue" 325msgid "tags:"
183msgstr "create an issue" 326msgstr "tags:"
184 327
185msgid "or" 328msgid "Edit tags"
186msgstr "or" 329msgstr "Edit tags"
187 330
188msgid "contact us by mail" 331msgid "save link!"
189msgstr "contact us by mail" 332msgstr "save link!"
190
191msgid "plop"
192msgstr "plop"
193 333
194msgid "home" 334msgid "home"
195msgstr "home" 335msgstr "home"
196 336
197msgid "favorites" 337msgid "tags"
198msgstr "favorites" 338msgstr "tags"
199 339
200msgid "logout" 340msgid "logout"
201msgstr "logout" 341msgstr "logout"
@@ -206,23 +346,179 @@ msgstr "powered by"
206msgid "debug mode is on so cache is off." 346msgid "debug mode is on so cache is off."
207msgstr "debug mode is on so cache is off." 347msgstr "debug mode is on so cache is off."
208 348
209msgid "your poche version:" 349msgid "your wallabag version:"
210msgstr "your poche version:" 350msgstr "your wallabag version:"
211 351
212msgid "storage:" 352msgid "storage:"
213msgstr "storage:" 353msgstr "storage:"
214 354
215msgid "login to your poche" 355msgid "save a link"
216msgstr "login to your poche" 356msgstr "save a link"
217 357
218msgid "you are in demo mode, some features may be disabled." 358msgid "back to home"
219msgstr "you are in demo mode, some features may be disabled." 359msgstr "back to home"
220 360
221msgid "Stay signed in" 361msgid "toggle mark as read"
222msgstr "Stay signed in" 362msgstr "toggle mark as read"
223 363
224msgid "(Do not check on public computers)" 364msgid "tweet"
225msgstr "(Do not check on public computers)" 365msgstr "tweet"
226 366
227msgid "Sign in" 367msgid "email"
228msgstr "Sign in" 368msgstr "email"
369
370msgid "this article appears wrong?"
371msgstr "this article appears wrong?"
372
373msgid "No link available here!"
374msgstr "No link available here!"
375
376msgid "Poching a link"
377msgstr "Poching a link"
378
379msgid "by filling this field"
380msgstr "by filling this field"
381
382msgid "bookmarklet: drag & drop this link to your bookmarks bar"
383msgstr "bookmarklet: drag & drop this link to your bookmarks bar"
384
385msgid "your version"
386msgstr "your version"
387
388msgid "latest stable version"
389msgstr "latest stable version"
390
391msgid "a more recent stable version is available."
392msgstr "a more recent stable version is available."
393
394msgid "you are up to date."
395msgstr "you are up to date."
396
397msgid "latest dev version"
398msgstr "latest dev version"
399
400msgid "a more recent development version is available."
401msgstr "a more recent development version is available."
402
403msgid "Please execute the import script locally, it can take a very long time."
404msgstr "Please execute the import script locally, it can take a very long time."
405
406msgid "More infos in the official doc:"
407msgstr "More infos in the official doc:"
408
409msgid "import from Pocket"
410msgstr "import from Pocket"
411
412msgid "import from Readability"
413msgstr "import from Readability"
414
415msgid "import from Instapaper"
416msgstr "import from Instapaper"
417
418msgid "Tags"
419msgstr "Tags"
420
421msgid "Untitled"
422msgstr "Untitled"
423
424msgid "the link has been added successfully"
425msgstr "the link has been added successfully"
426
427msgid "error during insertion : the link wasn't added"
428msgstr "error during insertion : the link wasn't added"
429
430msgid "the link has been deleted successfully"
431msgstr "the link has been deleted successfully"
432
433msgid "the link wasn't deleted"
434msgstr "the link wasn't deleted"
435
436msgid "Article not found!"
437msgstr "Article not found!"
438
439msgid "previous"
440msgstr "previous"
441
442msgid "next"
443msgstr "next"
444
445msgid "in demo mode, you can't update your password"
446msgstr "in demo mode, you can't update your password"
447
448msgid "your password has been updated"
449msgstr "your password has been updated"
450
451msgid "the two fields have to be filled & the password must be the same in the two fields"
452msgstr "the two fields have to be filled & the password must be the same in the two fields"
453
454msgid "still using the \""
455msgstr "still using the \""
456
457msgid "that theme does not seem to be installed"
458msgstr "that theme does not seem to be installed"
459
460msgid "you have changed your theme preferences"
461msgstr "you have changed your theme preferences"
462
463msgid "that language does not seem to be installed"
464msgstr "that language does not seem to be installed"
465
466msgid "you have changed your language preferences"
467msgstr "you have changed your language preferences"
468
469msgid "login failed: you have to fill all fields"
470msgstr "login failed: you have to fill all fields"
471
472msgid "welcome to your wallabag"
473msgstr "welcome to your wallabag"
474
475msgid "login failed: bad login or password"
476msgstr "login failed: bad login or password"
477
478msgid "import from instapaper completed"
479msgstr "import from instapaper completed"
480
481msgid "import from pocket completed"
482msgstr "import from pocket completed"
483
484msgid "import from Readability completed. "
485msgstr "import from Readability completed. "
486
487msgid "import from Poche completed. "
488msgstr "import from Poche completed. "
489
490msgid "Unknown import provider."
491msgstr "Unknown import provider."
492
493msgid "Incomplete inc/poche/define.inc.php file, please define \""
494msgstr "Incomplete inc/poche/define.inc.php file, please define \""
495
496msgid "Could not find required \""
497msgstr "Could not find required \""
498
499msgid "Uh, there is a problem while generating feeds."
500msgstr "Uh, there is a problem while generating feeds."
501
502msgid "Cache deleted."
503msgstr "Cache deleted."
504
505msgid "Oops, it seems you don't have PHP 5."
506msgstr "Oops, it seems you don't have PHP 5."
507
508#~ msgid "poche it!"
509#~ msgstr "poche it!"
510
511#~ msgid "Updating poche"
512#~ msgstr "Updating poche"
513
514#~ msgid "create an issue"
515#~ msgstr "create an issue"
516
517#~ msgid "or"
518#~ msgstr "or"
519
520#~ msgid "contact us by mail"
521#~ msgstr "contact us by mail"
522
523#~ msgid "your poche version:"
524#~ msgstr "your poche version:"
diff --git a/locale/es_ES.utf8/LC_MESSAGES/es_ES.utf8.po b/locale/es_ES.utf8/LC_MESSAGES/es_ES.utf8.po
index afe0595d..c08decfe 100644
--- a/locale/es_ES.utf8/LC_MESSAGES/es_ES.utf8.po
+++ b/locale/es_ES.utf8/LC_MESSAGES/es_ES.utf8.po
@@ -1,51 +1,136 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: \n" 3"Project-Id-Version: \n"
4"POT-Creation-Date: \n" 4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2014-02-25 15:16+0300\n"
5"PO-Revision-Date: \n" 6"PO-Revision-Date: \n"
6"Last-Translator: Nicolas Lœuillet <nicolas.loeuillet@gmail.com>\n" 7"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n"
7"Language-Team: \n" 8"Language-Team: \n"
9"Language: \n"
8"MIME-Version: 1.0\n" 10"MIME-Version: 1.0\n"
9"Content-Type: text/plain; charset=UTF-8\n" 11"Content-Type: text/plain; charset=UTF-8\n"
10"Content-Transfer-Encoding: 8bit\n" 12"Content-Transfer-Encoding: 8bit\n"
11"X-Generator: Poedit 1.5.4\n" 13"X-Generator: Poedit 1.5.4\n"
14"X-Poedit-Language: Spanish\n"
15"X-Poedit-Basepath: .\n"
16"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag-master-testing\n"
17
18msgid "wallabag, a read it later open source system"
19msgstr ""
20
21msgid "login failed: user doesn't exist"
22msgstr ""
23
24msgid "return home"
25msgstr ""
12 26
13msgid "config" 27msgid "config"
14msgstr "configuración" 28msgstr "configuración"
15 29
16msgid "Poching a link" 30msgid "Saving articles"
17msgstr "Pochear un enlace" 31msgstr ""
32
33msgid "There are several ways to save an article:"
34msgstr ""
18 35
19msgid "read the documentation" 36msgid "read the documentation"
20msgstr "leer la documentación" 37msgstr "leer la documentación"
21 38
22msgid "by filling this field" 39msgid "download the extension"
40msgstr ""
41
42msgid "via F-Droid"
43msgstr ""
44
45msgid " or "
46msgstr ""
47
48msgid "via Google Play"
49msgstr ""
50
51msgid "download the application"
52msgstr ""
53
54#, fuzzy
55msgid "By filling this field"
23msgstr "rellenando este campo" 56msgstr "rellenando este campo"
24 57
25msgid "poche it!" 58msgid "bag it!"
26msgstr "pochéalo!" 59msgstr ""
27 60
28msgid "Updating poche" 61msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
29msgstr "Actualizar" 62msgstr ""
30 63
31msgid "your version" 64msgid "Upgrading wallabag"
32msgstr "su versión" 65msgstr ""
33 66
34msgid "latest stable version" 67#, fuzzy
68msgid "Installed version"
35msgstr "ultima versión estable" 69msgstr "ultima versión estable"
36 70
37msgid "a more recent stable version is available." 71#, fuzzy
72msgid "Latest stable version"
73msgstr "ultima versión estable"
74
75#, fuzzy
76msgid "A more recent stable version is available."
38msgstr "una versión estable más reciente está disponible." 77msgstr "una versión estable más reciente está disponible."
39 78
40msgid "you are up to date." 79#, fuzzy
80msgid "You are up to date."
41msgstr "estás actualizado." 81msgstr "estás actualizado."
42 82
43msgid "latest dev version" 83#, fuzzy
84msgid "Latest dev version"
44msgstr "ultima versión de desarollo" 85msgstr "ultima versión de desarollo"
45 86
46msgid "a more recent development version is available." 87#, fuzzy
88msgid "A more recent development version is available."
47msgstr "una versión de desarollo más reciente está disponible." 89msgstr "una versión de desarollo más reciente está disponible."
48 90
91msgid "Feeds"
92msgstr ""
93
94msgid "Your feed token is currently empty and must first be generated to enable feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
95msgstr ""
96
97msgid "Unread feed"
98msgstr ""
99
100#, fuzzy
101msgid "Favorites feed"
102msgstr "preferidos"
103
104#, fuzzy
105msgid "Archive feed"
106msgstr "archivos"
107
108msgid "Your token:"
109msgstr ""
110
111msgid "Your user id:"
112msgstr ""
113
114msgid "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>."
115msgstr ""
116
117#, fuzzy
118msgid "Change your theme"
119msgstr "Modificar tu contraseña"
120
121msgid "Theme:"
122msgstr ""
123
124msgid "Update"
125msgstr "Actualizar"
126
127#, fuzzy
128msgid "Change your language"
129msgstr "Modificar tu contraseña"
130
131msgid "Language:"
132msgstr ""
133
49msgid "Change your password" 134msgid "Change your password"
50msgstr "Modificar tu contraseña" 135msgstr "Modificar tu contraseña"
51 136
@@ -58,66 +143,68 @@ msgstr "Contraseña"
58msgid "Repeat your new password:" 143msgid "Repeat your new password:"
59msgstr "Repetir la nueva contraseña :" 144msgstr "Repetir la nueva contraseña :"
60 145
61msgid "Update"
62msgstr "Actualizar"
63
64msgid "Import" 146msgid "Import"
65msgstr "Importar" 147msgstr "Importar"
66 148
67msgid "Please execute the import script locally, it can take a very long time." 149#, fuzzy
68msgstr "" 150msgid "Please execute the import script locally as it can take a very long time."
69"Por favor, ejecute la importación en local, esto puede demorar un tiempo." 151msgstr "Por favor, ejecute la importación en local, esto puede demorar un tiempo."
70 152
71msgid "More info in the official doc:" 153#, fuzzy
154msgid "More info in the official documentation:"
72msgstr "Más información en la documentación oficial :" 155msgstr "Más información en la documentación oficial :"
73 156
74msgid "import from Pocket" 157#, fuzzy
158msgid "Import from Pocket"
75msgstr "importación desde Pocket" 159msgstr "importación desde Pocket"
76 160
77msgid "import from Readability" 161#, php-format
162msgid "(you must have a %s file on your server)"
163msgstr ""
164
165#, fuzzy
166msgid "Import from Readability"
78msgstr "importación desde Readability" 167msgstr "importación desde Readability"
79 168
80msgid "import from Instapaper" 169#, fuzzy
170msgid "Import from Instapaper"
81msgstr "importación desde Instapaper" 171msgstr "importación desde Instapaper"
82 172
83msgid "Export your poche data" 173#, fuzzy
174msgid "Import from wallabag"
175msgstr "importación desde Readability"
176
177#, fuzzy
178msgid "Export your wallabag data"
84msgstr "Exportar sus datos de poche" 179msgstr "Exportar sus datos de poche"
85 180
86msgid "Click here" 181msgid "Click here"
87msgstr "Haga clic aquí" 182msgstr "Haga clic aquí"
88 183
89msgid "to export your poche data." 184msgid "to download your database."
90msgstr "para exportar sus datos de poche." 185msgstr ""
91
92msgid "back to home"
93msgstr "volver a la página de inicio"
94 186
95msgid "installation" 187#, fuzzy
96msgstr "instalación" 188msgid "to export your wallabag data."
189msgstr "para exportar sus datos de poche."
97 190
98msgid "install your poche" 191msgid "Cache"
99msgstr "instala tu Poche" 192msgstr ""
100 193
101msgid "" 194msgid "to delete cache."
102"Poche is still not installed. Please fill the below form to install it. "
103"Don't hesitate to <a href='http://doc.inthepoche.com'>read the documentation "
104"on poche website</a>."
105msgstr "" 195msgstr ""
106"Poche todavia no està instalado. Por favor, completa los campos siguientes "
107"para instalarlo. No dudes de <a href='http://doc.inthepoche.com'>leer la "
108"documentación en el sitio de Poche</a>."
109 196
110msgid "Login" 197msgid "You can enter multiple tags, separated by commas."
111msgstr "Nombre de usuario" 198msgstr ""
112 199
113msgid "Repeat your password" 200msgid "return to article"
114msgstr "Repita su contraseña" 201msgstr ""
115 202
116msgid "Install" 203msgid "plop"
117msgstr "Instalar" 204msgstr "plop"
118 205
119msgid "back to top" 206msgid "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>."
120msgstr "volver arriba" 207msgstr ""
121 208
122msgid "favoris" 209msgid "favoris"
123msgstr "preferidos" 210msgstr "preferidos"
@@ -146,10 +233,14 @@ msgstr "por título"
146msgid "by title desc" 233msgid "by title desc"
147msgstr "por título descendiente" 234msgstr "por título descendiente"
148 235
149msgid "No link available here!" 236msgid "Tag"
150msgstr "¡No hay ningún enlace disponible por aquí!" 237msgstr ""
151 238
152msgid "toggle mark as read" 239msgid "No articles found."
240msgstr ""
241
242#, fuzzy
243msgid "Toggle mark as read"
153msgstr "marcar como leído" 244msgstr "marcar como leído"
154 245
155msgid "toggle favorite" 246msgid "toggle favorite"
@@ -161,13 +252,95 @@ msgstr "eliminar"
161msgid "original" 252msgid "original"
162msgstr "original" 253msgstr "original"
163 254
255msgid "estimated reading time:"
256msgstr ""
257
258msgid "mark all the entries as read"
259msgstr ""
260
164msgid "results" 261msgid "results"
165msgstr "resultados" 262msgstr "resultados"
166 263
167msgid "tweet" 264msgid "installation"
265msgstr "instalación"
266
267#, fuzzy
268msgid "install your wallabag"
269msgstr "instala tu Poche"
270
271#, fuzzy
272msgid "wallabag is still not installed. Please fill the below form to install it. Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation on wallabag website</a>."
273msgstr "Poche todavia no està instalado. Por favor, completa los campos siguientes para instalarlo. No dudes de <a href='http://doc.inthepoche.com'>leer la documentación en el sitio de Poche</a>."
274
275msgid "Login"
276msgstr "Nombre de usuario"
277
278msgid "Repeat your password"
279msgstr "Repita su contraseña"
280
281msgid "Install"
282msgstr "Instalar"
283
284#, fuzzy
285msgid "login to your wallabag"
286msgstr "conectarse a tu Poche"
287
288msgid "Login to wallabag"
289msgstr ""
290
291msgid "you are in demo mode, some features may be disabled."
292msgstr "este es el modo de demostración, algunas funcionalidades pueden estar desactivadas."
293
294msgid "Username"
295msgstr ""
296
297msgid "Stay signed in"
298msgstr "Seguir conectado"
299
300msgid "(Do not check on public computers)"
301msgstr "(no marcar en un ordenador público)"
302
303msgid "Sign in"
304msgstr "Iniciar sesión"
305
306msgid "favorites"
307msgstr "preferidos"
308
309msgid "estimated reading time :"
310msgstr ""
311
312msgid "Mark all the entries as read"
313msgstr ""
314
315msgid "Return home"
316msgstr ""
317
318#, fuzzy
319msgid "Back to top"
320msgstr "volver arriba"
321
322#, fuzzy
323msgid "Mark as read"
324msgstr "marcar como leído"
325
326#, fuzzy
327msgid "Favorite"
328msgstr "preferidos"
329
330#, fuzzy
331msgid "Toggle favorite"
332msgstr "preferido"
333
334#, fuzzy
335msgid "Delete"
336msgstr "eliminar"
337
338#, fuzzy
339msgid "Tweet"
168msgstr "tweetear" 340msgstr "tweetear"
169 341
170msgid "email" 342#, fuzzy
343msgid "Email"
171msgstr "enviar por mail" 344msgstr "enviar por mail"
172 345
173msgid "shaarli" 346msgid "shaarli"
@@ -176,26 +349,24 @@ msgstr "shaarli"
176msgid "flattr" 349msgid "flattr"
177msgstr "flattr" 350msgstr "flattr"
178 351
179msgid "this article appears wrong?" 352#, fuzzy
353msgid "Does this article appear wrong?"
180msgstr "este articulo no se ve bien?" 354msgstr "este articulo no se ve bien?"
181 355
182msgid "create an issue" 356msgid "tags:"
183msgstr "crear un ticket" 357msgstr ""
184
185msgid "or"
186msgstr "o"
187 358
188msgid "contact us by mail" 359msgid "Edit tags"
189msgstr "contactarnos por mail" 360msgstr ""
190 361
191msgid "plop" 362msgid "save link!"
192msgstr "plop" 363msgstr ""
193 364
194msgid "home" 365msgid "home"
195msgstr "inicio" 366msgstr "inicio"
196 367
197msgid "favorites" 368msgid "tags"
198msgstr "preferidos" 369msgstr ""
199 370
200msgid "logout" 371msgid "logout"
201msgstr "cerrar sesión" 372msgstr "cerrar sesión"
@@ -206,25 +377,187 @@ msgstr "hecho con"
206msgid "debug mode is on so cache is off." 377msgid "debug mode is on so cache is off."
207msgstr "el modo de depuración está activado, así que la cache está desactivada." 378msgstr "el modo de depuración está activado, así que la cache está desactivada."
208 379
209msgid "your poche version:" 380#, fuzzy
210msgstr "tu versión de Poche:" 381msgid "your wallabag version:"
382msgstr "su versión"
211 383
212msgid "storage:" 384msgid "storage:"
213msgstr "almacenamiento:" 385msgstr "almacenamiento:"
214 386
215msgid "login to your poche" 387msgid "save a link"
216msgstr "conectarse a tu Poche" 388msgstr ""
217 389
218msgid "you are in demo mode, some features may be disabled." 390msgid "back to home"
391msgstr "volver a la página de inicio"
392
393msgid "toggle mark as read"
394msgstr "marcar como leído"
395
396msgid "tweet"
397msgstr "tweetear"
398
399msgid "email"
400msgstr "enviar por mail"
401
402msgid "this article appears wrong?"
403msgstr "este articulo no se ve bien?"
404
405msgid "No link available here!"
406msgstr "¡No hay ningún enlace disponible por aquí!"
407
408msgid "Poching a link"
409msgstr "Pochear un enlace"
410
411msgid "by filling this field"
412msgstr "rellenando este campo"
413
414msgid "bookmarklet: drag & drop this link to your bookmarks bar"
219msgstr "" 415msgstr ""
220"este es el modo de demostración, algunas funcionalidades pueden estar "
221"desactivadas."
222 416
223msgid "Stay signed in" 417msgid "your version"
224msgstr "Seguir conectado" 418msgstr "su versión"
225 419
226msgid "(Do not check on public computers)" 420msgid "latest stable version"
227msgstr "(no marcar en un ordenador público)" 421msgstr "ultima versión estable"
228 422
229msgid "Sign in" 423msgid "a more recent stable version is available."
230msgstr "Iniciar sesión" 424msgstr "una versión estable más reciente está disponible."
425
426msgid "you are up to date."
427msgstr "estás actualizado."
428
429msgid "latest dev version"
430msgstr "ultima versión de desarollo"
431
432msgid "a more recent development version is available."
433msgstr "una versión de desarollo más reciente está disponible."
434
435msgid "Please execute the import script locally, it can take a very long time."
436msgstr "Por favor, ejecute la importación en local, esto puede demorar un tiempo."
437
438#, fuzzy
439msgid "More infos in the official doc:"
440msgstr "Más información en la documentación oficial :"
441
442msgid "import from Pocket"
443msgstr "importación desde Pocket"
444
445msgid "import from Readability"
446msgstr "importación desde Readability"
447
448msgid "import from Instapaper"
449msgstr "importación desde Instapaper"
450
451msgid "Tags"
452msgstr ""
453
454#, fuzzy
455msgid "Untitled"
456msgstr "por título"
457
458msgid "the link has been added successfully"
459msgstr ""
460
461msgid "error during insertion : the link wasn't added"
462msgstr ""
463
464msgid "the link has been deleted successfully"
465msgstr ""
466
467msgid "the link wasn't deleted"
468msgstr ""
469
470msgid "Article not found!"
471msgstr ""
472
473msgid "previous"
474msgstr ""
475
476msgid "next"
477msgstr ""
478
479msgid "in demo mode, you can't update your password"
480msgstr ""
481
482msgid "your password has been updated"
483msgstr ""
484
485msgid "the two fields have to be filled & the password must be the same in the two fields"
486msgstr ""
487
488msgid "still using the \""
489msgstr ""
490
491msgid "that theme does not seem to be installed"
492msgstr ""
493
494msgid "you have changed your theme preferences"
495msgstr ""
496
497msgid "that language does not seem to be installed"
498msgstr ""
499
500msgid "you have changed your language preferences"
501msgstr ""
502
503msgid "login failed: you have to fill all fields"
504msgstr ""
505
506msgid "welcome to your wallabag"
507msgstr ""
508
509msgid "login failed: bad login or password"
510msgstr ""
511
512#, fuzzy
513msgid "import from instapaper completed"
514msgstr "importación desde Instapaper"
515
516#, fuzzy
517msgid "import from pocket completed"
518msgstr "importación desde Pocket"
519
520#, fuzzy
521msgid "import from Readability completed. "
522msgstr "importación desde Readability"
523
524#, fuzzy
525msgid "import from Poche completed. "
526msgstr "importación desde Pocket"
527
528msgid "Unknown import provider."
529msgstr ""
530
531msgid "Incomplete inc/poche/define.inc.php file, please define \""
532msgstr ""
533
534msgid "Could not find required \""
535msgstr ""
536
537msgid "Uh, there is a problem while generating feeds."
538msgstr ""
539
540#, fuzzy
541msgid "Cache deleted."
542msgstr "eliminar"
543
544msgid "Oops, it seems you don't have PHP 5."
545msgstr ""
546
547#~ msgid "poche it!"
548#~ msgstr "pochéalo!"
549
550#~ msgid "Updating poche"
551#~ msgstr "Actualizar"
552
553#~ msgid "create an issue"
554#~ msgstr "crear un ticket"
555
556#~ msgid "or"
557#~ msgstr "o"
558
559#~ msgid "contact us by mail"
560#~ msgstr "contactarnos por mail"
561
562#~ msgid "your poche version:"
563#~ msgstr "tu versión de Poche:"
diff --git a/locale/fa_IR.utf8/LC_MESSAGES/fa_IR.utf8.po b/locale/fa_IR.utf8/LC_MESSAGES/fa_IR.utf8.po
index 74a763c6..f24814d5 100644
--- a/locale/fa_IR.utf8/LC_MESSAGES/fa_IR.utf8.po
+++ b/locale/fa_IR.utf8/LC_MESSAGES/fa_IR.utf8.po
@@ -1,51 +1,136 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: \n" 3"Project-Id-Version: \n"
4"POT-Creation-Date: \n" 4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2014-02-25 15:15+0300\n"
5"PO-Revision-Date: \n" 6"PO-Revision-Date: \n"
6"Last-Translator: Nicolas Lœuillet <nicolas.loeuillet@gmail.com>\n" 7"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n"
7"Language-Team: \n" 8"Language-Team: \n"
9"Language: \n"
8"MIME-Version: 1.0\n" 10"MIME-Version: 1.0\n"
9"Content-Type: text/plain; charset=UTF-8\n" 11"Content-Type: text/plain; charset=UTF-8\n"
10"Content-Transfer-Encoding: 8bit\n" 12"Content-Transfer-Encoding: 8bit\n"
11"X-Generator: Poedit 1.5.4\n" 13"X-Generator: Poedit 1.5.4\n"
14"X-Poedit-Language: Persian\n"
15"X-Poedit-Basepath: .\n"
16"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag-master-testing\n"
17
18msgid "wallabag, a read it later open source system"
19msgstr ""
20
21msgid "login failed: user doesn't exist"
22msgstr ""
23
24msgid "return home"
25msgstr ""
12 26
13msgid "config" 27msgid "config"
14msgstr "تنظیمات" 28msgstr "تنظیمات"
15 29
16msgid "Poching a link" 30msgid "Saving articles"
17msgstr "پیوندی را poche کنید" 31msgstr ""
32
33msgid "There are several ways to save an article:"
34msgstr ""
18 35
19msgid "read the documentation" 36msgid "read the documentation"
20msgstr "راهنما را بخوانید" 37msgstr "راهنما را بخوانید"
21 38
22msgid "by filling this field" 39msgid "download the extension"
40msgstr ""
41
42msgid "via F-Droid"
43msgstr ""
44
45msgid " or "
46msgstr ""
47
48msgid "via Google Play"
49msgstr ""
50
51msgid "download the application"
52msgstr ""
53
54#, fuzzy
55msgid "By filling this field"
23msgstr "با پرکردن این بخش" 56msgstr "با پرکردن این بخش"
24 57
25msgid "poche it!" 58msgid "bag it!"
26msgstr "poche کنید!" 59msgstr ""
27 60
28msgid "Updating poche" 61msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
29msgstr "به‌روزرسانی poche" 62msgstr ""
30 63
31msgid "your version" 64msgid "Upgrading wallabag"
32msgstr "نسخهٔ شما" 65msgstr ""
33 66
34msgid "latest stable version" 67#, fuzzy
68msgid "Installed version"
35msgstr "آخرین نسخهٔ پایدار" 69msgstr "آخرین نسخهٔ پایدار"
36 70
37msgid "a more recent stable version is available." 71#, fuzzy
72msgid "Latest stable version"
73msgstr "آخرین نسخهٔ پایدار"
74
75#, fuzzy
76msgid "A more recent stable version is available."
38msgstr "نسخهٔ پایدار تازه‌ای منتشر شده است." 77msgstr "نسخهٔ پایدار تازه‌ای منتشر شده است."
39 78
40msgid "you are up to date." 79#, fuzzy
80msgid "You are up to date."
41msgstr "شما به‌روز هستید." 81msgstr "شما به‌روز هستید."
42 82
43msgid "latest dev version" 83#, fuzzy
84msgid "Latest dev version"
44msgstr "آخرین نسخهٔ آزمایشی" 85msgstr "آخرین نسخهٔ آزمایشی"
45 86
46msgid "a more recent development version is available." 87#, fuzzy
88msgid "A more recent development version is available."
47msgstr "نسخهٔ آزمایشی تازه‌ای منتشر شده است." 89msgstr "نسخهٔ آزمایشی تازه‌ای منتشر شده است."
48 90
91msgid "Feeds"
92msgstr ""
93
94msgid "Your feed token is currently empty and must first be generated to enable feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
95msgstr ""
96
97msgid "Unread feed"
98msgstr ""
99
100#, fuzzy
101msgid "Favorites feed"
102msgstr "بهترین‌ها"
103
104#, fuzzy
105msgid "Archive feed"
106msgstr "بایگانی"
107
108msgid "Your token:"
109msgstr ""
110
111msgid "Your user id:"
112msgstr ""
113
114msgid "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>."
115msgstr ""
116
117#, fuzzy
118msgid "Change your theme"
119msgstr "گذرواژهٔ خود را تغییر دهید"
120
121msgid "Theme:"
122msgstr ""
123
124msgid "Update"
125msgstr "به‌روزرسانی"
126
127#, fuzzy
128msgid "Change your language"
129msgstr "گذرواژهٔ خود را تغییر دهید"
130
131msgid "Language:"
132msgstr ""
133
49msgid "Change your password" 134msgid "Change your password"
50msgstr "گذرواژهٔ خود را تغییر دهید" 135msgstr "گذرواژهٔ خود را تغییر دهید"
51 136
@@ -58,64 +143,68 @@ msgstr "گذرواژه"
58msgid "Repeat your new password:" 143msgid "Repeat your new password:"
59msgstr "گذرواژهٔ تازه را دوباره وارد کنید" 144msgstr "گذرواژهٔ تازه را دوباره وارد کنید"
60 145
61msgid "Update"
62msgstr "به‌روزرسانی"
63
64msgid "Import" 146msgid "Import"
65msgstr "درون‌ریزی" 147msgstr "درون‌ریزی"
66 148
67msgid "Please execute the import script locally, it can take a very long time." 149#, fuzzy
150msgid "Please execute the import script locally as it can take a very long time."
68msgstr "لطفاً برنامهٔ درون‌ریزی را به‌طور محلی اجرا کنید، شاید خیلی طول بکشد." 151msgstr "لطفاً برنامهٔ درون‌ریزی را به‌طور محلی اجرا کنید، شاید خیلی طول بکشد."
69 152
70msgid "More info in the official doc:" 153#, fuzzy
154msgid "More info in the official documentation:"
71msgstr "اطلاعات بیشتر در راهنمای رسمی:" 155msgstr "اطلاعات بیشتر در راهنمای رسمی:"
72 156
73msgid "import from Pocket" 157#, fuzzy
158msgid "Import from Pocket"
74msgstr "درون‌ریزی از Pocket" 159msgstr "درون‌ریزی از Pocket"
75 160
76msgid "import from Readability" 161#, php-format
162msgid "(you must have a %s file on your server)"
163msgstr ""
164
165#, fuzzy
166msgid "Import from Readability"
77msgstr "درون‌ریزی از Readability" 167msgstr "درون‌ریزی از Readability"
78 168
79msgid "import from Instapaper" 169#, fuzzy
170msgid "Import from Instapaper"
80msgstr "درون‌ریزی از Instapaper" 171msgstr "درون‌ریزی از Instapaper"
81 172
82msgid "Export your poche data" 173#, fuzzy
174msgid "Import from wallabag"
175msgstr "درون‌ریزی از Readability"
176
177#, fuzzy
178msgid "Export your wallabag data"
83msgstr "داده‌های poche خود را برون‌بری کنید" 179msgstr "داده‌های poche خود را برون‌بری کنید"
84 180
85msgid "Click here" 181msgid "Click here"
86msgstr "اینجا را کلیک کنید" 182msgstr "اینجا را کلیک کنید"
87 183
88msgid "to export your poche data." 184msgid "to download your database."
89msgstr "برای برون‌بری داده‌های poche شما" 185msgstr ""
90
91msgid "back to home"
92msgstr "بازگشت به خانه"
93 186
94msgid "installation" 187#, fuzzy
95msgstr "نصب" 188msgid "to export your wallabag data."
189msgstr "برای برون‌بری داده‌های poche شما"
96 190
97msgid "install your poche" 191msgid "Cache"
98msgstr "poche خود را نصب کنید" 192msgstr ""
99 193
100msgid "" 194msgid "to delete cache."
101"poche is still not installed. Please fill the below form to install it. "
102"Don't hesitate to <a href='http://doc.inthepoche.com'>read the documentation "
103"on poche website</a>."
104msgstr "" 195msgstr ""
105"poche هنوز نصب نیست. برای نصب لطفاً فرم زیر را پر کنید. خواندن <a "
106"href='http://doc.inthepoche.com'>راهنما در وبگاه poche</a> را از یاد نبرید."
107 196
108msgid "Login" 197msgid "You can enter multiple tags, separated by commas."
109msgstr "ورود" 198msgstr ""
110 199
111msgid "Repeat your password" 200msgid "return to article"
112msgstr "گذرواژه را دوباره وارد کنید" 201msgstr ""
113 202
114msgid "Install" 203msgid "plop"
115msgstr "نصب" 204msgstr "plop"
116 205
117msgid "back to top" 206msgid "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>."
118msgstr "بازگشت به بالای صفحه" 207msgstr ""
119 208
120msgid "favoris" 209msgid "favoris"
121msgstr "بهترین‌ها" 210msgstr "بهترین‌ها"
@@ -144,10 +233,14 @@ msgstr "با عنوان"
144msgid "by title desc" 233msgid "by title desc"
145msgstr "با عنوان (الفبایی معکوس)" 234msgstr "با عنوان (الفبایی معکوس)"
146 235
147msgid "No link available here!" 236msgid "Tag"
148msgstr "اینجا پیوندی موجود نیست!" 237msgstr ""
149 238
150msgid "toggle mark as read" 239msgid "No articles found."
240msgstr ""
241
242#, fuzzy
243msgid "Toggle mark as read"
151msgstr "خوانده‌شده/خوانده‌نشده" 244msgstr "خوانده‌شده/خوانده‌نشده"
152 245
153msgid "toggle favorite" 246msgid "toggle favorite"
@@ -159,13 +252,95 @@ msgstr "پاک‌کردن"
159msgid "original" 252msgid "original"
160msgstr "اصلی" 253msgstr "اصلی"
161 254
255msgid "estimated reading time:"
256msgstr ""
257
258msgid "mark all the entries as read"
259msgstr ""
260
162msgid "results" 261msgid "results"
163msgstr "نتایج" 262msgstr "نتایج"
164 263
165msgid "tweet" 264msgid "installation"
265msgstr "نصب"
266
267#, fuzzy
268msgid "install your wallabag"
269msgstr "poche خود را نصب کنید"
270
271#, fuzzy
272msgid "wallabag is still not installed. Please fill the below form to install it. Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation on wallabag website</a>."
273msgstr "poche هنوز نصب نیست. برای نصب لطفاً فرم زیر را پر کنید. خواندن <a href='http://doc.inthepoche.com'>راهنما در وبگاه poche</a> را از یاد نبرید."
274
275msgid "Login"
276msgstr "ورود"
277
278msgid "Repeat your password"
279msgstr "گذرواژه را دوباره وارد کنید"
280
281msgid "Install"
282msgstr "نصب"
283
284#, fuzzy
285msgid "login to your wallabag"
286msgstr "به poche خود وارد شوید"
287
288msgid "Login to wallabag"
289msgstr ""
290
291msgid "you are in demo mode, some features may be disabled."
292msgstr "این تنها نسخهٔ نمایشی است، برخی از ویژگی‌ها کار نمی‌کنند."
293
294msgid "Username"
295msgstr ""
296
297msgid "Stay signed in"
298msgstr "مرا به خاطر بسپار"
299
300msgid "(Do not check on public computers)"
301msgstr "(روی رایانه‌های عمومی این کار را نکنید)"
302
303msgid "Sign in"
304msgstr "ورود"
305
306msgid "favorites"
307msgstr "بهترین‌ها"
308
309msgid "estimated reading time :"
310msgstr ""
311
312msgid "Mark all the entries as read"
313msgstr ""
314
315msgid "Return home"
316msgstr ""
317
318#, fuzzy
319msgid "Back to top"
320msgstr "بازگشت به بالای صفحه"
321
322#, fuzzy
323msgid "Mark as read"
324msgstr "خوانده‌شده/خوانده‌نشده"
325
326#, fuzzy
327msgid "Favorite"
328msgstr "بهترین‌ها"
329
330#, fuzzy
331msgid "Toggle favorite"
332msgstr "جزء بهترین‌ها هست/نیست"
333
334#, fuzzy
335msgid "Delete"
336msgstr "پاک‌کردن"
337
338#, fuzzy
339msgid "Tweet"
166msgstr "توییت" 340msgstr "توییت"
167 341
168msgid "email" 342#, fuzzy
343msgid "Email"
169msgstr "ایمیل" 344msgstr "ایمیل"
170 345
171msgid "shaarli" 346msgid "shaarli"
@@ -174,26 +349,24 @@ msgstr "shaarli"
174msgid "flattr" 349msgid "flattr"
175msgstr "flattr" 350msgstr "flattr"
176 351
177msgid "this article appears wrong?" 352#, fuzzy
353msgid "Does this article appear wrong?"
178msgstr "این مطلب اشتباه نمایش داده شده؟" 354msgstr "این مطلب اشتباه نمایش داده شده؟"
179 355
180msgid "create an issue" 356msgid "tags:"
181msgstr "یک درخواست رفع‌مشکل بنویسید" 357msgstr ""
182
183msgid "or"
184msgstr "یا"
185 358
186msgid "contact us by mail" 359msgid "Edit tags"
187msgstr "به ما ایمیل بزنید" 360msgstr ""
188 361
189msgid "plop" 362msgid "save link!"
190msgstr "plop" 363msgstr ""
191 364
192msgid "home" 365msgid "home"
193msgstr "خانه" 366msgstr "خانه"
194 367
195msgid "favorites" 368msgid "tags"
196msgstr "بهترین‌ها" 369msgstr ""
197 370
198msgid "logout" 371msgid "logout"
199msgstr "بیرون رفتن" 372msgstr "بیرون رفتن"
@@ -204,23 +377,187 @@ msgstr "نیروگرفته از"
204msgid "debug mode is on so cache is off." 377msgid "debug mode is on so cache is off."
205msgstr "حالت عیب‌یابی فعال است، پس کاشه خاموش است." 378msgstr "حالت عیب‌یابی فعال است، پس کاشه خاموش است."
206 379
207msgid "your poche version:" 380#, fuzzy
208msgstr "نسخهٔ poche شما:" 381msgid "your wallabag version:"
382msgstr "نسخهٔ شما"
209 383
210msgid "storage:" 384msgid "storage:"
211msgstr "ذخیره‌سازی:" 385msgstr "ذخیره‌سازی:"
212 386
213msgid "login to your poche" 387msgid "save a link"
214msgstr "به poche خود وارد شوید" 388msgstr ""
215 389
216msgid "you are in demo mode, some features may be disabled." 390msgid "back to home"
217msgstr "ین نه نهٔ نمایی س، برخی از یژگی‌ا ک نی‌کنند." 391msgstr "اگشت به ن"
218 392
219msgid "Stay signed in" 393msgid "toggle mark as read"
220msgstr "ما ه س" 394msgstr "وانهه/ون‌نه"
221 395
222msgid "(Do not check on public computers)" 396msgid "tweet"
223msgstr "(وی راینه‌های عمومی این کار را نکنید)" 397msgstr "ویی"
224 398
225msgid "Sign in" 399msgid "email"
226msgstr "ورود" 400msgstr "ایمیل"
401
402msgid "this article appears wrong?"
403msgstr "این مطلب اشتباه نمایش داده شده؟"
404
405msgid "No link available here!"
406msgstr "اینجا پیوندی موجود نیست!"
407
408msgid "Poching a link"
409msgstr "پیوندی را poche کنید"
410
411msgid "by filling this field"
412msgstr "با پرکردن این بخش"
413
414msgid "bookmarklet: drag & drop this link to your bookmarks bar"
415msgstr ""
416
417msgid "your version"
418msgstr "نسخهٔ شما"
419
420msgid "latest stable version"
421msgstr "آخرین نسخهٔ پایدار"
422
423msgid "a more recent stable version is available."
424msgstr "نسخهٔ پایدار تازه‌ای منتشر شده است."
425
426msgid "you are up to date."
427msgstr "شما به‌روز هستید."
428
429msgid "latest dev version"
430msgstr "آخرین نسخهٔ آزمایشی"
431
432msgid "a more recent development version is available."
433msgstr "نسخهٔ آزمایشی تازه‌ای منتشر شده است."
434
435msgid "Please execute the import script locally, it can take a very long time."
436msgstr "لطفاً برنامهٔ درون‌ریزی را به‌طور محلی اجرا کنید، شاید خیلی طول بکشد."
437
438#, fuzzy
439msgid "More infos in the official doc:"
440msgstr "اطلاعات بیشتر در راهنمای رسمی:"
441
442msgid "import from Pocket"
443msgstr "درون‌ریزی از Pocket"
444
445msgid "import from Readability"
446msgstr "درون‌ریزی از Readability"
447
448msgid "import from Instapaper"
449msgstr "درون‌ریزی از Instapaper"
450
451msgid "Tags"
452msgstr ""
453
454#, fuzzy
455msgid "Untitled"
456msgstr "با عنوان"
457
458msgid "the link has been added successfully"
459msgstr ""
460
461msgid "error during insertion : the link wasn't added"
462msgstr ""
463
464msgid "the link has been deleted successfully"
465msgstr ""
466
467msgid "the link wasn't deleted"
468msgstr ""
469
470msgid "Article not found!"
471msgstr ""
472
473msgid "previous"
474msgstr ""
475
476msgid "next"
477msgstr ""
478
479msgid "in demo mode, you can't update your password"
480msgstr ""
481
482msgid "your password has been updated"
483msgstr ""
484
485msgid "the two fields have to be filled & the password must be the same in the two fields"
486msgstr ""
487
488msgid "still using the \""
489msgstr ""
490
491msgid "that theme does not seem to be installed"
492msgstr ""
493
494msgid "you have changed your theme preferences"
495msgstr ""
496
497msgid "that language does not seem to be installed"
498msgstr ""
499
500msgid "you have changed your language preferences"
501msgstr ""
502
503msgid "login failed: you have to fill all fields"
504msgstr ""
505
506msgid "welcome to your wallabag"
507msgstr ""
508
509msgid "login failed: bad login or password"
510msgstr ""
511
512#, fuzzy
513msgid "import from instapaper completed"
514msgstr "درون‌ریزی از Instapaper"
515
516#, fuzzy
517msgid "import from pocket completed"
518msgstr "درون‌ریزی از Pocket"
519
520#, fuzzy
521msgid "import from Readability completed. "
522msgstr "درون‌ریزی از Readability"
523
524#, fuzzy
525msgid "import from Poche completed. "
526msgstr "درون‌ریزی از Pocket"
527
528msgid "Unknown import provider."
529msgstr ""
530
531msgid "Incomplete inc/poche/define.inc.php file, please define \""
532msgstr ""
533
534msgid "Could not find required \""
535msgstr ""
536
537msgid "Uh, there is a problem while generating feeds."
538msgstr ""
539
540#, fuzzy
541msgid "Cache deleted."
542msgstr "پاک‌کردن"
543
544msgid "Oops, it seems you don't have PHP 5."
545msgstr ""
546
547#~ msgid "poche it!"
548#~ msgstr "poche کنید!"
549
550#~ msgid "Updating poche"
551#~ msgstr "به‌روزرسانی poche"
552
553#~ msgid "create an issue"
554#~ msgstr "یک درخواست رفع‌مشکل بنویسید"
555
556#~ msgid "or"
557#~ msgstr "یا"
558
559#~ msgid "contact us by mail"
560#~ msgstr "به ما ایمیل بزنید"
561
562#~ msgid "your poche version:"
563#~ msgstr "نسخهٔ poche شما:"
diff --git a/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mo b/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mo
index 76872b5a..b625e346 100644
--- a/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mo
+++ b/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.mo
Binary files differ
diff --git a/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po b/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po
index f105d180..0343bfec 100644
--- a/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po
+++ b/locale/fr_FR.utf8/LC_MESSAGES/fr_FR.utf8.po
@@ -1,50 +1,228 @@
1msgid "" 1msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: \n" 3"Project-Id-Version: wallabag 1.7\n"
4"POT-Creation-Date: \n" 4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2014-05-10 20:09+0100\n"
5"PO-Revision-Date: \n" 6"PO-Revision-Date: \n"
6"Last-Translator: Nicolas Lœuillet <nicolas.loeuillet@gmail.com>\n" 7"Last-Translator: Gilles Wittezaële <gilles.wittezaele@laposte.net>\n"
7"Language-Team: \n" 8"Language-Team: \n"
9"Language: fr_FR\n"
8"MIME-Version: 1.0\n" 10"MIME-Version: 1.0\n"
9"Content-Type: text/plain; charset=UTF-8\n" 11"Content-Type: text/plain; charset=UTF-8\n"
10"Content-Transfer-Encoding: 8bit\n" 12"Content-Transfer-Encoding: 8bit\n"
13"X-Poedit-KeywordsList: _;gettext;gettext_noop\n"
14"X-Poedit-SourceCharset: UTF-8\n"
11"X-Generator: Poedit 1.5.4\n" 15"X-Generator: Poedit 1.5.4\n"
16"Plural-Forms: nplurals=2; plural=(n != 1);\n"
17
18msgid "wallabag, a read it later open source system"
19msgstr "wallabag, un système open source de lecture différé"
20
21msgid "login failed: user doesn't exist"
22msgstr "échec de l'identification : cet utilisateur n'existe pas"
23
24msgid "save link!"
25msgstr "enregistrer le lien !"
26
27msgid "plop"
28msgstr "plop"
29
30msgid "powered by"
31msgstr "propulsé par"
32
33msgid "debug mode is on so cache is off."
34msgstr "le mode de debug est actif, le cache est donc désactivé."
35
36msgid "your wallabag version:"
37msgstr "votre version de wallabag :"
38
39msgid "storage:"
40msgstr "stockage :"
41
42msgid "login to your wallabag"
43msgstr "se connecter à votre wallabag"
44
45msgid "Login to wallabag"
46msgstr "Se connecter à wallabag"
47
48msgid "you are in demo mode, some features may be disabled."
49msgstr ""
50"vous êtes en mode démo, certaines fonctionnalités peuvent être désactivées."
51
52msgid "Username"
53msgstr "Nom d'utilisateur"
54
55msgid "Password"
56msgstr "Mot de passe"
57
58msgid "Stay signed in"
59msgstr "Rester connecté"
60
61msgid "(Do not check on public computers)"
62msgstr "(Ne pas cocher sur un ordinateur public)"
63
64msgid "Sign in"
65msgstr "Se connecter"
66
67msgid "back to home"
68msgstr "retour à l'accueil"
69
70msgid "favorites"
71msgstr "favoris"
72
73msgid "archive"
74msgstr "archive"
75
76msgid "unread"
77msgstr "non lus"
78
79msgid "Tag"
80msgstr "Tag"
81
82msgid "No articles found."
83msgstr "Aucun article trouvé."
84
85msgid "estimated reading time:"
86msgstr "temps de lecture estimé :"
87
88msgid "estimated reading time :"
89msgstr "temps de lecture estimé :"
90
91msgid "Toggle mark as read"
92msgstr "Marquer comme lu / non lu"
93
94msgid "toggle favorite"
95msgstr "marquer / enlever comme favori"
96
97msgid "delete"
98msgstr "supprimer"
99
100msgid "original"
101msgstr "original"
102
103msgid "Mark all the entries as read"
104msgstr "Marquer tous les articles comme lus"
105
106msgid "results"
107msgstr "résultats"
108
109msgid " found for « "
110msgstr "trouvé pour « "
111
112msgid "Only one result found for "
113msgstr "Seulement un résultat trouvé pour "
12 114
13msgid "config" 115msgid "config"
14msgstr "configuration" 116msgstr "configuration"
15 117
16msgid "Poching a link" 118msgid "Saving articles"
17msgstr "Pocher un lien" 119msgstr "Sauvegarde des articles"
120
121msgid "There are several ways to save an article:"
122msgstr "Il y a plusieurs façons d'enregistrer un article :"
18 123
19msgid "read the documentation" 124msgid "read the documentation"
20msgstr "lisez la documentation" 125msgstr "lisez la documentation"
21 126
22msgid "by filling this field" 127msgid "download the extension"
23msgstr "en remplissant ce champ" 128msgstr "téléchargez l'extension"
24 129
25msgid "poche it!" 130msgid "via F-Droid"
26msgstr "pochez-le !" 131msgstr "via F-Droid"
27 132
28msgid "Updating poche" 133msgid " or "
29msgstr "Mettre à jour poche" 134msgstr " ou "
30 135
31msgid "your version" 136msgid "via Google Play"
32msgstr "votre version" 137msgstr "via Google PlayStore"
33 138
34msgid "latest stable version" 139msgid "download the application"
35msgstr "dernire version stable" 140msgstr "tléchargez l'application"
36 141
37msgid "a more recent stable version is available." 142msgid "By filling this field"
38msgstr "une version stable plus récente est disponible." 143msgstr "En remplissant ce champ"
39 144
40msgid "you are up to date." 145msgid "bag it!"
41msgstr "vous êtes à jour." 146msgstr "bag it !"
42 147
43msgid "latest dev version" 148msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
44msgstr "dernire version de veloppement" 149msgstr "Bookmarklet : glissez-dposez ce lien dans votre barre de favoris"
45 150
46msgid "a more recent development version is available." 151msgid "Upgrading wallabag"
47msgstr "une version de développement plus récente est disponible." 152msgstr "Mise à jour de wallabag"
153
154msgid "Installed version"
155msgstr "Version installée"
156
157msgid "Latest stable version"
158msgstr "Dernière version stable"
159
160msgid "A more recent stable version is available."
161msgstr "Une version stable plus récente est disponible."
162
163msgid "You are up to date."
164msgstr "Vous êtes à jour."
165
166msgid "Last check:"
167msgstr "Dernière vérification: "
168
169msgid "Latest dev version"
170msgstr "Dernière version de développement"
171
172msgid "A more recent development version is available."
173msgstr "Une version de développement plus récente est disponible."
174
175msgid "You can clear cache to check the latest release."
176msgstr ""
177"Vous pouvez vider le cache pour vérifier que vous avez la dernière version."
178
179msgid "Feeds"
180msgstr "Flux"
181
182msgid ""
183"Your feed token is currently empty and must first be generated to enable "
184"feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
185msgstr ""
186"Votre jeton de flux est actuellement vide et doit d'abord être généré pour "
187"activer les flux. Cliquez <a href='?feed&amp;action=generate'>ici</a> pour "
188"le générer."
189
190msgid "Unread feed"
191msgstr "Flux des non lus"
192
193msgid "Favorites feed"
194msgstr "Flux des favoris"
195
196msgid "Archive feed"
197msgstr "Flux des archives"
198
199msgid "Your token:"
200msgstr "Votre jeton :"
201
202msgid "Your user id:"
203msgstr "Votre ID utilisateur :"
204
205msgid ""
206"You can regenerate your token: <a href='?feed&amp;action=generate'>generate!"
207"</a>."
208msgstr ""
209"Vous pouvez regénérer votre jeton : <a href='?feed&amp;"
210"action=generate'>génération !</a>."
211
212msgid "Change your theme"
213msgstr "Changer votre thème"
214
215msgid "Theme:"
216msgstr "Thème :"
217
218msgid "Update"
219msgstr "Mettre à jour"
220
221msgid "Change your language"
222msgstr "Changer votre langue"
223
224msgid "Language:"
225msgstr "Langue :"
48 226
49msgid "Change your password" 227msgid "Change your password"
50msgstr "Modifier votre mot de passe" 228msgstr "Modifier votre mot de passe"
@@ -52,80 +230,214 @@ msgstr "Modifier votre mot de passe"
52msgid "New password:" 230msgid "New password:"
53msgstr "Nouveau mot de passe :" 231msgstr "Nouveau mot de passe :"
54 232
55msgid "Password"
56msgstr "Mot de passe"
57
58msgid "Repeat your new password:" 233msgid "Repeat your new password:"
59msgstr "Répétez votre nouveau mot de passe :" 234msgstr "Répétez votre nouveau mot de passe :"
60 235
61msgid "Update"
62msgstr "Mettre à jour"
63
64msgid "Import" 236msgid "Import"
65msgstr "Importer" 237msgstr "Importer"
66 238
67msgid "Please execute the import script locally, it can take a very long time." 239msgid ""
68msgstr "Merci d'exécuter l'import en local, cela peut prendre du temps." 240"You can import your Pocket, Readability, Instapaper, Wallabag or any data in "
241"appropriate json or html format."
242msgstr ""
243"Vous pouvez importer depuis Pocket, Readability, Instapaper, wallabag, ou "
244"n'importe quel fichier au format JSON ou HTML approprié."
69 245
70msgid "More info in the official doc:" 246msgid ""
71msgstr "Plus d'infos sur la documentation officielle" 247"Please select export file on your computer and press \"Import\" button below."
248"<br>Wallabag will parse your file, insert all URLs and start fetching of "
249"articles if required.<br>Fetching process is controlled by two constants in "
250"your config file: IMPORT_LIMIT (how many articles are fetched at once) and "
251"IMPORT_DELAY (delay between fetch of next batch of articles)."
252msgstr ""
253"Sélectionner le fichier à importer sur votre disque dur, et pressez la "
254"bouton « Importer » ci-dessous.<br />wallabag analysera votre fichier, "
255"ajoutera toutes les URL trouvées et commencera à télécharger les contenus si "
256"nécessaire.<br />Le processus de téléchargement est contrôlé par deux "
257"constantes dans votre fichier de configuration:<wbr /><code>IMPORT_LIMIT</"
258"code> (nombre d'éléments téléchargés à la fois) et <code>IMPORT_DELAY</code> "
259"(le délai d'attente entre deux séquences de téléchargement)."
260
261msgid "File:"
262msgstr "Fichier : "
263
264msgid "You can click here to fetch content for articles with no content."
265msgstr ""
266"Vous pouvez cliquer ici pour télécharger le contenu des articles vides."
72 267
73msgid "import from Pocket" 268msgid "Export your wallabag data"
74msgstr "import depuis Pocket" 269msgstr "Exporter vos données de wallabag"
75 270
76msgid "import from Readability" 271msgid "Click here"
77msgstr "import depuis Readability" 272msgstr "Cliquez ici"
78 273
79msgid "import from Instapaper" 274msgid "to download your database."
80msgstr "import depuis Instapaper" 275msgstr "pour télécharger votre base de dones."
81 276
82msgid "Export your poche data" 277msgid "to export your wallabag data."
83msgstr "Exporter vos données de poche" 278msgstr "pour exporter vos données de wallabag."
84 279
85msgid "Click here" 280msgid "Cache"
86msgstr "Cliquez-ici" 281msgstr "Cache"
87 282
88msgid "to export your poche data." 283msgid "to delete cache."
89msgstr "pour exporter vos données de poche." 284msgstr "pour effacer le cache."
90 285
91msgid "back to home" 286msgid "Add user"
92msgstr "retour à l'accueil" 287msgstr "Ajouter un utilisateur"
93 288
94msgid "installation" 289msgid "Add a new user :"
95msgstr "installation" 290msgstr "Ajouter un nouvel utilisateur : "
291
292msgid "Login for new user"
293msgstr "Identifiant du nouvel utilisateur"
294
295msgid "Login"
296msgstr "Nom d'utilisateur"
297
298msgid "Password for new user"
299msgstr "Mot de passe du nouvel utilisateur"
300
301msgid "Send"
302msgstr "Envoyer"
303
304msgid "Delete account"
305msgstr "Supprimer le compte"
96 306
97msgid "install your poche" 307msgid "You can delete your account by entering your password and validating."
98msgstr "installez votre poche" 308msgstr ""
309"Vous pouvez supprimer votre compte en entrant votre mot de passe et en "
310"validant."
311
312msgid "Be careful, data will be erased forever (that is a very long time)."
313msgstr "Attention, les données seront perdues pour toujours."
314
315msgid "Type here your password"
316msgstr "Entrez votre mot de passe ici"
317
318msgid "You are the only user, you cannot delete your own account."
319msgstr ""
320"Vous êtes l'unique utilisateur, vous ne pouvez pas supprimer votre compte."
99 321
100msgid "" 322msgid ""
101"poche is still not installed. Please fill the below form to install it. " 323"To completely remove wallabag, delete the wallabag folder on your web server."
102"Don't hesitate to <a href='http://doc.inthepoche.com'>read the documentation "
103"on poche website</a>."
104msgstr "" 324msgstr ""
105"poche n'est pas encore installé. Merci de remplir le formulaire suivant pour " 325"Pour désinstaller complètement wallabag, supprimez le répertoire "
106"l'installer. N'hésitez pas à <a href='http://doc.inthepoche.com'>lire la " 326"<code>wallabag</code> de votre serveur Web."
107"documentation sur le site de poche</a>."
108 327
109msgid "Login" 328msgid "Save a link"
110msgstr "Nom d'utilisateur" 329msgstr "Ajouter un lien"
111 330
112msgid "Repeat your password" 331msgid "Return home"
113msgstr "Répétez votre mot de passe" 332msgstr "Retour accueil"
114 333
115msgid "Install" 334msgid "Back to top"
116msgstr "Installer" 335msgstr "Haut de page"
336
337msgid "Mark as read"
338msgstr "Marquer comme lu"
339
340msgid "Favorite"
341msgstr "Favoris"
342
343msgid "Toggle favorite"
344msgstr "Marquer / enlever comme favori"
117 345
118msgid "back to top" 346msgid "Delete"
119msgstr "retour en haut de page" 347msgstr "Effacer"
348
349msgid "Tweet"
350msgstr "Tweet"
351
352msgid "Email"
353msgstr "E-mail"
354
355msgid "shaarli"
356msgstr "Shaarli"
357
358msgid "flattr"
359msgstr "Flattr"
360
361msgid "Print"
362msgstr "Imprimer"
363
364msgid "Does this article appear wrong?"
365msgstr "Cet article s'affiche mal ?"
366
367msgid "tags:"
368msgstr "tags :"
369
370msgid "Edit tags"
371msgstr "Modifier les tags"
120 372
121msgid "favoris" 373msgid "favoris"
122msgstr "favoris" 374msgstr "favoris"
123 375
124msgid "archive" 376msgid "mark all the entries as read"
125msgstr "archive" 377msgstr "marquer tous les articles comme lus"
126 378
127msgid "unread" 379msgid "toggle view mode"
128msgstr "non lus" 380msgstr "changer de mode de visualisation"
381
382msgid "return home"
383msgstr "retour à l'accueil"
384
385msgid "Poching a link"
386msgstr "Enregistrer un lien"
387
388msgid "by filling this field"
389msgstr "en remplissant ce champ"
390
391msgid "bookmarklet: drag & drop this link to your bookmarks bar"
392msgstr "bookmarklet : glissez-déposez ce lien dans votre barre de favoris"
393
394msgid "your version"
395msgstr "votre version"
396
397msgid "latest stable version"
398msgstr "dernière version stable"
399
400msgid "a more recent stable version is available."
401msgstr "une version stable plus récente est disponible."
402
403msgid "you are up to date."
404msgstr "vous êtes à jour."
405
406msgid "latest dev version"
407msgstr "dernière version de développement"
408
409msgid "a more recent development version is available."
410msgstr "une version de développement plus récente est disponible."
411
412msgid "Please execute the import script locally, it can take a very long time."
413msgstr ""
414"Merci d'exécuter le script d'importation en local car cela peut prendre du "
415"temps."
416
417msgid "More infos in the official doc:"
418msgstr "Plus d'infos dans la documentation officielle :"
419
420msgid "import from Pocket"
421msgstr "importer depuis Pocket"
422
423#, php-format
424msgid "(you must have a %s file on your server)"
425msgstr "(le fichier %s doit être présent sur le serveur)"
426
427msgid "import from Readability"
428msgstr "importer depuis Readability"
429
430msgid "import from Instapaper"
431msgstr "importer depuis Instapaper"
432
433msgid "Start typing for auto complete."
434msgstr "Commencez à taper pour activer l'auto-complétion."
435
436msgid "You can enter multiple tags, separated by commas."
437msgstr "Vous pouvez entrer plusieurs tags, séparés par des virgules."
438
439msgid "return to article"
440msgstr "retourner à l'article"
129 441
130msgid "by date asc" 442msgid "by date asc"
131msgstr "par date asc" 443msgstr "par date asc"
@@ -145,84 +457,196 @@ msgstr "par titre"
145msgid "by title desc" 457msgid "by title desc"
146msgstr "par titre desc" 458msgstr "par titre desc"
147 459
460msgid "home"
461msgstr "accueil"
462
463msgid "tags"
464msgstr "tags"
465
466msgid "save a link"
467msgstr "sauver un lien"
468
469msgid "search"
470msgstr "rechercher"
471
472msgid "logout"
473msgstr "déconnexion"
474
475msgid "installation"
476msgstr "installation"
477
478msgid "install your wallabag"
479msgstr "installez votre wallabag"
480
481msgid ""
482"wallabag is still not installed. Please fill the below form to install it. "
483"Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation "
484"on wallabag website</a>."
485msgstr ""
486"wallabag n'est pas encore installé. Merci de remplir le formulaire suivant "
487"pour l'installer. N'hésitez pas à <a href='http://doc.wallabag.org'>lire la "
488"documentation sur le site de wallabag</a>."
489
490msgid "Repeat your password"
491msgstr "Répétez votre mot de passe"
492
493msgid "Install"
494msgstr "Installer"
495
496msgid ""
497"You can <a href='wallabag_compatibility_test.php'>check your configuration "
498"here</a>."
499msgstr ""
500"Vous pouvez vérifier votre configuration <a "
501"href='wallabag_compatibility_test.php'>ici</a>."
502
503msgid "Tags"
504msgstr "Tags"
505
148msgid "No link available here!" 506msgid "No link available here!"
149msgstr "Aucun lien n'est disponible ici !" 507msgstr "Aucun lien n'est disponible ici !"
150 508
151msgid "toggle mark as read" 509msgid "toggle mark as read"
152msgstr "marquer comme lu / non lu" 510msgstr "marquer comme lu / non lu"
153 511
154msgid "toggle favorite" 512msgid "tweet"
155msgstr "marquer comme favori" 513msgstr "tweet"
156 514
157msgid "delete" 515msgid "email"
158msgstr "supprimer" 516msgstr "e-mail"
159 517
160msgid "original" 518msgid "this article appears wrong?"
161msgstr "original" 519msgstr "cet article s'affiche mal ?"
162 520
163msgid "results" 521msgid "Search"
164msgstr "résultats" 522msgstr "Rechercher"
165 523
166msgid "tweet" 524msgid "Download required for "
167msgstr "tweet" 525msgstr "Téléchargement requis pour "
168 526
169msgid "email" 527msgid "records"
170msgstr "email" 528msgstr " articles"
171 529
172msgid "shaarli" 530msgid "Downloading next "
173msgstr "shaarli" 531msgstr "Téléchargement des "
174 532
175msgid "flattr" 533msgid "articles, please wait"
176msgstr "flattr" 534msgstr " articles suivants, veuillez patienter..."
177 535
178msgid "this article appears wrong?" 536msgid "Enter your search here"
179msgstr "cet article s'affiche mal ?" 537msgstr "Entrez votre recherche ici"
180 538
181msgid "create an issue" 539#, php-format
182msgstr "créez un ticket" 540msgid ""
541"The new user %s has been installed. Do you want to <a href=\"?logout"
542"\">logout ?</a>"
543msgstr ""
544"Le nouvel utilisateur « %s » a été ajouté. Voulez-vous vous <a href=\"?"
545"logout\">déconnecter ?</a>"
183 546
184msgid "or" 547#, php-format
185msgstr "ou" 548msgid "Error : An user with the name %s already exists !"
549msgstr "Erreur : Un utilisateur avec le nom « %s » existe déjà !"
186 550
187msgid "contact us by mail" 551#, php-format
188msgstr "contactez-nous par email" 552msgid "User %s has been successfully deleted !"
553msgstr "L'utilisateur « %s » a été supprimé avec succès !"
189 554
190msgid "plop" 555msgid "Error : The password is wrong !"
191msgstr "plop" 556msgstr "Erreur : le mot de passe est incorrect !"
192 557
193msgid "home" 558msgid "Error : You are the only user, you cannot delete your account !"
194msgstr "accueil" 559msgstr ""
560"Erreur : Vous êtes l'unique utilisateur, vous ne pouvez pas supprimer votre "
561"compte !"
195 562
196msgid "favorites" 563msgid "Untitled"
197msgstr "favoris" 564msgstr "Sans titre"
198 565
199msgid "logout" 566msgid "the link has been added successfully"
200msgstr "déconnexion" 567msgstr "le lien a été ajouté avec succès"
201 568
202msgid "powered by" 569msgid "error during insertion : the link wasn't added"
203msgstr "propulsé par" 570msgstr "erreur pendant l'insertion : le lien n'a pas é ajouté"
204 571
205msgid "debug mode is on so cache is off." 572msgid "the link has been deleted successfully"
206msgstr "le mode de debug est actif, le cache est donc sactiv." 573msgstr "le lien a été effacé avec succs"
207 574
208msgid "your poche version:" 575msgid "the link wasn't deleted"
209msgstr "votre version de poche :" 576msgstr "le lien n'a pas été effacé"
210 577
211msgid "storage:" 578msgid "Article not found!"
212msgstr "stockage :" 579msgstr "Article non trouvé !"
213 580
214msgid "login to your poche" 581msgid "previous"
215msgstr "se connecter votre poche" 582msgstr "prcédent"
216 583
217msgid "you are in demo mode, some features may be disabled." 584msgid "next"
585msgstr "suivant"
586
587msgid "in demo mode, you can't update your password"
588msgstr "en mode démo, vous ne pouvez pas mettre à jour votre mot de passe"
589
590msgid "your password has been updated"
591msgstr "votre mot de passe a été mis à jour"
592
593msgid ""
594"the two fields have to be filled & the password must be the same in the two "
595"fields"
218msgstr "" 596msgstr ""
219"vous êtes en mode démo, certaines fonctionnalités peuvent être désactivées." 597"les deux champs doivent être remplis & le mot de passe doit être le même "
598"dans les deux"
220 599
221msgid "Stay signed in" 600msgid "still using the \""
222msgstr "Rester connecté" 601msgstr "vous utilisez toujours \""
223 602
224msgid "(Do not check on public computers)" 603msgid "that theme does not seem to be installed"
225msgstr "(ne pas cocher sur un ordinateur public)" 604msgstr "ce thème ne semble pas instal"
226 605
227msgid "Sign in" 606msgid "you have changed your theme preferences"
228msgstr "Se connecter" 607msgstr "vous avez changé vos préférences de thème"
608
609msgid "that language does not seem to be installed"
610msgstr "cette langue ne semble pas être installée"
611
612msgid "you have changed your language preferences"
613msgstr "vous avez changé vos préférences de langue"
614
615msgid "login failed: you have to fill all fields"
616msgstr "échec de l'identification : vous devez remplir tous les champs"
617
618msgid "welcome to your wallabag"
619msgstr "bienvenue dans votre wallabag"
620
621msgid "login failed: bad login or password"
622msgstr "échec de l'identification : mauvais identifiant ou mot de passe"
623
624msgid "Untitled - Import - "
625msgstr "Sans titre - Importer - "
626
627msgid "click to finish import"
628msgstr "cliquez pour terminer l'importation"
629
630msgid "Articles inserted: "
631msgstr "Articles ajoutés : "
632
633msgid ". Please note, that some may be marked as \"read\"."
634msgstr ". Notez que certains pourraient être marqués comme \"lus\"."
635
636msgid "Import finished."
637msgstr "Importation terminée."
638
639msgid "Undefined"
640msgstr "Non défini"
641
642msgid "User with this id ("
643msgstr "Utilisateur avec cet identifiant ("
644
645msgid "Uh, there is a problem while generating feeds."
646msgstr "Hum, il y a un problème lors de la génération des flux."
647
648msgid "Cache deleted."
649msgstr "Cache effacé."
650
651msgid "Oops, it seems you don't have PHP 5."
652msgstr "Oups, vous ne semblez pas avoir PHP 5."
diff --git a/locale/it_IT.utf8/LC_MESSAGES/it_IT.utf8.po b/locale/it_IT.utf8/LC_MESSAGES/it_IT.utf8.po
index 429d5d55..41cc01e0 100644
--- a/locale/it_IT.utf8/LC_MESSAGES/it_IT.utf8.po
+++ b/locale/it_IT.utf8/LC_MESSAGES/it_IT.utf8.po
@@ -4,54 +4,138 @@
4msgid "" 4msgid ""
5msgstr "" 5msgstr ""
6"Project-Id-Version: poche\n" 6"Project-Id-Version: poche\n"
7"POT-Creation-Date: \n" 7"Report-Msgid-Bugs-To: \n"
8"PO-Revision-Date: 2013-11-25 09:47+0100\n" 8"POT-Creation-Date: 2014-02-25 15:13+0300\n"
9"Last-Translator: Nicolas Lœuillet <nicolas.loeuillet@gmail.com>\n" 9"PO-Revision-Date: 2014-02-25 15:13+0300\n"
10"Language-Team: Italian (http://www.transifex.com/projects/p/poche/language/" 10"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n"
11"it/)\n" 11"Language-Team: Italian (http://www.transifex.com/projects/p/poche/language/it/)\n"
12"Language: it\n"
12"MIME-Version: 1.0\n" 13"MIME-Version: 1.0\n"
13"Content-Type: text/plain; charset=UTF-8\n" 14"Content-Type: text/plain; charset=UTF-8\n"
14"Content-Transfer-Encoding: 8bit\n" 15"Content-Transfer-Encoding: 8bit\n"
15"Language: it\n"
16"Plural-Forms: nplurals=2; plural=(n != 1);\n" 16"Plural-Forms: nplurals=2; plural=(n != 1);\n"
17"X-Generator: Poedit 1.5.4\n" 17"X-Generator: Poedit 1.5.4\n"
18"X-Poedit-Language: Italian\n"
19"X-Poedit-Country: ITALY\n"
20"X-Poedit-Basepath: .\n"
21"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag-master-testing\n"
22
23msgid "wallabag, a read it later open source system"
24msgstr ""
25
26msgid "login failed: user doesn't exist"
27msgstr ""
28
29msgid "return home"
30msgstr ""
18 31
19msgid "config" 32msgid "config"
20msgstr "configurazione" 33msgstr "configurazione"
21 34
22msgid "Poching a link" 35msgid "Saving articles"
23msgstr "Pochare un link" 36msgstr ""
37
38msgid "There are several ways to save an article:"
39msgstr ""
24 40
25msgid "read the documentation" 41msgid "read the documentation"
26msgstr "leggi la documentazione" 42msgstr "leggi la documentazione"
27 43
28msgid "by filling this field" 44msgid "download the extension"
45msgstr ""
46
47msgid "via F-Droid"
48msgstr ""
49
50msgid " or "
51msgstr ""
52
53msgid "via Google Play"
54msgstr ""
55
56msgid "download the application"
57msgstr ""
58
59#, fuzzy
60msgid "By filling this field"
29msgstr "compilando questo campo" 61msgstr "compilando questo campo"
30 62
31msgid "poche it!" 63msgid "bag it!"
32msgstr "pochalo!" 64msgstr ""
33 65
34msgid "Updating poche" 66msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
35msgstr "Aggiornamento poche" 67msgstr ""
36 68
37msgid "your version" 69msgid "Upgrading wallabag"
38msgstr "la tua versione" 70msgstr ""
39 71
40msgid "latest stable version" 72#, fuzzy
73msgid "Installed version"
41msgstr "ultima versione stabile" 74msgstr "ultima versione stabile"
42 75
43msgid "a more recent stable version is available." 76#, fuzzy
77msgid "Latest stable version"
78msgstr "ultima versione stabile"
79
80#, fuzzy
81msgid "A more recent stable version is available."
44msgstr "è disponibile una versione stabile più recente." 82msgstr "è disponibile una versione stabile più recente."
45 83
46msgid "you are up to date." 84#, fuzzy
85msgid "You are up to date."
47msgstr "sei aggiornato." 86msgstr "sei aggiornato."
48 87
49msgid "latest dev version" 88#, fuzzy
89msgid "Latest dev version"
50msgstr "ultima versione di sviluppo" 90msgstr "ultima versione di sviluppo"
51 91
52msgid "a more recent development version is available." 92#, fuzzy
93msgid "A more recent development version is available."
53msgstr "è disponibile una versione di sviluppo più recente." 94msgstr "è disponibile una versione di sviluppo più recente."
54 95
96msgid "Feeds"
97msgstr ""
98
99msgid "Your feed token is currently empty and must first be generated to enable feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
100msgstr ""
101
102msgid "Unread feed"
103msgstr ""
104
105#, fuzzy
106msgid "Favorites feed"
107msgstr "preferiti"
108
109#, fuzzy
110msgid "Archive feed"
111msgstr "archivio"
112
113msgid "Your token:"
114msgstr ""
115
116msgid "Your user id:"
117msgstr ""
118
119msgid "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>."
120msgstr ""
121
122#, fuzzy
123msgid "Change your theme"
124msgstr "Cambia la tua password"
125
126msgid "Theme:"
127msgstr ""
128
129msgid "Update"
130msgstr "Aggiorna"
131
132#, fuzzy
133msgid "Change your language"
134msgstr "Cambia la tua password"
135
136msgid "Language:"
137msgstr ""
138
55msgid "Change your password" 139msgid "Change your password"
56msgstr "Cambia la tua password" 140msgstr "Cambia la tua password"
57 141
@@ -64,67 +148,68 @@ msgstr "Password"
64msgid "Repeat your new password:" 148msgid "Repeat your new password:"
65msgstr "Ripeti la nuova password:" 149msgstr "Ripeti la nuova password:"
66 150
67msgid "Update"
68msgstr "Aggiorna"
69
70msgid "Import" 151msgid "Import"
71msgstr "Importa" 152msgstr "Importa"
72 153
73msgid "Please execute the import script locally, it can take a very long time." 154#, fuzzy
74msgstr "" 155msgid "Please execute the import script locally as it can take a very long time."
75"Si prega di eseguire lo script di importazione a livello locale, può " 156msgstr "Si prega di eseguire lo script di importazione a livello locale, può richiedere un tempo molto lungo."
76"richiedere un tempo molto lungo."
77 157
78msgid "More info in the official doc:" 158#, fuzzy
159msgid "More info in the official documentation:"
79msgstr "Maggiori info nella documentazione ufficiale" 160msgstr "Maggiori info nella documentazione ufficiale"
80 161
81msgid "import from Pocket" 162#, fuzzy
163msgid "Import from Pocket"
82msgstr "Importa da Pocket" 164msgstr "Importa da Pocket"
83 165
84msgid "import from Readability" 166#, php-format
167msgid "(you must have a %s file on your server)"
168msgstr ""
169
170#, fuzzy
171msgid "Import from Readability"
85msgstr "Importa da Readability" 172msgstr "Importa da Readability"
86 173
87msgid "import from Instapaper" 174#, fuzzy
175msgid "Import from Instapaper"
88msgstr "Importa da Instapaper" 176msgstr "Importa da Instapaper"
89 177
90msgid "Export your poche data" 178#, fuzzy
179msgid "Import from wallabag"
180msgstr "Importa da Readability"
181
182#, fuzzy
183msgid "Export your wallabag data"
91msgstr "Esporta i tuoi dati di poche" 184msgstr "Esporta i tuoi dati di poche"
92 185
93msgid "Click here" 186msgid "Click here"
94msgstr "Fai clic qui" 187msgstr "Fai clic qui"
95 188
96msgid "to export your poche data." 189msgid "to download your database."
97msgstr "per esportare i tuoi dati di poche." 190msgstr ""
98
99msgid "back to home"
100msgstr "torna alla home"
101 191
102msgid "installation" 192#, fuzzy
103msgstr "installazione" 193msgid "to export your wallabag data."
194msgstr "per esportare i tuoi dati di poche."
104 195
105msgid "install your poche" 196msgid "Cache"
106msgstr "installa il tuo poche" 197msgstr ""
107 198
108msgid "" 199msgid "to delete cache."
109"poche is still not installed. Please fill the below form to install it. "
110"Don't hesitate to <a href='http://doc.inthepoche.com'>read the documentation "
111"on poche website</a>."
112msgstr "" 200msgstr ""
113"poche non è ancora installato. Si prega di riempire il modulo sottostante "
114"per completare l'installazione. <a href='http://doc.inthepoche.com'>Leggere "
115"la documentazione sul sito di poche</a>."
116 201
117msgid "Login" 202msgid "You can enter multiple tags, separated by commas."
118msgstr "Nome utente" 203msgstr ""
119 204
120msgid "Repeat your password" 205msgid "return to article"
121msgstr "Ripeti la tua password" 206msgstr ""
122 207
123msgid "Install" 208msgid "plop"
124msgstr "Installa" 209msgstr "plop"
125 210
126msgid "back to top" 211msgid "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>."
127msgstr "torna a inizio pagina" 212msgstr ""
128 213
129msgid "favoris" 214msgid "favoris"
130msgstr "preferiti" 215msgstr "preferiti"
@@ -153,10 +238,14 @@ msgstr "per titolo"
153msgid "by title desc" 238msgid "by title desc"
154msgstr "per titolo decr" 239msgstr "per titolo decr"
155 240
156msgid "No link available here!" 241msgid "Tag"
157msgstr "Nessun link disponibile!" 242msgstr ""
158 243
159msgid "toggle mark as read" 244msgid "No articles found."
245msgstr ""
246
247#, fuzzy
248msgid "Toggle mark as read"
160msgstr "segna come letto / non letto" 249msgstr "segna come letto / non letto"
161 250
162msgid "toggle favorite" 251msgid "toggle favorite"
@@ -168,13 +257,95 @@ msgstr "elimina"
168msgid "original" 257msgid "original"
169msgstr "originale" 258msgstr "originale"
170 259
260msgid "estimated reading time:"
261msgstr ""
262
263msgid "mark all the entries as read"
264msgstr ""
265
171msgid "results" 266msgid "results"
172msgstr "risultati" 267msgstr "risultati"
173 268
174msgid "tweet" 269msgid "installation"
270msgstr "installazione"
271
272#, fuzzy
273msgid "install your wallabag"
274msgstr "installa il tuo poche"
275
276#, fuzzy
277msgid "wallabag is still not installed. Please fill the below form to install it. Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation on wallabag website</a>."
278msgstr "poche non è ancora installato. Si prega di riempire il modulo sottostante per completare l'installazione. <a href='http://doc.inthepoche.com'>Leggere la documentazione sul sito di poche</a>."
279
280msgid "Login"
281msgstr "Nome utente"
282
283msgid "Repeat your password"
284msgstr "Ripeti la tua password"
285
286msgid "Install"
287msgstr "Installa"
288
289#, fuzzy
290msgid "login to your wallabag"
291msgstr "accedi al tuo poche"
292
293msgid "Login to wallabag"
294msgstr ""
295
296msgid "you are in demo mode, some features may be disabled."
297msgstr "sei in modalità dimostrazione, alcune funzionalità potrebbero essere disattivate."
298
299msgid "Username"
300msgstr ""
301
302msgid "Stay signed in"
303msgstr "Resta connesso"
304
305msgid "(Do not check on public computers)"
306msgstr "(non selezionare su computer pubblici)"
307
308msgid "Sign in"
309msgstr "Accedi"
310
311msgid "favorites"
312msgstr "preferiti"
313
314msgid "estimated reading time :"
315msgstr ""
316
317msgid "Mark all the entries as read"
318msgstr ""
319
320msgid "Return home"
321msgstr ""
322
323#, fuzzy
324msgid "Back to top"
325msgstr "torna a inizio pagina"
326
327#, fuzzy
328msgid "Mark as read"
329msgstr "segna come letto / non letto"
330
331#, fuzzy
332msgid "Favorite"
333msgstr "preferiti"
334
335#, fuzzy
336msgid "Toggle favorite"
337msgstr "segna come preferito"
338
339#, fuzzy
340msgid "Delete"
341msgstr "elimina"
342
343#, fuzzy
344msgid "Tweet"
175msgstr "twitta" 345msgstr "twitta"
176 346
177msgid "email" 347#, fuzzy
348msgid "Email"
178msgstr "email" 349msgstr "email"
179 350
180msgid "shaarli" 351msgid "shaarli"
@@ -183,26 +354,24 @@ msgstr "shaarli"
183msgid "flattr" 354msgid "flattr"
184msgstr "flattr" 355msgstr "flattr"
185 356
186msgid "this article appears wrong?" 357#, fuzzy
358msgid "Does this article appear wrong?"
187msgstr "articolo non visualizzato correttamente?" 359msgstr "articolo non visualizzato correttamente?"
188 360
189msgid "create an issue" 361msgid "tags:"
190msgstr "crea una segnalazione" 362msgstr ""
191
192msgid "or"
193msgstr "oppure"
194 363
195msgid "contact us by mail" 364msgid "Edit tags"
196msgstr "contattaci via email" 365msgstr ""
197 366
198msgid "plop" 367msgid "save link!"
199msgstr "plop" 368msgstr ""
200 369
201msgid "home" 370msgid "home"
202msgstr "home" 371msgstr "home"
203 372
204msgid "favorites" 373msgid "tags"
205msgstr "preferiti" 374msgstr ""
206 375
207msgid "logout" 376msgid "logout"
208msgstr "esci" 377msgstr "esci"
@@ -213,25 +382,187 @@ msgstr "realizzato con"
213msgid "debug mode is on so cache is off." 382msgid "debug mode is on so cache is off."
214msgstr "modalità di debug attiva, cache disattivata." 383msgstr "modalità di debug attiva, cache disattivata."
215 384
216msgid "your poche version:" 385#, fuzzy
217msgstr "la tua versione di poche:" 386msgid "your wallabag version:"
387msgstr "la tua versione"
218 388
219msgid "storage:" 389msgid "storage:"
220msgstr "memoria:" 390msgstr "memoria:"
221 391
222msgid "login to your poche" 392msgid "save a link"
223msgstr "accedi al tuo poche" 393msgstr ""
224 394
225msgid "you are in demo mode, some features may be disabled." 395msgid "back to home"
396msgstr "torna alla home"
397
398msgid "toggle mark as read"
399msgstr "segna come letto / non letto"
400
401msgid "tweet"
402msgstr "twitta"
403
404msgid "email"
405msgstr "email"
406
407msgid "this article appears wrong?"
408msgstr "articolo non visualizzato correttamente?"
409
410msgid "No link available here!"
411msgstr "Nessun link disponibile!"
412
413msgid "Poching a link"
414msgstr "Pochare un link"
415
416msgid "by filling this field"
417msgstr "compilando questo campo"
418
419msgid "bookmarklet: drag & drop this link to your bookmarks bar"
226msgstr "" 420msgstr ""
227"sei in modalità dimostrazione, alcune funzionalità potrebbero essere "
228"disattivate."
229 421
230msgid "Stay signed in" 422msgid "your version"
231msgstr "Resta connesso" 423msgstr "la tua versione"
232 424
233msgid "(Do not check on public computers)" 425msgid "latest stable version"
234msgstr "(non selezionare su computer pubblici)" 426msgstr "ultima versione stabile"
235 427
236msgid "Sign in" 428msgid "a more recent stable version is available."
237msgstr "Accedi" 429msgstr "è disponibile una versione stabile più recente."
430
431msgid "you are up to date."
432msgstr "sei aggiornato."
433
434msgid "latest dev version"
435msgstr "ultima versione di sviluppo"
436
437msgid "a more recent development version is available."
438msgstr "è disponibile una versione di sviluppo più recente."
439
440msgid "Please execute the import script locally, it can take a very long time."
441msgstr "Si prega di eseguire lo script di importazione a livello locale, può richiedere un tempo molto lungo."
442
443#, fuzzy
444msgid "More infos in the official doc:"
445msgstr "Maggiori info nella documentazione ufficiale"
446
447msgid "import from Pocket"
448msgstr "Importa da Pocket"
449
450msgid "import from Readability"
451msgstr "Importa da Readability"
452
453msgid "import from Instapaper"
454msgstr "Importa da Instapaper"
455
456msgid "Tags"
457msgstr ""
458
459#, fuzzy
460msgid "Untitled"
461msgstr "per titolo"
462
463msgid "the link has been added successfully"
464msgstr ""
465
466msgid "error during insertion : the link wasn't added"
467msgstr ""
468
469msgid "the link has been deleted successfully"
470msgstr ""
471
472msgid "the link wasn't deleted"
473msgstr ""
474
475msgid "Article not found!"
476msgstr ""
477
478msgid "previous"
479msgstr ""
480
481msgid "next"
482msgstr ""
483
484msgid "in demo mode, you can't update your password"
485msgstr ""
486
487msgid "your password has been updated"
488msgstr ""
489
490msgid "the two fields have to be filled & the password must be the same in the two fields"
491msgstr ""
492
493msgid "still using the \""
494msgstr ""
495
496msgid "that theme does not seem to be installed"
497msgstr ""
498
499msgid "you have changed your theme preferences"
500msgstr ""
501
502msgid "that language does not seem to be installed"
503msgstr ""
504
505msgid "you have changed your language preferences"
506msgstr ""
507
508msgid "login failed: you have to fill all fields"
509msgstr ""
510
511msgid "welcome to your wallabag"
512msgstr ""
513
514msgid "login failed: bad login or password"
515msgstr ""
516
517#, fuzzy
518msgid "import from instapaper completed"
519msgstr "Importa da Instapaper"
520
521#, fuzzy
522msgid "import from pocket completed"
523msgstr "Importa da Pocket"
524
525#, fuzzy
526msgid "import from Readability completed. "
527msgstr "Importa da Readability"
528
529#, fuzzy
530msgid "import from Poche completed. "
531msgstr "Importa da Pocket"
532
533msgid "Unknown import provider."
534msgstr ""
535
536msgid "Incomplete inc/poche/define.inc.php file, please define \""
537msgstr ""
538
539msgid "Could not find required \""
540msgstr ""
541
542msgid "Uh, there is a problem while generating feeds."
543msgstr ""
544
545#, fuzzy
546msgid "Cache deleted."
547msgstr "elimina"
548
549msgid "Oops, it seems you don't have PHP 5."
550msgstr ""
551
552#~ msgid "poche it!"
553#~ msgstr "pochalo!"
554
555#~ msgid "Updating poche"
556#~ msgstr "Aggiornamento poche"
557
558#~ msgid "create an issue"
559#~ msgstr "crea una segnalazione"
560
561#~ msgid "or"
562#~ msgstr "oppure"
563
564#~ msgid "contact us by mail"
565#~ msgstr "contattaci via email"
566
567#~ msgid "your poche version:"
568#~ msgstr "la tua versione di poche:"
diff --git a/locale/pl_PL.utf8/LC_MESSAGES/pl_PL.utf8.mo b/locale/pl_PL.utf8/LC_MESSAGES/pl_PL.utf8.mo
index 297516c0..297516c0 100755..100644
--- a/locale/pl_PL.utf8/LC_MESSAGES/pl_PL.utf8.mo
+++ b/locale/pl_PL.utf8/LC_MESSAGES/pl_PL.utf8.mo
Binary files differ
diff --git a/locale/pt_BR.utf8/LC_MESSAGES/pt_BR.utf8.mo b/locale/pt_BR.utf8/LC_MESSAGES/pt_BR.utf8.mo
new file mode 100644
index 00000000..c1ce317f
--- /dev/null
+++ b/locale/pt_BR.utf8/LC_MESSAGES/pt_BR.utf8.mo
Binary files differ
diff --git a/locale/pt_BR.utf8/LC_MESSAGES/pt_BR.utf8.po b/locale/pt_BR.utf8/LC_MESSAGES/pt_BR.utf8.po
new file mode 100644
index 00000000..e1453922
--- /dev/null
+++ b/locale/pt_BR.utf8/LC_MESSAGES/pt_BR.utf8.po
@@ -0,0 +1,549 @@
1msgid ""
2msgstr ""
3"Project-Id-Version: wallabag\n"
4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2014-02-25 15:17+0300\n"
6"PO-Revision-Date: \n"
7"Last-Translator: @iancamporez <ian@camporez.com>\n"
8"Language-Team: @iancamporez <ian@camporez.com>\n"
9"Language: pt_BR\n"
10"MIME-Version: 1.0\n"
11"Content-Type: text/plain; charset=UTF-8\n"
12"Content-Transfer-Encoding: 8bit\n"
13"X-Generator: Poedit 1.6.4\n"
14"X-Poedit-Basepath: .\n"
15"X-Poedit-SearchPath-0: /home/ian/Projetos/wallabag/locale/en_EN.utf8/"
16"LC_MESSAGES\n"
17
18msgid "wallabag, a read it later open source system"
19msgstr "wallabag, um \"read it later\" de código aberto"
20
21msgid "login failed: user doesn't exist"
22msgstr "falha ao entrar: o usuário não existe"
23
24msgid "return home"
25msgstr "retornar à página inicial"
26
27msgid "config"
28msgstr "configurar"
29
30msgid "Saving articles"
31msgstr "Salvando artigos"
32
33msgid "There are several ways to save an article:"
34msgstr "Existem várias maneiras de salvar um artigo:"
35
36msgid "read the documentation"
37msgstr "ler a documentação"
38
39msgid "download the extension"
40msgstr "baixar a extensão"
41
42msgid "via F-Droid"
43msgstr "via F-Droid"
44
45msgid " or "
46msgstr "ou "
47
48msgid "via Google Play"
49msgstr "via Google Play"
50
51msgid "download the application"
52msgstr "baixar o aplicativo"
53
54msgid "By filling this field"
55msgstr "Preenchendo esse campo"
56
57msgid "bag it!"
58msgstr "adicionar ao wallabag"
59
60msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
61msgstr "Bookmarklet: arraste esse link para sua barra de favoritos"
62
63msgid "Upgrading wallabag"
64msgstr "Atualizando o wallabag"
65
66msgid "Installed version"
67msgstr "Versão instalada"
68
69msgid "Latest stable version"
70msgstr "Última versão estável"
71
72msgid "A more recent stable version is available."
73msgstr "Uma versão estável mais nova está disponível"
74
75msgid "You are up to date."
76msgstr "Você está atualizado."
77
78msgid "Latest dev version"
79msgstr "Última versão em desenvolvimento"
80
81msgid "A more recent development version is available."
82msgstr "Uma versão em desenvolvimento mais recente está disponível"
83
84msgid "Feeds"
85msgstr "Feeds"
86
87msgid ""
88"Your feed token is currently empty and must first be generated to enable "
89"feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
90msgstr ""
91"Seu token do feed e precisa ser gerado para ativar os feeds. Clique <a "
92"href='?feed&amp;action=generate'>aqui para gerar um token</a>."
93
94msgid "Unread feed"
95msgstr "Feed de artigos não lidos"
96
97msgid "Favorites feed"
98msgstr "Feed dos favoritos"
99
100msgid "Archive feed"
101msgstr "Feed de arquivados"
102
103msgid "Your token:"
104msgstr "Seu token:"
105
106msgid "Your user id:"
107msgstr "Seu código de usuário:"
108
109msgid ""
110"You can regenerate your token: <a href='?feed&amp;action=generate'>generate!"
111"</a>."
112msgstr ""
113"Você pode regerar seu token: <a href='?feed&amp;action=generate'>gerar!</a>."
114
115msgid "Change your theme"
116msgstr "Mudar seu tema"
117
118msgid "Theme:"
119msgstr "Tema:"
120
121msgid "Update"
122msgstr "Atualizar"
123
124msgid "Change your language"
125msgstr "Alterar seu idioma"
126
127msgid "Language:"
128msgstr "Idioma:"
129
130msgid "Change your password"
131msgstr "Alterar sua senha"
132
133msgid "New password:"
134msgstr "Nova senha:"
135
136msgid "Password"
137msgstr "Senha"
138
139msgid "Repeat your new password:"
140msgstr "Repetir sua nova senha:"
141
142msgid "Import"
143msgstr "Importar"
144
145msgid ""
146"Please execute the import script locally as it can take a very long time."
147msgstr ""
148"Por favor, execute o script de importação localmente, pois pode demorar "
149"muito tempo."
150
151msgid "More info in the official documentation:"
152msgstr "Mais informações na documentação oficial:"
153
154msgid "Import from Pocket"
155msgstr "Importar do Pocket"
156
157#, php-format
158msgid "(you must have a %s file on your server)"
159msgstr "(você deve ter um arquivo %s no seu servidor)"
160
161msgid "Import from Readability"
162msgstr "Importar do Readability"
163
164msgid "Import from Instapaper"
165msgstr "Importar do Instapaper"
166
167msgid "Import from wallabag"
168msgstr "Importar do wallabag"
169
170msgid "Export your wallabag data"
171msgstr "Exportar seus dados do wallabag"
172
173msgid "Click here"
174msgstr "Clique aqui"
175
176msgid "to download your database."
177msgstr "para baixar seu banco de dados."
178
179msgid "to export your wallabag data."
180msgstr "para exportar seus dados do wallabag."
181
182msgid "Cache"
183msgstr "Cache"
184
185msgid "to delete cache."
186msgstr "para apagar o cache."
187
188msgid "You can enter multiple tags, separated by commas."
189msgstr "Você pode inserir várias tags, separadas por vírgulas."
190
191msgid "return to article"
192msgstr "retornar ao artigo"
193
194msgid "plop"
195msgstr "plop"
196
197msgid ""
198"You can <a href='wallabag_compatibility_test.php'>check your configuration "
199"here</a>."
200msgstr ""
201"Você pode <a href='wallabag_compatibility_test.php'>checar suas "
202"configurações aqui</a>."
203
204msgid "favoris"
205msgstr "favoritos"
206
207msgid "archive"
208msgstr "arquivo"
209
210msgid "unread"
211msgstr "não lidos"
212
213msgid "by date asc"
214msgstr "por data (cresc.)"
215
216msgid "by date"
217msgstr "por data"
218
219msgid "by date desc"
220msgstr "por data (decr.)"
221
222msgid "by title asc"
223msgstr "por título (cresc.)"
224
225msgid "by title"
226msgstr "por título"
227
228msgid "by title desc"
229msgstr "por título (decr.)"
230
231msgid "Tag"
232msgstr "Tag"
233
234msgid "No articles found."
235msgstr "Nenhum artigo encontrado."
236
237msgid "Toggle mark as read"
238msgstr "Alterar \"marcar como lido\""
239
240msgid "toggle favorite"
241msgstr "alterar \"favorito\""
242
243msgid "delete"
244msgstr "deletar"
245
246msgid "original"
247msgstr "original"
248
249msgid "estimated reading time:"
250msgstr "tempo estimado de leitura:"
251
252msgid "mark all the entries as read"
253msgstr "marcar todas as entradas como lidas"
254
255msgid "results"
256msgstr "resultados"
257
258msgid "installation"
259msgstr "instalação"
260
261msgid "install your wallabag"
262msgstr "instalar seu wallabag"
263
264msgid ""
265"wallabag is still not installed. Please fill the below form to install it. "
266"Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation "
267"on wallabag website</a>."
268msgstr ""
269"O wallabag ainda não está instalado. Preencha o formulário abaixo para "
270"instalá-lo. Não hesite em <a href='http://doc.wallabag.org/'>ler a "
271"documentação no site do wallabag</a>."
272
273msgid "Login"
274msgstr "Login"
275
276msgid "Repeat your password"
277msgstr "Repetir sua senha"
278
279msgid "Install"
280msgstr "Instalar"
281
282msgid "login to your wallabag"
283msgstr "entrar no seu wallabag"
284
285msgid "Login to wallabag"
286msgstr "Entrar no wallabag"
287
288msgid "you are in demo mode, some features may be disabled."
289msgstr "você está no modo demo, alguns recursos podem estar desativados."
290
291msgid "Username"
292msgstr "Nome de usuário"
293
294msgid "Stay signed in"
295msgstr "Continuar conectado"
296
297msgid "(Do not check on public computers)"
298msgstr "(Não marque em computadores públicos)"
299
300msgid "Sign in"
301msgstr "Entrar"
302
303msgid "favorites"
304msgstr "favoritos"
305
306msgid "estimated reading time :"
307msgstr "tempo estimado de leitura :"
308
309msgid "Mark all the entries as read"
310msgstr "Marcar todas as entradas como lidas"
311
312msgid "Return home"
313msgstr "Retornar à página inicial"
314
315msgid "Back to top"
316msgstr "Voltar ao topo"
317
318msgid "Mark as read"
319msgstr "Marcar como lido"
320
321msgid "Favorite"
322msgstr "Favoritar"
323
324msgid "Toggle favorite"
325msgstr "Alterar \"favorito\""
326
327msgid "Delete"
328msgstr "Deletar"
329
330msgid "Tweet"
331msgstr "Tweetar"
332
333msgid "Email"
334msgstr "Email"
335
336msgid "shaarli"
337msgstr "shaarli"
338
339msgid "flattr"
340msgstr "flattr"
341
342msgid "Does this article appear wrong?"
343msgstr "Esse artigo está sendo exibido incorretamente?"
344
345msgid "tags:"
346msgstr "tags:"
347
348msgid "Edit tags"
349msgstr "Editar tags"
350
351msgid "save link!"
352msgstr "salvar link!"
353
354msgid "home"
355msgstr "início"
356
357msgid "tags"
358msgstr "tags"
359
360msgid "logout"
361msgstr "sair"
362
363msgid "powered by"
364msgstr "powered by"
365
366msgid "debug mode is on so cache is off."
367msgstr "o modo debug está ativo, então o cache foi desativado."
368
369msgid "your wallabag version:"
370msgstr "sua versão do wallabag:"
371
372msgid "storage:"
373msgstr "armazenamento:"
374
375msgid "save a link"
376msgstr "salvar link"
377
378msgid "back to home"
379msgstr "voltar à página inicial"
380
381msgid "toggle mark as read"
382msgstr "alterar \"marcar como lido\""
383
384msgid "tweet"
385msgstr "tweetar"
386
387msgid "email"
388msgstr "email"
389
390msgid "this article appears wrong?"
391msgstr "esse artigo está sendo mostrado incorretamente?"
392
393msgid "No link available here!"
394msgstr "Nenhum link disponível aqui!"
395
396msgid "Poching a link"
397msgstr "Pocheando um link"
398
399msgid "by filling this field"
400msgstr "preenchendo esse campo"
401
402msgid "bookmarklet: drag & drop this link to your bookmarks bar"
403msgstr "bookmarklet: arraste esse link para a sua barra de favoritos"
404
405msgid "your version"
406msgstr "sua versão"
407
408msgid "latest stable version"
409msgstr "última versão estável"
410
411msgid "a more recent stable version is available."
412msgstr "uma versão estável mais nova está disponível"
413
414msgid "you are up to date."
415msgstr "você está atualizado."
416
417msgid "latest dev version"
418msgstr "última versão em desenvolvimento"
419
420msgid "a more recent development version is available."
421msgstr "uma versão em desenvolvimento mais nova está disponível"
422
423msgid "Please execute the import script locally, it can take a very long time."
424msgstr ""
425"Por favor, execute o script de importação localmente, pois pode demorar "
426"muito tempo."
427
428msgid "More infos in the official doc:"
429msgstr "Mais informações na documentação oficial:"
430
431msgid "import from Pocket"
432msgstr "importar do Pocket"
433
434msgid "import from Readability"
435msgstr "importar do Readability"
436
437msgid "import from Instapaper"
438msgstr "importar do Instapaper"
439
440msgid "Tags"
441msgstr "Tags"
442
443msgid "Untitled"
444msgstr "Sem título"
445
446msgid "the link has been added successfully"
447msgstr "o link foi adicionado com sucesso"
448
449msgid "error during insertion : the link wasn't added"
450msgstr "erro durante a inserção: o link não foi adicionado"
451
452msgid "the link has been deleted successfully"
453msgstr "o link foi deletado com sucesso"
454
455msgid "the link wasn't deleted"
456msgstr "o link não foi deletado"
457
458msgid "Article not found!"
459msgstr "Artigo não encontrado!"
460
461msgid "previous"
462msgstr "anterior"
463
464msgid "next"
465msgstr "próximo"
466
467msgid "in demo mode, you can't update your password"
468msgstr "você não pode alterar a senha no modo demo"
469
470msgid "your password has been updated"
471msgstr "sua senha foi atualizada"
472
473msgid ""
474"the two fields have to be filled & the password must be the same in the two "
475"fields"
476msgstr ""
477"os dois campos devem estar preenchidos e as senhas devem ser iguais em ambos"
478
479msgid "still using the \""
480msgstr "ainda usando o \""
481
482msgid "that theme does not seem to be installed"
483msgstr "esse tema aparentemente não está instalado"
484
485msgid "you have changed your theme preferences"
486msgstr "você alterou suas preferências de tema"
487
488msgid "that language does not seem to be installed"
489msgstr "esse idioma aparentemente não está instalado"
490
491msgid "you have changed your language preferences"
492msgstr "você alterou suas preferências de idioma"
493
494msgid "login failed: you have to fill all fields"
495msgstr "falha ao entrar: você deve preencher todos os campos"
496
497msgid "welcome to your wallabag"
498msgstr "bem-vindo ao seu wallabag"
499
500msgid "login failed: bad login or password"
501msgstr "falha ao entrar: login ou senha incorretos"
502
503msgid "import from instapaper completed"
504msgstr "importação do instapaper completa"
505
506msgid "import from pocket completed"
507msgstr "importação do pocket completa"
508
509msgid "import from Readability completed. "
510msgstr "importação do Readability completa. "
511
512msgid "import from Poche completed. "
513msgstr "importação do Poche completa. "
514
515msgid "Unknown import provider."
516msgstr "Serviço de importação desconhecido."
517
518msgid "Incomplete inc/poche/define.inc.php file, please define \""
519msgstr "Arquivo inc/poche/define.inc.php incompleto, por favor defina \""
520
521msgid "Could not find required \""
522msgstr "Não foi possível encontrar o requerido \""
523
524msgid "Uh, there is a problem while generating feeds."
525msgstr "Uh, houve um problema ao gerar os feeds."
526
527msgid "Cache deleted."
528msgstr "Cache deletado."
529
530msgid "Oops, it seems you don't have PHP 5."
531msgstr "Oops, parece que você não tem o PHP 5."
532
533#~ msgid "poche it!"
534#~ msgstr "poche it!"
535
536#~ msgid "Updating poche"
537#~ msgstr "Updating poche"
538
539#~ msgid "create an issue"
540#~ msgstr "create an issue"
541
542#~ msgid "or"
543#~ msgstr "or"
544
545#~ msgid "contact us by mail"
546#~ msgstr "contact us by mail"
547
548#~ msgid "your poche version:"
549#~ msgstr "your poche version:"
diff --git a/locale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.mo b/locale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.mo
index c9cf1a55..5cbfad16 100755
--- a/locale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.mo
+++ b/locale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.mo
Binary files differ
diff --git a/locale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.po b/locale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.po
index aa1769cf..08f12b7c 100755
--- a/locale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.po
+++ b/locale/ru_RU.utf8/LC_MESSAGES/ru_RU.utf8.po
@@ -2,7 +2,7 @@ msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: \n" 3"Project-Id-Version: \n"
4"Report-Msgid-Bugs-To: \n" 4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2014-02-07 12:40+0300\n" 5"POT-Creation-Date: 2014-02-25 15:09+0300\n"
6"PO-Revision-Date: \n" 6"PO-Revision-Date: \n"
7"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n" 7"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n"
8"Language-Team: \n" 8"Language-Team: \n"
@@ -14,135 +14,25 @@ msgstr ""
14"X-Poedit-Language: Russian\n" 14"X-Poedit-Language: Russian\n"
15"X-Poedit-Country: RUSSIA\n" 15"X-Poedit-Country: RUSSIA\n"
16"X-Poedit-SourceCharset: utf-8\n" 16"X-Poedit-SourceCharset: utf-8\n"
17"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag\n" 17"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag-master-testing\n"
18 18
19msgid "poche, a read it later open source system" 19msgid "wallabag, a read it later open source system"
20msgstr "poche, сервис отложенного чтения с открытым исходным кодом" 20msgstr "wallabag, сервис отложенного чтения с открытым исходным кодом"
21 21
22msgid "login failed: user doesn't exist" 22msgid "login failed: user doesn't exist"
23msgstr "войти не удалось: пользователь не существует" 23msgstr "войти не удалось: пользователь не существует"
24 24
25msgid "Return home"
26msgstr "На главную"
27
28msgid "Back to top"
29msgstr "Наверх"
30
31msgid "original"
32msgstr "источник"
33
34msgid "Mark as read"
35msgstr "Отметить как прочитанное"
36
37msgid "Toggle mark as read"
38msgstr "Изменить отметку 'прочитано'"
39
40msgid "Favorite"
41msgstr "Избранное"
42
43msgid "Toggle favorite"
44msgstr "Изменить метку избранного"
45
46msgid "Delete"
47msgstr "Удалить"
48
49msgid "Tweet"
50msgstr "Твитнуть"
51
52msgid "Email"
53msgstr "Отправить по почте"
54
55msgid "shaarli"
56msgstr "shaarli"
57
58msgid "flattr"
59msgstr "проспонсировать"
60
61msgid "Does this article appear wrong?"
62msgstr "Статья выглядит криво?"
63
64msgid "tags:"
65msgstr "теги:"
66
67msgid "Edit tags"
68msgstr "Редактировать теги"
69
70msgid "return home" 25msgid "return home"
71msgstr "на главную" 26msgstr "на главную"
72 27
73msgid "powered by"
74msgstr "при поддержке"
75
76msgid "debug mode is on so cache is off."
77msgstr "включён режим отладки - кеш выключен."
78
79msgid "your poche version:"
80msgstr "ваша версия poche:"
81
82msgid "storage:"
83msgstr "хранилище:"
84
85msgid "favoris"
86msgstr "избранное"
87
88msgid "archive"
89msgstr "архив"
90
91msgid "unread"
92msgstr "непрочитанное"
93
94msgid "by date asc"
95msgstr "по дате, сперва старые"
96
97msgid "by date"
98msgstr "по дате"
99
100msgid "by date desc"
101msgstr "по дате, сперва новые"
102
103msgid "by title asc"
104msgstr "по заголовку (прямой)"
105
106msgid "by title"
107msgstr "по заголовку"
108
109msgid "by title desc"
110msgstr "по заголовку (обратный)"
111
112msgid "No articles found."
113msgstr "Статей не найдено."
114
115msgid "toggle favorite"
116msgstr "изменить метку избранного"
117
118msgid "delete"
119msgstr "удалить"
120
121msgid "estimated reading time:"
122msgstr "ориентировочное время чтения:"
123
124msgid "results"
125msgstr "найдено"
126
127msgid "home"
128msgstr "главная"
129
130msgid "favorites"
131msgstr "избранное"
132
133msgid "tags"
134msgstr "теги"
135
136msgid "config" 28msgid "config"
137msgstr "настройки" 29msgstr "настройки"
138 30
139msgid "logout" 31msgid "Saving articles"
140msgstr "выход" 32msgstr "Сохранение статей"
141
142msgid "Poching links"
143msgstr "Сохранение ссылок"
144 33
145msgid "There are several ways to poche a link:" 34#, fuzzy
35msgid "There are several ways to save an article:"
146msgstr "Существует несколько способов сохранить ссылку:" 36msgstr "Существует несколько способов сохранить ссылку:"
147 37
148msgid "read the documentation" 38msgid "read the documentation"
@@ -166,14 +56,14 @@ msgstr "скачать приложение"
166msgid "By filling this field" 56msgid "By filling this field"
167msgstr "Заполнением этого поля" 57msgstr "Заполнением этого поля"
168 58
169msgid "poche it!" 59msgid "bag it!"
170msgstr "прикарманить!" 60msgstr "прикарманить!"
171 61
172msgid "Bookmarklet: drag & drop this link to your bookmarks bar" 62msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
173msgstr "Закладка: перетащите и опустите ссылку на панель закладок" 63msgstr "Закладка: перетащите и опустите ссылку на панель закладок"
174 64
175msgid "Updating poche" 65msgid "Upgrading wallabag"
176msgstr "Обновления poche" 66msgstr "Обновление wallabag"
177 67
178msgid "Installed version" 68msgid "Installed version"
179msgstr "Установленная версия" 69msgstr "Установленная версия"
@@ -187,15 +77,14 @@ msgstr "Доступна новая стабильная версия."
187msgid "You are up to date." 77msgid "You are up to date."
188msgstr "У вас всё самое новое." 78msgstr "У вас всё самое новое."
189 79
190msgid "latest dev version" 80#, fuzzy
81msgid "Latest dev version"
191msgstr "последняя версия в разработке" 82msgstr "последняя версия в разработке"
192 83
193msgid "a more recent development version is available." 84#, fuzzy
85msgid "A more recent development version is available."
194msgstr "есть более свежая версия в разработке." 86msgstr "есть более свежая версия в разработке."
195 87
196msgid "you are up to date."
197msgstr "у вас всё самое новое."
198
199msgid "Feeds" 88msgid "Feeds"
200msgstr "Ленты (feeds)" 89msgstr "Ленты (feeds)"
201 90
@@ -253,7 +142,8 @@ msgstr "Импортировать"
253msgid "Please execute the import script locally as it can take a very long time." 142msgid "Please execute the import script locally as it can take a very long time."
254msgstr "Пожалуйста, выполните сценарий импорта локально - это может занять слишком много времени." 143msgstr "Пожалуйста, выполните сценарий импорта локально - это может занять слишком много времени."
255 144
256msgid "More info in the official docs:" 145#, fuzzy
146msgid "More info in the official documentation:"
257msgstr "Больше сведений в официальной документации:" 147msgstr "Больше сведений в официальной документации:"
258 148
259msgid "Import from Pocket" 149msgid "Import from Pocket"
@@ -269,11 +159,11 @@ msgstr "Импортировать из Readability"
269msgid "Import from Instapaper" 159msgid "Import from Instapaper"
270msgstr "Импортировать из Instapaper" 160msgstr "Импортировать из Instapaper"
271 161
272msgid "Import from poche" 162msgid "Import from wallabag"
273msgstr "Импортировать из poche" 163msgstr "Импортировать из wallabag"
274 164
275msgid "Export your poche data" 165msgid "Export your wallabag data"
276msgstr "Экспортировать данные poche" 166msgstr "Экспортировать данные wallabag"
277 167
278msgid "Click here" 168msgid "Click here"
279msgstr "Кликните здесь" 169msgstr "Кликните здесь"
@@ -281,17 +171,14 @@ msgstr "Кликните здесь"
281msgid "to download your database." 171msgid "to download your database."
282msgstr "чтобы скачать вашу базу данных" 172msgstr "чтобы скачать вашу базу данных"
283 173
284msgid "to export your poche data." 174msgid "to export your wallabag data."
285msgstr "чтобы экспортировать свои записи из poche." 175msgstr "чтобы экспортировать свои записи из wallabag."
286 176
287msgid "Tag" 177msgid "Cache"
288msgstr "Тег" 178msgstr "Кэш"
289
290msgid "No link available here!"
291msgstr "Здесь нет ссылки!"
292 179
293msgid "toggle mark as read" 180msgid "to delete cache."
294msgstr "измени оку 'роитно'" 181msgstr "т сроить эш."
295 182
296msgid "You can enter multiple tags, separated by commas." 183msgid "You can enter multiple tags, separated by commas."
297msgstr "Вы можете ввести несколько тегов, разделяя их запятой." 184msgstr "Вы можете ввести несколько тегов, разделяя их запятой."
@@ -305,6 +192,60 @@ msgstr "plop"
305msgid "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>." 192msgid "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>."
306msgstr "Вы можете <a href='wallabag_compatibility_test.php'>проверить конфигурацию здесь</a>." 193msgstr "Вы можете <a href='wallabag_compatibility_test.php'>проверить конфигурацию здесь</a>."
307 194
195msgid "favoris"
196msgstr "избранное"
197
198msgid "archive"
199msgstr "архив"
200
201msgid "unread"
202msgstr "непрочитанное"
203
204msgid "by date asc"
205msgstr "по дате, сперва старые"
206
207msgid "by date"
208msgstr "по дате"
209
210msgid "by date desc"
211msgstr "по дате, сперва новые"
212
213msgid "by title asc"
214msgstr "по заголовку (прямой)"
215
216msgid "by title"
217msgstr "по заголовку"
218
219msgid "by title desc"
220msgstr "по заголовку (обратный)"
221
222msgid "Tag"
223msgstr "Тег"
224
225msgid "No articles found."
226msgstr "Статей не найдено."
227
228msgid "Toggle mark as read"
229msgstr "Изменить отметку 'прочитано'"
230
231msgid "toggle favorite"
232msgstr "изменить метку избранного"
233
234msgid "delete"
235msgstr "удалить"
236
237msgid "original"
238msgstr "источник"
239
240msgid "estimated reading time:"
241msgstr "ориентировочное время чтения:"
242
243msgid "mark all the entries as read"
244msgstr "отметить все статьи как прочитанные "
245
246msgid "results"
247msgstr "найдено"
248
308msgid "installation" 249msgid "installation"
309msgstr "установка" 250msgstr "установка"
310 251
@@ -341,6 +282,159 @@ msgstr "Запомнить меня"
341msgid "(Do not check on public computers)" 282msgid "(Do not check on public computers)"
342msgstr "(Не отмечайте на чужих компьютерах)" 283msgstr "(Не отмечайте на чужих компьютерах)"
343 284
285msgid "Sign in"
286msgstr "Зарегистрироваться"
287
288msgid "favorites"
289msgstr "избранное"
290
291#, fuzzy
292msgid "estimated reading time :"
293msgstr "ориентировочное время чтения:"
294
295msgid "Mark all the entries as read"
296msgstr "Отметить все как прочитанное"
297
298msgid "Return home"
299msgstr "На главную"
300
301msgid "Back to top"
302msgstr "Наверх"
303
304msgid "Mark as read"
305msgstr "Отметить как прочитанное"
306
307msgid "Favorite"
308msgstr "Избранное"
309
310msgid "Toggle favorite"
311msgstr "Изменить метку избранного"
312
313msgid "Delete"
314msgstr "Удалить"
315
316msgid "Tweet"
317msgstr "Твитнуть"
318
319msgid "Email"
320msgstr "Отправить по почте"
321
322msgid "shaarli"
323msgstr "shaarli"
324
325msgid "flattr"
326msgstr "проспонсировать"
327
328msgid "Does this article appear wrong?"
329msgstr "Статья выглядит криво?"
330
331msgid "tags:"
332msgstr "теги:"
333
334msgid "Edit tags"
335msgstr "Редактировать теги"
336
337msgid "save link!"
338msgstr "сохранить ссылку!"
339
340msgid "home"
341msgstr "главная"
342
343msgid "tags"
344msgstr "теги"
345
346msgid "logout"
347msgstr "выход"
348
349msgid "powered by"
350msgstr "при поддержке"
351
352msgid "debug mode is on so cache is off."
353msgstr "включён режим отладки - кеш выключен."
354
355msgid "your wallabag version:"
356msgstr "Ваша версия wallabag:"
357
358msgid "storage:"
359msgstr "хранилище:"
360
361msgid "save a link"
362msgstr "сохранить ссылку"
363
364msgid "back to home"
365msgstr "домой"
366
367msgid "toggle mark as read"
368msgstr "изменить отметку 'прочитано'"
369
370msgid "tweet"
371msgstr "твитнуть"
372
373msgid "email"
374msgstr "email"
375
376#, fuzzy
377msgid "this article appears wrong?"
378msgstr "Статья выглядит криво?"
379
380msgid "No link available here!"
381msgstr "Здесь нет ссылки!"
382
383#, fuzzy
384msgid "Poching a link"
385msgstr "Сохранение ссылок"
386
387#, fuzzy
388msgid "by filling this field"
389msgstr "Заполнением этого поля"
390
391#, fuzzy
392msgid "bookmarklet: drag & drop this link to your bookmarks bar"
393msgstr "Закладка: перетащите и опустите ссылку на панель закладок"
394
395msgid "your version"
396msgstr "Ваша версия"
397
398#, fuzzy
399msgid "latest stable version"
400msgstr "Последняя стабильная версия"
401
402#, fuzzy
403msgid "a more recent stable version is available."
404msgstr "Доступна новая стабильная версия."
405
406msgid "you are up to date."
407msgstr "у вас всё самое новое."
408
409msgid "latest dev version"
410msgstr "последняя версия в разработке"
411
412msgid "a more recent development version is available."
413msgstr "есть более свежая версия в разработке."
414
415#, fuzzy
416msgid "Please execute the import script locally, it can take a very long time."
417msgstr "Пожалуйста, выполните сценарий импорта локально - это может занять слишком много времени."
418
419#, fuzzy
420msgid "More infos in the official doc:"
421msgstr "Больше сведений в официальной документации:"
422
423#, fuzzy
424msgid "import from Pocket"
425msgstr "Импортировать из Pocket"
426
427#, fuzzy
428msgid "import from Readability"
429msgstr "Импортировать из Readability"
430
431#, fuzzy
432msgid "import from Instapaper"
433msgstr "Импортировать из Instapaper"
434
435msgid "Tags"
436msgstr "Теги"
437
344msgid "Untitled" 438msgid "Untitled"
345msgstr "Без названия" 439msgstr "Без названия"
346 440
@@ -356,6 +450,9 @@ msgstr "ссылка успешно удалена"
356msgid "the link wasn't deleted" 450msgid "the link wasn't deleted"
357msgstr "ссылка не удалена" 451msgstr "ссылка не удалена"
358 452
453msgid "Article not found!"
454msgstr "Статью не найдено."
455
359msgid "previous" 456msgid "previous"
360msgstr "предыдущая" 457msgstr "предыдущая"
361 458
@@ -389,15 +486,12 @@ msgstr "вы изменили свои настройки языка"
389msgid "login failed: you have to fill all fields" 486msgid "login failed: you have to fill all fields"
390msgstr "войти не удалось: вы должны заполнить все поля" 487msgstr "войти не удалось: вы должны заполнить все поля"
391 488
392msgid "welcome to your poche" 489msgid "welcome to your wallabag"
393msgstr "добро пожаловать в ваш poche" 490msgstr "добро пожаловать в wallabag"
394 491
395msgid "login failed: bad login or password" 492msgid "login failed: bad login or password"
396msgstr "войти не удалось: неправильное имя пользователя или пароль" 493msgstr "войти не удалось: неправильное имя пользователя или пароль"
397 494
398msgid "see you soon!"
399msgstr "увидимся!"
400
401msgid "import from instapaper completed" 495msgid "import from instapaper completed"
402msgstr "импорт из instapaper завершен" 496msgstr "импорт из instapaper завершен"
403 497
@@ -422,14 +516,40 @@ msgstr "Не удалось найти требуемый \""
422msgid "Uh, there is a problem while generating feeds." 516msgid "Uh, there is a problem while generating feeds."
423msgstr "Ох, возникла проблема при создании ленты." 517msgstr "Ох, возникла проблема при создании ленты."
424 518
519msgid "Cache deleted."
520msgstr "Кэш очищен. "
521
425msgid "Oops, it seems you don't have PHP 5." 522msgid "Oops, it seems you don't have PHP 5."
426msgstr "Упс, кажется у вас не установлен PHP 5." 523msgstr "Упс, кажется у вас не установлен PHP 5."
427 524
428#~ msgid "your version" 525#~ msgid "You can poche a link by several methods:"
429#~ msgstr "Ваша версия" 526#~ msgstr "Вы можете сохранить ссылку несколькими путями:"
527
528#~ msgid "poche it!"
529#~ msgstr "прикарманить!"
530
531#~ msgid "Updating poche"
532#~ msgstr "Обновления poche"
533
534#, fuzzy
535#~ msgid "Export your poche datas"
536#~ msgstr "Экспортировать данные poche"
537
538#, fuzzy
539#~ msgid "to export your poche datas."
540#~ msgstr "чтобы экспортировать свои записи из poche."
430 541
431#~ msgid "back to home" 542#~ msgid "your poche version:"
432#~ msgstr "домой" 543#~ msgstr "ваша версия poche:"
544
545#~ msgid "Import from poche"
546#~ msgstr "Импортировать из poche"
547
548#~ msgid "welcome to your poche"
549#~ msgstr "добро пожаловать в ваш poche"
550
551#~ msgid "see you soon!"
552#~ msgstr "увидимся!"
433 553
434#~ msgid "create an issue" 554#~ msgid "create an issue"
435#~ msgstr "оповестить об ошибке" 555#~ msgstr "оповестить об ошибке"
@@ -439,6 +559,3 @@ msgstr "Упс, кажется у вас не установлен PHP 5."
439 559
440#~ msgid "contact us by mail" 560#~ msgid "contact us by mail"
441#~ msgstr "связаться по почте" 561#~ msgstr "связаться по почте"
442
443#~ msgid "Sign in"
444#~ msgstr "Зарегистрироваться"
diff --git a/locale/sl_SI.utf8/LC_MESSAGES/sl_SI.utf8.po b/locale/sl_SI.utf8/LC_MESSAGES/sl_SI.utf8.po
index 6eac8e01..8ad2eded 100644
--- a/locale/sl_SI.utf8/LC_MESSAGES/sl_SI.utf8.po
+++ b/locale/sl_SI.utf8/LC_MESSAGES/sl_SI.utf8.po
@@ -4,55 +4,138 @@
4msgid "" 4msgid ""
5msgstr "" 5msgstr ""
6"Project-Id-Version: wallabag\n" 6"Project-Id-Version: wallabag\n"
7"POT-Creation-Date: \n" 7"Report-Msgid-Bugs-To: \n"
8"PO-Revision-Date: 2014-02-21 15:09+0100\n" 8"POT-Creation-Date: 2014-02-25 15:12+0300\n"
9"Last-Translator: Nicolas Lœuillet <nicolas.loeuillet@gmail.com>\n" 9"PO-Revision-Date: 2014-02-25 15:12+0300\n"
10"Language-Team: Slovenian (Slovenia) (http://www.transifex.com/projects/p/" 10"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n"
11"wallabag/language/sl_SI/)\n" 11"Language-Team: Slovenian (Slovenia) (http://www.transifex.com/projects/p/wallabag/language/sl_SI/)\n"
12"Language: sl_SI\n"
12"MIME-Version: 1.0\n" 13"MIME-Version: 1.0\n"
13"Content-Type: text/plain; charset=UTF-8\n" 14"Content-Type: text/plain; charset=UTF-8\n"
14"Content-Transfer-Encoding: 8bit\n" 15"Content-Transfer-Encoding: 8bit\n"
15"Language: sl_SI\n" 16"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
16"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
17"%100==4 ? 2 : 3);\n"
18"X-Generator: Poedit 1.5.4\n" 17"X-Generator: Poedit 1.5.4\n"
18"X-Poedit-Language: Slovenian\n"
19"X-Poedit-Country: SLOVENIA\n"
20"X-Poedit-Basepath: .\n"
21"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag-master-testing\n"
22
23msgid "wallabag, a read it later open source system"
24msgstr ""
25
26msgid "login failed: user doesn't exist"
27msgstr ""
28
29msgid "return home"
30msgstr ""
19 31
20msgid "config" 32msgid "config"
21msgstr "nastavitve" 33msgstr "nastavitve"
22 34
23msgid "Poching a link" 35msgid "Saving articles"
24msgstr "Shrani povezavo" 36msgstr ""
37
38msgid "There are several ways to save an article:"
39msgstr ""
25 40
26msgid "read the documentation" 41msgid "read the documentation"
27msgstr "preberite dokumentacijo" 42msgstr "preberite dokumentacijo"
28 43
29msgid "by filling this field" 44msgid "download the extension"
45msgstr ""
46
47msgid "via F-Droid"
48msgstr ""
49
50msgid " or "
51msgstr ""
52
53msgid "via Google Play"
54msgstr ""
55
56msgid "download the application"
57msgstr ""
58
59#, fuzzy
60msgid "By filling this field"
30msgstr "z vnosom v to polje" 61msgstr "z vnosom v to polje"
31 62
32msgid "poche it!" 63msgid "bag it!"
33msgstr "shrani!" 64msgstr ""
34 65
35msgid "Updating poche" 66msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
36msgstr "Posodabljam Poche" 67msgstr ""
37 68
38msgid "your version" 69msgid "Upgrading wallabag"
39msgstr "vaša različica" 70msgstr ""
40 71
41msgid "latest stable version" 72#, fuzzy
73msgid "Installed version"
42msgstr "zadnja stabilna različica" 74msgstr "zadnja stabilna različica"
43 75
44msgid "a more recent stable version is available." 76#, fuzzy
77msgid "Latest stable version"
78msgstr "zadnja stabilna različica"
79
80#, fuzzy
81msgid "A more recent stable version is available."
45msgstr "na voljo je nova stabilna različica." 82msgstr "na voljo je nova stabilna različica."
46 83
47msgid "you are up to date." 84#, fuzzy
85msgid "You are up to date."
48msgstr "imate najnovejšo različico." 86msgstr "imate najnovejšo različico."
49 87
50msgid "latest dev version" 88#, fuzzy
89msgid "Latest dev version"
51msgstr "zadnja razvojna različica" 90msgstr "zadnja razvojna različica"
52 91
53msgid "a more recent development version is available." 92#, fuzzy
93msgid "A more recent development version is available."
54msgstr "na voljo je nova razvojna različica." 94msgstr "na voljo je nova razvojna različica."
55 95
96msgid "Feeds"
97msgstr ""
98
99msgid "Your feed token is currently empty and must first be generated to enable feeds. Click <a href='?feed&amp;action=generate'>here to generate it</a>."
100msgstr ""
101
102msgid "Unread feed"
103msgstr ""
104
105#, fuzzy
106msgid "Favorites feed"
107msgstr "priljubljeni"
108
109#, fuzzy
110msgid "Archive feed"
111msgstr "arhiv"
112
113msgid "Your token:"
114msgstr ""
115
116msgid "Your user id:"
117msgstr ""
118
119msgid "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>."
120msgstr ""
121
122#, fuzzy
123msgid "Change your theme"
124msgstr "Zamenjava gesla"
125
126msgid "Theme:"
127msgstr ""
128
129msgid "Update"
130msgstr "Posodobi"
131
132#, fuzzy
133msgid "Change your language"
134msgstr "Zamenjava gesla"
135
136msgid "Language:"
137msgstr ""
138
56msgid "Change your password" 139msgid "Change your password"
57msgstr "Zamenjava gesla" 140msgstr "Zamenjava gesla"
58 141
@@ -65,67 +148,69 @@ msgstr "Geslo"
65msgid "Repeat your new password:" 148msgid "Repeat your new password:"
66msgstr "Ponovite novo geslo:" 149msgstr "Ponovite novo geslo:"
67 150
68msgid "Update"
69msgstr "Posodobi"
70
71msgid "Import" 151msgid "Import"
72msgstr "Uvozi" 152msgstr "Uvozi"
73 153
74msgid "Please execute the import script locally, it can take a very long time." 154#, fuzzy
75msgstr "" 155msgid "Please execute the import script locally as it can take a very long time."
76"Prosimo poženite skripto za uvoz lokalno, saj lahko postopek traja precej " 156msgstr "Prosimo poženite skripto za uvoz lokalno, saj lahko postopek traja precej časa."
77"časa."
78 157
79msgid "More infos in the official doc:" 158#, fuzzy
159msgid "More info in the official documentation:"
80msgstr "Več informacij v uradni dokumentaciji:" 160msgstr "Več informacij v uradni dokumentaciji:"
81 161
82msgid "import from Pocket" 162#, fuzzy
163msgid "Import from Pocket"
83msgstr "Uvoz iz aplikacije Pocket" 164msgstr "Uvoz iz aplikacije Pocket"
84 165
85msgid "import from Readability" 166#, php-format
167msgid "(you must have a %s file on your server)"
168msgstr ""
169
170#, fuzzy
171msgid "Import from Readability"
86msgstr "Uvoz iz aplikacije Readability" 172msgstr "Uvoz iz aplikacije Readability"
87 173
88msgid "import from Instapaper" 174#, fuzzy
175msgid "Import from Instapaper"
89msgstr "Uvoz iz aplikacije Instapaper" 176msgstr "Uvoz iz aplikacije Instapaper"
90 177
91msgid "Export your poche datas" 178#, fuzzy
179msgid "Import from wallabag"
180msgstr "Uvoz iz aplikacije Readability"
181
182#, fuzzy
183msgid "Export your wallabag data"
92msgstr "Izvoz vsebine" 184msgstr "Izvoz vsebine"
93 185
94msgid "Click here" 186msgid "Click here"
95msgstr "Kliknite tukaj" 187msgstr "Kliknite tukaj"
96 188
97msgid "to export your poche datas." 189#, fuzzy
190msgid "to download your database."
98msgstr "za izvoz vsebine aplikacije Poche." 191msgstr "za izvoz vsebine aplikacije Poche."
99 192
100msgid "back to home" 193#, fuzzy
101msgstr "Nazaj domov" 194msgid "to export your wallabag data."
102 195msgstr "za izvoz vsebine aplikacije Poche."
103msgid "installation"
104msgstr "Namestitev"
105 196
106msgid "install your poche" 197msgid "Cache"
107msgstr "Namestitev aplikacije Poche" 198msgstr ""
108 199
109msgid "" 200msgid "to delete cache."
110"poche is still not installed. Please fill the below form to install it. "
111"Don't hesitate to <a href='http://inthepoche.com/doc'>read the documentation "
112"on poche website</a>."
113msgstr "" 201msgstr ""
114"Poche še vedno ni nameščen. Za namestitev izpolnite spodnji obrazec. Za več "
115"informacij <a href='http://inthepoche.com/doc'>preberite dokumentacijo na "
116"spletni strani</a>."
117 202
118msgid "Login" 203msgid "You can enter multiple tags, separated by commas."
119msgstr "Prijava" 204msgstr ""
120 205
121msgid "Repeat your password" 206msgid "return to article"
122msgstr "Ponovite geslo" 207msgstr ""
123 208
124msgid "Install" 209msgid "plop"
125msgstr "Namesti" 210msgstr "štrbunk"
126 211
127msgid "back to top" 212msgid "You can <a href='wallabag_compatibility_test.php'>check your configuration here</a>."
128msgstr "nazaj na vrh" 213msgstr ""
129 214
130msgid "favoris" 215msgid "favoris"
131msgstr "priljubljeni" 216msgstr "priljubljeni"
@@ -154,10 +239,14 @@ msgstr "po naslovu"
154msgid "by title desc" 239msgid "by title desc"
155msgstr "po naslovu - padajoče" 240msgstr "po naslovu - padajoče"
156 241
157msgid "No link available here!" 242msgid "Tag"
158msgstr "Povezava ni na voljo!" 243msgstr ""
159 244
160msgid "toggle mark as read" 245msgid "No articles found."
246msgstr ""
247
248#, fuzzy
249msgid "Toggle mark as read"
161msgstr "označi kot prebrano" 250msgstr "označi kot prebrano"
162 251
163msgid "toggle favorite" 252msgid "toggle favorite"
@@ -169,13 +258,95 @@ msgstr "zavrzi"
169msgid "original" 258msgid "original"
170msgstr "izvirnik" 259msgstr "izvirnik"
171 260
261msgid "estimated reading time:"
262msgstr ""
263
264msgid "mark all the entries as read"
265msgstr ""
266
172msgid "results" 267msgid "results"
173msgstr "rezultati" 268msgstr "rezultati"
174 269
175msgid "tweet" 270msgid "installation"
271msgstr "Namestitev"
272
273#, fuzzy
274msgid "install your wallabag"
275msgstr "Namestitev aplikacije Poche"
276
277#, fuzzy
278msgid "wallabag is still not installed. Please fill the below form to install it. Don't hesitate to <a href='http://doc.wallabag.org/'>read the documentation on wallabag website</a>."
279msgstr "Poche še vedno ni nameščen. Za namestitev izpolnite spodnji obrazec. Za več informacij <a href='http://inthepoche.com/doc'>preberite dokumentacijo na spletni strani</a>."
280
281msgid "Login"
282msgstr "Prijava"
283
284msgid "Repeat your password"
285msgstr "Ponovite geslo"
286
287msgid "Install"
288msgstr "Namesti"
289
290#, fuzzy
291msgid "login to your wallabag"
292msgstr "prijavite se v svoj Poche"
293
294msgid "Login to wallabag"
295msgstr ""
296
297msgid "you are in demo mode, some features may be disabled."
298msgstr "uporabljate vzorčno različico programa, zato so lahko nekatere funkcije izklopljene."
299
300msgid "Username"
301msgstr ""
302
303msgid "Stay signed in"
304msgstr "Ostani prijavljen"
305
306msgid "(Do not check on public computers)"
307msgstr "(Ne označi na javnih napravah)"
308
309msgid "Sign in"
310msgstr "Prijava"
311
312msgid "favorites"
313msgstr "priljubljeni"
314
315msgid "estimated reading time :"
316msgstr ""
317
318msgid "Mark all the entries as read"
319msgstr ""
320
321msgid "Return home"
322msgstr ""
323
324#, fuzzy
325msgid "Back to top"
326msgstr "nazaj na vrh"
327
328#, fuzzy
329msgid "Mark as read"
330msgstr "označi kot prebrano"
331
332#, fuzzy
333msgid "Favorite"
334msgstr "priljubljeni"
335
336#, fuzzy
337msgid "Toggle favorite"
338msgstr "označi kot priljubljeno"
339
340#, fuzzy
341msgid "Delete"
342msgstr "zavrzi"
343
344#, fuzzy
345msgid "Tweet"
176msgstr "tvitni" 346msgstr "tvitni"
177 347
178msgid "email" 348#, fuzzy
349msgid "Email"
179msgstr "pošlji po e-pošti" 350msgstr "pošlji po e-pošti"
180 351
181msgid "shaarli" 352msgid "shaarli"
@@ -184,26 +355,24 @@ msgstr "shaarli"
184msgid "flattr" 355msgid "flattr"
185msgstr "flattr" 356msgstr "flattr"
186 357
187msgid "this article appears wrong?" 358#, fuzzy
359msgid "Does this article appear wrong?"
188msgstr "napaka?" 360msgstr "napaka?"
189 361
190msgid "create an issue" 362msgid "tags:"
191msgstr "prijavi napako" 363msgstr ""
192
193msgid "or"
194msgstr "ali"
195 364
196msgid "contact us by mail" 365msgid "Edit tags"
197msgstr "pošlji e-pošto razvijalcem" 366msgstr ""
198 367
199msgid "plop" 368msgid "save link!"
200msgstr "štrbunk" 369msgstr ""
201 370
202msgid "home" 371msgid "home"
203msgstr "domov" 372msgstr "domov"
204 373
205msgid "favorites" 374msgid "tags"
206msgstr "priljubljeni" 375msgstr ""
207 376
208msgid "logout" 377msgid "logout"
209msgstr "odjava" 378msgstr "odjava"
@@ -212,28 +381,188 @@ msgid "powered by"
212msgstr "stran poganja" 381msgstr "stran poganja"
213 382
214msgid "debug mode is on so cache is off." 383msgid "debug mode is on so cache is off."
215msgstr "" 384msgstr "vklopljen je način odpravljanja napak, zato je predpomnilnik izključen."
216"vklopljen je način odpravljanja napak, zato je predpomnilnik izključen."
217 385
218msgid "your poche version:" 386#, fuzzy
219msgstr "vaša verzija Poche:" 387msgid "your wallabag version:"
388msgstr "vaša različica"
220 389
221msgid "storage:" 390msgid "storage:"
222msgstr "pomnilnik:" 391msgstr "pomnilnik:"
223 392
224msgid "login to your poche" 393msgid "save a link"
225msgstr "prijavite se v svoj Poche" 394msgstr ""
226 395
227msgid "you are in demo mode, some features may be disabled." 396msgid "back to home"
397msgstr "Nazaj domov"
398
399msgid "toggle mark as read"
400msgstr "označi kot prebrano"
401
402msgid "tweet"
403msgstr "tvitni"
404
405msgid "email"
406msgstr "pošlji po e-pošti"
407
408msgid "this article appears wrong?"
409msgstr "napaka?"
410
411msgid "No link available here!"
412msgstr "Povezava ni na voljo!"
413
414msgid "Poching a link"
415msgstr "Shrani povezavo"
416
417msgid "by filling this field"
418msgstr "z vnosom v to polje"
419
420msgid "bookmarklet: drag & drop this link to your bookmarks bar"
228msgstr "" 421msgstr ""
229"uporabljate vzorčno različico programa, zato so lahko nekatere funkcije "
230"izklopljene."
231 422
232msgid "Stay signed in" 423msgid "your version"
233msgstr "Ostani prijavljen" 424msgstr "vaša različica"
234 425
235msgid "(Do not check on public computers)" 426msgid "latest stable version"
236msgstr "(Ne označi na javnih napravah)" 427msgstr "zadnja stabilna različica"
237 428
238msgid "Sign in" 429msgid "a more recent stable version is available."
239msgstr "Prijava" 430msgstr "na voljo je nova stabilna različica."
431
432msgid "you are up to date."
433msgstr "imate najnovejšo različico."
434
435msgid "latest dev version"
436msgstr "zadnja razvojna različica"
437
438msgid "a more recent development version is available."
439msgstr "na voljo je nova razvojna različica."
440
441msgid "Please execute the import script locally, it can take a very long time."
442msgstr "Prosimo poženite skripto za uvoz lokalno, saj lahko postopek traja precej časa."
443
444msgid "More infos in the official doc:"
445msgstr "Več informacij v uradni dokumentaciji:"
446
447msgid "import from Pocket"
448msgstr "Uvoz iz aplikacije Pocket"
449
450msgid "import from Readability"
451msgstr "Uvoz iz aplikacije Readability"
452
453msgid "import from Instapaper"
454msgstr "Uvoz iz aplikacije Instapaper"
455
456msgid "Tags"
457msgstr ""
458
459#, fuzzy
460msgid "Untitled"
461msgstr "po naslovu"
462
463msgid "the link has been added successfully"
464msgstr ""
465
466msgid "error during insertion : the link wasn't added"
467msgstr ""
468
469msgid "the link has been deleted successfully"
470msgstr ""
471
472msgid "the link wasn't deleted"
473msgstr ""
474
475msgid "Article not found!"
476msgstr ""
477
478msgid "previous"
479msgstr ""
480
481msgid "next"
482msgstr ""
483
484msgid "in demo mode, you can't update your password"
485msgstr ""
486
487msgid "your password has been updated"
488msgstr ""
489
490msgid "the two fields have to be filled & the password must be the same in the two fields"
491msgstr ""
492
493msgid "still using the \""
494msgstr ""
495
496msgid "that theme does not seem to be installed"
497msgstr ""
498
499msgid "you have changed your theme preferences"
500msgstr ""
501
502msgid "that language does not seem to be installed"
503msgstr ""
504
505msgid "you have changed your language preferences"
506msgstr ""
507
508msgid "login failed: you have to fill all fields"
509msgstr ""
510
511msgid "welcome to your wallabag"
512msgstr ""
513
514msgid "login failed: bad login or password"
515msgstr ""
516
517#, fuzzy
518msgid "import from instapaper completed"
519msgstr "Uvoz iz aplikacije Instapaper"
520
521#, fuzzy
522msgid "import from pocket completed"
523msgstr "Uvoz iz aplikacije Pocket"
524
525#, fuzzy
526msgid "import from Readability completed. "
527msgstr "Uvoz iz aplikacije Readability"
528
529#, fuzzy
530msgid "import from Poche completed. "
531msgstr "Uvoz iz aplikacije Pocket"
532
533msgid "Unknown import provider."
534msgstr ""
535
536msgid "Incomplete inc/poche/define.inc.php file, please define \""
537msgstr ""
538
539msgid "Could not find required \""
540msgstr ""
541
542msgid "Uh, there is a problem while generating feeds."
543msgstr ""
544
545#, fuzzy
546msgid "Cache deleted."
547msgstr "zavrzi"
548
549msgid "Oops, it seems you don't have PHP 5."
550msgstr ""
551
552#~ msgid "poche it!"
553#~ msgstr "shrani!"
554
555#~ msgid "Updating poche"
556#~ msgstr "Posodabljam Poche"
557
558#~ msgid "create an issue"
559#~ msgstr "prijavi napako"
560
561#~ msgid "or"
562#~ msgstr "ali"
563
564#~ msgid "contact us by mail"
565#~ msgstr "pošlji e-pošto razvijalcem"
566
567#~ msgid "your poche version:"
568#~ msgstr "vaša verzija Poche:"
diff --git a/locale/tools/fillCache.php b/locale/tools/fillCache.php
new file mode 100755
index 00000000..bdd9cc58
--- /dev/null
+++ b/locale/tools/fillCache.php
@@ -0,0 +1,59 @@
1<?php
2
3// this script compile all twig templates and put it in cahce to get Poedit (or xgettext) to extract phrases fron chached templates.
4
5// gettext command line tools:
6// msgunfmt - get po from mo
7// msgfmt - get mo from po
8// xgettext - extract phrases from files
9
10
11 $siteRoot = dirname(__FILE__) . '/../..';
12
13 require_once $siteRoot . '/vendor/twig/twig/lib/Twig/Autoloader.php';
14 Twig_Autoloader::register();
15
16 require_once $siteRoot . '/vendor/twig/extensions/lib/Twig/Extensions/Autoloader.php';
17 Twig_Extensions_Autoloader::register();
18
19 //$tplDir = $siteRoot.'/themes/default';
20 $tplDirRoot = $siteRoot.'/themes/';
21 $tmpDir = $siteRoot. '/cache/';
22
23 foreach (new IteratorIterator(new DirectoryIterator($tplDirRoot)) as $tplDir) {
24
25 if ($tplDir->isDir() and $tplDir!='.' and $tplDir!='..') {
26 echo "\n$tplDir\n";
27
28 $loader = new Twig_Loader_Filesystem($tplDirRoot.$tplDir);
29
30 // force auto-reload to always have the latest version of the template
31 $twig = new Twig_Environment($loader, array(
32 'cache' => $tmpDir,
33 'auto_reload' => true
34 ));
35
36 $twig->addExtension(new Twig_Extensions_Extension_I18n());
37
38 $filter = new Twig_SimpleFilter('getDomain', 'Tools::getDomain');
39 $twig->addFilter($filter);
40
41 $filter = new Twig_SimpleFilter('getReadingTime', 'Tools::getReadingTime');
42 $twig->addFilter($filter);
43
44 $filter = new Twig_SimpleFilter('getPrettyFilename', function($string) { return str_replace($siteRoot, '', $string); });
45 $twig->addFilter($filter);
46
47// // iterate over all your templates
48 foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tplDirRoot.$tplDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
49 // force compilation
50 if ($file->isFile() and pathinfo($file, PATHINFO_EXTENSION)=='twig') {
51 echo "\t$file\n";
52 $twig->loadTemplate(str_replace($tplDirRoot.$tplDir.'/', '', $file));
53 }
54 }
55
56 }
57
58 }
59
diff --git a/locale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.mo b/locale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.mo
index 80972b65..4884abf5 100755
--- a/locale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.mo
+++ b/locale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.mo
Binary files differ
diff --git a/locale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.po b/locale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.po
index 6092cbab..08797705 100755
--- a/locale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.po
+++ b/locale/uk_UA.utf8/LC_MESSAGES/uk_UA.utf8.po
@@ -2,8 +2,8 @@ msgid ""
2msgstr "" 2msgstr ""
3"Project-Id-Version: wballabag\n" 3"Project-Id-Version: wballabag\n"
4"Report-Msgid-Bugs-To: \n" 4"Report-Msgid-Bugs-To: \n"
5"POT-Creation-Date: 2014-02-06 19:23+0300\n" 5"POT-Creation-Date: 2014-02-25 15:06+0300\n"
6"PO-Revision-Date: 2014-02-06 19:24+0300\n" 6"PO-Revision-Date: 2014-02-25 15:08+0300\n"
7"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n" 7"Last-Translator: Maryana <mariroz@mr.lviv.ua>\n"
8"Language-Team: \n" 8"Language-Team: \n"
9"Language: \n" 9"Language: \n"
@@ -15,52 +15,25 @@ msgstr ""
15"X-Poedit-Language: Ukrainian\n" 15"X-Poedit-Language: Ukrainian\n"
16"X-Poedit-Country: UKRAINE\n" 16"X-Poedit-Country: UKRAINE\n"
17"X-Poedit-SourceCharset: utf-8\n" 17"X-Poedit-SourceCharset: utf-8\n"
18"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag\n" 18"X-Poedit-SearchPath-0: /home/mariroz/_DEV/web/wallabag/wallabag-master-testing\n"
19 19
20msgid "poche, a read it later open source system" 20msgid "wallabag, a read it later open source system"
21msgstr "poche, сервіс відкладеного читання з відкритим кодом" 21msgstr "wallabag, сервіс відкладеного читання з відкритим кодом"
22 22
23msgid "login failed: user doesn't exist" 23msgid "login failed: user doesn't exist"
24msgstr "увійти не вдалося: користувач не існує" 24msgstr "увійти не вдалося: користувач не існує"
25 25
26msgid "powered by" 26msgid "return home"
27msgstr "за підтримки" 27msgstr "повернутися на головну"
28
29msgid "debug mode is on so cache is off."
30msgstr "режим відладки включено, отже кеш виключено."
31
32msgid "your poche version:"
33msgstr "версія вашої poche:"
34
35msgid "storage:"
36msgstr "сховище:"
37
38msgid "home"
39msgstr "головна"
40
41msgid "favorites"
42msgstr "вибране"
43
44msgid "archive"
45msgstr "архів"
46
47msgid "tags"
48msgstr "теги"
49 28
50msgid "config" 29msgid "config"
51msgstr "налаштування" 30msgstr "налаштування"
52 31
53msgid "logout" 32msgid "Saving articles"
54msgstr "вихід"
55
56msgid "return home"
57msgstr "повернутися на головну"
58
59msgid "Poching links"
60msgstr "Зберігання посилань" 33msgstr "Зберігання посилань"
61 34
62msgid "There are several ways to poche a link:" 35msgid "There are several ways to save an article:"
63msgstr "Є кілька способів зберегти послнн:" 36msgstr "Є кілька способів зберегти статю:"
64 37
65msgid "read the documentation" 38msgid "read the documentation"
66msgstr "читати документацію" 39msgstr "читати документацію"
@@ -83,14 +56,14 @@ msgstr "завантажити додаток"
83msgid "By filling this field" 56msgid "By filling this field"
84msgstr "Заповнивши це поле" 57msgstr "Заповнивши це поле"
85 58
86msgid "poche it!" 59msgid "bag it!"
87msgstr "зберегти!" 60msgstr "зберегти!"
88 61
89msgid "Bookmarklet: drag & drop this link to your bookmarks bar" 62msgid "Bookmarklet: drag & drop this link to your bookmarks bar"
90msgstr "З допомогою закладки: перетягніть і відпустіть посилання на панель закладок" 63msgstr "З допомогою закладки: перетягніть і відпустіть посилання на панель закладок"
91 64
92msgid "Updating poche" 65msgid "Upgrading wallabag"
93msgstr "Оновлення poche" 66msgstr "Оновлення wallabag"
94 67
95msgid "Installed version" 68msgid "Installed version"
96msgstr "Встановлено ​​версію" 69msgstr "Встановлено ​​версію"
@@ -104,14 +77,11 @@ msgstr "Є новіша стабільна версія."
104msgid "You are up to date." 77msgid "You are up to date."
105msgstr "У вас остання версія." 78msgstr "У вас остання версія."
106 79
107msgid "latest dev version" 80msgid "Latest dev version"
108msgstr "остання версія в розробці" 81msgstr "Остання версія в розробці"
109
110msgid "a more recent development version is available."
111msgstr "доступна новіша версія в розробці."
112 82
113msgid "you are up to date." 83msgid "A more recent development version is available."
114msgstr "у с стнн версія." 84msgstr "остун овша версія в розробці."
115 85
116msgid "Feeds" 86msgid "Feeds"
117msgstr "Завантаження (feeds)" 87msgstr "Завантаження (feeds)"
@@ -170,8 +140,8 @@ msgstr "Імпортування"
170msgid "Please execute the import script locally as it can take a very long time." 140msgid "Please execute the import script locally as it can take a very long time."
171msgstr "Будь ласка, виконайте сценарій імпорту локально, оскільки це може тривати досить довго." 141msgstr "Будь ласка, виконайте сценарій імпорту локально, оскільки це може тривати досить довго."
172 142
173msgid "More info in the official docs:" 143msgid "More info in the official documentation:"
174msgstr "Більш тальна інформаці в офіційній документації:" 144msgstr "Більше інформаці в офіційній документації:"
175 145
176msgid "Import from Pocket" 146msgid "Import from Pocket"
177msgstr "Імпорт з Pocket-а" 147msgstr "Імпорт з Pocket-а"
@@ -186,11 +156,11 @@ msgstr "Імпорт з Readability"
186msgid "Import from Instapaper" 156msgid "Import from Instapaper"
187msgstr "Імпорт з Instapaper" 157msgstr "Імпорт з Instapaper"
188 158
189msgid "Import from poche" 159msgid "Import from wallabag"
190msgstr "Імпорт з poche" 160msgstr "Імпорт з wallabag"
191 161
192msgid "Export your poche data" 162msgid "Export your wallabag data"
193msgstr "Експортувати ваші дані з poche" 163msgstr "Експортувати ваші дані з wallabag"
194 164
195msgid "Click here" 165msgid "Click here"
196msgstr "Клікніть тут" 166msgstr "Клікніть тут"
@@ -198,32 +168,14 @@ msgstr "Клікніть тут"
198msgid "to download your database." 168msgid "to download your database."
199msgstr "щоб завантажити вашу базу даних." 169msgstr "щоб завантажити вашу базу даних."
200 170
201msgid "to export your poche data." 171msgid "to export your wallabag data."
202msgstr "щоб експортувати ваші дані poche." 172msgstr "щоб експортувати ваші дані wallabag."
203 173
204msgid "Tag" 174msgid "Cache"
205msgstr "ег" 175msgstr "еш"
206 176
207msgid "No link available here!" 177msgid "to delete cache."
208msgstr "Немає доступних посилань!" 178msgstr "щоб очистити кеш."
209
210msgid "toggle mark as read"
211msgstr "змінити мітку на прочитано"
212
213msgid "toggle favorite"
214msgstr "змінити мітку вибраного"
215
216msgid "delete"
217msgstr "видалити"
218
219msgid "original"
220msgstr "оригінал"
221
222msgid "estimated reading time:"
223msgstr "приблизний час читання:"
224
225msgid "results"
226msgstr "результат(ів)"
227 179
228msgid "You can enter multiple tags, separated by commas." 180msgid "You can enter multiple tags, separated by commas."
229msgstr "Ви можете ввести декілька тегів, розділених комами." 181msgstr "Ви можете ввести декілька тегів, розділених комами."
@@ -240,6 +192,9 @@ msgstr "Ви можете <a href='wallabag_compatibility_test.php'>переві
240msgid "favoris" 192msgid "favoris"
241msgstr "вибране" 193msgstr "вибране"
242 194
195msgid "archive"
196msgstr "архів"
197
243msgid "unread" 198msgid "unread"
244msgstr "непрочитане" 199msgstr "непрочитане"
245 200
@@ -261,12 +216,33 @@ msgstr "за назвою"
261msgid "by title desc" 216msgid "by title desc"
262msgstr "за назвою по спаданню" 217msgstr "за назвою по спаданню"
263 218
219msgid "Tag"
220msgstr "Тег"
221
264msgid "No articles found." 222msgid "No articles found."
265msgstr "Статей не знайдено." 223msgstr "Статей не знайдено."
266 224
267msgid "Toggle mark as read" 225msgid "Toggle mark as read"
268msgstr "змінити мітку прочитаного" 226msgstr "змінити мітку прочитаного"
269 227
228msgid "toggle favorite"
229msgstr "змінити мітку вибраного"
230
231msgid "delete"
232msgstr "видалити"
233
234msgid "original"
235msgstr "оригінал"
236
237msgid "estimated reading time:"
238msgstr "приблизний час читання:"
239
240msgid "mark all the entries as read"
241msgstr "відмітити всі статті як прочитані"
242
243msgid "results"
244msgstr "результат(ів)"
245
270msgid "installation" 246msgid "installation"
271msgstr "інсталяція" 247msgstr "інсталяція"
272 248
@@ -303,6 +279,18 @@ msgstr "Запам'ятати мене"
303msgid "(Do not check on public computers)" 279msgid "(Do not check on public computers)"
304msgstr "(Не відмічайте на загальнодоступних комп'ютерах)" 280msgstr "(Не відмічайте на загальнодоступних комп'ютерах)"
305 281
282msgid "Sign in"
283msgstr "Увійти"
284
285msgid "favorites"
286msgstr "вибране"
287
288msgid "estimated reading time :"
289msgstr "приблизний час читання:"
290
291msgid "Mark all the entries as read"
292msgstr "Відмітити все як прочитане"
293
306msgid "Return home" 294msgid "Return home"
307msgstr "Повернутися на головну" 295msgstr "Повернутися на головну"
308 296
@@ -342,11 +330,95 @@ msgstr "теги:"
342msgid "Edit tags" 330msgid "Edit tags"
343msgstr "Редагувати теги" 331msgstr "Редагувати теги"
344 332
345msgid "previous" 333msgid "save link!"
346msgstr "ре" 334msgstr "ерет лнк!"
347 335
348msgid "next" 336msgid "home"
349msgstr "наступна" 337msgstr "головна"
338
339msgid "tags"
340msgstr "теги"
341
342msgid "logout"
343msgstr "вихід"
344
345msgid "powered by"
346msgstr "за підтримки"
347
348msgid "debug mode is on so cache is off."
349msgstr "режим відладки включено, отже кеш виключено."
350
351msgid "your wallabag version:"
352msgstr "версія вашого wallabag:"
353
354msgid "storage:"
355msgstr "сховище:"
356
357msgid "save a link"
358msgstr "зберегти лінк"
359
360msgid "back to home"
361msgstr "назад на головну"
362
363msgid "toggle mark as read"
364msgstr "змінити мітку на прочитано"
365
366msgid "tweet"
367msgstr "твітнути"
368
369msgid "email"
370msgstr "email"
371
372msgid "this article appears wrong?"
373msgstr "ця стаття виглядає не так, як треба?"
374
375msgid "No link available here!"
376msgstr "Немає доступних посилань!"
377
378msgid "Poching a link"
379msgstr "Зберігання посилання"
380
381msgid "by filling this field"
382msgstr "заповнивши це поле"
383
384msgid "bookmarklet: drag & drop this link to your bookmarks bar"
385msgstr "з допомогою закладки: перетягніть і відпустіть посилання на панель закладок"
386
387msgid "your version"
388msgstr "ваша версія:"
389
390msgid "latest stable version"
391msgstr "остання стабільна версія"
392
393msgid "a more recent stable version is available."
394msgstr "є новіша стабільна версія."
395
396msgid "you are up to date."
397msgstr "у вас остання версія."
398
399msgid "latest dev version"
400msgstr "остання версія в розробці"
401
402msgid "a more recent development version is available."
403msgstr "доступна новіша версія в розробці."
404
405msgid "Please execute the import script locally, it can take a very long time."
406msgstr "Будь ласка, виконайте сценарій імпорту локально, оскільки це може тривати досить довго."
407
408msgid "More infos in the official doc:"
409msgstr "Більше інформації в офіційній документації:"
410
411msgid "import from Pocket"
412msgstr "імпорт з Pocket-а"
413
414msgid "import from Readability"
415msgstr "імпорт з Readability"
416
417msgid "import from Instapaper"
418msgstr "імпорт з Instapaper"
419
420msgid "Tags"
421msgstr "Теги"
350 422
351msgid "Untitled" 423msgid "Untitled"
352msgstr "Без назви" 424msgstr "Без назви"
@@ -363,6 +435,15 @@ msgstr "посилання успішно видалено"
363msgid "the link wasn't deleted" 435msgid "the link wasn't deleted"
364msgstr "посилання не було видалено" 436msgstr "посилання не було видалено"
365 437
438msgid "Article not found!"
439msgstr "Статтю не знайдено!"
440
441msgid "previous"
442msgstr "попередня"
443
444msgid "next"
445msgstr "наступна"
446
366msgid "in demo mode, you can't update your password" 447msgid "in demo mode, you can't update your password"
367msgstr "в демонстраційному режимі ви не можете змінювати свій пароль" 448msgstr "в демонстраційному режимі ви не можете змінювати свій пароль"
368 449
@@ -390,15 +471,12 @@ msgstr "ви змінили свої налаштування мови"
390msgid "login failed: you have to fill all fields" 471msgid "login failed: you have to fill all fields"
391msgstr "увійти не вдалося: ви повинні заповнити всі поля" 472msgstr "увійти не вдалося: ви повинні заповнити всі поля"
392 473
393msgid "welcome to your poche" 474msgid "welcome to your wallabag"
394msgstr "ласкаво просимо до вашого poche" 475msgstr "ласкаво просимо до вашого wallabag"
395 476
396msgid "login failed: bad login or password" 477msgid "login failed: bad login or password"
397msgstr "увійти не вдалося: не вірний логін або пароль" 478msgstr "увійти не вдалося: не вірний логін або пароль"
398 479
399msgid "see you soon!"
400msgstr "бувайте, ще побачимось!"
401
402msgid "import from instapaper completed" 480msgid "import from instapaper completed"
403msgstr "імпорт з instapaper-а завершено" 481msgstr "імпорт з instapaper-а завершено"
404 482
@@ -423,6 +501,34 @@ msgstr "Не вдалося знайти потрібний \""
423msgid "Uh, there is a problem while generating feeds." 501msgid "Uh, there is a problem while generating feeds."
424msgstr "Ох, є проблема при створенні завантажень (feeds)." 502msgstr "Ох, є проблема при створенні завантажень (feeds)."
425 503
504msgid "Cache deleted."
505msgstr "Кеш очищено."
506
426msgid "Oops, it seems you don't have PHP 5." 507msgid "Oops, it seems you don't have PHP 5."
427msgstr "Упс, здається, у вас немає PHP 5." 508msgstr "Упс, здається, у вас немає PHP 5."
428 509
510#~ msgid "You can poche a link by several methods:"
511#~ msgstr "Ви можете зберегти посилання кількома способами:"
512
513#~ msgid "poche it!"
514#~ msgstr "зберегти!"
515
516#~ msgid "Updating poche"
517#~ msgstr "Оновлення poche"
518
519#, fuzzy
520#~ msgid "Export your poche datas"
521#~ msgstr "Експортувати ваші дані з poche"
522
523#, fuzzy
524#~ msgid "to export your poche datas."
525#~ msgstr "щоб експортувати ваші дані poche."
526
527#~ msgid "Import from poche"
528#~ msgstr "Імпорт з poche"
529
530#~ msgid "welcome to your poche"
531#~ msgstr "ласкаво просимо до вашого poche"
532
533#~ msgid "see you soon!"
534#~ msgstr "бувайте, ще побачимось!"
diff --git a/themes/baggy/_display-mode.twig b/themes/baggy/_display-mode.twig
new file mode 100755
index 00000000..382dd5f1
--- /dev/null
+++ b/themes/baggy/_display-mode.twig
@@ -0,0 +1,5 @@
1<div id="display-mode">
2 <a href="javascript: void(null);" id="listmode" class="listmode">
3 <img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/blank.png" alt="{% trans "toggle view mode" %}" title="{% trans "toggle view mode" %}" width="16" height="16">
4 </a>
5</div>
diff --git a/themes/baggy/_head.twig b/themes/baggy/_head.twig
index 206d5aae..be11673f 100644..100755
--- a/themes/baggy/_head.twig
+++ b/themes/baggy/_head.twig
@@ -1,12 +1,16 @@
1 <link rel="shortcut icon" type="image/x-icon" href="{{ poche_url }}/themes/{{theme}}/img/favicon.ico" /> 1 <link rel="shortcut icon" type="image/x-icon" href="{{ poche_url }}themes/{{theme}}/img/favicon.ico" />
2 <link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ poche_url }}/themes/{{theme}}/img/apple-touch-icon-144x144-precomposed.png"> 2 <link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ poche_url }}themes/{{theme}}/img/apple-touch-icon-144x144-precomposed.png">
3 <link rel="apple-touch-icon-precomposed" sizes="72x72" href="{{ poche_url }}/themes/{{theme}}/img/apple-touch-icon-72x72-precomposed.png"> 3 <link rel="apple-touch-icon-precomposed" sizes="72x72" href="{{ poche_url }}themes/{{theme}}/img/apple-touch-icon-72x72-precomposed.png">
4 <link rel="apple-touch-icon-precomposed" href="{{ poche_url }}/themes/{{theme}}/img/apple-touch-icon-precomposed.png"> 4 <link rel="apple-touch-icon-precomposed" href="{{ poche_url }}themes/{{theme}}/img/apple-touch-icon-precomposed.png">
5 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/ratatouille.css" media="all"> 5 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/ratatouille.css" media="all">
6 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/font.css" media="all"> 6 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/font.css" media="all">
7 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/main.css" media="all"> 7 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/main.css" media="all">
8 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/messages.css" media="all"> 8 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/messages.css" media="all">
9 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/print.css" media="print"> 9 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/print.css" media="print">
10 <script src="{{ poche_url }}/themes/{{theme}}/js/jquery-2.0.3.min.js"></script> 10 <script src="{{ poche_url }}themes/default/js/jquery-2.0.3.min.js"></script>
11 <script src="{{ poche_url }}/themes/{{theme}}/js/init.js"></script> 11 <script src="{{ poche_url }}themes/default/js/autoClose.js"></script>
12 <script src="{{ poche_url }}/themes/{{ constant('DEFAULT_THEME') }}/js/closeMessage.js"></script> 12 <script src="{{ poche_url }}themes/{{theme}}/js/jquery.cookie.js"></script>
13 <script src="{{ poche_url }}themes/{{theme}}/js/init.js"></script>
14 <script src="{{ poche_url }}themes/default/js/saveLink.js"></script>
15 <script src="{{ poche_url }}themes/default/js/popupForm.js"></script>
16 <script src="{{ poche_url }}themes/{{theme}}/js/closeMessage.js"></script>
diff --git a/themes/baggy/_menu.twig b/themes/baggy/_menu.twig
index e9cd9d4a..f1b75cd5 100644
--- a/themes/baggy/_menu.twig
+++ b/themes/baggy/_menu.twig
@@ -4,9 +4,13 @@
4 <li><a href="./?view=fav" {% if view == 'fav' %}class="current"{% endif %}>{% trans "favorites" %}</a></li> 4 <li><a href="./?view=fav" {% if view == 'fav' %}class="current"{% endif %}>{% trans "favorites" %}</a></li>
5 <li><a href="./?view=archive" {% if view == 'archive' %}class="current"{% endif %}>{% trans "archive" %}</a></li> 5 <li><a href="./?view=archive" {% if view == 'archive' %}class="current"{% endif %}>{% trans "archive" %}</a></li>
6 <li><a href="./?view=tags" {% if view == 'tags' %}class="current"{% endif %}>{% trans "tags" %}</a></li> 6 <li><a href="./?view=tags" {% if view == 'tags' %}class="current"{% endif %}>{% trans "tags" %}</a></li>
7 <li><a href="javascript: void(null);" id="pocheit">{% trans "save a link" %}</a></li> 7 <li style="position: relative;"><a href="javascript: void(null);" id="bagit">{% trans "save a link" %}</a>
8 {% include '_pocheit-form.twig' %}
9 </li>
10 <li style="position: relative;"><a href="javascript: void(null);" id="search">{% trans "search" %}</a>
11 {% include '_search-form.twig' %}
12 </li>
8 <li><a href="./?view=config" {% if view == 'config' %}class="current"{% endif %}>{% trans "config" %}</a></li> 13 <li><a href="./?view=config" {% if view == 'config' %}class="current"{% endif %}>{% trans "config" %}</a></li>
9 <li><a class="icon icon-power" href="./?logout" title="{% trans "logout" %}">{% trans "logout" %}</a></li> 14 <li><a class="icon icon-power" href="./?logout" title="{% trans "logout" %}">{% trans "logout" %}</a></li>
10 </ul> 15 </ul>
11 16
12 {% include '_pocheit-form.twig' %}
diff --git a/themes/baggy/_pocheit-form.twig b/themes/baggy/_pocheit-form.twig
new file mode 100755
index 00000000..505ec368
--- /dev/null
+++ b/themes/baggy/_pocheit-form.twig
@@ -0,0 +1,10 @@
1<div id="bagit-form" class="messages info">
2 <form method="get" action="index.php" target="_blank" id="bagit-form-form">
3 <h2><a href="javascript: void(null);" id="bagit-form-close">X</a>
4 {% trans "Save a link" %}</h2>
5 <input type="hidden" name="autoclose" value="1" />
6 <input required placeholder="example.com/article" class="addurl" id="plainurl" name="plainurl" type="url" />
7 <input type="submit" value="{% trans "save link!" %}" />
8 <div id="add-link-result"></div>
9 </form>
10</div>
diff --git a/themes/baggy/_search-form.twig b/themes/baggy/_search-form.twig
new file mode 100644
index 00000000..b8ac3bfa
--- /dev/null
+++ b/themes/baggy/_search-form.twig
@@ -0,0 +1,7 @@
1<div id="search-form" class="messages info">
2<form method="get" action="index.php">
3 <input type="hidden" name="view" value="search"></input>
4 <label><a href="javascript: void(null);" id="search-form-close">X</a>{% trans "Search" %}</label> : <input type="text" name="search" />
5 <input id="submit-search" type="submit" value="{% trans "Search" %} !"></input>
6</form>
7</div> \ No newline at end of file
diff --git a/themes/baggy/_top.twig b/themes/baggy/_top.twig
index 4f476a37..a31c0925 100644..100755
--- a/themes/baggy/_top.twig
+++ b/themes/baggy/_top.twig
@@ -1,6 +1,6 @@
1 <header class="w600p center mbm"> 1 <header class="w600p center mbm">
2 <h1 class="logo"> 2 <h1 class="logo">
3 {% if view == 'home' %}{% block logo %}<img width="100" height="100" src="{{ poche_url }}/themes/{{theme}}/img/logo-w.png" alt="logo poche" />{% endblock %} 3 {% if view == 'home' %}{% block logo %}<img width="100" height="100" src="{{ poche_url }}themes/{{theme}}/img/logo-w.png" alt="wallabag logo" />{% endblock %}
4 {% else %}<a href="./" title="{% trans "return home" %}" >{{ block('logo') }}</a> 4 {% else %}<a href="./" title="{% trans "return home" %}" >{{ block('logo') }}</a>
5 {% endif %} 5 {% endif %}
6 </h1> 6 </h1>
diff --git a/themes/baggy/config.twig b/themes/baggy/config.twig
index b37ac115..46735f07 100644..100755
--- a/themes/baggy/config.twig
+++ b/themes/baggy/config.twig
@@ -8,10 +8,11 @@
8 <h2>{% trans "Saving articles" %}</h2> 8 <h2>{% trans "Saving articles" %}</h2>
9 <p>{% trans "There are several ways to save an article:" %} (<a href="http://doc.wallabag.org/" title="{% trans "read the documentation" %}">?</a>)</p> 9 <p>{% trans "There are several ways to save an article:" %} (<a href="http://doc.wallabag.org/" title="{% trans "read the documentation" %}">?</a>)</p>
10 <ul> 10 <ul>
11 <li>Firefox: <a href="https://addons.mozilla.org/firefox/addon/poche/" title="download the firefox extension">{% trans "download the extension" %}</a></li> 11 <li>Firefox: <a href="https://addons.mozilla.org/firefox/addon/wallabag/" title="download the firefox extension">{% trans "download the extension" %}</a></li>
12 <li>Chrome: <a href="http://doc.wallabag.org/doku.php?id=users:chrome_extension" title="download the chrome extension">{% trans "download the extension" %}</a></li> 12 <li>Chrome: <a href="http://doc.wallabag.org/doku.php?id=users:chrome_extension" title="download the chrome extension">{% trans "download the extension" %}</a></li>
13 <li>Android: <a href="https://f-droid.org/repository/browse/?fdid=fr.gaulupeau.apps.Poche" title="download the application">{% trans "via F-Droid" %}</a> {% trans " or " %} <a href="https://play.google.com/store/apps/details?id=fr.gaulupeau.apps.InThePoche" title="download the application">{% trans "via Google Play" %}</a></li> 13 <li>Android: <a href="https://f-droid.org/app/fr.gaulupeau.apps.InThePoche" title="download the application">{% trans "via F-Droid" %}</a> {% trans " or " %} <a href="https://play.google.com/store/apps/details?id=fr.gaulupeau.apps.InThePoche" title="download the application">{% trans "via Google Play" %}</a></li>
14 <li>Windows Phone: <a href="https://www.windowsphone.com/en-us/store/app/poche/334de2f0-51b5-4826-8549-a3d805a37e83" title="download the window phone application">{% trans "download the application" %}</a></li> 14 <li>iOS: <a href="https://itunes.apple.com/app/wallabag/id828331015?mt=8" title="download the iOS application">{% trans "download the application" %}</a></li>
15 <li>Windows Phone: <a href="http://www.windowsphone.com/en-us/store/app/wallabag/ff890514-348c-4d0b-9b43-153fff3f7450" title="download the window phone application">{% trans "download the application" %}</a></li>
15 <li> 16 <li>
16 <form method="get" action="index.php"> 17 <form method="get" action="index.php">
17 <label class="addurl" for="config_plainurl">{% trans "By filling this field" %}:</label> 18 <label class="addurl" for="config_plainurl">{% trans "By filling this field" %}:</label>
@@ -25,9 +26,10 @@
25 <h2>{% trans "Upgrading wallabag" %}</h2> 26 <h2>{% trans "Upgrading wallabag" %}</h2>
26 <ul> 27 <ul>
27 <li>{% trans "Installed version" %} : <strong>{{ constant('POCHE') }}</strong></li> 28 <li>{% trans "Installed version" %} : <strong>{{ constant('POCHE') }}</strong></li>
28 <li>{% trans "Latest stable version" %} : {{ prod }}. {% if compare_prod == -1 %}<strong><a href="http://wallabag.org/">{% trans "A more recent stable version is available." %}</a></strong>{% else %}{% trans "You are up to date." %}{% endif %}</li> 29 <li>{% trans "Latest stable version" %} : {{ prod }}. {% if compare_prod == -1 %}<strong><a href="http://wallabag.org/">{% trans "A more recent stable version is available." %}</a></strong>{% else %}{% trans "You are up to date." %}{% endif %} ({% trans "Last check:" %} {{ check_time_prod }})</li>
29 {% if constant('DEBUG_POCHE') == 1 %}<li>{% trans "Latest dev version" %} : {{ dev }}. {% if compare_dev == -1 %}<strong><a href="http://wallabag.org/">{% trans "A more recent development version is available." %}</a></strong>{% else %}{% trans "You are up to date." %}{% endif %}</li>{% endif %} 30 {% if constant('DEBUG_POCHE') == 1 %}<li>{% trans "Latest dev version" %} : {{ dev }}. {% if compare_dev == -1 %}<strong><a href="http://wallabag.org/">{% trans "A more recent development version is available." %}</a></strong>{% else %}{% trans "You are up to date." %}{% endif %} ({% trans "Last check:" %} {{ check_time_dev }}){% endif %}</li>
30 </ul> 31 </ul>
32 <p>{% trans "You can clear cache to check the latest release." %}</p>
31 33
32 <h2>{% trans "Feeds" %}</h2> 34 <h2>{% trans "Feeds" %}</h2>
33 {% if token == '' %} 35 {% if token == '' %}
@@ -42,7 +44,7 @@
42 <p>{% trans "Your user id:" %} <strong>{{user_id}}</strong></p> 44 <p>{% trans "Your user id:" %} <strong>{{user_id}}</strong></p>
43 <p>{% trans "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>." %}</p> 45 <p>{% trans "You can regenerate your token: <a href='?feed&amp;action=generate'>generate!</a>." %}</p>
44 {% endif %} 46 {% endif %}
45 47
46 <h2>{% trans "Change your theme" %}</h2> 48 <h2>{% trans "Change your theme" %}</h2>
47 <form method="post" action="?updatetheme" name="changethemeform"> 49 <form method="post" action="?updatetheme" name="changethemeform">
48 <fieldset class="w500p inline"> 50 <fieldset class="w500p inline">
@@ -103,21 +105,63 @@
103 {% endif %} 105 {% endif %}
104 106
105 <h2>{% trans "Import" %}</h2> 107 <h2>{% trans "Import" %}</h2>
106 <p>{% trans "Please execute the import script locally as it can take a very long time." %}</p> 108 <p>{% trans "You can import your Pocket, Readability, Instapaper, Wallabag or any data in appropriate json or html format." %}</p>
107 <p>{% trans "More info in the official documentation:" %} <a href="http://doc.wallabag.org/doku.php?id=users:migrate">wallabag.org</a></p> 109 <p>{% trans "Please select export file on your computer and press \"Import\" button below.<br>Wallabag will parse your file, insert all URLs and start fetching of articles if required.<br>Fetching process is controlled by two constants in your config file: IMPORT_LIMIT (how many articles are fetched at once) and IMPORT_DELAY (delay between fetch of next batch of articles)." %}</p>
108 <ul> 110 <form method="post" action="?import" name="uploadfile" enctype="multipart/form-data">
109 <li><a href="./?import&amp;from=pocket">{% trans "Import from Pocket" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('POCKET_FILE')) }}</li> 111 <fieldset class="w500p">
110 <li><a href="./?import&amp;from=readability">{% trans "Import from Readability" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('READABILITY_FILE')) }}</li> 112 <div class="row">
111 <li><a href="./?import&amp;from=instapaper">{% trans "Import from Instapaper" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('INSTAPAPER_FILE')) }}</li> 113 <label class="col w150p" for="file">{% trans "File:" %}</label>
112 <li><a href="./?import&amp;from=poche">{% trans "Import from wallabag" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('POCHE_FILE')) }}</li> 114 <input class="col" type="file" id="file" name="file" tabindex="4">
113 </ul> 115 </div>
114 116 <div class="row mts txtcenter">
117 <button class="bouton" type="submit" tabindex="4">{% trans "Import" %}</button>
118 </div>
119 </fieldset>
120 </form>
121 <p><a href="?import">{% trans "You can click here to fetch content for articles with no content." %}</a></p>
122
115 <h2>{% trans "Export your wallabag data" %}</h2> 123 <h2>{% trans "Export your wallabag data" %}</h2>
116 {% if constant('STORAGE') == 'sqlite' %} 124 {% if constant('STORAGE') == 'sqlite' %}
117 <p><a href="?download" target="_blank">{% trans "Click here" %}</a> {% trans "to download your database." %}</p>{% endif %} 125 <p><a href="?download" target="_blank">{% trans "Click here" %}</a> {% trans "to download your database." %}</p>{% endif %}
118 <p><a href="?export" target="_blank">{% trans "Click here" %}</a> {% trans "to export your wallabag data." %}</p> 126 <p><a href="?export" target="_blank">{% trans "Click here" %}</a> {% trans "to export your wallabag data." %}</p>
127
128 <h2>{% trans "Fancy an E-Book ?" %}</h2>
129 <p>{% trans "Click on <a href=\"./?epub&amp;method=all\" title=\"Generate ePub\">this link</a> to get all your articles in one ebook (ePub 3 format)." %}
130 <br>{% trans "This can <b>take a while</b> and can <b>even fail</b> if you have too many articles, depending on your server configuration." %}</p>
119 131
120 <h2>{% trans "Cache" %}</h2> 132 <h2>{% trans "Cache" %}</h2>
121 <p><a href="?empty-cache">{% trans "Click here" %}</a> {% trans "to delete cache." %}</p> 133 <p><a href="?empty-cache">{% trans "Click here" %}</a> {% trans "to delete cache." %}</p>
122 134
135 <h2>{% trans 'Add user' %}</h2>
136 <p>{% trans 'Add a new user :' %}</p>
137 <form method="post" action="?newuser">
138 <fieldset class="w500p">
139 <div class="row">
140 <label class="col w150p" for="newusername">{% trans 'Login for new user' %}</label>
141 <input class="col" type="text" id="newusername" name="newusername" placeholder="{% trans 'Login' %}">
142 </div>
143 <div class="row">
144 <label class="col w150p" for="password4newuser">{% trans "Password for new user" %}</label>
145 <input class="col" type="password" id="password4newuser" name="password4newuser" placeholder="{% trans "Password" %}">
146 </div>
147 <div class="row mts txtcenter">
148 <button type="submit">{% trans "Send" %}</button>
149 </div>
150 </fieldset>
151 </form>
152
153 <h2>{% trans "Delete account" %}</h2>
154 {% if not only_user %}<form method="post" action="?deluser">
155 <p>{% trans "You can delete your account by entering your password and validating." %}<br /><b>{% trans "Be careful, data will be erased forever (that is a very long time)." %}</b></p>
156 <fieldset class="w500p">
157 <div class="row">
158 <label class="col w150p" for="password4deletinguser">{% trans "Type here your password" %}</label>
159 <input class="col" type="password" id="password4deletinguser" name="password4deletinguser" placeholder="{% trans "Password" %}">
160 </div>
161 <div class="row mts txtcenter">
162 <button type="submit">{% trans "Send" %}</button>
163 </div>
164 </form>
165 {% else %}<p>{% trans "You are the only user, you cannot delete your own account." %}<br />
166 {% trans "To completely remove wallabag, delete the wallabag folder on your web server." %}</p>{% endif %}
123{% endblock %} 167{% endblock %}
diff --git a/themes/baggy/css/main.css b/themes/baggy/css/main.css
index 3e128b08..6d320cd2 100755
--- a/themes/baggy/css/main.css
+++ b/themes/baggy/css/main.css
@@ -173,21 +173,22 @@ h2:after {
173#links { 173#links {
174 position: fixed; 174 position: fixed;
175 top: 0; 175 top: 0;
176 width: 9em; 176 width: 10em;
177 left: 0; 177 left: 0;
178 text-align: right; 178 text-align: right;
179 background: #333; 179 background: #333;
180 padding-top: 9em; 180 padding-top: 9.5em;
181 height: 100%; 181 height: 100%;
182 box-shadow:inset -4px 0 20px rgba(0,0,0,0.6); 182 box-shadow:inset -4px 0 20px rgba(0,0,0,0.6);
183 z-index: 10; 183 z-index: 10;
184} 184}
185 185
186#main { 186#main {
187 margin-left: 12em; 187 margin-left: 13em;
188 position: relative; 188 position: relative;
189 z-index: 10; 189 z-index: 10;
190 padding-right: 5%; 190 padding-right: 5%;
191 padding-bottom: 1em;
191} 192}
192 193
193 #links a { 194 #links a {
@@ -227,7 +228,7 @@ h2:after {
227 #links li:last-child { 228 #links li:last-child {
228 position: fixed; 229 position: fixed;
229 bottom: 1em; 230 bottom: 1em;
230 width: 10%; 231 width: 10em;
231 } 232 }
232 233
233 #links li:last-child a:before { 234 #links li:last-child a:before {
@@ -237,6 +238,61 @@ h2:after {
237 } 238 }
238 239
239 240
241#sort {
242 padding: 0;
243 list-style-type: none;
244 opacity: 0.5;
245 display: inline-block;
246}
247
248#sort li {
249 display: inline;
250 font-size: 0.9em;
251}
252
253#sort li + li {
254 margin-left: 10px;
255}
256
257#sort a {
258 padding: 2px 2px 0;
259 vertical-align: middle;
260}
261
262#sort img {
263 vertical-align: baseline;
264}
265#sort img:hover {
266 cursor: pointer;
267}
268
269#display-mode {
270 float: right;
271 vertical-align: middle;
272 margin-top: 10px;
273 margin-bottom: 10px;
274 opacity: 0.5;
275}
276#listmode {
277 width: 16px;
278 display: inline-block;
279 text-decoration: none;
280}
281#listmode a:hover {
282 opacity: 1;
283}
284.tablemode {
285 background-image: url("../img/baggy/table.png");
286 background-repeat: no-repeat;
287 background-position: bottom;
288}
289.listmode {
290 background-image: url("../img/baggy/list.png");
291 background-repeat: no-repeat;
292 background-position: bottom;
293}
294
295
240/* ========================================================================== 296/* ==========================================================================
241 2 = Layout 297 2 = Layout
242 ========================================================================== */ 298 ========================================================================== */
@@ -248,7 +304,7 @@ h2:after {
248 304
249footer { 305footer {
250 text-align: right; 306 text-align: right;
251 position: fixed; 307 position: relative;
252 bottom: 0; 308 bottom: 0;
253 right: 5em; 309 right: 5em;
254 color: #999; 310 color: #999;
@@ -266,6 +322,15 @@ footer a {
266 letter-spacing:-5px; 322 letter-spacing:-5px;
267} 323}
268 324
325.listmode .entrie {
326 width: 100%!important;
327 margin-left: 0!important;
328}
329
330.listmode .entrie p {
331 display: none;
332}
333
269.list-entries + .results { 334.list-entries + .results {
270 margin-bottom: 2em; 335 margin-bottom: 2em;
271} 336}
@@ -359,6 +424,7 @@ footer a {
359 content: none; 424 content: none;
360 } 425 }
361 426
427
362.entrie h2 a { 428.entrie h2 a {
363 display: block; 429 display: block;
364 text-decoration: none; 430 text-decoration: none;
@@ -370,7 +436,7 @@ footer a {
370 -o-transition: all 0.5s ease; 436 -o-transition: all 0.5s ease;
371 transition: all 0.5s ease; 437 transition: all 0.5s ease;
372} 438}
373 439/*
374.entrie h2 a:after { 440.entrie h2 a:after {
375 content: ""; 441 content: "";
376 position: absolute; 442 position: absolute;
@@ -379,6 +445,7 @@ footer a {
379 height: 100%; 445 height: 100%;
380 left: 0; 446 left: 0;
381} 447}
448*/
382 449
383.entrie p { 450.entrie p {
384 color: #666; 451 color: #666;
@@ -442,6 +509,7 @@ footer a {
442 509
443.pagination { 510.pagination {
444 text-align: right; 511 text-align: right;
512 margin-bottom:50px;
445} 513}
446 514
447.nb-results { 515.nb-results {
@@ -469,6 +537,153 @@ footer a {
469} 537}
470 538
471/* ========================================================================== 539/* ==========================================================================
540 2.1 = "save a link" related styles
541 ========================================================================== */
542
543#bagit-form, #search-form {
544 background: rgba(0,0,0,0.5);
545 position: absolute;
546 top: 0;
547 left: 10em;
548 z-index: 20;
549 height: 100%;
550 width: 100%;
551 margin: 0;
552 margin-top: -30%;
553 padding: 2em;
554 display: none;
555 border-left: 1px #EEE solid;
556}
557
558#bagit-form form, #search-form form {
559 background: #FFF;
560 position: absolute;
561 top: 0;
562 left: 0;
563 z-index: 20;
564 border: 10px solid #000;
565 width: 400px;
566 height: 200px;
567 /* margin: -150px 0 0 -300px; */
568 padding: 2em;
569}
570
571a#bagit-form-close, a#search-form-close {
572 background: #000;
573 color: #FFF;
574 padding: 0.2em 0.5em;
575 text-decoration: none;
576 display: inline-block;
577 float: right;
578 font-size: 0.6em;
579}
580a#bagit-form-close:hover, a#search-form-close:hover {
581 background: #999;
582 color: #000;
583}
584
585.active-current {
586 background-color: #999;
587}
588
589.active-current:after {
590 content: "";
591 width: 0;
592 height: 0;
593 position: absolute;
594 border-style: solid;
595 border-width: 10px;
596 border-color: transparent #EEE transparent transparent;
597 right: 0;
598 top: 50%;
599 margin-top: -10px;
600}
601
602.opacity03 {
603 opacity: 0.3;
604}
605
606.add-to-wallabag-link-after {
607 background-color: #000;
608 color: #fff;
609 padding: 0 3px 2px 3px;
610}
611
612a.add-to-wallabag-link-after {
613 visibility: hidden;
614 position: absolute;
615 opacity: 0;
616 transition-duration: 2s;
617 transition-timing-function: ease-out;
618}
619
620#article article a:hover + a.add-to-wallabag-link-after, a.add-to-wallabag-link-after:hover {
621 opacity: 1;
622 visibility: visible;
623 transition-duration: .3s;
624 transition-timing-function: ease-in;
625}
626
627a.add-to-wallabag-link-after:after {
628 content: "w";
629}
630
631#add-link-result {
632 font-weight: bold;
633 margin-top: 10px;
634}
635
636/* ==========================================================================
637 2.2 = "search for articles" popup div related styles
638 ========================================================================== */
639#search-form {
640 background: rgba(0,0,0,0.5);
641 position: absolute;
642 top: 0;
643 left: 10em;
644 z-index: 20;
645 height: 100%;
646 width: 100%;
647 margin: 0;
648 margin-top: -30%;
649 padding: 2em;
650 display: none;
651 border-left: 1px #EEE solid;
652}
653
654#search-form form {
655 background: #FFF;
656 position: absolute;
657 top: 0;
658 left: 0;
659 z-index: 20;
660 border: 10px solid #000;
661 width: 400px;
662 height: 200px;
663 /* margin: -150px 0 0 -300px; */
664 padding: 2em;
665}
666
667a#search-form-close {
668 background: #000;
669 color: #FFF;
670 padding: 0.2em 0.5em;
671 text-decoration: none;
672 display: inline-block;
673 float: right;
674 font-size: 1.2em;
675}
676a#search-form-close:hover {
677 background: #999;
678 color: #000;
679}
680
681#submit-search{
682margin-left: 4em;
683margin-top:1em;
684}
685
686/* ==========================================================================
472 3 = Pictos 687 3 = Pictos
473 ========================================================================== */ 688 ========================================================================== */
474 689
@@ -550,6 +765,9 @@ footer a {
550.icon-rss:before { 765.icon-rss:before {
551 content: "\e808"; 766 content: "\e808";
552} 767}
768.icon-print:before {
769 content: "\e80d";
770}
553 771
554/* ========================================================================== 772/* ==========================================================================
555 Icon selected 773 Icon selected
@@ -583,7 +801,7 @@ footer a {
583 } 801 }
584 802
585.warning { 803.warning {
586 font-size: 3em; 804 /* font-size: 3em;
587 color: #999; 805 color: #999;
588 font-style: italic; 806 font-style: italic;
589 position: absolute; 807 position: absolute;
@@ -592,7 +810,10 @@ footer a {
592 width: 100%; 810 width: 100%;
593 text-align: center; 811 text-align: center;
594 padding-right: 5%; 812 padding-right: 5%;
595 margin-top: -2em; 813 margin-top: -2em;*/
814 font-weight: bold;
815 display: block;
816 width: 100%;
596} 817}
597 818
598/* ========================================================================== 819/* ==========================================================================
@@ -602,6 +823,7 @@ footer a {
602#article { 823#article {
603 width: 70%; 824 width: 70%;
604 margin-bottom: 3em; 825 margin-bottom: 3em;
826 text-align: justify;
605} 827}
606 828
607#article .tags { 829#article .tags {
@@ -731,6 +953,9 @@ blockquote {
731 width: 100%; 953 width: 100%;
732 margin-left: 0; 954 margin-left: 0;
733 } 955 }
956 #display-mode {
957 display: none;
958 }
734} 959}
735 960
736@media screen and (max-width: 500px) { 961@media screen and (max-width: 500px) {
@@ -754,8 +979,8 @@ blockquote {
754 content: none; 979 content: none;
755 } 980 }
756 .logo { 981 .logo {
757 width: 1.5em; 982 width: 1.25em;
758 height: 1.5em; 983 height: 1.25em;
759 left: 0; 984 left: 0;
760 top: 0; 985 top: 0;
761 } 986 }
@@ -805,6 +1030,7 @@ blockquote {
805 margin-left: 1.5em; 1030 margin-left: 1.5em;
806 padding-right: 1.5em; 1031 padding-right: 1.5em;
807 position: static; 1032 position: static;
1033 margin-top: 3em;
808 } 1034 }
809 #article_toolbar .topPosF { 1035 #article_toolbar .topPosF {
810 display: none; 1036 display: none;
@@ -820,4 +1046,12 @@ blockquote {
820 #article_toolbar a { 1046 #article_toolbar a {
821 padding: 0.3em 0.4em 0.2em; 1047 padding: 0.3em 0.4em 0.2em;
822 } 1048 }
1049
1050 #display-mode {
1051 display: none;
1052 }
1053
1054 #bagit-form, #search-form {
1055 left: 0;
1056 }
823} 1057}
diff --git a/themes/baggy/css/print.css b/themes/baggy/css/print.css
index 9aefa779..9dd6d295 100755
--- a/themes/baggy/css/print.css
+++ b/themes/baggy/css/print.css
@@ -17,6 +17,7 @@ img {
17 17
18/* Hide useless blocks */ 18/* Hide useless blocks */
19body > header, 19body > header,
20#article_toolbar,
20#links, 21#links,
21#sort, 22#sort,
22body > footer, 23body > footer,
@@ -46,3 +47,16 @@ abbr[title]:after {
46.pagination span.current { 47.pagination span.current {
47 border-style: dashed; 48 border-style: dashed;
48} 49}
50
51#main {
52 width: 100%;
53 padding: 0;
54 margin: 0;
55 margin-left: 0;
56 padding-right: 0;
57 padding-bottom: 0;
58}
59
60#article {
61 width: 100%;
62}
diff --git a/themes/baggy/edit-tags.twig b/themes/baggy/edit-tags.twig
index 9e9012ee..2113e964 100644..100755
--- a/themes/baggy/edit-tags.twig
+++ b/themes/baggy/edit-tags.twig
@@ -4,6 +4,11 @@
4{% include '_menu.twig' %} 4{% include '_menu.twig' %}
5{% endblock %} 5{% endblock %}
6{% block content %} 6{% block content %}
7
8<script src="{{ poche_url }}themes/default/js/jquery-ui-1.10.4.custom.min.js"></script>
9<script src="{{ poche_url }}themes/default/js/autoCompleteTags.js"></script>
10<link rel="stylesheet" href="{{ poche_url }}themes/default/css/jquery-ui-1.10.4.custom.min.css" media="all">
11
7<div id="article"> 12<div id="article">
8 <h2>{{ entry.title|raw }}</21> 13 <h2>{{ entry.title|raw }}</21>
9</div> 14</div>
@@ -17,7 +22,8 @@
17 <input type="hidden" name="entry_id" value="{{ entry_id }}" /> 22 <input type="hidden" name="entry_id" value="{{ entry_id }}" />
18 <label for="value">Add tags: </label><input type="text" placeholder="interview, editorial, video" id="value" name="value" required="required" /> 23 <label for="value">Add tags: </label><input type="text" placeholder="interview, editorial, video" id="value" name="value" required="required" />
19 <input type="submit" value="Tag" /> 24 <input type="submit" value="Tag" />
20 <p>{% trans "You can enter multiple tags, separated by commas." %}</p> 25 <p>{% trans "Start typing for auto complete." %}<br>
26 {% trans "You can enter multiple tags, separated by commas." %}</p>
21</form> 27</form>
22<a class="icon icon-reply return" href="./?view=view&id={{ entry_id }}">{% trans "return to article" %}</a> 28<a class="icon icon-reply return" href="./?view=view&id={{ entry_id }}">{% trans "return to article" %}</a>
23{% endblock %} 29{% endblock %}
diff --git a/themes/baggy/fonts/icomoon.eot b/themes/baggy/fonts/icomoon.eot
index 02f53b36..56323516 100755..100644
--- a/themes/baggy/fonts/icomoon.eot
+++ b/themes/baggy/fonts/icomoon.eot
Binary files differ
diff --git a/themes/baggy/fonts/icomoon.svg b/themes/baggy/fonts/icomoon.svg
index d9c35b98..b4586136 100755..100644
--- a/themes/baggy/fonts/icomoon.svg
+++ b/themes/baggy/fonts/icomoon.svg
@@ -1,7 +1,21 @@
1<?xml version="1.0" standalone="no"?> 1<?xml version="1.0" standalone="no"?>
2<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > 2<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
3<svg xmlns="http://www.w3.org/2000/svg"> 3<svg xmlns="http://www.w3.org/2000/svg">
4<metadata>Generated by IcoMoon</metadata> 4<metadata>
5<json>
6{
7 "fontFamily": "icomoon",
8 "majorVersion": 1,
9 "minorVersion": 0,
10 "version": "Version 1.0",
11 "fontId": "icomoon",
12 "psName": "icomoon",
13 "subFamily": "Regular",
14 "fullName": "icomoon",
15 "description": "Generated by IcoMoon"
16}
17</json>
18</metadata>
5<defs> 19<defs>
6<font id="icomoon" horiz-adv-x="512"> 20<font id="icomoon" horiz-adv-x="512">
7<font-face units-per-em="512" ascent="480" descent="-32" /> 21<font-face units-per-em="512" ascent="480" descent="-32" />
@@ -23,4 +37,5 @@
23<glyph unicode="&#xe80a;" d="M475.648 50.432v219.136q-9.216-10.24-19.968-18.944-76.288-58.368-121.856-96.256-14.336-12.288-23.552-19.456t-24.576-13.824-29.184-6.656h-1.024q-13.312 0-29.184 6.656t-24.576 13.824-23.552 19.456q-45.056 37.888-121.856 96.256-10.752 8.704-19.968 18.944v-219.136q0-4.096 3.072-6.656t6.144-2.56h420.864q3.584 0 6.144 2.56t3.072 6.656zM475.648 350.464v7.168t-0.512 3.584-0.512 3.584-1.536 2.56-2.56 2.048-4.096 1.024h-420.864q-3.584 0-6.144-3.072t-3.072-6.144q0-48.128 41.984-81.408 55.296-43.52 114.688-90.624 2.048-1.024 10.24-8.192t12.8-10.752 12.8-9.216 14.336-7.68 12.288-2.56h1.024q5.632 0 12.288 2.56t14.336 7.68 12.8 9.216 12.8 10.752 10.24 8.192q59.392 47.104 114.688 90.624 15.36 12.288 28.672 33.28t13.312 37.376zM512 361.216v-310.784q0-18.944-13.312-32.256t-32.256-13.824h-420.864q-18.432 0-32.256 13.824t-13.312 32.256v310.784q0 18.944 13.312 32.256t32.256 13.312h420.864q18.944 0 32.256-13.312t13.312-32.256z" /> 37<glyph unicode="&#xe80a;" d="M475.648 50.432v219.136q-9.216-10.24-19.968-18.944-76.288-58.368-121.856-96.256-14.336-12.288-23.552-19.456t-24.576-13.824-29.184-6.656h-1.024q-13.312 0-29.184 6.656t-24.576 13.824-23.552 19.456q-45.056 37.888-121.856 96.256-10.752 8.704-19.968 18.944v-219.136q0-4.096 3.072-6.656t6.144-2.56h420.864q3.584 0 6.144 2.56t3.072 6.656zM475.648 350.464v7.168t-0.512 3.584-0.512 3.584-1.536 2.56-2.56 2.048-4.096 1.024h-420.864q-3.584 0-6.144-3.072t-3.072-6.144q0-48.128 41.984-81.408 55.296-43.52 114.688-90.624 2.048-1.024 10.24-8.192t12.8-10.752 12.8-9.216 14.336-7.68 12.288-2.56h1.024q5.632 0 12.288 2.56t14.336 7.68 12.8 9.216 12.8 10.752 10.24 8.192q59.392 47.104 114.688 90.624 15.36 12.288 28.672 33.28t13.312 37.376zM512 361.216v-310.784q0-18.944-13.312-32.256t-32.256-13.824h-420.864q-18.432 0-32.256 13.824t-13.312 32.256v310.784q0 18.944 13.312 32.256t32.256 13.312h420.864q18.944 0 32.256-13.312t13.312-32.256z" />
24<glyph unicode="&#xe80b;" d="M0 133.888l256 256 256-256-75.776-75.776-180.224 179.712-180.224-179.712z" /> 38<glyph unicode="&#xe80b;" d="M0 133.888l256 256 256-256-75.776-75.776-180.224 179.712-180.224-179.712z" />
25<glyph unicode="&#xe80c;" d="M25.6 279.296q62.464-35.84 168.96-35.84t168.96 35.84l-27.648-248.832q-1.024-7.168-17.92-18.432t-51.2-22.016-72.192-10.752-71.68 10.752-51.2 22.016-18.432 18.432zM275.456 432.896q48.128-9.216 80.896-28.16t32.768-36.352v-5.12q0-29.696-57.344-50.688t-137.216-20.992-137.216 20.992-57.344 50.688v5.12q0 17.408 32.768 36.352t80.896 28.16l21.504 24.576q11.264 13.312 35.84 13.312h47.104q26.624 0 35.84-13.312zM247.808 375.552h43.008q-47.104 56.32-53.248 64.512-7.168 8.192-16.384 8.192h-52.224q-11.264 0-16.384-8.192l-54.272-64.512h43.008l32.768 33.792h41.984z" horiz-adv-x="389" /> 39<glyph unicode="&#xe80c;" d="M25.6 279.296q62.464-35.84 168.96-35.84t168.96 35.84l-27.648-248.832q-1.024-7.168-17.92-18.432t-51.2-22.016-72.192-10.752-71.68 10.752-51.2 22.016-18.432 18.432zM275.456 432.896q48.128-9.216 80.896-28.16t32.768-36.352v-5.12q0-29.696-57.344-50.688t-137.216-20.992-137.216 20.992-57.344 50.688v5.12q0 17.408 32.768 36.352t80.896 28.16l21.504 24.576q11.264 13.312 35.84 13.312h47.104q26.624 0 35.84-13.312zM247.808 375.552h43.008q-47.104 56.32-53.248 64.512-7.168 8.192-16.384 8.192h-52.224q-11.264 0-16.384-8.192l-54.272-64.512h43.008l32.768 33.792h41.984z" horiz-adv-x="389" />
40<glyph unicode="&#xe80d;" d="M128 448h256v-64h-256zM480 352h-448c-17.6 0-32-14.4-32-32v-160c0-17.6 14.398-32 32-32h96v-128h256v128h96c17.6 0 32 14.4 32 32v160c0 17.6-14.4 32-32 32zM352 32h-192v160h192v-160zM487.2 304c0-12.813-10.387-23.2-23.199-23.2-12.813 0-23.201 10.387-23.201 23.2s10.388 23.2 23.201 23.2c12.813 0 23.199-10.387 23.199-23.2z" />
26</font></defs></svg> \ No newline at end of file 41</font></defs></svg> \ No newline at end of file
diff --git a/themes/baggy/fonts/icomoon.ttf b/themes/baggy/fonts/icomoon.ttf
index 3860dd04..bb1f21f8 100755..100644
--- a/themes/baggy/fonts/icomoon.ttf
+++ b/themes/baggy/fonts/icomoon.ttf
Binary files differ
diff --git a/themes/baggy/fonts/icomoon.woff b/themes/baggy/fonts/icomoon.woff
index c3a91ed7..bab13779 100755..100644
--- a/themes/baggy/fonts/icomoon.woff
+++ b/themes/baggy/fonts/icomoon.woff
Binary files differ
diff --git a/themes/baggy/home.twig b/themes/baggy/home.twig
index 4f9db063..3942d3bf 100644..100755
--- a/themes/baggy/home.twig
+++ b/themes/baggy/home.twig
@@ -18,17 +18,27 @@
18 {% if entries is empty %} 18 {% if entries is empty %}
19 <div class="messages warning"><p>{% trans "No articles found." %}</p></div> 19 <div class="messages warning"><p>{% trans "No articles found." %}</p></div>
20 {% else %} 20 {% else %}
21 <div>
22 {% include '_display-mode.twig' %}
23 {% include '_sorting.twig' %}
24 </div>
21 {% block pager %} 25 {% block pager %}
22 {% if nb_results > 1 %} 26 {% if nb_results > 1 %}
23 <div class="results"> 27 <div class="results">
24 <div class="nb-results">{{ nb_results }} {% trans "results" %}</div> 28 <div class="nb-results">{{ nb_results }} {% trans "results" %}{% if search_term is defined %} {% trans %}found for « {{ search_term }} »{% endtrans %}{% endif %}</div>
25 {{ page_links | raw }} 29 {{ page_links | raw }}
26 </div> 30 </div>
31 {% elseif nb_results == 1 %}
32 {% if search_term is defined %}
33 <div class="results">
34 <div class="nb-results">{% trans "Only one result found for " %} « {{ search_term }} »</div>
35 </div>
36 {% endif %}
27 {% endif %} 37 {% endif %}
28 {% endblock %} 38 {% endblock %}
29 <div class="list-entries"> 39 <div id="list-entries" class="list-entries">
30 {% for entry in entries %} 40 {% for entry in entries %}
31 <div id="entry-{{ entry.id|e }}" class="entrie"> 41 <div id="entry-{{ entry.id|e }}" class="entrie"{% if listmode %} style="width:100%; margin-left:0;"{% endif %}>
32 <h2><a href="index.php?view=view&amp;id={{ entry.id|e }}">{{ entry.title|raw }}</a></h2> 42 <h2><a href="index.php?view=view&amp;id={{ entry.id|e }}">{{ entry.title|raw }}</a></h2>
33 {% if entry.content| getReadingTime > 0 %} 43 {% if entry.content| getReadingTime > 0 %}
34 <div class="estimatedTime"><a target="_blank" title="{% trans "estimated reading time:" %} {{ entry.content| getReadingTime }} min" class="tool reading-time"><span>{% trans "estimated reading time :" %} {{ entry.content| getReadingTime }} min</span></div> 44 <div class="estimatedTime"><a target="_blank" title="{% trans "estimated reading time:" %} {{ entry.content| getReadingTime }} min" class="tool reading-time"><span>{% trans "estimated reading time :" %} {{ entry.content| getReadingTime }} min</span></div>
@@ -46,7 +56,14 @@
46 56
47 {% endfor %} 57 {% endfor %}
48 </div> 58 </div>
59 {{ block('pager') }}
49 {% if view == 'home' %}{% if nb_results > 1 %}<a title="{% trans "Mark all the entries as read" %}" href="./?action=archive_all">{{ "Mark all the entries as read" }}</a>{% endif %}{% endif %} 60 {% if view == 'home' %}{% if nb_results > 1 %}<a title="{% trans "Mark all the entries as read" %}" href="./?action=archive_all">{{ "Mark all the entries as read" }}</a>{% endif %}{% endif %}
61
62 {% if search_term is defined %}<a title="{% trans %} Apply the tag {{ search_term }} to this search {% endtrans %}" href="./?action=add_tag&search={{ search_term }}">{% trans %} Apply the tag {{ search_term }} to this search {% endtrans %}</a>{% endif %}
63
64 {% if tag %}<a title="{% trans "Download the articles from this tag in an epub" %}" href="./?epub&amp;method=tag&amp;tag={{ tag.value }}">{% trans "Download the articles from this tag in an epub" %}</a>
65 {% elseif search_term is defined %}<a title="{% trans "Download the articles from this search in an epub" %}" href="./?epub&amp;method=search&amp;search={{ search_term }}">{% trans "Download the articles from this search in an epub" %}</a>
66 {% else %}<a title="{% trans "Download the articles from this category in an epub" %}" href="./?epub&amp;method=category&amp;category={{ view }}">{% trans "Download the articles from this category in an epub" %}</a>{% endif %}
67
50 {% endif %} 68 {% endif %}
51 {{ block('pager') }}
52{% endblock %} 69{% endblock %}
diff --git a/themes/baggy/img/baggy/blank.png b/themes/baggy/img/baggy/blank.png
new file mode 100755
index 00000000..63e09844
--- /dev/null
+++ b/themes/baggy/img/baggy/blank.png
Binary files differ
diff --git a/themes/baggy/img/baggy/down.png b/themes/baggy/img/baggy/down.png
new file mode 100644
index 00000000..b9d536a7
--- /dev/null
+++ b/themes/baggy/img/baggy/down.png
Binary files differ
diff --git a/themes/baggy/img/baggy/list.png b/themes/baggy/img/baggy/list.png
new file mode 100755
index 00000000..bd5aff5a
--- /dev/null
+++ b/themes/baggy/img/baggy/list.png
Binary files differ
diff --git a/themes/baggy/img/baggy/table.png b/themes/baggy/img/baggy/table.png
new file mode 100755
index 00000000..859c4cd8
--- /dev/null
+++ b/themes/baggy/img/baggy/table.png
Binary files differ
diff --git a/themes/baggy/img/baggy/top.png b/themes/baggy/img/baggy/top.png
new file mode 100644
index 00000000..954a8c0a
--- /dev/null
+++ b/themes/baggy/img/baggy/top.png
Binary files differ
diff --git a/themes/baggy/js/autoClose.js b/themes/baggy/js/autoClose.js
deleted file mode 100644
index e9145b7e..00000000
--- a/themes/baggy/js/autoClose.js
+++ /dev/null
@@ -1,6 +0,0 @@
1$(document).ready(function() {
2 current_url = window.location.href
3 if (current_url.match("&closewin=true")) {
4 window.close();
5 }
6});
diff --git a/themes/baggy/js/init.js b/themes/baggy/js/init.js
index c1d3c0ec..00470fbf 100755
--- a/themes/baggy/js/init.js
+++ b/themes/baggy/js/init.js
@@ -1,12 +1,48 @@
1document.addEventListener('DOMContentLoaded', function() { 1$.fn.ready(function() {
2 var menu = document.getElementById('menu'); 2
3 3 var $listmode = $('#listmode'),
4 menu.addEventListener('click', function(){ 4 $listentries = $("#list-entries");
5 if(this.nextElementSibling.style.display === "block") { 5
6 this.nextElementSibling.style.display = "none"; 6 /* ==========================================================================
7 }else { 7 Menu
8 this.nextElementSibling.style.display = "block"; 8 ========================================================================== */
9
10 $("#menu").click(function(){
11 $("#links").toggle();
12 });
13
14 /* ==========================================================================
15 List mode or Table Mode
16 ========================================================================== */
17
18 $listmode.click(function(){
19 if ( $.cookie("listmode") == 1 ) {
20 // Cookie
21 $.removeCookie("listmode");
22
23 $listentries.removeClass("listmode");
24 $listmode.removeClass("tablemode");
25 $listmode.addClass("listmode");
9 } 26 }
10 27 else {
28 // Cookie
29 $.cookie("listmode", 1, {expires: 365});
30
31 $listentries.addClass("listmode");
32 $listmode.removeClass("listmode");
33 $listmode.addClass("tablemode");
34 }
35
11 }); 36 });
12}); \ No newline at end of file 37
38 /* ==========================================================================
39 Cookie listmode
40 ========================================================================== */
41
42 if ( $.cookie("listmode") == 1 ) {
43 $listentries.addClass("listmode");
44 $listmode.removeClass("listmode");
45 $listmode.addClass("tablemode");
46 }
47
48});
diff --git a/themes/baggy/js/jquery-2.0.3.min.js b/themes/baggy/js/jquery-2.0.3.min.js
deleted file mode 100644
index a4dd0a2f..00000000
--- a/themes/baggy/js/jquery-2.0.3.min.js
+++ /dev/null
@@ -1 +0,0 @@
1(function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],p="2.0.3",f=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:p,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return f.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,p,f,h,d,g,m,y,v="sizzle"+-new Date,b=e.document,w=0,T=0,C=st(),k=st(),N=st(),E=!1,S=function(e,t){return e===t?(E=!0,0):0},j=typeof undefined,D=1<<31,A={}.hasOwnProperty,L=[],q=L.pop,H=L.push,O=L.push,F=L.slice,P=L.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",W="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",$=W.replace("w","w#"),B="\\["+M+"*("+W+")"+M+"*(?:([*^$|!~]?=)"+M+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+$+")|)|)"+M+"*\\]",I=":("+W+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+B.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=RegExp("^"+M+"*,"+M+"*"),X=RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=RegExp(M+"*[+~]"),Y=RegExp("="+M+"*([^\\]'\"]*)"+M+"*\\]","g"),V=RegExp(I),G=RegExp("^"+$+"$"),J={ID:RegExp("^#("+W+")"),CLASS:RegExp("^\\.("+W+")"),TAG:RegExp("^("+W.replace("w","w*")+")"),ATTR:RegExp("^"+B),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:RegExp("^(?:"+R+")$","i"),needsContext:RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/^(?:input|select|textarea|button)$/i,et=/^h\d$/i,tt=/'|\\/g,nt=RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),rt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{O.apply(L=F.call(b.childNodes),b.childNodes),L[b.childNodes.length].nodeType}catch(it){O={apply:L.length?function(e,t){H.apply(e,F.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function ot(e,t,r,i){var o,s,a,u,l,f,g,m,x,w;if((t?t.ownerDocument||t:b)!==p&&c(t),t=t||p,r=r||[],!e||"string"!=typeof e)return r;if(1!==(u=t.nodeType)&&9!==u)return[];if(h&&!i){if(o=K.exec(e))if(a=o[1]){if(9===u){if(s=t.getElementById(a),!s||!s.parentNode)return r;if(s.id===a)return r.push(s),r}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(a))&&y(t,s)&&s.id===a)return r.push(s),r}else{if(o[2])return O.apply(r,t.getElementsByTagName(e)),r;if((a=o[3])&&n.getElementsByClassName&&t.getElementsByClassName)return O.apply(r,t.getElementsByClassName(a)),r}if(n.qsa&&(!d||!d.test(e))){if(m=g=v,x=t,w=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){f=gt(e),(g=t.getAttribute("id"))?m=g.replace(tt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",l=f.length;while(l--)f[l]=m+mt(f[l]);x=U.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return O.apply(r,x.querySelectorAll(w)),r}catch(T){}finally{g||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,r,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>i.cacheLength&&delete t[e.shift()],t[n]=r}return t}function at(e){return e[v]=!0,e}function ut(e){var t=p.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function lt(e,t){var n=e.split("|"),r=e.length;while(r--)i.attrHandle[n[r]]=t}function ct(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return at(function(t){return t=+t,at(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}s=ot.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},n=ot.support={},c=ot.setDocument=function(e){var t=e?e.ownerDocument||e:b,r=t.defaultView;return t!==p&&9===t.nodeType&&t.documentElement?(p=t,f=t.documentElement,h=!s(t),r&&r.attachEvent&&r!==r.top&&r.attachEvent("onbeforeunload",function(){c()}),n.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ut(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=ut(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),n.getById=ut(function(e){return f.appendChild(e).id=v,!t.getElementsByName||!t.getElementsByName(v).length}),n.getById?(i.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){return e.getAttribute("id")===t}}):(delete i.find.ID,i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=n.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.CLASS=n.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&h?t.getElementsByClassName(e):undefined},g=[],d=[],(n.qsa=Q.test(t.querySelectorAll))&&(ut(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||d.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll(":checked").length||d.push(":checked")}),ut(function(e){var n=t.createElement("input");n.setAttribute("type","hidden"),e.appendChild(n).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&d.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||d.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),d.push(",.*:")})),(n.matchesSelector=Q.test(m=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&ut(function(e){n.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",I)}),d=d.length&&RegExp(d.join("|")),g=g.length&&RegExp(g.join("|")),y=Q.test(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},S=f.compareDocumentPosition?function(e,r){if(e===r)return E=!0,0;var i=r.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(r);return i?1&i||!n.sortDetached&&r.compareDocumentPosition(e)===i?e===t||y(b,e)?-1:r===t||y(b,r)?1:l?P.call(l,e)-P.call(l,r):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],u=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:l?P.call(l,e)-P.call(l,n):0;if(o===s)return ct(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)u.unshift(r);while(a[i]===u[i])i++;return i?ct(a[i],u[i]):a[i]===b?-1:u[i]===b?1:0},t):p},ot.matches=function(e,t){return ot(e,null,null,t)},ot.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Y,"='$1']"),!(!n.matchesSelector||!h||g&&g.test(t)||d&&d.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return ot(t,p,null,[e]).length>0},ot.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},ot.attr=function(e,t){(e.ownerDocument||e)!==p&&c(e);var r=i.attrHandle[t.toLowerCase()],o=r&&A.call(i.attrHandle,t.toLowerCase())?r(e,t,!h):undefined;return o===undefined?n.attributes||!h?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null:o},ot.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ot.uniqueSort=function(e){var t,r=[],i=0,o=0;if(E=!n.detectDuplicates,l=!n.sortStable&&e.slice(0),e.sort(S),E){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return e},o=ot.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=ot.selectors={cacheLength:50,createPseudo:at,match:J,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(nt,rt),e[3]=(e[4]||e[5]||"").replace(nt,rt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ot.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ot.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return J.CHILD.test(e[0])?null:(e[3]&&e[4]!==undefined?e[2]=e[4]:n&&V.test(n)&&(t=gt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(nt,rt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ot.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,y=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){p=t;while(p=p[g])if(a?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[v]||(m[v]={}),l=c[e]||[],h=l[0]===w&&l[1],f=l[0]===w&&l[2],p=h&&m.childNodes[h];while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[w,h,f];break}}else if(x&&(l=(t[v]||(t[v]={}))[e])&&l[0]===w)f=l[1];else while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if((a?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(x&&((p[v]||(p[v]={}))[e]=[w,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||ot.error("unsupported pseudo: "+e);return r[v]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?at(function(e,n){var i,o=r(e,t),s=o.length;while(s--)i=P.call(e,o[s]),e[i]=!(n[i]=o[s])}):function(e){return r(e,0,n)}):r}},pseudos:{not:at(function(e){var t=[],n=[],r=a(e.replace(z,"$1"));return r[v]?at(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:at(function(e){return function(t){return ot(e,t).length>0}}),contains:at(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:at(function(e){return G.test(e||"")||ot.error("unsupported lang: "+e),e=e.replace(nt,rt).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return et.test(e.nodeName)},input:function(e){return Z.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},i.pseudos.nth=i.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[t]=pt(t);for(t in{submit:!0,reset:!0})i.pseudos[t]=ft(t);function dt(){}dt.prototype=i.filters=i.pseudos,i.setFilters=new dt;function gt(e,t){var n,r,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=i.preFilter;while(a){(!n||(r=_.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=X.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(z," ")}),a=a.slice(n.length));for(s in i.filter)!(r=J[s].exec(a))||l[s]&&!(r=l[s](r))||(n=r.shift(),o.push({value:n,type:s,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ot.error(e):k(e,u).slice(0)}function mt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function yt(e,t,n){var i=t.dir,o=n&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,a){var u,l,c,p=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[v]||(t[v]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,a)||r,l[1]===!0)return!0}}function vt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function bt(e,t,n,r,i,o){return r&&!r[v]&&(r=bt(r)),i&&!i[v]&&(i=bt(i,o)),at(function(o,s,a,u){var l,c,p,f=[],h=[],d=s.length,g=o||Ct(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:xt(g,f,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=xt(y,h),r(l,[],a,u),c=l.length;while(c--)(p=l[c])&&(y[h[c]]=!(m[h[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?P.call(o,p):f[c])>-1&&(o[l]=!(s[l]=p))}}else y=xt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):O.apply(s,y)})}function wt(e){var t,n,r,o=e.length,s=i.relative[e[0].type],a=s||i.relative[" "],l=s?1:0,c=yt(function(e){return e===t},a,!0),p=yt(function(e){return P.call(t,e)>-1},a,!0),f=[function(e,n,r){return!s&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>l;l++)if(n=i.relative[e[l].type])f=[yt(vt(f),n)];else{if(n=i.filter[e[l].type].apply(null,e[l].matches),n[v]){for(r=++l;o>r;r++)if(i.relative[e[r].type])break;return bt(l>1&&vt(f),l>1&&mt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&wt(e.slice(l,r)),o>r&&wt(e=e.slice(r)),o>r&&mt(e))}f.push(n)}return vt(f)}function Tt(e,t){var n=0,o=t.length>0,s=e.length>0,a=function(a,l,c,f,h){var d,g,m,y=[],v=0,x="0",b=a&&[],T=null!=h,C=u,k=a||s&&i.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(u=l!==p&&l,r=n);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,c)){f.push(d);break}T&&(w=N,r=++n)}o&&((d=!m&&d)&&v--,a&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,c);if(a){if(v>0)while(x--)b[x]||y[x]||(y[x]=q.call(f));y=xt(y)}O.apply(f,y),T&&!a&&y.length>0&&v+t.length>1&&ot.uniqueSort(f)}return T&&(w=N,u=C),b};return o?at(a):a}a=ot.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=gt(e)),n=t.length;while(n--)o=wt(t[n]),o[v]?r.push(o):i.push(o);o=N(e,Tt(i,r))}return o};function Ct(e,t,n){var r=0,i=t.length;for(;i>r;r++)ot(e,t[r],n);return n}function kt(e,t,r,o){var s,u,l,c,p,f=gt(e);if(!o&&1===f.length){if(u=f[0]=f[0].slice(0),u.length>2&&"ID"===(l=u[0]).type&&n.getById&&9===t.nodeType&&h&&i.relative[u[1].type]){if(t=(i.find.ID(l.matches[0].replace(nt,rt),t)||[])[0],!t)return r;e=e.slice(u.shift().value.length)}s=J.needsContext.test(e)?0:u.length;while(s--){if(l=u[s],i.relative[c=l.type])break;if((p=i.find[c])&&(o=p(l.matches[0].replace(nt,rt),U.test(u[0].type)&&t.parentNode||t))){if(u.splice(s,1),e=o.length&&mt(u),!e)return O.apply(r,o),r;break}}}return a(e,f)(o,t,!h,r,U.test(e)),r}n.sortStable=v.split("").sort(S).join("")===v,n.detectDuplicates=E,c(),n.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(p.createElement("div"))}),ut(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||lt("type|href|height|width",function(e,t,n){return n?undefined:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ut(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||lt("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?undefined:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||lt(R,function(e,t,n){var r;return n?undefined:(r=e.getAttributeNode(t))&&r.specified?r.value:e[t]===!0?t.toLowerCase():null}),x.find=ot,x.expr=ot.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ot.uniqueSort,x.text=ot.getText,x.isXMLDoc=ot.isXML,x.contains=ot.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(p){for(t=e.memory&&p,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(p[0],p[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!a||n&&!u||(t=t||[],t=[e,t.slice?t.slice():t],r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,q,H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))x.extend(this.cache[i],t);else for(r in t)o[r]=t[r];return o},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){var r;return t===undefined||t&&"string"==typeof t&&n===undefined?(r=this.get(e,t),r!==undefined?r:this.get(e,x.camelCase(t))):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i,o=this.key(e),s=this.cache[o];if(t===undefined)this.cache[o]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):(i=x.camelCase(t),t in s?r=[t,i]:(r=i,r=r in s?[r]:r.match(w)||[])),n=r.length;while(n--)delete s[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){e[this.expando]&&delete this.cache[e[this.expando]]}},L=new F,q=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||q.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return q.access(e,t,n)},_removeData:function(e,t){q.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!q.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.slice(5)),P(i,r,s[r]));q.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:H.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=q.get(e,t),n&&(!r||x.isArray(n)?r=q.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return q.get(e,n)||q.access(e,n,{empty:x.Callbacks("once memory").add(function(){q.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t);x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=q.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n\f]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,i=0,o=x(this),s=e.match(w)||[];while(t=s[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===r||"boolean"===n)&&(this.className&&q.set(this,"__className__",this.className),this.className=this.className||e===!1?"":q.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,x(this).val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.bool.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,p,f,h,d,g,m,y=q.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(f=x.event.special[d]||{},d=(o?f.delegateType:f.bindType)||d,f=x.event.special[d]||{},p=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,f.setup&&f.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),f.add&&(f.add.call(e,p),p.handler.guid||(p.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,p):h.push(p),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,p,f,h,d,g,m=q.hasData(e)&&q.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){p=x.event.special[h]||{},h=(r?p.delegateType:p.bindType)||h,f=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=f.length;while(o--)c=f[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(f.splice(o,1),c.selector&&f.delegateCount--,p.remove&&p.remove.call(e,c));s&&!f.length&&(p.teardown&&p.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,q.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,p,f,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),f=x.event.special[d]||{},i||!f.trigger||f.trigger.apply(r,n)!==!1)){if(!i&&!f.noBubble&&!x.isWindow(r)){for(l=f.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:f.bindType||d,p=(q.get(a,"events")||{})[t.type]&&q.get(a,"handle"),p&&p.apply(a,n),p=c&&a[c],p&&x.acceptData(a)&&p.apply&&p.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||f._default&&f._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(q.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,s=e,a=this.fixHooks[i];a||(this.fixHooks[i]=a=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new x.Event(s),t=r.length;while(t--)n=r[t],e[n]=s[n];return e.target||(e.target=o),3===e.target.nodeType&&(e.target=e.target.parentNode),a.filter?a.filter(e,s):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=/^(?:parents|prev(?:Until|All))/,Q=x.expr.match.needsContext,K={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(et(this,e||[],!0))},filter:function(e){return this.pushStack(et(this,e||[],!1))},is:function(e){return!!et(this,"string"==typeof e&&Q.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],s=Q.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function Z(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return Z(e,"nextSibling")},prev:function(e){return Z(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return e.contentDocument||x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(K[e]||x.unique(i),J.test(e)&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function et(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var tt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,nt=/<([\w:]+)/,rt=/<|&#?\w+;/,it=/<(?:script|style|link)/i,ot=/^(?:checkbox|radio)$/i,st=/checked\s*(?:[^=]|=\s*.checked.)/i,at=/^$|\/(?:java|ecma)script/i,ut=/^true\/(.*)/,lt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,ct={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};ct.optgroup=ct.option,ct.tbody=ct.tfoot=ct.colgroup=ct.caption=ct.thead,ct.th=ct.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(mt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&dt(mt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(mt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!it.test(e)&&!ct[(nt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(tt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(mt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=f.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,p=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&st.test(d))return this.each(function(r){var i=p.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(mt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,mt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,ht),l=0;s>l;l++)a=o[l],at.test(a.type||"")&&!q.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(lt,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=mt(a),o=mt(e),r=0,i=o.length;i>r;r++)yt(o[r],s[r]);if(t)if(n)for(o=o||mt(e),s=s||mt(a),r=0,i=o.length;i>r;r++)gt(o[r],s[r]);else gt(e,a);return s=mt(a,"script"),s.length>0&&dt(s,!u&&mt(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,p=e.length,f=t.createDocumentFragment(),h=[];for(;p>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(rt.test(i)){o=o||f.appendChild(t.createElement("div")),s=(nt.exec(i)||["",""])[1].toLowerCase(),a=ct[s]||ct._default,o.innerHTML=a[1]+i.replace(tt,"<$1></$2>")+a[2],l=a[0];while(l--)o=o.lastChild;x.merge(h,o.childNodes),o=f.firstChild,o.textContent=""}else h.push(t.createTextNode(i));f.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=mt(f.appendChild(i),"script"),u&&dt(o),n)){l=0;while(i=o[l++])at.test(i.type||"")&&n.push(i)}return f},cleanData:function(e){var t,n,r,i,o,s,a=x.event.special,u=0;for(;(n=e[u])!==undefined;u++){if(F.accepts(n)&&(o=n[q.expando],o&&(t=q.cache[o]))){if(r=Object.keys(t.events||{}),r.length)for(s=0;(i=r[s])!==undefined;s++)a[i]?x.event.remove(n,i):x.removeEvent(n,i,t.handle);q.cache[o]&&delete q.cache[o]}delete L.cache[n[L.expando]]}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}});function pt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function ht(e){var t=ut.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function dt(e,t){var n=e.length,r=0;for(;n>r;r++)q.set(e[r],"globalEval",!t||q.get(t[r],"globalEval"))}function gt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(q.hasData(e)&&(o=q.access(e),s=q.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function mt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function yt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&ot.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var vt,xt,bt=/^(none|table(?!-c[ea]).+)/,wt=/^margin/,Tt=RegExp("^("+b+")(.*)$","i"),Ct=RegExp("^("+b+")(?!px)[a-z%]+$","i"),kt=RegExp("^([+-])=("+b+")","i"),Nt={BODY:"block"},Et={position:"absolute",visibility:"hidden",display:"block"},St={letterSpacing:0,fontWeight:400},jt=["Top","Right","Bottom","Left"],Dt=["Webkit","O","Moz","ms"];function At(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Dt.length;while(i--)if(t=Dt[i]+n,t in e)return t;return r}function Lt(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function qt(t){return e.getComputedStyle(t,null)}function Ht(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=q.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&Lt(r)&&(o[s]=q.access(r,"olddisplay",Rt(r.nodeName)))):o[s]||(i=Lt(r),(n&&"none"!==n||!i)&&q.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=qt(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return Ht(this,!0)},hide:function(){return Ht(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){Lt(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=vt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=At(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=kt.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=At(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=vt(e,t,r)),"normal"===i&&t in St&&(i=St[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),vt=function(e,t,n){var r,i,o,s=n||qt(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Ct.test(a)&&wt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ot(e,t,n){var r=Tt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ft(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+jt[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+jt[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+jt[o]+"Width",!0,i))):(s+=x.css(e,"padding"+jt[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+jt[o]+"Width",!0,i)));return s}function Pt(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=qt(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=vt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Ct.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ft(e,t,n||(s?"border":"content"),r,o)+"px"}function Rt(e){var t=o,n=Nt[e];return n||(n=Mt(e,t),"none"!==n&&n||(xt=(xt||x("<iframe frameborder='0' width='0' height='0'/>").css("cssText","display:block !important")).appendTo(t.documentElement),t=(xt[0].contentWindow||xt[0].contentDocument).document,t.write("<!doctype html><html><body>"),t.close(),n=Mt(e,t),xt.detach()),Nt[e]=n),n}function Mt(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,t){x.cssHooks[t]={get:function(e,n,r){return n?0===e.offsetWidth&&bt.test(x.css(e,"display"))?x.swap(e,Et,function(){return Pt(e,t,r)}):Pt(e,t,r):undefined},set:function(e,n,r){var i=r&&qt(e);return Ot(e,n,r?Ft(e,t,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,t){return t?x.swap(e,{display:"inline-block"},vt,[e,"marginRight"]):undefined}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,t){x.cssHooks[t]={get:function(e,n){return n?(n=vt(e,t),Ct.test(n)?x(e).position()[t]+"px":n):undefined}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+jt[r]+t]=o[r]||o[r-2]||o[0];return i}},wt.test(e)||(x.cssHooks[e+t].set=Ot)});var Wt=/%20/g,$t=/\[\]$/,Bt=/\r?\n/g,It=/^(?:submit|button|image|reset|file)$/i,zt=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&zt.test(this.nodeName)&&!It.test(e)&&(this.checked||!ot.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(Bt,"\r\n")}}):{name:t.name,value:n.replace(Bt,"\r\n")}}).get()}}),x.param=function(e,t){var n,r=[],i=function(e,t){t=x.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(t===undefined&&(t=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){i(this.name,this.value)});else for(n in e)_t(n,e[n],t,i);return r.join("&").replace(Wt,"+")};function _t(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||$t.test(e)?r(e,i):_t(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)_t(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Xt,Ut,Yt=x.now(),Vt=/\?/,Gt=/#.*$/,Jt=/([?&])_=[^&]*/,Qt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Kt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Zt=/^(?:GET|HEAD)$/,en=/^\/\//,tn=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,nn=x.fn.load,rn={},on={},sn="*/".concat("*");try{Ut=i.href}catch(an){Ut=o.createElement("a"),Ut.href="",Ut=Ut.href}Xt=tn.exec(Ut.toLowerCase())||[];function un(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(w)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function ln(e,t,n,r){var i={},o=e===on;function s(a){var u;return i[a]=!0,x.each(e[a]||[],function(e,a){var l=a(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):undefined:(t.dataTypes.unshift(l),s(l),!1)}),u}return s(t.dataTypes[0])||!i["*"]&&s("*")}function cn(e,t){var n,r,i=x.ajaxSettings.flatOptions||{};for(n in t)t[n]!==undefined&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,t,n){if("string"!=typeof e&&nn)return nn.apply(this,arguments);var r,i,o,s=this,a=e.indexOf(" ");return a>=0&&(r=e.slice(a),e=e.slice(0,a)),x.isFunction(t)?(n=t,t=undefined):t&&"object"==typeof t&&(i="POST"),s.length>0&&x.ajax({url:e,type:i,dataType:"html",data:t}).done(function(e){o=arguments,s.html(r?x("<div>").append(x.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){s.each(n,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ut,type:"GET",isLocal:Kt.test(Xt[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":sn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?cn(cn(e,x.ajaxSettings),t):cn(x.ajaxSettings,e)},ajaxPrefilter:un(rn),ajaxTransport:un(on),ajax:function(e,t){"object"==typeof e&&(t=e,e=undefined),t=t||{};var n,r,i,o,s,a,u,l,c=x.ajaxSetup({},t),p=c.context||c,f=c.context&&(p.nodeType||p.jquery)?x(p):x.event,h=x.Deferred(),d=x.Callbacks("once memory"),g=c.statusCode||{},m={},y={},v=0,b="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(2===v){if(!o){o={};while(t=Qt.exec(i))o[t[1].toLowerCase()]=t[2]}t=o[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===v?i:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return v||(e=y[n]=y[n]||e,m[e]=t),this},overrideMimeType:function(e){return v||(c.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>v)for(t in e)g[t]=[g[t],e[t]];else T.always(e[T.status]);return this},abort:function(e){var t=e||b;return n&&n.abort(t),k(0,t),this}};if(h.promise(T).complete=d.add,T.success=T.done,T.error=T.fail,c.url=((e||c.url||Ut)+"").replace(Gt,"").replace(en,Xt[1]+"//"),c.type=t.method||t.type||c.method||c.type,c.dataTypes=x.trim(c.dataType||"*").toLowerCase().match(w)||[""],null==c.crossDomain&&(a=tn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===Xt[1]&&a[2]===Xt[2]&&(a[3]||("http:"===a[1]?"80":"443"))===(Xt[3]||("http:"===Xt[1]?"80":"443")))),c.data&&c.processData&&"string"!=typeof c.data&&(c.data=x.param(c.data,c.traditional)),ln(rn,c,t,T),2===v)return T;u=c.global,u&&0===x.active++&&x.event.trigger("ajaxStart"),c.type=c.type.toUpperCase(),c.hasContent=!Zt.test(c.type),r=c.url,c.hasContent||(c.data&&(r=c.url+=(Vt.test(r)?"&":"?")+c.data,delete c.data),c.cache===!1&&(c.url=Jt.test(r)?r.replace(Jt,"$1_="+Yt++):r+(Vt.test(r)?"&":"?")+"_="+Yt++)),c.ifModified&&(x.lastModified[r]&&T.setRequestHeader("If-Modified-Since",x.lastModified[r]),x.etag[r]&&T.setRequestHeader("If-None-Match",x.etag[r])),(c.data&&c.hasContent&&c.contentType!==!1||t.contentType)&&T.setRequestHeader("Content-Type",c.contentType),T.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+("*"!==c.dataTypes[0]?", "+sn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)T.setRequestHeader(l,c.headers[l]);if(c.beforeSend&&(c.beforeSend.call(p,T,c)===!1||2===v))return T.abort();b="abort";for(l in{success:1,error:1,complete:1})T[l](c[l]);if(n=ln(on,c,t,T)){T.readyState=1,u&&f.trigger("ajaxSend",[T,c]),c.async&&c.timeout>0&&(s=setTimeout(function(){T.abort("timeout")},c.timeout));try{v=1,n.send(m,k)}catch(C){if(!(2>v))throw C;k(-1,C)}}else k(-1,"No Transport");function k(e,t,o,a){var l,m,y,b,w,C=t;2!==v&&(v=2,s&&clearTimeout(s),n=undefined,i=a||"",T.readyState=e>0?4:0,l=e>=200&&300>e||304===e,o&&(b=pn(c,T,o)),b=fn(c,b,T,l),l?(c.ifModified&&(w=T.getResponseHeader("Last-Modified"),w&&(x.lastModified[r]=w),w=T.getResponseHeader("etag"),w&&(x.etag[r]=w)),204===e||"HEAD"===c.type?C="nocontent":304===e?C="notmodified":(C=b.state,m=b.data,y=b.error,l=!y)):(y=C,(e||!C)&&(C="error",0>e&&(e=0))),T.status=e,T.statusText=(t||C)+"",l?h.resolveWith(p,[m,C,T]):h.rejectWith(p,[T,C,y]),T.statusCode(g),g=undefined,u&&f.trigger(l?"ajaxSuccess":"ajaxError",[T,c,l?m:y]),d.fireWith(p,[T,C]),u&&(f.trigger("ajaxComplete",[T,c]),--x.active||x.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,t){return x.get(e,undefined,t,"script")}}),x.each(["get","post"],function(e,t){x[t]=function(e,n,r,i){return x.isFunction(n)&&(i=i||r,r=n,n=undefined),x.ajax({url:e,type:t,dataType:i,data:n,success:r})}});function pn(e,t,n){var r,i,o,s,a=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),r===undefined&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in a)if(a[i]&&a[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}s||(s=i)}o=o||s}return o?(o!==u[0]&&u.unshift(o),n[o]):undefined}function fn(e,t,n,r){var i,o,s,a,u,l={},c=e.dataTypes.slice();if(c[1])for(s in e.converters)l[s.toLowerCase()]=e.converters[s];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(s=l[u+" "+o]||l["* "+o],!s)for(i in l)if(a=i.split(" "),a[1]===o&&(s=l[u+" "+a[0]]||l["* "+a[0]])){s===!0?s=l[i]:l[i]!==!0&&(o=a[0],c.unshift(a[1]));break}if(s!==!0)if(s&&e["throws"])t=s(t);else try{t=s(t)}catch(p){return{state:"parsererror",error:s?p:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===undefined&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),x.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(r,i){t=x("<script>").prop({async:!0,charset:e.scriptCharset,src:e.url}).on("load error",n=function(e){t.remove(),n=null,e&&i("error"===e.type?404:200,e.type)}),o.head.appendChild(t[0])},abort:function(){n&&n()}}}});var hn=[],dn=/(=)\?(?=&|$)|\?\?/;x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=hn.pop()||x.expando+"_"+Yt++;return this[e]=!0,e}}),x.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,s,a=t.jsonp!==!1&&(dn.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&dn.test(t.data)&&"data");return a||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=x.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,a?t[a]=t[a].replace(dn,"$1"+i):t.jsonp!==!1&&(t.url+=(Vt.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return s||x.error(i+" was not called"),s[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){s=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,hn.push(i)),s&&x.isFunction(o)&&o(s[0]),s=o=undefined}),"script"):undefined}),x.ajaxSettings.xhr=function(){try{return new XMLHttpRequest}catch(e){}};var gn=x.ajaxSettings.xhr(),mn={0:200,1223:204},yn=0,vn={};e.ActiveXObject&&x(e).on("unload",function(){for(var e in vn)vn[e]();vn=undefined}),x.support.cors=!!gn&&"withCredentials"in gn,x.support.ajax=gn=!!gn,x.ajaxTransport(function(e){var t;return x.support.cors||gn&&!e.crossDomain?{send:function(n,r){var i,o,s=e.xhr();if(s.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)s[i]=e.xhrFields[i];e.mimeType&&s.overrideMimeType&&s.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)s.setRequestHeader(i,n[i]);t=function(e){return function(){t&&(delete vn[o],t=s.onload=s.onerror=null,"abort"===e?s.abort():"error"===e?r(s.status||404,s.statusText):r(mn[s.status]||s.status,s.statusText,"string"==typeof s.responseText?{text:s.responseText}:undefined,s.getAllResponseHeaders()))}},s.onload=t(),s.onerror=t("error"),t=vn[o=yn++]=t("abort"),s.send(e.hasContent&&e.data||null)},abort:function(){t&&t()}}:undefined});var xn,bn,wn=/^(?:toggle|show|hide)$/,Tn=RegExp("^(?:([+-])=|)("+b+")([a-z%]*)$","i"),Cn=/queueHooks$/,kn=[An],Nn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=Tn.exec(t),o=i&&i[3]||(x.cssNumber[e]?"":"px"),s=(x.cssNumber[e]||"px"!==o&&+r)&&Tn.exec(x.css(n.elem,e)),a=1,u=20;if(s&&s[3]!==o){o=o||s[3],i=i||[],s=+r||1;do a=a||".5",s/=a,x.style(n.elem,e,s+o);while(a!==(a=n.cur()/r)&&1!==a&&--u)}return i&&(s=n.start=+s||+r||0,n.unit=o,n.end=i[1]?s+(i[1]+1)*i[2]:+i[2]),n}]};function En(){return setTimeout(function(){xn=undefined}),xn=x.now()}function Sn(e,t,n){var r,i=(Nn[t]||[]).concat(Nn["*"]),o=0,s=i.length;for(;s>o;o++)if(r=i[o].call(n,t,e))return r}function jn(e,t,n){var r,i,o=0,s=kn.length,a=x.Deferred().always(function(){delete u.elem}),u=function(){if(i)return!1;var t=xn||En(),n=Math.max(0,l.startTime+l.duration-t),r=n/l.duration||0,o=1-r,s=0,u=l.tweens.length;for(;u>s;s++)l.tweens[s].run(o);return a.notifyWith(e,[l,o,n]),1>o&&u?n:(a.resolveWith(e,[l]),!1)},l=a.promise({elem:e,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:xn||En(),duration:n.duration,tweens:[],createTween:function(t,n){var r=x.Tween(e,l.opts,t,n,l.opts.specialEasing[t]||l.opts.easing);return l.tweens.push(r),r},stop:function(t){var n=0,r=t?l.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)l.tweens[n].run(1);return t?a.resolveWith(e,[l,t]):a.rejectWith(e,[l,t]),this}}),c=l.props;for(Dn(c,l.opts.specialEasing);s>o;o++)if(r=kn[o].call(l,e,c,l.opts))return r;return x.map(c,Sn,l),x.isFunction(l.opts.start)&&l.opts.start.call(e,l),x.fx.timer(x.extend(u,{elem:e,anim:l,queue:l.opts.queue})),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always)}function Dn(e,t){var n,r,i,o,s;for(n in e)if(r=x.camelCase(n),i=t[r],o=e[n],x.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),s=x.cssHooks[r],s&&"expand"in s){o=s.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}x.Animation=x.extend(jn,{tweener:function(e,t){x.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;i>r;r++)n=e[r],Nn[n]=Nn[n]||[],Nn[n].unshift(t)},prefilter:function(e,t){t?kn.unshift(e):kn.push(e)}});function An(e,t,n){var r,i,o,s,a,u,l=this,c={},p=e.style,f=e.nodeType&&Lt(e),h=q.get(e,"fxshow");n.queue||(a=x._queueHooks(e,"fx"),null==a.unqueued&&(a.unqueued=0,u=a.empty.fire,a.empty.fire=function(){a.unqueued||u()}),a.unqueued++,l.always(function(){l.always(function(){a.unqueued--,x.queue(e,"fx").length||a.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],"inline"===x.css(e,"display")&&"none"===x.css(e,"float")&&(p.display="inline-block")),n.overflow&&(p.overflow="hidden",l.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],wn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(f?"hide":"show")){if("show"!==i||!h||h[r]===undefined)continue;f=!0}c[r]=h&&h[r]||x.style(e,r)}if(!x.isEmptyObject(c)){h?"hidden"in h&&(f=h.hidden):h=q.access(e,"fxshow",{}),o&&(h.hidden=!f),f?x(e).show():l.done(function(){x(e).hide()}),l.done(function(){var t;q.remove(e,"fxshow");for(t in c)x.style(e,t,c[t])});for(r in c)s=Sn(f?h[r]:0,r,l),r in h||(h[r]=s.start,f&&(s.end=s.start,s.start="width"===r||"height"===r?1:0))}}function Ln(e,t,n,r,i){return new Ln.prototype.init(e,t,n,r,i)}x.Tween=Ln,Ln.prototype={constructor:Ln,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(x.cssNumber[n]?"":"px")},cur:function(){var e=Ln.propHooks[this.prop];return e&&e.get?e.get(this):Ln.propHooks._default.get(this)},run:function(e){var t,n=Ln.propHooks[this.prop];return this.pos=t=this.options.duration?x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Ln.propHooks._default.set(this),this}},Ln.prototype.init.prototype=Ln.prototype,Ln.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=x.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[x.cssProps[e.prop]]||x.cssHooks[e.prop])?x.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},Ln.propHooks.scrollTop=Ln.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.each(["toggle","show","hide"],function(e,t){var n=x.fn[t];x.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(qn(t,!0),e,r,i)}}),x.fn.extend({fadeTo:function(e,t,n,r){return this.filter(Lt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=x.isEmptyObject(e),o=x.speed(t,n,r),s=function(){var t=jn(this,x.extend({},e),o);(i||q.get(this,"finish"))&&t.stop(!0)};return s.finish=s,i||o.queue===!1?this.each(s):this.queue(o.queue,s)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=undefined),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=x.timers,s=q.get(this);if(i)s[i]&&s[i].stop&&r(s[i]);else for(i in s)s[i]&&s[i].stop&&Cn.test(i)&&r(s[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&x.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=q.get(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=x.timers,s=r?r.length:0;for(n.finish=!0,x.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;s>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}});function qn(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=jt[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}x.each({slideDown:qn("show"),slideUp:qn("hide"),slideToggle:qn("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){x.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),x.speed=function(e,t,n){var r=e&&"object"==typeof e?x.extend({},e):{complete:n||!n&&t||x.isFunction(e)&&e,duration:e,easing:n&&t||t&&!x.isFunction(t)&&t};return r.duration=x.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in x.fx.speeds?x.fx.speeds[r.duration]:x.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){x.isFunction(r.old)&&r.old.call(this),r.queue&&x.dequeue(this,r.queue)},r},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},x.timers=[],x.fx=Ln.prototype.init,x.fx.tick=function(){var e,t=x.timers,n=0;for(xn=x.now();t.length>n;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||x.fx.stop(),xn=undefined},x.fx.timer=function(e){e()&&x.timers.push(e)&&x.fx.start()},x.fx.interval=13,x.fx.start=function(){bn||(bn=setInterval(x.fx.tick,x.fx.interval))},x.fx.stop=function(){clearInterval(bn),bn=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fx.step={},x.expr&&x.expr.filters&&(x.expr.filters.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length}),x.fn.offset=function(e){if(arguments.length)return e===undefined?this:this.each(function(t){x.offset.setOffset(this,e,t)});var t,n,i=this[0],o={top:0,left:0},s=i&&i.ownerDocument;if(s)return t=s.documentElement,x.contains(t,i)?(typeof i.getBoundingClientRect!==r&&(o=i.getBoundingClientRect()),n=Hn(s),{top:o.top+n.pageYOffset-t.clientTop,left:o.left+n.pageXOffset-t.clientLeft}):o},x.offset={setOffset:function(e,t,n){var r,i,o,s,a,u,l,c=x.css(e,"position"),p=x(e),f={};"static"===c&&(e.style.position="relative"),a=p.offset(),o=x.css(e,"top"),u=x.css(e,"left"),l=("absolute"===c||"fixed"===c)&&(o+u).indexOf("auto")>-1,l?(r=p.position(),s=r.top,i=r.left):(s=parseFloat(o)||0,i=parseFloat(u)||0),x.isFunction(t)&&(t=t.call(e,n,a)),null!=t.top&&(f.top=t.top-a.top+s),null!=t.left&&(f.left=t.left-a.left+i),"using"in t?t.using.call(e,f):p.css(f)}},x.fn.extend({position:function(){if(this[0]){var e,t,n=this[0],r={top:0,left:0};return"fixed"===x.css(n,"position")?t=n.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),x.nodeName(e[0],"html")||(r=e.offset()),r.top+=x.css(e[0],"borderTopWidth",!0),r.left+=x.css(e[0],"borderLeftWidth",!0)),{top:t.top-r.top-x.css(n,"marginTop",!0),left:t.left-r.left-x.css(n,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||s;while(e&&!x.nodeName(e,"html")&&"static"===x.css(e,"position"))e=e.offsetParent;return e||s})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,n){var r="pageYOffset"===n;x.fn[t]=function(i){return x.access(this,function(t,i,o){var s=Hn(t);return o===undefined?s?s[n]:t[i]:(s?s.scrollTo(r?e.pageXOffset:o,r?o:e.pageYOffset):t[i]=o,undefined)},t,i,arguments.length,null)}});function Hn(e){return x.isWindow(e)?e:9===e.nodeType&&e.defaultView}x.each({Height:"height",Width:"width"},function(e,t){x.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){x.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),s=n||(r===!0||i===!0?"margin":"border");return x.access(this,function(t,n,r){var i;return x.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):r===undefined?x.css(t,n,s):x.style(t,n,r,s)},t,o?r:undefined,o,null)}})}),x.fn.size=function(){return this.length},x.fn.andSelf=x.fn.addBack,"object"==typeof module&&module&&"object"==typeof module.exports?module.exports=x:"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}),"object"==typeof e&&"object"==typeof e.document&&(e.jQuery=e.$=x)})(window); \ No newline at end of file
diff --git a/themes/baggy/js/jquery.cookie.js b/themes/baggy/js/jquery.cookie.js
new file mode 100755
index 00000000..92719000
--- /dev/null
+++ b/themes/baggy/js/jquery.cookie.js
@@ -0,0 +1,117 @@
1/*!
2 * jQuery Cookie Plugin v1.4.0
3 * https://github.com/carhartl/jquery-cookie
4 *
5 * Copyright 2013 Klaus Hartl
6 * Released under the MIT license
7 */
8(function (factory) {
9 if (typeof define === 'function' && define.amd) {
10 // AMD. Register as anonymous module.
11 define(['jquery'], factory);
12 } else {
13 // Browser globals.
14 factory(jQuery);
15 }
16}(function ($) {
17
18 var pluses = /\+/g;
19
20 function encode(s) {
21 return config.raw ? s : encodeURIComponent(s);
22 }
23
24 function decode(s) {
25 return config.raw ? s : decodeURIComponent(s);
26 }
27
28 function stringifyCookieValue(value) {
29 return encode(config.json ? JSON.stringify(value) : String(value));
30 }
31
32 function parseCookieValue(s) {
33 if (s.indexOf('"') === 0) {
34 // This is a quoted cookie as according to RFC2068, unescape...
35 s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
36 }
37
38 try {
39 // Replace server-side written pluses with spaces.
40 // If we can't decode the cookie, ignore it, it's unusable.
41 s = decodeURIComponent(s.replace(pluses, ' '));
42 } catch(e) {
43 return;
44 }
45
46 try {
47 // If we can't parse the cookie, ignore it, it's unusable.
48 return config.json ? JSON.parse(s) : s;
49 } catch(e) {}
50 }
51
52 function read(s, converter) {
53 var value = config.raw ? s : parseCookieValue(s);
54 return $.isFunction(converter) ? converter(value) : value;
55 }
56
57 var config = $.cookie = function (key, value, options) {
58
59 // Write
60 if (value !== undefined && !$.isFunction(value)) {
61 options = $.extend({}, config.defaults, options);
62
63 if (typeof options.expires === 'number') {
64 var days = options.expires, t = options.expires = new Date();
65 t.setDate(t.getDate() + days);
66 }
67
68 return (document.cookie = [
69 encode(key), '=', stringifyCookieValue(value),
70 options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
71 options.path ? '; path=' + options.path : '',
72 options.domain ? '; domain=' + options.domain : '',
73 options.secure ? '; secure' : ''
74 ].join(''));
75 }
76
77 // Read
78
79 var result = key ? undefined : {};
80
81 // To prevent the for loop in the first place assign an empty array
82 // in case there are no cookies at all. Also prevents odd result when
83 // calling $.cookie().
84 var cookies = document.cookie ? document.cookie.split('; ') : [];
85
86 for (var i = 0, l = cookies.length; i < l; i++) {
87 var parts = cookies[i].split('=');
88 var name = decode(parts.shift());
89 var cookie = parts.join('=');
90
91 if (key && key === name) {
92 // If second argument (value) is a function it's a converter...
93 result = read(cookie, value);
94 break;
95 }
96
97 // Prevent storing a cookie that we couldn't decode.
98 if (!key && (cookie = read(cookie)) !== undefined) {
99 result[name] = cookie;
100 }
101 }
102
103 return result;
104 };
105
106 config.defaults = {};
107
108 $.removeCookie = function (key, options) {
109 if ($.cookie(key) !== undefined) {
110 // Must not alter options, thus extending a fresh object...
111 $.cookie(key, '', $.extend({}, options, { expires: -1 }));
112 return true;
113 }
114 return false;
115 };
116
117}));
diff --git a/themes/baggy/layout.twig b/themes/baggy/layout.twig
index dfebc3ea..8de12749 100644..100755
--- a/themes/baggy/layout.twig
+++ b/themes/baggy/layout.twig
@@ -21,6 +21,9 @@
21 {% block precontent %}{% endblock %} 21 {% block precontent %}{% endblock %}
22 {% block messages %} 22 {% block messages %}
23 {% include '_messages.twig' %} 23 {% include '_messages.twig' %}
24 {% if includeImport %}
25 {% include '_import.twig' %}
26 {% endif %}
24 {% endblock %} 27 {% endblock %}
25 <div id="content" class="w600p center"> 28 <div id="content" class="w600p center">
26 {% block content %}{% endblock %} 29 {% block content %}{% endblock %}
diff --git a/themes/baggy/tags.twig b/themes/baggy/tags.twig
index 9df44bb7..9bb93a45 100644..100755
--- a/themes/baggy/tags.twig
+++ b/themes/baggy/tags.twig
@@ -6,7 +6,7 @@
6{% block content %} 6{% block content %}
7<h2>{% trans "Tags" %}</h2> 7<h2>{% trans "Tags" %}</h2>
8<ul class="list-tags"> 8<ul class="list-tags">
9{% for tag in tags %}<li>{% if token != '' %}<a class="icon icon-rss" href="?feed&amp;type=tag&amp;user_id={{ user_id }}&amp;tag_id={{ tag.id }}&amp;token={{ token }}" target="_blank"><span>rss</span></a>{% endif %} <a href="./?view=tag&amp;id={{ tag.id }}">{{ tag.value }}</a> 9{% for tag in tags %}<li>{% if token != '' %}<a class="icon icon-rss" href="?feed&amp;type=tag&amp;user_id={{ user_id }}&amp;tag_id={{ tag.id }}&amp;token={{ token }}" target="_blank"><span>rss</span></a>{% endif %} <a href="./?view=tag&amp;id={{ tag.id }}">{{ tag.value }}</a> ({{ tag.entriescount }})
10</li> 10</li>
11{% endfor %} 11{% endfor %}
12</ul> 12</ul>
diff --git a/themes/baggy/view.twig b/themes/baggy/view.twig
index 4751c4cb..af97407d 100644..100755
--- a/themes/baggy/view.twig
+++ b/themes/baggy/view.twig
@@ -8,13 +8,15 @@
8 <ul class="links"> 8 <ul class="links">
9 <li class="topPosF"><a href="#top" title="{% trans "Back to top" %}" class="tool top icon icon-arrow-up-thick"><span>{% trans "Back to top" %}</span></a></li> 9 <li class="topPosF"><a href="#top" title="{% trans "Back to top" %}" class="tool top icon icon-arrow-up-thick"><span>{% trans "Back to top" %}</span></a></li>
10 <li><a href="{{ entry.url|e }}" target="_blank" title="{% trans "original" %} : {{ entry.title|e }}" class="tool link icon icon-link"><span>{{ entry.url | e | getDomain }}</span></a></li> 10 <li><a href="{{ entry.url|e }}" target="_blank" title="{% trans "original" %} : {{ entry.title|e }}" class="tool link icon icon-link"><span>{{ entry.url | e | getDomain }}</span></a></li>
11 <li><a title="{% trans "Mark as read" %}" class="tool icon icon-check {% if entry.is_read == 0 %}archive-off{% else %}archive{% endif %}" href="./?action=toggle_archive&amp;id={{ entry.id|e }}"><span>{% trans "Toggle mark as read" %}</span></a></li> 11 <li><a title="{% trans "Mark as read" %}" class="tool icon icon-check {% if entry.is_read == 0 %}archive-off{% else %}archive{% endif %}" href="javascript: void(null);" id="markAsRead"><span>{% trans "Toggle mark as read" %}</span></a></li>
12 <li><a title="{% trans "Favorite" %}" class="tool icon icon-star {% if entry.is_fav == 0 %}fav-off{% else %}fav{% endif %}" href="./?action=toggle_fav&amp;id={{ entry.id|e }}"><span>{% trans "Toggle favorite" %}</span></a></li> 12 <li><a title="{% trans "Favorite" %}" class="tool icon icon-star {% if entry.is_fav == 0 %}fav-off{% else %}fav{% endif %}" href="javascript: void(null);" id="setFav"><span>{% trans "Toggle favorite" %}</span></a></li>
13 <li><a title="{% trans "Delete" %}" class="tool delete icon icon-trash" href="./?action=delete&amp;id={{ entry.id|e }}"><span>{% trans "Delete" %}</span></a></li> 13 <li><a title="{% trans "Delete" %}" class="tool delete icon icon-trash" href="./?action=delete&amp;id={{ entry.id|e }}"><span>{% trans "Delete" %}</span></a></li>
14 {% if constant('SHARE_TWITTER') == 1 %}<li><a href="https://twitter.com/home?status={{entry.title|url_encode}}%20{{ entry.url|url_encode }}%20via%20@wallabagapp" target="_blank" class="tool twitter icon icon-twitter" title="{% trans "Tweet" %}"><span>{% trans "Tweet" %}</span></a></li>{% endif %} 14 {% if constant('SHARE_TWITTER') == 1 %}<li><a href="https://twitter.com/home?status={{entry.title|url_encode}}%20{{ entry.url|url_encode }}%20via%20@wallabagapp" target="_blank" class="tool twitter icon icon-twitter" title="{% trans "Tweet" %}"><span>{% trans "Tweet" %}</span></a></li>{% endif %}
15 {% if constant('SHARE_MAIL') == 1 %}<li><a href="mailto:?subject={{ entry.title|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" class="tool email icon icon-mail" title="{% trans "Email" %}"><span>{% trans "Email" %}</span></a></li>{% endif %} 15 {% if constant('SHARE_MAIL') == 1 %}<li><a href="mailto:?subject={{ entry.title|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" class="tool email icon icon-mail" title="{% trans "Email" %}"><span>{% trans "Email" %}</span></a></li>{% endif %}
16 {% if constant('SHARE_SHAARLI') == 1 %}<li><a href="{{ constant('SHAARLI_URL') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}" target="_blank" class="tool shaarli" title="{% trans "shaarli" %}"><span>{% trans "shaarli" %}</span></a></li>{% endif %} 16 {% if constant('SHARE_SHAARLI') == 1 %}<li><a href="{{ constant('SHAARLI_URL') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}" target="_blank" class="tool shaarli" title="{% trans "shaarli" %}"><span>{% trans "shaarli" %}</span></a></li>{% endif %}
17 {% if constant('FLATTR') == 1 %}{% if flattr.status == constant('FLATTRABLE') %}<li><a href="http://flattr.com/submit/auto?url={{ entry.url }}" class="tool flattr icon icon-flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span></a></li>{% elseif flattr.status == constant('FLATTRED') %}<li><a href="{{ flattr.flattrItemURL }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span> ({{ flattr.numflattrs }})</a></li>{% endif %}{% endif %} 17 {% if constant('FLATTR') == 1 %}{% if flattr.status == constant('FLATTRABLE') %}<li><a href="http://flattr.com/submit/auto?url={{ entry.url }}" class="tool flattr icon icon-flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span></a></li>{% elseif flattr.status == constant('FLATTRED') %}<li><a href="{{ flattr.flattrItemURL }}" class="tool flattr icon icon-flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span> ({{ flattr.numflattrs }})</a></li>{% endif %}{% endif %}
18 {% if constant('SHOW_PRINTLINK') == 1 %}<li><a title="{% trans "Print" %}" class="tool icon icon-print" href="javascript: window.print();"><span>{% trans "Print" %}</span></a></li>{% endif %}
19 <li><a href="./?epub&amp;method=id&amp;id={{ entry.id|e }}" title="Generate epub file">EPUB</a></li>
18 <li><a href="mailto:hello@wallabag.org?subject=Wrong%20display%20in%20wallabag&amp;body={{ entry.url|url_encode }}" title="{% trans "Does this article appear wrong?" %}" class="tool bad-display icon icon-delete"><span>{% trans "Does this article appear wrong?" %}</span></a></li> 20 <li><a href="mailto:hello@wallabag.org?subject=Wrong%20display%20in%20wallabag&amp;body={{ entry.url|url_encode }}" title="{% trans "Does this article appear wrong?" %}" class="tool bad-display icon icon-delete"><span>{% trans "Does this article appear wrong?" %}</span></a></li>
19 </ul> 21 </ul>
20 </div> 22 </div>
@@ -29,4 +31,67 @@
29 {{ content | raw }} 31 {{ content | raw }}
30 </article> 32 </article>
31 </div> 33 </div>
34 <script src="{{ poche_url }}themes/{{theme}}/js/restoreScroll.js"></script>
35 <script type="text/javascript">
36 $(document).ready(function() {
37
38 // toggle read property of current article
39 $('#markAsRead').click(function(){
40 $("body").css("cursor", "wait");
41 $.ajax( { url: './?action=toggle_archive&id={{ entry.id|e }}' }).done(
42 function( data ) {
43 if ( data == '1' ) {
44 if ( $('#markAsRead').hasClass("archive-off") ) {
45 $('#markAsRead').removeClass("archive-off");
46 $('#markAsRead').addClass("archive");
47 }
48 else {
49 $('#markAsRead').removeClass("archive");
50 $('#markAsRead').addClass("archive-off");
51 }
52 }
53 else {
54 alert('Error! Pls check if you are logged in.');
55 }
56 });
57 $("body").css("cursor", "auto");
58 });
59
60 // toggle favorite property of current article
61 $('#setFav').click(function(){
62 $("body").css("cursor", "wait");
63 $.ajax( { url: './?action=toggle_fav&id={{ entry.id|e }}' }).done(
64 function( data ) {
65 if ( data == '1' ) {
66 if ( $('#setFav').hasClass("fav-off") ) {
67 $('#setFav').removeClass("fav-off");
68 $('#setFav').addClass("fav");
69 }
70 else {
71 $('#setFav').removeClass("fav");
72 $('#setFav').addClass("fav-off");
73 }
74 }
75 else {
76 alert('Error! Pls check if you are logged in.');
77 }
78 });
79 $("body").css("cursor", "auto");
80 });
81
82 $(window).scroll(function(e){
83 var scrollTop = $(window).scrollTop();
84 var docHeight = $(document).height();
85 var scrollPercent = (scrollTop) / (docHeight);
86 var scrollPercentRounded = Math.round(scrollPercent*100)/100;
87 savePercent({{ entry.id|e }}, scrollPercentRounded);
88 });
89
90 retrievePercent({{ entry.id|e }});
91
92 $(window).resize(function(){
93 retrievePercent({{ entry.id|e }});
94 });
95 });
96 </script>
32{% endblock %} 97{% endblock %}
diff --git a/themes/courgette/_head.twig b/themes/courgette/_head.twig
index 57b40f41..059936d9 100755
--- a/themes/courgette/_head.twig
+++ b/themes/courgette/_head.twig
@@ -1,11 +1,11 @@
1 <link rel="shortcut icon" type="image/x-icon" href="{{ poche_url }}/themes/{{theme}}/img/favicon.ico" /> 1 <link rel="shortcut icon" type="image/x-icon" href="{{ poche_url }}themes/{{theme}}/img/favicon.ico" />
2 <link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ poche_url }}/themes/{{theme}}/img/apple-touch-icon-144x144-precomposed.png"> 2 <link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ poche_url }}themes/{{theme}}/img/apple-touch-icon-144x144-precomposed.png">
3 <link rel="apple-touch-icon-precomposed" sizes="72x72" href="{{ poche_url }}/themes/{{theme}}/img/apple-touch-icon-72x72-precomposed.png"> 3 <link rel="apple-touch-icon-precomposed" sizes="72x72" href="{{ poche_url }}themes/{{theme}}/img/apple-touch-icon-72x72-precomposed.png">
4 <link rel="apple-touch-icon-precomposed" href="{{ poche_url }}/themes/{{theme}}/img/apple-touch-icon-precomposed.png"> 4 <link rel="apple-touch-icon-precomposed" href="{{ poche_url }}themes/{{theme}}/img/apple-touch-icon-precomposed.png">
5 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/font.css" media="all"> 5 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/font.css" media="all">
6 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/style.css" media="all"> 6 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/style.css" media="all">
7 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/messages.css" media="all"> 7 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/messages.css" media="all">
8 <link rel="stylesheet" href="{{ poche_url }}/themes/{{theme}}/css/print.css" media="print"> 8 <link rel="stylesheet" href="{{ poche_url }}themes/{{theme}}/css/print.css" media="print">
9 <link href='//fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> 9 <link href='//fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
10 <script src="//codeorigin.jquery.com/jquery-2.0.3.min.js"></script> 10 <script src="//codeorigin.jquery.com/jquery-2.0.3.min.js"></script>
11 <script src="{{ poche_url }}/themes/{{theme}}/js/init.js"></script> 11 <script src="{{ poche_url }}themes/{{theme}}/js/init.js"></script>
diff --git a/themes/courgette/_top.twig b/themes/courgette/_top.twig
index 792687c0..2d41db17 100755
--- a/themes/courgette/_top.twig
+++ b/themes/courgette/_top.twig
@@ -1,6 +1,6 @@
1 <header> 1 <header>
2 <h1> 2 <h1>
3 {% if view == 'home' %}{% block logo %}<img src="{{ poche_url }}/themes/{{ constant('DEFAULT_THEME') }}/img/logo.svg" alt="logo poche" />{% endblock %} 3 {% if view == 'home' %}{% block logo %}<img src="{{ poche_url }}themes/{{theme}}/img/logo.svg" alt="wallabag logo" />{% endblock %}
4 {% elseif view == 'fav' %}<a href="./" title="{% trans "back to home" %}" >{{ block('logo') }} <span>Favoris</span></a> 4 {% elseif view == 'fav' %}<a href="./" title="{% trans "back to home" %}" >{{ block('logo') }} <span>Favoris</span></a>
5 {% elseif view == 'archive' %}<a href="./" title="{% trans "back to home" %}" >{{ block('logo') }} <span>Archive</span></a> 5 {% elseif view == 'archive' %}<a href="./" title="{% trans "back to home" %}" >{{ block('logo') }} <span>Archive</span></a>
6 {% else %}<a href="./" title="{% trans "back to home" %}" >{{ block('logo') }}</a> 6 {% else %}<a href="./" title="{% trans "back to home" %}" >{{ block('logo') }}</a>
diff --git a/themes/courgette/_view.twig b/themes/courgette/_view.twig
index e80829ef..25479a3d 100755
--- a/themes/courgette/_view.twig
+++ b/themes/courgette/_view.twig
@@ -12,6 +12,7 @@
12 {% if constant('SHARE_MAIL') == 1 %}<li><a href="mailto:?subject={{ entry.title|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" class="tool email" title="{% trans "email" %}"><span>{% trans "email" %}</span></a></li>{% endif %} 12 {% if constant('SHARE_MAIL') == 1 %}<li><a href="mailto:?subject={{ entry.title|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" class="tool email" title="{% trans "email" %}"><span>{% trans "email" %}</span></a></li>{% endif %}
13 {% if constant('SHARE_SHAARLI') == 1 %}<li><a href="{{ constant('SHAARLI_URL') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}" target="_blank" class="tool shaarli" title="{% trans "shaarli" %}"><span>{% trans "shaarli" %}</span></a></li>{% endif %} 13 {% if constant('SHARE_SHAARLI') == 1 %}<li><a href="{{ constant('SHAARLI_URL') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}" target="_blank" class="tool shaarli" title="{% trans "shaarli" %}"><span>{% trans "shaarli" %}</span></a></li>{% endif %}
14 {% if constant('FLATTR') == 1 %}{% if flattr.status == constant('FLATTRABLE') %}<li class="flattrli"><a href="http://flattr.com/submit/auto?url={{ entry.url }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span></a></li>{% elseif flattr.status == constant('FLATTRED') %}<li><a href="{{ flattr.flattrItemURL }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span>{{ flattr.numflattrs }}</a></li>{% endif %}{% endif %} 14 {% if constant('FLATTR') == 1 %}{% if flattr.status == constant('FLATTRABLE') %}<li class="flattrli"><a href="http://flattr.com/submit/auto?url={{ entry.url }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span></a></li>{% elseif flattr.status == constant('FLATTRED') %}<li><a href="{{ flattr.flattrItemURL }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span>{{ flattr.numflattrs }}</a></li>{% endif %}{% endif %}
15 <li><a href="./?epub&amp;method=id&amp;id={{ entry.id|e }}" title="Generate epub file">EPUB</a></li>
15 <li><a href="mailto:hello@wallabag.org?subject=Wrong%20display%20in%20wallabag&amp;body={{ entry.url|url_encode }}" title="{% trans "this article appears wrong?" %}" class="tool bad-display"><span>{% trans "this article appears wrong?" %}</span></a></li> 16 <li><a href="mailto:hello@wallabag.org?subject=Wrong%20display%20in%20wallabag&amp;body={{ entry.url|url_encode }}" title="{% trans "this article appears wrong?" %}" class="tool bad-display"><span>{% trans "this article appears wrong?" %}</span></a></li>
16 </ul> 17 </ul>
17 </div> 18 </div>
@@ -23,7 +24,7 @@
23 {{ content | raw }} 24 {{ content | raw }}
24 </article> 25 </article>
25 </div> 26 </div>
26 <script src="{{ poche_url }}/themes/{{ constant('DEFAULT_THEME') }}/js/restoreScroll.js"></script> 27 <script src="{{ poche_url }}themes/{{theme}}/js/restoreScroll.js"></script>
27 <script type="text/javascript"> 28 <script type="text/javascript">
28 $(document).ready(function() { 29 $(document).ready(function() {
29 30
diff --git a/themes/courgette/config.twig b/themes/courgette/config.twig
index 26da7289..9ab58461 100755
--- a/themes/courgette/config.twig
+++ b/themes/courgette/config.twig
@@ -7,22 +7,24 @@
7{% block content %} 7{% block content %}
8 <div id="config"> 8 <div id="config">
9 <h2>{% trans "Poching a link" %}</h2> 9 <h2>{% trans "Poching a link" %}</h2>
10 <p>{% trans "You can poche a link by several methods:" %} (<a class="special" href="http://doc.wallabag.org" title="{% trans "read the documentation" %}">?</a>)</p> 10 <p>{% trans "There are several ways to save an article:" %} (<a class="special" href="http://doc.wallabag.org" title="{% trans "read the documentation" %}">?</a>)</p>
11 <ul> 11 <ul>
12 <li>firefox: <a href="https://bitbucket.org/jogaulupeau/poche/downloads/poche.xpi" title="download the firefox extension">{% trans "download the extension" %}</a></li> 12 <li>Firefox: <a href="https://addons.mozilla.org/firefox/addon/wallabag/" title="download the firefox extension">{% trans "download the extension" %}</a></li>
13 <li>chrome: <a href="https://bitbucket.org/jogaulupeau/poche/downloads/poche.crx" title="download the chrome extension">{% trans "download the extension" %}</a></li> 13 <li>Chrome: <a href="http://doc.wallabag.org/doku.php?id=users:chrome_extension" title="download the chrome extension">{% trans "download the extension" %}</a></li>
14 <li>android: <a href="https://bitbucket.org/jogaulupeau/poche/downloads/Poche.apk" title="download the application">{% trans "download the application" %}</a></li> 14 <li>Android: <a href="https://f-droid.org/app/fr.gaulupeau.apps.InThePoche" title="download the application">{% trans "via F-Droid" %}</a> {% trans " or " %} <a href="https://play.google.com/store/apps/details?id=fr.gaulupeau.apps.InThePoche" title="download the application">{% trans "via Google Play" %}</a></li>
15 <li>iOS: <a href="https://itunes.apple.com/app/wallabag/id828331015?mt=8" title="download the iOS application">{% trans "download the application" %}</a></li>
16 <li>Windows Phone: <a href="http://www.windowsphone.com/en-us/store/app/wallabag/ff890514-348c-4d0b-9b43-153fff3f7450" title="download the window phone application">{% trans "download the application" %}</a></li>
15 <li> 17 <li>
16 <form method="get" action="index.php"> 18 <form method="get" action="index.php">
17 <label class="addurl" for="plainurl">{% trans "by filling this field" %}:</label> 19 <label class="addurl" for="plainurl">{% trans "by filling this field" %}:</label>
18 <input required placeholder="Ex:mywebsite.com/article" class="addurl" id="plainurl" name="plainurl" type="url" /> 20 <input required placeholder="Ex:mywebsite.com/article" class="addurl" id="plainurl" name="plainurl" type="url" />
19 <input type="submit" value="{% trans "poche it!" %}" /> 21 <input type="submit" value="{% trans "bag it!" %}" />
20 </form> 22 </form>
21 </li> 23 </li>
22 <li>{% trans "bookmarklet: drag & drop this link to your bookmarks bar" %} <a id="bookmarklet" ondragend="this.click();" title="i am a bookmarklet, use me !" href="javascript:if(top['bookmarklet-url@wallabag.org']){top['bookmarklet-url@wallabag.org'];}else{(function(){var%20url%20=%20location.href%20||%20url;window.open('{{ poche_url }}?action=add&url='%20+%20btoa(url),'_self');})();void(0);}">{% trans "poche it!" %}</a></li> 24 <li>{% trans "bookmarklet: drag & drop this link to your bookmarks bar" %} <a id="bookmarklet" ondragend="this.click();" title="i am a bookmarklet, use me !" href="javascript:if(top['bookmarklet-url@wallabag.org']){top['bookmarklet-url@wallabag.org'];}else{(function(){var%20url%20=%20location.href%20||%20url;window.open('{{ poche_url }}?action=add&url='%20+%20btoa(url),'_self');})();void(0);}">{% trans "bag it!" %}</a></li>
23 </ul> 25 </ul>
24 26
25 <h2>{% trans "Updating poche" %}</h2> 27 <h2>{% trans "Upgrading wallabag" %}</h2>
26 <ul> 28 <ul>
27 <li>{% trans "your version" %} : <strong>{{ constant('POCHE') }}</strong></li> 29 <li>{% trans "your version" %} : <strong>{{ constant('POCHE') }}</strong></li>
28 <li>{% trans "latest stable version" %} : {{ prod }}. {% if compare_prod == -1 %}<strong><a href="http://wallabag.org/">{% trans "a more recent stable version is available." %}</a></strong>{% else %}{% trans "you are up to date." %}{% endif %}</li> 30 <li>{% trans "latest stable version" %} : {{ prod }}. {% if compare_prod == -1 %}<strong><a href="http://wallabag.org/">{% trans "a more recent stable version is available." %}</a></strong>{% else %}{% trans "you are up to date." %}{% endif %}</li>
@@ -76,7 +78,44 @@
76 <li><a href="./?import&amp;from=instapaper">{% trans "import from Instapaper" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('INSTAPAPER_FILE')) }}</li> 78 <li><a href="./?import&amp;from=instapaper">{% trans "import from Instapaper" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('INSTAPAPER_FILE')) }}</li>
77 </ul> 79 </ul>
78 80
79 <h2>{% trans "Export your poche datas" %}</h2> 81 <h2>{% trans "Export your wallabag data" %}</h2>
80 <p><a href="./?export" target="_blank">{% trans "Click here" %}</a> {% trans "to export your poche datas." %}</p> 82 <p><a href="./?export" target="_blank">{% trans "Click here" %}</a> {% trans "to export your wallabag data." %}</p>
83
84 <h2>{% trans "Fancy an E-Book ?" %}</h2>
85 <p>{% trans "Click on <a href=\"./?epub&amp;method=all\" title=\"Generate ePub\">this link</a> to get all your articles in one ebook (ePub 3 format)." %}
86 <br>{% trans "This can <b>take a while</b> and can <b>even fail</b> if you have too many articles, depending on your server configuration." %}</p>
87
88 <h2>{% trans 'Add user' %}</h2>
89 <p>{% trans 'Add a new user :' %}</p>
90 <form method="post" action="?newuser">
91 <fieldset class="w500p">
92 <div class="row">
93 <label class="col w150p" for="newusername">{% trans 'Login for new user' %}</label>
94 <input class="col" type="text" id="newusername" name="newusername" placeholder="{% trans 'Login' %}">
95 </div>
96 <div class="row">
97 <label class="col w150p" for="password4newuser">{% trans "Password for new user" %}</label>
98 <input class="col" type="password" id="password4newuser" name="password4newuser" placeholder="{% trans "Password" %}">
99 </div>
100 <div class="row mts txtcenter">
101 <button type="submit">{% trans "Send" %}</button>
102 </div>
103 </fieldset>
104 </form>
105
106 <h2>{% trans "Delete account" %}</h2>
107 {% if not only_user %}<form method="post" action="?deluser">
108 <p>{% trans "You can delete your account by entering your password and validating." %}<br /><b>{% trans "Be careful, data will be erased forever (that is a very long time)." %}</b></p>
109 <fieldset class="w500p">
110 <div class="row">
111 <label class="col w150p" for="password4deletinguser">{% trans "Type here your password" %}</label>
112 <input class="col" type="password" id="password4deletinguser" name="password4deletinguser" placeholder="{% trans "Password" %}">
113 </div>
114 <div class="row mts txtcenter">
115 <button type="submit">{% trans "Send" %}</button>
116 </div>
117 </form>
118 {% else %}<p>{% trans "You are the only user, you cannot delete your own account." %}<br />
119 {% trans "To completely remove wallabag, delete the wallabag folder on your web server." %}</p>{% endif %}
81 </div> 120 </div>
82{% endblock %} \ No newline at end of file 121{% endblock %}
diff --git a/themes/courgette/home.twig b/themes/courgette/home.twig
index 416cfa43..401f3f20 100755
--- a/themes/courgette/home.twig
+++ b/themes/courgette/home.twig
@@ -14,8 +14,8 @@
14{% block precontent %} 14{% block precontent %}
15 {% if entries|length > 1 %} 15 {% if entries|length > 1 %}
16 <ul id="sort"> 16 <ul id="sort">
17 <li><a href="./?sort=ia&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/top.png" alt="{% trans "by date asc" %}" title="{% trans "by date asc" %}" /></a> {% trans "by date" %} <a href="./?sort=id&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/down.png" alt="{% trans "by date desc" %}" title="{% trans "by date desc" %}" /></a></li> 17 <li><a href="./?sort=ia&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/top.png" alt="{% trans "by date asc" %}" title="{% trans "by date asc" %}" /></a> {% trans "by date" %} <a href="./?sort=id&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/down.png" alt="{% trans "by date desc" %}" title="{% trans "by date desc" %}" /></a></li>
18 <li><a href="./?sort=ta&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/top.png" alt="{% trans "by title asc" %}" title="{% trans "by title asc" %}" /></a> {% trans "by title" %} <a href="./?sort=td&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/down.png" alt="{% trans "by title desc" %}" title="{% trans "by title desc" %}" /></a></li> 18 <li><a href="./?sort=ta&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/top.png" alt="{% trans "by title asc" %}" title="{% trans "by title asc" %}" /></a> {% trans "by title" %} <a href="./?sort=td&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/down.png" alt="{% trans "by title desc" %}" title="{% trans "by title desc" %}" /></a></li>
19 </ul> 19 </ul>
20 {% endif %} 20 {% endif %}
21{% endblock %} 21{% endblock %}
@@ -26,9 +26,15 @@
26 {% block pager %} 26 {% block pager %}
27 {% if nb_results > 1 %} 27 {% if nb_results > 1 %}
28 <div class="results"> 28 <div class="results">
29 <div class="nb-results">{{ nb_results }} {% trans "results" %}</div> 29 <div class="nb-results">{{ nb_results }} {% trans "results" %}{% if search_term is defined %}{% trans " found for « " %} {{ search_term }} »{% endif %}</div>
30 {{ page_links | raw }} 30 {{ page_links | raw }}
31 </div> 31 </div>
32 {% elseif nb_results == 1 %}
33 {% if search_term is defined %}
34 <div class="results">
35 <div class="nb-results">{% trans "Only one result found for " %} « {{ search_term }} »</div>
36 </div>
37 {% endif %}
32 {% endif %} 38 {% endif %}
33 {% endblock %} 39 {% endblock %}
34 {% for entry in entries %} 40 {% for entry in entries %}
@@ -44,6 +50,13 @@
44 <p>{{ entry.content|striptags|slice(0, 300) }}...</p> 50 <p>{{ entry.content|striptags|slice(0, 300) }}...</p>
45 </div> 51 </div>
46 {% endfor %} 52 {% endfor %}
47 {% endif %} 53
48 {{ block('pager') }} 54 {{ block('pager') }}
55
56 {% if tag %}<a title="{% trans "Download the articles from this tag in an epub" %}" href="./?epub&amp;method=tag&amp;tag={{ tag.value }}">{% trans "Download the articles from this tag in an epub" %}</a>
57 {% elseif search_term is defined %}<a title="{% trans "Download the articles from this search in an epub" %}" href="./?epub&amp;method=search&amp;search={{ search_term }}">{% trans "Download the articles from this search in an epub" %}</a>
58 {% else %}<a title="{% trans "Download the articles from this category in an epub" %}" href="./?epub&amp;method=category&amp;category={{ view }}">{% trans "Download the articles from this category in an epub" %}</a>{% endif %}
59
60 {% endif %}
61
49{% endblock %} \ No newline at end of file 62{% endblock %} \ No newline at end of file
diff --git a/themes/courgette/tags.twig b/themes/courgette/tags.twig
index 9dfdab52..b11dce28 100644..100755
--- a/themes/courgette/tags.twig
+++ b/themes/courgette/tags.twig
@@ -4,5 +4,5 @@
4{% include '_menu.twig' %} 4{% include '_menu.twig' %}
5{% endblock %} 5{% endblock %}
6{% block content %} 6{% block content %}
7{% for tag in tags %}<a class="tag" href="./?view=tag&amp;id={{ tag.id }}">{{ tag.value }}</a> {% if token != '' %}<a href="?feed&amp;type=tag&amp;user_id={{ user_id }}&amp;tag_id={{ tag.id }}&amp;token={{ token }}" target="_blank"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/rss.png" /></a>{% endif %} {% endfor %} 7{% for tag in tags %}<a class="tag" href="./?view=tag&amp;id={{ tag.id }}">{{ tag.value }}</a> {% if token != '' %}<a href="?feed&amp;type=tag&amp;user_id={{ user_id }}&amp;tag_id={{ tag.id }}&amp;token={{ token }}" target="_blank"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/rss.png" /></a>{% endif %} {% endfor %}
8{% endblock %} \ No newline at end of file 8{% endblock %} \ No newline at end of file
diff --git a/themes/dark/img/dark/rss.png b/themes/dark/img/dark/rss.png
new file mode 100644
index 00000000..21bad1a1
--- /dev/null
+++ b/themes/dark/img/dark/rss.png
Binary files differ
diff --git a/themes/default/_bookmarklet.twig b/themes/default/_bookmarklet.twig
index 2e3071ad..61996353 100644
--- a/themes/default/_bookmarklet.twig
+++ b/themes/default/_bookmarklet.twig
@@ -1,3 +1,3 @@
1 <script type="text/javascript"> 1 <script type="text/javascript">
2 top["bookmarklet-url@wallabag.org"]=""+"<!DOCTYPE html>"+"<html>"+"<head>"+"<title>poche it !</title>"+'<link rel="icon" href="{{poche_url}}tpl/img/favicon.ico" />'+"</head>"+"<body>"+"<script>"+"window.onload=function(){"+"window.setTimeout(function(){"+"history.back();"+"},250);"+"};"+"</scr"+"ipt>"+"</body>"+"</html>" 2 top["bookmarklet-url@wallabag.org"]=""+"<!DOCTYPE html>"+"<html>"+"<head>"+"<title>bag it!</title>"+'<link rel="icon" href="{{poche_url}}tpl/img/favicon.ico" />'+"</head>"+"<body>"+"<script>"+"window.onload=function(){"+"window.setTimeout(function(){"+"history.back();"+"},250);"+"};"+"</scr"+"ipt>"+"</body>"+"</html>"
3 </script> \ No newline at end of file 3 </script> \ No newline at end of file
diff --git a/themes/default/_head.twig b/themes/default/_head.twig
index f310e420..8c939e30 100644..100755
--- a/themes/default/_head.twig
+++ b/themes/default/_head.twig
@@ -1,12 +1,14 @@
1 <link rel="shortcut icon" type="image/x-icon" href="{{ poche_url }}/themes/default/img/favicon.ico" /> 1 <link rel="shortcut icon" type="image/x-icon" href="{{ poche_url }}themes/default/img/favicon.ico" />
2 <link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ poche_url }}/themes/default/img/apple-touch-icon-144x144-precomposed.png"> 2 <link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ poche_url }}themes/default/img/apple-touch-icon-144x144-precomposed.png">
3 <link rel="apple-touch-icon-precomposed" sizes="72x72" href="{{ poche_url }}/themes/default/img/apple-touch-icon-72x72-precomposed.png"> 3 <link rel="apple-touch-icon-precomposed" sizes="72x72" href="{{ poche_url }}themes/default/img/apple-touch-icon-72x72-precomposed.png">
4 <link rel="apple-touch-icon-precomposed" href="{{ poche_url }}/themes/default/img/apple-touch-icon-precomposed.png"> 4 <link rel="apple-touch-icon-precomposed" href="{{ poche_url }}themes/default/img/apple-touch-icon-precomposed.png">
5 <link rel="stylesheet" href="{{ poche_url }}/themes/default/css/knacss.css" media="all"> 5 <link rel="stylesheet" href="{{ poche_url }}themes/default/css/knacss.css" media="all">
6 <link rel="stylesheet" href="{{ poche_url }}/themes/default/css/style.css" media="all"> 6 <link rel="stylesheet" href="{{ poche_url }}themes/default/css/style.css" media="all">
7 <link rel="stylesheet" href="{{ poche_url }}/themes/{{ theme }}/css/style-{{ theme }}.css" media="all" title="{{ theme }} theme"> 7 <link rel="stylesheet" href="{{ poche_url }}themes/{{ theme }}/css/style-{{ theme }}.css" media="all" title="{{ theme }} theme">
8 <link rel="stylesheet" href="{{ poche_url }}/themes/default/css/messages.css" media="all"> 8 <link rel="stylesheet" href="{{ poche_url }}themes/default/css/messages.css" media="all">
9 <link rel="stylesheet" href="{{ poche_url }}/themes/default/css/print.css" media="print"> 9 <link rel="stylesheet" href="{{ poche_url }}themes/default/css/print.css" media="print">
10 <script src="{{ poche_url }}/themes/default/js/jquery-2.0.3.min.js"></script> 10 <script src="{{ poche_url }}themes/default/js/jquery-2.0.3.min.js"></script>
11 <script src="{{ poche_url }}/themes/default/js/autoClose.js"></script> 11 <script src="{{ poche_url }}themes/default/js/autoClose.js"></script>
12 <script src="{{ poche_url }}/themes/default/js/closeMessage.js"></script> \ No newline at end of file 12 <script src="{{ poche_url }}themes/default/js/closeMessage.js"></script>
13 <script src="{{ poche_url }}themes/default/js/saveLink.js"></script>
14 <script src="{{ poche_url }}themes/default/js/popupForm.js"></script> \ No newline at end of file
diff --git a/themes/default/_import.twig b/themes/default/_import.twig
new file mode 100755
index 00000000..c59b7a15
--- /dev/null
+++ b/themes/default/_import.twig
@@ -0,0 +1,15 @@
1<script type="text/javascript">
2<!--
3 $(document).ready(function() {
4 $("body").css("cursor", "wait");
5
6 setTimeout(function(){
7 window.location = './?import';
8 }, {{ import.delay }} );
9 });
10//-->
11</script>
12<div class="messages warning">
13 <p>{% trans "Download required for " %} {{ import.recordsDownloadRequired }} {% trans "records" %}.</p>
14 <p>{% trans "Downloading next " %} {{ import.recordsUnderDownload }} {% trans "articles, please wait" %}...</p>
15</div>
diff --git a/themes/default/_menu.twig b/themes/default/_menu.twig
index 55583b3d..0daa0b03 100644
--- a/themes/default/_menu.twig
+++ b/themes/default/_menu.twig
@@ -3,9 +3,11 @@
3 <li><a href="./?view=fav" {% if view == 'fav' %}class="current"{% endif %}>{% trans "favorites" %}</a></li> 3 <li><a href="./?view=fav" {% if view == 'fav' %}class="current"{% endif %}>{% trans "favorites" %}</a></li>
4 <li><a href="./?view=archive" {% if view == 'archive' %}class="current"{% endif %}>{% trans "archive" %}</a></li> 4 <li><a href="./?view=archive" {% if view == 'archive' %}class="current"{% endif %}>{% trans "archive" %}</a></li>
5 <li><a href="./?view=tags" {% if view == 'tags' %}class="current"{% endif %}>{% trans "tags" %}</a></li> 5 <li><a href="./?view=tags" {% if view == 'tags' %}class="current"{% endif %}>{% trans "tags" %}</a></li>
6 <li><a href="javascript: void(null);" id="pocheit">{% trans "save a link" %}</a><span id="pocheit-arrow"></span></li> 6 <li><a href="javascript: void(null);" id="bagit">{% trans "save a link" %}</a><span id="bagit-arrow"></span></li>
7 <li><a href="javascript: void(null);" id="search">{% trans "search" %}</a><span id="search-arrow"></span></li>
7 <li><a href="./?view=config" {% if view == 'config' %}class="current"{% endif %}>{% trans "config" %}</a></li> 8 <li><a href="./?view=config" {% if view == 'config' %}class="current"{% endif %}>{% trans "config" %}</a></li>
8 <li><a href="./?logout" title="{% trans "logout" %}">{% trans "logout" %}</a></li> 9 <li><a href="./?logout" title="{% trans "logout" %}">{% trans "logout" %}</a></li>
9 </ul> 10 </ul>
10 {% include '_pocheit-form.twig' %} 11 {% include '_pocheit-form.twig' %}
12 {% include '_search-form.twig' %}
11 13
diff --git a/themes/default/_pocheit-form.twig b/themes/default/_pocheit-form.twig
index 13096159..8c982db0 100755
--- a/themes/default/_pocheit-form.twig
+++ b/themes/default/_pocheit-form.twig
@@ -1,22 +1,8 @@
1<div id="pocheit-form" class="messages info"> 1<div id="bagit-form" class="messages info">
2 <center> 2 <a href="javascript: void(null);" id="bagit-form-close">&nbsp;</a>
3 <form method="get" action="index.php"> 3 <form method="get" action="index.php" id="bagit-form-form">
4 <input required placeholder="example.com/article" class="addurl" id="plainurl" name="plainurl" type="url" /> 4 <input required placeholder="example.com/article" class="addurl" id="plainurl" name="plainurl" type="url" />
5 <input type="submit" value="{% trans "save link!" %}" /> 5 <input type="submit" value="{% trans "save link!" %}" />
6 <div id="add-link-result"></div>
6 </form> 7 </form>
7 </center>
8</div> 8</div>
9<script type="text/javascript">
10 $(document).ready(function() {
11
12 $("#pocheit-form").hide();
13
14 $("#pocheit").click(function(){
15 $("#pocheit-form").toggle();
16 $("#pocheit").toggleClass("current");
17 $("#pocheit-arrow").toggleClass("arrow-down");
18 });
19
20
21 });
22</script>
diff --git a/themes/default/_search-form.twig b/themes/default/_search-form.twig
new file mode 100755
index 00000000..33bea20d
--- /dev/null
+++ b/themes/default/_search-form.twig
@@ -0,0 +1,9 @@
1<div id="search-form" class="messages info">
2<form method="get" action="index.php">
3 <p>
4 <input type="hidden" name="view" value="search"></input>
5 <label>{% trans "Search" %}</label> : <input type="text" placeholder="{% trans "Enter your search here" %}" name="search" />
6 <input type="submit" value="{% trans "Search" %} !"></input>
7 </p>
8</form>
9</div>
diff --git a/themes/default/_sorting.twig b/themes/default/_sorting.twig
new file mode 100755
index 00000000..ce3d38bc
--- /dev/null
+++ b/themes/default/_sorting.twig
@@ -0,0 +1,6 @@
1{% if entries|length > 1 %}
2 <ul id="sort">
3 <li><a href="./?sort=ia&amp;view={{ view }}{% if search_term is defined %}&amp;search={{ search_term }}{% endif %}&amp;id={{ id }}"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/top.png" alt="{% trans "by date asc" %}" title="{% trans "by date asc" %}" /></a> {% trans "by date" %} <a href="./?sort=id&amp;view={{ view }}{% if search_term is defined %}&amp;search={{ search_term }}{% endif %}&amp;id={{ id }}"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/down.png" alt="{% trans "by date desc" %}" title="{% trans "by date desc" %}" /></a></li>
4 <li><a href="./?sort=ta&amp;view={{ view }}{% if search_term is defined %}&amp;search={{ search_term }}{% endif %}&amp;id={{ id }}"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/top.png" alt="{% trans "by title asc" %}" title="{% trans "by title asc" %}" /></a> {% trans "by title" %} <a href="./?sort=td&amp;view={{ view }}{% if search_term is defined %}&amp;search={{ search_term }}{% endif %}&amp;id={{ id }}"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/down.png" alt="{% trans "by title desc" %}" title="{% trans "by title desc" %}" /></a></li>
5 </ul>
6{% endif %}
diff --git a/themes/default/_top.twig b/themes/default/_top.twig
index 0711acdc..45806f01 100644..100755
--- a/themes/default/_top.twig
+++ b/themes/default/_top.twig
@@ -1,6 +1,6 @@
1 <header class="w600p center mbm"> 1 <header class="w600p center mbm">
2 <h1> 2 <h1>
3 {% if view == 'home' %}{% block logo %}<img width="100" height="100" src="{{ poche_url }}/themes/baggy/img/logo-other_themes.png" alt="logo poche" />{% endblock %} 3 {% if view == 'home' %}{% block logo %}<img width="100" height="100" src="{{ poche_url }}themes/baggy/img/logo-other_themes.png" alt="wallabag logo" />{% endblock %}
4 {% else %}<a href="./" title="{% trans "return home" %}" >{{ block('logo') }}</a> 4 {% else %}<a href="./" title="{% trans "return home" %}" >{{ block('logo') }}</a>
5 {% endif %} 5 {% endif %}
6 </h1> 6 </h1>
diff --git a/themes/default/config.twig b/themes/default/config.twig
index cd4d074d..160f6046 100644..100755
--- a/themes/default/config.twig
+++ b/themes/default/config.twig
@@ -8,10 +8,11 @@
8 <h2>{% trans "Saving articles" %}</h2> 8 <h2>{% trans "Saving articles" %}</h2>
9 <p>{% trans "There are several ways to save an article:" %} (<a href="http://doc.wallabag.org/" title="{% trans "read the documentation" %}">?</a>)</p> 9 <p>{% trans "There are several ways to save an article:" %} (<a href="http://doc.wallabag.org/" title="{% trans "read the documentation" %}">?</a>)</p>
10 <ul> 10 <ul>
11 <li>Firefox: <a href="https://addons.mozilla.org/firefox/addon/poche/" title="download the firefox extension">{% trans "download the extension" %}</a></li> 11 <li>Firefox: <a href="https://addons.mozilla.org/firefox/addon/wallabag/" title="download the firefox extension">{% trans "download the extension" %}</a></li>
12 <li>Chrome: <a href="http://doc.wallabag.org/doku.php?id=users:chrome_extension" title="download the chrome extension">{% trans "download the extension" %}</a></li> 12 <li>Chrome: <a href="http://doc.wallabag.org/doku.php?id=users:chrome_extension" title="download the chrome extension">{% trans "download the extension" %}</a></li>
13 <li>Android: <a href="https://f-droid.org/repository/browse/?fdid=fr.gaulupeau.apps.Poche" title="download the application">{% trans "via F-Droid" %}</a> {% trans " or " %} <a href="https://play.google.com/store/apps/details?id=fr.gaulupeau.apps.InThePoche" title="download the application">{% trans "via Google Play" %}</a></li> 13 <li>Android: <a href="https://f-droid.org/app/fr.gaulupeau.apps.InThePoche" title="download the application">{% trans "via F-Droid" %}</a> {% trans " or " %} <a href="https://play.google.com/store/apps/details?id=fr.gaulupeau.apps.InThePoche" title="download the application">{% trans "via Google Play" %}</a></li>
14 <li>Windows Phone: <a href="https://www.windowsphone.com/en-us/store/app/poche/334de2f0-51b5-4826-8549-a3d805a37e83" title="download the window phone application">{% trans "download the application" %}</a></li> 14 <li>iOS: <a href="https://itunes.apple.com/app/wallabag/id828331015?mt=8" title="download the iOS application">{% trans "download the application" %}</a></li>
15 <li>Windows Phone: <a href="http://www.windowsphone.com/en-us/store/app/wallabag/ff890514-348c-4d0b-9b43-153fff3f7450" title="download the window phone application">{% trans "download the application" %}</a></li>
15 <li> 16 <li>
16 <form method="get" action="index.php"> 17 <form method="get" action="index.php">
17 <label class="addurl" for="config_plainurl">{% trans "By filling this field" %}:</label> 18 <label class="addurl" for="config_plainurl">{% trans "By filling this field" %}:</label>
@@ -103,14 +104,20 @@
103 {% endif %} 104 {% endif %}
104 105
105 <h2>{% trans "Import" %}</h2> 106 <h2>{% trans "Import" %}</h2>
106 <p>{% trans "Please execute the import script locally as it can take a very long time." %}</p> 107 <p>{% trans "You can import your Pocket, Readability, Instapaper, Wallabag or any data in appropriate json or html format." %}</p>
107 <p>{% trans "More info in the official documentation:" %} <a href="http://doc.wallabag.org/doku.php?id=users:migrate">wallabag.org</a></p> 108 <p>{% trans "Please select export file on your computer and press \"Import\" button below.<br>Wallabag will parse your file, insert all URLs and start fetching of articles if required.<br>Fetching process is controlled by two constants in your config file: IMPORT_LIMIT (how many articles are fetched at once) and IMPORT_DELAY (delay between fetch of next batch of articles)." %}</p>
108 <ul> 109 <form method="post" action="?import" name="uploadfile" enctype="multipart/form-data">
109 <li><a href="./?import&amp;from=pocket">{% trans "Import from Pocket" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('POCKET_FILE')) }}</li> 110 <fieldset class="w500p">
110 <li><a href="./?import&amp;from=readability">{% trans "Import from Readability" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('READABILITY_FILE')) }}</li> 111 <div class="row">
111 <li><a href="./?import&amp;from=instapaper">{% trans "Import from Instapaper" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('INSTAPAPER_FILE')) }}</li> 112 <label class="col w150p" for="file">{% trans "File:" %}</label>
112 <li><a href="./?import&amp;from=poche">{% trans "Import from wallabag" %}</a> {{ '(you must have a %s file on your server)'|trans|format(constant('POCHE_FILE')) }}</li> 113 <input class="col" type="file" id="file" name="file" tabindex="4">
113 </ul> 114 </div>
115 <div class="row mts txtcenter">
116 <button class="bouton" type="submit" tabindex="4">{% trans "Import" %}</button>
117 </div>
118 </fieldset>
119 </form>
120 <p><a href="?import">{% trans "You can click here to fetch content for articles with no content." %}</a></p>
114 121
115 <h2>{% trans "Export your wallabag data" %}</h2> 122 <h2>{% trans "Export your wallabag data" %}</h2>
116 {% if constant('STORAGE') == 'sqlite' %} 123 {% if constant('STORAGE') == 'sqlite' %}
@@ -119,4 +126,41 @@
119 126
120 <h2>{% trans "Cache" %}</h2> 127 <h2>{% trans "Cache" %}</h2>
121 <p><a href="?empty-cache">{% trans "Click here" %}</a> {% trans "to delete cache." %}</p> 128 <p><a href="?empty-cache">{% trans "Click here" %}</a> {% trans "to delete cache." %}</p>
129
130 <h2>{% trans "Fancy an E-Book ?" %}</h2>
131 <p>{% trans "Click on <a href=\"./?epub&amp;method=all\" title=\"Generate ePub\">this link</a> to get all your articles in one ebook (ePub 3 format)." %}
132 <br>{% trans "This can <b>take a while</b> and can <b>even fail</b> if you have too many articles, depending on your server configuration." %}</p>
133
134 <h2>{% trans 'Add user' %}</h2>
135 <p>{% trans 'Add a new user :' %}</p>
136 <form method="post" action="?newuser">
137 <fieldset class="w500p">
138 <div class="row">
139 <label class="col w150p" for="newusername">{% trans 'Login for new user' %}</label>
140 <input class="col" type="text" id="newusername" name="newusername" placeholder="{% trans 'Login' %}">
141 </div>
142 <div class="row">
143 <label class="col w150p" for="password4newuser">{% trans "Password for new user" %}</label>
144 <input class="col" type="password" id="password4newuser" name="password4newuser" placeholder="{% trans "Password" %}">
145 </div>
146 <div class="row mts txtcenter">
147 <button type="submit">{% trans "Send" %}</button>
148 </div>
149 </fieldset>
150 </form>
151
152 <h2>{% trans "Delete account" %}</h2>
153 {% if not only_user %}<form method="post" action="?deluser">
154 <p>{% trans "You can delete your account by entering your password and validating." %}<br /><b>{% trans "Be careful, data will be erased forever (that is a very long time)." %}</b></p>
155 <fieldset class="w500p">
156 <div class="row">
157 <label class="col w150p" for="password4deletinguser">{% trans "Type here your password" %}</label>
158 <input class="col" type="password" id="password4deletinguser" name="password4deletinguser" placeholder="{% trans "Password" %}">
159 </div>
160 <div class="row mts txtcenter">
161 <button type="submit">{% trans "Send" %}</button>
162 </div>
163 </form>
164 {% else %}<p>{% trans "You are the only user, you cannot delete your own account." %}<br />
165 {% trans "To completely remove wallabag, delete the wallabag folder on your web server." %}</p>{% endif %}
122{% endblock %} 166{% endblock %}
diff --git a/themes/default/css/images/animated-overlay.gif b/themes/default/css/images/animated-overlay.gif
new file mode 100644
index 00000000..d441f75e
--- /dev/null
+++ b/themes/default/css/images/animated-overlay.gif
Binary files differ
diff --git a/themes/default/css/images/ui-bg_flat_0_aaaaaa_40x100.png b/themes/default/css/images/ui-bg_flat_0_aaaaaa_40x100.png
new file mode 100644
index 00000000..9f10cb65
--- /dev/null
+++ b/themes/default/css/images/ui-bg_flat_0_aaaaaa_40x100.png
Binary files differ
diff --git a/themes/default/css/images/ui-bg_flat_75_ffffff_40x100.png b/themes/default/css/images/ui-bg_flat_75_ffffff_40x100.png
new file mode 100644
index 00000000..b89b914b
--- /dev/null
+++ b/themes/default/css/images/ui-bg_flat_75_ffffff_40x100.png
Binary files differ
diff --git a/themes/default/css/images/ui-bg_glass_55_fbf9ee_1x400.png b/themes/default/css/images/ui-bg_glass_55_fbf9ee_1x400.png
new file mode 100644
index 00000000..780d3ffd
--- /dev/null
+++ b/themes/default/css/images/ui-bg_glass_55_fbf9ee_1x400.png
Binary files differ
diff --git a/themes/default/css/images/ui-bg_glass_65_ffffff_1x400.png b/themes/default/css/images/ui-bg_glass_65_ffffff_1x400.png
new file mode 100644
index 00000000..da53255b
--- /dev/null
+++ b/themes/default/css/images/ui-bg_glass_65_ffffff_1x400.png
Binary files differ
diff --git a/themes/default/css/images/ui-bg_glass_75_dadada_1x400.png b/themes/default/css/images/ui-bg_glass_75_dadada_1x400.png
new file mode 100644
index 00000000..f6306608
--- /dev/null
+++ b/themes/default/css/images/ui-bg_glass_75_dadada_1x400.png
Binary files differ
diff --git a/themes/default/css/images/ui-bg_glass_75_e6e6e6_1x400.png b/themes/default/css/images/ui-bg_glass_75_e6e6e6_1x400.png
new file mode 100644
index 00000000..7d347065
--- /dev/null
+++ b/themes/default/css/images/ui-bg_glass_75_e6e6e6_1x400.png
Binary files differ
diff --git a/themes/default/css/images/ui-bg_glass_95_fef1ec_1x400.png b/themes/default/css/images/ui-bg_glass_95_fef1ec_1x400.png
new file mode 100644
index 00000000..533c4900
--- /dev/null
+++ b/themes/default/css/images/ui-bg_glass_95_fef1ec_1x400.png
Binary files differ
diff --git a/themes/default/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/themes/default/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png
new file mode 100644
index 00000000..bf3a9481
--- /dev/null
+++ b/themes/default/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png
Binary files differ
diff --git a/themes/default/css/images/ui-icons_222222_256x240.png b/themes/default/css/images/ui-icons_222222_256x240.png
new file mode 100644
index 00000000..c1cb1170
--- /dev/null
+++ b/themes/default/css/images/ui-icons_222222_256x240.png
Binary files differ
diff --git a/themes/default/css/images/ui-icons_2e83ff_256x240.png b/themes/default/css/images/ui-icons_2e83ff_256x240.png
new file mode 100644
index 00000000..84b601bf
--- /dev/null
+++ b/themes/default/css/images/ui-icons_2e83ff_256x240.png
Binary files differ
diff --git a/themes/default/css/images/ui-icons_454545_256x240.png b/themes/default/css/images/ui-icons_454545_256x240.png
new file mode 100644
index 00000000..b6db1acd
--- /dev/null
+++ b/themes/default/css/images/ui-icons_454545_256x240.png
Binary files differ
diff --git a/themes/default/css/images/ui-icons_888888_256x240.png b/themes/default/css/images/ui-icons_888888_256x240.png
new file mode 100644
index 00000000..feea0e20
--- /dev/null
+++ b/themes/default/css/images/ui-icons_888888_256x240.png
Binary files differ
diff --git a/themes/default/css/images/ui-icons_cd0a0a_256x240.png b/themes/default/css/images/ui-icons_cd0a0a_256x240.png
new file mode 100644
index 00000000..ed5b6b09
--- /dev/null
+++ b/themes/default/css/images/ui-icons_cd0a0a_256x240.png
Binary files differ
diff --git a/themes/default/css/jquery-ui-1.10.4.custom.css b/themes/default/css/jquery-ui-1.10.4.custom.css
new file mode 100644
index 00000000..5690172f
--- /dev/null
+++ b/themes/default/css/jquery-ui-1.10.4.custom.css
@@ -0,0 +1,560 @@
1/*! jQuery UI - v1.10.4 - 2014-03-09
2* http://jqueryui.com
3* Includes: jquery.ui.core.css, jquery.ui.autocomplete.css, jquery.ui.menu.css, jquery.ui.theme.css
4* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
5* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
6
7/* Layout helpers
8----------------------------------*/
9.ui-helper-hidden {
10 display: none;
11}
12.ui-helper-hidden-accessible {
13 border: 0;
14 clip: rect(0 0 0 0);
15 height: 1px;
16 margin: -1px;
17 overflow: hidden;
18 padding: 0;
19 position: absolute;
20 width: 1px;
21}
22.ui-helper-reset {
23 margin: 0;
24 padding: 0;
25 border: 0;
26 outline: 0;
27 line-height: 1.3;
28 text-decoration: none;
29 font-size: 100%;
30 list-style: none;
31}
32.ui-helper-clearfix:before,
33.ui-helper-clearfix:after {
34 content: "";
35 display: table;
36 border-collapse: collapse;
37}
38.ui-helper-clearfix:after {
39 clear: both;
40}
41.ui-helper-clearfix {
42 min-height: 0; /* support: IE7 */
43}
44.ui-helper-zfix {
45 width: 100%;
46 height: 100%;
47 top: 0;
48 left: 0;
49 position: absolute;
50 opacity: 0;
51 filter:Alpha(Opacity=0);
52}
53
54.ui-front {
55 z-index: 100;
56}
57
58
59/* Interaction Cues
60----------------------------------*/
61.ui-state-disabled {
62 cursor: default !important;
63}
64
65
66/* Icons
67----------------------------------*/
68
69/* states and images */
70.ui-icon {
71 display: block;
72 text-indent: -99999px;
73 overflow: hidden;
74 background-repeat: no-repeat;
75}
76
77
78/* Misc visuals
79----------------------------------*/
80
81/* Overlays */
82.ui-widget-overlay {
83 position: fixed;
84 top: 0;
85 left: 0;
86 width: 100%;
87 height: 100%;
88}
89.ui-autocomplete {
90 position: absolute;
91 top: 0;
92 left: 0;
93 cursor: default;
94}
95.ui-menu {
96 list-style: none;
97 padding: 2px;
98 margin: 0;
99 display: block;
100 outline: none;
101}
102.ui-menu .ui-menu {
103 margin-top: -3px;
104 position: absolute;
105}
106.ui-menu .ui-menu-item {
107 margin: 0;
108 padding: 0;
109 width: 100%;
110 /* support: IE10, see #8844 */
111 list-style-image: url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7);
112}
113.ui-menu .ui-menu-divider {
114 margin: 5px -2px 5px -2px;
115 height: 0;
116 font-size: 0;
117 line-height: 0;
118 border-width: 1px 0 0 0;
119}
120.ui-menu .ui-menu-item a {
121 text-decoration: none;
122 display: block;
123 padding: 2px .4em;
124 line-height: 1.5;
125 min-height: 0; /* support: IE7 */
126 font-weight: normal;
127}
128.ui-menu .ui-menu-item a.ui-state-focus,
129.ui-menu .ui-menu-item a.ui-state-active {
130 font-weight: normal;
131 margin: -1px;
132}
133
134.ui-menu .ui-state-disabled {
135 font-weight: normal;
136 margin: .4em 0 .2em;
137 line-height: 1.5;
138}
139.ui-menu .ui-state-disabled a {
140 cursor: default;
141}
142
143/* icon support */
144.ui-menu-icons {
145 position: relative;
146}
147.ui-menu-icons .ui-menu-item a {
148 position: relative;
149 padding-left: 2em;
150}
151
152/* left-aligned */
153.ui-menu .ui-icon {
154 position: absolute;
155 top: .2em;
156 left: .2em;
157}
158
159/* right-aligned */
160.ui-menu .ui-menu-icon {
161 position: static;
162 float: right;
163}
164
165/* Component containers
166----------------------------------*/
167.ui-widget {
168 font-family: Verdana,Arial,sans-serif;
169 font-size: 1.1em;
170}
171.ui-widget .ui-widget {
172 font-size: 1em;
173}
174.ui-widget input,
175.ui-widget select,
176.ui-widget textarea,
177.ui-widget button {
178 font-family: Verdana,Arial,sans-serif;
179 font-size: 1em;
180}
181.ui-widget-content {
182 border: 1px solid #aaaaaa;
183 background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;
184 color: #222222;
185}
186.ui-widget-content a {
187 color: #222222;
188}
189.ui-widget-header {
190 border: 1px solid #aaaaaa;
191 background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;
192 color: #222222;
193 font-weight: bold;
194}
195.ui-widget-header a {
196 color: #222222;
197}
198
199/* Interaction states
200----------------------------------*/
201.ui-state-default,
202.ui-widget-content .ui-state-default,
203.ui-widget-header .ui-state-default {
204 border: 1px solid #d3d3d3;
205 background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;
206 font-weight: normal;
207 color: #555555;
208}
209.ui-state-default a,
210.ui-state-default a:link,
211.ui-state-default a:visited {
212 color: #555555;
213 text-decoration: none;
214}
215.ui-state-hover,
216.ui-widget-content .ui-state-hover,
217.ui-widget-header .ui-state-hover,
218.ui-state-focus,
219.ui-widget-content .ui-state-focus,
220.ui-widget-header .ui-state-focus {
221 border: 1px solid #999999;
222 background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;
223 font-weight: normal;
224 color: #212121;
225}
226.ui-state-hover a,
227.ui-state-hover a:hover,
228.ui-state-hover a:link,
229.ui-state-hover a:visited,
230.ui-state-focus a,
231.ui-state-focus a:hover,
232.ui-state-focus a:link,
233.ui-state-focus a:visited {
234 color: #212121;
235 text-decoration: none;
236}
237.ui-state-active,
238.ui-widget-content .ui-state-active,
239.ui-widget-header .ui-state-active {
240 border: 1px solid #aaaaaa;
241 background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;
242 font-weight: normal;
243 color: #212121;
244}
245.ui-state-active a,
246.ui-state-active a:link,
247.ui-state-active a:visited {
248 color: #212121;
249 text-decoration: none;
250}
251
252/* Interaction Cues
253----------------------------------*/
254.ui-state-highlight,
255.ui-widget-content .ui-state-highlight,
256.ui-widget-header .ui-state-highlight {
257 border: 1px solid #fcefa1;
258 background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;
259 color: #363636;
260}
261.ui-state-highlight a,
262.ui-widget-content .ui-state-highlight a,
263.ui-widget-header .ui-state-highlight a {
264 color: #363636;
265}
266.ui-state-error,
267.ui-widget-content .ui-state-error,
268.ui-widget-header .ui-state-error {
269 border: 1px solid #cd0a0a;
270 background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;
271 color: #cd0a0a;
272}
273.ui-state-error a,
274.ui-widget-content .ui-state-error a,
275.ui-widget-header .ui-state-error a {
276 color: #cd0a0a;
277}
278.ui-state-error-text,
279.ui-widget-content .ui-state-error-text,
280.ui-widget-header .ui-state-error-text {
281 color: #cd0a0a;
282}
283.ui-priority-primary,
284.ui-widget-content .ui-priority-primary,
285.ui-widget-header .ui-priority-primary {
286 font-weight: bold;
287}
288.ui-priority-secondary,
289.ui-widget-content .ui-priority-secondary,
290.ui-widget-header .ui-priority-secondary {
291 opacity: .7;
292 filter:Alpha(Opacity=70);
293 font-weight: normal;
294}
295.ui-state-disabled,
296.ui-widget-content .ui-state-disabled,
297.ui-widget-header .ui-state-disabled {
298 opacity: .35;
299 filter:Alpha(Opacity=35);
300 background-image: none;
301}
302.ui-state-disabled .ui-icon {
303 filter:Alpha(Opacity=35); /* For IE8 - See #6059 */
304}
305
306/* Icons
307----------------------------------*/
308
309/* states and images */
310.ui-icon {
311 width: 16px;
312 height: 16px;
313}
314.ui-icon,
315.ui-widget-content .ui-icon {
316 background-image: url(images/ui-icons_222222_256x240.png);
317}
318.ui-widget-header .ui-icon {
319 background-image: url(images/ui-icons_222222_256x240.png);
320}
321.ui-state-default .ui-icon {
322 background-image: url(images/ui-icons_888888_256x240.png);
323}
324.ui-state-hover .ui-icon,
325.ui-state-focus .ui-icon {
326 background-image: url(images/ui-icons_454545_256x240.png);
327}
328.ui-state-active .ui-icon {
329 background-image: url(images/ui-icons_454545_256x240.png);
330}
331.ui-state-highlight .ui-icon {
332 background-image: url(images/ui-icons_2e83ff_256x240.png);
333}
334.ui-state-error .ui-icon,
335.ui-state-error-text .ui-icon {
336 background-image: url(images/ui-icons_cd0a0a_256x240.png);
337}
338
339/* positioning */
340.ui-icon-blank { background-position: 16px 16px; }
341.ui-icon-carat-1-n { background-position: 0 0; }
342.ui-icon-carat-1-ne { background-position: -16px 0; }
343.ui-icon-carat-1-e { background-position: -32px 0; }
344.ui-icon-carat-1-se { background-position: -48px 0; }
345.ui-icon-carat-1-s { background-position: -64px 0; }
346.ui-icon-carat-1-sw { background-position: -80px 0; }
347.ui-icon-carat-1-w { background-position: -96px 0; }
348.ui-icon-carat-1-nw { background-position: -112px 0; }
349.ui-icon-carat-2-n-s { background-position: -128px 0; }
350.ui-icon-carat-2-e-w { background-position: -144px 0; }
351.ui-icon-triangle-1-n { background-position: 0 -16px; }
352.ui-icon-triangle-1-ne { background-position: -16px -16px; }
353.ui-icon-triangle-1-e { background-position: -32px -16px; }
354.ui-icon-triangle-1-se { background-position: -48px -16px; }
355.ui-icon-triangle-1-s { background-position: -64px -16px; }
356.ui-icon-triangle-1-sw { background-position: -80px -16px; }
357.ui-icon-triangle-1-w { background-position: -96px -16px; }
358.ui-icon-triangle-1-nw { background-position: -112px -16px; }
359.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
360.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
361.ui-icon-arrow-1-n { background-position: 0 -32px; }
362.ui-icon-arrow-1-ne { background-position: -16px -32px; }
363.ui-icon-arrow-1-e { background-position: -32px -32px; }
364.ui-icon-arrow-1-se { background-position: -48px -32px; }
365.ui-icon-arrow-1-s { background-position: -64px -32px; }
366.ui-icon-arrow-1-sw { background-position: -80px -32px; }
367.ui-icon-arrow-1-w { background-position: -96px -32px; }
368.ui-icon-arrow-1-nw { background-position: -112px -32px; }
369.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
370.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
371.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
372.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
373.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
374.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
375.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
376.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
377.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
378.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
379.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
380.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
381.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
382.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
383.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
384.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
385.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
386.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
387.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
388.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
389.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
390.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
391.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
392.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
393.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
394.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
395.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
396.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
397.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
398.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
399.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
400.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
401.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
402.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
403.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
404.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
405.ui-icon-arrow-4 { background-position: 0 -80px; }
406.ui-icon-arrow-4-diag { background-position: -16px -80px; }
407.ui-icon-extlink { background-position: -32px -80px; }
408.ui-icon-newwin { background-position: -48px -80px; }
409.ui-icon-refresh { background-position: -64px -80px; }
410.ui-icon-shuffle { background-position: -80px -80px; }
411.ui-icon-transfer-e-w { background-position: -96px -80px; }
412.ui-icon-transferthick-e-w { background-position: -112px -80px; }
413.ui-icon-folder-collapsed { background-position: 0 -96px; }
414.ui-icon-folder-open { background-position: -16px -96px; }
415.ui-icon-document { background-position: -32px -96px; }
416.ui-icon-document-b { background-position: -48px -96px; }
417.ui-icon-note { background-position: -64px -96px; }
418.ui-icon-mail-closed { background-position: -80px -96px; }
419.ui-icon-mail-open { background-position: -96px -96px; }
420.ui-icon-suitcase { background-position: -112px -96px; }
421.ui-icon-comment { background-position: -128px -96px; }
422.ui-icon-person { background-position: -144px -96px; }
423.ui-icon-print { background-position: -160px -96px; }
424.ui-icon-trash { background-position: -176px -96px; }
425.ui-icon-locked { background-position: -192px -96px; }
426.ui-icon-unlocked { background-position: -208px -96px; }
427.ui-icon-bookmark { background-position: -224px -96px; }
428.ui-icon-tag { background-position: -240px -96px; }
429.ui-icon-home { background-position: 0 -112px; }
430.ui-icon-flag { background-position: -16px -112px; }
431.ui-icon-calendar { background-position: -32px -112px; }
432.ui-icon-cart { background-position: -48px -112px; }
433.ui-icon-pencil { background-position: -64px -112px; }
434.ui-icon-clock { background-position: -80px -112px; }
435.ui-icon-disk { background-position: -96px -112px; }
436.ui-icon-calculator { background-position: -112px -112px; }
437.ui-icon-zoomin { background-position: -128px -112px; }
438.ui-icon-zoomout { background-position: -144px -112px; }
439.ui-icon-search { background-position: -160px -112px; }
440.ui-icon-wrench { background-position: -176px -112px; }
441.ui-icon-gear { background-position: -192px -112px; }
442.ui-icon-heart { background-position: -208px -112px; }
443.ui-icon-star { background-position: -224px -112px; }
444.ui-icon-link { background-position: -240px -112px; }
445.ui-icon-cancel { background-position: 0 -128px; }
446.ui-icon-plus { background-position: -16px -128px; }
447.ui-icon-plusthick { background-position: -32px -128px; }
448.ui-icon-minus { background-position: -48px -128px; }
449.ui-icon-minusthick { background-position: -64px -128px; }
450.ui-icon-close { background-position: -80px -128px; }
451.ui-icon-closethick { background-position: -96px -128px; }
452.ui-icon-key { background-position: -112px -128px; }
453.ui-icon-lightbulb { background-position: -128px -128px; }
454.ui-icon-scissors { background-position: -144px -128px; }
455.ui-icon-clipboard { background-position: -160px -128px; }
456.ui-icon-copy { background-position: -176px -128px; }
457.ui-icon-contact { background-position: -192px -128px; }
458.ui-icon-image { background-position: -208px -128px; }
459.ui-icon-video { background-position: -224px -128px; }
460.ui-icon-script { background-position: -240px -128px; }
461.ui-icon-alert { background-position: 0 -144px; }
462.ui-icon-info { background-position: -16px -144px; }
463.ui-icon-notice { background-position: -32px -144px; }
464.ui-icon-help { background-position: -48px -144px; }
465.ui-icon-check { background-position: -64px -144px; }
466.ui-icon-bullet { background-position: -80px -144px; }
467.ui-icon-radio-on { background-position: -96px -144px; }
468.ui-icon-radio-off { background-position: -112px -144px; }
469.ui-icon-pin-w { background-position: -128px -144px; }
470.ui-icon-pin-s { background-position: -144px -144px; }
471.ui-icon-play { background-position: 0 -160px; }
472.ui-icon-pause { background-position: -16px -160px; }
473.ui-icon-seek-next { background-position: -32px -160px; }
474.ui-icon-seek-prev { background-position: -48px -160px; }
475.ui-icon-seek-end { background-position: -64px -160px; }
476.ui-icon-seek-start { background-position: -80px -160px; }
477/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
478.ui-icon-seek-first { background-position: -80px -160px; }
479.ui-icon-stop { background-position: -96px -160px; }
480.ui-icon-eject { background-position: -112px -160px; }
481.ui-icon-volume-off { background-position: -128px -160px; }
482.ui-icon-volume-on { background-position: -144px -160px; }
483.ui-icon-power { background-position: 0 -176px; }
484.ui-icon-signal-diag { background-position: -16px -176px; }
485.ui-icon-signal { background-position: -32px -176px; }
486.ui-icon-battery-0 { background-position: -48px -176px; }
487.ui-icon-battery-1 { background-position: -64px -176px; }
488.ui-icon-battery-2 { background-position: -80px -176px; }
489.ui-icon-battery-3 { background-position: -96px -176px; }
490.ui-icon-circle-plus { background-position: 0 -192px; }
491.ui-icon-circle-minus { background-position: -16px -192px; }
492.ui-icon-circle-close { background-position: -32px -192px; }
493.ui-icon-circle-triangle-e { background-position: -48px -192px; }
494.ui-icon-circle-triangle-s { background-position: -64px -192px; }
495.ui-icon-circle-triangle-w { background-position: -80px -192px; }
496.ui-icon-circle-triangle-n { background-position: -96px -192px; }
497.ui-icon-circle-arrow-e { background-position: -112px -192px; }
498.ui-icon-circle-arrow-s { background-position: -128px -192px; }
499.ui-icon-circle-arrow-w { background-position: -144px -192px; }
500.ui-icon-circle-arrow-n { background-position: -160px -192px; }
501.ui-icon-circle-zoomin { background-position: -176px -192px; }
502.ui-icon-circle-zoomout { background-position: -192px -192px; }
503.ui-icon-circle-check { background-position: -208px -192px; }
504.ui-icon-circlesmall-plus { background-position: 0 -208px; }
505.ui-icon-circlesmall-minus { background-position: -16px -208px; }
506.ui-icon-circlesmall-close { background-position: -32px -208px; }
507.ui-icon-squaresmall-plus { background-position: -48px -208px; }
508.ui-icon-squaresmall-minus { background-position: -64px -208px; }
509.ui-icon-squaresmall-close { background-position: -80px -208px; }
510.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
511.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
512.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
513.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
514.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
515.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
516
517
518/* Misc visuals
519----------------------------------*/
520
521/* Corner radius */
522.ui-corner-all,
523.ui-corner-top,
524.ui-corner-left,
525.ui-corner-tl {
526 border-top-left-radius: 4px;
527}
528.ui-corner-all,
529.ui-corner-top,
530.ui-corner-right,
531.ui-corner-tr {
532 border-top-right-radius: 4px;
533}
534.ui-corner-all,
535.ui-corner-bottom,
536.ui-corner-left,
537.ui-corner-bl {
538 border-bottom-left-radius: 4px;
539}
540.ui-corner-all,
541.ui-corner-bottom,
542.ui-corner-right,
543.ui-corner-br {
544 border-bottom-right-radius: 4px;
545}
546
547/* Overlays */
548.ui-widget-overlay {
549 background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;
550 opacity: .3;
551 filter: Alpha(Opacity=30);
552}
553.ui-widget-shadow {
554 margin: -8px 0 0 -8px;
555 padding: 8px;
556 background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;
557 opacity: .3;
558 filter: Alpha(Opacity=30);
559 border-radius: 8px;
560}
diff --git a/themes/default/css/jquery-ui-1.10.4.custom.min.css b/themes/default/css/jquery-ui-1.10.4.custom.min.css
new file mode 100644
index 00000000..4dba92a8
--- /dev/null
+++ b/themes/default/css/jquery-ui-1.10.4.custom.min.css
@@ -0,0 +1,7 @@
1/*! jQuery UI - v1.10.4 - 2014-03-09
2* http://jqueryui.com
3* Includes: jquery.ui.core.css, jquery.ui.autocomplete.css, jquery.ui.menu.css, jquery.ui.theme.css
4* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
5* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
6
7.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} \ No newline at end of file
diff --git a/themes/default/css/style-default.css b/themes/default/css/style-default.css
index 9dc7c0b0..889a9f5c 100644..100755
--- a/themes/default/css/style-default.css
+++ b/themes/default/css/style-default.css
@@ -52,4 +52,8 @@ a.link span {
52 52
53a.bad-display span { 53a.bad-display span {
54 background-image: url('../img/default/bad-display.png'); 54 background-image: url('../img/default/bad-display.png');
55}
56
57a.print span {
58 background-image: url('../img/default/print.png');
55} \ No newline at end of file 59} \ No newline at end of file
diff --git a/themes/default/css/style.css b/themes/default/css/style.css
index 28675907..e58ef81a 100644..100755
--- a/themes/default/css/style.css
+++ b/themes/default/css/style.css
@@ -331,7 +331,8 @@ a.email span,
331a.delete span, 331a.delete span,
332a.link span, 332a.link span,
333a.bad-display span, 333a.bad-display span,
334a.reading-time span { 334a.reading-time span,
335a.print span {
335 background-repeat: no-repeat; 336 background-repeat: no-repeat;
336} 337}
337 338
@@ -347,3 +348,85 @@ a.reading-time span {
347 margin-left: -30px; 348 margin-left: -30px;
348} 349}
349 350
351.two-column {
352 display: block;
353 width: 50%;
354 paddig-right: 20px;
355 float: left;
356 vertical-align: top;
357}
358
359
360/* ==========================================================================
361 "save a link" popup div related styles
362 ========================================================================== */
363
364#bagit-form {
365 display: none;
366 padding-left: 30px;
367 width: 450px;
368
369}
370
371a#bagit-form-close {
372 color: #FFF;
373 display: inline-block;
374 float: right;
375 background: url("../img/messages/close.png") no-repeat scroll 0 0 rgba(0, 0, 0, 0);
376 height: 16px;
377 margin: -14px -8px 0 0;
378 width: 16px;
379 text-decoration: none;
380}
381
382
383.add-to-wallabag-link-after {
384 background-color: #000;
385 color: #fff;
386 padding: 0 4px 1px 3px;
387 font-weight: bold;
388 font-size: 0.7em;
389 border-radius: 4px;
390}
391.add-to-wallabag-link-after:hover, .add-to-wallabag-link-after:active {
392 color: #fff;
393}
394.add-to-wallabag-link-after:visited {
395 color: #999;
396}
397a.add-to-wallabag-link-after {
398 visibility: hidden;
399 position: absolute;
400 opacity: 0;
401 transition-duration: 2s;
402 transition-timing-function: ease-out;
403}
404#article article a:hover + a.add-to-wallabag-link-after, a.add-to-wallabag-link-after:hover {
405 opacity: 1;
406 visibility: visible;
407 transition-duration: .3s;
408 transition-timing-function: ease-in;
409}
410a.add-to-wallabag-link-after:after {
411 content: "w";
412}
413
414
415#add-link-result {
416 display: inline;
417 padding-left: 10px;
418}
419
420.opacity03 {
421 /*opacity: 0.3;*/
422}
423
424#readLeftPercent {
425 display: inline-block;
426 /* Show textual content */
427 overflow: visible;
428 text-align: left;
429 text-indent: 0;
430 color: black;
431 width: 50px;
432} \ No newline at end of file
diff --git a/themes/default/edit-tags.twig b/themes/default/edit-tags.twig
index 83f04aa0..bd498d26 100644..100755
--- a/themes/default/edit-tags.twig
+++ b/themes/default/edit-tags.twig
@@ -5,6 +5,10 @@
5{% endblock %} 5{% endblock %}
6{% block content %} 6{% block content %}
7 7
8<script src="{{ poche_url }}themes/default/js/jquery-ui-1.10.4.custom.min.js"></script>
9<script src="{{ poche_url }}themes/default/js/autoCompleteTags.js"></script>
10<link rel="stylesheet" href="{{ poche_url }}themes/default/css/jquery-ui-1.10.4.custom.min.css" media="all">
11
8<div id="article"> 12<div id="article">
9 <header class="mbm"> 13 <header class="mbm">
10 <h1>{{ entry.title|raw }}</h1> 14 <h1>{{ entry.title|raw }}</h1>
@@ -17,13 +21,15 @@ no tags
17<ul> 21<ul>
18{% for tag in tags %}<li>{{ tag.value }} <a href="./?action=remove_tag&amp;tag_id={{ tag.id }}&amp;id={{ entry_id }}">✘</a></li>{% endfor %} 22{% for tag in tags %}<li>{{ tag.value }} <a href="./?action=remove_tag&amp;tag_id={{ tag.id }}&amp;id={{ entry_id }}">✘</a></li>{% endfor %}
19</ul> 23</ul>
20<form method="post" action="./?action=add_tag"> 24<form method="post" action="./?action=add_tag" id="editTags">
21 <input type="hidden" name="entry_id" value="{{ entry_id }}" /> 25 <input type="hidden" name="entry_id" value="{{ entry_id }}" />
22 <label for="value">Add tags: </label> 26 <label for="value">Add tags: </label>
23 <input type="text" placeholder="interview, editorial, video" id="value" name="value" required="required" /> 27 <input type="text" placeholder="interview, editorial, video" id="value" name="value" required="required" />
24 <input type="submit" value="Tag" /> 28 <input type="submit" value="Tag" />
25 <p>{% trans "You can enter multiple tags, separated by commas." %}</p> 29 <p>{% trans "Start typing for auto complete." %}<br>
30 {% trans "You can enter multiple tags, separated by commas." %}</p>
26 31
27</form> 32</form>
33<br>
28<a href="./?view=view&id={{ entry_id }}">&laquo; {% trans "return to article" %}</a> 34<a href="./?view=view&id={{ entry_id }}">&laquo; {% trans "return to article" %}</a>
29{% endblock %} 35{% endblock %}
diff --git a/themes/default/home.twig b/themes/default/home.twig
index 165fecc6..e6c781f5 100644..100755
--- a/themes/default/home.twig
+++ b/themes/default/home.twig
@@ -12,14 +12,15 @@
12{% include '_menu.twig' %} 12{% include '_menu.twig' %}
13{% endblock %} 13{% endblock %}
14{% block precontent %} 14{% block precontent %}
15 {% if entries|length > 1 %} 15
16 <ul id="sort"> 16{% include '_sorting.twig' %}
17 <li><a href="./?sort=ia&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/top.png" alt="{% trans "by date asc" %}" title="{% trans "by date asc" %}" /></a> {% trans "by date" %} <a href="./?sort=id&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/down.png" alt="{% trans "by date desc" %}" title="{% trans "by date desc" %}" /></a></li>
18 <li><a href="./?sort=ta&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/top.png" alt="{% trans "by title asc" %}" title="{% trans "by title asc" %}" /></a> {% trans "by title" %} <a href="./?sort=td&amp;view={{ view }}&amp;id={{ id }}"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/down.png" alt="{% trans "by title desc" %}" title="{% trans "by title desc" %}" /></a></li>
19 </ul>
20 {% endif %}
21{% endblock %} 17{% endblock %}
22{% block content %} 18{% block content %}
19
20 {% if includeImport %}
21 {% include '_import.twig' %}
22 {% endif %}
23
23 {% if tag %} 24 {% if tag %}
24 <h3>{% trans "Tag" %}: <b>{{ tag.value }}</b></h3> 25 <h3>{% trans "Tag" %}: <b>{{ tag.value }}</b></h3>
25 {% endif %} 26 {% endif %}
@@ -30,9 +31,15 @@
30 {% block pager %} 31 {% block pager %}
31 {% if nb_results > 1 %} 32 {% if nb_results > 1 %}
32 <div class="results"> 33 <div class="results">
33 <div class="nb-results">{{ nb_results }} {% trans "results" %}</div> 34 <div class="nb-results">{{ nb_results }} {% trans "results" %}{% if search_term is defined %}{% trans " found for « " %} {{ search_term }} »{% endif %}</div>
34 {{ page_links | raw }} 35 {{ page_links | raw }}
35 </div> 36 </div>
37 {% elseif nb_results == 1 %}
38 {% if search_term is defined %}
39 <div class="results">
40 <div class="nb-results">{% trans "Only one result found for " %} « {{ search_term }} »</div>
41 </div>
42 {% endif %}
36 {% endif %} 43 {% endif %}
37 {% endblock %} 44 {% endblock %}
38 {% for entry in entries %} 45 {% for entry in entries %}
@@ -48,7 +55,14 @@
48 <p>{{ entry.content|striptags|slice(0, 300) }}...</p> 55 <p>{{ entry.content|striptags|slice(0, 300) }}...</p>
49 </div> 56 </div>
50 {% endfor %} 57 {% endfor %}
51 {% endif %} 58
52 {{ block('pager') }} 59 {{ block('pager') }}
60
53 {% if view == 'home' %}{% if nb_results > 1 %}<a title="{% trans "mark all the entries as read" %}" href="./?action=archive_all">{% trans "mark all the entries as read" %}</a>{% endif %}{% endif %} 61 {% if view == 'home' %}{% if nb_results > 1 %}<a title="{% trans "mark all the entries as read" %}" href="./?action=archive_all">{% trans "mark all the entries as read" %}</a>{% endif %}{% endif %}
62
63 {% if tag %}<a title="{% trans "Download the articles from this tag in an epub" %}" href="./?epub&amp;method=tag&amp;tag={{ tag.value }}">{% trans "Download the articles from this tag in an epub" %}</a>
64 {% elseif search_term is defined %}<a title="{% trans "Download the articles from this search in an epub" %}" href="./?epub&amp;method=search&amp;search={{ search_term }}">{% trans "Download the articles from this search in an epub" %}</a>
65 {% else %}<a title="{% trans "Download the articles from this category in an epub" %}" href="./?epub&amp;method=category&amp;category={{ view }}">{% trans "Download the articles from this category in an epub" %}</a>{% endif %}
66
67 {% endif %}
54{% endblock %} 68{% endblock %}
diff --git a/themes/default/img/default/print.png b/themes/default/img/default/print.png
new file mode 100755
index 00000000..83d6445b
--- /dev/null
+++ b/themes/default/img/default/print.png
Binary files differ
diff --git a/themes/default/js/autoCompleteTags.js b/themes/default/js/autoCompleteTags.js
new file mode 100755
index 00000000..90bc982c
--- /dev/null
+++ b/themes/default/js/autoCompleteTags.js
@@ -0,0 +1,47 @@
1jQuery(function($) {
2
3 function split( val ) {
4 return val.split( /,\s*/ );
5 }
6 function extractLast( term ) {
7 return split( term ).pop();
8 }
9
10
11 $("#value").bind("keydown", function(event) {
12 if (event.keyCode === $.ui.keyCode.TAB && $(this).data("ui-autocomplete").menu.active) {
13 event.preventDefault();
14 }
15 }).autocomplete({
16 source : function(request, response) {
17 $.getJSON("./?view=tags", {
18 term : extractLast(request.term),
19 //id: $(':hidden#entry_id').val()
20 }, response);
21 },
22 search : function() {
23 // custom minLength
24 var term = extractLast(this.value);
25 if (term.length < 1) {
26 return false;
27 }
28 },
29 focus : function() {
30 // prevent value inserted on focus
31 return false;
32 },
33 select : function(event, ui) {
34 var terms = split(this.value);
35 // remove the current input
36 terms.pop();
37 // add the selected item
38 terms.push(ui.item.value);
39 // add placeholder to get the comma-and-space at the end
40 terms.push("");
41 this.value = terms.join(", ");
42 return false;
43 }
44 });
45
46
47});
diff --git a/themes/default/js/jquery-ui-1.10.4.custom.js b/themes/default/js/jquery-ui-1.10.4.custom.js
new file mode 100644
index 00000000..6f599fca
--- /dev/null
+++ b/themes/default/js/jquery-ui-1.10.4.custom.js
@@ -0,0 +1,2519 @@
1/*! jQuery UI - v1.10.4 - 2014-03-08
2* http://jqueryui.com
3* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.position.js, jquery.ui.autocomplete.js, jquery.ui.menu.js
4* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
5
6(function( $, undefined ) {
7
8var uuid = 0,
9 runiqueId = /^ui-id-\d+$/;
10
11// $.ui might exist from components with no dependencies, e.g., $.ui.position
12$.ui = $.ui || {};
13
14$.extend( $.ui, {
15 version: "1.10.4",
16
17 keyCode: {
18 BACKSPACE: 8,
19 COMMA: 188,
20 DELETE: 46,
21 DOWN: 40,
22 END: 35,
23 ENTER: 13,
24 ESCAPE: 27,
25 HOME: 36,
26 LEFT: 37,
27 NUMPAD_ADD: 107,
28 NUMPAD_DECIMAL: 110,
29 NUMPAD_DIVIDE: 111,
30 NUMPAD_ENTER: 108,
31 NUMPAD_MULTIPLY: 106,
32 NUMPAD_SUBTRACT: 109,
33 PAGE_DOWN: 34,
34 PAGE_UP: 33,
35 PERIOD: 190,
36 RIGHT: 39,
37 SPACE: 32,
38 TAB: 9,
39 UP: 38
40 }
41});
42
43// plugins
44$.fn.extend({
45 focus: (function( orig ) {
46 return function( delay, fn ) {
47 return typeof delay === "number" ?
48 this.each(function() {
49 var elem = this;
50 setTimeout(function() {
51 $( elem ).focus();
52 if ( fn ) {
53 fn.call( elem );
54 }
55 }, delay );
56 }) :
57 orig.apply( this, arguments );
58 };
59 })( $.fn.focus ),
60
61 scrollParent: function() {
62 var scrollParent;
63 if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) {
64 scrollParent = this.parents().filter(function() {
65 return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x"));
66 }).eq(0);
67 } else {
68 scrollParent = this.parents().filter(function() {
69 return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x"));
70 }).eq(0);
71 }
72
73 return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent;
74 },
75
76 zIndex: function( zIndex ) {
77 if ( zIndex !== undefined ) {
78 return this.css( "zIndex", zIndex );
79 }
80
81 if ( this.length ) {
82 var elem = $( this[ 0 ] ), position, value;
83 while ( elem.length && elem[ 0 ] !== document ) {
84 // Ignore z-index if position is set to a value where z-index is ignored by the browser
85 // This makes behavior of this function consistent across browsers
86 // WebKit always returns auto if the element is positioned
87 position = elem.css( "position" );
88 if ( position === "absolute" || position === "relative" || position === "fixed" ) {
89 // IE returns 0 when zIndex is not specified
90 // other browsers return a string
91 // we ignore the case of nested elements with an explicit value of 0
92 // <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
93 value = parseInt( elem.css( "zIndex" ), 10 );
94 if ( !isNaN( value ) && value !== 0 ) {
95 return value;
96 }
97 }
98 elem = elem.parent();
99 }
100 }
101
102 return 0;
103 },
104
105 uniqueId: function() {
106 return this.each(function() {
107 if ( !this.id ) {
108 this.id = "ui-id-" + (++uuid);
109 }
110 });
111 },
112
113 removeUniqueId: function() {
114 return this.each(function() {
115 if ( runiqueId.test( this.id ) ) {
116 $( this ).removeAttr( "id" );
117 }
118 });
119 }
120});
121
122// selectors
123function focusable( element, isTabIndexNotNaN ) {
124 var map, mapName, img,
125 nodeName = element.nodeName.toLowerCase();
126 if ( "area" === nodeName ) {
127 map = element.parentNode;
128 mapName = map.name;
129 if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
130 return false;
131 }
132 img = $( "img[usemap=#" + mapName + "]" )[0];
133 return !!img && visible( img );
134 }
135 return ( /input|select|textarea|button|object/.test( nodeName ) ?
136 !element.disabled :
137 "a" === nodeName ?
138 element.href || isTabIndexNotNaN :
139 isTabIndexNotNaN) &&
140 // the element and all of its ancestors must be visible
141 visible( element );
142}
143
144function visible( element ) {
145 return $.expr.filters.visible( element ) &&
146 !$( element ).parents().addBack().filter(function() {
147 return $.css( this, "visibility" ) === "hidden";
148 }).length;
149}
150
151$.extend( $.expr[ ":" ], {
152 data: $.expr.createPseudo ?
153 $.expr.createPseudo(function( dataName ) {
154 return function( elem ) {
155 return !!$.data( elem, dataName );
156 };
157 }) :
158 // support: jQuery <1.8
159 function( elem, i, match ) {
160 return !!$.data( elem, match[ 3 ] );
161 },
162
163 focusable: function( element ) {
164 return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) );
165 },
166
167 tabbable: function( element ) {
168 var tabIndex = $.attr( element, "tabindex" ),
169 isTabIndexNaN = isNaN( tabIndex );
170 return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN );
171 }
172});
173
174// support: jQuery <1.8
175if ( !$( "<a>" ).outerWidth( 1 ).jquery ) {
176 $.each( [ "Width", "Height" ], function( i, name ) {
177 var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
178 type = name.toLowerCase(),
179 orig = {
180 innerWidth: $.fn.innerWidth,
181 innerHeight: $.fn.innerHeight,
182 outerWidth: $.fn.outerWidth,
183 outerHeight: $.fn.outerHeight
184 };
185
186 function reduce( elem, size, border, margin ) {
187 $.each( side, function() {
188 size -= parseFloat( $.css( elem, "padding" + this ) ) || 0;
189 if ( border ) {
190 size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0;
191 }
192 if ( margin ) {
193 size -= parseFloat( $.css( elem, "margin" + this ) ) || 0;
194 }
195 });
196 return size;
197 }
198
199 $.fn[ "inner" + name ] = function( size ) {
200 if ( size === undefined ) {
201 return orig[ "inner" + name ].call( this );
202 }
203
204 return this.each(function() {
205 $( this ).css( type, reduce( this, size ) + "px" );
206 });
207 };
208
209 $.fn[ "outer" + name] = function( size, margin ) {
210 if ( typeof size !== "number" ) {
211 return orig[ "outer" + name ].call( this, size );
212 }
213
214 return this.each(function() {
215 $( this).css( type, reduce( this, size, true, margin ) + "px" );
216 });
217 };
218 });
219}
220
221// support: jQuery <1.8
222if ( !$.fn.addBack ) {
223 $.fn.addBack = function( selector ) {
224 return this.add( selector == null ?
225 this.prevObject : this.prevObject.filter( selector )
226 );
227 };
228}
229
230// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413)
231if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) {
232 $.fn.removeData = (function( removeData ) {
233 return function( key ) {
234 if ( arguments.length ) {
235 return removeData.call( this, $.camelCase( key ) );
236 } else {
237 return removeData.call( this );
238 }
239 };
240 })( $.fn.removeData );
241}
242
243
244
245
246
247// deprecated
248$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() );
249
250$.support.selectstart = "onselectstart" in document.createElement( "div" );
251$.fn.extend({
252 disableSelection: function() {
253 return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) +
254 ".ui-disableSelection", function( event ) {
255 event.preventDefault();
256 });
257 },
258
259 enableSelection: function() {
260 return this.unbind( ".ui-disableSelection" );
261 }
262});
263
264$.extend( $.ui, {
265 // $.ui.plugin is deprecated. Use $.widget() extensions instead.
266 plugin: {
267 add: function( module, option, set ) {
268 var i,
269 proto = $.ui[ module ].prototype;
270 for ( i in set ) {
271 proto.plugins[ i ] = proto.plugins[ i ] || [];
272 proto.plugins[ i ].push( [ option, set[ i ] ] );
273 }
274 },
275 call: function( instance, name, args ) {
276 var i,
277 set = instance.plugins[ name ];
278 if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) {
279 return;
280 }
281
282 for ( i = 0; i < set.length; i++ ) {
283 if ( instance.options[ set[ i ][ 0 ] ] ) {
284 set[ i ][ 1 ].apply( instance.element, args );
285 }
286 }
287 }
288 },
289
290 // only used by resizable
291 hasScroll: function( el, a ) {
292
293 //If overflow is hidden, the element might have extra content, but the user wants to hide it
294 if ( $( el ).css( "overflow" ) === "hidden") {
295 return false;
296 }
297
298 var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop",
299 has = false;
300
301 if ( el[ scroll ] > 0 ) {
302 return true;
303 }
304
305 // TODO: determine which cases actually cause this to happen
306 // if the element doesn't have the scroll set, see if it's possible to
307 // set the scroll
308 el[ scroll ] = 1;
309 has = ( el[ scroll ] > 0 );
310 el[ scroll ] = 0;
311 return has;
312 }
313});
314
315})( jQuery );
316(function( $, undefined ) {
317
318var uuid = 0,
319 slice = Array.prototype.slice,
320 _cleanData = $.cleanData;
321$.cleanData = function( elems ) {
322 for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
323 try {
324 $( elem ).triggerHandler( "remove" );
325 // http://bugs.jquery.com/ticket/8235
326 } catch( e ) {}
327 }
328 _cleanData( elems );
329};
330
331$.widget = function( name, base, prototype ) {
332 var fullName, existingConstructor, constructor, basePrototype,
333 // proxiedPrototype allows the provided prototype to remain unmodified
334 // so that it can be used as a mixin for multiple widgets (#8876)
335 proxiedPrototype = {},
336 namespace = name.split( "." )[ 0 ];
337
338 name = name.split( "." )[ 1 ];
339 fullName = namespace + "-" + name;
340
341 if ( !prototype ) {
342 prototype = base;
343 base = $.Widget;
344 }
345
346 // create selector for plugin
347 $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
348 return !!$.data( elem, fullName );
349 };
350
351 $[ namespace ] = $[ namespace ] || {};
352 existingConstructor = $[ namespace ][ name ];
353 constructor = $[ namespace ][ name ] = function( options, element ) {
354 // allow instantiation without "new" keyword
355 if ( !this._createWidget ) {
356 return new constructor( options, element );
357 }
358
359 // allow instantiation without initializing for simple inheritance
360 // must use "new" keyword (the code above always passes args)
361 if ( arguments.length ) {
362 this._createWidget( options, element );
363 }
364 };
365 // extend with the existing constructor to carry over any static properties
366 $.extend( constructor, existingConstructor, {
367 version: prototype.version,
368 // copy the object used to create the prototype in case we need to
369 // redefine the widget later
370 _proto: $.extend( {}, prototype ),
371 // track widgets that inherit from this widget in case this widget is
372 // redefined after a widget inherits from it
373 _childConstructors: []
374 });
375
376 basePrototype = new base();
377 // we need to make the options hash a property directly on the new instance
378 // otherwise we'll modify the options hash on the prototype that we're
379 // inheriting from
380 basePrototype.options = $.widget.extend( {}, basePrototype.options );
381 $.each( prototype, function( prop, value ) {
382 if ( !$.isFunction( value ) ) {
383 proxiedPrototype[ prop ] = value;
384 return;
385 }
386 proxiedPrototype[ prop ] = (function() {
387 var _super = function() {
388 return base.prototype[ prop ].apply( this, arguments );
389 },
390 _superApply = function( args ) {
391 return base.prototype[ prop ].apply( this, args );
392 };
393 return function() {
394 var __super = this._super,
395 __superApply = this._superApply,
396 returnValue;
397
398 this._super = _super;
399 this._superApply = _superApply;
400
401 returnValue = value.apply( this, arguments );
402
403 this._super = __super;
404 this._superApply = __superApply;
405
406 return returnValue;
407 };
408 })();
409 });
410 constructor.prototype = $.widget.extend( basePrototype, {
411 // TODO: remove support for widgetEventPrefix
412 // always use the name + a colon as the prefix, e.g., draggable:start
413 // don't prefix for widgets that aren't DOM-based
414 widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name
415 }, proxiedPrototype, {
416 constructor: constructor,
417 namespace: namespace,
418 widgetName: name,
419 widgetFullName: fullName
420 });
421
422 // If this widget is being redefined then we need to find all widgets that
423 // are inheriting from it and redefine all of them so that they inherit from
424 // the new version of this widget. We're essentially trying to replace one
425 // level in the prototype chain.
426 if ( existingConstructor ) {
427 $.each( existingConstructor._childConstructors, function( i, child ) {
428 var childPrototype = child.prototype;
429
430 // redefine the child widget using the same prototype that was
431 // originally used, but inherit from the new version of the base
432 $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
433 });
434 // remove the list of existing child constructors from the old constructor
435 // so the old child constructors can be garbage collected
436 delete existingConstructor._childConstructors;
437 } else {
438 base._childConstructors.push( constructor );
439 }
440
441 $.widget.bridge( name, constructor );
442};
443
444$.widget.extend = function( target ) {
445 var input = slice.call( arguments, 1 ),
446 inputIndex = 0,
447 inputLength = input.length,
448 key,
449 value;
450 for ( ; inputIndex < inputLength; inputIndex++ ) {
451 for ( key in input[ inputIndex ] ) {
452 value = input[ inputIndex ][ key ];
453 if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
454 // Clone objects
455 if ( $.isPlainObject( value ) ) {
456 target[ key ] = $.isPlainObject( target[ key ] ) ?
457 $.widget.extend( {}, target[ key ], value ) :
458 // Don't extend strings, arrays, etc. with objects
459 $.widget.extend( {}, value );
460 // Copy everything else by reference
461 } else {
462 target[ key ] = value;
463 }
464 }
465 }
466 }
467 return target;
468};
469
470$.widget.bridge = function( name, object ) {
471 var fullName = object.prototype.widgetFullName || name;
472 $.fn[ name ] = function( options ) {
473 var isMethodCall = typeof options === "string",
474 args = slice.call( arguments, 1 ),
475 returnValue = this;
476
477 // allow multiple hashes to be passed on init
478 options = !isMethodCall && args.length ?
479 $.widget.extend.apply( null, [ options ].concat(args) ) :
480 options;
481
482 if ( isMethodCall ) {
483 this.each(function() {
484 var methodValue,
485 instance = $.data( this, fullName );
486 if ( !instance ) {
487 return $.error( "cannot call methods on " + name + " prior to initialization; " +
488 "attempted to call method '" + options + "'" );
489 }
490 if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
491 return $.error( "no such method '" + options + "' for " + name + " widget instance" );
492 }
493 methodValue = instance[ options ].apply( instance, args );
494 if ( methodValue !== instance && methodValue !== undefined ) {
495 returnValue = methodValue && methodValue.jquery ?
496 returnValue.pushStack( methodValue.get() ) :
497 methodValue;
498 return false;
499 }
500 });
501 } else {
502 this.each(function() {
503 var instance = $.data( this, fullName );
504 if ( instance ) {
505 instance.option( options || {} )._init();
506 } else {
507 $.data( this, fullName, new object( options, this ) );
508 }
509 });
510 }
511
512 return returnValue;
513 };
514};
515
516$.Widget = function( /* options, element */ ) {};
517$.Widget._childConstructors = [];
518
519$.Widget.prototype = {
520 widgetName: "widget",
521 widgetEventPrefix: "",
522 defaultElement: "<div>",
523 options: {
524 disabled: false,
525
526 // callbacks
527 create: null
528 },
529 _createWidget: function( options, element ) {
530 element = $( element || this.defaultElement || this )[ 0 ];
531 this.element = $( element );
532 this.uuid = uuid++;
533 this.eventNamespace = "." + this.widgetName + this.uuid;
534 this.options = $.widget.extend( {},
535 this.options,
536 this._getCreateOptions(),
537 options );
538
539 this.bindings = $();
540 this.hoverable = $();
541 this.focusable = $();
542
543 if ( element !== this ) {
544 $.data( element, this.widgetFullName, this );
545 this._on( true, this.element, {
546 remove: function( event ) {
547 if ( event.target === element ) {
548 this.destroy();
549 }
550 }
551 });
552 this.document = $( element.style ?
553 // element within the document
554 element.ownerDocument :
555 // element is window or document
556 element.document || element );
557 this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
558 }
559
560 this._create();
561 this._trigger( "create", null, this._getCreateEventData() );
562 this._init();
563 },
564 _getCreateOptions: $.noop,
565 _getCreateEventData: $.noop,
566 _create: $.noop,
567 _init: $.noop,
568
569 destroy: function() {
570 this._destroy();
571 // we can probably remove the unbind calls in 2.0
572 // all event bindings should go through this._on()
573 this.element
574 .unbind( this.eventNamespace )
575 // 1.9 BC for #7810
576 // TODO remove dual storage
577 .removeData( this.widgetName )
578 .removeData( this.widgetFullName )
579 // support: jquery <1.6.3
580 // http://bugs.jquery.com/ticket/9413
581 .removeData( $.camelCase( this.widgetFullName ) );
582 this.widget()
583 .unbind( this.eventNamespace )
584 .removeAttr( "aria-disabled" )
585 .removeClass(
586 this.widgetFullName + "-disabled " +
587 "ui-state-disabled" );
588
589 // clean up events and states
590 this.bindings.unbind( this.eventNamespace );
591 this.hoverable.removeClass( "ui-state-hover" );
592 this.focusable.removeClass( "ui-state-focus" );
593 },
594 _destroy: $.noop,
595
596 widget: function() {
597 return this.element;
598 },
599
600 option: function( key, value ) {
601 var options = key,
602 parts,
603 curOption,
604 i;
605
606 if ( arguments.length === 0 ) {
607 // don't return a reference to the internal hash
608 return $.widget.extend( {}, this.options );
609 }
610
611 if ( typeof key === "string" ) {
612 // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
613 options = {};
614 parts = key.split( "." );
615 key = parts.shift();
616 if ( parts.length ) {
617 curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
618 for ( i = 0; i < parts.length - 1; i++ ) {
619 curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
620 curOption = curOption[ parts[ i ] ];
621 }
622 key = parts.pop();
623 if ( arguments.length === 1 ) {
624 return curOption[ key ] === undefined ? null : curOption[ key ];
625 }
626 curOption[ key ] = value;
627 } else {
628 if ( arguments.length === 1 ) {
629 return this.options[ key ] === undefined ? null : this.options[ key ];
630 }
631 options[ key ] = value;
632 }
633 }
634
635 this._setOptions( options );
636
637 return this;
638 },
639 _setOptions: function( options ) {
640 var key;
641
642 for ( key in options ) {
643 this._setOption( key, options[ key ] );
644 }
645
646 return this;
647 },
648 _setOption: function( key, value ) {
649 this.options[ key ] = value;
650
651 if ( key === "disabled" ) {
652 this.widget()
653 .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value )
654 .attr( "aria-disabled", value );
655 this.hoverable.removeClass( "ui-state-hover" );
656 this.focusable.removeClass( "ui-state-focus" );
657 }
658
659 return this;
660 },
661
662 enable: function() {
663 return this._setOption( "disabled", false );
664 },
665 disable: function() {
666 return this._setOption( "disabled", true );
667 },
668
669 _on: function( suppressDisabledCheck, element, handlers ) {
670 var delegateElement,
671 instance = this;
672
673 // no suppressDisabledCheck flag, shuffle arguments
674 if ( typeof suppressDisabledCheck !== "boolean" ) {
675 handlers = element;
676 element = suppressDisabledCheck;
677 suppressDisabledCheck = false;
678 }
679
680 // no element argument, shuffle and use this.element
681 if ( !handlers ) {
682 handlers = element;
683 element = this.element;
684 delegateElement = this.widget();
685 } else {
686 // accept selectors, DOM elements
687 element = delegateElement = $( element );
688 this.bindings = this.bindings.add( element );
689 }
690
691 $.each( handlers, function( event, handler ) {
692 function handlerProxy() {
693 // allow widgets to customize the disabled handling
694 // - disabled as an array instead of boolean
695 // - disabled class as method for disabling individual parts
696 if ( !suppressDisabledCheck &&
697 ( instance.options.disabled === true ||
698 $( this ).hasClass( "ui-state-disabled" ) ) ) {
699 return;
700 }
701 return ( typeof handler === "string" ? instance[ handler ] : handler )
702 .apply( instance, arguments );
703 }
704
705 // copy the guid so direct unbinding works
706 if ( typeof handler !== "string" ) {
707 handlerProxy.guid = handler.guid =
708 handler.guid || handlerProxy.guid || $.guid++;
709 }
710
711 var match = event.match( /^(\w+)\s*(.*)$/ ),
712 eventName = match[1] + instance.eventNamespace,
713 selector = match[2];
714 if ( selector ) {
715 delegateElement.delegate( selector, eventName, handlerProxy );
716 } else {
717 element.bind( eventName, handlerProxy );
718 }
719 });
720 },
721
722 _off: function( element, eventName ) {
723 eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace;
724 element.unbind( eventName ).undelegate( eventName );
725 },
726
727 _delay: function( handler, delay ) {
728 function handlerProxy() {
729 return ( typeof handler === "string" ? instance[ handler ] : handler )
730 .apply( instance, arguments );
731 }
732 var instance = this;
733 return setTimeout( handlerProxy, delay || 0 );
734 },
735
736 _hoverable: function( element ) {
737 this.hoverable = this.hoverable.add( element );
738 this._on( element, {
739 mouseenter: function( event ) {
740 $( event.currentTarget ).addClass( "ui-state-hover" );
741 },
742 mouseleave: function( event ) {
743 $( event.currentTarget ).removeClass( "ui-state-hover" );
744 }
745 });
746 },
747
748 _focusable: function( element ) {
749 this.focusable = this.focusable.add( element );
750 this._on( element, {
751 focusin: function( event ) {
752 $( event.currentTarget ).addClass( "ui-state-focus" );
753 },
754 focusout: function( event ) {
755 $( event.currentTarget ).removeClass( "ui-state-focus" );
756 }
757 });
758 },
759
760 _trigger: function( type, event, data ) {
761 var prop, orig,
762 callback = this.options[ type ];
763
764 data = data || {};
765 event = $.Event( event );
766 event.type = ( type === this.widgetEventPrefix ?
767 type :
768 this.widgetEventPrefix + type ).toLowerCase();
769 // the original event may come from any element
770 // so we need to reset the target on the new event
771 event.target = this.element[ 0 ];
772
773 // copy original event properties over to the new event
774 orig = event.originalEvent;
775 if ( orig ) {
776 for ( prop in orig ) {
777 if ( !( prop in event ) ) {
778 event[ prop ] = orig[ prop ];
779 }
780 }
781 }
782
783 this.element.trigger( event, data );
784 return !( $.isFunction( callback ) &&
785 callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
786 event.isDefaultPrevented() );
787 }
788};
789
790$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
791 $.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
792 if ( typeof options === "string" ) {
793 options = { effect: options };
794 }
795 var hasOptions,
796 effectName = !options ?
797 method :
798 options === true || typeof options === "number" ?
799 defaultEffect :
800 options.effect || defaultEffect;
801 options = options || {};
802 if ( typeof options === "number" ) {
803 options = { duration: options };
804 }
805 hasOptions = !$.isEmptyObject( options );
806 options.complete = callback;
807 if ( options.delay ) {
808 element.delay( options.delay );
809 }
810 if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
811 element[ method ]( options );
812 } else if ( effectName !== method && element[ effectName ] ) {
813 element[ effectName ]( options.duration, options.easing, callback );
814 } else {
815 element.queue(function( next ) {
816 $( this )[ method ]();
817 if ( callback ) {
818 callback.call( element[ 0 ] );
819 }
820 next();
821 });
822 }
823 };
824});
825
826})( jQuery );
827(function( $, undefined ) {
828
829$.ui = $.ui || {};
830
831var cachedScrollbarWidth,
832 max = Math.max,
833 abs = Math.abs,
834 round = Math.round,
835 rhorizontal = /left|center|right/,
836 rvertical = /top|center|bottom/,
837 roffset = /[\+\-]\d+(\.[\d]+)?%?/,
838 rposition = /^\w+/,
839 rpercent = /%$/,
840 _position = $.fn.position;
841
842function getOffsets( offsets, width, height ) {
843 return [
844 parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
845 parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
846 ];
847}
848
849function parseCss( element, property ) {
850 return parseInt( $.css( element, property ), 10 ) || 0;
851}
852
853function getDimensions( elem ) {
854 var raw = elem[0];
855 if ( raw.nodeType === 9 ) {
856 return {
857 width: elem.width(),
858 height: elem.height(),
859 offset: { top: 0, left: 0 }
860 };
861 }
862 if ( $.isWindow( raw ) ) {
863 return {
864 width: elem.width(),
865 height: elem.height(),
866 offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
867 };
868 }
869 if ( raw.preventDefault ) {
870 return {
871 width: 0,
872 height: 0,
873 offset: { top: raw.pageY, left: raw.pageX }
874 };
875 }
876 return {
877 width: elem.outerWidth(),
878 height: elem.outerHeight(),
879 offset: elem.offset()
880 };
881}
882
883$.position = {
884 scrollbarWidth: function() {
885 if ( cachedScrollbarWidth !== undefined ) {
886 return cachedScrollbarWidth;
887 }
888 var w1, w2,
889 div = $( "<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ),
890 innerDiv = div.children()[0];
891
892 $( "body" ).append( div );
893 w1 = innerDiv.offsetWidth;
894 div.css( "overflow", "scroll" );
895
896 w2 = innerDiv.offsetWidth;
897
898 if ( w1 === w2 ) {
899 w2 = div[0].clientWidth;
900 }
901
902 div.remove();
903
904 return (cachedScrollbarWidth = w1 - w2);
905 },
906 getScrollInfo: function( within ) {
907 var overflowX = within.isWindow || within.isDocument ? "" :
908 within.element.css( "overflow-x" ),
909 overflowY = within.isWindow || within.isDocument ? "" :
910 within.element.css( "overflow-y" ),
911 hasOverflowX = overflowX === "scroll" ||
912 ( overflowX === "auto" && within.width < within.element[0].scrollWidth ),
913 hasOverflowY = overflowY === "scroll" ||
914 ( overflowY === "auto" && within.height < within.element[0].scrollHeight );
915 return {
916 width: hasOverflowY ? $.position.scrollbarWidth() : 0,
917 height: hasOverflowX ? $.position.scrollbarWidth() : 0
918 };
919 },
920 getWithinInfo: function( element ) {
921 var withinElement = $( element || window ),
922 isWindow = $.isWindow( withinElement[0] ),
923 isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9;
924 return {
925 element: withinElement,
926 isWindow: isWindow,
927 isDocument: isDocument,
928 offset: withinElement.offset() || { left: 0, top: 0 },
929 scrollLeft: withinElement.scrollLeft(),
930 scrollTop: withinElement.scrollTop(),
931 width: isWindow ? withinElement.width() : withinElement.outerWidth(),
932 height: isWindow ? withinElement.height() : withinElement.outerHeight()
933 };
934 }
935};
936
937$.fn.position = function( options ) {
938 if ( !options || !options.of ) {
939 return _position.apply( this, arguments );
940 }
941
942 // make a copy, we don't want to modify arguments
943 options = $.extend( {}, options );
944
945 var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
946 target = $( options.of ),
947 within = $.position.getWithinInfo( options.within ),
948 scrollInfo = $.position.getScrollInfo( within ),
949 collision = ( options.collision || "flip" ).split( " " ),
950 offsets = {};
951
952 dimensions = getDimensions( target );
953 if ( target[0].preventDefault ) {
954 // force left top to allow flipping
955 options.at = "left top";
956 }
957 targetWidth = dimensions.width;
958 targetHeight = dimensions.height;
959 targetOffset = dimensions.offset;
960 // clone to reuse original targetOffset later
961 basePosition = $.extend( {}, targetOffset );
962
963 // force my and at to have valid horizontal and vertical positions
964 // if a value is missing or invalid, it will be converted to center
965 $.each( [ "my", "at" ], function() {
966 var pos = ( options[ this ] || "" ).split( " " ),
967 horizontalOffset,
968 verticalOffset;
969
970 if ( pos.length === 1) {
971 pos = rhorizontal.test( pos[ 0 ] ) ?
972 pos.concat( [ "center" ] ) :
973 rvertical.test( pos[ 0 ] ) ?
974 [ "center" ].concat( pos ) :
975 [ "center", "center" ];
976 }
977 pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
978 pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
979
980 // calculate offsets
981 horizontalOffset = roffset.exec( pos[ 0 ] );
982 verticalOffset = roffset.exec( pos[ 1 ] );
983 offsets[ this ] = [
984 horizontalOffset ? horizontalOffset[ 0 ] : 0,
985 verticalOffset ? verticalOffset[ 0 ] : 0
986 ];
987
988 // reduce to just the positions without the offsets
989 options[ this ] = [
990 rposition.exec( pos[ 0 ] )[ 0 ],
991 rposition.exec( pos[ 1 ] )[ 0 ]
992 ];
993 });
994
995 // normalize collision option
996 if ( collision.length === 1 ) {
997 collision[ 1 ] = collision[ 0 ];
998 }
999
1000 if ( options.at[ 0 ] === "right" ) {
1001 basePosition.left += targetWidth;
1002 } else if ( options.at[ 0 ] === "center" ) {
1003 basePosition.left += targetWidth / 2;
1004 }
1005
1006 if ( options.at[ 1 ] === "bottom" ) {
1007 basePosition.top += targetHeight;
1008 } else if ( options.at[ 1 ] === "center" ) {
1009 basePosition.top += targetHeight / 2;
1010 }
1011
1012 atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
1013 basePosition.left += atOffset[ 0 ];
1014 basePosition.top += atOffset[ 1 ];
1015
1016 return this.each(function() {
1017 var collisionPosition, using,
1018 elem = $( this ),
1019 elemWidth = elem.outerWidth(),
1020 elemHeight = elem.outerHeight(),
1021 marginLeft = parseCss( this, "marginLeft" ),
1022 marginTop = parseCss( this, "marginTop" ),
1023 collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width,
1024 collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height,
1025 position = $.extend( {}, basePosition ),
1026 myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
1027
1028 if ( options.my[ 0 ] === "right" ) {
1029 position.left -= elemWidth;
1030 } else if ( options.my[ 0 ] === "center" ) {
1031 position.left -= elemWidth / 2;
1032 }
1033
1034 if ( options.my[ 1 ] === "bottom" ) {
1035 position.top -= elemHeight;
1036 } else if ( options.my[ 1 ] === "center" ) {
1037 position.top -= elemHeight / 2;
1038 }
1039
1040 position.left += myOffset[ 0 ];
1041 position.top += myOffset[ 1 ];
1042
1043 // if the browser doesn't support fractions, then round for consistent results
1044 if ( !$.support.offsetFractions ) {
1045 position.left = round( position.left );
1046 position.top = round( position.top );
1047 }
1048
1049 collisionPosition = {
1050 marginLeft: marginLeft,
1051 marginTop: marginTop
1052 };
1053
1054 $.each( [ "left", "top" ], function( i, dir ) {
1055 if ( $.ui.position[ collision[ i ] ] ) {
1056 $.ui.position[ collision[ i ] ][ dir ]( position, {
1057 targetWidth: targetWidth,
1058 targetHeight: targetHeight,
1059 elemWidth: elemWidth,
1060 elemHeight: elemHeight,
1061 collisionPosition: collisionPosition,
1062 collisionWidth: collisionWidth,
1063 collisionHeight: collisionHeight,
1064 offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
1065 my: options.my,
1066 at: options.at,
1067 within: within,
1068 elem : elem
1069 });
1070 }
1071 });
1072
1073 if ( options.using ) {
1074 // adds feedback as second argument to using callback, if present
1075 using = function( props ) {
1076 var left = targetOffset.left - position.left,
1077 right = left + targetWidth - elemWidth,
1078 top = targetOffset.top - position.top,
1079 bottom = top + targetHeight - elemHeight,
1080 feedback = {
1081 target: {
1082 element: target,
1083 left: targetOffset.left,
1084 top: targetOffset.top,
1085 width: targetWidth,
1086 height: targetHeight
1087 },
1088 element: {
1089 element: elem,
1090 left: position.left,
1091 top: position.top,
1092 width: elemWidth,
1093 height: elemHeight
1094 },
1095 horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
1096 vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
1097 };
1098 if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
1099 feedback.horizontal = "center";
1100 }
1101 if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
1102 feedback.vertical = "middle";
1103 }
1104 if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
1105 feedback.important = "horizontal";
1106 } else {
1107 feedback.important = "vertical";
1108 }
1109 options.using.call( this, props, feedback );
1110 };
1111 }
1112
1113 elem.offset( $.extend( position, { using: using } ) );
1114 });
1115};
1116
1117$.ui.position = {
1118 fit: {
1119 left: function( position, data ) {
1120 var within = data.within,
1121 withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
1122 outerWidth = within.width,
1123 collisionPosLeft = position.left - data.collisionPosition.marginLeft,
1124 overLeft = withinOffset - collisionPosLeft,
1125 overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
1126 newOverRight;
1127
1128 // element is wider than within
1129 if ( data.collisionWidth > outerWidth ) {
1130 // element is initially over the left side of within
1131 if ( overLeft > 0 && overRight <= 0 ) {
1132 newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset;
1133 position.left += overLeft - newOverRight;
1134 // element is initially over right side of within
1135 } else if ( overRight > 0 && overLeft <= 0 ) {
1136 position.left = withinOffset;
1137 // element is initially over both left and right sides of within
1138 } else {
1139 if ( overLeft > overRight ) {
1140 position.left = withinOffset + outerWidth - data.collisionWidth;
1141 } else {
1142 position.left = withinOffset;
1143 }
1144 }
1145 // too far left -> align with left edge
1146 } else if ( overLeft > 0 ) {
1147 position.left += overLeft;
1148 // too far right -> align with right edge
1149 } else if ( overRight > 0 ) {
1150 position.left -= overRight;
1151 // adjust based on position and margin
1152 } else {
1153 position.left = max( position.left - collisionPosLeft, position.left );
1154 }
1155 },
1156 top: function( position, data ) {
1157 var within = data.within,
1158 withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
1159 outerHeight = data.within.height,
1160 collisionPosTop = position.top - data.collisionPosition.marginTop,
1161 overTop = withinOffset - collisionPosTop,
1162 overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
1163 newOverBottom;
1164
1165 // element is taller than within
1166 if ( data.collisionHeight > outerHeight ) {
1167 // element is initially over the top of within
1168 if ( overTop > 0 && overBottom <= 0 ) {
1169 newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset;
1170 position.top += overTop - newOverBottom;
1171 // element is initially over bottom of within
1172 } else if ( overBottom > 0 && overTop <= 0 ) {
1173 position.top = withinOffset;
1174 // element is initially over both top and bottom of within
1175 } else {
1176 if ( overTop > overBottom ) {
1177 position.top = withinOffset + outerHeight - data.collisionHeight;
1178 } else {
1179 position.top = withinOffset;
1180 }
1181 }
1182 // too far up -> align with top
1183 } else if ( overTop > 0 ) {
1184 position.top += overTop;
1185 // too far down -> align with bottom edge
1186 } else if ( overBottom > 0 ) {
1187 position.top -= overBottom;
1188 // adjust based on position and margin
1189 } else {
1190 position.top = max( position.top - collisionPosTop, position.top );
1191 }
1192 }
1193 },
1194 flip: {
1195 left: function( position, data ) {
1196 var within = data.within,
1197 withinOffset = within.offset.left + within.scrollLeft,
1198 outerWidth = within.width,
1199 offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
1200 collisionPosLeft = position.left - data.collisionPosition.marginLeft,
1201 overLeft = collisionPosLeft - offsetLeft,
1202 overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
1203 myOffset = data.my[ 0 ] === "left" ?
1204 -data.elemWidth :
1205 data.my[ 0 ] === "right" ?
1206 data.elemWidth :
1207 0,
1208 atOffset = data.at[ 0 ] === "left" ?
1209 data.targetWidth :
1210 data.at[ 0 ] === "right" ?
1211 -data.targetWidth :
1212 0,
1213 offset = -2 * data.offset[ 0 ],
1214 newOverRight,
1215 newOverLeft;
1216
1217 if ( overLeft < 0 ) {
1218 newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset;
1219 if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
1220 position.left += myOffset + atOffset + offset;
1221 }
1222 }
1223 else if ( overRight > 0 ) {
1224 newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft;
1225 if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
1226 position.left += myOffset + atOffset + offset;
1227 }
1228 }
1229 },
1230 top: function( position, data ) {
1231 var within = data.within,
1232 withinOffset = within.offset.top + within.scrollTop,
1233 outerHeight = within.height,
1234 offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
1235 collisionPosTop = position.top - data.collisionPosition.marginTop,
1236 overTop = collisionPosTop - offsetTop,
1237 overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
1238 top = data.my[ 1 ] === "top",
1239 myOffset = top ?
1240 -data.elemHeight :
1241 data.my[ 1 ] === "bottom" ?
1242 data.elemHeight :
1243 0,
1244 atOffset = data.at[ 1 ] === "top" ?
1245 data.targetHeight :
1246 data.at[ 1 ] === "bottom" ?
1247 -data.targetHeight :
1248 0,
1249 offset = -2 * data.offset[ 1 ],
1250 newOverTop,
1251 newOverBottom;
1252 if ( overTop < 0 ) {
1253 newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset;
1254 if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) {
1255 position.top += myOffset + atOffset + offset;
1256 }
1257 }
1258 else if ( overBottom > 0 ) {
1259 newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop;
1260 if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) {
1261 position.top += myOffset + atOffset + offset;
1262 }
1263 }
1264 }
1265 },
1266 flipfit: {
1267 left: function() {
1268 $.ui.position.flip.left.apply( this, arguments );
1269 $.ui.position.fit.left.apply( this, arguments );
1270 },
1271 top: function() {
1272 $.ui.position.flip.top.apply( this, arguments );
1273 $.ui.position.fit.top.apply( this, arguments );
1274 }
1275 }
1276};
1277
1278// fraction support test
1279(function () {
1280 var testElement, testElementParent, testElementStyle, offsetLeft, i,
1281 body = document.getElementsByTagName( "body" )[ 0 ],
1282 div = document.createElement( "div" );
1283
1284 //Create a "fake body" for testing based on method used in jQuery.support
1285 testElement = document.createElement( body ? "div" : "body" );
1286 testElementStyle = {
1287 visibility: "hidden",
1288 width: 0,
1289 height: 0,
1290 border: 0,
1291 margin: 0,
1292 background: "none"
1293 };
1294 if ( body ) {
1295 $.extend( testElementStyle, {
1296 position: "absolute",
1297 left: "-1000px",
1298 top: "-1000px"
1299 });
1300 }
1301 for ( i in testElementStyle ) {
1302 testElement.style[ i ] = testElementStyle[ i ];
1303 }
1304 testElement.appendChild( div );
1305 testElementParent = body || document.documentElement;
1306 testElementParent.insertBefore( testElement, testElementParent.firstChild );
1307
1308 div.style.cssText = "position: absolute; left: 10.7432222px;";
1309
1310 offsetLeft = $( div ).offset().left;
1311 $.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11;
1312
1313 testElement.innerHTML = "";
1314 testElementParent.removeChild( testElement );
1315})();
1316
1317}( jQuery ) );
1318(function( $, undefined ) {
1319
1320$.widget( "ui.autocomplete", {
1321 version: "1.10.4",
1322 defaultElement: "<input>",
1323 options: {
1324 appendTo: null,
1325 autoFocus: false,
1326 delay: 300,
1327 minLength: 1,
1328 position: {
1329 my: "left top",
1330 at: "left bottom",
1331 collision: "none"
1332 },
1333 source: null,
1334
1335 // callbacks
1336 change: null,
1337 close: null,
1338 focus: null,
1339 open: null,
1340 response: null,
1341 search: null,
1342 select: null
1343 },
1344
1345 requestIndex: 0,
1346 pending: 0,
1347
1348 _create: function() {
1349 // Some browsers only repeat keydown events, not keypress events,
1350 // so we use the suppressKeyPress flag to determine if we've already
1351 // handled the keydown event. #7269
1352 // Unfortunately the code for & in keypress is the same as the up arrow,
1353 // so we use the suppressKeyPressRepeat flag to avoid handling keypress
1354 // events when we know the keydown event was used to modify the
1355 // search term. #7799
1356 var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
1357 nodeName = this.element[0].nodeName.toLowerCase(),
1358 isTextarea = nodeName === "textarea",
1359 isInput = nodeName === "input";
1360
1361 this.isMultiLine =
1362 // Textareas are always multi-line
1363 isTextarea ? true :
1364 // Inputs are always single-line, even if inside a contentEditable element
1365 // IE also treats inputs as contentEditable
1366 isInput ? false :
1367 // All other element types are determined by whether or not they're contentEditable
1368 this.element.prop( "isContentEditable" );
1369
1370 this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
1371 this.isNewMenu = true;
1372
1373 this.element
1374 .addClass( "ui-autocomplete-input" )
1375 .attr( "autocomplete", "off" );
1376
1377 this._on( this.element, {
1378 keydown: function( event ) {
1379 if ( this.element.prop( "readOnly" ) ) {
1380 suppressKeyPress = true;
1381 suppressInput = true;
1382 suppressKeyPressRepeat = true;
1383 return;
1384 }
1385
1386 suppressKeyPress = false;
1387 suppressInput = false;
1388 suppressKeyPressRepeat = false;
1389 var keyCode = $.ui.keyCode;
1390 switch( event.keyCode ) {
1391 case keyCode.PAGE_UP:
1392 suppressKeyPress = true;
1393 this._move( "previousPage", event );
1394 break;
1395 case keyCode.PAGE_DOWN:
1396 suppressKeyPress = true;
1397 this._move( "nextPage", event );
1398 break;
1399 case keyCode.UP:
1400 suppressKeyPress = true;
1401 this._keyEvent( "previous", event );
1402 break;
1403 case keyCode.DOWN:
1404 suppressKeyPress = true;
1405 this._keyEvent( "next", event );
1406 break;
1407 case keyCode.ENTER:
1408 case keyCode.NUMPAD_ENTER:
1409 // when menu is open and has focus
1410 if ( this.menu.active ) {
1411 // #6055 - Opera still allows the keypress to occur
1412 // which causes forms to submit
1413 suppressKeyPress = true;
1414 event.preventDefault();
1415 this.menu.select( event );
1416 }
1417 break;
1418 case keyCode.TAB:
1419 if ( this.menu.active ) {
1420 this.menu.select( event );
1421 }
1422 break;
1423 case keyCode.ESCAPE:
1424 if ( this.menu.element.is( ":visible" ) ) {
1425 this._value( this.term );
1426 this.close( event );
1427 // Different browsers have different default behavior for escape
1428 // Single press can mean undo or clear
1429 // Double press in IE means clear the whole form
1430 event.preventDefault();
1431 }
1432 break;
1433 default:
1434 suppressKeyPressRepeat = true;
1435 // search timeout should be triggered before the input value is changed
1436 this._searchTimeout( event );
1437 break;
1438 }
1439 },
1440 keypress: function( event ) {
1441 if ( suppressKeyPress ) {
1442 suppressKeyPress = false;
1443 if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
1444 event.preventDefault();
1445 }
1446 return;
1447 }
1448 if ( suppressKeyPressRepeat ) {
1449 return;
1450 }
1451
1452 // replicate some key handlers to allow them to repeat in Firefox and Opera
1453 var keyCode = $.ui.keyCode;
1454 switch( event.keyCode ) {
1455 case keyCode.PAGE_UP:
1456 this._move( "previousPage", event );
1457 break;
1458 case keyCode.PAGE_DOWN:
1459 this._move( "nextPage", event );
1460 break;
1461 case keyCode.UP:
1462 this._keyEvent( "previous", event );
1463 break;
1464 case keyCode.DOWN:
1465 this._keyEvent( "next", event );
1466 break;
1467 }
1468 },
1469 input: function( event ) {
1470 if ( suppressInput ) {
1471 suppressInput = false;
1472 event.preventDefault();
1473 return;
1474 }
1475 this._searchTimeout( event );
1476 },
1477 focus: function() {
1478 this.selectedItem = null;
1479 this.previous = this._value();
1480 },
1481 blur: function( event ) {
1482 if ( this.cancelBlur ) {
1483 delete this.cancelBlur;
1484 return;
1485 }
1486
1487 clearTimeout( this.searching );
1488 this.close( event );
1489 this._change( event );
1490 }
1491 });
1492
1493 this._initSource();
1494 this.menu = $( "<ul>" )
1495 .addClass( "ui-autocomplete ui-front" )
1496 .appendTo( this._appendTo() )
1497 .menu({
1498 // disable ARIA support, the live region takes care of that
1499 role: null
1500 })
1501 .hide()
1502 .data( "ui-menu" );
1503
1504 this._on( this.menu.element, {
1505 mousedown: function( event ) {
1506 // prevent moving focus out of the text field
1507 event.preventDefault();
1508
1509 // IE doesn't prevent moving focus even with event.preventDefault()
1510 // so we set a flag to know when we should ignore the blur event
1511 this.cancelBlur = true;
1512 this._delay(function() {
1513 delete this.cancelBlur;
1514 });
1515
1516 // clicking on the scrollbar causes focus to shift to the body
1517 // but we can't detect a mouseup or a click immediately afterward
1518 // so we have to track the next mousedown and close the menu if
1519 // the user clicks somewhere outside of the autocomplete
1520 var menuElement = this.menu.element[ 0 ];
1521 if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
1522 this._delay(function() {
1523 var that = this;
1524 this.document.one( "mousedown", function( event ) {
1525 if ( event.target !== that.element[ 0 ] &&
1526 event.target !== menuElement &&
1527 !$.contains( menuElement, event.target ) ) {
1528 that.close();
1529 }
1530 });
1531 });
1532 }
1533 },
1534 menufocus: function( event, ui ) {
1535 // support: Firefox
1536 // Prevent accidental activation of menu items in Firefox (#7024 #9118)
1537 if ( this.isNewMenu ) {
1538 this.isNewMenu = false;
1539 if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
1540 this.menu.blur();
1541
1542 this.document.one( "mousemove", function() {
1543 $( event.target ).trigger( event.originalEvent );
1544 });
1545
1546 return;
1547 }
1548 }
1549
1550 var item = ui.item.data( "ui-autocomplete-item" );
1551 if ( false !== this._trigger( "focus", event, { item: item } ) ) {
1552 // use value to match what will end up in the input, if it was a key event
1553 if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
1554 this._value( item.value );
1555 }
1556 } else {
1557 // Normally the input is populated with the item's value as the
1558 // menu is navigated, causing screen readers to notice a change and
1559 // announce the item. Since the focus event was canceled, this doesn't
1560 // happen, so we update the live region so that screen readers can
1561 // still notice the change and announce it.
1562 this.liveRegion.text( item.value );
1563 }
1564 },
1565 menuselect: function( event, ui ) {
1566 var item = ui.item.data( "ui-autocomplete-item" ),
1567 previous = this.previous;
1568
1569 // only trigger when focus was lost (click on menu)
1570 if ( this.element[0] !== this.document[0].activeElement ) {
1571 this.element.focus();
1572 this.previous = previous;
1573 // #6109 - IE triggers two focus events and the second
1574 // is asynchronous, so we need to reset the previous
1575 // term synchronously and asynchronously :-(
1576 this._delay(function() {
1577 this.previous = previous;
1578 this.selectedItem = item;
1579 });
1580 }
1581
1582 if ( false !== this._trigger( "select", event, { item: item } ) ) {
1583 this._value( item.value );
1584 }
1585 // reset the term after the select event
1586 // this allows custom select handling to work properly
1587 this.term = this._value();
1588
1589 this.close( event );
1590 this.selectedItem = item;
1591 }
1592 });
1593
1594 this.liveRegion = $( "<span>", {
1595 role: "status",
1596 "aria-live": "polite"
1597 })
1598 .addClass( "ui-helper-hidden-accessible" )
1599 .insertBefore( this.element );
1600
1601 // turning off autocomplete prevents the browser from remembering the
1602 // value when navigating through history, so we re-enable autocomplete
1603 // if the page is unloaded before the widget is destroyed. #7790
1604 this._on( this.window, {
1605 beforeunload: function() {
1606 this.element.removeAttr( "autocomplete" );
1607 }
1608 });
1609 },
1610
1611 _destroy: function() {
1612 clearTimeout( this.searching );
1613 this.element
1614 .removeClass( "ui-autocomplete-input" )
1615 .removeAttr( "autocomplete" );
1616 this.menu.element.remove();
1617 this.liveRegion.remove();
1618 },
1619
1620 _setOption: function( key, value ) {
1621 this._super( key, value );
1622 if ( key === "source" ) {
1623 this._initSource();
1624 }
1625 if ( key === "appendTo" ) {
1626 this.menu.element.appendTo( this._appendTo() );
1627 }
1628 if ( key === "disabled" && value && this.xhr ) {
1629 this.xhr.abort();
1630 }
1631 },
1632
1633 _appendTo: function() {
1634 var element = this.options.appendTo;
1635
1636 if ( element ) {
1637 element = element.jquery || element.nodeType ?
1638 $( element ) :
1639 this.document.find( element ).eq( 0 );
1640 }
1641
1642 if ( !element ) {
1643 element = this.element.closest( ".ui-front" );
1644 }
1645
1646 if ( !element.length ) {
1647 element = this.document[0].body;
1648 }
1649
1650 return element;
1651 },
1652
1653 _initSource: function() {
1654 var array, url,
1655 that = this;
1656 if ( $.isArray(this.options.source) ) {
1657 array = this.options.source;
1658 this.source = function( request, response ) {
1659 response( $.ui.autocomplete.filter( array, request.term ) );
1660 };
1661 } else if ( typeof this.options.source === "string" ) {
1662 url = this.options.source;
1663 this.source = function( request, response ) {
1664 if ( that.xhr ) {
1665 that.xhr.abort();
1666 }
1667 that.xhr = $.ajax({
1668 url: url,
1669 data: request,
1670 dataType: "json",
1671 success: function( data ) {
1672 response( data );
1673 },
1674 error: function() {
1675 response( [] );
1676 }
1677 });
1678 };
1679 } else {
1680 this.source = this.options.source;
1681 }
1682 },
1683
1684 _searchTimeout: function( event ) {
1685 clearTimeout( this.searching );
1686 this.searching = this._delay(function() {
1687 // only search if the value has changed
1688 if ( this.term !== this._value() ) {
1689 this.selectedItem = null;
1690 this.search( null, event );
1691 }
1692 }, this.options.delay );
1693 },
1694
1695 search: function( value, event ) {
1696 value = value != null ? value : this._value();
1697
1698 // always save the actual value, not the one passed as an argument
1699 this.term = this._value();
1700
1701 if ( value.length < this.options.minLength ) {
1702 return this.close( event );
1703 }
1704
1705 if ( this._trigger( "search", event ) === false ) {
1706 return;
1707 }
1708
1709 return this._search( value );
1710 },
1711
1712 _search: function( value ) {
1713 this.pending++;
1714 this.element.addClass( "ui-autocomplete-loading" );
1715 this.cancelSearch = false;
1716
1717 this.source( { term: value }, this._response() );
1718 },
1719
1720 _response: function() {
1721 var index = ++this.requestIndex;
1722
1723 return $.proxy(function( content ) {
1724 if ( index === this.requestIndex ) {
1725 this.__response( content );
1726 }
1727
1728 this.pending--;
1729 if ( !this.pending ) {
1730 this.element.removeClass( "ui-autocomplete-loading" );
1731 }
1732 }, this );
1733 },
1734
1735 __response: function( content ) {
1736 if ( content ) {
1737 content = this._normalize( content );
1738 }
1739 this._trigger( "response", null, { content: content } );
1740 if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
1741 this._suggest( content );
1742 this._trigger( "open" );
1743 } else {
1744 // use ._close() instead of .close() so we don't cancel future searches
1745 this._close();
1746 }
1747 },
1748
1749 close: function( event ) {
1750 this.cancelSearch = true;
1751 this._close( event );
1752 },
1753
1754 _close: function( event ) {
1755 if ( this.menu.element.is( ":visible" ) ) {
1756 this.menu.element.hide();
1757 this.menu.blur();
1758 this.isNewMenu = true;
1759 this._trigger( "close", event );
1760 }
1761 },
1762
1763 _change: function( event ) {
1764 if ( this.previous !== this._value() ) {
1765 this._trigger( "change", event, { item: this.selectedItem } );
1766 }
1767 },
1768
1769 _normalize: function( items ) {
1770 // assume all items have the right format when the first item is complete
1771 if ( items.length && items[0].label && items[0].value ) {
1772 return items;
1773 }
1774 return $.map( items, function( item ) {
1775 if ( typeof item === "string" ) {
1776 return {
1777 label: item,
1778 value: item
1779 };
1780 }
1781 return $.extend({
1782 label: item.label || item.value,
1783 value: item.value || item.label
1784 }, item );
1785 });
1786 },
1787
1788 _suggest: function( items ) {
1789 var ul = this.menu.element.empty();
1790 this._renderMenu( ul, items );
1791 this.isNewMenu = true;
1792 this.menu.refresh();
1793
1794 // size and position menu
1795 ul.show();
1796 this._resizeMenu();
1797 ul.position( $.extend({
1798 of: this.element
1799 }, this.options.position ));
1800
1801 if ( this.options.autoFocus ) {
1802 this.menu.next();
1803 }
1804 },
1805
1806 _resizeMenu: function() {
1807 var ul = this.menu.element;
1808 ul.outerWidth( Math.max(
1809 // Firefox wraps long text (possibly a rounding bug)
1810 // so we add 1px to avoid the wrapping (#7513)
1811 ul.width( "" ).outerWidth() + 1,
1812 this.element.outerWidth()
1813 ) );
1814 },
1815
1816 _renderMenu: function( ul, items ) {
1817 var that = this;
1818 $.each( items, function( index, item ) {
1819 that._renderItemData( ul, item );
1820 });
1821 },
1822
1823 _renderItemData: function( ul, item ) {
1824 return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
1825 },
1826
1827 _renderItem: function( ul, item ) {
1828 return $( "<li>" )
1829 .append( $( "<a>" ).text( item.label ) )
1830 .appendTo( ul );
1831 },
1832
1833 _move: function( direction, event ) {
1834 if ( !this.menu.element.is( ":visible" ) ) {
1835 this.search( null, event );
1836 return;
1837 }
1838 if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
1839 this.menu.isLastItem() && /^next/.test( direction ) ) {
1840 this._value( this.term );
1841 this.menu.blur();
1842 return;
1843 }
1844 this.menu[ direction ]( event );
1845 },
1846
1847 widget: function() {
1848 return this.menu.element;
1849 },
1850
1851 _value: function() {
1852 return this.valueMethod.apply( this.element, arguments );
1853 },
1854
1855 _keyEvent: function( keyEvent, event ) {
1856 if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
1857 this._move( keyEvent, event );
1858
1859 // prevents moving cursor to beginning/end of the text field in some browsers
1860 event.preventDefault();
1861 }
1862 }
1863});
1864
1865$.extend( $.ui.autocomplete, {
1866 escapeRegex: function( value ) {
1867 return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
1868 },
1869 filter: function(array, term) {
1870 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
1871 return $.grep( array, function(value) {
1872 return matcher.test( value.label || value.value || value );
1873 });
1874 }
1875});
1876
1877
1878// live region extension, adding a `messages` option
1879// NOTE: This is an experimental API. We are still investigating
1880// a full solution for string manipulation and internationalization.
1881$.widget( "ui.autocomplete", $.ui.autocomplete, {
1882 options: {
1883 messages: {
1884 noResults: "No search results.",
1885 results: function( amount ) {
1886 return amount + ( amount > 1 ? " results are" : " result is" ) +
1887 " available, use up and down arrow keys to navigate.";
1888 }
1889 }
1890 },
1891
1892 __response: function( content ) {
1893 var message;
1894 this._superApply( arguments );
1895 if ( this.options.disabled || this.cancelSearch ) {
1896 return;
1897 }
1898 if ( content && content.length ) {
1899 message = this.options.messages.results( content.length );
1900 } else {
1901 message = this.options.messages.noResults;
1902 }
1903 this.liveRegion.text( message );
1904 }
1905});
1906
1907}( jQuery ));
1908(function( $, undefined ) {
1909
1910$.widget( "ui.menu", {
1911 version: "1.10.4",
1912 defaultElement: "<ul>",
1913 delay: 300,
1914 options: {
1915 icons: {
1916 submenu: "ui-icon-carat-1-e"
1917 },
1918 menus: "ul",
1919 position: {
1920 my: "left top",
1921 at: "right top"
1922 },
1923 role: "menu",
1924
1925 // callbacks
1926 blur: null,
1927 focus: null,
1928 select: null
1929 },
1930
1931 _create: function() {
1932 this.activeMenu = this.element;
1933 // flag used to prevent firing of the click handler
1934 // as the event bubbles up through nested menus
1935 this.mouseHandled = false;
1936 this.element
1937 .uniqueId()
1938 .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
1939 .toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length )
1940 .attr({
1941 role: this.options.role,
1942 tabIndex: 0
1943 })
1944 // need to catch all clicks on disabled menu
1945 // not possible through _on
1946 .bind( "click" + this.eventNamespace, $.proxy(function( event ) {
1947 if ( this.options.disabled ) {
1948 event.preventDefault();
1949 }
1950 }, this ));
1951
1952 if ( this.options.disabled ) {
1953 this.element
1954 .addClass( "ui-state-disabled" )
1955 .attr( "aria-disabled", "true" );
1956 }
1957
1958 this._on({
1959 // Prevent focus from sticking to links inside menu after clicking
1960 // them (focus should always stay on UL during navigation).
1961 "mousedown .ui-menu-item > a": function( event ) {
1962 event.preventDefault();
1963 },
1964 "click .ui-state-disabled > a": function( event ) {
1965 event.preventDefault();
1966 },
1967 "click .ui-menu-item:has(a)": function( event ) {
1968 var target = $( event.target ).closest( ".ui-menu-item" );
1969 if ( !this.mouseHandled && target.not( ".ui-state-disabled" ).length ) {
1970 this.select( event );
1971
1972 // Only set the mouseHandled flag if the event will bubble, see #9469.
1973 if ( !event.isPropagationStopped() ) {
1974 this.mouseHandled = true;
1975 }
1976
1977 // Open submenu on click
1978 if ( target.has( ".ui-menu" ).length ) {
1979 this.expand( event );
1980 } else if ( !this.element.is( ":focus" ) && $( this.document[ 0 ].activeElement ).closest( ".ui-menu" ).length ) {
1981
1982 // Redirect focus to the menu
1983 this.element.trigger( "focus", [ true ] );
1984
1985 // If the active item is on the top level, let it stay active.
1986 // Otherwise, blur the active item since it is no longer visible.
1987 if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) {
1988 clearTimeout( this.timer );
1989 }
1990 }
1991 }
1992 },
1993 "mouseenter .ui-menu-item": function( event ) {
1994 var target = $( event.currentTarget );
1995 // Remove ui-state-active class from siblings of the newly focused menu item
1996 // to avoid a jump caused by adjacent elements both having a class with a border
1997 target.siblings().children( ".ui-state-active" ).removeClass( "ui-state-active" );
1998 this.focus( event, target );
1999 },
2000 mouseleave: "collapseAll",
2001 "mouseleave .ui-menu": "collapseAll",
2002 focus: function( event, keepActiveItem ) {
2003 // If there's already an active item, keep it active
2004 // If not, activate the first item
2005 var item = this.active || this.element.children( ".ui-menu-item" ).eq( 0 );
2006
2007 if ( !keepActiveItem ) {
2008 this.focus( event, item );
2009 }
2010 },
2011 blur: function( event ) {
2012 this._delay(function() {
2013 if ( !$.contains( this.element[0], this.document[0].activeElement ) ) {
2014 this.collapseAll( event );
2015 }
2016 });
2017 },
2018 keydown: "_keydown"
2019 });
2020
2021 this.refresh();
2022
2023 // Clicks outside of a menu collapse any open menus
2024 this._on( this.document, {
2025 click: function( event ) {
2026 if ( !$( event.target ).closest( ".ui-menu" ).length ) {
2027 this.collapseAll( event );
2028 }
2029
2030 // Reset the mouseHandled flag
2031 this.mouseHandled = false;
2032 }
2033 });
2034 },
2035
2036 _destroy: function() {
2037 // Destroy (sub)menus
2038 this.element
2039 .removeAttr( "aria-activedescendant" )
2040 .find( ".ui-menu" ).addBack()
2041 .removeClass( "ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons" )
2042 .removeAttr( "role" )
2043 .removeAttr( "tabIndex" )
2044 .removeAttr( "aria-labelledby" )
2045 .removeAttr( "aria-expanded" )
2046 .removeAttr( "aria-hidden" )
2047 .removeAttr( "aria-disabled" )
2048 .removeUniqueId()
2049 .show();
2050
2051 // Destroy menu items
2052 this.element.find( ".ui-menu-item" )
2053 .removeClass( "ui-menu-item" )
2054 .removeAttr( "role" )
2055 .removeAttr( "aria-disabled" )
2056 .children( "a" )
2057 .removeUniqueId()
2058 .removeClass( "ui-corner-all ui-state-hover" )
2059 .removeAttr( "tabIndex" )
2060 .removeAttr( "role" )
2061 .removeAttr( "aria-haspopup" )
2062 .children().each( function() {
2063 var elem = $( this );
2064 if ( elem.data( "ui-menu-submenu-carat" ) ) {
2065 elem.remove();
2066 }
2067 });
2068
2069 // Destroy menu dividers
2070 this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" );
2071 },
2072
2073 _keydown: function( event ) {
2074 var match, prev, character, skip, regex,
2075 preventDefault = true;
2076
2077 function escape( value ) {
2078 return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
2079 }
2080
2081 switch ( event.keyCode ) {
2082 case $.ui.keyCode.PAGE_UP:
2083 this.previousPage( event );
2084 break;
2085 case $.ui.keyCode.PAGE_DOWN:
2086 this.nextPage( event );
2087 break;
2088 case $.ui.keyCode.HOME:
2089 this._move( "first", "first", event );
2090 break;
2091 case $.ui.keyCode.END:
2092 this._move( "last", "last", event );
2093 break;
2094 case $.ui.keyCode.UP:
2095 this.previous( event );
2096 break;
2097 case $.ui.keyCode.DOWN:
2098 this.next( event );
2099 break;
2100 case $.ui.keyCode.LEFT:
2101 this.collapse( event );
2102 break;
2103 case $.ui.keyCode.RIGHT:
2104 if ( this.active && !this.active.is( ".ui-state-disabled" ) ) {
2105 this.expand( event );
2106 }
2107 break;
2108 case $.ui.keyCode.ENTER:
2109 case $.ui.keyCode.SPACE:
2110 this._activate( event );
2111 break;
2112 case $.ui.keyCode.ESCAPE:
2113 this.collapse( event );
2114 break;
2115 default:
2116 preventDefault = false;
2117 prev = this.previousFilter || "";
2118 character = String.fromCharCode( event.keyCode );
2119 skip = false;
2120
2121 clearTimeout( this.filterTimer );
2122
2123 if ( character === prev ) {
2124 skip = true;
2125 } else {
2126 character = prev + character;
2127 }
2128
2129 regex = new RegExp( "^" + escape( character ), "i" );
2130 match = this.activeMenu.children( ".ui-menu-item" ).filter(function() {
2131 return regex.test( $( this ).children( "a" ).text() );
2132 });
2133 match = skip && match.index( this.active.next() ) !== -1 ?
2134 this.active.nextAll( ".ui-menu-item" ) :
2135 match;
2136
2137 // If no matches on the current filter, reset to the last character pressed
2138 // to move down the menu to the first item that starts with that character
2139 if ( !match.length ) {
2140 character = String.fromCharCode( event.keyCode );
2141 regex = new RegExp( "^" + escape( character ), "i" );
2142 match = this.activeMenu.children( ".ui-menu-item" ).filter(function() {
2143 return regex.test( $( this ).children( "a" ).text() );
2144 });
2145 }
2146
2147 if ( match.length ) {
2148 this.focus( event, match );
2149 if ( match.length > 1 ) {
2150 this.previousFilter = character;
2151 this.filterTimer = this._delay(function() {
2152 delete this.previousFilter;
2153 }, 1000 );
2154 } else {
2155 delete this.previousFilter;
2156 }
2157 } else {
2158 delete this.previousFilter;
2159 }
2160 }
2161
2162 if ( preventDefault ) {
2163 event.preventDefault();
2164 }
2165 },
2166
2167 _activate: function( event ) {
2168 if ( !this.active.is( ".ui-state-disabled" ) ) {
2169 if ( this.active.children( "a[aria-haspopup='true']" ).length ) {
2170 this.expand( event );
2171 } else {
2172 this.select( event );
2173 }
2174 }
2175 },
2176
2177 refresh: function() {
2178 var menus,
2179 icon = this.options.icons.submenu,
2180 submenus = this.element.find( this.options.menus );
2181
2182 this.element.toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length );
2183
2184 // Initialize nested menus
2185 submenus.filter( ":not(.ui-menu)" )
2186 .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" )
2187 .hide()
2188 .attr({
2189 role: this.options.role,
2190 "aria-hidden": "true",
2191 "aria-expanded": "false"
2192 })
2193 .each(function() {
2194 var menu = $( this ),
2195 item = menu.prev( "a" ),
2196 submenuCarat = $( "<span>" )
2197 .addClass( "ui-menu-icon ui-icon " + icon )
2198 .data( "ui-menu-submenu-carat", true );
2199
2200 item
2201 .attr( "aria-haspopup", "true" )
2202 .prepend( submenuCarat );
2203 menu.attr( "aria-labelledby", item.attr( "id" ) );
2204 });
2205
2206 menus = submenus.add( this.element );
2207
2208 // Don't refresh list items that are already adapted
2209 menus.children( ":not(.ui-menu-item):has(a)" )
2210 .addClass( "ui-menu-item" )
2211 .attr( "role", "presentation" )
2212 .children( "a" )
2213 .uniqueId()
2214 .addClass( "ui-corner-all" )
2215 .attr({
2216 tabIndex: -1,
2217 role: this._itemRole()
2218 });
2219
2220 // Initialize unlinked menu-items containing spaces and/or dashes only as dividers
2221 menus.children( ":not(.ui-menu-item)" ).each(function() {
2222 var item = $( this );
2223 // hyphen, em dash, en dash
2224 if ( !/[^\-\u2014\u2013\s]/.test( item.text() ) ) {
2225 item.addClass( "ui-widget-content ui-menu-divider" );
2226 }
2227 });
2228
2229 // Add aria-disabled attribute to any disabled menu item
2230 menus.children( ".ui-state-disabled" ).attr( "aria-disabled", "true" );
2231
2232 // If the active item has been removed, blur the menu
2233 if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
2234 this.blur();
2235 }
2236 },
2237
2238 _itemRole: function() {
2239 return {
2240 menu: "menuitem",
2241 listbox: "option"
2242 }[ this.options.role ];
2243 },
2244
2245 _setOption: function( key, value ) {
2246 if ( key === "icons" ) {
2247 this.element.find( ".ui-menu-icon" )
2248 .removeClass( this.options.icons.submenu )
2249 .addClass( value.submenu );
2250 }
2251 this._super( key, value );
2252 },
2253
2254 focus: function( event, item ) {
2255 var nested, focused;
2256 this.blur( event, event && event.type === "focus" );
2257
2258 this._scrollIntoView( item );
2259
2260 this.active = item.first();
2261 focused = this.active.children( "a" ).addClass( "ui-state-focus" );
2262 // Only update aria-activedescendant if there's a role
2263 // otherwise we assume focus is managed elsewhere
2264 if ( this.options.role ) {
2265 this.element.attr( "aria-activedescendant", focused.attr( "id" ) );
2266 }
2267
2268 // Highlight active parent menu item, if any
2269 this.active
2270 .parent()
2271 .closest( ".ui-menu-item" )
2272 .children( "a:first" )
2273 .addClass( "ui-state-active" );
2274
2275 if ( event && event.type === "keydown" ) {
2276 this._close();
2277 } else {
2278 this.timer = this._delay(function() {
2279 this._close();
2280 }, this.delay );
2281 }
2282
2283 nested = item.children( ".ui-menu" );
2284 if ( nested.length && event && ( /^mouse/.test( event.type ) ) ) {
2285 this._startOpening(nested);
2286 }
2287 this.activeMenu = item.parent();
2288
2289 this._trigger( "focus", event, { item: item } );
2290 },
2291
2292 _scrollIntoView: function( item ) {
2293 var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight;
2294 if ( this._hasScroll() ) {
2295 borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0;
2296 paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0;
2297 offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop;
2298 scroll = this.activeMenu.scrollTop();
2299 elementHeight = this.activeMenu.height();
2300 itemHeight = item.height();
2301
2302 if ( offset < 0 ) {
2303 this.activeMenu.scrollTop( scroll + offset );
2304 } else if ( offset + itemHeight > elementHeight ) {
2305 this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight );
2306 }
2307 }
2308 },
2309
2310 blur: function( event, fromFocus ) {
2311 if ( !fromFocus ) {
2312 clearTimeout( this.timer );
2313 }
2314
2315 if ( !this.active ) {
2316 return;
2317 }
2318
2319 this.active.children( "a" ).removeClass( "ui-state-focus" );
2320 this.active = null;
2321
2322 this._trigger( "blur", event, { item: this.active } );
2323 },
2324
2325 _startOpening: function( submenu ) {
2326 clearTimeout( this.timer );
2327
2328 // Don't open if already open fixes a Firefox bug that caused a .5 pixel
2329 // shift in the submenu position when mousing over the carat icon
2330 if ( submenu.attr( "aria-hidden" ) !== "true" ) {
2331 return;
2332 }
2333
2334 this.timer = this._delay(function() {
2335 this._close();
2336 this._open( submenu );
2337 }, this.delay );
2338 },
2339
2340 _open: function( submenu ) {
2341 var position = $.extend({
2342 of: this.active
2343 }, this.options.position );
2344
2345 clearTimeout( this.timer );
2346 this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) )
2347 .hide()
2348 .attr( "aria-hidden", "true" );
2349
2350 submenu
2351 .show()
2352 .removeAttr( "aria-hidden" )
2353 .attr( "aria-expanded", "true" )
2354 .position( position );
2355 },
2356
2357 collapseAll: function( event, all ) {
2358 clearTimeout( this.timer );
2359 this.timer = this._delay(function() {
2360 // If we were passed an event, look for the submenu that contains the event
2361 var currentMenu = all ? this.element :
2362 $( event && event.target ).closest( this.element.find( ".ui-menu" ) );
2363
2364 // If we found no valid submenu ancestor, use the main menu to close all sub menus anyway
2365 if ( !currentMenu.length ) {
2366 currentMenu = this.element;
2367 }
2368
2369 this._close( currentMenu );
2370
2371 this.blur( event );
2372 this.activeMenu = currentMenu;
2373 }, this.delay );
2374 },
2375
2376 // With no arguments, closes the currently active menu - if nothing is active
2377 // it closes all menus. If passed an argument, it will search for menus BELOW
2378 _close: function( startMenu ) {
2379 if ( !startMenu ) {
2380 startMenu = this.active ? this.active.parent() : this.element;
2381 }
2382
2383 startMenu
2384 .find( ".ui-menu" )
2385 .hide()
2386 .attr( "aria-hidden", "true" )
2387 .attr( "aria-expanded", "false" )
2388 .end()
2389 .find( "a.ui-state-active" )
2390 .removeClass( "ui-state-active" );
2391 },
2392
2393 collapse: function( event ) {
2394 var newItem = this.active &&
2395 this.active.parent().closest( ".ui-menu-item", this.element );
2396 if ( newItem && newItem.length ) {
2397 this._close();
2398 this.focus( event, newItem );
2399 }
2400 },
2401
2402 expand: function( event ) {
2403 var newItem = this.active &&
2404 this.active
2405 .children( ".ui-menu " )
2406 .children( ".ui-menu-item" )
2407 .first();
2408
2409 if ( newItem && newItem.length ) {
2410 this._open( newItem.parent() );
2411
2412 // Delay so Firefox will not hide activedescendant change in expanding submenu from AT
2413 this._delay(function() {
2414 this.focus( event, newItem );
2415 });
2416 }
2417 },
2418
2419 next: function( event ) {
2420 this._move( "next", "first", event );
2421 },
2422
2423 previous: function( event ) {
2424 this._move( "prev", "last", event );
2425 },
2426
2427 isFirstItem: function() {
2428 return this.active && !this.active.prevAll( ".ui-menu-item" ).length;
2429 },
2430
2431 isLastItem: function() {
2432 return this.active && !this.active.nextAll( ".ui-menu-item" ).length;
2433 },
2434
2435 _move: function( direction, filter, event ) {
2436 var next;
2437 if ( this.active ) {
2438 if ( direction === "first" || direction === "last" ) {
2439 next = this.active
2440 [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
2441 .eq( -1 );
2442 } else {
2443 next = this.active
2444 [ direction + "All" ]( ".ui-menu-item" )
2445 .eq( 0 );
2446 }
2447 }
2448 if ( !next || !next.length || !this.active ) {
2449 next = this.activeMenu.children( ".ui-menu-item" )[ filter ]();
2450 }
2451
2452 this.focus( event, next );
2453 },
2454
2455 nextPage: function( event ) {
2456 var item, base, height;
2457
2458 if ( !this.active ) {
2459 this.next( event );
2460 return;
2461 }
2462 if ( this.isLastItem() ) {
2463 return;
2464 }
2465 if ( this._hasScroll() ) {
2466 base = this.active.offset().top;
2467 height = this.element.height();
2468 this.active.nextAll( ".ui-menu-item" ).each(function() {
2469 item = $( this );
2470 return item.offset().top - base - height < 0;
2471 });
2472
2473 this.focus( event, item );
2474 } else {
2475 this.focus( event, this.activeMenu.children( ".ui-menu-item" )
2476 [ !this.active ? "first" : "last" ]() );
2477 }
2478 },
2479
2480 previousPage: function( event ) {
2481 var item, base, height;
2482 if ( !this.active ) {
2483 this.next( event );
2484 return;
2485 }
2486 if ( this.isFirstItem() ) {
2487 return;
2488 }
2489 if ( this._hasScroll() ) {
2490 base = this.active.offset().top;
2491 height = this.element.height();
2492 this.active.prevAll( ".ui-menu-item" ).each(function() {
2493 item = $( this );
2494 return item.offset().top - base + height > 0;
2495 });
2496
2497 this.focus( event, item );
2498 } else {
2499 this.focus( event, this.activeMenu.children( ".ui-menu-item" ).first() );
2500 }
2501 },
2502
2503 _hasScroll: function() {
2504 return this.element.outerHeight() < this.element.prop( "scrollHeight" );
2505 },
2506
2507 select: function( event ) {
2508 // TODO: It should never be possible to not have an active item at this
2509 // point, but the tests don't trigger mouseenter before click.
2510 this.active = this.active || $( event.target ).closest( ".ui-menu-item" );
2511 var ui = { item: this.active };
2512 if ( !this.active.has( ".ui-menu" ).length ) {
2513 this.collapseAll( event, true );
2514 }
2515 this._trigger( "select", event, ui );
2516 }
2517});
2518
2519}( jQuery ));
diff --git a/themes/default/js/jquery-ui-1.10.4.custom.min.js b/themes/default/js/jquery-ui-1.10.4.custom.min.js
new file mode 100644
index 00000000..31635026
--- /dev/null
+++ b/themes/default/js/jquery-ui-1.10.4.custom.min.js
@@ -0,0 +1,6 @@
1/*! jQuery UI - v1.10.4 - 2014-03-08
2* http://jqueryui.com
3* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.position.js, jquery.ui.autocomplete.js, jquery.ui.menu.js
4* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */
5
6(function(e,t){function i(t,i){var s,a,o,r=t.nodeName.toLowerCase();return"area"===r?(s=t.parentNode,a=s.name,t.href&&a&&"map"===s.nodeName.toLowerCase()?(o=e("img[usemap=#"+a+"]")[0],!!o&&n(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||i:i)&&n(t)}function n(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var s=0,a=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,n){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),n&&n.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var n,s,a=e(this[0]);a.length&&a[0]!==document;){if(n=a.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(a.css("zIndex"),10),!isNaN(s)&&0!==s))return s;a=a.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){a.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,n){return!!e.data(t,n[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(t,!s)}}),e("<a>").outerWidth(1).jquery||e.each(["Width","Height"],function(i,n){function s(t,i,n,s){return e.each(a,function(){i-=parseFloat(e.css(t,"padding"+this))||0,n&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var a="Width"===n?["Left","Right"]:["Top","Bottom"],o=n.toLowerCase(),r={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+n]=function(i){return i===t?r["inner"+n].call(this):this.each(function(){e(this).css(o,s(this,i)+"px")})},e.fn["outer"+n]=function(t,i){return"number"!=typeof t?r["outer"+n].call(this,t):this.each(function(){e(this).css(o,s(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("<a>").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,n){var s,a=e.ui[t].prototype;for(s in n)a.plugins[s]=a.plugins[s]||[],a.plugins[s].push([i,n[s]])},call:function(e,t,i){var n,s=e.plugins[t];if(s&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)e.options[s[n][0]]&&s[n][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return t[n]>0?!0:(t[n]=1,s=t[n]>0,t[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,h,l={},c=i.split(".")[0];i=i.split(".")[1],o=c+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[c]=t[c]||{},a=t[c][i],r=t[c][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),h=new s,h.options=t.widget.extend({},h.options),t.each(n,function(i,n){return t.isFunction(n)?(l[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(l[i]=n,e)}),r.prototype=t.widget.extend(h,{widgetEventPrefix:a?h.widgetEventPrefix||i:i},l,{constructor:r,namespace:c,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,h=a.length;h>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,h=s.call(arguments,1),l=this;return a=!r&&h.length?t.widget.extend.apply(null,[a].concat(h)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,h),s!==n&&s!==e?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"<div>",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function h(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(h.guid=r.guid=r.guid||h.guid||t.guid++);var l=n.match(/^(\w+)\s*(.*)$/),c=l[1]+a.eventNamespace,u=l[2];u?o.delegate(u,c,h):s.bind(c,h)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.width<e.element[0].scrollWidth,a="scroll"===s||"auto"===s&&e.height<e.element[0].scrollHeight;return{width:a?t.position.scrollbarWidth():0,height:n?t.position.scrollbarWidth():0}},getWithinInfo:function(e){var i=t(e||window),s=t.isWindow(i[0]),n=!!i[0]&&9===i[0].nodeType;return{element:i,isWindow:s,isDocument:n,offset:i.offset()||{left:0,top:0},scrollLeft:i.scrollLeft(),scrollTop:i.scrollTop(),width:s?i.width():i.outerWidth(),height:s?i.height():i.outerHeight()}}},t.fn.position=function(e){if(!e||!e.of)return f.apply(this,arguments);e=t.extend({},e);var a,p,g,m,v,_,b=t(e.of),y=t.position.getWithinInfo(e.within),k=t.position.getScrollInfo(y),w=(e.collision||"flip").split(" "),D={};return _=n(b),b[0].preventDefault&&(e.at="left top"),p=_.width,g=_.height,m=_.offset,v=t.extend({},m),t.each(["my","at"],function(){var t,i,s=(e[this]||"").split(" ");1===s.length&&(s=h.test(s[0])?s.concat(["center"]):c.test(s[0])?["center"].concat(s):["center","center"]),s[0]=h.test(s[0])?s[0]:"center",s[1]=c.test(s[1])?s[1]:"center",t=u.exec(s[0]),i=u.exec(s[1]),D[this]=[t?t[0]:0,i?i[0]:0],e[this]=[d.exec(s[0])[0],d.exec(s[1])[0]]}),1===w.length&&(w[1]=w[0]),"right"===e.at[0]?v.left+=p:"center"===e.at[0]&&(v.left+=p/2),"bottom"===e.at[1]?v.top+=g:"center"===e.at[1]&&(v.top+=g/2),a=i(D.at,p,g),v.left+=a[0],v.top+=a[1],this.each(function(){var n,h,c=t(this),u=c.outerWidth(),d=c.outerHeight(),f=s(this,"marginLeft"),_=s(this,"marginTop"),x=u+f+s(this,"marginRight")+k.width,C=d+_+s(this,"marginBottom")+k.height,M=t.extend({},v),T=i(D.my,c.outerWidth(),c.outerHeight());"right"===e.my[0]?M.left-=u:"center"===e.my[0]&&(M.left-=u/2),"bottom"===e.my[1]?M.top-=d:"center"===e.my[1]&&(M.top-=d/2),M.left+=T[0],M.top+=T[1],t.support.offsetFractions||(M.left=l(M.left),M.top=l(M.top)),n={marginLeft:f,marginTop:_},t.each(["left","top"],function(i,s){t.ui.position[w[i]]&&t.ui.position[w[i]][s](M,{targetWidth:p,targetHeight:g,elemWidth:u,elemHeight:d,collisionPosition:n,collisionWidth:x,collisionHeight:C,offset:[a[0]+T[0],a[1]+T[1]],my:e.my,at:e.at,within:y,elem:c})}),e.using&&(h=function(t){var i=m.left-M.left,s=i+p-u,n=m.top-M.top,a=n+g-d,l={target:{element:b,left:m.left,top:m.top,width:p,height:g},element:{element:c,left:M.left,top:M.top,width:u,height:d},horizontal:0>s?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(l.horizontal="center"),d>g&&g>r(n+a)&&(l.vertical="middle"),l.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,l)}),c.offset(t.extend(M,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-o-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-o-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-o-a,t.top+p+f+g>c&&(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,t.top+p+f+g>u&&(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(e){e.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"<input>",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("<ul>").addClass("ui-autocomplete ui-front").appendTo(this._appendTo()).menu({role:null}).hide().data("ui-menu"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur});var i=this.menu.element[0];e(t.target).closest(".ui-menu-item").length||this._delay(function(){var t=this;this.document.one("mousedown",function(s){s.target===t.element[0]||s.target===i||e.contains(i,s.target)||t.close()})})},menufocus:function(t,i){if(this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent&&/^mouse/.test(t.originalEvent.type)))return this.menu.blur(),this.document.one("mousemove",function(){e(t.target).trigger(t.originalEvent)}),undefined;var s=i.item.data("ui-autocomplete-item");!1!==this._trigger("focus",t,{item:s})?t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value):this.liveRegion.text(s.value)},menuselect:function(e,t){var i=t.item.data("ui-autocomplete-item"),s=this.previous;this.element[0]!==this.document[0].activeElement&&(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s,this.selectedItem=i})),!1!==this._trigger("select",e,{item:i})&&this._value(i.value),this.term=this._value(),this.close(e),this.selectedItem=i}}),this.liveRegion=e("<span>",{role:"status","aria-live":"polite"}).addClass("ui-helper-hidden-accessible").insertBefore(this.element),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(e,t){this._super(e,t),"source"===e&&this._initSource(),"appendTo"===e&&this.menu.element.appendTo(this._appendTo()),"disabled"===e&&t&&this.xhr&&this.xhr.abort()},_appendTo:function(){var t=this.options.appendTo;return t&&(t=t.jquery||t.nodeType?e(t):this.document.find(t).eq(0)),t||(t=this.element.closest(".ui-front")),t.length||(t=this.document[0].body),t},_initSource:function(){var t,i,s=this;e.isArray(this.options.source)?(t=this.options.source,this.source=function(i,s){s(e.ui.autocomplete.filter(t,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(t,n){s.xhr&&s.xhr.abort(),s.xhr=e.ajax({url:i,data:t,dataType:"json",success:function(e){n(e)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(e){clearTimeout(this.searching),this.searching=this._delay(function(){this.term!==this._value()&&(this.selectedItem=null,this.search(null,e))},this.options.delay)},search:function(e,t){return e=null!=e?e:this._value(),this.term=this._value(),e.length<this.options.minLength?this.close(t):this._trigger("search",t)!==!1?this._search(e):undefined},_search:function(e){this.pending++,this.element.addClass("ui-autocomplete-loading"),this.cancelSearch=!1,this.source({term:e},this._response())},_response:function(){var t=++this.requestIndex;return e.proxy(function(e){t===this.requestIndex&&this.__response(e),this.pending--,this.pending||this.element.removeClass("ui-autocomplete-loading")},this)},__response:function(e){e&&(e=this._normalize(e)),this._trigger("response",null,{content:e}),!this.options.disabled&&e&&e.length&&!this.cancelSearch?(this._suggest(e),this._trigger("open")):this._close()},close:function(e){this.cancelSearch=!0,this._close(e)},_close:function(e){this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.blur(),this.isNewMenu=!0,this._trigger("close",e))},_change:function(e){this.previous!==this._value()&&this._trigger("change",e,{item:this.selectedItem})},_normalize:function(t){return t.length&&t[0].label&&t[0].value?t:e.map(t,function(t){return"string"==typeof t?{label:t,value:t}:e.extend({label:t.label||t.value,value:t.value||t.label},t)})},_suggest:function(t){var i=this.menu.element.empty();this._renderMenu(i,t),this.isNewMenu=!0,this.menu.refresh(),i.show(),this._resizeMenu(),i.position(e.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next()},_resizeMenu:function(){var e=this.menu.element;e.outerWidth(Math.max(e.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(t,i){var s=this;e.each(i,function(e,i){s._renderItemData(t,i)})},_renderItemData:function(e,t){return this._renderItem(e,t).data("ui-autocomplete-item",t)},_renderItem:function(t,i){return e("<li>").append(e("<a>").text(i.label)).appendTo(t)},_move:function(e,t){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(e)||this.menu.isLastItem()&&/^next/.test(e)?(this._value(this.term),this.menu.blur(),undefined):(this.menu[e](t),undefined):(this.search(null,t),undefined)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(e,t){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(e,t),t.preventDefault())}}),e.extend(e.ui.autocomplete,{escapeRegex:function(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,i){var s=RegExp(e.ui.autocomplete.escapeRegex(i),"i");return e.grep(t,function(e){return s.test(e.label||e.value||e)})}}),e.widget("ui.autocomplete",e.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(e){return e+(e>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var t;this._superApply(arguments),this.options.disabled||this.cancelSearch||(t=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.text(t))}})})(jQuery);(function(t){t.widget("ui.menu",{version:"1.10.4",defaultElement:"<ul>",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,t.proxy(function(t){this.options.disabled&&t.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(t){t.preventDefault()},"click .ui-state-disabled > a":function(t){t.preventDefault()},"click .ui-menu-item:has(a)":function(e){var i=t(e.target).closest(".ui-menu-item");!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&t(this.document[0].activeElement).closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){var i=t(e.currentTarget);i.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(e,i)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.children(".ui-menu-item").eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){t.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(e){t(e.target).closest(".ui-menu").length||this.collapseAll(e),this.mouseHandled=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var e=t(this);e.data("ui-menu-submenu-carat")&&e.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(e){function i(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var s,n,a,o,r,l=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:l=!1,n=this.previousFilter||"",a=String.fromCharCode(e.keyCode),o=!1,clearTimeout(this.filterTimer),a===n?o=!0:a=n+a,r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())}),s=o&&-1!==s.index(this.active.next())?this.active.nextAll(".ui-menu-item"):s,s.length||(a=String.fromCharCode(e.keyCode),r=RegExp("^"+i(a),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return r.test(t(this).children("a").text())})),s.length?(this.focus(e,s),s.length>1?(this.previousFilter=a,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}l&&e.preventDefault()},_activate:function(t){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i=this.options.icons.submenu,s=this.element.find(this.options.menus);this.element.toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length),s.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),s=e.prev("a"),n=t("<span>").addClass("ui-menu-icon ui-icon "+i).data("ui-menu-submenu-carat",!0);s.attr("aria-haspopup","true").prepend(n),e.attr("aria-labelledby",s.attr("id"))}),e=s.add(this.element),e.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),e.children(":not(.ui-menu-item)").each(function(){var e=t(this);/[^\-\u2014\u2013\s]/.test(e.text())||e.addClass("ui-widget-content ui-menu-divider")}),e.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){"icons"===t&&this.element.find(".ui-menu-icon").removeClass(this.options.icons.submenu).addClass(e.submenu),this._super(t,e)},focus:function(t,e){var i,s;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,a,o,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,a=this.activeMenu.scrollTop(),o=this.activeMenu.height(),r=e.height(),0>n?this.activeMenu.scrollTop(a+n):n+r>o&&this.activeMenu.scrollTop(a+n-o+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",t,{item:this.active}))},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.children(".ui-menu-item")[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())),undefined):(this.next(e),undefined)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.children(".ui-menu-item").first())),undefined):(this.next(e),undefined)},_hasScroll:function(){return this.element.outerHeight()<this.element.prop("scrollHeight")},select:function(e){this.active=this.active||t(e.target).closest(".ui-menu-item");var i={item:this.active};this.active.has(".ui-menu").length||this.collapseAll(e,!0),this._trigger("select",e,i)}})})(jQuery); \ No newline at end of file
diff --git a/themes/default/js/popupForm.js b/themes/default/js/popupForm.js
new file mode 100644
index 00000000..06be3f0c
--- /dev/null
+++ b/themes/default/js/popupForm.js
@@ -0,0 +1,20 @@
1$(document).ready(function() {
2
3 $("#search-form").hide();
4
5 function closeSearch() {
6 $("#search-form").toggle();
7 $("#search").toggleClass("current");
8 $("#search-arrow").toggleClass("arrow-down");
9 }
10
11 $("#search").click(function(){
12 closeSearch();
13 });
14
15 $("#search-form-close").click(function(){
16 closeSearch();
17 });
18
19
20}); \ No newline at end of file
diff --git a/themes/default/js/saveLink.js b/themes/default/js/saveLink.js
new file mode 100755
index 00000000..6dbce97e
--- /dev/null
+++ b/themes/default/js/saveLink.js
@@ -0,0 +1,101 @@
1$.fn.ready(function() {
2
3 var $bagit = $('#bagit'),
4 $bagitForm = $('#bagit-form'),
5 $bagitFormForm = $('#bagit-form-form');
6
7 /* ==========================================================================
8 bag it link and close button
9 ========================================================================== */
10
11 function toggleSaveLinkForm(url, event) {
12 $("#add-link-result").empty();
13
14 $bagit.toggleClass("active-current");
15
16 //only if bagiti link is not presented on page
17 if ( $bagit.length === 0 ) {
18 if ( event !== 'undefined' && event ) {
19 $bagitForm.css( {position:"absolute", top:event.pageY, left:event.pageX-200});
20 }
21 else {
22 $bagitForm.css( {position:"relative", top:"auto", left:"auto"});
23 }
24 }
25
26 $bagitForm.toggle();
27 $('#content').toggleClass("opacity03");
28 if (url !== 'undefined' && url) {
29 $('#plainurl').val(url);
30 }
31 $('#plainurl').focus();
32 }
33
34
35 $bagit.click(function(){
36 $bagit.toggleClass("current");
37 $("#bagit-arrow").toggleClass("arrow-down");
38 toggleSaveLinkForm();
39 });
40
41 $("#bagit-form-close").click(function(){
42 $bagit.removeClass("current");
43 $("#bagit-arrow").removeClass("arrow-down");
44 toggleSaveLinkForm();
45 });
46
47
48 //send "bag it link" form request via ajax
49 $bagitFormForm.submit( function(event) {
50 $("body").css("cursor", "wait");
51 $("#add-link-result").empty();
52
53 $.ajax({
54 type: $bagitFormForm.attr('method'),
55 url: $bagitFormForm.attr('action'),
56 data: $bagitFormForm.serialize(),
57 success: function(data) {
58 $('#add-link-result').html("Done!");
59 $('#plainurl').val('');
60 $('#plainurl').blur('');
61 $("body").css("cursor", "auto");
62 //setTimeout( function() { toggleSaveLinkForm(); }, 1000); //close form after 1000 delay
63 },
64 error: function(data) {
65 $('#add-link-result').html("Failed!");
66 $("body").css("cursor", "auto");
67 }
68 });
69
70 event.preventDefault();
71 });
72
73 /* ==========================================================================
74 Keyboard gestion
75 ========================================================================== */
76
77 $(window).keydown(function(e){
78 if ( ( e.target.tagName.toLowerCase() !== 'input' && e.keyCode == 83 ) || (e.keyCode == 27 && $bagitForm.is(':visible') ) ) {
79 $bagit.removeClass("current");
80 $("#bagit-arrow").removeClass("arrow-down");
81 toggleSaveLinkForm();
82 return false;
83 }
84 });
85
86 /* ==========================================================================
87 Process all links inside an article
88 ========================================================================== */
89
90 $("article a[href^='http']").after(function() {
91 return " <a href=\"" + $(this).attr('href') + "\" class=\"add-to-wallabag-link-after\" alt=\"add to wallabag\" title=\"add to wallabag\"></a> ";
92 });
93
94 $(".add-to-wallabag-link-after").click(function(event){
95 toggleSaveLinkForm($(this).attr('href'), event);
96 event.preventDefault();
97 });
98
99});
100
101
diff --git a/themes/default/tags.twig b/themes/default/tags.twig
index cff6b1d7..e5be748e 100644..100755
--- a/themes/default/tags.twig
+++ b/themes/default/tags.twig
@@ -4,5 +4,15 @@
4{% include '_menu.twig' %} 4{% include '_menu.twig' %}
5{% endblock %} 5{% endblock %}
6{% block content %} 6{% block content %}
7{% for tag in tags %}<a href="./?view=tag&amp;id={{ tag.id }}">{{ tag.value }}</a> {% if token != '' %}<a href="?feed&amp;type=tag&amp;user_id={{ user_id }}&amp;tag_id={{ tag.id }}&amp;token={{ token }}" target="_blank"><img src="{{ poche_url }}/themes/{{ theme }}/img/{{ theme }}/rss.png" /></a>{% endif %} {% endfor %} 7 <div class="two-column">
8 {% for tag in tags %}
9 <a href="./?view=tag&amp;id={{ tag.id }}">{{ tag.value }}</a> ({{ tag.entriescount }}) {% if token != '' %}<a href="?feed&amp;type=tag&amp;user_id={{ user_id }}&amp;tag_id={{ tag.id }}&amp;token={{ token }}" target="_blank"><img src="{{ poche_url }}themes/{{ theme }}/img/{{ theme }}/rss.png" /></a>{% endif %}
10 <br>
11
12 {% if loop.index == '%d'|format(loop.length/2 + 0.5) %}
13 </div><div class="two-column">
14 {% endif %}
15
16 {% endfor %}
17 </div>
8{% endblock %} \ No newline at end of file 18{% endblock %} \ No newline at end of file
diff --git a/themes/default/view.twig b/themes/default/view.twig
index 916abe0d..b7d48c00 100644..100755
--- a/themes/default/view.twig
+++ b/themes/default/view.twig
@@ -1,19 +1,23 @@
1{% extends "layout.twig" %} 1{% extends "layout.twig" %}
2{% block title %}{{ entry.title|raw }} ({{ entry.url | e | getDomain }}){% endblock %} 2{% block title %}{{ entry.title|raw }} ({{ entry.url | e | getDomain }}){% endblock %}
3{% block content %} 3{% block content %}
4 {% include '_pocheit-form.twig' %}
4 <div id="article_toolbar"> 5 <div id="article_toolbar">
5 <ul> 6 <ul>
6 <li><a href="./" title="{% trans "Return home" %}" class="tool back"><span>{% trans "Return home" %}</span></a></li> 7 <li><a href="./" title="{% trans "Return home" %}" class="tool back"><span>{% trans "Return home" %}</span></a></li>
7 <li><a href="#top" title="{% trans "Back to top" %}" class="tool top"><span>{% trans "Back to top" %}</span></a></li> 8 <li><a href="#top" title="{% trans "Back to top" %}" class="tool top"><span>{% trans "Back to top" %}</span></a></li>
8 <li><a href="{{ entry.url|e }}" target="_blank" title="{% trans "original" %} : {{ entry.title|e }}" class="tool link"><span>{{ entry.url | e | getDomain }}</span></a></li> 9 <li><a href="{{ entry.url|e }}" target="_blank" title="{% trans "original" %} : {{ entry.title|e }}" class="tool link"><span>{{ entry.url | e | getDomain }}</span></a></li>
9 <li><a title="{% trans "Mark as read" %}" class="tool {% if entry.is_read == 0 %}archive-off{% else %}archive{% endif %}" href="./?action=toggle_archive&amp;id={{ entry.id|e }}"><span>{% trans "Toggle mark as read" %}</span></a></li> 10 <li><a title="{% trans "Mark as read" %}" class="tool {% if entry.is_read == 0 %}archive-off{% else %}archive{% endif %}" href="javascript: void(null);" id="markAsRead"><span>{% trans "Toggle mark as read" %}</span></a></li>
10 <li><a title="{% trans "Favorite" %}" class="tool {% if entry.is_fav == 0 %}fav-off{% else %}fav{% endif %}" href="./?action=toggle_fav&amp;id={{ entry.id|e }}"><span>{% trans "Toggle favorite" %}</span></a></li> 11 <li><a title="{% trans "Favorite" %}" class="tool {% if entry.is_fav == 0 %}fav-off{% else %}fav{% endif %}" href="javascript: void(null);" id="setFav"><span>{% trans "Toggle favorite" %}</span></a></li>
11 <li><a title="{% trans "Delete" %}" class="tool delete" href="./?action=delete&amp;id={{ entry.id|e }}"><span>{% trans "Delete" %}</span></a></li> 12 <li><a title="{% trans "Delete" %}" class="tool delete" href="./?action=delete&amp;id={{ entry.id|e }}"><span>{% trans "Delete" %}</span></a></li>
12 {% if constant('SHARE_TWITTER') == 1 %}<li><a href="https://twitter.com/home?status={{entry.title|url_encode}}%20{{ entry.url|url_encode }}%20via%20@wallabagapp" target="_blank" class="tool twitter" title="{% trans "Tweet" %}"><span>{% trans "Tweet" %}</span></a></li>{% endif %} 13 {% if constant('SHARE_TWITTER') == 1 %}<li><a href="https://twitter.com/home?status={{entry.title|url_encode}}%20{{ entry.url|url_encode }}%20via%20@wallabagapp" target="_blank" class="tool twitter" title="{% trans "Tweet" %}"><span>{% trans "Tweet" %}</span></a></li>{% endif %}
13 {% if constant('SHARE_MAIL') == 1 %}<li><a href="mailto:?subject={{ entry.title|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" class="tool email" title="{% trans "Email" %}"><span>{% trans "Email" %}</span></a></li>{% endif %} 14 {% if constant('SHARE_MAIL') == 1 %}<li><a href="mailto:?subject={{ entry.title|url_encode }}&amp;body={{ entry.url|url_encode }}%20via%20@wallabagapp" class="tool email" title="{% trans "Email" %}"><span>{% trans "Email" %}</span></a></li>{% endif %}
14 {% if constant('SHARE_SHAARLI') == 1 %}<li><a href="{{ constant('SHAARLI_URL') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}" target="_blank" class="tool shaarli" title="{% trans "shaarli" %}"><span>{% trans "shaarli" %}</span></a></li>{% endif %} 15 {% if constant('SHARE_SHAARLI') == 1 %}<li><a href="{{ constant('SHAARLI_URL') }}/index.php?post={{ entry.url|url_encode }}&amp;title={{ entry.title|url_encode }}" target="_blank" class="tool shaarli" title="{% trans "shaarli" %}"><span>{% trans "shaarli" %}</span></a></li>{% endif %}
15 {% if constant('FLATTR') == 1 %}{% if flattr.status == constant('FLATTRABLE') %}<li><a href="http://flattr.com/submit/auto?url={{ entry.url }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span></a></li>{% elseif flattr.status == constant('FLATTRED') %}<li><a href="{{ flattr.flattrItemURL }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span>{{ flattr.numflattrs }}</a></li>{% endif %}{% endif %} 16 {% if constant('FLATTR') == 1 %}{% if flattr.status == constant('FLATTRABLE') %}<li><a href="http://flattr.com/submit/auto?url={{ entry.url }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span></a></li>{% elseif flattr.status == constant('FLATTRED') %}<li><a href="{{ flattr.flattrItemURL }}" class="tool flattr" target="_blank" title="{% trans "flattr" %}"><span>{% trans "flattr" %}</span>{{ flattr.numflattrs }}</a></li>{% endif %}{% endif %}
17 {% if constant('SHOW_PRINTLINK') == 1 %}<li><a title="{% trans "Print" %}" class="tool print" href="javascript: window.print();"><span>{% trans "Print" %}</span></a></li>{% endif %}
18 <li><a href="./?epub&amp;method=id&amp;id={{ entry.id|e }}" title="Generate epub file">EPUB</a></li>
16 <li><a href="mailto:hello@wallabag.org?subject=Wrong%20display%20in%20wallabag&amp;body={{ entry.url|url_encode }}" title="{% trans "Does this article appear wrong?" %}" class="tool bad-display"><span>{% trans "Does this article appear wrong?" %}</span></a></li> 19 <li><a href="mailto:hello@wallabag.org?subject=Wrong%20display%20in%20wallabag&amp;body={{ entry.url|url_encode }}" title="{% trans "Does this article appear wrong?" %}" class="tool bad-display"><span>{% trans "Does this article appear wrong?" %}</span></a></li>
20 {% if constant('SHOW_READPERCENT') == 1 %}<li><div id="readLeftPercent">0%</div></li>{% endif %}
17 </ul> 21 </ul>
18 </div> 22 </div>
19 <div id="article"> 23 <div id="article">
@@ -30,9 +34,63 @@
30 </article> 34 </article>
31 {{ block('tags') }} 35 {{ block('tags') }}
32 </div> 36 </div>
33 <script src="{{ poche_url }}/themes/{{ constant('DEFAULT_THEME') }}/js/restoreScroll.js"></script> 37 <script src="{{ poche_url }}themes/{{theme}}/js/restoreScroll.js"></script>
34 <script type="text/javascript"> 38 <script type="text/javascript">
35 $(document).ready(function() { 39 $(document).ready(function() {
40
41 // toggle read property of current article
42 $('#markAsRead').click(function(){
43 $("body").css("cursor", "wait");
44 $.ajax( { url: './?action=toggle_archive&id={{ entry.id|e }}' }).done(
45 function( data ) {
46 if ( data == '1' ) {
47 if ( $('#markAsRead').hasClass("archive-off") ) {
48 $('#markAsRead').removeClass("archive-off");
49 $('#markAsRead').addClass("archive");
50 }
51 else {
52 $('#markAsRead').removeClass("archive");
53 $('#markAsRead').addClass("archive-off");
54 }
55 }
56 else {
57 alert('Error! Pls check if you are logged in.');
58 }
59 });
60 $("body").css("cursor", "auto");
61 });
62
63 // toggle favorite property of current article
64 $('#setFav').click(function(){
65 $("body").css("cursor", "wait");
66 $.ajax( { url: './?action=toggle_fav&id={{ entry.id|e }}' }).done(
67 function( data ) {
68 if ( data == '1' ) {
69 if ( $('#setFav').hasClass("fav-off") ) {
70 $('#setFav').removeClass("fav-off");
71 $('#setFav').addClass("fav");
72 }
73 else {
74 $('#setFav').removeClass("fav");
75 $('#setFav').addClass("fav-off");
76 }
77 }
78 else {
79 alert('Error! Pls check if you are logged in.');
80 }
81 });
82 $("body").css("cursor", "auto");
83 });
84
85 // set percent of read on startup
86 if ( $(document).height() <= $(window).innerHeight() ) {
87 pp = 100;
88 }
89 else {
90 pp = 0;
91 }
92 $('#readLeftPercent').text( pp + '%' );
93
36 94
37 $(window).scroll(function(e){ 95 $(window).scroll(function(e){
38 var scrollTop = $(window).scrollTop(); 96 var scrollTop = $(window).scrollTop();
@@ -40,6 +98,10 @@
40 var scrollPercent = (scrollTop) / (docHeight); 98 var scrollPercent = (scrollTop) / (docHeight);
41 var scrollPercentRounded = Math.round(scrollPercent*100)/100; 99 var scrollPercentRounded = Math.round(scrollPercent*100)/100;
42 savePercent({{ entry.id|e }}, scrollPercentRounded); 100 savePercent({{ entry.id|e }}, scrollPercentRounded);
101
102 // change percent of read on scroll
103 pp = Math.round(scrollTop * 100 / ( docHeight - $(window).innerHeight() ));
104 $('#readLeftPercent').text( pp + '%' );
43 }); 105 });
44 106
45 retrievePercent({{ entry.id|e }}); 107 retrievePercent({{ entry.id|e }});
@@ -55,3 +117,4 @@
55 }); 117 });
56 </script> 118 </script>
57{% endblock %} 119{% endblock %}
120
diff --git a/themes/dmagenta/img/dmagenta/rss.png b/themes/dmagenta/img/dmagenta/rss.png
new file mode 100644
index 00000000..21bad1a1
--- /dev/null
+++ b/themes/dmagenta/img/dmagenta/rss.png
Binary files differ
diff --git a/themes/solarized-dark/img/solarized-dark/rss.png b/themes/solarized-dark/img/solarized-dark/rss.png
new file mode 100644
index 00000000..21bad1a1
--- /dev/null
+++ b/themes/solarized-dark/img/solarized-dark/rss.png
Binary files differ
diff --git a/themes/solarized/img/solarized/rss.png b/themes/solarized/img/solarized/rss.png
new file mode 100644
index 00000000..21bad1a1
--- /dev/null
+++ b/themes/solarized/img/solarized/rss.png
Binary files differ
diff --git a/wallabag_compatibility_test.php b/wallabag_compatibility_test.php
index 26dce018..d6f22156 100644
--- a/wallabag_compatibility_test.php
+++ b/wallabag_compatibility_test.php
@@ -1,5 +1,5 @@
1<?php 1<?php
2$app_name = 'wallabag 1'; 2$app_name = 'wallabag';
3 3
4$php_ok = (function_exists('version_compare') && version_compare(phpversion(), '5.3.3', '>=')); 4$php_ok = (function_exists('version_compare') && version_compare(phpversion(), '5.3.3', '>='));
5$pcre_ok = extension_loaded('pcre'); 5$pcre_ok = extension_loaded('pcre');
@@ -8,9 +8,11 @@ $mbstring_ok = extension_loaded('mbstring');
8$iconv_ok = extension_loaded('iconv'); 8$iconv_ok = extension_loaded('iconv');
9$tidy_ok = function_exists('tidy_parse_string'); 9$tidy_ok = function_exists('tidy_parse_string');
10$curl_ok = function_exists('curl_exec'); 10$curl_ok = function_exists('curl_exec');
11$parse_ini_ok = function_exists('parse_ini_file');
11$parallel_ok = ((extension_loaded('http') && class_exists('HttpRequestPool')) || ($curl_ok && function_exists('curl_multi_init'))); 12$parallel_ok = ((extension_loaded('http') && class_exists('HttpRequestPool')) || ($curl_ok && function_exists('curl_multi_init')));
12$allow_url_fopen_ok = (bool)ini_get('allow_url_fopen'); 13$allow_url_fopen_ok = (bool)ini_get('allow_url_fopen');
13$filter_ok = extension_loaded('filter'); 14$filter_ok = extension_loaded('filter');
15$gettext_ok = function_exists("gettext");
14 16
15if (extension_loaded('xmlreader')) { 17if (extension_loaded('xmlreader')) {
16 $xml_ok = true; 18 $xml_ok = true;
@@ -130,8 +132,6 @@ table#chart tr.enabled td {
130 132
131table#chart tr.disabled td, 133table#chart tr.disabled td,
132table#chart tr.disabled td a { 134table#chart tr.disabled td a {
133 color:#999;
134 font-style:italic;
135} 135}
136 136
137table#chart tr.disabled td a { 137table#chart tr.disabled td a {
@@ -154,12 +154,31 @@ div.chunk {
154 background-color:transparent; 154 background-color:transparent;
155 font-style:italic; 155 font-style:italic;
156} 156}
157
158.good{
159background-color:#52CC5B;
160}
161.bad{
162background-color:#F74343;
163font-style:italic;
164font-weight: bold;
165}
166.pass{
167background-color:#FF9500;
168}
169
157</style> 170</style>
158 171
159</head> 172</head>
160 173
161<body> 174<body>
162 175<?php
176$frominstall = false;
177if (isset($_GET['from'])){
178 if ($_GET['from'] == 'install'){
179 $frominstall = true;
180 }}
181?>
163<div id="site"> 182<div id="site">
164 <div id="content"> 183 <div id="content">
165 184
@@ -177,58 +196,68 @@ div.chunk {
177 <tr class="<?php echo ($php_ok) ? 'enabled' : 'disabled'; ?>"> 196 <tr class="<?php echo ($php_ok) ? 'enabled' : 'disabled'; ?>">
178 <td>PHP</td> 197 <td>PHP</td>
179 <td>5.3.3 or higher</td> 198 <td>5.3.3 or higher</td>
180 <td><?php echo phpversion(); ?></td> 199 <td class="<?php echo ($php_ok) ? 'good' : 'disabled'; ?>"><?php echo phpversion(); ?></td>
181 </tr> 200 </tr>
182 <tr class="<?php echo ($xml_ok) ? 'enabled, and sane' : 'disabled, or broken'; ?>"> 201 <tr class="<?php echo ($xml_ok) ? 'enabled' : 'disabled'; ?>">
183 <td><a href="http://php.net/xml">XML</a></td> 202 <td><a href="http://php.net/xml">XML</a></td>
184 <td>Enabled</td> 203 <td>Enabled</td>
185 <td><?php echo ($xml_ok) ? 'Enabled, and sane' : 'Disabled, or broken'; ?></td> 204 <?php echo ($xml_ok) ? '<td class="good">Enabled, and sane</span>' : '<td class="bad">Disabled, or broken'; ?></td>
186 </tr> 205 </tr>
187 <tr class="<?php echo ($pcre_ok) ? 'enabled' : 'disabled'; ?>"> 206 <tr class="<?php echo ($pcre_ok) ? 'enabled' : 'disabled'; ?>">
188 <td><a href="http://php.net/pcre">PCRE</a></td> 207 <td><a href="http://php.net/pcre">PCRE</a></td>
189 <td>Enabled</td> 208 <td>Enabled</td>
190 <td><?php echo ($pcre_ok) ? 'Enabled' : 'Disabled'; ?></td> 209 <?php echo ($pcre_ok) ? '<td class="good">Enabled' : '<td class="bad">Disabled'; ?></td>
191 </tr> 210 </tr>
192<!-- <tr class="<?php echo ($zlib_ok) ? 'enabled' : 'disabled'; ?>"> 211<!-- <tr class="<?php echo ($zlib_ok) ? 'enabled' : 'disabled'; ?>">
193 <td><a href="http://php.net/zlib">Zlib</a></td> 212 <td><a href="http://php.net/zlib">Zlib</a></td>
194 <td>Enabled</td> 213 <td>Enabled</td>
195 <td><?php echo ($zlib_ok) ? 'Enabled' : 'Disabled'; ?></td> 214 <?php echo ($zlib_ok) ? '<td class="good">Enabled' : '<td class="bad">Disabled'; ?></td>
196 </tr> --> 215 </tr> -->
197<!-- <tr class="<?php echo ($mbstring_ok) ? 'enabled' : 'disabled'; ?>"> 216<!-- <tr class="<?php echo ($mbstring_ok) ? 'enabled' : 'disabled'; ?>">
198 <td><a href="http://php.net/mbstring">mbstring</a></td> 217 <td><a href="http://php.net/mbstring">mbstring</a></td>
199 <td>Enabled</td> 218 <td>Enabled</td>
200 <td><?php echo ($mbstring_ok) ? 'Enabled' : 'Disabled'; ?></td> 219 <?php echo ($mbstring_ok) ? '<td class="good">Enabled' : '<td class="bad">Disabled'; ?></td>
201 </tr> --> 220 </tr> -->
202<!-- <tr class="<?php echo ($iconv_ok) ? 'enabled' : 'disabled'; ?>"> 221<!-- <tr class="<?php echo ($iconv_ok) ? 'enabled' : 'disabled'; ?>">
203 <td><a href="http://php.net/iconv">iconv</a></td> 222 <td><a href="http://php.net/iconv">iconv</a></td>
204 <td>Enabled</td> 223 <td>Enabled</td>
205 <td><?php echo ($iconv_ok) ? 'Enabled' : 'Disabled'; ?></td> 224 <?php echo ($iconv_ok) ? '<td class="good">Enabled' : '<td class="bad">Disabled'; ?></td>
206 </tr> --> 225 </tr> -->
207 <tr class="<?php echo ($filter_ok) ? 'enabled' : 'disabled'; ?>"> 226 <tr class="<?php echo ($filter_ok) ? 'enabled' : 'disabled'; ?>">
208 <td><a href="http://uk.php.net/manual/en/book.filter.php">Data filtering</a></td> 227 <td><a href="http://uk.php.net/manual/en/book.filter.php">Data filtering</a></td>
209 <td>Enabled</td> 228 <td>Enabled</td>
210 <td><?php echo ($filter_ok) ? 'Enabled' : 'Disabled'; ?></td> 229 <?php echo ($filter_ok) ? '<td class="good">Enabled' : '<td class="pass">Disabled'; ?></td>
211 </tr> 230 </tr>
212 <tr class="<?php echo ($tidy_ok) ? 'enabled' : 'disabled'; ?>"> 231 <tr class="<?php echo ($tidy_ok) ? 'enabled' : 'disabled'; ?>">
213 <td><a href="http://php.net/tidy">Tidy</a></td> 232 <td><a href="http://php.net/tidy">Tidy</a></td>
214 <td>Enabled</td> 233 <td>Enabled</td>
215 <td><?php echo ($tidy_ok) ? 'Enabled' : 'Disabled'; ?></td> 234 <?php echo ($tidy_ok) ? '<td class="good">Enabled' : '<td class="pass">Disabled'; ?></td>
216 </tr> 235 </tr>
217 <tr class="<?php echo ($curl_ok) ? 'enabled' : 'disabled'; ?>"> 236 <tr class="<?php echo ($curl_ok) ? 'enabled' : 'disabled'; ?>">
218 <td><a href="http://php.net/curl">cURL</a></td> 237 <td><a href="http://php.net/curl">cURL</a></td>
219 <td>Enabled</td> 238 <td>Enabled</td>
220 <td><?php echo (extension_loaded('curl')) ? 'Enabled' : 'Disabled'; ?></td> 239 <?php echo (extension_loaded('curl')) ? '<td class="good">Enabled' : '<td class="pass">Disabled'; ?></td>
221 </tr> 240 </tr>
241 <tr class="<?php echo ($parse_ini_ok) ? 'enabled' : 'disabled'; ?>">
242 <td><a href="http://uk.php.net/manual/en/function.parse-ini-file.php">Parse ini file</td>
243 <td>Enabled</td>
244 <?php echo ($parse_ini_ok) ? '<td class="good">Enabled' : '<td class="bad">Disabled'; ?></td>
245 </tr>
222 <tr class="<?php echo ($parallel_ok) ? 'enabled' : 'disabled'; ?>"> 246 <tr class="<?php echo ($parallel_ok) ? 'enabled' : 'disabled'; ?>">
223 <td>Parallel URL fetching</td> 247 <td>Parallel URL fetching</td>
224 <td>Enabled</td> 248 <td>Enabled</td>
225 <td><?php echo ($parallel_ok) ? 'Enabled' : 'Disabled'; ?></td> 249 <?php echo ($parallel_ok) ? '<td class="good">Enabled' : '<td class="pass">Disabled'; ?></td>
226 </tr> 250 </tr>
227 <tr class="<?php echo ($allow_url_fopen_ok) ? 'enabled' : 'disabled'; ?>"> 251 <tr class="<?php echo ($allow_url_fopen_ok) ? 'enabled' : 'disabled'; ?>">
228 <td><a href="http://www.php.net/manual/en/filesystem.configuration.php#ini.allow-url-fopen">allow_url_fopen</a></td> 252 <td><a href="http://www.php.net/manual/en/filesystem.configuration.php#ini.allow-url-fopen">allow_url_fopen</a></td>
229 <td>Enabled</td> 253 <td>Enabled</td>
230 <td><?php echo ($allow_url_fopen_ok) ? 'Enabled' : 'Disabled'; ?></td> 254 <?php echo ($allow_url_fopen_ok) ? '<td class="good">Enabled' : '<td class="bad">Disabled'; ?></td>
231 </tr> 255 </tr>
256 <tr class="<?php echo ($gettext_ok) ? 'enabled' : 'disabled'; ?>">
257 <td><a href="http://php.net/manual/en/book.gettext.php">gettext</a></td>
258 <td>Enabled</td>
259 <?php echo ($gettext_ok) ? '<td class="good">Enabled' : '<td class="bad">Disabled'; ?></td>
260 </tr>
232 </tbody> 261 </tbody>
233 </table> 262 </table>
234 </div> 263 </div>
@@ -237,7 +266,7 @@ div.chunk {
237 <h3>What does this mean?</h3> 266 <h3>What does this mean?</h3>
238 <ol> 267 <ol>
239 <?php //if ($php_ok && $xml_ok && $pcre_ok && $mbstring_ok && $iconv_ok && $filter_ok && $zlib_ok && $tidy_ok && $curl_ok && $parallel_ok && $allow_url_fopen_ok): ?> 268 <?php //if ($php_ok && $xml_ok && $pcre_ok && $mbstring_ok && $iconv_ok && $filter_ok && $zlib_ok && $tidy_ok && $curl_ok && $parallel_ok && $allow_url_fopen_ok): ?>
240 <?php if ($php_ok && $xml_ok && $pcre_ok && $filter_ok && $tidy_ok && $curl_ok && $parallel_ok && $allow_url_fopen_ok): ?> 269 <?php if ($php_ok && $xml_ok && $pcre_ok && $filter_ok && $tidy_ok && $curl_ok && $parallel_ok && $allow_url_fopen_ok && $gettext_ok && $parse_ini_ok): ?>
241 <li><em>You have everything you need to run <?php echo $app_name; ?> properly! Congratulations!</em></li> 270 <li><em>You have everything you need to run <?php echo $app_name; ?> properly! Congratulations!</em></li>
242 <?php else: ?> 271 <?php else: ?>
243 <?php if ($php_ok): ?> 272 <?php if ($php_ok): ?>
@@ -250,59 +279,72 @@ div.chunk {
250 <?php if ($allow_url_fopen_ok): ?> 279 <?php if ($allow_url_fopen_ok): ?>
251 <li><strong>allow_url_fopen:</strong> You have allow_url_fopen enabled. <em>No problems here.</em></li> 280 <li><strong>allow_url_fopen:</strong> You have allow_url_fopen enabled. <em>No problems here.</em></li>
252 281
253 <?php if ($filter_ok): ?> 282 <?php if ($gettext_ok): ?>
254 <li><strong>Data filtering:</strong> You have the PHP filter extension enabled. <em>No problems here.</em></li> 283 <li><strong>Gettext:</strong> You have <code>gettext</code> enabled. <em>No problems here.</em></li>
255 284
256 <?php if ($zlib_ok): ?> 285 <?php if ($parse_ini_ok): ?>
257 <li><strong>Zlib:</strong> You have <code>Zlib</code> enabled. This allows SimplePie to support GZIP-encoded feeds. <em>No problems here.</em></li> 286
258 <?php else: ?> 287 <?php if ($filter_ok): ?>
259 <li><strong>Zlib:</strong> The <code>Zlib</code> extension is not available. SimplePie will ignore any GZIP-encoding, and instead handle feeds as uncompressed text.</li> 288 <li><strong>Data filtering:</strong> You have the PHP filter extension enabled. <em>No problems here.</em></li>
260 <?php endif; ?> 289
261 290 <?php if ($zlib_ok): ?>
262 <?php if ($mbstring_ok && $iconv_ok): ?> 291 <li><strong>Zlib:</strong> You have <code>Zlib</code> enabled. This allows SimplePie to support GZIP-encoded feeds. <em>No problems here.</em></li>
263 <li><strong>mbstring and iconv:</strong> You have both <code>mbstring</code> and <code>iconv</code> installed! This will allow <?php echo $app_name; ?> to handle the greatest number of languages. <em>No problems here.</em></li> 292 <?php else: ?>
264 <?php elseif ($mbstring_ok): ?> 293 <li><strong>Zlib:</strong> The <code>Zlib</code> extension is not available. SimplePie will ignore any GZIP-encoding, and instead handle feeds as uncompressed text.</li>
265 <li><strong>mbstring:</strong> <code>mbstring</code> is installed, but <code>iconv</code> is not.</li> 294 <?php endif; ?>
266 <?php elseif ($iconv_ok): ?> 295
267 <li><strong>iconv:</strong> <code>iconv</code> is installed, but <code>mbstring</code> is not.</li> 296 <?php if ($mbstring_ok && $iconv_ok): ?>
268 <?php else: ?> 297 <li><strong>mbstring and iconv:</strong> You have both <code>mbstring</code> and <code>iconv</code> installed! This will allow <?php echo $app_name; ?> to handle the greatest number of languages. <em>No problems here.</em></li>
269 <li><strong>mbstring and iconv:</strong> <em>You do not have either of the extensions installed.</em> This will significantly impair your ability to read non-English feeds, as well as even some English ones.</li> 298 <?php elseif ($mbstring_ok): ?>
270 <?php endif; ?> 299 <li><strong>mbstring:</strong> <code>mbstring</code> is installed, but <code>iconv</code> is not.</li>
271 300 <?php elseif ($iconv_ok): ?>
272 <?php if ($tidy_ok): ?> 301 <li><strong>iconv:</strong> <code>iconv</code> is installed, but <code>mbstring</code> is not.</li>
273 <li><strong>Tidy:</strong> You have <code>Tidy</code> support installed. <em>No problems here.</em></li> 302 <?php else: ?>
274 <?php else: ?> 303 <li><strong>mbstring and iconv:</strong> <em>You do not have either of the extensions installed.</em> This will significantly impair your ability to read non-English feeds, as well as even some English ones.</li>
275 <li><strong>Tidy:</strong> The <code>Tidy</code> extension is not available. <?php echo $app_name; ?> should still work with most feeds, but you may experience problems with some.</li> 304 <?php endif; ?>
276 <?php endif; ?> 305
306 <?php if ($tidy_ok): ?>
307 <li><strong>Tidy:</strong> You have <code>Tidy</code> support installed. <em>No problems here.</em></li>
308 <?php else: ?>
309 <li><strong>Tidy:</strong> The <code>Tidy</code> extension is not available. <?php echo $app_name; ?> should still work with most feeds, but you may experience problems with some.</li>
310 <?php endif; ?>
311
312 <?php if ($curl_ok): ?>
313 <li><strong>cURL:</strong> You have <code>cURL</code> support installed. <em>No problems here.</em></li>
314 <?php else: ?>
315 <li><strong>cURL:</strong> The <code>cURL</code> extension is not available. SimplePie will use <code>fsockopen()</code> instead.</li>
316 <?php endif; ?>
317
318 <?php if ($parallel_ok): ?>
319 <li><strong>Parallel URL fetching:</strong> You have <code>HttpRequestPool</code> or <code>curl_multi</code> support installed. <em>No problems here.</em></li>
320 <?php else: ?>
321 <li><strong>Parallel URL fetching:</strong> <code>HttpRequestPool</code> or <code>curl_multi</code> support is not available. <?php echo $app_name; ?> will use <code>file_get_contents()</code> instead to fetch URLs sequentially rather than in parallel.</li>
322 <?php endif; ?>
323
324 <?php else: ?>
325 <li><strong>Data filtering:</strong> Your PHP configuration has the filter extension disabled. <strong><?php echo $app_name; ?> will not work here.</strong></li>
326 <?php endif; ?>
327
328 <?php else : ?>
329 <li><strong>Parse ini files function :</strong> Bad luck : your webhost has decided to block the use of the <em>parse_ini_file</em> function. <strong><?php echo $app_name; ?> will not work here.</strong>
330 <?php endif; ?>
277 331
278 <?php if ($curl_ok): ?>
279 <li><strong>cURL:</strong> You have <code>cURL</code> support installed. <em>No problems here.</em></li>
280 <?php else: ?>
281 <li><strong>cURL:</strong> The <code>cURL</code> extension is not available. SimplePie will use <code>fsockopen()</code> instead.</li>
282 <?php endif; ?>
283
284 <?php if ($parallel_ok): ?>
285 <li><strong>Parallel URL fetching:</strong> You have <code>HttpRequestPool</code> or <code>curl_multi</code> support installed. <em>No problems here.</em></li>
286 <?php else: ?>
287 <li><strong>Parallel URL fetching:</strong> <code>HttpRequestPool</code> or <code>curl_multi</code> support is not available. <?php echo $app_name; ?> will use <code>file_get_contents()</code> instead to fetch URLs sequentially rather than in parallel.</li>
288 <?php endif; ?>
289
290 <?php else: ?> 332 <?php else: ?>
291 <li><strong>Data filtering:</strong> Your PHP configuration has the filter extension disabled. <em><?php echo $app_name; ?> will not work here.</em></li> 333 <li><strong>GetText:</strong> The <code>gettext</code> extension is not available. The system we use to display wallabag in various languages is not available. <strong><?php echo $app_name; ?> will not work here.</strong></li>
292 <?php endif; ?> 334 <?php endif; ?>
293 335
294 <?php else: ?> 336 <?php else: ?>
295 <li><strong>allow_url_fopen:</strong> Your PHP configuration has allow_url_fopen disabled. <em><?php echo $app_name; ?> will not work here.</em></li> 337 <li><strong>allow_url_fopen:</strong> Your PHP configuration has allow_url_fopen disabled. <strong><?php echo $app_name; ?> will not work here.</strong></li>
296 <?php endif; ?> 338 <?php endif; ?>
297 339
298 <?php else: ?> 340 <?php else: ?>
299 <li><strong>PCRE:</strong> Your PHP installation doesn't support Perl-Compatible Regular Expressions. <em><?php echo $app_name; ?> will not work here.</em></li> 341 <li><strong>PCRE:</strong> Your PHP installation doesn't support Perl-Compatible Regular Expressions. <strong><?php echo $app_name; ?> will not work here.</strong></li>
300 <?php endif; ?> 342 <?php endif; ?>
301 <?php else: ?> 343 <?php else: ?>
302 <li><strong>XML:</strong> Your PHP installation doesn't support XML parsing. <em><?php echo $app_name; ?> will not work here.</em></li> 344 <li><strong>XML:</strong> Your PHP installation doesn't support XML parsing. <strong><?php echo $app_name; ?> will not work here.</strong></li>
303 <?php endif; ?> 345 <?php endif; ?>
304 <?php else: ?> 346 <?php else: ?>
305 <li><strong>PHP:</strong> You are running an unsupported version of PHP. <em><?php echo $app_name; ?> will not work here.</em></li> 347 <li><strong>PHP:</strong> You are running an unsupported version of PHP. <strong><?php echo $app_name; ?> will not work here.</strong></li>
306 <?php endif; ?> 348 <?php endif; ?>
307 <?php endif; ?> 349 <?php endif; ?>
308 </ol> 350 </ol>
@@ -310,16 +352,26 @@ div.chunk {
310 352
311 <div class="chunk"> 353 <div class="chunk">
312 <?php //if ($php_ok && $xml_ok && $pcre_ok && $mbstring_ok && $iconv_ok && $filter_ok && $allow_url_fopen_ok) { ?> 354 <?php //if ($php_ok && $xml_ok && $pcre_ok && $mbstring_ok && $iconv_ok && $filter_ok && $allow_url_fopen_ok) { ?>
313 <?php if ($php_ok && $xml_ok && $pcre_ok && $filter_ok && $allow_url_fopen_ok) { ?> 355 <?php if ($php_ok && $xml_ok && $pcre_ok && $filter_ok && $allow_url_fopen_ok && $gettext_ok && $parse_ini_ok) { ?>
314 <h3>Bottom Line: Yes, you can!</h3> 356 <h3>Bottom Line: Yes, you can!</h3>
315 <p><em>Your webhost has its act together!</em></p> 357 <p><em>Your webhost has its act together!</em></p>
358 <?php if (!$frominstall) { ?>
316 <p>You can download the latest version of <?php echo $app_name; ?> from <a href="http://wallabag.org/download">wallabag.org</a>.</p> 359 <p>You can download the latest version of <?php echo $app_name; ?> from <a href="http://wallabag.org/download">wallabag.org</a>.</p>
360 <p>If you already have done that, you should access <a href="index.php">the index.php file</a> of your installation to configure and/or start using wallabag</p>
361 <?php } else { ?>
362 <p>You can now <a href="index.php">return to the installation section</a>.</p>
363 <?php } ?>
317 <p><strong>Note</strong>: Passing this test does not guarantee that <?php echo $app_name; ?> will run on your webhost &mdash; it only ensures that the basic requirements have been addressed. If you experience any problems, please let us know.</p> 364 <p><strong>Note</strong>: Passing this test does not guarantee that <?php echo $app_name; ?> will run on your webhost &mdash; it only ensures that the basic requirements have been addressed. If you experience any problems, please let us know.</p>
318 <?php //} else if ($php_ok && $xml_ok && $pcre_ok && $mbstring_ok && $allow_url_fopen_ok && $filter_ok) { ?> 365 <?php //} else if ($php_ok && $xml_ok && $pcre_ok && $mbstring_ok && $allow_url_fopen_ok && $filter_ok) { ?>
319 <?php } else if ($php_ok && $xml_ok && $pcre_ok && $allow_url_fopen_ok && $filter_ok) { ?> 366 <?php } else if ($php_ok && $xml_ok && $pcre_ok && $allow_url_fopen_ok && $filter_ok && $gettext_ok && $parse_ini_ok) { ?>
320 <h3>Bottom Line: Yes, you can!</h3> 367 <h3>Bottom Line: Yes, you can!</h3>
321 <p><em>For most feeds, it'll run with no problems.</em> There are certain languages that you might have a hard time with though.</p> 368 <p><em>For most feeds, it'll run with no problems.</em> There are certain languages that you might have a hard time with though.</p>
369 <?php if (!$frominstall) { ?>
322 <p>You can download the latest version of <?php echo $app_name; ?> from <a href="http://wallabag.org/download">wallabag.org</a>.</p> 370 <p>You can download the latest version of <?php echo $app_name; ?> from <a href="http://wallabag.org/download">wallabag.org</a>.</p>
371 <p>If you already have done that, you should access <a href="index.php">the index.php file</a> of your installation to configure and/or start using wallabag</p>
372 <?php } else { ?>
373 <p>You can now <a href="index.php">return to the installation section</a>.</p>
374 <?php } ?>
323 <p><strong>Note</strong>: Passing this test does not guarantee that <?php echo $app_name; ?> will run on your webhost &mdash; it only ensures that the basic requirements have been addressed. If you experience any problems, please let us know.</p> 375 <p><strong>Note</strong>: Passing this test does not guarantee that <?php echo $app_name; ?> will run on your webhost &mdash; it only ensures that the basic requirements have been addressed. If you experience any problems, please let us know.</p>
324 <?php } else { ?> 376 <?php } else { ?>
325 <h3>Bottom Line: We're sorry…</h3> 377 <h3>Bottom Line: We're sorry…</h3>