aboutsummaryrefslogtreecommitdiffhomepage
path: root/inc
diff options
context:
space:
mode:
Diffstat (limited to 'inc')
-rw-r--r--inc/3rdparty/Session.class.php13
-rw-r--r--inc/3rdparty/libraries/feedwriter/FeedItem.php1
-rwxr-xr-xinc/3rdparty/makefulltextfeed.php353
-rwxr-xr-xinc/3rdparty/makefulltextfeedHelpers.php355
-rwxr-xr-xinc/poche/Database.class.php76
-rwxr-xr-xinc/poche/Poche.class.php210
-rwxr-xr-x[-rw-r--r--]inc/poche/Tools.class.php73
7 files changed, 602 insertions, 479 deletions
diff --git a/inc/3rdparty/Session.class.php b/inc/3rdparty/Session.class.php
index b30a31f3..599b68cd 100644
--- a/inc/3rdparty/Session.class.php
+++ b/inc/3rdparty/Session.class.php
@@ -33,7 +33,7 @@ class Session
33 // his/her session is considered expired (3600 sec. = 1 hour) 33 // his/her session is considered expired (3600 sec. = 1 hour)
34 public static $inactivityTimeout = 86400; 34 public static $inactivityTimeout = 86400;
35 // Extra timeout for long sessions (if enabled) (82800 sec. = 23 hours) 35 // Extra timeout for long sessions (if enabled) (82800 sec. = 23 hours)
36 public static $longSessionTimeout = 31536000; 36 public static $longSessionTimeout = 604800; // 604800 = a week
37 // If you get disconnected often or if your IP address changes often. 37 // If you get disconnected often or if your IP address changes often.
38 // Let you disable session cookie hijacking protection 38 // Let you disable session cookie hijacking protection
39 public static $disableSessionProtection = false; 39 public static $disableSessionProtection = false;
@@ -61,7 +61,7 @@ class Session
61 if (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") { 61 if (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") {
62 $ssl = true; 62 $ssl = true;
63 } 63 }
64 session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['HTTP_HOST'], $ssl); 64 session_set_cookie_params(self::$longSessionTimeout, $cookiedir, $_SERVER['HTTP_HOST'], $ssl);
65 // Use cookies to store session. 65 // Use cookies to store session.
66 ini_set('session.use_cookies', 1); 66 ini_set('session.use_cookies', 1);
67 // Force cookies for session (phpsessionID forbidden in URL) 67 // Force cookies for session (phpsessionID forbidden in URL)
@@ -143,7 +143,14 @@ class Session
143 */ 143 */
144 public static function logout() 144 public static function logout()
145 { 145 {
146 unset($_SESSION['uid'],$_SESSION['ip'],$_SESSION['expires_on'],$_SESSION['tokens'], $_SESSION['login'], $_SESSION['pass'], $_SESSION['longlastingsession'], $_SESSION['poche_user']); 146 // unset($_SESSION['uid'],$_SESSION['ip'],$_SESSION['expires_on'],$_SESSION['tokens'], $_SESSION['login'], $_SESSION['pass'], $_SESSION['longlastingsession'], $_SESSION['poche_user']);
147
148 // Destruction du cookie (le code peut paraître complexe mais c'est pour être certain de reprendre les mêmes paramètres)
149 $args = array_merge(array(session_name(), ''), array_values(session_get_cookie_params()));
150 $args[2] = time() - 3600;
151 call_user_func_array('setcookie', $args);
152 // Suppression physique de la session
153 session_destroy();
147 } 154 }
148 155
149 /** 156 /**
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 @@
156 if($this->version == RSS2 || $this->version == RSS1) 156 if($this->version == RSS2 || $this->version == RSS1)
157 { 157 {
158 $this->setElement('link', $link); 158 $this->setElement('link', $link);
159 $this->setElement('guid', $link);
159 } 160 }
160 else 161 else
161 { 162 {
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()) {
55 55
56// set include path 56// set include path
57set_include_path(realpath(dirname(__FILE__).'/libraries').PATH_SEPARATOR.get_include_path()); 57set_include_path(realpath(dirname(__FILE__).'/libraries').PATH_SEPARATOR.get_include_path());
58// Autoloading of classes allows us to include files only when they're 58
59// needed. If we've got a cached copy, for example, only Zend_Cache is loaded. 59require_once dirname(__FILE__).'/makefulltextfeedHelpers.php';
60function autoload($class_name) {
61 static $dir = null;
62 if ($dir === null) $dir = dirname(__FILE__).'/libraries/';
63 static $mapping = array(
64 // Include FeedCreator for RSS/Atom creation
65 'FeedWriter' => 'feedwriter/FeedWriter.php',
66 'FeedItem' => 'feedwriter/FeedItem.php',
67 // Include ContentExtractor and Readability for identifying and extracting content from URLs
68 'ContentExtractor' => 'content-extractor/ContentExtractor.php',
69 'SiteConfig' => 'content-extractor/SiteConfig.php',
70 'Readability' => 'readability/Readability.php',
71 // Include Humble HTTP Agent to allow parallel requests and response caching
72 'HumbleHttpAgent' => 'humble-http-agent/HumbleHttpAgent.php',
73 'SimplePie_HumbleHttpAgent' => 'humble-http-agent/SimplePie_HumbleHttpAgent.php',
74 'CookieJar' => 'humble-http-agent/CookieJar.php',
75 // Include Zend Cache to improve performance (cache results)
76 'Zend_Cache' => 'Zend/Cache.php',
77 // Language detect
78 'Text_LanguageDetect' => 'language-detect/LanguageDetect.php',
79 // HTML5 Lib
80 'HTML5_Parser' => 'html5/Parser.php',
81 // htmLawed - used if XSS filter is enabled (xss_filter)
82 'htmLawed' => 'htmLawed/htmLawed.php'
83 );
84 if (isset($mapping[$class_name])) {
85 debug("** Loading class $class_name ({$mapping[$class_name]})");
86 require $dir.$mapping[$class_name];
87 return true;
88 } else {
89 return false;
90 }
91}
92spl_autoload_register('autoload');
93require dirname(__FILE__).'/libraries/simplepie/autoloader.php';
94 60
95//////////////////////////////// 61////////////////////////////////
96// Load config file 62// Load config file
@@ -415,6 +381,7 @@ if (!$debug_mode) {
415////////////////////////////////// 381//////////////////////////////////
416// Set up HTTP agent 382// Set up HTTP agent
417////////////////////////////////// 383//////////////////////////////////
384global $http;
418$http = new HumbleHttpAgent(); 385$http = new HumbleHttpAgent();
419$http->debug = $debug_mode; 386$http->debug = $debug_mode;
420$http->userAgentMap = $options->user_agents; 387$http->userAgentMap = $options->user_agents;
@@ -478,29 +445,6 @@ if ($html_only || !$result) {
478 $isDummyFeed = true; 445 $isDummyFeed = true;
479 unset($feed, $result); 446 unset($feed, $result);
480 // create single item dummy feed object 447 // create single item dummy feed object
481 class DummySingleItemFeed {
482 public $item;
483 function __construct($url) { $this->item = new DummySingleItem($url); }
484 public function get_title() { return ''; }
485 public function get_description() { return 'Content extracted from '.$this->item->url; }
486 public function get_link() { return $this->item->url; }
487 public function get_language() { return false; }
488 public function get_image_url() { return false; }
489 public function get_items($start=0, $max=1) { return array(0=>$this->item); }
490 }
491 class DummySingleItem {
492 public $url;
493 function __construct($url) { $this->url = $url; }
494 public function get_permalink() { return $this->url; }
495 public function get_title() { return null; }
496 public function get_date($format='') { return false; }
497 public function get_author($key=0) { return null; }
498 public function get_authors() { return null; }
499 public function get_description() { return ''; }
500 public function get_enclosure($key=0, $prefer=null) { return null; }
501 public function get_enclosures() { return null; }
502 public function get_categories() { return null; }
503 }
504 $feed = new DummySingleItemFeed($url); 448 $feed = new DummySingleItemFeed($url);
505} 449}
506 450
@@ -903,294 +847,3 @@ if (!$debug_mode) {
903 if ($callback) echo ');'; 847 if ($callback) echo ');';
904} 848}
905 849
906///////////////////////////////
907// HELPER FUNCTIONS
908///////////////////////////////
909
910function url_allowed($url) {
911 global $options;
912 if (!empty($options->allowed_urls)) {
913 $allowed = false;
914 foreach ($options->allowed_urls as $allowurl) {
915 if (stristr($url, $allowurl) !== false) {
916 $allowed = true;
917 break;
918 }
919 }
920 if (!$allowed) return false;
921 } else {
922 foreach ($options->blocked_urls as $blockurl) {
923 if (stristr($url, $blockurl) !== false) {
924 return false;
925 }
926 }
927 }
928 return true;
929}
930
931//////////////////////////////////////////////
932// Convert $html to UTF8
933// (uses HTTP headers and HTML to find encoding)
934// adapted from http://stackoverflow.com/questions/910793/php-detect-encoding-and-make-everything-utf-8
935//////////////////////////////////////////////
936function convert_to_utf8($html, $header=null)
937{
938 $encoding = null;
939 if ($html || $header) {
940 if (is_array($header)) $header = implode("\n", $header);
941 if (!$header || !preg_match_all('/^Content-Type:\s+([^;]+)(?:;\s*charset=["\']?([^;"\'\n]*))?/im', $header, $match, PREG_SET_ORDER)) {
942 // error parsing the response
943 debug('Could not find Content-Type header in HTTP response');
944 } else {
945 $match = end($match); // get last matched element (in case of redirects)
946 if (isset($match[2])) $encoding = trim($match[2], "\"' \r\n\0\x0B\t");
947 }
948 // TODO: check to see if encoding is supported (can we convert it?)
949 // If it's not, result will be empty string.
950 // For now we'll check for invalid encoding types returned by some sites, e.g. 'none'
951 // Problem URL: http://facta.co.jp/blog/archives/20111026001026.html
952 if (!$encoding || $encoding == 'none') {
953 // search for encoding in HTML - only look at the first 50000 characters
954 // Why 50000? See, for example, http://www.lemonde.fr/festival-de-cannes/article/2012/05/23/deux-cretes-en-goguette-sur-la-croisette_1705732_766360.html
955 // TODO: improve this so it looks at smaller chunks first
956 $html_head = substr($html, 0, 50000);
957 if (preg_match('/^<\?xml\s+version=(?:"[^"]*"|\'[^\']*\')\s+encoding=("[^"]*"|\'[^\']*\')/s', $html_head, $match)) {
958 $encoding = trim($match[1], '"\'');
959 } elseif (preg_match('/<meta\s+http-equiv=["\']?Content-Type["\']? content=["\'][^;]+;\s*charset=["\']?([^;"\'>]+)/i', $html_head, $match)) {
960 $encoding = trim($match[1]);
961 } elseif (preg_match_all('/<meta\s+([^>]+)>/i', $html_head, $match)) {
962 foreach ($match[1] as $_test) {
963 if (preg_match('/charset=["\']?([^"\']+)/i', $_test, $_m)) {
964 $encoding = trim($_m[1]);
965 break;
966 }
967 }
968 }
969 }
970 if (isset($encoding)) $encoding = trim($encoding);
971 // trim is important here!
972 if (!$encoding || (strtolower($encoding) == 'iso-8859-1')) {
973 // replace MS Word smart qutoes
974 $trans = array();
975 $trans[chr(130)] = '&sbquo;'; // Single Low-9 Quotation Mark
976 $trans[chr(131)] = '&fnof;'; // Latin Small Letter F With Hook
977 $trans[chr(132)] = '&bdquo;'; // Double Low-9 Quotation Mark
978 $trans[chr(133)] = '&hellip;'; // Horizontal Ellipsis
979 $trans[chr(134)] = '&dagger;'; // Dagger
980 $trans[chr(135)] = '&Dagger;'; // Double Dagger
981 $trans[chr(136)] = '&circ;'; // Modifier Letter Circumflex Accent
982 $trans[chr(137)] = '&permil;'; // Per Mille Sign
983 $trans[chr(138)] = '&Scaron;'; // Latin Capital Letter S With Caron
984 $trans[chr(139)] = '&lsaquo;'; // Single Left-Pointing Angle Quotation Mark
985 $trans[chr(140)] = '&OElig;'; // Latin Capital Ligature OE
986 $trans[chr(145)] = '&lsquo;'; // Left Single Quotation Mark
987 $trans[chr(146)] = '&rsquo;'; // Right Single Quotation Mark
988 $trans[chr(147)] = '&ldquo;'; // Left Double Quotation Mark
989 $trans[chr(148)] = '&rdquo;'; // Right Double Quotation Mark
990 $trans[chr(149)] = '&bull;'; // Bullet
991 $trans[chr(150)] = '&ndash;'; // En Dash
992 $trans[chr(151)] = '&mdash;'; // Em Dash
993 $trans[chr(152)] = '&tilde;'; // Small Tilde
994 $trans[chr(153)] = '&trade;'; // Trade Mark Sign
995 $trans[chr(154)] = '&scaron;'; // Latin Small Letter S With Caron
996 $trans[chr(155)] = '&rsaquo;'; // Single Right-Pointing Angle Quotation Mark
997 $trans[chr(156)] = '&oelig;'; // Latin Small Ligature OE
998 $trans[chr(159)] = '&Yuml;'; // Latin Capital Letter Y With Diaeresis
999 $html = strtr($html, $trans);
1000 }
1001 if (!$encoding) {
1002 debug('No character encoding found, so treating as UTF-8');
1003 $encoding = 'utf-8';
1004 } else {
1005 debug('Character encoding: '.$encoding);
1006 if (strtolower($encoding) != 'utf-8') {
1007 debug('Converting to UTF-8');
1008 $html = SimplePie_Misc::change_encoding($html, $encoding, 'utf-8');
1009 /*
1010 if (function_exists('iconv')) {
1011 // iconv appears to handle certain character encodings better than mb_convert_encoding
1012 $html = iconv($encoding, 'utf-8', $html);
1013 } else {
1014 $html = mb_convert_encoding($html, 'utf-8', $encoding);
1015 }
1016 */
1017 }
1018 }
1019 }
1020 return $html;
1021}
1022
1023function makeAbsolute($base, $elem) {
1024 $base = new SimplePie_IRI($base);
1025 // remove '//' in URL path (used to prevent URLs from resolving properly)
1026 // TODO: check if this is still the case
1027 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
1028 foreach(array('a'=>'href', 'img'=>'src') as $tag => $attr) {
1029 $elems = $elem->getElementsByTagName($tag);
1030 for ($i = $elems->length-1; $i >= 0; $i--) {
1031 $e = $elems->item($i);
1032 //$e->parentNode->replaceChild($articleContent->ownerDocument->createTextNode($e->textContent), $e);
1033 makeAbsoluteAttr($base, $e, $attr);
1034 }
1035 if (strtolower($elem->tagName) == $tag) makeAbsoluteAttr($base, $elem, $attr);
1036 }
1037}
1038function makeAbsoluteAttr($base, $e, $attr) {
1039 if ($e->hasAttribute($attr)) {
1040 // Trim leading and trailing white space. I don't really like this but
1041 // unfortunately it does appear on some sites. e.g. <img src=" /path/to/image.jpg" />
1042 $url = trim(str_replace('%20', ' ', $e->getAttribute($attr)));
1043 $url = str_replace(' ', '%20', $url);
1044 if (!preg_match('!https?://!i', $url)) {
1045 if ($absolute = SimplePie_IRI::absolutize($base, $url)) {
1046 $e->setAttribute($attr, $absolute);
1047 }
1048 }
1049 }
1050}
1051function makeAbsoluteStr($base, $url) {
1052 $base = new SimplePie_IRI($base);
1053 // remove '//' in URL path (causes URLs not to resolve properly)
1054 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
1055 if (preg_match('!^https?://!i', $url)) {
1056 // already absolute
1057 return $url;
1058 } else {
1059 if ($absolute = SimplePie_IRI::absolutize($base, $url)) {
1060 return $absolute;
1061 }
1062 return false;
1063 }
1064}
1065// returns single page response, or false if not found
1066function getSinglePage($item, $html, $url) {
1067 global $http, $extractor;
1068 debug('Looking for site config files to see if single page link exists');
1069 $site_config = $extractor->buildSiteConfig($url, $html);
1070 $splink = null;
1071 if (!empty($site_config->single_page_link)) {
1072 $splink = $site_config->single_page_link;
1073 } elseif (!empty($site_config->single_page_link_in_feed)) {
1074 // single page link xpath is targeted at feed
1075 $splink = $site_config->single_page_link_in_feed;
1076 // so let's replace HTML with feed item description
1077 $html = $item->get_description();
1078 }
1079 if (isset($splink)) {
1080 // Build DOM tree from HTML
1081 $readability = new Readability($html, $url);
1082 $xpath = new DOMXPath($readability->dom);
1083 // Loop through single_page_link xpath expressions
1084 $single_page_url = null;
1085 foreach ($splink as $pattern) {
1086 $elems = @$xpath->evaluate($pattern, $readability->dom);
1087 if (is_string($elems)) {
1088 $single_page_url = trim($elems);
1089 break;
1090 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
1091 foreach ($elems as $item) {
1092 if ($item instanceof DOMElement && $item->hasAttribute('href')) {
1093 $single_page_url = $item->getAttribute('href');
1094 break 2;
1095 } elseif ($item instanceof DOMAttr && $item->value) {
1096 $single_page_url = $item->value;
1097 break 2;
1098 }
1099 }
1100 }
1101 }
1102 // If we've got URL, resolve against $url
1103 if (isset($single_page_url) && ($single_page_url = makeAbsoluteStr($url, $single_page_url))) {
1104 // check it's not what we have already!
1105 if ($single_page_url != $url) {
1106 // it's not, so let's try to fetch it...
1107 $_prev_ref = $http->referer;
1108 $http->referer = $single_page_url;
1109 if (($response = $http->get($single_page_url, true)) && $response['status_code'] < 300) {
1110 $http->referer = $_prev_ref;
1111 return $response;
1112 }
1113 $http->referer = $_prev_ref;
1114 }
1115 }
1116 }
1117 return false;
1118}
1119
1120// based on content-type http header, decide what to do
1121// param: HTTP headers string
1122// return: array with keys: 'mime', 'type', 'subtype', 'action', 'name'
1123// e.g. array('mime'=>'image/jpeg', 'type'=>'image', 'subtype'=>'jpeg', 'action'=>'link', 'name'=>'Image')
1124function get_mime_action_info($headers) {
1125 global $options;
1126 // check if action defined for returned Content-Type
1127 $info = array();
1128 if (preg_match('!^Content-Type:\s*(([-\w]+)/([-\w\+]+))!im', $headers, $match)) {
1129 // look for full mime type (e.g. image/jpeg) or just type (e.g. image)
1130 // match[1] = full mime type, e.g. image/jpeg
1131 // match[2] = first part, e.g. image
1132 // match[3] = last part, e.g. jpeg
1133 $info['mime'] = strtolower(trim($match[1]));
1134 $info['type'] = strtolower(trim($match[2]));
1135 $info['subtype'] = strtolower(trim($match[3]));
1136 foreach (array($info['mime'], $info['type']) as $_mime) {
1137 if (isset($options->content_type_exc[$_mime])) {
1138 $info['action'] = $options->content_type_exc[$_mime]['action'];
1139 $info['name'] = $options->content_type_exc[$_mime]['name'];
1140 break;
1141 }
1142 }
1143 }
1144 return $info;
1145}
1146
1147function remove_url_cruft($url) {
1148 // remove google analytics for the time being
1149 // regex adapted from http://navitronic.co.uk/2010/12/removing-google-analytics-cruft-from-urls/
1150 // https://gist.github.com/758177
1151 return preg_replace('/(\?|\&)utm_[a-z]+=[^\&]+/', '', $url);
1152}
1153
1154function make_substitutions($string) {
1155 if ($string == '') return $string;
1156 global $item, $effective_url;
1157 $string = str_replace('{url}', htmlspecialchars($item->get_permalink()), $string);
1158 $string = str_replace('{effective-url}', htmlspecialchars($effective_url), $string);
1159 return $string;
1160}
1161
1162function get_cache() {
1163 global $options, $valid_key;
1164 static $cache = null;
1165 if ($cache === null) {
1166 $frontendOptions = array(
1167 'lifetime' => 10*60, // cache lifetime of 10 minutes
1168 'automatic_serialization' => false,
1169 'write_control' => false,
1170 'automatic_cleaning_factor' => $options->cache_cleanup,
1171 'ignore_user_abort' => false
1172 );
1173 $backendOptions = array(
1174 'cache_dir' => ($valid_key) ? $options->cache_dir.'/rss-with-key/' : $options->cache_dir.'/rss/', // directory where to put the cache files
1175 'file_locking' => false,
1176 'read_control' => true,
1177 'read_control_type' => 'strlen',
1178 'hashed_directory_level' => $options->cache_directory_level,
1179 'hashed_directory_perm' => 0777,
1180 'cache_file_perm' => 0664,
1181 'file_name_prefix' => 'ff'
1182 );
1183 // getting a Zend_Cache_Core object
1184 $cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
1185 }
1186 return $cache;
1187}
1188
1189function debug($msg) {
1190 global $debug_mode;
1191 if ($debug_mode) {
1192 echo '* ',$msg,"\n";
1193 ob_flush();
1194 flush();
1195 }
1196} \ No newline at end of file
diff --git a/inc/3rdparty/makefulltextfeedHelpers.php b/inc/3rdparty/makefulltextfeedHelpers.php
new file mode 100755
index 00000000..1c11b8f6
--- /dev/null
+++ b/inc/3rdparty/makefulltextfeedHelpers.php
@@ -0,0 +1,355 @@
1<?php
2
3// Autoloading of classes allows us to include files only when they're
4// needed. If we've got a cached copy, for example, only Zend_Cache is loaded.
5function autoload($class_name) {
6 static $dir = null;
7 if ($dir === null) $dir = dirname(__FILE__).'/libraries/';
8 static $mapping = array(
9 // Include FeedCreator for RSS/Atom creation
10 'FeedWriter' => 'feedwriter/FeedWriter.php',
11 'FeedItem' => 'feedwriter/FeedItem.php',
12 // Include ContentExtractor and Readability for identifying and extracting content from URLs
13 'ContentExtractor' => 'content-extractor/ContentExtractor.php',
14 'SiteConfig' => 'content-extractor/SiteConfig.php',
15 'Readability' => 'readability/Readability.php',
16 // Include Humble HTTP Agent to allow parallel requests and response caching
17 'HumbleHttpAgent' => 'humble-http-agent/HumbleHttpAgent.php',
18 'SimplePie_HumbleHttpAgent' => 'humble-http-agent/SimplePie_HumbleHttpAgent.php',
19 'CookieJar' => 'humble-http-agent/CookieJar.php',
20 // Include Zend Cache to improve performance (cache results)
21 'Zend_Cache' => 'Zend/Cache.php',
22 // Language detect
23 'Text_LanguageDetect' => 'language-detect/LanguageDetect.php',
24 // HTML5 Lib
25 'HTML5_Parser' => 'html5/Parser.php',
26 // htmLawed - used if XSS filter is enabled (xss_filter)
27 'htmLawed' => 'htmLawed/htmLawed.php'
28 );
29 if (isset($mapping[$class_name])) {
30 debug("** Loading class $class_name ({$mapping[$class_name]})");
31 require $dir.$mapping[$class_name];
32 return true;
33 } else {
34 return false;
35 }
36}
37spl_autoload_register('autoload');
38require dirname(__FILE__).'/libraries/simplepie/autoloader.php';
39
40
41class DummySingleItemFeed {
42 public $item;
43 function __construct($url) { $this->item = new DummySingleItem($url); }
44 public function get_title() { return ''; }
45 public function get_description() { return 'Content extracted from '.$this->item->url; }
46 public function get_link() { return $this->item->url; }
47 public function get_language() { return false; }
48 public function get_image_url() { return false; }
49 public function get_items($start=0, $max=1) { return array(0=>$this->item); }
50}
51class DummySingleItem {
52 public $url;
53 function __construct($url) { $this->url = $url; }
54 public function get_permalink() { return $this->url; }
55 public function get_title() { return null; }
56 public function get_date($format='') { return false; }
57 public function get_author($key=0) { return null; }
58 public function get_authors() { return null; }
59 public function get_description() { return ''; }
60 public function get_enclosure($key=0, $prefer=null) { return null; }
61 public function get_enclosures() { return null; }
62 public function get_categories() { return null; }
63}
64
65///////////////////////////////
66// HELPER FUNCTIONS
67///////////////////////////////
68
69function url_allowed($url) {
70 global $options;
71 if (!empty($options->allowed_urls)) {
72 $allowed = false;
73 foreach ($options->allowed_urls as $allowurl) {
74 if (stristr($url, $allowurl) !== false) {
75 $allowed = true;
76 break;
77 }
78 }
79 if (!$allowed) return false;
80 } else {
81 foreach ($options->blocked_urls as $blockurl) {
82 if (stristr($url, $blockurl) !== false) {
83 return false;
84 }
85 }
86 }
87 return true;
88}
89
90//////////////////////////////////////////////
91// Convert $html to UTF8
92// (uses HTTP headers and HTML to find encoding)
93// adapted from http://stackoverflow.com/questions/910793/php-detect-encoding-and-make-everything-utf-8
94//////////////////////////////////////////////
95function convert_to_utf8($html, $header=null)
96{
97 $encoding = null;
98 if ($html || $header) {
99 if (is_array($header)) $header = implode("\n", $header);
100 if (!$header || !preg_match_all('/^Content-Type:\s+([^;]+)(?:;\s*charset=["\']?([^;"\'\n]*))?/im', $header, $match, PREG_SET_ORDER)) {
101 // error parsing the response
102 debug('Could not find Content-Type header in HTTP response');
103 } else {
104 $match = end($match); // get last matched element (in case of redirects)
105 if (isset($match[2])) $encoding = trim($match[2], "\"' \r\n\0\x0B\t");
106 }
107 // TODO: check to see if encoding is supported (can we convert it?)
108 // If it's not, result will be empty string.
109 // For now we'll check for invalid encoding types returned by some sites, e.g. 'none'
110 // Problem URL: http://facta.co.jp/blog/archives/20111026001026.html
111 if (!$encoding || $encoding == 'none') {
112 // search for encoding in HTML - only look at the first 50000 characters
113 // 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
114 // TODO: improve this so it looks at smaller chunks first
115 $html_head = substr($html, 0, 50000);
116 if (preg_match('/^<\?xml\s+version=(?:"[^"]*"|\'[^\']*\')\s+encoding=("[^"]*"|\'[^\']*\')/s', $html_head, $match)) {
117 $encoding = trim($match[1], '"\'');
118 } elseif (preg_match('/<meta\s+http-equiv=["\']?Content-Type["\']? content=["\'][^;]+;\s*charset=["\']?([^;"\'>]+)/i', $html_head, $match)) {
119 $encoding = trim($match[1]);
120 } elseif (preg_match_all('/<meta\s+([^>]+)>/i', $html_head, $match)) {
121 foreach ($match[1] as $_test) {
122 if (preg_match('/charset=["\']?([^"\']+)/i', $_test, $_m)) {
123 $encoding = trim($_m[1]);
124 break;
125 }
126 }
127 }
128 }
129 if (isset($encoding)) $encoding = trim($encoding);
130 // trim is important here!
131 if (!$encoding || (strtolower($encoding) == 'iso-8859-1')) {
132 // replace MS Word smart qutoes
133 $trans = array();
134 $trans[chr(130)] = '&sbquo;'; // Single Low-9 Quotation Mark
135 $trans[chr(131)] = '&fnof;'; // Latin Small Letter F With Hook
136 $trans[chr(132)] = '&bdquo;'; // Double Low-9 Quotation Mark
137 $trans[chr(133)] = '&hellip;'; // Horizontal Ellipsis
138 $trans[chr(134)] = '&dagger;'; // Dagger
139 $trans[chr(135)] = '&Dagger;'; // Double Dagger
140 $trans[chr(136)] = '&circ;'; // Modifier Letter Circumflex Accent
141 $trans[chr(137)] = '&permil;'; // Per Mille Sign
142 $trans[chr(138)] = '&Scaron;'; // Latin Capital Letter S With Caron
143 $trans[chr(139)] = '&lsaquo;'; // Single Left-Pointing Angle Quotation Mark
144 $trans[chr(140)] = '&OElig;'; // Latin Capital Ligature OE
145 $trans[chr(145)] = '&lsquo;'; // Left Single Quotation Mark
146 $trans[chr(146)] = '&rsquo;'; // Right Single Quotation Mark
147 $trans[chr(147)] = '&ldquo;'; // Left Double Quotation Mark
148 $trans[chr(148)] = '&rdquo;'; // Right Double Quotation Mark
149 $trans[chr(149)] = '&bull;'; // Bullet
150 $trans[chr(150)] = '&ndash;'; // En Dash
151 $trans[chr(151)] = '&mdash;'; // Em Dash
152 $trans[chr(152)] = '&tilde;'; // Small Tilde
153 $trans[chr(153)] = '&trade;'; // Trade Mark Sign
154 $trans[chr(154)] = '&scaron;'; // Latin Small Letter S With Caron
155 $trans[chr(155)] = '&rsaquo;'; // Single Right-Pointing Angle Quotation Mark
156 $trans[chr(156)] = '&oelig;'; // Latin Small Ligature OE
157 $trans[chr(159)] = '&Yuml;'; // Latin Capital Letter Y With Diaeresis
158 $html = strtr($html, $trans);
159 }
160 if (!$encoding) {
161 debug('No character encoding found, so treating as UTF-8');
162 $encoding = 'utf-8';
163 } else {
164 debug('Character encoding: '.$encoding);
165 if (strtolower($encoding) != 'utf-8') {
166 debug('Converting to UTF-8');
167 $html = SimplePie_Misc::change_encoding($html, $encoding, 'utf-8');
168 /*
169 if (function_exists('iconv')) {
170 // iconv appears to handle certain character encodings better than mb_convert_encoding
171 $html = iconv($encoding, 'utf-8', $html);
172 } else {
173 $html = mb_convert_encoding($html, 'utf-8', $encoding);
174 }
175 */
176 }
177 }
178 }
179 return $html;
180}
181
182function makeAbsolute($base, $elem) {
183 $base = new SimplePie_IRI($base);
184 // remove '//' in URL path (used to prevent URLs from resolving properly)
185 // TODO: check if this is still the case
186 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
187 foreach(array('a'=>'href', 'img'=>'src') as $tag => $attr) {
188 $elems = $elem->getElementsByTagName($tag);
189 for ($i = $elems->length-1; $i >= 0; $i--) {
190 $e = $elems->item($i);
191 //$e->parentNode->replaceChild($articleContent->ownerDocument->createTextNode($e->textContent), $e);
192 makeAbsoluteAttr($base, $e, $attr);
193 }
194 if (strtolower($elem->tagName) == $tag) makeAbsoluteAttr($base, $elem, $attr);
195 }
196}
197function makeAbsoluteAttr($base, $e, $attr) {
198 if ($e->hasAttribute($attr)) {
199 // Trim leading and trailing white space. I don't really like this but
200 // unfortunately it does appear on some sites. e.g. <img src=" /path/to/image.jpg" />
201 $url = trim(str_replace('%20', ' ', $e->getAttribute($attr)));
202 $url = str_replace(' ', '%20', $url);
203 if (!preg_match('!https?://!i', $url)) {
204 if ($absolute = SimplePie_IRI::absolutize($base, $url)) {
205 $e->setAttribute($attr, $absolute);
206 }
207 }
208 }
209}
210function makeAbsoluteStr($base, $url) {
211 $base = new SimplePie_IRI($base);
212 // remove '//' in URL path (causes URLs not to resolve properly)
213 if (isset($base->path)) $base->path = preg_replace('!//+!', '/', $base->path);
214 if (preg_match('!^https?://!i', $url)) {
215 // already absolute
216 return $url;
217 } else {
218 if ($absolute = SimplePie_IRI::absolutize($base, $url)) {
219 return $absolute;
220 }
221 return false;
222 }
223}
224// returns single page response, or false if not found
225function getSinglePage($item, $html, $url) {
226 global $http, $extractor;
227 debug('Looking for site config files to see if single page link exists');
228 $site_config = $extractor->buildSiteConfig($url, $html);
229 $splink = null;
230 if (!empty($site_config->single_page_link)) {
231 $splink = $site_config->single_page_link;
232 } elseif (!empty($site_config->single_page_link_in_feed)) {
233 // single page link xpath is targeted at feed
234 $splink = $site_config->single_page_link_in_feed;
235 // so let's replace HTML with feed item description
236 $html = $item->get_description();
237 }
238 if (isset($splink)) {
239 // Build DOM tree from HTML
240 $readability = new Readability($html, $url);
241 $xpath = new DOMXPath($readability->dom);
242 // Loop through single_page_link xpath expressions
243 $single_page_url = null;
244 foreach ($splink as $pattern) {
245 $elems = @$xpath->evaluate($pattern, $readability->dom);
246 if (is_string($elems)) {
247 $single_page_url = trim($elems);
248 break;
249 } elseif ($elems instanceof DOMNodeList && $elems->length > 0) {
250 foreach ($elems as $item) {
251 if ($item instanceof DOMElement && $item->hasAttribute('href')) {
252 $single_page_url = $item->getAttribute('href');
253 break 2;
254 } elseif ($item instanceof DOMAttr && $item->value) {
255 $single_page_url = $item->value;
256 break 2;
257 }
258 }
259 }
260 }
261 // If we've got URL, resolve against $url
262 if (isset($single_page_url) && ($single_page_url = makeAbsoluteStr($url, $single_page_url))) {
263 // check it's not what we have already!
264 if ($single_page_url != $url) {
265 // it's not, so let's try to fetch it...
266 $_prev_ref = $http->referer;
267 $http->referer = $single_page_url;
268 if (($response = $http->get($single_page_url, true)) && $response['status_code'] < 300) {
269 $http->referer = $_prev_ref;
270 return $response;
271 }
272 $http->referer = $_prev_ref;
273 }
274 }
275 }
276 return false;
277}
278
279// based on content-type http header, decide what to do
280// param: HTTP headers string
281// return: array with keys: 'mime', 'type', 'subtype', 'action', 'name'
282// e.g. array('mime'=>'image/jpeg', 'type'=>'image', 'subtype'=>'jpeg', 'action'=>'link', 'name'=>'Image')
283function get_mime_action_info($headers) {
284 global $options;
285 // check if action defined for returned Content-Type
286 $info = array();
287 if (preg_match('!^Content-Type:\s*(([-\w]+)/([-\w\+]+))!im', $headers, $match)) {
288 // look for full mime type (e.g. image/jpeg) or just type (e.g. image)
289 // match[1] = full mime type, e.g. image/jpeg
290 // match[2] = first part, e.g. image
291 // match[3] = last part, e.g. jpeg
292 $info['mime'] = strtolower(trim($match[1]));
293 $info['type'] = strtolower(trim($match[2]));
294 $info['subtype'] = strtolower(trim($match[3]));
295 foreach (array($info['mime'], $info['type']) as $_mime) {
296 if (isset($options->content_type_exc[$_mime])) {
297 $info['action'] = $options->content_type_exc[$_mime]['action'];
298 $info['name'] = $options->content_type_exc[$_mime]['name'];
299 break;
300 }
301 }
302 }
303 return $info;
304}
305
306function remove_url_cruft($url) {
307 // remove google analytics for the time being
308 // regex adapted from http://navitronic.co.uk/2010/12/removing-google-analytics-cruft-from-urls/
309 // https://gist.github.com/758177
310 return preg_replace('/(\?|\&)utm_[a-z]+=[^\&]+/', '', $url);
311}
312
313function make_substitutions($string) {
314 if ($string == '') return $string;
315 global $item, $effective_url;
316 $string = str_replace('{url}', htmlspecialchars($item->get_permalink()), $string);
317 $string = str_replace('{effective-url}', htmlspecialchars($effective_url), $string);
318 return $string;
319}
320
321function get_cache() {
322 global $options, $valid_key;
323 static $cache = null;
324 if ($cache === null) {
325 $frontendOptions = array(
326 'lifetime' => 10*60, // cache lifetime of 10 minutes
327 'automatic_serialization' => false,
328 'write_control' => false,
329 'automatic_cleaning_factor' => $options->cache_cleanup,
330 'ignore_user_abort' => false
331 );
332 $backendOptions = array(
333 'cache_dir' => ($valid_key) ? $options->cache_dir.'/rss-with-key/' : $options->cache_dir.'/rss/', // directory where to put the cache files
334 'file_locking' => false,
335 'read_control' => true,
336 'read_control_type' => 'strlen',
337 'hashed_directory_level' => $options->cache_directory_level,
338 'hashed_directory_perm' => 0777,
339 'cache_file_perm' => 0664,
340 'file_name_prefix' => 'ff'
341 );
342 // getting a Zend_Cache_Core object
343 $cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
344 }
345 return $cache;
346}
347
348function debug($msg) {
349 global $debug_mode;
350 if ($debug_mode) {
351 echo '* ',$msg,"\n";
352 ob_flush();
353 flush();
354 }
355}
diff --git a/inc/poche/Database.class.php b/inc/poche/Database.class.php
index c998fe14..5b51b507 100755
--- a/inc/poche/Database.class.php
+++ b/inc/poche/Database.class.php
@@ -230,8 +230,30 @@ class Database {
230 } 230 }
231 } 231 }
232 232
233 public function updateContentAndTitle($id, $title, $body, $user_id) {
234 $sql_action = 'UPDATE entries SET content = ?, title = ? WHERE id=? AND user_id=?';
235 $params_action = array($body, $title, $id, $user_id);
236 $query = $this->executeQuery($sql_action, $params_action);
237
238 return $query;
239 }
240
241 public function retrieveUnfetchedEntries($user_id, $limit) {
242
243 $sql_limit = "LIMIT 0,".$limit;
244 if (STORAGE == 'postgres') {
245 $sql_limit = "LIMIT ".$limit." OFFSET 0";
246 }
247
248 $sql = "SELECT * FROM entries WHERE (content = '' OR content IS NULL) AND user_id=? ORDER BY id " . $sql_limit;
249 $query = $this->executeQuery($sql, array($user_id));
250 $entries = $query->fetchAll();
251
252 return $entries;
253 }
254
233 public function retrieveAll($user_id) { 255 public function retrieveAll($user_id) {
234 $sql = "SELECT * FROM entries WHERE user_id=? ORDER BY id"; 256 $sql = "SELECT * FROM entries WHERE content <> '' AND user_id=? ORDER BY id";
235 $query = $this->executeQuery($sql, array($user_id)); 257 $query = $this->executeQuery($sql, array($user_id));
236 $entries = $query->fetchAll(); 258 $entries = $query->fetchAll();
237 259
@@ -250,7 +272,7 @@ class Database {
250 272
251 public function retrieveOneByURL($url, $user_id) { 273 public function retrieveOneByURL($url, $user_id) {
252 $entry = NULL; 274 $entry = NULL;
253 $sql = "SELECT * FROM entries WHERE url=? AND user_id=?"; 275 $sql = "SELECT * FROM entries WHERE content <> '' AND url=? AND user_id=?";
254 $params = array($url, $user_id); 276 $params = array($url, $user_id);
255 $query = $this->executeQuery($sql, $params); 277 $query = $this->executeQuery($sql, $params);
256 $entry = $query->fetchAll(); 278 $entry = $query->fetchAll();
@@ -267,21 +289,22 @@ class Database {
267 public function getEntriesByView($view, $user_id, $limit = '', $tag_id = 0) { 289 public function getEntriesByView($view, $user_id, $limit = '', $tag_id = 0) {
268 switch ($view) { 290 switch ($view) {
269 case 'archive': 291 case 'archive':
270 $sql = "SELECT * FROM entries WHERE user_id=? AND is_read=? "; 292 $sql = "SELECT * FROM entries WHERE content <> '' AND user_id=? AND is_read=? ";
271 $params = array($user_id, 1); 293 $params = array($user_id, 1);
272 break; 294 break;
273 case 'fav' : 295 case 'fav' :
274 $sql = "SELECT * FROM entries WHERE user_id=? AND is_fav=? "; 296 $sql = "SELECT * FROM entries WHERE content <> '' AND user_id=? AND is_fav=? ";
275 $params = array($user_id, 1); 297 $params = array($user_id, 1);
276 break; 298 break;
277 case 'tag' : 299 case 'tag' :
278 $sql = "SELECT entries.* FROM entries 300 $sql = "SELECT entries.* FROM entries
279 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id 301 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id
280 WHERE entries.user_id=? AND tags_entries.tag_id = ? "; 302 WHERE entries.content <> '' AND
303 entries.user_id=? AND tags_entries.tag_id = ? ";
281 $params = array($user_id, $tag_id); 304 $params = array($user_id, $tag_id);
282 break; 305 break;
283 default: 306 default:
284 $sql = "SELECT * FROM entries WHERE user_id=? AND is_read=? "; 307 $sql = "SELECT * FROM entries WHERE content <> '' AND user_id=? AND is_read=? ";
285 $params = array($user_id, 0); 308 $params = array($user_id, 0);
286 break; 309 break;
287 } 310 }
@@ -294,24 +317,25 @@ class Database {
294 return $entries; 317 return $entries;
295 } 318 }
296 319
297 public function getEntriesByViewCount($view, $user_id, $tag_id = 0) { 320 public function getEntriesByViewCount($view, $user_id, $tag_id = 0) {
298 switch ($view) { 321 switch ($view) {
299 case 'archive': 322 case 'archive':
300 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? "; 323 $sql = "SELECT count(*) FROM entries WHERE content <> '' AND user_id=? AND is_read=? ";
301 $params = array($user_id, 1); 324 $params = array($user_id, 1);
302 break; 325 break;
303 case 'fav' : 326 case 'fav' :
304 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_fav=? "; 327 $sql = "SELECT count(*) FROM entries WHERE content <> '' AND user_id=? AND is_fav=? ";
305 $params = array($user_id, 1); 328 $params = array($user_id, 1);
306 break; 329 break;
307 case 'tag' : 330 case 'tag' :
308 $sql = "SELECT count(*) FROM entries 331 $sql = "SELECT count(*) FROM entries
309 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id 332 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id
310 WHERE entries.user_id=? AND tags_entries.tag_id = ? "; 333 WHERE entries.content <> '' AND
311 $params = array($user_id, $tag_id); 334 entries.user_id=? AND tags_entries.tag_id = ? ";
312 break; 335 $params = array($user_id, $tag_id);
336 break;
313 default: 337 default:
314 $sql = "SELECT count(*) FROM entries WHERE user_id=? AND is_read=? "; 338 $sql = "SELECT count(*) FROM entries WHERE content <> '' AND user_id=? AND is_read=? ";
315 $params = array($user_id, 0); 339 $params = array($user_id, 0);
316 break; 340 break;
317 } 341 }
@@ -319,7 +343,7 @@ class Database {
319 $query = $this->executeQuery($sql, $params); 343 $query = $this->executeQuery($sql, $params);
320 list($count) = $query->fetch(); 344 list($count) = $query->fetch();
321 345
322 return $count; 346 return $count;
323 } 347 }
324 348
325 public function updateContent($id, $content, $user_id) { 349 public function updateContent($id, $content, $user_id) {
@@ -365,12 +389,15 @@ class Database {
365 return $this->getHandle()->lastInsertId($column); 389 return $this->getHandle()->lastInsertId($column);
366 } 390 }
367 391
368 public function retrieveAllTags($user_id) { 392 public function retrieveAllTags($user_id, $term = null) {
369 $sql = "SELECT DISTINCT tags.* FROM tags 393 $sql = "SELECT DISTINCT tags.*, count(entries.id) AS entriescount FROM tags
370 LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id 394 LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id
371 LEFT JOIN entries ON tags_entries.entry_id=entries.id 395 LEFT JOIN entries ON tags_entries.entry_id=entries.id
372 WHERE entries.user_id=?"; 396 WHERE entries.content <> '' AND entries.user_id=?
373 $query = $this->executeQuery($sql, array($user_id)); 397 ". (($term) ? "AND lower(tags.value) LIKE ?" : '') ."
398 GROUP BY tags.id, tags.value
399 ORDER BY tags.value";
400 $query = $this->executeQuery($sql, (($term)? array($user_id, strtolower('%'.$term.'%')) : array($user_id) ));
374 $tags = $query->fetchAll(); 401 $tags = $query->fetchAll();
375 402
376 return $tags; 403 return $tags;
@@ -381,7 +408,7 @@ class Database {
381 $sql = "SELECT DISTINCT tags.* FROM tags 408 $sql = "SELECT DISTINCT tags.* FROM tags
382 LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id 409 LEFT JOIN tags_entries ON tags_entries.tag_id=tags.id
383 LEFT JOIN entries ON tags_entries.entry_id=entries.id 410 LEFT JOIN entries ON tags_entries.entry_id=entries.id
384 WHERE tags.id=? AND entries.user_id=?"; 411 WHERE entries.content <> '' AND tags.id=? AND entries.user_id=?";
385 $params = array(intval($id), $user_id); 412 $params = array(intval($id), $user_id);
386 $query = $this->executeQuery($sql, $params); 413 $query = $this->executeQuery($sql, $params);
387 $tag = $query->fetchAll(); 414 $tag = $query->fetchAll();
@@ -393,7 +420,8 @@ class Database {
393 $sql = 420 $sql =
394 "SELECT entries.* FROM entries 421 "SELECT entries.* FROM entries
395 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id 422 LEFT JOIN tags_entries ON tags_entries.entry_id=entries.id
396 WHERE tags_entries.tag_id = ? AND entries.user_id=?"; 423 WHERE entries.content <> '' AND
424 tags_entries.tag_id = ? AND entries.user_id=?";
397 $query = $this->executeQuery($sql, array($tag_id, $user_id)); 425 $query = $this->executeQuery($sql, array($tag_id, $user_id));
398 $entries = $query->fetchAll(); 426 $entries = $query->fetchAll();
399 427
diff --git a/inc/poche/Poche.class.php b/inc/poche/Poche.class.php
index 34f2ff5a..7bf33308 100755
--- a/inc/poche/Poche.class.php
+++ b/inc/poche/Poche.class.php
@@ -34,7 +34,8 @@ class Poche
34 'pl_PL.utf8' => 'Polski', 34 'pl_PL.utf8' => 'Polski',
35 'ru_RU.utf8' => 'Pусский', 35 'ru_RU.utf8' => 'Pусский',
36 'sl_SI.utf8' => 'Slovenščina', 36 'sl_SI.utf8' => 'Slovenščina',
37 'uk_UA.utf8' => 'Український', 37 'uk_UA.utf8' => 'Українська',
38 'pt_BR.utf8' => 'Brasileiro',
38 ); 39 );
39 public function __construct() 40 public function __construct()
40 { 41 {
@@ -336,7 +337,7 @@ class Poche
336 while (($language = readdir($handle)) !== false) { 337 while (($language = readdir($handle)) !== false) {
337 # Languages are stored in a directory, so all directory names are languages 338 # Languages are stored in a directory, so all directory names are languages
338 # @todo move language installation data to database 339 # @todo move language installation data to database
339 if (! is_dir(LOCALE . '/' . $language) || in_array($language, array('..', '.'))) { 340 if (! is_dir(LOCALE . '/' . $language) || in_array($language, array('..', '.', 'tools'))) {
340 continue; 341 continue;
341 } 342 }
342 343
@@ -346,7 +347,7 @@ class Poche
346 $current = true; 347 $current = true;
347 } 348 }
348 349
349 $languages[] = array('name' => $this->language_names[$language], 'value' => $language, 'current' => $current); 350 $languages[] = array('name' => (isset($this->language_names[$language]) ? $this->language_names[$language] : $language), 'value' => $language, 'current' => $current);
350 } 351 }
351 352
352 return $languages; 353 return $languages;
@@ -361,60 +362,6 @@ class Poche
361 ); 362 );
362 } 363 }
363 364
364 protected function getPageContent(Url $url)
365 {
366 // Saving and clearing context
367 $REAL = array();
368 foreach( $GLOBALS as $key => $value ) {
369 if( $key != "GLOBALS" && $key != "_SESSION" ) {
370 $GLOBALS[$key] = array();
371 $REAL[$key] = $value;
372 }
373 }
374 // Saving and clearing session
375 $REAL_SESSION = array();
376 foreach( $_SESSION as $key => $value ) {
377 $REAL_SESSION[$key] = $value;
378 unset($_SESSION[$key]);
379 }
380
381 // Running code in different context
382 $scope = function() {
383 extract( func_get_arg(1) );
384 $_GET = $_REQUEST = array(
385 "url" => $url->getUrl(),
386 "max" => 5,
387 "links" => "preserve",
388 "exc" => "",
389 "format" => "json",
390 "submit" => "Create Feed"
391 );
392 ob_start();
393 require func_get_arg(0);
394 $json = ob_get_flush();
395 return $json;
396 };
397 $json = $scope( "inc/3rdparty/makefulltextfeed.php", array("url" => $url) );
398
399 // Clearing and restoring context
400 foreach( $GLOBALS as $key => $value ) {
401 if( $key != "GLOBALS" && $key != "_SESSION" ) {
402 unset($GLOBALS[$key]);
403 }
404 }
405 foreach( $REAL as $key => $value ) {
406 $GLOBALS[$key] = $value;
407 }
408 // Clearing and restoring session
409 foreach( $_SESSION as $key => $value ) {
410 unset($_SESSION[$key]);
411 }
412 foreach( $REAL_SESSION as $key => $value ) {
413 $_SESSION[$key] = $value;
414 }
415 return json_decode($json, true);
416 }
417
418 /** 365 /**
419 * Call action (mark as fav, archive, delete, etc.) 366 * Call action (mark as fav, archive, delete, etc.)
420 */ 367 */
@@ -423,17 +370,25 @@ class Poche
423 switch ($action) 370 switch ($action)
424 { 371 {
425 case 'add': 372 case 'add':
426 $content = $this->getPageContent($url); 373 if (!$import) {
427 $title = ($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'); 374 $content = Tools::getPageContent($url);
428 $body = $content['rss']['channel']['item']['description']; 375 $title = ($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled');
429 376 $body = $content['rss']['channel']['item']['description'];
430 // clean content from prevent xss attack 377
431 $config = HTMLPurifier_Config::createDefault(); 378 // clean content from prevent xss attack
432 $purifier = new HTMLPurifier($config); 379 $config = HTMLPurifier_Config::createDefault();
433 $title = $purifier->purify($title); 380 $config->set('Cache.SerializerPath', CACHE);
434 $body = $purifier->purify($body); 381 $purifier = new HTMLPurifier($config);
382 $title = $purifier->purify($title);
383 $body = $purifier->purify($body);
384 }
385 else {
386 $title = '';
387 $body = '';
388 }
435 389
436 //search for possible duplicate if not in import mode 390 //search for possible duplicate if not in import mode
391 $duplicate = NULL;
437 if (!$import) { 392 if (!$import) {
438 $duplicate = $this->store->retrieveOneByURL($url->getUrl(), $this->user->getId()); 393 $duplicate = $this->store->retrieveOneByURL($url->getUrl(), $this->user->getId());
439 } 394 }
@@ -534,25 +489,33 @@ class Poche
534 Tools::logm('error : article not found'); 489 Tools::logm('error : article not found');
535 Tools::redirect(); 490 Tools::redirect();
536 } 491 }
492 //get all already set tags to preven duplicates
493 $already_set_tags = array();
494 $entry_tags = $this->store->retrieveTagsByEntry($entry_id);
495 foreach ($entry_tags as $tag) {
496 $already_set_tags[] = $tag['value'];
497 }
537 foreach($tags as $key => $tag_value) { 498 foreach($tags as $key => $tag_value) {
538 $value = trim($tag_value); 499 $value = trim($tag_value);
539 $tag = $this->store->retrieveTagByValue($value); 500 if ($value && !in_array($value, $already_set_tags)) {
540 501 $tag = $this->store->retrieveTagByValue($value);
541 if (is_null($tag)) { 502
542 # we create the tag 503 if (is_null($tag)) {
543 $tag = $this->store->createTag($value); 504 # we create the tag
544 $sequence = ''; 505 $tag = $this->store->createTag($value);
545 if (STORAGE == 'postgres') { 506 $sequence = '';
546 $sequence = 'tags_id_seq'; 507 if (STORAGE == 'postgres') {
547 } 508 $sequence = 'tags_id_seq';
548 $tag_id = $this->store->getLastId($sequence); 509 }
510 $tag_id = $this->store->getLastId($sequence);
511 }
512 else {
513 $tag_id = $tag['id'];
514 }
515
516 # we assign the tag to the article
517 $this->store->setTagToEntry($tag_id, $entry_id);
549 } 518 }
550 else {
551 $tag_id = $tag['id'];
552 }
553
554 # we assign the tag to the article
555 $this->store->setTagToEntry($tag_id, $entry_id);
556 } 519 }
557 if(!$import) { 520 if(!$import) {
558 Tools::redirect(); 521 Tools::redirect();
@@ -581,8 +544,12 @@ class Poche
581 switch ($view) 544 switch ($view)
582 { 545 {
583 case 'config': 546 case 'config':
584 $dev = trim($this->getPocheVersion('dev')); 547 $dev_infos = $this->getPocheVersion('dev');
585 $prod = trim($this->getPocheVersion('prod')); 548 $dev = trim($dev_infos[0]);
549 $check_time_dev = date('d-M-Y H:i', $dev_infos[1]);
550 $prod_infos = $this->getPocheVersion('prod');
551 $prod = trim($prod_infos[0]);
552 $check_time_prod = date('d-M-Y H:i', $prod_infos[1]);
586 $compare_dev = version_compare(POCHE, $dev); 553 $compare_dev = version_compare(POCHE, $dev);
587 $compare_prod = version_compare(POCHE, $prod); 554 $compare_prod = version_compare(POCHE, $prod);
588 $themes = $this->getInstalledThemes(); 555 $themes = $this->getInstalledThemes();
@@ -594,6 +561,8 @@ class Poche
594 'languages' => $languages, 561 'languages' => $languages,
595 'dev' => $dev, 562 'dev' => $dev,
596 'prod' => $prod, 563 'prod' => $prod,
564 'check_time_dev' => $check_time_dev,
565 'check_time_prod' => $check_time_prod,
597 'compare_dev' => $compare_dev, 566 'compare_dev' => $compare_dev,
598 'compare_prod' => $compare_prod, 567 'compare_prod' => $compare_prod,
599 'token' => $token, 568 'token' => $token,
@@ -619,7 +588,17 @@ class Poche
619 break; 588 break;
620 case 'tags': 589 case 'tags':
621 $token = $this->user->getConfigValue('token'); 590 $token = $this->user->getConfigValue('token');
622 $tags = $this->store->retrieveAllTags($this->user->getId()); 591 //if term is set - search tags for this term
592 $term = Tools::checkVar('term');
593 $tags = $this->store->retrieveAllTags($this->user->getId(), $term);
594 if (Tools::isAjaxRequest()) {
595 $result = array();
596 foreach ($tags as $tag) {
597 $result[] = $tag['value'];
598 }
599 echo json_encode($result);
600 exit;
601 }
623 $tpl_vars = array( 602 $tpl_vars = array(
624 'token' => $token, 603 'token' => $token,
625 'user_id' => $this->user->getId(), 604 'user_id' => $this->user->getId(),
@@ -660,6 +639,7 @@ class Poche
660 'entries' => '', 639 'entries' => '',
661 'page_links' => '', 640 'page_links' => '',
662 'nb_results' => '', 641 'nb_results' => '',
642 'listmode' => (isset($_COOKIE['listmode']) ? true : false),
663 ); 643 );
664 644
665 //if id is given - we retrive entries by tag: id is tag id 645 //if id is given - we retrive entries by tag: id is tag id
@@ -750,6 +730,8 @@ class Poche
750 $currentConfig['theme'] = $_POST['theme']; 730 $currentConfig['theme'] = $_POST['theme'];
751 731
752 $_SESSION['poche_user']->setConfig($currentConfig); 732 $_SESSION['poche_user']->setConfig($currentConfig);
733
734 $this->emptyCache();
753 735
754 Tools::redirect('?view=config'); 736 Tools::redirect('?view=config');
755 } 737 }
@@ -788,6 +770,8 @@ class Poche
788 $currentConfig['language'] = $_POST['language']; 770 $currentConfig['language'] = $_POST['language'];
789 771
790 $_SESSION['poche_user']->setConfig($currentConfig); 772 $_SESSION['poche_user']->setConfig($currentConfig);
773
774 $this->emptyCache();
791 775
792 Tools::redirect('?view=config'); 776 Tools::redirect('?view=config');
793 } 777 }
@@ -891,7 +875,9 @@ class Poche
891 # the second <ol> is for read links 875 # the second <ol> is for read links
892 $read = 1; 876 $read = 1;
893 } 877 }
894 $this->messages->add('s', _('import from instapaper completed')); 878
879 $unlink = unlink($targetFile);
880 $this->messages->add('s', _('import from instapaper completed. You have to execute the cron to fetch content.'));
895 Tools::logm('import from instapaper completed'); 881 Tools::logm('import from instapaper completed');
896 Tools::redirect(); 882 Tools::redirect();
897 } 883 }
@@ -935,7 +921,9 @@ class Poche
935 # the second <ul> is for read links 921 # the second <ul> is for read links
936 $read = 1; 922 $read = 1;
937 } 923 }
938 $this->messages->add('s', _('import from pocket completed')); 924
925 $unlink = unlink($targetFile);
926 $this->messages->add('s', _('import from pocket completed. You have to execute the cron to fetch content.'));
939 Tools::logm('import from pocket completed'); 927 Tools::logm('import from pocket completed');
940 Tools::redirect(); 928 Tools::redirect();
941 } 929 }
@@ -991,7 +979,9 @@ class Poche
991 } 979 }
992 } 980 }
993 } 981 }
994 $this->messages->add('s', _('import from Readability completed. ' . $count . ' new links.')); 982
983 unlink($targetFile);
984 $this->messages->add('s', _('import from Readability completed. You have to execute the cron to fetch content.'));
995 Tools::logm('import from Readability completed'); 985 Tools::logm('import from Readability completed');
996 Tools::redirect(); 986 Tools::redirect();
997 } 987 }
@@ -1037,7 +1027,9 @@ class Poche
1037 } 1027 }
1038 1028
1039 } 1029 }
1040 $this->messages->add('s', _('import from Poche completed. ' . $count . ' new links.')); 1030
1031 unlink($targetFile);
1032 $this->messages->add('s', _('import from Poche completed. You have to execute the cron to fetch content.'));
1041 Tools::logm('import from Poche completed'); 1033 Tools::logm('import from Poche completed');
1042 Tools::redirect(); 1034 Tools::redirect();
1043 } 1035 }
@@ -1062,13 +1054,7 @@ class Poche
1062 Tools::redirect(); 1054 Tools::redirect();
1063 } 1055 }
1064 1056
1065 $targetDefinition = 'IMPORT_' . strtoupper($from) . '_FILE'; 1057 $targetFile = CACHE . '/' . constant(strtoupper($from) . '_FILE');
1066 $targetFile = constant($targetDefinition);
1067
1068 if (! defined($targetDefinition)) {
1069 $this->messages->add('e', _('Incomplete inc/poche/define.inc.php file, please define "' . $targetDefinition . '".'));
1070 Tools::redirect();
1071 }
1072 1058
1073 if (! file_exists($targetFile)) { 1059 if (! file_exists($targetFile)) {
1074 $this->messages->add('e', _('Could not find required "' . $targetFile . '" import file.')); 1060 $this->messages->add('e', _('Could not find required "' . $targetFile . '" import file.'));
@@ -1078,6 +1064,22 @@ class Poche
1078 $this->$providers[$from]($targetFile); 1064 $this->$providers[$from]($targetFile);
1079 } 1065 }
1080 1066
1067 public function uploadFile() {
1068 if(isset($_FILES['file']))
1069 {
1070 $dir = CACHE . '/';
1071 $file = basename($_FILES['file']['name']);
1072 if(move_uploaded_file($_FILES['file']['tmp_name'], $dir . $file)) {
1073 $this->messages->add('s', _('File uploaded. You can now execute import.'));
1074 }
1075 else {
1076 $this->messages->add('e', _('Error while importing file. Do you have access to upload it?'));
1077 }
1078 }
1079
1080 Tools::redirect('?view=config');
1081 }
1082
1081 /** 1083 /**
1082 * export poche entries in json 1084 * export poche entries in json
1083 * @return json all poche entries 1085 * @return json all poche entries
@@ -1099,15 +1101,17 @@ class Poche
1099 private function getPocheVersion($which = 'prod') 1101 private function getPocheVersion($which = 'prod')
1100 { 1102 {
1101 $cache_file = CACHE . '/' . $which; 1103 $cache_file = CACHE . '/' . $which;
1104 $check_time = time();
1102 1105
1103 # checks if the cached version file exists 1106 # checks if the cached version file exists
1104 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) { 1107 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
1105 $version = file_get_contents($cache_file); 1108 $version = file_get_contents($cache_file);
1109 $check_time = filemtime($cache_file);
1106 } else { 1110 } else {
1107 $version = file_get_contents('http://static.wallabag.org/versions/' . $which); 1111 $version = file_get_contents('http://static.wallabag.org/versions/' . $which);
1108 file_put_contents($cache_file, $version, LOCK_EX); 1112 file_put_contents($cache_file, $version, LOCK_EX);
1109 } 1113 }
1110 return $version; 1114 return array($version, $check_time);
1111 } 1115 }
1112 1116
1113 public function generateToken() 1117 public function generateToken()
@@ -1124,6 +1128,7 @@ class Poche
1124 $currentConfig = $_SESSION['poche_user']->config; 1128 $currentConfig = $_SESSION['poche_user']->config;
1125 $currentConfig['token'] = $token; 1129 $currentConfig['token'] = $token;
1126 $_SESSION['poche_user']->setConfig($currentConfig); 1130 $_SESSION['poche_user']->setConfig($currentConfig);
1131 Tools::redirect();
1127 } 1132 }
1128 1133
1129 public function generateFeeds($token, $user_id, $tag_id, $type = 'home') 1134 public function generateFeeds($token, $user_id, $tag_id, $type = 'home')
@@ -1131,6 +1136,10 @@ class Poche
1131 $allowed_types = array('home', 'fav', 'archive', 'tag'); 1136 $allowed_types = array('home', 'fav', 'archive', 'tag');
1132 $config = $this->store->getConfigUser($user_id); 1137 $config = $this->store->getConfigUser($user_id);
1133 1138
1139 if ($config == null) {
1140 die(_('User with this id (' . $user_id . ') does not exist.'));
1141 }
1142
1134 if (!in_array($type, $allowed_types) || 1143 if (!in_array($type, $allowed_types) ||
1135 $token != $config['token']) { 1144 $token != $config['token']) {
1136 die(_('Uh, there is a problem while generating feeds.')); 1145 die(_('Uh, there is a problem while generating feeds.'));
@@ -1140,8 +1149,9 @@ class Poche
1140 $feed = new FeedWriter(RSS2); 1149 $feed = new FeedWriter(RSS2);
1141 $feed->setTitle('wallabag — ' . $type . ' feed'); 1150 $feed->setTitle('wallabag — ' . $type . ' feed');
1142 $feed->setLink(Tools::getPocheUrl()); 1151 $feed->setLink(Tools::getPocheUrl());
1143 $feed->setChannelElement('updated', date(DATE_RSS , time())); 1152 $feed->setChannelElement('pubDate', date(DATE_RSS , time()));
1144 $feed->setChannelElement('author', 'wallabag'); 1153 $feed->setChannelElement('generator', 'wallabag');
1154 $feed->setDescription('wallabag ' . $type . ' elements');
1145 1155
1146 if ($type == 'tag') { 1156 if ($type == 'tag') {
1147 $entries = $this->store->retrieveEntriesByTag($tag_id, $user_id); 1157 $entries = $this->store->retrieveEntriesByTag($tag_id, $user_id);
@@ -1154,7 +1164,7 @@ class Poche
1154 foreach ($entries as $entry) { 1164 foreach ($entries as $entry) {
1155 $newItem = $feed->createNewItem(); 1165 $newItem = $feed->createNewItem();
1156 $newItem->setTitle($entry['title']); 1166 $newItem->setTitle($entry['title']);
1157 $newItem->setLink(Tools::getPocheUrl() . '?view=view&amp;id=' . $entry['id']); 1167 $newItem->setLink($entry['url']);
1158 $newItem->setDate(time()); 1168 $newItem->setDate(time());
1159 $newItem->setDescription($entry['content']); 1169 $newItem->setDescription($entry['content']);
1160 $feed->addItem($newItem); 1170 $feed->addItem($newItem);
diff --git a/inc/poche/Tools.class.php b/inc/poche/Tools.class.php
index 4ed28ed1..ad451fc6 100644..100755
--- a/inc/poche/Tools.class.php
+++ b/inc/poche/Tools.class.php
@@ -193,7 +193,7 @@ class Tools
193 193
194 public static function logm($message) 194 public static function logm($message)
195 { 195 {
196 if (DEBUG_POCHE) { 196 if (DEBUG_POCHE && php_sapi_name() != 'cli') {
197 $t = strval(date('Y/m/d_H:i:s')) . ' - ' . $_SERVER["REMOTE_ADDR"] . ' - ' . strval($message) . "\n"; 197 $t = strval(date('Y/m/d_H:i:s')) . ' - ' . $_SERVER["REMOTE_ADDR"] . ' - ' . strval($message) . "\n";
198 file_put_contents(CACHE . '/log.txt', $t, FILE_APPEND); 198 file_put_contents(CACHE . '/log.txt', $t, FILE_APPEND);
199 error_log('DEBUG POCHE : ' . $message); 199 error_log('DEBUG POCHE : ' . $message);
@@ -241,7 +241,6 @@ class Tools
241 } 241 }
242 } 242 }
243 243
244
245 public static function download_db() { 244 public static function download_db() {
246 header('Content-Disposition: attachment; filename="poche.sqlite.gz"'); 245 header('Content-Disposition: attachment; filename="poche.sqlite.gz"');
247 self::status(200); 246 self::status(200);
@@ -252,4 +251,74 @@ class Tools
252 251
253 exit; 252 exit;
254 } 253 }
254
255 public static function getPageContent(Url $url)
256 {
257 // Saving and clearing context
258 $REAL = array();
259 foreach( $GLOBALS as $key => $value ) {
260 if( $key != 'GLOBALS' && $key != '_SESSION' && $key != 'HTTP_SESSION_VARS' ) {
261 $GLOBALS[$key] = array();
262 $REAL[$key] = $value;
263 }
264 }
265 // Saving and clearing session
266 if ( isset($_SESSION) ) {
267 $REAL_SESSION = array();
268 foreach( $_SESSION as $key => $value ) {
269 $REAL_SESSION[$key] = $value;
270 unset($_SESSION[$key]);
271 }
272 }
273
274 // Running code in different context
275 $scope = function() {
276 extract( func_get_arg(1) );
277 $_GET = $_REQUEST = array(
278 "url" => $url->getUrl(),
279 "max" => 5,
280 "links" => "preserve",
281 "exc" => "",
282 "format" => "json",
283 "submit" => "Create Feed"
284 );
285 ob_start();
286 require func_get_arg(0);
287 $json = ob_get_contents();
288 ob_end_clean();
289 return $json;
290 };
291 $json = $scope( "inc/3rdparty/makefulltextfeed.php", array("url" => $url) );
292
293 // Clearing and restoring context
294 foreach( $GLOBALS as $key => $value ) {
295 if( $key != "GLOBALS" && $key != "_SESSION" ) {
296 unset($GLOBALS[$key]);
297 }
298 }
299 foreach( $REAL as $key => $value ) {
300 $GLOBALS[$key] = $value;
301 }
302 // Clearing and restoring session
303 if ( isset($REAL_SESSION) ) {
304 foreach( $_SESSION as $key => $value ) {
305 unset($_SESSION[$key]);
306 }
307 foreach( $REAL_SESSION as $key => $value ) {
308 $_SESSION[$key] = $value;
309 }
310 }
311
312 return json_decode($json, true);
313 }
314
315 /**
316 * Returns whether we handle an AJAX (XMLHttpRequest) request.
317 * @return boolean whether we handle an AJAX (XMLHttpRequest) request.
318 */
319 public static function isAjaxRequest()
320 {
321 return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest';
322 }
323
255} 324}