From: Nicolas Lœuillet Date: Thu, 3 Apr 2014 12:42:03 +0000 (+0200) Subject: Merge pull request #595 from wallabag/dev X-Git-Tag: 1.6.0 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=0d67b00d5d3b7ce1b76b639dcc65c415a5f13439;hp=99679d06884120c57f43b44e55e03595f1f87bed;p=github%2Fwallabag%2Fwallabag.git Merge pull request #595 from wallabag/dev wallabag 1.6.0 --- diff --git a/.gitignore b/.gitignore index 2abed7ed..aec2e3ef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ cache/* vendor composer.phar db/poche.sqlite -inc/poche/config.inc.php \ No newline at end of file +inc/poche/config.inc.php +inc/3rdparty/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/ \ No newline at end of file diff --git a/TRANSLATION.md b/TRANSLATION.md new file mode 100755 index 00000000..2e38d5cc --- /dev/null +++ b/TRANSLATION.md @@ -0,0 +1,67 @@ +# How to manage translations of wallabag + +This guide will describe procedure of translation management of wallabag web application. + +All translation are made using [gettext](http://en.wikipedia.org/wiki/Gettext) system and tools. + +You 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. + +You 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. + +**It's assumed, that you have wallabag installed locally on your computer or on the server you have access to.** + +## To change existing translation you will need to do: + +### 1. Clear cache +You 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) + +OR + +from command line: +go to root of your installation of wallabag project and run next command: + +`rm -rf ./cache/*` + +(this may require root privileges if you run, for example Apatche web server with mod_php) + +### 2. Generate php files from all twig templates +Do this using next command: + +`php ./locale/tools/fillCache.php` + +OR + +from your browser: **http://your-wallabag-host.com/locale/tools/fillCache.php** (this may require removal of .htacces file in locale/ directory). + +### 3. Configure your Poedit +Open Poedit editor, open Edit->Preferences. Go to "Parsers" tab, click on PHP and press "Edit" button. Make sure your "Parser command:" looks like + +`xgettext --no-location --force-po -o %o %C %K %F` + +Usualy it is required to add "--no-location" to default value. + +### 4. Open .po file you want to edit in Poedit and change it's settings +Open, for example `locale/pl_PL.utf8/LC_MESSAGES/pl_PL.utf8.po` file in your Poedit. + +Go 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. + +You can also check "project into" tab to be sure, that "Language" is set correctly (this will allow you to spell check your translation). + +### 5. Update opened .po file from sources +Once 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. + +As 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). + +### 6. Translate and save your .po file +If you have any dificulties on this step, please consult with Poedit manual. +Every time you save your .po file, Poedit will also comple appropriate .mo file by default (of course, if not disabled in preferences). + +So, you are almost done. + +### 7. Clear cache again +This 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). + + +##To create new translation +Please simple create appropriate directories in locale folder and perform all steps, described above. Instead of opening an existing file just create new one. + 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', '<')) { } } -// Check PDO Sqlite -if (! extension_loaded('pdo_sqlite')) { - die('PHP extension required: pdo_sqlite'); -} - -// Check ZIP -if (! extension_loaded('zip')) { - die('PHP extension required: zip'); -} - // Check if /cache is writeable if (! is_writable('cache')) { 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 public static $sessionName = ''; // If the user does not access any page within this time, // his/her session is considered expired (3600 sec. = 1 hour) - public static $inactivityTimeout = 86400; + public static $inactivityTimeout = 3600; // Extra timeout for long sessions (if enabled) (82800 sec. = 23 hours) - public static $longSessionTimeout = 31536000; + public static $longSessionTimeout = 7776000; // 7776000 = 90 days // If you get disconnected often or if your IP address changes often. // Let you disable session cookie hijacking protection public static $disableSessionProtection = false; @@ -48,8 +48,13 @@ class Session /** * Initialize session */ - public static function init() + public static function init($longlastingsession = false) { + //check if session name is correct + if ( (session_id() && !empty(self::$sessionName) && session_name()!=self::$sessionName) || $longlastingsession ) { + session_destroy(); + } + // Force cookie path (but do not change lifetime) $cookie = session_get_cookie_params(); // Default cookie expiration and path. @@ -61,12 +66,22 @@ class Session if (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") { $ssl = true; } - session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['HTTP_HOST'], $ssl); + + if ( $longlastingsession ) { + session_set_cookie_params(self::$longSessionTimeout, $cookiedir, null, $ssl, true); + } + else { + session_set_cookie_params(0, $cookiedir, null, $ssl, true); + } + //set server side valid session timeout + //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 + ini_set('session.gc_maxlifetime', self::$longSessionTimeout); + // Use cookies to store session. ini_set('session.use_cookies', 1); // Force cookies for session (phpsessionID forbidden in URL) ini_set('session.use_only_cookies', 1); - if (!session_id()) { + if ( !session_id() ) { // Prevent php to use sessionID in URL if cookies are disabled. ini_set('session.use_trans_sid', false); if (!empty(self::$sessionName)) { @@ -115,6 +130,9 @@ class Session if (self::banCanLogin()) { if ($login === $loginTest && $password === $passwordTest) { self::banLoginOk(); + + self::init($longlastingsession); + // Generate unique random number to sign forms (HMAC) $_SESSION['uid'] = sha1(uniqid('', true).'_'.mt_rand()); $_SESSION['ip'] = self::_allIPs(); @@ -135,6 +153,7 @@ class Session self::banLoginFailed(); } + self::init(); return false; } @@ -143,7 +162,14 @@ class Session */ public static function logout() { - unset($_SESSION['uid'],$_SESSION['ip'],$_SESSION['expires_on'],$_SESSION['tokens'], $_SESSION['login'], $_SESSION['pass'], $_SESSION['longlastingsession'], $_SESSION['poche_user']); + // unset($_SESSION['uid'],$_SESSION['ip'],$_SESSION['expires_on'],$_SESSION['tokens'], $_SESSION['login'], $_SESSION['pass'], $_SESSION['longlastingsession'], $_SESSION['poche_user']); + + // Destruction du cookie (le code peut paraître complexe mais c'est pour être certain de reprendre les mêmes paramètres) + $args = array_merge(array(session_name(), ''), array_values(session_get_cookie_params())); + $args[2] = time() - 3600; + call_user_func_array('setcookie', $args); + // Suppression physique de la session + session_destroy(); } /** @@ -157,7 +183,7 @@ class Session || (self::$disableSessionProtection === false && $_SESSION['ip'] !== self::_allIPs()) || time() >= $_SESSION['expires_on']) { - self::logout(); + //self::logout(); return false; } 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 { $this->msgId = md5(uniqid()); // Create the session array if it doesnt already exist + settype($_SESSION, 'array'); if( !array_key_exists('flash_messages', $_SESSION) ) $_SESSION['flash_messages'] = array(); } @@ -228,4 +229,4 @@ class Messages { } // end class -?> \ No newline at end of file +?> diff --git a/inc/3rdparty/libraries/feedwriter/FeedItem.php b/inc/3rdparty/libraries/feedwriter/FeedItem.php index 9373deeb..0eae5e08 100644 --- a/inc/3rdparty/libraries/feedwriter/FeedItem.php +++ b/inc/3rdparty/libraries/feedwriter/FeedItem.php @@ -156,6 +156,7 @@ if($this->version == RSS2 || $this->version == RSS1) { $this->setElement('link', $link); + $this->setElement('guid', $link); } else { diff --git a/inc/3rdparty/libraries/feedwriter/FeedWriter.php b/inc/3rdparty/libraries/feedwriter/FeedWriter.php old mode 100644 new mode 100755 index adb2526c..5d16e765 --- a/inc/3rdparty/libraries/feedwriter/FeedWriter.php +++ b/inc/3rdparty/libraries/feedwriter/FeedWriter.php @@ -9,9 +9,9 @@ define('JSONP', 3, true); * Genarate RSS2 or JSON (original: RSS 1.0, RSS2.0 and ATOM Feed) * * Modified for FiveFilters.org's Full-Text RSS project - * to allow for inclusion of hubs, JSON output. + * to allow for inclusion of hubs, JSON output. * Stripped RSS1 and ATOM support. - * + * * @package UnivarselFeedWriter * @author Anis uddin Ahmad * @link http://www.ajaxray.com/projects/rss @@ -26,32 +26,32 @@ define('JSONP', 3, true); private $CDATAEncoding = array(); // The tag names which have to encoded as CDATA private $xsl = null; // stylesheet to render RSS (used by Chrome) private $json = null; // JSON object - - private $version = null; - + + private $version = null; + /** * Constructor - * - * @param constant the version constant (RSS2 or JSON). - */ + * + * @param constant the version constant (RSS2 or JSON). + */ function __construct($version = RSS2) - { + { $this->version = $version; - + // Setting default value for assential channel elements $this->channels['title'] = $version . ' Feed'; $this->channels['link'] = 'http://www.ajaxray.com/blog'; - + //Tag names to encode in CDATA $this->CDATAEncoding = array('description', 'content:encoded', 'content', 'subtitle', 'summary'); } - + public function setFormat($format) { $this->version = $format; } // Start # public functions --------------------------------------------- - + /** * Set a channel element * @access public @@ -63,11 +63,11 @@ define('JSONP', 3, true); { $this->channels[$elementName] = $content ; } - + /** - * Set multiple channel elements from an array. Array elements + * Set multiple channel elements from an array. Array elements * should be 'channelName' => 'channelContent' format. - * + * * @access public * @param array array of channels * @return void @@ -75,30 +75,30 @@ define('JSONP', 3, true); public function setChannelElementsFromArray($elementArray) { if(! is_array($elementArray)) return; - foreach ($elementArray as $elementName => $content) + foreach ($elementArray as $elementName => $content) { $this->setChannelElement($elementName, $content); } } - + /** * Genarate the actual RSS/JSON file - * + * * @access public * @return void - */ + */ public function genarateFeed() { if ($this->version == RSS2) { - header('Content-type: text/xml; charset=UTF-8'); +// header('Content-type: text/xml; charset=UTF-8'); // this line prevents Chrome 20 from prompting download // used by Google: https://news.google.com/news/feeds?ned=us&topic=b&output=rss - header('X-content-type-options: nosniff'); +// header('X-content-type-options: nosniff'); } elseif ($this->version == JSON) { - header('Content-type: application/json; charset=UTF-8'); +// header('Content-type: application/json; charset=UTF-8'); $this->json = new stdClass(); } elseif ($this->version == JSONP) { - header('Content-type: application/javascript; charset=UTF-8'); +// header('Content-type: application/javascript; charset=UTF-8'); $this->json = new stdClass(); } $this->printHead(); @@ -109,10 +109,10 @@ define('JSONP', 3, true); echo json_encode($this->json); } } - + /** * Create a new FeedItem. - * + * * @access public * @return object instance of FeedItem class */ @@ -121,24 +121,24 @@ define('JSONP', 3, true); $Item = new FeedItem($this->version); return $Item; } - + /** * Add a FeedItem to the main class - * + * * @access public * @param object instance of FeedItem class * @return void */ public function addItem($feedItem) { - $this->items[] = $feedItem; + $this->items[] = $feedItem; } - + // Wrapper functions ------------------------------------------------------------------- - + /** * Set the 'title' channel element - * + * * @access public * @param srting value of 'title' channel tag * @return void @@ -147,59 +147,59 @@ define('JSONP', 3, true); { $this->setChannelElement('title', $title); } - + /** * Add a hub to the channel element - * + * * @access public * @param string URL * @return void */ public function addHub($hub) { - $this->hubs[] = $hub; + $this->hubs[] = $hub; } - + /** * Set XSL URL - * + * * @access public * @param string URL * @return void */ public function setXsl($xsl) { - $this->xsl = $xsl; - } - + $this->xsl = $xsl; + } + /** * Set self URL - * + * * @access public * @param string URL * @return void */ public function setSelf($self) { - $this->self = $self; - } - + $this->self = $self; + } + /** * Set the 'description' channel element - * + * * @access public * @param srting value of 'description' channel tag * @return void */ public function setDescription($desciption) { - $tag = ($this->version == ATOM)? 'subtitle' : 'description'; + $tag = ($this->version == ATOM)? 'subtitle' : 'description'; $this->setChannelElement($tag, $desciption); } - + /** * Set the 'link' channel element - * + * * @access public * @param srting value of 'link' channel tag * @return void @@ -208,10 +208,10 @@ define('JSONP', 3, true); { $this->setChannelElement('link', $link); } - + /** * Set the 'image' channel element - * + * * @access public * @param srting title of image * @param srting link url of the imahe @@ -222,14 +222,14 @@ define('JSONP', 3, true); { $this->setChannelElement('image', array('title'=>$title, 'link'=>$link, 'url'=>$url)); } - + // End # public functions ---------------------------------------------- - + // Start # private functions ---------------------------------------------- - + /** * Prints the xml and rss namespace - * + * * @access private * @return void */ @@ -247,10 +247,10 @@ define('JSONP', 3, true); $this->json->rss = array('@attributes' => array('version' => '2.0')); } } - + /** * Closes the open tags at the end of file - * + * * @access private * @return void */ @@ -258,14 +258,14 @@ define('JSONP', 3, true); { if ($this->version == RSS2) { - echo '',PHP_EOL,''; - } + echo '',PHP_EOL,''; + } // do nothing for JSON } /** * Creates a single node as xml format - * + * * @access private * @param string name of the tag * @param mixed tag value as string or array of nested tags in 'tagName' => 'tagValue' format @@ -273,22 +273,22 @@ define('JSONP', 3, true); * @return string formatted xml tag */ private function makeNode($tagName, $tagContent, $attributes = null) - { + { if ($this->version == RSS2) { $nodeText = ''; $attrText = ''; if (is_array($attributes)) { - foreach ($attributes as $key => $value) + foreach ($attributes as $key => $value) { $attrText .= " $key=\"$value\" "; } } $nodeText .= "<{$tagName}{$attrText}>"; if (is_array($tagContent)) - { - foreach ($tagContent as $key => $value) + { + foreach ($tagContent as $key => $value) { $nodeText .= $this->makeNode($key, $value); } @@ -297,7 +297,7 @@ define('JSONP', 3, true); { //$nodeText .= (in_array($tagName, $this->CDATAEncoding))? $tagContent : htmlentities($tagContent); $nodeText .= htmlspecialchars($tagContent); - } + } //$nodeText .= (in_array($tagName, $this->CDATAEncoding))? "]]>" : ""; $nodeText .= ""; return $nodeText . PHP_EOL; @@ -321,7 +321,7 @@ define('JSONP', 3, true); } return ''; // should not get here } - + private function json_keys(array $array) { $new = array(); foreach ($array as $key => $val) { @@ -334,7 +334,7 @@ define('JSONP', 3, true); } return $new; } - + /** * @desc Print channels * @access private @@ -344,7 +344,7 @@ define('JSONP', 3, true); { //Start channel tag if ($this->version == RSS2) { - echo '' . PHP_EOL; + echo '' . PHP_EOL; // add hubs foreach ($this->hubs as $hub) { //echo $this->makeNode('link', '', array('rel'=>'hub', 'href'=>$hub, 'xmlns'=>'http://www.w3.org/2005/Atom')); @@ -356,7 +356,7 @@ define('JSONP', 3, true); echo '' . PHP_EOL; } //Print Items of channel - foreach ($this->channels as $key => $value) + foreach ($this->channels as $key => $value) { echo $this->makeNode($key, $value); } @@ -364,26 +364,26 @@ define('JSONP', 3, true); $this->json->rss['channel'] = (object)$this->json_keys($this->channels); } } - + /** * Prints formatted feed items - * + * * @access private * @return void */ private function printItems() - { + { foreach ($this->items as $item) { $itemElements = $item->getElements(); - + echo $this->startItem(); - + if ($this->version == JSON || $this->version == JSONP) { $json_item = array(); } - + foreach ($itemElements as $thisElement) { - foreach ($thisElement as $instance) { + foreach ($thisElement as $instance) { if ($this->version == RSS2) { echo $this->makeNode($instance['name'], $instance['content'], $instance['attributes']); } elseif ($this->version == JSON || $this->version == JSONP) { @@ -406,10 +406,10 @@ define('JSONP', 3, true); } } } - + /** * Make the starting tag of channels - * + * * @access private * @return void */ @@ -417,14 +417,14 @@ define('JSONP', 3, true); { if ($this->version == RSS2) { - echo '' . PHP_EOL; - } + echo '' . PHP_EOL; + } // nothing for JSON } - + /** * Closes feed item tag - * + * * @access private * @return void */ @@ -432,10 +432,10 @@ define('JSONP', 3, true); { if ($this->version == RSS2) { - echo '' . PHP_EOL; - } + echo '' . PHP_EOL; + } // nothing for JSON } - + // End # private functions ---------------------------------------------- } \ No newline at end of file diff --git a/inc/3rdparty/makefulltextfeed.php b/inc/3rdparty/makefulltextfeed.php index 2852c4c2..135964f1 100755 --- a/inc/3rdparty/makefulltextfeed.php +++ b/inc/3rdparty/makefulltextfeed.php @@ -55,42 +55,8 @@ if (get_magic_quotes_gpc()) { // set include path set_include_path(realpath(dirname(__FILE__).'/libraries').PATH_SEPARATOR.get_include_path()); -// Autoloading of classes allows us to include files only when they're -// needed. If we've got a cached copy, for example, only Zend_Cache is loaded. -function autoload($class_name) { - static $dir = null; - if ($dir === null) $dir = dirname(__FILE__).'/libraries/'; - static $mapping = array( - // Include FeedCreator for RSS/Atom creation - 'FeedWriter' => 'feedwriter/FeedWriter.php', - 'FeedItem' => 'feedwriter/FeedItem.php', - // Include ContentExtractor and Readability for identifying and extracting content from URLs - 'ContentExtractor' => 'content-extractor/ContentExtractor.php', - 'SiteConfig' => 'content-extractor/SiteConfig.php', - 'Readability' => 'readability/Readability.php', - // Include Humble HTTP Agent to allow parallel requests and response caching - 'HumbleHttpAgent' => 'humble-http-agent/HumbleHttpAgent.php', - 'SimplePie_HumbleHttpAgent' => 'humble-http-agent/SimplePie_HumbleHttpAgent.php', - 'CookieJar' => 'humble-http-agent/CookieJar.php', - // Include Zend Cache to improve performance (cache results) - 'Zend_Cache' => 'Zend/Cache.php', - // Language detect - 'Text_LanguageDetect' => 'language-detect/LanguageDetect.php', - // HTML5 Lib - 'HTML5_Parser' => 'html5/Parser.php', - // htmLawed - used if XSS filter is enabled (xss_filter) - 'htmLawed' => 'htmLawed/htmLawed.php' - ); - if (isset($mapping[$class_name])) { - debug("** Loading class $class_name ({$mapping[$class_name]})"); - require $dir.$mapping[$class_name]; - return true; - } else { - return false; - } -} -spl_autoload_register('autoload'); -require dirname(__FILE__).'/libraries/simplepie/autoloader.php'; + +require_once dirname(__FILE__).'/makefulltextfeedHelpers.php'; //////////////////////////////// // Load config file @@ -415,6 +381,7 @@ if (!$debug_mode) { ////////////////////////////////// // Set up HTTP agent ////////////////////////////////// +global $http; $http = new HumbleHttpAgent(); $http->debug = $debug_mode; $http->userAgentMap = $options->user_agents; @@ -478,29 +445,6 @@ if ($html_only || !$result) { $isDummyFeed = true; unset($feed, $result); // create single item dummy feed object - class DummySingleItemFeed { - public $item; - function __construct($url) { $this->item = new DummySingleItem($url); } - public function get_title() { return ''; } - public function get_description() { return 'Content extracted from '.$this->item->url; } - public function get_link() { return $this->item->url; } - public function get_language() { return false; } - public function get_image_url() { return false; } - public function get_items($start=0, $max=1) { return array(0=>$this->item); } - } - class DummySingleItem { - public $url; - function __construct($url) { $this->url = $url; } - public function get_permalink() { return $this->url; } - public function get_title() { return null; } - public function get_date($format='') { return false; } - public function get_author($key=0) { return null; } - public function get_authors() { return null; } - public function get_description() { return ''; } - public function get_enclosure($key=0, $prefer=null) { return null; } - public function get_enclosures() { return null; } - public function get_categories() { return null; } - } $feed = new DummySingleItemFeed($url); } @@ -903,294 +847,3 @@ if (!$debug_mode) { if ($callback) echo ');'; } -/////////////////////////////// -// HELPER FUNCTIONS -/////////////////////////////// - -function url_allowed($url) { - global $options; - if (!empty($options->allowed_urls)) { - $allowed = false; - foreach ($options->allowed_urls as $allowurl) { - if (stristr($url, $allowurl) !== false) { - $allowed = true; - break; - } - } - if (!$allowed) return false; - } else { - foreach ($options->blocked_urls as $blockurl) { - if (stristr($url, $blockurl) !== false) { - return false; - } - } - } - return true; -} - -////////////////////////////////////////////// -// Convert $html to UTF8 -// (uses HTTP headers and HTML to find encoding) -// adapted from http://stackoverflow.com/questions/910793/php-detect-encoding-and-make-everything-utf-8 -////////////////////////////////////////////// -function convert_to_utf8($html, $header=null) -{ - $encoding = null; - if ($html || $header) { - if (is_array($header)) $header = implode("\n", $header); - if (!$header || !preg_match_all('/^Content-Type:\s+([^;]+)(?:;\s*charset=["\']?([^;"\'\n]*))?/im', $header, $match, PREG_SET_ORDER)) { - // error parsing the response - debug('Could not find Content-Type header in HTTP response'); - } else { - $match = end($match); // get last matched element (in case of redirects) - if (isset($match[2])) $encoding = trim($match[2], "\"' \r\n\0\x0B\t"); - } - // TODO: check to see if encoding is supported (can we convert it?) - // If it's not, result will be empty string. - // For now we'll check for invalid encoding types returned by some sites, e.g. 'none' - // Problem URL: http://facta.co.jp/blog/archives/20111026001026.html - if (!$encoding || $encoding == 'none') { - // search for encoding in HTML - only look at the first 50000 characters - // 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 - // TODO: improve this so it looks at smaller chunks first - $html_head = substr($html, 0, 50000); - if (preg_match('/^<\?xml\s+version=(?:"[^"]*"|\'[^\']*\')\s+encoding=("[^"]*"|\'[^\']*\')/s', $html_head, $match)) { - $encoding = trim($match[1], '"\''); - } elseif (preg_match('/]+)/i', $html_head, $match)) { - $encoding = trim($match[1]); - } elseif (preg_match_all('/]+)>/i', $html_head, $match)) { - foreach ($match[1] as $_test) { - if (preg_match('/charset=["\']?([^"\']+)/i', $_test, $_m)) { - $encoding = trim($_m[1]); - break; - } - } - } - } - if (isset($encoding)) $encoding = trim($encoding); - // trim is important here! - if (!$encoding || (strtolower($encoding) == 'iso-8859-1')) { - // replace MS Word smart qutoes - $trans = array(); - $trans[chr(130)] = '‚'; // Single Low-9 Quotation Mark - $trans[chr(131)] = 'ƒ'; // Latin Small Letter F With Hook - $trans[chr(132)] = '„'; // Double Low-9 Quotation Mark - $trans[chr(133)] = '…'; // Horizontal Ellipsis - $trans[chr(134)] = '†'; // Dagger - $trans[chr(135)] = '‡'; // Double Dagger - $trans[chr(136)] = 'ˆ'; // Modifier Letter Circumflex Accent - $trans[chr(137)] = '‰'; // Per Mille Sign - $trans[chr(138)] = 'Š'; // Latin Capital Letter S With Caron - $trans[chr(139)] = '‹'; // Single Left-Pointing Angle Quotation Mark - $trans[chr(140)] = 'Œ'; // Latin Capital Ligature OE - $trans[chr(145)] = '‘'; // Left Single Quotation Mark - $trans[chr(146)] = '’'; // Right Single Quotation Mark - $trans[chr(147)] = '“'; // Left Double Quotation Mark - $trans[chr(148)] = '”'; // Right Double Quotation Mark - $trans[chr(149)] = '•'; // Bullet - $trans[chr(150)] = '–'; // En Dash - $trans[chr(151)] = '—'; // Em Dash - $trans[chr(152)] = '˜'; // Small Tilde - $trans[chr(153)] = '™'; // Trade Mark Sign - $trans[chr(154)] = 'š'; // Latin Small Letter S With Caron - $trans[chr(155)] = '›'; // Single Right-Pointing Angle Quotation Mark - $trans[chr(156)] = 'œ'; // Latin Small Ligature OE - $trans[chr(159)] = 'Ÿ'; // Latin Capital Letter Y With Diaeresis - $html = strtr($html, $trans); - } - if (!$encoding) { - debug('No character encoding found, so treating as UTF-8'); - $encoding = 'utf-8'; - } else { - debug('Character encoding: '.$encoding); - if (strtolower($encoding) != 'utf-8') { - debug('Converting to UTF-8'); - $html = SimplePie_Misc::change_encoding($html, $encoding, 'utf-8'); - /* - if (function_exists('iconv')) { - // iconv appears to handle certain character encodings better than mb_convert_encoding - $html = iconv($encoding, 'utf-8', $html); - } else { - $html = mb_convert_encoding($html, 'utf-8', $encoding); - } - */ - } - } - } - return $html; -} - -function makeAbsolute($base, $elem) { - $base = new SimplePie_IRI($base); - // remove '//' in URL path (used to prevent URLs from resolving properly) - // TODO: check if this is still the case - if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path); - foreach(array('a'=>'href', 'img'=>'src') as $tag => $attr) { - $elems = $elem->getElementsByTagName($tag); - for ($i = $elems->length-1; $i >= 0; $i--) { - $e = $elems->item($i); - //$e->parentNode->replaceChild($articleContent->ownerDocument->createTextNode($e->textContent), $e); - makeAbsoluteAttr($base, $e, $attr); - } - if (strtolower($elem->tagName) == $tag) makeAbsoluteAttr($base, $elem, $attr); - } -} -function makeAbsoluteAttr($base, $e, $attr) { - if ($e->hasAttribute($attr)) { - // Trim leading and trailing white space. I don't really like this but - // unfortunately it does appear on some sites. e.g. - $url = trim(str_replace('%20', ' ', $e->getAttribute($attr))); - $url = str_replace(' ', '%20', $url); - if (!preg_match('!https?://!i', $url)) { - if ($absolute = SimplePie_IRI::absolutize($base, $url)) { - $e->setAttribute($attr, $absolute); - } - } - } -} -function makeAbsoluteStr($base, $url) { - $base = new SimplePie_IRI($base); - // remove '//' in URL path (causes URLs not to resolve properly) - if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path); - if (preg_match('!^https?://!i', $url)) { - // already absolute - return $url; - } else { - if ($absolute = SimplePie_IRI::absolutize($base, $url)) { - return $absolute; - } - return false; - } -} -// returns single page response, or false if not found -function getSinglePage($item, $html, $url) { - global $http, $extractor; - debug('Looking for site config files to see if single page link exists'); - $site_config = $extractor->buildSiteConfig($url, $html); - $splink = null; - if (!empty($site_config->single_page_link)) { - $splink = $site_config->single_page_link; - } elseif (!empty($site_config->single_page_link_in_feed)) { - // single page link xpath is targeted at feed - $splink = $site_config->single_page_link_in_feed; - // so let's replace HTML with feed item description - $html = $item->get_description(); - } - if (isset($splink)) { - // Build DOM tree from HTML - $readability = new Readability($html, $url); - $xpath = new DOMXPath($readability->dom); - // Loop through single_page_link xpath expressions - $single_page_url = null; - foreach ($splink as $pattern) { - $elems = @$xpath->evaluate($pattern, $readability->dom); - if (is_string($elems)) { - $single_page_url = trim($elems); - break; - } elseif ($elems instanceof DOMNodeList && $elems->length > 0) { - foreach ($elems as $item) { - if ($item instanceof DOMElement && $item->hasAttribute('href')) { - $single_page_url = $item->getAttribute('href'); - break 2; - } elseif ($item instanceof DOMAttr && $item->value) { - $single_page_url = $item->value; - break 2; - } - } - } - } - // If we've got URL, resolve against $url - if (isset($single_page_url) && ($single_page_url = makeAbsoluteStr($url, $single_page_url))) { - // check it's not what we have already! - if ($single_page_url != $url) { - // it's not, so let's try to fetch it... - $_prev_ref = $http->referer; - $http->referer = $single_page_url; - if (($response = $http->get($single_page_url, true)) && $response['status_code'] < 300) { - $http->referer = $_prev_ref; - return $response; - } - $http->referer = $_prev_ref; - } - } - } - return false; -} - -// based on content-type http header, decide what to do -// param: HTTP headers string -// return: array with keys: 'mime', 'type', 'subtype', 'action', 'name' -// e.g. array('mime'=>'image/jpeg', 'type'=>'image', 'subtype'=>'jpeg', 'action'=>'link', 'name'=>'Image') -function get_mime_action_info($headers) { - global $options; - // check if action defined for returned Content-Type - $info = array(); - if (preg_match('!^Content-Type:\s*(([-\w]+)/([-\w\+]+))!im', $headers, $match)) { - // look for full mime type (e.g. image/jpeg) or just type (e.g. image) - // match[1] = full mime type, e.g. image/jpeg - // match[2] = first part, e.g. image - // match[3] = last part, e.g. jpeg - $info['mime'] = strtolower(trim($match[1])); - $info['type'] = strtolower(trim($match[2])); - $info['subtype'] = strtolower(trim($match[3])); - foreach (array($info['mime'], $info['type']) as $_mime) { - if (isset($options->content_type_exc[$_mime])) { - $info['action'] = $options->content_type_exc[$_mime]['action']; - $info['name'] = $options->content_type_exc[$_mime]['name']; - break; - } - } - } - return $info; -} - -function remove_url_cruft($url) { - // remove google analytics for the time being - // regex adapted from http://navitronic.co.uk/2010/12/removing-google-analytics-cruft-from-urls/ - // https://gist.github.com/758177 - return preg_replace('/(\?|\&)utm_[a-z]+=[^\&]+/', '', $url); -} - -function make_substitutions($string) { - if ($string == '') return $string; - global $item, $effective_url; - $string = str_replace('{url}', htmlspecialchars($item->get_permalink()), $string); - $string = str_replace('{effective-url}', htmlspecialchars($effective_url), $string); - return $string; -} - -function get_cache() { - global $options, $valid_key; - static $cache = null; - if ($cache === null) { - $frontendOptions = array( - 'lifetime' => 10*60, // cache lifetime of 10 minutes - 'automatic_serialization' => false, - 'write_control' => false, - 'automatic_cleaning_factor' => $options->cache_cleanup, - 'ignore_user_abort' => false - ); - $backendOptions = array( - 'cache_dir' => ($valid_key) ? $options->cache_dir.'/rss-with-key/' : $options->cache_dir.'/rss/', // directory where to put the cache files - 'file_locking' => false, - 'read_control' => true, - 'read_control_type' => 'strlen', - 'hashed_directory_level' => $options->cache_directory_level, - 'hashed_directory_perm' => 0777, - 'cache_file_perm' => 0664, - 'file_name_prefix' => 'ff' - ); - // getting a Zend_Cache_Core object - $cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions); - } - return $cache; -} - -function debug($msg) { - global $debug_mode; - if ($debug_mode) { - echo '* ',$msg,"\n"; - ob_flush(); - flush(); - } -} \ No newline at end of file diff --git a/inc/3rdparty/makefulltextfeedHelpers.php b/inc/3rdparty/makefulltextfeedHelpers.php new file mode 100755 index 00000000..1c11b8f6 --- /dev/null +++ b/inc/3rdparty/makefulltextfeedHelpers.php @@ -0,0 +1,355 @@ + 'feedwriter/FeedWriter.php', + 'FeedItem' => 'feedwriter/FeedItem.php', + // Include ContentExtractor and Readability for identifying and extracting content from URLs + 'ContentExtractor' => 'content-extractor/ContentExtractor.php', + 'SiteConfig' => 'content-extractor/SiteConfig.php', + 'Readability' => 'readability/Readability.php', + // Include Humble HTTP Agent to allow parallel requests and response caching + 'HumbleHttpAgent' => 'humble-http-agent/HumbleHttpAgent.php', + 'SimplePie_HumbleHttpAgent' => 'humble-http-agent/SimplePie_HumbleHttpAgent.php', + 'CookieJar' => 'humble-http-agent/CookieJar.php', + // Include Zend Cache to improve performance (cache results) + 'Zend_Cache' => 'Zend/Cache.php', + // Language detect + 'Text_LanguageDetect' => 'language-detect/LanguageDetect.php', + // HTML5 Lib + 'HTML5_Parser' => 'html5/Parser.php', + // htmLawed - used if XSS filter is enabled (xss_filter) + 'htmLawed' => 'htmLawed/htmLawed.php' + ); + if (isset($mapping[$class_name])) { + debug("** Loading class $class_name ({$mapping[$class_name]})"); + require $dir.$mapping[$class_name]; + return true; + } else { + return false; + } +} +spl_autoload_register('autoload'); +require dirname(__FILE__).'/libraries/simplepie/autoloader.php'; + + +class DummySingleItemFeed { + public $item; + function __construct($url) { $this->item = new DummySingleItem($url); } + public function get_title() { return ''; } + public function get_description() { return 'Content extracted from '.$this->item->url; } + public function get_link() { return $this->item->url; } + public function get_language() { return false; } + public function get_image_url() { return false; } + public function get_items($start=0, $max=1) { return array(0=>$this->item); } +} +class DummySingleItem { + public $url; + function __construct($url) { $this->url = $url; } + public function get_permalink() { return $this->url; } + public function get_title() { return null; } + public function get_date($format='') { return false; } + public function get_author($key=0) { return null; } + public function get_authors() { return null; } + public function get_description() { return ''; } + public function get_enclosure($key=0, $prefer=null) { return null; } + public function get_enclosures() { return null; } + public function get_categories() { return null; } +} + +/////////////////////////////// +// HELPER FUNCTIONS +/////////////////////////////// + +function url_allowed($url) { + global $options; + if (!empty($options->allowed_urls)) { + $allowed = false; + foreach ($options->allowed_urls as $allowurl) { + if (stristr($url, $allowurl) !== false) { + $allowed = true; + break; + } + } + if (!$allowed) return false; + } else { + foreach ($options->blocked_urls as $blockurl) { + if (stristr($url, $blockurl) !== false) { + return false; + } + } + } + return true; +} + +////////////////////////////////////////////// +// Convert $html to UTF8 +// (uses HTTP headers and HTML to find encoding) +// adapted from http://stackoverflow.com/questions/910793/php-detect-encoding-and-make-everything-utf-8 +////////////////////////////////////////////// +function convert_to_utf8($html, $header=null) +{ + $encoding = null; + if ($html || $header) { + if (is_array($header)) $header = implode("\n", $header); + if (!$header || !preg_match_all('/^Content-Type:\s+([^;]+)(?:;\s*charset=["\']?([^;"\'\n]*))?/im', $header, $match, PREG_SET_ORDER)) { + // error parsing the response + debug('Could not find Content-Type header in HTTP response'); + } else { + $match = end($match); // get last matched element (in case of redirects) + if (isset($match[2])) $encoding = trim($match[2], "\"' \r\n\0\x0B\t"); + } + // TODO: check to see if encoding is supported (can we convert it?) + // If it's not, result will be empty string. + // For now we'll check for invalid encoding types returned by some sites, e.g. 'none' + // Problem URL: http://facta.co.jp/blog/archives/20111026001026.html + if (!$encoding || $encoding == 'none') { + // search for encoding in HTML - only look at the first 50000 characters + // 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 + // TODO: improve this so it looks at smaller chunks first + $html_head = substr($html, 0, 50000); + if (preg_match('/^<\?xml\s+version=(?:"[^"]*"|\'[^\']*\')\s+encoding=("[^"]*"|\'[^\']*\')/s', $html_head, $match)) { + $encoding = trim($match[1], '"\''); + } elseif (preg_match('/]+)/i', $html_head, $match)) { + $encoding = trim($match[1]); + } elseif (preg_match_all('/]+)>/i', $html_head, $match)) { + foreach ($match[1] as $_test) { + if (preg_match('/charset=["\']?([^"\']+)/i', $_test, $_m)) { + $encoding = trim($_m[1]); + break; + } + } + } + } + if (isset($encoding)) $encoding = trim($encoding); + // trim is important here! + if (!$encoding || (strtolower($encoding) == 'iso-8859-1')) { + // replace MS Word smart qutoes + $trans = array(); + $trans[chr(130)] = '‚'; // Single Low-9 Quotation Mark + $trans[chr(131)] = 'ƒ'; // Latin Small Letter F With Hook + $trans[chr(132)] = '„'; // Double Low-9 Quotation Mark + $trans[chr(133)] = '…'; // Horizontal Ellipsis + $trans[chr(134)] = '†'; // Dagger + $trans[chr(135)] = '‡'; // Double Dagger + $trans[chr(136)] = 'ˆ'; // Modifier Letter Circumflex Accent + $trans[chr(137)] = '‰'; // Per Mille Sign + $trans[chr(138)] = 'Š'; // Latin Capital Letter S With Caron + $trans[chr(139)] = '‹'; // Single Left-Pointing Angle Quotation Mark + $trans[chr(140)] = 'Œ'; // Latin Capital Ligature OE + $trans[chr(145)] = '‘'; // Left Single Quotation Mark + $trans[chr(146)] = '’'; // Right Single Quotation Mark + $trans[chr(147)] = '“'; // Left Double Quotation Mark + $trans[chr(148)] = '”'; // Right Double Quotation Mark + $trans[chr(149)] = '•'; // Bullet + $trans[chr(150)] = '–'; // En Dash + $trans[chr(151)] = '—'; // Em Dash + $trans[chr(152)] = '˜'; // Small Tilde + $trans[chr(153)] = '™'; // Trade Mark Sign + $trans[chr(154)] = 'š'; // Latin Small Letter S With Caron + $trans[chr(155)] = '›'; // Single Right-Pointing Angle Quotation Mark + $trans[chr(156)] = 'œ'; // Latin Small Ligature OE + $trans[chr(159)] = 'Ÿ'; // Latin Capital Letter Y With Diaeresis + $html = strtr($html, $trans); + } + if (!$encoding) { + debug('No character encoding found, so treating as UTF-8'); + $encoding = 'utf-8'; + } else { + debug('Character encoding: '.$encoding); + if (strtolower($encoding) != 'utf-8') { + debug('Converting to UTF-8'); + $html = SimplePie_Misc::change_encoding($html, $encoding, 'utf-8'); + /* + if (function_exists('iconv')) { + // iconv appears to handle certain character encodings better than mb_convert_encoding + $html = iconv($encoding, 'utf-8', $html); + } else { + $html = mb_convert_encoding($html, 'utf-8', $encoding); + } + */ + } + } + } + return $html; +} + +function makeAbsolute($base, $elem) { + $base = new SimplePie_IRI($base); + // remove '//' in URL path (used to prevent URLs from resolving properly) + // TODO: check if this is still the case + if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path); + foreach(array('a'=>'href', 'img'=>'src') as $tag => $attr) { + $elems = $elem->getElementsByTagName($tag); + for ($i = $elems->length-1; $i >= 0; $i--) { + $e = $elems->item($i); + //$e->parentNode->replaceChild($articleContent->ownerDocument->createTextNode($e->textContent), $e); + makeAbsoluteAttr($base, $e, $attr); + } + if (strtolower($elem->tagName) == $tag) makeAbsoluteAttr($base, $elem, $attr); + } +} +function makeAbsoluteAttr($base, $e, $attr) { + if ($e->hasAttribute($attr)) { + // Trim leading and trailing white space. I don't really like this but + // unfortunately it does appear on some sites. e.g. + $url = trim(str_replace('%20', ' ', $e->getAttribute($attr))); + $url = str_replace(' ', '%20', $url); + if (!preg_match('!https?://!i', $url)) { + if ($absolute = SimplePie_IRI::absolutize($base, $url)) { + $e->setAttribute($attr, $absolute); + } + } + } +} +function makeAbsoluteStr($base, $url) { + $base = new SimplePie_IRI($base); + // remove '//' in URL path (causes URLs not to resolve properly) + if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path); + if (preg_match('!^https?://!i', $url)) { + // already absolute + return $url; + } else { + if ($absolute = SimplePie_IRI::absolutize($base, $url)) { + return $absolute; + } + return false; + } +} +// returns single page response, or false if not found +function getSinglePage($item, $html, $url) { + global $http, $extractor; + debug('Looking for site config files to see if single page link exists'); + $site_config = $extractor->buildSiteConfig($url, $html); + $splink = null; + if (!empty($site_config->single_page_link)) { + $splink = $site_config->single_page_link; + } elseif (!empty($site_config->single_page_link_in_feed)) { + // single page link xpath is targeted at feed + $splink = $site_config->single_page_link_in_feed; + // so let's replace HTML with feed item description + $html = $item->get_description(); + } + if (isset($splink)) { + // Build DOM tree from HTML + $readability = new Readability($html, $url); + $xpath = new DOMXPath($readability->dom); + // Loop through single_page_link xpath expressions + $single_page_url = null; + foreach ($splink as $pattern) { + $elems = @$xpath->evaluate($pattern, $readability->dom); + if (is_string($elems)) { + $single_page_url = trim($elems); + break; + } elseif ($elems instanceof DOMNodeList && $elems->length > 0) { + foreach ($elems as $item) { + if ($item instanceof DOMElement && $item->hasAttribute('href')) { + $single_page_url = $item->getAttribute('href'); + break 2; + } elseif ($item instanceof DOMAttr && $item->value) { + $single_page_url = $item->value; + break 2; + } + } + } + } + // If we've got URL, resolve against $url + if (isset($single_page_url) && ($single_page_url = makeAbsoluteStr($url, $single_page_url))) { + // check it's not what we have already! + if ($single_page_url != $url) { + // it's not, so let's try to fetch it... + $_prev_ref = $http->referer; + $http->referer = $single_page_url; + if (($response = $http->get($single_page_url, true)) && $response['status_code'] < 300) { + $http->referer = $_prev_ref; + return $response; + } + $http->referer = $_prev_ref; + } + } + } + return false; +} + +// based on content-type http header, decide what to do +// param: HTTP headers string +// return: array with keys: 'mime', 'type', 'subtype', 'action', 'name' +// e.g. array('mime'=>'image/jpeg', 'type'=>'image', 'subtype'=>'jpeg', 'action'=>'link', 'name'=>'Image') +function get_mime_action_info($headers) { + global $options; + // check if action defined for returned Content-Type + $info = array(); + if (preg_match('!^Content-Type:\s*(([-\w]+)/([-\w\+]+))!im', $headers, $match)) { + // look for full mime type (e.g. image/jpeg) or just type (e.g. image) + // match[1] = full mime type, e.g. image/jpeg + // match[2] = first part, e.g. image + // match[3] = last part, e.g. jpeg + $info['mime'] = strtolower(trim($match[1])); + $info['type'] = strtolower(trim($match[2])); + $info['subtype'] = strtolower(trim($match[3])); + foreach (array($info['mime'], $info['type']) as $_mime) { + if (isset($options->content_type_exc[$_mime])) { + $info['action'] = $options->content_type_exc[$_mime]['action']; + $info['name'] = $options->content_type_exc[$_mime]['name']; + break; + } + } + } + return $info; +} + +function remove_url_cruft($url) { + // remove google analytics for the time being + // regex adapted from http://navitronic.co.uk/2010/12/removing-google-analytics-cruft-from-urls/ + // https://gist.github.com/758177 + return preg_replace('/(\?|\&)utm_[a-z]+=[^\&]+/', '', $url); +} + +function make_substitutions($string) { + if ($string == '') return $string; + global $item, $effective_url; + $string = str_replace('{url}', htmlspecialchars($item->get_permalink()), $string); + $string = str_replace('{effective-url}', htmlspecialchars($effective_url), $string); + return $string; +} + +function get_cache() { + global $options, $valid_key; + static $cache = null; + if ($cache === null) { + $frontendOptions = array( + 'lifetime' => 10*60, // cache lifetime of 10 minutes + 'automatic_serialization' => false, + 'write_control' => false, + 'automatic_cleaning_factor' => $options->cache_cleanup, + 'ignore_user_abort' => false + ); + $backendOptions = array( + 'cache_dir' => ($valid_key) ? $options->cache_dir.'/rss-with-key/' : $options->cache_dir.'/rss/', // directory where to put the cache files + 'file_locking' => false, + 'read_control' => true, + 'read_control_type' => 'strlen', + 'hashed_directory_level' => $options->cache_directory_level, + 'hashed_directory_perm' => 0777, + 'cache_file_perm' => 0664, + 'file_name_prefix' => 'ff' + ); + // getting a Zend_Cache_Core object + $cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions); + } + return $cache; +} + +function debug($msg) { + global $debug_mode; + if ($debug_mode) { + echo '* ',$msg,"\n"; + ob_flush(); + flush(); + } +} diff --git a/inc/poche/Database.class.php b/inc/poche/Database.class.php index c998fe14..6244df88 100755 --- a/inc/poche/Database.class.php +++ b/inc/poche/Database.class.php @@ -18,7 +18,7 @@ class Database { 'default' => 'ORDER BY entries.id' ); - function __construct() + function __construct() { switch (STORAGE) { case 'sqlite': @@ -27,11 +27,11 @@ class Database { break; case 'mysql': $db_path = 'mysql:host=' . STORAGE_SERVER . ';dbname=' . STORAGE_DB; - $this->handle = new PDO($db_path, STORAGE_USER, STORAGE_PASSWORD); + $this->handle = new PDO($db_path, STORAGE_USER, STORAGE_PASSWORD); break; case 'postgres': $db_path = 'pgsql:host=' . STORAGE_SERVER . ';dbname=' . STORAGE_DB; - $this->handle = new PDO($db_path, STORAGE_USER, STORAGE_PASSWORD); + $this->handle = new PDO($db_path, STORAGE_USER, STORAGE_PASSWORD); break; } @@ -51,7 +51,7 @@ class Database { } $hasAdmin = count($query->fetchAll()); - if ($hasAdmin == 0) + if ($hasAdmin == 0) return false; return true; @@ -140,7 +140,7 @@ class Database { $sql = 'INSERT INTO users_config ( user_id, name, value ) VALUES (?, ?, ?)'; $params = array($id_user, 'language', LANG); $query = $this->executeQuery($sql, $params); - + $sql = 'INSERT INTO users_config ( user_id, name, value ) VALUES (?, ?, ?)'; $params = array($id_user, 'theme', DEFAULT_THEME); $query = $this->executeQuery($sql, $params); @@ -153,7 +153,7 @@ class Database { $query = $this->executeQuery($sql, array($id)); $result = $query->fetchAll(); $user_config = array(); - + foreach ($result as $key => $value) { $user_config[$value['name']] = $value['value']; } @@ -201,10 +201,10 @@ class Database { $params_update = array($password, $userId); $query = $this->executeQuery($sql_update, $params_update); } - + public function updateUserConfig($userId, $key, $value) { $config = $this->getConfigUser($userId); - + if (! isset($config[$key])) { $sql = "INSERT INTO users_config (value, user_id, name) VALUES (?, ?, ?)"; } @@ -230,6 +230,36 @@ class Database { } } + public function updateContentAndTitle($id, $title, $body, $user_id) { + $sql_action = 'UPDATE entries SET content = ?, title = ? WHERE id=? AND user_id=?'; + $params_action = array($body, $title, $id, $user_id); + $query = $this->executeQuery($sql_action, $params_action); + + return $query; + } + + public function retrieveUnfetchedEntries($user_id, $limit) { + + $sql_limit = "LIMIT 0,".$limit; + if (STORAGE == 'postgres') { + $sql_limit = "LIMIT ".$limit." OFFSET 0"; + } + + $sql = "SELECT * FROM entries WHERE (content = '' OR content IS NULL) AND user_id=? ORDER BY id " . $sql_limit; + $query = $this->executeQuery($sql, array($user_id)); + $entries = $query->fetchAll(); + + return $entries; + } + + public function retrieveUnfetchedEntriesCount($user_id) { + $sql = "SELECT count(*) FROM entries WHERE (content = '' OR content IS NULL) AND user_id=?"; + $query = $this->executeQuery($sql, array($user_id)); + list($count) = $query->fetch(); + + return $count; + } + public function retrieveAll($user_id) { $sql = "SELECT * FROM entries WHERE user_id=? ORDER BY id"; $query = $this->executeQuery($sql, array($user_id)); @@ -294,24 +324,24 @@ class Database { return $entries; } - public function getEntriesByViewCount($view, $user_id, $tag_id = 0) { - switch ($view) { + public function getEntriesByViewCount($view, $user_id, $tag_id = 0) { + switch ($view) { case 'archive': - $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? "; + $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? "; $params = array($user_id, 1); break; case 'fav' : - $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_fav=? "; + $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_fav=? "; $params = array($user_id, 1); break; - case 'tag' : - $sql = "SELECT count(*) FROM entries - LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id - WHERE entries.user_id=? AND tags_entries.tag_id = ? "; - $params = array($user_id, $tag_id); - break; + case 'tag' : + $sql = "SELECT count(*) FROM entries + LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id + WHERE entries.user_id=? AND tags_entries.tag_id = ? "; + $params = array($user_id, $tag_id); + break; default: - $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? "; + $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? "; $params = array($user_id, 0); break; } @@ -319,7 +349,7 @@ class Database { $query = $this->executeQuery($sql, $params); list($count) = $query->fetch(); - return $count; + return $count; } public function updateContent($id, $content, $user_id) { @@ -329,11 +359,24 @@ class Database { return $query; } - public function add($url, $title, $content, $user_id) { - $sql_action = 'INSERT INTO entries ( url, title, content, user_id ) VALUES (?, ?, ?, ?)'; - $params_action = array($url, $title, $content, $user_id); - $query = $this->executeQuery($sql_action, $params_action); - return $query; + /** + * + * @param string $url + * @param string $title + * @param string $content + * @param integer $user_id + * @return integer $id of inserted record + */ + public function add($url, $title, $content, $user_id, $isFavorite=0, $isRead=0) { + $sql_action = 'INSERT INTO entries ( url, title, content, user_id, is_fav, is_read ) VALUES (?, ?, ?, ?, ?, ?)'; + $params_action = array($url, $title, $content, $user_id, $isFavorite, $isRead); + if ( !$this->executeQuery($sql_action, $params_action) ) { + $id = null; + } + else { + $id = intval($this->getLastId( (STORAGE == 'postgres') ? 'users_id_seq' : '' )); + } + return $id; } public function deleteById($id, $user_id) { @@ -364,13 +407,25 @@ class Database { public function getLastId($column = '') { return $this->getHandle()->lastInsertId($column); } + + public function search($term, $user_id, $limit = '') { + $search = '%'.$term.'%'; + $sql_action = "SELECT * FROM entries WHERE user_id=? AND (content LIKE ? OR title LIKE ? OR url LIKE ?) "; //searches in content, title and URL + $sql_action .= $this->getEntriesOrder().' ' . $limit; + $params_action = array($user_id, $search, $search, $search); + $query = $this->executeQuery($sql_action, $params_action); + return $query->fetchAll(); + } - public function retrieveAllTags($user_id) { - $sql = "SELECT DISTINCT tags.* FROM tags + public function retrieveAllTags($user_id, $term = null) { + $sql = "SELECT DISTINCT tags.*, count(entries.id) AS entriescount FROM tags LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id LEFT JOIN entries ON tags_entries.entry_id=entries.id - WHERE entries.user_id=?"; - $query = $this->executeQuery($sql, array($user_id)); + WHERE entries.user_id=? + ". (($term) ? "AND lower(tags.value) LIKE ?" : '') ." + GROUP BY tags.id, tags.value + ORDER BY tags.value"; + $query = $this->executeQuery($sql, (($term)? array($user_id, strtolower('%'.$term.'%')) : array($user_id) )); $tags = $query->fetchAll(); return $tags; @@ -390,7 +445,7 @@ class Database { } public function retrieveEntriesByTag($tag_id, $user_id) { - $sql = + $sql = "SELECT entries.* FROM entries LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id WHERE tags_entries.tag_id = ? AND entries.user_id=?"; @@ -401,7 +456,7 @@ class Database { } public function retrieveTagsByEntry($entry_id) { - $sql = + $sql = "SELECT tags.* FROM tags LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id WHERE tags_entries.entry_id = ?"; diff --git a/inc/poche/Poche.class.php b/inc/poche/Poche.class.php index 34f2ff5a..a662f695 100755 --- a/inc/poche/Poche.class.php +++ b/inc/poche/Poche.class.php @@ -18,7 +18,7 @@ class Poche public $tpl; public $messages; public $pagination; - + private $currentTheme = ''; private $currentLanguage = ''; private $notInstalledMessage = array(); @@ -32,20 +32,21 @@ class Poche 'fr_FR.utf8' => 'Français', 'it_IT.utf8' => 'Italiano', 'pl_PL.utf8' => 'Polski', + 'pt_BR.utf8' => 'Português (Brasil)', 'ru_RU.utf8' => 'Pусский', 'sl_SI.utf8' => 'Slovenščina', - 'uk_UA.utf8' => 'Український', + 'uk_UA.utf8' => 'Українська', ); public function __construct() { if ($this->configFileIsAvailable()) { $this->init(); } - + if ($this->themeIsInstalled()) { $this->initTpl(); } - + if ($this->systemIsInstalled()) { $this->store = new Database(); $this->messages = new Messages(); @@ -56,12 +57,10 @@ class Poche $this->store->checkTags(); } } - - private function init() + + private function init() { Tools::initPhp(); - Session::$sessionName = 'poche'; - Session::init(); if (isset($_SESSION['poche_user']) && $_SESSION['poche_user'] != array()) { $this->user = $_SESSION['poche_user']; @@ -75,28 +74,28 @@ class Poche $language = $this->user->getConfigValue('language'); putenv('LC_ALL=' . $language); setlocale(LC_ALL, $language); - bindtextdomain($language, LOCALE); - textdomain($language); + bindtextdomain($language, LOCALE); + textdomain($language); # Pagination $this->pagination = new Paginator($this->user->getConfigValue('pager'), 'p'); - + # Set up theme $themeDirectory = $this->user->getConfigValue('theme'); - + if ($themeDirectory === false) { $themeDirectory = DEFAULT_THEME; } - + $this->currentTheme = $themeDirectory; # Set up language $languageDirectory = $this->user->getConfigValue('language'); - + if ($languageDirectory === false) { $languageDirectory = DEFAULT_THEME; } - + $this->currentLanguage = $languageDirectory; } @@ -109,7 +108,7 @@ class Poche return true; } - + public function themeIsInstalled() { $passTheme = TRUE; # 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 self::$canRenderTemplates = false; $passTheme = FALSE; - } - + } + # Check if the selected theme and its requirements are present $theme = $this->getTheme(); if ($theme != '' && ! is_dir(THEME . '/' . $theme)) { $this->notInstalledMessage[] = 'The currently selected theme (' . $theme . ') does not seem to be properly installed (Missing directory: ' . THEME . '/' . $theme . ')'; - + self::$canRenderTemplates = false; - + $passTheme = FALSE; } - + $themeInfo = $this->getThemeInfo($theme); if (isset($themeInfo['requirements']) && is_array($themeInfo['requirements'])) { foreach ($themeInfo['requirements'] as $requiredTheme) { if (! is_dir(THEME . '/' . $requiredTheme)) { $this->notInstalledMessage[] = 'The required "' . $requiredTheme . '" theme is missing for the current theme (' . $theme . ')'; - + self::$canRenderTemplates = false; - + $passTheme = FALSE; } } @@ -154,21 +153,21 @@ class Poche return FALSE; } - + return true; } - + /** * all checks before installation. * @todo move HTML to template - * @return boolean + * @return boolean */ public function systemIsInstalled() { $msg = TRUE; - + $configSalt = defined('SALT') ? constant('SALT') : ''; - + if (empty($configSalt)) { $this->notInstalledMessage[] = 'You have not yet filled in the SALT value in the config.inc.php file.'; $msg = FALSE; @@ -194,7 +193,7 @@ class Poche return true; } - + public function getNotInstalledMessage() { return $this->notInstalledMessage; } @@ -203,7 +202,7 @@ class Poche { $loaderChain = new Twig_Loader_Chain(); $theme = $this->getTheme(); - + # add the current theme as first to the loader chain so Twig will look there first for overridden template files try { $loaderChain->addLoader(new Twig_Loader_Filesystem(THEME . '/' . $theme)); @@ -211,7 +210,7 @@ class Poche # @todo isInstalled() should catch this, inject Twig later die('The currently selected theme (' . $theme . ') does not seem to be properly installed (' . THEME . '/' . $theme .' is missing)'); } - + # add all required themes to the loader chain $themeInfo = $this->getThemeInfo($theme); if (isset($themeInfo['requirements']) && is_array($themeInfo['requirements'])) { @@ -224,16 +223,16 @@ class Poche } } } - + if (DEBUG_POCHE) { $twigParams = array(); } else { $twigParams = array('cache' => CACHE); } - + $this->tpl = new Twig_Environment($loaderChain, $twigParams); $this->tpl->addExtension(new Twig_Extensions_Extension_I18n()); - + # filter to display domain name of an url $filter = new Twig_SimpleFilter('getDomain', 'Tools::getDomain'); $this->tpl->addFilter($filter); @@ -252,7 +251,7 @@ class Poche 'poche_url' => Tools::getPocheUrl() )); if (isset($_GET['install'])) { - if (($_POST['password'] == $_POST['password_repeat']) + if (($_POST['password'] == $_POST['password_repeat']) && $_POST['password'] != "" && $_POST['login'] != "") { # let's rock, install poche baby ! if ($this->store->install($_POST['login'], Tools::encodeString($_POST['password'] . $_POST['login']))) @@ -269,7 +268,7 @@ class Poche } exit(); } - + public function getTheme() { return $this->currentTheme; } @@ -294,7 +293,7 @@ class Poche if (is_file($themeIniFile) && is_readable($themeIniFile)) { $themeInfo = parse_ini_file($themeIniFile); } - + if ($themeInfo === false) { $themeInfo = array(); } @@ -305,7 +304,7 @@ class Poche return $themeInfo; } - + public function getInstalledThemes() { $handle = opendir(THEME); $themes = array(); @@ -332,28 +331,28 @@ class Poche public function getInstalledLanguages() { $handle = opendir(LOCALE); $languages = array(); - + while (($language = readdir($handle)) !== false) { # Languages are stored in a directory, so all directory names are languages # @todo move language installation data to database - if (! is_dir(LOCALE . '/' . $language) || in_array($language, array('..', '.'))) { + if (! is_dir(LOCALE . '/' . $language) || in_array($language, array('..', '.', 'tools'))) { continue; } - + $current = false; - + if ($language === $this->getLanguage()) { $current = true; } - - $languages[] = array('name' => $this->language_names[$language], 'value' => $language, 'current' => $current); + + $languages[] = array('name' => (isset($this->language_names[$language]) ? $this->language_names[$language] : $language), 'value' => $language, 'current' => $current); } - + return $languages; } public function getDefaultConfig() - { + { return array( 'pager' => PAGINATION, 'language' => LANG, @@ -361,60 +360,6 @@ class Poche ); } - protected function getPageContent(Url $url) - { - // Saving and clearing context - $REAL = array(); - foreach( $GLOBALS as $key => $value ) { - if( $key != "GLOBALS" && $key != "_SESSION" ) { - $GLOBALS[$key] = array(); - $REAL[$key] = $value; - } - } - // Saving and clearing session - $REAL_SESSION = array(); - foreach( $_SESSION as $key => $value ) { - $REAL_SESSION[$key] = $value; - unset($_SESSION[$key]); - } - - // Running code in different context - $scope = function() { - extract( func_get_arg(1) ); - $_GET = $_REQUEST = array( - "url" => $url->getUrl(), - "max" => 5, - "links" => "preserve", - "exc" => "", - "format" => "json", - "submit" => "Create Feed" - ); - ob_start(); - require func_get_arg(0); - $json = ob_get_flush(); - return $json; - }; - $json = $scope( "inc/3rdparty/makefulltextfeed.php", array("url" => $url) ); - - // Clearing and restoring context - foreach( $GLOBALS as $key => $value ) { - if( $key != "GLOBALS" && $key != "_SESSION" ) { - unset($GLOBALS[$key]); - } - } - foreach( $REAL as $key => $value ) { - $GLOBALS[$key] = $value; - } - // Clearing and restoring session - foreach( $_SESSION as $key => $value ) { - unset($_SESSION[$key]); - } - foreach( $REAL_SESSION as $key => $value ) { - $_SESSION[$key] = $value; - } - return json_decode($json, true); - } - /** * Call action (mark as fav, archive, delete, etc.) */ @@ -423,28 +368,24 @@ class Poche switch ($action) { case 'add': - $content = $this->getPageContent($url); + $content = Tools::getPageContent($url); $title = ($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'); $body = $content['rss']['channel']['item']['description']; // clean content from prevent xss attack $config = HTMLPurifier_Config::createDefault(); + $config->set('Cache.SerializerPath', CACHE); $purifier = new HTMLPurifier($config); $title = $purifier->purify($title); $body = $purifier->purify($body); - //search for possible duplicate if not in import mode - if (!$import) { - $duplicate = $this->store->retrieveOneByURL($url->getUrl(), $this->user->getId()); - } + //search for possible duplicate + $duplicate = NULL; + $duplicate = $this->store->retrieveOneByURL($url->getUrl(), $this->user->getId()); - if ($this->store->add($url->getUrl(), $title, $body, $this->user->getId())) { + $last_id = $this->store->add($url->getUrl(), $title, $body, $this->user->getId()); + if ( $last_id ) { Tools::logm('add link ' . $url->getUrl()); - $sequence = ''; - if (STORAGE == 'postgres') { - $sequence = 'entries_id_seq'; - } - $last_id = $this->store->getLastId($sequence); if (DOWNLOAD_PICTURES) { $content = filtre_picture($body, $url->getUrl(), $last_id); Tools::logm('updating content article'); @@ -464,23 +405,17 @@ class Poche } } - if (!$import) { - $this->messages->add('s', _('the link has been added successfully')); - } + $this->messages->add('s', _('the link has been added successfully')); } else { - if (!$import) { - $this->messages->add('e', _('error during insertion : the link wasn\'t added')); - Tools::logm('error during insertion : the link wasn\'t added ' . $url->getUrl()); - } + $this->messages->add('e', _('error during insertion : the link wasn\'t added')); + Tools::logm('error during insertion : the link wasn\'t added ' . $url->getUrl()); } - if (!$import) { - if ($autoclose == TRUE) { - Tools::redirect('?view=home'); - } else { - Tools::redirect('?view=home&closewin=true'); - } + if ($autoclose == TRUE) { + Tools::redirect('?view=home'); + } else { + Tools::redirect('?view=home&closewin=true'); } break; case 'delete': @@ -501,62 +436,56 @@ class Poche case 'toggle_fav' : $this->store->favoriteById($id, $this->user->getId()); Tools::logm('mark as favorite link #' . $id); - if (!$import) { - Tools::redirect(); - } + Tools::redirect(); break; case 'toggle_archive' : $this->store->archiveById($id, $this->user->getId()); Tools::logm('archive link #' . $id); - if (!$import) { - Tools::redirect(); - } + Tools::redirect(); break; case 'archive_all' : $this->store->archiveAll($this->user->getId()); Tools::logm('archive all links'); - if (!$import) { - Tools::redirect(); - } + Tools::redirect(); break; case 'add_tag' : - if($import){ - $entry_id = $id; - $tags = explode(',', $tags); - } - else{ - $tags = explode(',', $_POST['value']); - $entry_id = $_POST['entry_id']; - } + $tags = explode(',', $_POST['value']); + $entry_id = $_POST['entry_id']; $entry = $this->store->retrieveOneById($entry_id, $this->user->getId()); if (!$entry) { $this->messages->add('e', _('Article not found!')); Tools::logm('error : article not found'); Tools::redirect(); } + //get all already set tags to preven duplicates + $already_set_tags = array(); + $entry_tags = $this->store->retrieveTagsByEntry($entry_id); + foreach ($entry_tags as $tag) { + $already_set_tags[] = $tag['value']; + } foreach($tags as $key => $tag_value) { $value = trim($tag_value); - $tag = $this->store->retrieveTagByValue($value); - - if (is_null($tag)) { - # we create the tag - $tag = $this->store->createTag($value); - $sequence = ''; - if (STORAGE == 'postgres') { - $sequence = 'tags_id_seq'; - } - $tag_id = $this->store->getLastId($sequence); - } - else { - $tag_id = $tag['id']; + if ($value && !in_array($value, $already_set_tags)) { + $tag = $this->store->retrieveTagByValue($value); + + if (is_null($tag)) { + # we create the tag + $tag = $this->store->createTag($value); + $sequence = ''; + if (STORAGE == 'postgres') { + $sequence = 'tags_id_seq'; + } + $tag_id = $this->store->getLastId($sequence); + } + else { + $tag_id = $tag['id']; + } + + # we assign the tag to the article + $this->store->setTagToEntry($tag_id, $entry_id); } - - # we assign the tag to the article - $this->store->setTagToEntry($tag_id, $entry_id); - } - if(!$import) { - Tools::redirect(); } + Tools::redirect(); break; case 'remove_tag' : $tag_id = $_GET['tag_id']; @@ -581,8 +510,12 @@ class Poche switch ($view) { case 'config': - $dev = trim($this->getPocheVersion('dev')); - $prod = trim($this->getPocheVersion('prod')); + $dev_infos = $this->getPocheVersion('dev'); + $dev = trim($dev_infos[0]); + $check_time_dev = date('d-M-Y H:i', $dev_infos[1]); + $prod_infos = $this->getPocheVersion('prod'); + $prod = trim($prod_infos[0]); + $check_time_prod = date('d-M-Y H:i', $prod_infos[1]); $compare_dev = version_compare(POCHE, $dev); $compare_prod = version_compare(POCHE, $prod); $themes = $this->getInstalledThemes(); @@ -594,6 +527,8 @@ class Poche 'languages' => $languages, 'dev' => $dev, 'prod' => $prod, + 'check_time_dev' => $check_time_dev, + 'check_time_prod' => $check_time_prod, 'compare_dev' => $compare_dev, 'compare_prod' => $compare_prod, 'token' => $token, @@ -619,13 +554,36 @@ class Poche break; case 'tags': $token = $this->user->getConfigValue('token'); - $tags = $this->store->retrieveAllTags($this->user->getId()); + //if term is set - search tags for this term + $term = Tools::checkVar('term'); + $tags = $this->store->retrieveAllTags($this->user->getId(), $term); + if (Tools::isAjaxRequest()) { + $result = array(); + foreach ($tags as $tag) { + $result[] = $tag['value']; + } + echo json_encode($result); + exit; + } $tpl_vars = array( 'token' => $token, 'user_id' => $this->user->getId(), 'tags' => $tags, ); break; + case 'search': + if (isset($_GET['search'])) { + $search = filter_var($_GET['search'], FILTER_SANITIZE_STRING); + $tpl_vars['entries'] = $this->store->search($search, $this->user->getId()); + $count = count($tpl_vars['entries']); + $this->pagination->set_total($count); + $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')), + $this->pagination->page_links('?view=' . $view . '?search=' . $search . '&sort=' . $_SESSION['sort'] . '&' )); + $tpl_vars['page_links'] = $page_links; + $tpl_vars['nb_results'] = $count; + $tpl_vars['search_term'] = $search; + } + break; case 'view': $entry = $this->store->retrieveOneById($id, $this->user->getId()); if ($entry != NULL) { @@ -660,8 +618,9 @@ class Poche 'entries' => '', 'page_links' => '', 'nb_results' => '', + 'listmode' => (isset($_COOKIE['listmode']) ? true : false), ); - + //if id is given - we retrive entries by tag: id is tag id if ($id) { $tpl_vars['tag'] = $this->store->retrieveTag($id, $this->user->getId()); @@ -686,8 +645,8 @@ class Poche } /** - * update the password of the current user. - * if MODE_DEMO is TRUE, the password can't be updated. + * update the password of the current user. + * if MODE_DEMO is TRUE, the password can't be updated. * @todo add the return value * @todo set the new password in function header like this updatePassword($newPassword) * @return boolean @@ -715,42 +674,44 @@ class Poche } } } - + public function updateTheme() { # no data if (empty($_POST['theme'])) { } - + # we are not going to change it to the current theme... if ($_POST['theme'] == $this->getTheme()) { $this->messages->add('w', _('still using the "' . $this->getTheme() . '" theme!')); Tools::redirect('?view=config'); } - + $themes = $this->getInstalledThemes(); $actualTheme = false; - + foreach (array_keys($themes) as $theme) { if ($theme == $_POST['theme']) { $actualTheme = true; break; } } - + if (! $actualTheme) { $this->messages->add('e', _('that theme does not seem to be installed')); Tools::redirect('?view=config'); } - + $this->store->updateUserConfig($this->user->getId(), 'theme', $_POST['theme']); $this->messages->add('s', _('you have changed your theme preferences')); - + $currentConfig = $_SESSION['poche_user']->config; $currentConfig['theme'] = $_POST['theme']; - + $_SESSION['poche_user']->setConfig($currentConfig); - + + $this->emptyCache(); + Tools::redirect('?view=config'); } @@ -759,39 +720,40 @@ class Poche # no data if (empty($_POST['language'])) { } - + # we are not going to change it to the current language... if ($_POST['language'] == $this->getLanguage()) { $this->messages->add('w', _('still using the "' . $this->getLanguage() . '" language!')); Tools::redirect('?view=config'); } - + $languages = $this->getInstalledLanguages(); $actualLanguage = false; - + foreach ($languages as $language) { if ($language['value'] == $_POST['language']) { $actualLanguage = true; break; } } - + if (! $actualLanguage) { $this->messages->add('e', _('that language does not seem to be installed')); Tools::redirect('?view=config'); } - + $this->store->updateUserConfig($this->user->getId(), 'language', $_POST['language']); $this->messages->add('s', _('you have changed your language preferences')); - + $currentConfig = $_SESSION['poche_user']->config; $currentConfig['language'] = $_POST['language']; - + $_SESSION['poche_user']->setConfig($currentConfig); - + + $this->emptyCache(); + Tools::redirect('?view=config'); } - /** * get credentials from differents sources * it redirects the user to the $referer link @@ -846,7 +808,7 @@ class Poche /** * log out the poche user. It cleans the session. * @todo add the return value - * @return boolean + * @return boolean */ public function logout() { @@ -857,225 +819,123 @@ class Poche } /** - * import from Instapaper. poche needs a ./instapaper-export.html file - * @todo add the return value - * @param string $targetFile the file used for importing + * import datas into your poche * @return boolean */ - private function importFromInstapaper($targetFile) - { - # TODO gestion des articles favs - $html = new simple_html_dom(); - $html->load_file($targetFile); - Tools::logm('starting import from instapaper'); - - $read = 0; - $errors = array(); - foreach($html->find('ol') as $ul) - { - foreach($ul->find('li') as $li) - { - $a = $li->find('a'); - $url = new Url(base64_encode($a[0]->href)); - $this->action('add', $url, 0, TRUE); - if ($read == '1') { - $sequence = ''; - if (STORAGE == 'postgres') { - $sequence = 'entries_id_seq'; - } - $last_id = $this->store->getLastId($sequence); - $this->action('toggle_archive', $url, $last_id, TRUE); - } + public function import() { + + if (!defined('IMPORT_LIMIT')) { + define('IMPORT_LIMIT', 5); + } + if (!defined('IMPORT_DELAY')) { + define('IMPORT_DELAY', 5); + } + + if ( isset($_FILES['file']) ) { + // assume, that file is in json format + $str_data = file_get_contents($_FILES['file']['tmp_name']); + $data = json_decode($str_data, true); + + if ( $data === null ) { + //not json - assume html + $html = new simple_html_dom(); + $html->load_file($_FILES['file']['tmp_name']); + $data = array(); + $read = 0; + foreach (array('ol','ul') as $list) { + foreach ($html->find($list) as $ul) { + foreach ($ul->find('li') as $li) { + $tmpEntry = array(); + $a = $li->find('a'); + $tmpEntry['url'] = $a[0]->href; + $tmpEntry['tags'] = $a[0]->tags; + $tmpEntry['is_read'] = $read; + if ($tmpEntry['url']) { + $data[] = $tmpEntry; + } + } + # the second
    is for read links + $read = ((sizeof($data) && $read)?0:1); } - - # the second
      is for read links - $read = 1; + } } - $this->messages->add('s', _('import from instapaper completed')); - Tools::logm('import from instapaper completed'); - Tools::redirect(); - } - /** - * import from Pocket. poche needs a ./ril_export.html file - * @todo add the return value - * @param string $targetFile the file used for importing - * @return boolean - */ - private function importFromPocket($targetFile) - { - # TODO gestion des articles favs - $html = new simple_html_dom(); - $html->load_file($targetFile); - Tools::logm('starting import from pocket'); - - $read = 0; - $errors = array(); - foreach($html->find('ul') as $ul) - { - foreach($ul->find('li') as $li) - { - $a = $li->find('a'); - $url = new Url(base64_encode($a[0]->href)); - $this->action('add', $url, 0, TRUE); - $sequence = ''; - if (STORAGE == 'postgres') { - $sequence = 'entries_id_seq'; - } - $last_id = $this->store->getLastId($sequence); - if ($read == '1') { - $this->action('toggle_archive', $url, $last_id, TRUE); - } - $tags = $a[0]->tags; - if(!empty($tags)) { - $this->action('add_tag',$url,$last_id,true,false,$tags); - } + //for readability structure + foreach ($data as $record) { + if (is_array($record)) { + $data[] = $record; + foreach ($record as $record2) { + if (is_array($record2)) { + $data[] = $record2; + } + } + } + } + + $i = 0; //counter for articles inserted + foreach ($data as $record) { + $url = trim( isset($record['article__url']) ? $record['article__url'] : (isset($record['url']) ? $record['url'] : '') ); + if ( $url ) { + $title = (isset($record['title']) ? $record['title'] : _('Untitled - Import - ').' '._('click to finish import').''); + $body = (isset($record['content']) ? $record['content'] : ''); + $isRead = (isset($record['is_read']) ? intval($record['is_read']) : (isset($record['archive'])?intval($record['archive']):0)); + $isFavorite = (isset($record['is_fav']) ? intval($record['is_fav']) : (isset($record['favorite'])?intval($record['favorite']):0) ); + //insert new record + $id = $this->store->add($url, $title, $body, $this->user->getId(), $isFavorite, $isRead); + if ( $id ) { + //increment no of records inserted + $i++; + if ( isset($record['tags']) && trim($record['tags']) ) { + //@TODO: set tags + + } } - - # the second
        is for read links - $read = 1; + } } - $this->messages->add('s', _('import from pocket completed')); - Tools::logm('import from pocket completed'); - Tools::redirect(); - } - /** - * import from Readability. poche needs a ./readability file - * @todo add the return value - * @param string $targetFile the file used for importing - * @return boolean - */ - private function importFromReadability($targetFile) - { - # TODO gestion des articles lus / favs - $str_data = file_get_contents($targetFile); - $data = json_decode($str_data,true); - Tools::logm('starting import from Readability'); - $count = 0; - foreach ($data as $key => $value) { - $url = NULL; - $favorite = FALSE; - $archive = FALSE; - foreach ($value as $item) { - foreach ($item as $attr => $value) { - if ($attr == 'article__url') { - $url = new Url(base64_encode($value)); - } - $sequence = ''; - if (STORAGE == 'postgres') { - $sequence = 'entries_id_seq'; - } - if ($value == 'true') { - if ($attr == 'favorite') { - $favorite = TRUE; - } - if ($attr == 'archive') { - $archive = TRUE; - } - } - } - - # we can add the url - if (!is_null($url) && $url->isCorrect()) { - $this->action('add', $url, 0, TRUE); - $count++; - if ($favorite) { - $last_id = $this->store->getLastId($sequence); - $this->action('toggle_fav', $url, $last_id, TRUE); - } - if ($archive) { - $last_id = $this->store->getLastId($sequence); - $this->action('toggle_archive', $url, $last_id, TRUE); - } - } - } + if ( $i > 0 ) { + $this->messages->add('s', _('Articles inserted: ').$i._('. Please note, that some may be marked as "read".')); } - $this->messages->add('s', _('import from Readability completed. ' . $count . ' new links.')); - Tools::logm('import from Readability completed'); + } + //file parsing finished here + + //now download article contents if any + + //check if we need to download any content + $recordsDownloadRequired = $this->store->retrieveUnfetchedEntriesCount($this->user->getId()); + if ( $recordsDownloadRequired == 0 ) { + //nothing to download + $this->messages->add('s', _('Import finished.')); Tools::redirect(); - } + } + else { + //if just inserted - don't download anything, download will start in next reload + if ( !isset($_FILES['file']) ) { + //download next batch + $items = $this->store->retrieveUnfetchedEntries($this->user->getId(), IMPORT_LIMIT); - /** - * import from Poche exported file - * @param string $targetFile the file used for importing - * @return boolean - */ - private function importFromPoche($targetFile) - { - $str_data = file_get_contents($targetFile); - $data = json_decode($str_data,true); - Tools::logm('starting import from Poche'); + $config = HTMLPurifier_Config::createDefault(); + $config->set('Cache.SerializerPath', CACHE); + $purifier = new HTMLPurifier($config); + foreach ($items as $item) { + $url = new Url(base64_encode($item['url'])); + $content = Tools::getPageContent($url); - $sequence = ''; - if (STORAGE == 'postgres') { - $sequence = 'entries_id_seq'; - } + $title = (($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled')); + $body = (($content['rss']['channel']['item']['description'] != '') ? $content['rss']['channel']['item']['description'] : _('Undefined')); - $count = 0; - foreach ($data as $value) { - - $url = new Url(base64_encode($value['url'])); - $favorite = ($value['is_fav'] == -1); - $archive = ($value['is_read'] == -1); - - # we can add the url - if (!is_null($url) && $url->isCorrect()) { - - $this->action('add', $url, 0, TRUE); - - $count++; - if ($favorite) { - $last_id = $this->store->getLastId($sequence); - $this->action('toggle_fav', $url, $last_id, TRUE); - } - if ($archive) { - $last_id = $this->store->getLastId($sequence); - $this->action('toggle_archive', $url, $last_id, TRUE); - } - } - - } - $this->messages->add('s', _('import from Poche completed. ' . $count . ' new links.')); - Tools::logm('import from Poche completed'); - Tools::redirect(); - } + //clean content to prevent xss attack + $title = $purifier->purify($title); + $body = $purifier->purify($body); + + $this->store->updateContentAndTitle($item['id'], $title, $body, $this->user->getId()); + } - /** - * import datas into your poche - * @param string $from name of the service to import : pocket, instapaper or readability - * @todo add the return value - * @return boolean - */ - public function import($from) - { - $providers = array( - 'pocket' => 'importFromPocket', - 'readability' => 'importFromReadability', - 'instapaper' => 'importFromInstapaper', - 'poche' => 'importFromPoche', - ); - - if (! isset($providers[$from])) { - $this->messages->add('e', _('Unknown import provider.')); - Tools::redirect(); - } - - $targetDefinition = 'IMPORT_' . strtoupper($from) . '_FILE'; - $targetFile = constant($targetDefinition); - - if (! defined($targetDefinition)) { - $this->messages->add('e', _('Incomplete inc/poche/define.inc.php file, please define "' . $targetDefinition . '".')); - Tools::redirect(); - } - - if (! file_exists($targetFile)) { - $this->messages->add('e', _('Could not find required "' . $targetFile . '" import file.')); - Tools::redirect(); } - - $this->$providers[$from]($targetFile); + } + + return array('includeImport'=>true, 'import'=>array('recordsDownloadRequired'=>$recordsDownloadRequired, 'recordsUnderDownload'=> IMPORT_LIMIT, 'delay'=> IMPORT_DELAY * 1000) ); } /** @@ -1084,6 +944,9 @@ class Poche */ public function export() { + $filename = "wallabag-export-".$this->user->getId()."-".date("Y-m-d").".json"; + header('Content-Disposition: attachment; filename='.$filename); + $entries = $this->store->retrieveAll($this->user->getId()); echo $this->tpl->render('export.twig', array( 'export' => Tools::renderJson($entries), @@ -1099,21 +962,29 @@ class Poche private function getPocheVersion($which = 'prod') { $cache_file = CACHE . '/' . $which; + $check_time = time(); # checks if the cached version file exists if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) { $version = file_get_contents($cache_file); + $check_time = filemtime($cache_file); } else { $version = file_get_contents('http://static.wallabag.org/versions/' . $which); file_put_contents($cache_file, $version, LOCK_EX); } - return $version; + return array($version, $check_time); } public function generateToken() { if (ini_get('open_basedir') === '') { - $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15); + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + echo 'This is a server using Windows!'; + // alternative to /dev/urandom for Windows + $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20); + } else { + $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15); + } } else { $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20); @@ -1124,6 +995,7 @@ class Poche $currentConfig = $_SESSION['poche_user']->config; $currentConfig['token'] = $token; $_SESSION['poche_user']->setConfig($currentConfig); + Tools::redirect(); } public function generateFeeds($token, $user_id, $tag_id, $type = 'home') @@ -1131,6 +1003,10 @@ class Poche $allowed_types = array('home', 'fav', 'archive', 'tag'); $config = $this->store->getConfigUser($user_id); + if ($config == null) { + die(_('User with this id (' . $user_id . ') does not exist.')); + } + if (!in_array($type, $allowed_types) || $token != $config['token']) { die(_('Uh, there is a problem while generating feeds.')); @@ -1140,8 +1016,9 @@ class Poche $feed = new FeedWriter(RSS2); $feed->setTitle('wallabag — ' . $type . ' feed'); $feed->setLink(Tools::getPocheUrl()); - $feed->setChannelElement('updated', date(DATE_RSS , time())); - $feed->setChannelElement('author', 'wallabag'); + $feed->setChannelElement('pubDate', date(DATE_RSS , time())); + $feed->setChannelElement('generator', 'wallabag'); + $feed->setDescription('wallabag ' . $type . ' elements'); if ($type == 'tag') { $entries = $this->store->retrieveEntriesByTag($tag_id, $user_id); @@ -1154,7 +1031,7 @@ class Poche foreach ($entries as $entry) { $newItem = $feed->createNewItem(); $newItem->setTitle($entry['title']); - $newItem->setLink(Tools::getPocheUrl() . '?view=view&id=' . $entry['id']); + $newItem->setLink($entry['url']); $newItem->setDate(time()); $newItem->setDescription($entry['content']); $feed->addItem($newItem); diff --git a/inc/poche/Tools.class.php b/inc/poche/Tools.class.php old mode 100644 new mode 100755 index 4ed28ed1..a130e94b --- a/inc/poche/Tools.class.php +++ b/inc/poche/Tools.class.php @@ -7,7 +7,7 @@ * @copyright 2013 * @license http://www.wtfpl.net/ see COPYING file */ - + class Tools { public static function initPhp() @@ -42,7 +42,7 @@ class Tools && (strtolower($_SERVER['HTTPS']) == 'on')) || (isset($_SERVER["SERVER_PORT"]) && $_SERVER["SERVER_PORT"] == '443') // HTTPS detection. - || (isset($_SERVER["SERVER_PORT"]) //Custom HTTPS port detection + || (isset($_SERVER["SERVER_PORT"]) //Custom HTTPS port detection && $_SERVER["SERVER_PORT"] == SSL_PORT) || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'); @@ -148,7 +148,7 @@ class Tools ); # only download page lesser than 4MB - $data = @file_get_contents($url, false, $context, -1, 4000000); + $data = @file_get_contents($url, false, $context, -1, 4000000); if (isset($http_response_header) and isset($http_response_header[0])) { $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 +193,14 @@ class Tools public static function logm($message) { - if (DEBUG_POCHE) { + if (DEBUG_POCHE && php_sapi_name() != 'cli') { $t = strval(date('Y/m/d_H:i:s')) . ' - ' . $_SERVER["REMOTE_ADDR"] . ' - ' . strval($message) . "\n"; file_put_contents(CACHE . '/log.txt', $t, FILE_APPEND); error_log('DEBUG POCHE : ' . $message); } } - public static function encodeString($string) + public static function encodeString($string) { return sha1($string . SALT); } @@ -241,7 +241,6 @@ class Tools } } - public static function download_db() { header('Content-Disposition: attachment; filename="poche.sqlite.gz"'); self::status(200); @@ -252,4 +251,74 @@ class Tools exit; } + + public static function getPageContent(Url $url) + { + // Saving and clearing context + $REAL = array(); + foreach( $GLOBALS as $key => $value ) { + if( $key != 'GLOBALS' && $key != '_SESSION' && $key != 'HTTP_SESSION_VARS' ) { + $GLOBALS[$key] = array(); + $REAL[$key] = $value; + } + } + // Saving and clearing session + if ( isset($_SESSION) ) { + $REAL_SESSION = array(); + foreach( $_SESSION as $key => $value ) { + $REAL_SESSION[$key] = $value; + unset($_SESSION[$key]); + } + } + + // Running code in different context + $scope = function() { + extract( func_get_arg(1) ); + $_GET = $_REQUEST = array( + "url" => $url->getUrl(), + "max" => 5, + "links" => "preserve", + "exc" => "", + "format" => "json", + "submit" => "Create Feed" + ); + ob_start(); + require func_get_arg(0); + $json = ob_get_contents(); + ob_end_clean(); + return $json; + }; + $json = $scope( "inc/3rdparty/makefulltextfeed.php", array("url" => $url) ); + + // Clearing and restoring context + foreach( $GLOBALS as $key => $value ) { + if( $key != "GLOBALS" && $key != "_SESSION" ) { + unset($GLOBALS[$key]); + } + } + foreach( $REAL as $key => $value ) { + $GLOBALS[$key] = $value; + } + // Clearing and restoring session + if ( isset($REAL_SESSION) ) { + foreach( $_SESSION as $key => $value ) { + unset($_SESSION[$key]); + } + foreach( $REAL_SESSION as $key => $value ) { + $_SESSION[$key] = $value; + } + } + + return json_decode($json, true); + } + + /** + * Returns whether we handle an AJAX (XMLHttpRequest) request. + * @return boolean whether we handle an AJAX (XMLHttpRequest) request. + */ + public static function isAjaxRequest() + { + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest'; + } + } diff --git a/inc/poche/config.inc.php.new b/inc/poche/config.inc.php.new index 8d52497b..83b3c4c0 100755 --- a/inc/poche/config.inc.php.new +++ b/inc/poche/config.inc.php.new @@ -52,12 +52,8 @@ define ('CACHE', ROOT . '/cache'); define ('PAGINATION', '10'); -define ('POCKET_FILE', '/ril_export.html'); -define ('READABILITY_FILE', '/readability'); -define ('INSTAPAPER_FILE', '/instapaper-export.html'); -define ('POCHE_FILE', '/poche-export'); - -define ('IMPORT_POCKET_FILE', ROOT . POCKET_FILE); -define ('IMPORT_READABILITY_FILE', ROOT . READABILITY_FILE); -define ('IMPORT_INSTAPAPER_FILE', ROOT . INSTAPAPER_FILE); -define ('IMPORT_POCHE_FILE', ROOT . POCHE_FILE); \ No newline at end of file +//limit for download of articles during import +define ('IMPORT_LIMIT', 5); +//delay between downloads (in sec) +define ('IMPORT_DELAY', 5); + diff --git a/inc/poche/global.inc.php b/inc/poche/global.inc.php index d22b0588..15091387 100644 --- a/inc/poche/global.inc.php +++ b/inc/poche/global.inc.php @@ -38,7 +38,7 @@ if (! file_exists(ROOT . '/vendor/autoload.php')) { require_once ROOT . '/vendor/autoload.php'; } -# system configuration; database credentials et cetera +# system configuration; database credentials et caetera if (! file_exists(INCLUDES . '/poche/config.inc.php')) { Poche::$configFileAvailable = false; } else { diff --git a/index.php b/index.php old mode 100644 new mode 100755 index a26c42e1..7f72b146 --- a/index.php +++ b/index.php @@ -8,10 +8,13 @@ * @license http://www.wtfpl.net/ see COPYING file */ -define ('POCHE', '1.5.2'); +define ('POCHE', '1.6.0'); require 'check_setup.php'; require_once 'inc/poche/global.inc.php'; -session_start(); + +# Start session +Session::$sessionName = 'poche'; +Session::init(); # Start Poche $poche = new Poche(); @@ -30,14 +33,14 @@ $tpl_vars = array( 'referer' => $referer, 'view' => $view, 'poche_url' => Tools::getPocheUrl(), - 'title' => _('poche, a read it later open source system'), + 'title' => _('wallabag, a read it later open source system'), 'token' => Session::getToken(), 'theme' => $poche->getTheme() ); if (! empty($notInstalledMessage)) { if (! Poche::$canRenderTemplates || ! Poche::$configFileAvailable) { - # We cannot use Twig to display the error message + # We cannot use Twig to display the error message echo '

        Errors

          '; foreach ($notInstalledMessage as $message) { echo '
        1. ' . $message . '
        2. '; @@ -64,7 +67,8 @@ if (isset($_GET['login'])) { # Update password $poche->updatePassword(); } elseif (isset($_GET['import'])) { - $import = $poche->import($_GET['from']); + $import = $poche->import(); + $tpl_vars = array_merge($tpl_vars, $import); } elseif (isset($_GET['download'])) { Tools::download_db(); } elseif (isset($_GET['empty-cache'])) { @@ -75,6 +79,8 @@ if (isset($_GET['login'])) { $poche->updateTheme(); } elseif (isset($_GET['updatelanguage'])) { $poche->updateLanguage(); +} elseif (isset($_GET['uploadfile'])) { + $poche->uploadFile(); } elseif (isset($_GET['feed'])) { if (isset($_GET['action']) && $_GET['action'] == 'generate') { $poche->generateToken(); @@ -115,6 +121,7 @@ if (Session::isLogged()) { } else { $tpl_file = Tools::getTplFile('login'); $tpl_vars['http_auth'] = 0; + Session::logout(); } # 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..e02952e0 100644 --- a/install/index.php +++ b/install/index.php @@ -1,9 +1,30 @@ download it manually<∕a> and unzip it in your wallabag folder.'; + $errors[] = 'Impossible to download vendor.zip. Please
          download it manually and unzip it in your wallabag folder.'; } else { if (extension_loaded('zip')) { @@ -25,7 +46,7 @@ if ($_POST['download']) { } } } -else if ($_POST['install']) { +else if (isset($_POST['install'])) { if (!is_dir('vendor')) { $errors[] = 'You must install twig before.'; } @@ -64,6 +85,7 @@ else if ($_POST['install']) { else { $db_path = 'sqlite:' . realpath('') . '/db/poche.sqlite'; $handle = new PDO($db_path); + $sql_structure = ""; } } else { @@ -129,7 +151,7 @@ else if ($_POST['install']) { $params = array($id_user, 'language', 'en_EN.UTF8'); $query = executeQuery($handle, $sql, $params); - $successes[] = 'wallabag is now installed. Don\'t forget to delete install folder. Then, reload this page.'; + $successes[] = 'wallabag is now installed. You can now access it !'; } } } @@ -143,7 +165,7 @@ else if ($_POST['install']) { - wallabag — installation + wallabag - installation @@ -154,7 +176,7 @@ else if ($_POST['install']) { - + @@ -198,18 +220,18 @@ else if ($_POST['install']) {

          - wallabag seems already installed. If you want to update it, you only have to delete install folder. + wallabag seems already installed. If you want to update it, you only have to delete install folder, then reload this page.

          To install wallabag, you just have to fill the following fields. That's all.

          -

          Don't forget to check your server compatibility here.

          +

          Don't forget to check your server compatibility here.

          Technical settings -
          wallabag needs twig, a template engine (?). Two ways to install it: +
          wallabag needs twig, a template engine (?). Two ways to install it:
          • automatically download and extract vendor.zip into your wallabag folder.

            @@ -225,7 +247,11 @@ php composer.phar install
          • Database engine:

              -
            • +
            • +
              +

              You have to enable pdo_sqlite extension.

              +
              +
              • @@ -241,7 +267,7 @@ php composer.phar install
              • - id
              • +
            @@ -263,26 +289,49 @@ php composer.phar install

          - +
          - - + + + + + + + + + + + + + + + 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 @@
        3. {% trans "favorites" %}
        4. {% trans "archive" %}
        5. {% trans "tags" %}
        6. -
        7. {% trans "save a link" %}
        8. +
        9. {% trans "save a link" %} + {% include '_pocheit-form.twig' %} +
        10. +
        11. {% trans "search" %} + {% include '_search-form.twig' %} +
        12. {% trans "config" %}
        13. {% trans "logout" %}
      - {% 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 @@ +
      +
      +

      X + {% trans "Save a link" %}

      + + + + +
      +
      diff --git a/themes/baggy/_search-form.twig b/themes/baggy/_search-form.twig new file mode 100644 index 00000000..1fd4154e --- /dev/null +++ b/themes/baggy/_search-form.twig @@ -0,0 +1,21 @@ +
      +
      + + : + +
      +
      + \ No newline at end of file diff --git a/themes/baggy/_top.twig b/themes/baggy/_top.twig old mode 100644 new mode 100755 index 4f476a37..a31c0925 --- a/themes/baggy/_top.twig +++ b/themes/baggy/_top.twig @@ -1,6 +1,6 @@

      - {% if view == 'home' %}{% block logo %}logo poche{% endblock %} + {% if view == 'home' %}{% block logo %}wallabag logo{% endblock %} {% else %}{{ block('logo') }} {% endif %}

      diff --git a/themes/baggy/config.twig b/themes/baggy/config.twig old mode 100644 new mode 100755 index b37ac115..d441de7c --- a/themes/baggy/config.twig +++ b/themes/baggy/config.twig @@ -8,10 +8,11 @@

      {% trans "Saving articles" %}

      {% trans "There are several ways to save an article:" %} (?)

        -
      • Firefox: {% trans "download the extension" %}
      • +
      • Firefox: {% trans "download the extension" %}
      • Chrome: {% trans "download the extension" %}
      • -
      • Android: {% trans "via F-Droid" %} {% trans " or " %} {% trans "via Google Play" %}
      • -
      • Windows Phone: {% trans "download the application" %}
      • +
      • Android: {% trans "via F-Droid" %} {% trans " or " %} {% trans "via Google Play" %}
      • +
      • iOS: {% trans "download the application" %}
      • +
      • Windows Phone: {% trans "download the application" %}
      • @@ -25,9 +26,10 @@

        {% trans "Upgrading wallabag" %}

        +

        {% trans "You can clear cache to check the latest release." %}

        {% trans "Feeds" %}

        {% if token == '' %} @@ -42,7 +44,7 @@

        {% trans "Your user id:" %} {{user_id}}

        {% trans "You can regenerate your token: generate!." %}

        {% endif %} - +

        {% trans "Change your theme" %}

        @@ -103,15 +105,21 @@ {% endif %}

        {% trans "Import" %}

        -

        {% trans "Please execute the import script locally as it can take a very long time." %}

        -

        {% trans "More info in the official documentation:" %} wallabag.org

        - - +

        {% trans "You can import your Pocket, Readability, Instapaper, Wallabag or any data in appropriate json or html format." %}

        +

        {% trans "Please select export file on your computer and press \"Import\" button below.
        Wallabag will parse your file, insert all URLs and start fetching of articles if required.
        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)." %}

        + +
        +
        + + +
        +
        + +
        +
        + +

        {% trans "You can click here to fetch content for articles with no content." %}

        +

        {% trans "Export your wallabag data" %}

        {% if constant('STORAGE') == 'sqlite' %}

        {% trans "Click here" %} {% trans "to download your database." %}

        {% endif %} diff --git a/themes/baggy/css/main.css b/themes/baggy/css/main.css index 3e128b08..0dd552de 100755 --- a/themes/baggy/css/main.css +++ b/themes/baggy/css/main.css @@ -173,21 +173,22 @@ h2:after { #links { position: fixed; top: 0; - width: 9em; + width: 10em; left: 0; text-align: right; background: #333; - padding-top: 9em; + padding-top: 9.5em; height: 100%; box-shadow:inset -4px 0 20px rgba(0,0,0,0.6); z-index: 10; } #main { - margin-left: 12em; + margin-left: 13em; position: relative; z-index: 10; padding-right: 5%; + padding-bottom: 1em; } #links a { @@ -227,7 +228,7 @@ h2:after { #links li:last-child { position: fixed; bottom: 1em; - width: 10%; + width: 10em; } #links li:last-child a:before { @@ -237,6 +238,61 @@ h2:after { } +#sort { + padding: 0; + list-style-type: none; + opacity: 0.5; + display: inline-block; +} + +#sort li { + display: inline; + font-size: 0.9em; +} + +#sort li + li { + margin-left: 10px; +} + +#sort a { + padding: 2px 2px 0; + vertical-align: middle; +} + +#sort img { + vertical-align: baseline; +} +#sort img:hover { + cursor: pointer; +} + +#display-mode { + float: right; + vertical-align: middle; + margin-top: 10px; + margin-bottom: 10px; + opacity: 0.5; +} +#listmode { + width: 16px; + display: inline-block; + text-decoration: none; +} +#listmode a:hover { + opacity: 1; +} +.tablemode { + background-image: url("../img/baggy/table.png"); + background-repeat: no-repeat; + background-position: bottom; +} +.listmode { + background-image: url("../img/baggy/list.png"); + background-repeat: no-repeat; + background-position: bottom; +} + + /* ========================================================================== 2 = Layout ========================================================================== */ @@ -248,7 +304,7 @@ h2:after { footer { text-align: right; - position: fixed; + position: relative; bottom: 0; right: 5em; color: #999; @@ -266,6 +322,15 @@ footer a { letter-spacing:-5px; } +.listmode .entrie { + width: 100%!important; + margin-left: 0!important; +} + +.listmode .entrie p { + display: none; +} + .list-entries + .results { margin-bottom: 2em; } @@ -287,10 +352,10 @@ footer a { letter-spacing:normal; box-shadow: 0 3px 7px rgba(0,0,0,0.3); display: inline-block; - width: 32%; + width: 32%!important; margin-bottom: 1.5em; vertical-align: top; - margin-left: 1.5%; + margin-left: 1.5%!important; position: relative; overflow: hidden; padding: 1.5em 1.5em 3em 1.5em; @@ -359,6 +424,7 @@ footer a { content: none; } + .entrie h2 a { display: block; text-decoration: none; @@ -370,7 +436,7 @@ footer a { -o-transition: all 0.5s ease; transition: all 0.5s ease; } - +/* .entrie h2 a:after { content: ""; position: absolute; @@ -379,6 +445,7 @@ footer a { height: 100%; left: 0; } +*/ .entrie p { color: #666; @@ -425,7 +492,7 @@ footer a { } .entrie:nth-child(3n+1) { - margin-left: 0; + margin-left: 0!important; } .results { @@ -442,6 +509,7 @@ footer a { .pagination { text-align: right; + margin-bottom:50px; } .nb-results { @@ -468,6 +536,134 @@ footer a { display: none; } +/* ========================================================================== + 2.1 = "save a link" popup div related styles + ========================================================================== */ + +#bagit-form { + background: rgba(0,0,0,0.5); + position: absolute; + top: 0; + left: 10em; + z-index: 20; + height: 100%; + width: 100%; + margin: 0; + margin-top: -30%; + padding: 2em; + display: none; + border-left: 1px #EEE solid; +} + +#bagit-form form { + background: #FFF; + position: absolute; + top: 0; + left: 0; + z-index: 20; + border: 10px solid #000; + width: 400px; + height: 200px; + /* margin: -150px 0 0 -300px; */ + padding: 2em; +} + +a#bagit-form-close { + background: #000; + color: #FFF; + padding: 0.2em 0.5em; + text-decoration: none; + display: inline-block; + float: right; + font-size: 0.6em; +} +a#bagit-form-close:hover { + background: #999; + color: #000; +} + +.active-current { + background-color: #999; +} + +.active-current:after { + content: ""; + width: 0; + height: 0; + position: absolute; + border-style: solid; + border-width: 10px; + border-color: transparent #EEE transparent transparent; + right: 0; + top: 50%; + margin-top: -10px; +} + +.opacity03 { + opacity: 0.3; +} + +.add-to-wallabag-link-after { + background-color: #000; + color: #fff; + padding: 0 3px 2px 3px; +} + +#add-link-result { + font-weight: bold; + margin-top: 10px; +} + +/* ========================================================================== + 2.2 = "search for articles" popup div related styles + ========================================================================== */ +#search-form { + background: rgba(0,0,0,0.5); + position: absolute; + top: 0; + left: 10em; + z-index: 20; + height: 100%; + width: 100%; + margin: 0; + margin-top: -30%; + padding: 2em; + display: none; + border-left: 1px #EEE solid; +} + +#search-form form { + background: #FFF; + position: absolute; + top: 0; + left: 0; + z-index: 20; + border: 10px solid #000; + width: 400px; + height: 200px; + /* margin: -150px 0 0 -300px; */ + padding: 2em; +} + +a#search-form-close { + background: #000; + color: #FFF; + padding: 0.2em 0.5em; + text-decoration: none; + display: inline-block; + float: right; + font-size: 1.2em; +} +a#search-form-close:hover { + background: #999; + color: #000; +} + +#submit-search{ +margin-left: 4em; +margin-top:1em; +} + /* ========================================================================== 3 = Pictos ========================================================================== */ @@ -583,7 +779,7 @@ footer a { } .warning { - font-size: 3em; + /* font-size: 3em; color: #999; font-style: italic; position: absolute; @@ -592,7 +788,10 @@ footer a { width: 100%; text-align: center; padding-right: 5%; - margin-top: -2em; + margin-top: -2em;*/ + font-weight: bold; + display: block; + width: 100%; } /* ========================================================================== @@ -602,6 +801,7 @@ footer a { #article { width: 70%; margin-bottom: 3em; + text-align: justify; } #article .tags { @@ -731,6 +931,9 @@ blockquote { width: 100%; margin-left: 0; } + #display-mode { + display: none; + } } @media screen and (max-width: 500px) { @@ -820,4 +1023,12 @@ blockquote { #article_toolbar a { padding: 0.3em 0.4em 0.2em; } + + #display-mode { + display: none; + } + + #bagit-form { + left: 0; + } } diff --git a/themes/baggy/edit-tags.twig b/themes/baggy/edit-tags.twig old mode 100644 new mode 100755 index 9e9012ee..2113e964 --- a/themes/baggy/edit-tags.twig +++ b/themes/baggy/edit-tags.twig @@ -4,6 +4,11 @@ {% include '_menu.twig' %} {% endblock %} {% block content %} + + + + +

        {{ entry.title|raw }}

        @@ -17,7 +22,8 @@ -

        {% trans "You can enter multiple tags, separated by commas." %}

        +

        {% trans "Start typing for auto complete." %}
        + {% trans "You can enter multiple tags, separated by commas." %}

        {% trans "return to article" %} {% endblock %} diff --git a/themes/baggy/home.twig b/themes/baggy/home.twig old mode 100644 new mode 100755 index 4f9db063..5dd91307 --- a/themes/baggy/home.twig +++ b/themes/baggy/home.twig @@ -18,17 +18,27 @@ {% if entries is empty %}

        {% trans "No articles found." %}

        {% else %} +
        + {% include '_display-mode.twig' %} + {% include '_sorting.twig' %} +
        {% block pager %} {% if nb_results > 1 %}
        -
        {{ nb_results }} {% trans "results" %}
        +
        {{ nb_results }} {% trans "results" %}{% if search_term is defined %}{% trans " found for « " %} {{ search_term }} »{% endif %}
        {{ page_links | raw }}
        + {% elseif nb_results == 1 %} + {% if search_term is defined %} +
        +
        {% trans "Only one result found for " %} « {{ search_term }} »
        +
        + {% endif %} {% endif %} {% endblock %} -
        +
        {% for entry in entries %} -
        +

        {{ entry.title|raw }}

        {% if entry.content| getReadingTime > 0 %} diff --git a/themes/baggy/img/baggy/blank.png b/themes/baggy/img/baggy/blank.png new file mode 100755 index 00000000..63e09844 Binary files /dev/null and b/themes/baggy/img/baggy/blank.png differ diff --git a/themes/baggy/img/baggy/down.png b/themes/baggy/img/baggy/down.png new file mode 100644 index 00000000..b9d536a7 Binary files /dev/null and b/themes/baggy/img/baggy/down.png differ diff --git a/themes/baggy/img/baggy/list.png b/themes/baggy/img/baggy/list.png new file mode 100755 index 00000000..bd5aff5a Binary files /dev/null and b/themes/baggy/img/baggy/list.png differ diff --git a/themes/baggy/img/baggy/table.png b/themes/baggy/img/baggy/table.png new file mode 100755 index 00000000..859c4cd8 Binary files /dev/null and b/themes/baggy/img/baggy/table.png differ diff --git a/themes/baggy/img/baggy/top.png b/themes/baggy/img/baggy/top.png new file mode 100644 index 00000000..954a8c0a Binary files /dev/null and b/themes/baggy/img/baggy/top.png differ diff --git a/themes/baggy/img/logo-other_themes.png b/themes/baggy/img/logo-other_themes.png new file mode 100755 index 00000000..32543a44 Binary files /dev/null and b/themes/baggy/img/logo-other_themes.png 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 @@ -$(document).ready(function() { - current_url = window.location.href - if (current_url.match("&closewin=true")) { - window.close(); - } -}); 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 @@ -document.addEventListener('DOMContentLoaded', function() { - var menu = document.getElementById('menu'); - - menu.addEventListener('click', function(){ - if(this.nextElementSibling.style.display === "block") { - this.nextElementSibling.style.display = "none"; - }else { - this.nextElementSibling.style.display = "block"; +$.fn.ready(function() { + + var $listmode = $('#listmode'), + $listentries = $("#list-entries"); + + /* ========================================================================== + Menu + ========================================================================== */ + + $("#menu").click(function(){ + $("#links").toggle(); + }); + + /* ========================================================================== + List mode or Table Mode + ========================================================================== */ + + $listmode.click(function(){ + if ( $.cookie("listmode") == 1 ) { + // Cookie + $.removeCookie("listmode"); + + $listentries.removeClass("listmode"); + $listmode.removeClass("tablemode"); + $listmode.addClass("listmode"); } - + else { + // Cookie + $.cookie("listmode", 1, {expires: 365}); + + $listentries.addClass("listmode"); + $listmode.removeClass("listmode"); + $listmode.addClass("tablemode"); + } + }); -}); \ No newline at end of file + + /* ========================================================================== + Cookie listmode + ========================================================================== */ + + if ( $.cookie("listmode") == 1 ) { + $listentries.addClass("listmode"); + $listmode.removeClass("listmode"); + $listmode.addClass("tablemode"); + } + +}); 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 @@ -(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="
        ",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="",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="
        ","#"===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="",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*\s*$/g,ct={option:[1,""],thead:[1,"","
        "],col:[2,"","
        "],tr:[2,"","
        "],td:[3,"","
        "],_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>");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>")+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("