]> git.immae.eu Git - github/wallabag/wallabag.git/blob - inc/poche/Poche.class.php
new fields in database, reading time / date and domain name are stored
[github/wallabag/wallabag.git] / inc / poche / Poche.class.php
1 <?php
2 /**
3 * wallabag, self hostable application allowing you to not miss any content anymore
4 *
5 * @category wallabag
6 * @author Nicolas Lœuillet <nicolas@loeuillet.org>
7 * @copyright 2013
8 * @license http://www.wtfpl.net/ see COPYING file
9 */
10
11 class Poche
12 {
13 public static $canRenderTemplates = true;
14 public static $configFileAvailable = true;
15
16 public $user;
17 public $store;
18 public $tpl;
19 public $messages;
20 public $pagination;
21
22 private $currentTheme = '';
23 private $currentLanguage = '';
24 private $notInstalledMessage = array();
25
26 private $language_names = array(
27 'cs_CZ.utf8' => 'čeština',
28 'de_DE.utf8' => 'German',
29 'en_EN.utf8' => 'English',
30 'es_ES.utf8' => 'Español',
31 'fa_IR.utf8' => 'فارسی',
32 'fr_FR.utf8' => 'Français',
33 'it_IT.utf8' => 'Italiano',
34 'pl_PL.utf8' => 'Polski',
35 'pt_BR.utf8' => 'Português (Brasil)',
36 'ru_RU.utf8' => 'Pусский',
37 'sl_SI.utf8' => 'Slovenščina',
38 'uk_UA.utf8' => 'Українська',
39 );
40 public function __construct()
41 {
42 if ($this->configFileIsAvailable()) {
43 $this->init();
44 }
45
46 if ($this->themeIsInstalled()) {
47 $this->initTpl();
48 }
49
50 if ($this->systemIsInstalled()) {
51 $this->store = new Database();
52 $this->messages = new Messages();
53 # installation
54 if (! $this->store->isInstalled()) {
55 $this->install();
56 }
57 $this->store->checkTags();
58 }
59 }
60
61 private function init()
62 {
63 Tools::initPhp();
64
65 if (isset($_SESSION['poche_user']) && $_SESSION['poche_user'] != array()) {
66 $this->user = $_SESSION['poche_user'];
67 } else {
68 # fake user, just for install & login screens
69 $this->user = new User();
70 $this->user->setConfig($this->getDefaultConfig());
71 }
72
73 # l10n
74 $language = $this->user->getConfigValue('language');
75 @putenv('LC_ALL=' . $language);
76 setlocale(LC_ALL, $language);
77 bindtextdomain($language, LOCALE);
78 textdomain($language);
79
80 # Pagination
81 $this->pagination = new Paginator($this->user->getConfigValue('pager'), 'p');
82
83 # Set up theme
84 $themeDirectory = $this->user->getConfigValue('theme');
85
86 if ($themeDirectory === false) {
87 $themeDirectory = DEFAULT_THEME;
88 }
89
90 $this->currentTheme = $themeDirectory;
91
92 # Set up language
93 $languageDirectory = $this->user->getConfigValue('language');
94
95 if ($languageDirectory === false) {
96 $languageDirectory = DEFAULT_THEME;
97 }
98
99 $this->currentLanguage = $languageDirectory;
100 }
101
102 public function configFileIsAvailable() {
103 if (! self::$configFileAvailable) {
104 $this->notInstalledMessage[] = 'You have to copy (don\'t just rename!) inc/poche/config.inc.default.php to inc/poche/config.inc.php.';
105
106 return false;
107 }
108
109 return true;
110 }
111
112 public function themeIsInstalled() {
113 $passTheme = TRUE;
114 # Twig is an absolute requirement for Poche to function. Abort immediately if the Composer installer hasn't been run yet
115 if (! self::$canRenderTemplates) {
116 $this->notInstalledMessage[] = 'Twig does not seem to be installed. Please initialize the Composer installation to automatically fetch dependencies. You can also download <a href="http://wllbg.org/vendor">vendor.zip</a> and extract it in your wallabag folder.';
117 $passTheme = FALSE;
118 }
119
120 if (! is_writable(CACHE)) {
121 $this->notInstalledMessage[] = 'You don\'t have write access on cache directory.';
122
123 self::$canRenderTemplates = false;
124
125 $passTheme = FALSE;
126 }
127
128 # Check if the selected theme and its requirements are present
129 $theme = $this->getTheme();
130
131 if ($theme != '' && ! is_dir(THEME . '/' . $theme)) {
132 $this->notInstalledMessage[] = 'The currently selected theme (' . $theme . ') does not seem to be properly installed (Missing directory: ' . THEME . '/' . $theme . ')';
133
134 self::$canRenderTemplates = false;
135
136 $passTheme = FALSE;
137 }
138
139 $themeInfo = $this->getThemeInfo($theme);
140 if (isset($themeInfo['requirements']) && is_array($themeInfo['requirements'])) {
141 foreach ($themeInfo['requirements'] as $requiredTheme) {
142 if (! is_dir(THEME . '/' . $requiredTheme)) {
143 $this->notInstalledMessage[] = 'The required "' . $requiredTheme . '" theme is missing for the current theme (' . $theme . ')';
144
145 self::$canRenderTemplates = false;
146
147 $passTheme = FALSE;
148 }
149 }
150 }
151
152 if (!$passTheme) {
153 return FALSE;
154 }
155
156
157 return true;
158 }
159
160 /**
161 * all checks before installation.
162 * @todo move HTML to template
163 * @return boolean
164 */
165 public function systemIsInstalled()
166 {
167 $msg = TRUE;
168
169 $configSalt = defined('SALT') ? constant('SALT') : '';
170
171 if (empty($configSalt)) {
172 $this->notInstalledMessage[] = 'You have not yet filled in the SALT value in the config.inc.php file.';
173 $msg = FALSE;
174 }
175 if (STORAGE == 'sqlite' && ! file_exists(STORAGE_SQLITE)) {
176 Tools::logm('sqlite file doesn\'t exist');
177 $this->notInstalledMessage[] = 'sqlite file doesn\'t exist, you can find it in install folder. Copy it in /db folder.';
178 $msg = FALSE;
179 }
180 if (is_dir(ROOT . '/install') && ! DEBUG_POCHE) {
181 $this->notInstalledMessage[] = 'you have to delete the /install folder before using poche.';
182 $msg = FALSE;
183 }
184 if (STORAGE == 'sqlite' && ! is_writable(STORAGE_SQLITE)) {
185 Tools::logm('you don\'t have write access on sqlite file');
186 $this->notInstalledMessage[] = 'You don\'t have write access on sqlite file.';
187 $msg = FALSE;
188 }
189
190 if (! $msg) {
191 return false;
192 }
193
194 return true;
195 }
196
197 public function getNotInstalledMessage() {
198 return $this->notInstalledMessage;
199 }
200
201 private function initTpl()
202 {
203 $loaderChain = new Twig_Loader_Chain();
204 $theme = $this->getTheme();
205
206 # add the current theme as first to the loader chain so Twig will look there first for overridden template files
207 try {
208 $loaderChain->addLoader(new Twig_Loader_Filesystem(THEME . '/' . $theme));
209 } catch (Twig_Error_Loader $e) {
210 # @todo isInstalled() should catch this, inject Twig later
211 die('The currently selected theme (' . $theme . ') does not seem to be properly installed (' . THEME . '/' . $theme .' is missing)');
212 }
213
214 # add all required themes to the loader chain
215 $themeInfo = $this->getThemeInfo($theme);
216 if (isset($themeInfo['requirements']) && is_array($themeInfo['requirements'])) {
217 foreach ($themeInfo['requirements'] as $requiredTheme) {
218 try {
219 $loaderChain->addLoader(new Twig_Loader_Filesystem(THEME . '/' . $requiredTheme));
220 } catch (Twig_Error_Loader $e) {
221 # @todo isInstalled() should catch this, inject Twig later
222 die('The required "' . $requiredTheme . '" theme is missing for the current theme (' . $theme . ')');
223 }
224 }
225 }
226
227 if (DEBUG_POCHE) {
228 $twigParams = array();
229 } else {
230 $twigParams = array('cache' => CACHE);
231 }
232
233 $this->tpl = new Twig_Environment($loaderChain, $twigParams);
234 $this->tpl->addExtension(new Twig_Extensions_Extension_I18n());
235 }
236
237 public function createNewUser() {
238 if (isset($_GET['newuser'])){
239 if ($_POST['newusername'] != "" && $_POST['password4newuser'] != ""){
240 $newusername = filter_var($_POST['newusername'], FILTER_SANITIZE_STRING);
241 if (!$this->store->userExists($newusername)){
242 if ($this->store->install($newusername, Tools::encodeString($_POST['password4newuser'] . $newusername))) {
243 Tools::logm('The new user '.$newusername.' has been installed');
244 $this->messages->add('s', sprintf(_('The new user %s has been installed. Do you want to <a href="?logout">logout ?</a>'),$newusername));
245 Tools::redirect();
246 }
247 else {
248 Tools::logm('error during adding new user');
249 Tools::redirect();
250 }
251 }
252 else {
253 $this->messages->add('e', sprintf(_('Error : An user with the name %s already exists !'),$newusername));
254 Tools::logm('An user with the name '.$newusername.' already exists !');
255 Tools::redirect();
256 }
257 }
258 }
259 }
260
261 public function deleteUser(){
262 if (isset($_GET['deluser'])){
263 if ($this->store->listUsers() > 1) {
264 if (Tools::encodeString($_POST['password4deletinguser'].$this->user->getUsername()) == $this->store->getUserPassword($this->user->getId())) {
265 $username = $this->user->getUsername();
266 $this->store->deleteUserConfig($this->user->getId());
267 Tools::logm('The configuration for user '. $username .' has been deleted !');
268 $this->store->deleteTagsEntriesAndEntries($this->user->getId());
269 Tools::logm('The entries for user '. $username .' has been deleted !');
270 $this->store->deleteUser($this->user->getId());
271 Tools::logm('User '. $username .' has been completely deleted !');
272 Session::logout();
273 Tools::logm('logout');
274 Tools::redirect();
275 $this->messages->add('s', sprintf(_('User %s has been successfully deleted !'),$newusername));
276 }
277 else {
278 Tools::logm('Bad password !');
279 $this->messages->add('e', _('Error : The password is wrong !'));
280 }
281 }
282 else {
283 Tools::logm('Only user !');
284 $this->messages->add('e', _('Error : You are the only user, you cannot delete your account !'));
285 }
286 }
287 }
288
289 private function install()
290 {
291 Tools::logm('poche still not installed');
292 echo $this->tpl->render('install.twig', array(
293 'token' => Session::getToken(),
294 'theme' => $this->getTheme(),
295 'poche_url' => Tools::getPocheUrl()
296 ));
297 if (isset($_GET['install'])) {
298 if (($_POST['password'] == $_POST['password_repeat'])
299 && $_POST['password'] != "" && $_POST['login'] != "") {
300 # let's rock, install poche baby !
301 if ($this->store->install($_POST['login'], Tools::encodeString($_POST['password'] . $_POST['login'])))
302 {
303 Session::logout();
304 Tools::logm('poche is now installed');
305 Tools::redirect();
306 }
307 }
308 else {
309 Tools::logm('error during installation');
310 Tools::redirect();
311 }
312 }
313 exit();
314 }
315
316 public function getTheme() {
317 return $this->currentTheme;
318 }
319
320 /**
321 * Provides theme information by parsing theme.ini file if present in the theme's root directory.
322 * In all cases, the following data will be returned:
323 * - name: theme's name, or key if the theme is unnamed,
324 * - current: boolean informing if the theme is the current user theme.
325 *
326 * @param string $theme Theme key (directory name)
327 * @return array|boolean Theme information, or false if the theme doesn't exist.
328 */
329 public function getThemeInfo($theme) {
330 if (!is_dir(THEME . '/' . $theme)) {
331 return false;
332 }
333
334 $themeIniFile = THEME . '/' . $theme . '/theme.ini';
335 $themeInfo = array();
336
337 if (is_file($themeIniFile) && is_readable($themeIniFile)) {
338 $themeInfo = parse_ini_file($themeIniFile);
339 }
340
341 if ($themeInfo === false) {
342 $themeInfo = array();
343 }
344 if (!isset($themeInfo['name'])) {
345 $themeInfo['name'] = $theme;
346 }
347 $themeInfo['current'] = ($theme === $this->getTheme());
348
349 return $themeInfo;
350 }
351
352 public function getInstalledThemes() {
353 $handle = opendir(THEME);
354 $themes = array();
355
356 while (($theme = readdir($handle)) !== false) {
357 # Themes are stored in a directory, so all directory names are themes
358 # @todo move theme installation data to database
359 if (!is_dir(THEME . '/' . $theme) || in_array($theme, array('.', '..'))) {
360 continue;
361 }
362
363 $themes[$theme] = $this->getThemeInfo($theme);
364 }
365
366 ksort($themes);
367
368 return $themes;
369 }
370
371 public function getLanguage() {
372 return $this->currentLanguage;
373 }
374
375 public function getInstalledLanguages() {
376 $handle = opendir(LOCALE);
377 $languages = array();
378
379 while (($language = readdir($handle)) !== false) {
380 # Languages are stored in a directory, so all directory names are languages
381 # @todo move language installation data to database
382 if (! is_dir(LOCALE . '/' . $language) || in_array($language, array('..', '.', 'tools'))) {
383 continue;
384 }
385
386 $current = false;
387
388 if ($language === $this->getLanguage()) {
389 $current = true;
390 }
391
392 $languages[] = array('name' => (isset($this->language_names[$language]) ? $this->language_names[$language] : $language), 'value' => $language, 'current' => $current);
393 }
394
395 return $languages;
396 }
397
398 public function getDefaultConfig()
399 {
400 return array(
401 'pager' => PAGINATION,
402 'language' => LANG,
403 'theme' => DEFAULT_THEME
404 );
405 }
406
407 /**
408 * Call action (mark as fav, archive, delete, etc.)
409 */
410 public function action($action, Url $url, $id = 0, $import = FALSE, $autoclose = FALSE, $tags = null)
411 {
412 switch ($action)
413 {
414 case 'add':
415 $content = Tools::getPageContent($url);
416 $title = ($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled');
417 $body = $content['rss']['channel']['item']['description'];
418
419 // clean content from prevent xss attack
420 $purifier = $this->getPurifier();
421 $title = $purifier->purify($title);
422 $body = $purifier->purify($body);
423
424 //search for possible duplicate
425 $duplicate = NULL;
426 $duplicate = $this->store->retrieveOneByURL($url->getUrl(), $this->user->getId());
427
428 $last_id = $this->store->add($url->getUrl(), $title, $body, $this->user->getId());
429 if ( $last_id ) {
430 Tools::logm('add link ' . $url->getUrl());
431 if (DOWNLOAD_PICTURES) {
432 $content = filtre_picture($body, $url->getUrl(), $last_id);
433 Tools::logm('updating content article');
434 $this->store->updateContent($last_id, $content, $this->user->getId());
435 }
436
437 if ($duplicate != NULL) {
438 // duplicate exists, so, older entry needs to be deleted (as new entry should go to the top of list), BUT favorite mark and tags should be preserved
439 Tools::logm('link ' . $url->getUrl() . ' is a duplicate');
440 // 1) - preserve tags and favorite, then drop old entry
441 $this->store->reassignTags($duplicate['id'], $last_id);
442 if ($duplicate['is_fav']) {
443 $this->store->favoriteById($last_id, $this->user->getId());
444 }
445 if ($this->store->deleteById($duplicate['id'], $this->user->getId())) {
446 Tools::logm('previous link ' . $url->getUrl() .' entry deleted');
447 }
448 }
449
450 $this->messages->add('s', _('the link has been added successfully'));
451 }
452 else {
453 $this->messages->add('e', _('error during insertion : the link wasn\'t added'));
454 Tools::logm('error during insertion : the link wasn\'t added ' . $url->getUrl());
455 }
456
457 if ($autoclose == TRUE) {
458 Tools::redirect('?view=home');
459 } else {
460 Tools::redirect('?view=home&closewin=true');
461 }
462 break;
463 case 'delete':
464 $msg = 'delete link #' . $id;
465 if ($this->store->deleteById($id, $this->user->getId())) {
466 if (DOWNLOAD_PICTURES) {
467 remove_directory(ABS_PATH . $id);
468 }
469 $this->messages->add('s', _('the link has been deleted successfully'));
470 }
471 else {
472 $this->messages->add('e', _('the link wasn\'t deleted'));
473 $msg = 'error : can\'t delete link #' . $id;
474 }
475 Tools::logm($msg);
476 Tools::redirect('?');
477 break;
478 case 'toggle_fav' :
479 $this->store->favoriteById($id, $this->user->getId());
480 Tools::logm('mark as favorite link #' . $id);
481 if ( Tools::isAjaxRequest() ) {
482 echo 1;
483 exit;
484 }
485 else {
486 Tools::redirect();
487 }
488 break;
489 case 'toggle_archive' :
490 $this->store->archiveById($id, $this->user->getId());
491 Tools::logm('archive link #' . $id);
492 if ( Tools::isAjaxRequest() ) {
493 echo 1;
494 exit;
495 }
496 else {
497 Tools::redirect();
498 }
499 break;
500 case 'archive_all' :
501 $this->store->archiveAll($this->user->getId());
502 Tools::logm('archive all links');
503 Tools::redirect();
504 break;
505 case 'add_tag' :
506 if (isset($_GET['search'])) {
507 //when we want to apply a tag to a search
508 $tags = array($_GET['search']);
509 $allentry_ids = $this->store->search($tags[0], $this->user->getId());
510 $entry_ids = array();
511 foreach ($allentry_ids as $eachentry) {
512 $entry_ids[] = $eachentry[0];
513 }
514 } else { //add a tag to a single article
515 $tags = explode(',', $_POST['value']);
516 $entry_ids = array($_POST['entry_id']);
517 }
518 foreach($entry_ids as $entry_id) {
519 $entry = $this->store->retrieveOneById($entry_id, $this->user->getId());
520 if (!$entry) {
521 $this->messages->add('e', _('Article not found!'));
522 Tools::logm('error : article not found');
523 Tools::redirect();
524 }
525 //get all already set tags to preven duplicates
526 $already_set_tags = array();
527 $entry_tags = $this->store->retrieveTagsByEntry($entry_id);
528 foreach ($entry_tags as $tag) {
529 $already_set_tags[] = $tag['value'];
530 }
531 foreach($tags as $key => $tag_value) {
532 $value = trim($tag_value);
533 if ($value && !in_array($value, $already_set_tags)) {
534 $tag = $this->store->retrieveTagByValue($value);
535 if (is_null($tag)) {
536 # we create the tag
537 $tag = $this->store->createTag($value);
538 $sequence = '';
539 if (STORAGE == 'postgres') {
540 $sequence = 'tags_id_seq';
541 }
542 $tag_id = $this->store->getLastId($sequence);
543 }
544 else {
545 $tag_id = $tag['id'];
546 }
547
548 # we assign the tag to the article
549 $this->store->setTagToEntry($tag_id, $entry_id);
550 }
551 }
552 }
553 $this->messages->add('s', _('The tag has been applied successfully'));
554 Tools::logm('The tag has been applied successfully');
555 Tools::redirect();
556 break;
557 case 'remove_tag' :
558 $tag_id = $_GET['tag_id'];
559 $entry = $this->store->retrieveOneById($id, $this->user->getId());
560 if (!$entry) {
561 $this->messages->add('e', _('Article not found!'));
562 Tools::logm('error : article not found');
563 Tools::redirect();
564 }
565 $this->store->removeTagForEntry($id, $tag_id);
566 Tools::logm('tag entry deleted');
567 if ($this->store->cleanUnusedTag($tag_id)) {
568 Tools::logm('tag deleted');
569 }
570 $this->messages->add('s', _('The tag has been successfully deleted'));
571 Tools::redirect();
572 break;
573 default:
574 break;
575 }
576 }
577
578 function displayView($view, $id = 0)
579 {
580 $tpl_vars = array();
581
582 switch ($view)
583 {
584 case 'config':
585 $dev_infos = $this->getPocheVersion('dev');
586 $dev = trim($dev_infos[0]);
587 $check_time_dev = date('d-M-Y H:i', $dev_infos[1]);
588 $prod_infos = $this->getPocheVersion('prod');
589 $prod = trim($prod_infos[0]);
590 $check_time_prod = date('d-M-Y H:i', $prod_infos[1]);
591 $compare_dev = version_compare(POCHE, $dev);
592 $compare_prod = version_compare(POCHE, $prod);
593 $themes = $this->getInstalledThemes();
594 $languages = $this->getInstalledLanguages();
595 $token = $this->user->getConfigValue('token');
596 $http_auth = (isset($_SERVER['PHP_AUTH_USER']) || isset($_SERVER['REMOTE_USER'])) ? true : false;
597 $only_user = ($this->store->listUsers() > 1) ? false : true;
598 $tpl_vars = array(
599 'themes' => $themes,
600 'languages' => $languages,
601 'dev' => $dev,
602 'prod' => $prod,
603 'check_time_dev' => $check_time_dev,
604 'check_time_prod' => $check_time_prod,
605 'compare_dev' => $compare_dev,
606 'compare_prod' => $compare_prod,
607 'token' => $token,
608 'user_id' => $this->user->getId(),
609 'http_auth' => $http_auth,
610 'only_user' => $only_user
611 );
612 Tools::logm('config view');
613 break;
614 case 'edit-tags':
615 # tags
616 $entry = $this->store->retrieveOneById($id, $this->user->getId());
617 if (!$entry) {
618 $this->messages->add('e', _('Article not found!'));
619 Tools::logm('error : article not found');
620 Tools::redirect();
621 }
622 $tags = $this->store->retrieveTagsByEntry($id);
623 $tpl_vars = array(
624 'entry_id' => $id,
625 'tags' => $tags,
626 'entry' => $entry,
627 );
628 break;
629 case 'tags':
630 $token = $this->user->getConfigValue('token');
631 //if term is set - search tags for this term
632 $term = Tools::checkVar('term');
633 $tags = $this->store->retrieveAllTags($this->user->getId(), $term);
634 if (Tools::isAjaxRequest()) {
635 $result = array();
636 foreach ($tags as $tag) {
637 $result[] = $tag['value'];
638 }
639 echo json_encode($result);
640 exit;
641 }
642 $tpl_vars = array(
643 'token' => $token,
644 'user_id' => $this->user->getId(),
645 'tags' => $tags,
646 );
647 break;
648 case 'search':
649 if (isset($_GET['search'])) {
650 $search = filter_var($_GET['search'], FILTER_SANITIZE_STRING);
651 $tpl_vars['entries'] = $this->store->search($search, $this->user->getId());
652 $count = count($tpl_vars['entries']);
653 $this->pagination->set_total($count);
654 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
655 $this->pagination->page_links('?view=' . $view . '?search=' . $search . '&sort=' . $_SESSION['sort'] . '&' ));
656 $tpl_vars['page_links'] = $page_links;
657 $tpl_vars['nb_results'] = $count;
658 $tpl_vars['search_term'] = $search;
659 }
660 break;
661 case 'view':
662 $entry = $this->store->retrieveOneById($id, $this->user->getId());
663 if ($entry != NULL) {
664 Tools::logm('view link #' . $id);
665 $content = $entry['content'];
666 if (function_exists('tidy_parse_string')) {
667 $tidy = tidy_parse_string($content, array('indent'=>true, 'show-body-only' => true), 'UTF8');
668 $tidy->cleanRepair();
669 $content = $tidy->value;
670 }
671
672 # flattr checking
673 $flattr = new FlattrItem();
674 $flattr->checkItem($entry['url'], $entry['id']);
675
676 # tags
677 $tags = $this->store->retrieveTagsByEntry($entry['id']);
678
679 $tpl_vars = array(
680 'entry' => $entry,
681 'content' => $content,
682 'flattr' => $flattr,
683 'tags' => $tags
684 );
685 }
686 else {
687 Tools::logm('error in view call : entry is null');
688 }
689 break;
690 default: # home, favorites, archive and tag views
691 $tpl_vars = array(
692 'entries' => '',
693 'page_links' => '',
694 'nb_results' => '',
695 'listmode' => (isset($_COOKIE['listmode']) ? true : false),
696 );
697
698 //if id is given - we retrive entries by tag: id is tag id
699 if ($id) {
700 $tpl_vars['tag'] = $this->store->retrieveTag($id, $this->user->getId());
701 $tpl_vars['id'] = intval($id);
702 }
703
704 $count = $this->store->getEntriesByViewCount($view, $this->user->getId(), $id);
705
706 if ($count > 0) {
707 $this->pagination->set_total($count);
708 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
709 $this->pagination->page_links('?view=' . $view . '&sort=' . $_SESSION['sort'] . (($id)?'&id='.$id:'') . '&' ));
710 $tpl_vars['entries'] = $this->store->getEntriesByView($view, $this->user->getId(), $this->pagination->get_limit(), $id);
711 $tpl_vars['page_links'] = $page_links;
712 $tpl_vars['nb_results'] = $count;
713 }
714 Tools::logm('display ' . $view . ' view');
715 break;
716 }
717
718 return $tpl_vars;
719 }
720
721 /**
722 * update the password of the current user.
723 * if MODE_DEMO is TRUE, the password can't be updated.
724 * @todo add the return value
725 * @todo set the new password in function header like this updatePassword($newPassword)
726 * @return boolean
727 */
728 public function updatePassword()
729 {
730 if (MODE_DEMO) {
731 $this->messages->add('i', _('in demo mode, you can\'t update your password'));
732 Tools::logm('in demo mode, you can\'t do this');
733 Tools::redirect('?view=config');
734 }
735 else {
736 if (isset($_POST['password']) && isset($_POST['password_repeat'])) {
737 if ($_POST['password'] == $_POST['password_repeat'] && $_POST['password'] != "") {
738 $this->messages->add('s', _('your password has been updated'));
739 $this->store->updatePassword($this->user->getId(), Tools::encodeString($_POST['password'] . $this->user->getUsername()));
740 Session::logout();
741 Tools::logm('password updated');
742 Tools::redirect();
743 }
744 else {
745 $this->messages->add('e', _('the two fields have to be filled & the password must be the same in the two fields'));
746 Tools::redirect('?view=config');
747 }
748 }
749 }
750 }
751
752 public function updateTheme()
753 {
754 # no data
755 if (empty($_POST['theme'])) {
756 }
757
758 # we are not going to change it to the current theme...
759 if ($_POST['theme'] == $this->getTheme()) {
760 $this->messages->add('w', _('still using the "' . $this->getTheme() . '" theme!'));
761 Tools::redirect('?view=config');
762 }
763
764 $themes = $this->getInstalledThemes();
765 $actualTheme = false;
766
767 foreach (array_keys($themes) as $theme) {
768 if ($theme == $_POST['theme']) {
769 $actualTheme = true;
770 break;
771 }
772 }
773
774 if (! $actualTheme) {
775 $this->messages->add('e', _('that theme does not seem to be installed'));
776 Tools::redirect('?view=config');
777 }
778
779 $this->store->updateUserConfig($this->user->getId(), 'theme', $_POST['theme']);
780 $this->messages->add('s', _('you have changed your theme preferences'));
781
782 $currentConfig = $_SESSION['poche_user']->config;
783 $currentConfig['theme'] = $_POST['theme'];
784
785 $_SESSION['poche_user']->setConfig($currentConfig);
786
787 $this->emptyCache();
788
789 Tools::redirect('?view=config');
790 }
791
792 public function updateLanguage()
793 {
794 # no data
795 if (empty($_POST['language'])) {
796 }
797
798 # we are not going to change it to the current language...
799 if ($_POST['language'] == $this->getLanguage()) {
800 $this->messages->add('w', _('still using the "' . $this->getLanguage() . '" language!'));
801 Tools::redirect('?view=config');
802 }
803
804 $languages = $this->getInstalledLanguages();
805 $actualLanguage = false;
806
807 foreach ($languages as $language) {
808 if ($language['value'] == $_POST['language']) {
809 $actualLanguage = true;
810 break;
811 }
812 }
813
814 if (! $actualLanguage) {
815 $this->messages->add('e', _('that language does not seem to be installed'));
816 Tools::redirect('?view=config');
817 }
818
819 $this->store->updateUserConfig($this->user->getId(), 'language', $_POST['language']);
820 $this->messages->add('s', _('you have changed your language preferences'));
821
822 $currentConfig = $_SESSION['poche_user']->config;
823 $currentConfig['language'] = $_POST['language'];
824
825 $_SESSION['poche_user']->setConfig($currentConfig);
826
827 $this->emptyCache();
828
829 Tools::redirect('?view=config');
830 }
831 /**
832 * get credentials from differents sources
833 * it redirects the user to the $referer link
834 * @return array
835 */
836 private function credentials() {
837 if(isset($_SERVER['PHP_AUTH_USER'])) {
838 return array($_SERVER['PHP_AUTH_USER'],'php_auth',true);
839 }
840 if(!empty($_POST['login']) && !empty($_POST['password'])) {
841 return array($_POST['login'],$_POST['password'],false);
842 }
843 if(isset($_SERVER['REMOTE_USER'])) {
844 return array($_SERVER['REMOTE_USER'],'http_auth',true);
845 }
846
847 return array(false,false,false);
848 }
849
850 /**
851 * checks if login & password are correct and save the user in session.
852 * it redirects the user to the $referer link
853 * @param string $referer the url to redirect after login
854 * @todo add the return value
855 * @return boolean
856 */
857 public function login($referer)
858 {
859 list($login,$password,$isauthenticated)=$this->credentials();
860 if($login === false || $password === false) {
861 $this->messages->add('e', _('login failed: you have to fill all fields'));
862 Tools::logm('login failed');
863 Tools::redirect();
864 }
865 if (!empty($login) && !empty($password)) {
866 $user = $this->store->login($login, Tools::encodeString($password . $login), $isauthenticated);
867 if ($user != array()) {
868 # Save login into Session
869 $longlastingsession = isset($_POST['longlastingsession']);
870 $passwordTest = ($isauthenticated) ? $user['password'] : Tools::encodeString($password . $login);
871 Session::login($user['username'], $user['password'], $login, $passwordTest, $longlastingsession, array('poche_user' => new User($user)));
872 $this->messages->add('s', _('welcome to your wallabag'));
873 Tools::logm('login successful');
874 Tools::redirect($referer);
875 }
876 $this->messages->add('e', _('login failed: bad login or password'));
877 Tools::logm('login failed');
878 Tools::redirect();
879 }
880 }
881
882 /**
883 * log out the poche user. It cleans the session.
884 * @todo add the return value
885 * @return boolean
886 */
887 public function logout()
888 {
889 $this->user = array();
890 Session::logout();
891 Tools::logm('logout');
892 Tools::redirect();
893 }
894
895 /**
896 * import datas into your poche
897 * @return boolean
898 */
899 public function import() {
900
901 if ( isset($_FILES['file']) ) {
902 Tools::logm('Import stated: parsing file');
903
904 // assume, that file is in json format
905 $str_data = file_get_contents($_FILES['file']['tmp_name']);
906 $data = json_decode($str_data, true);
907
908 if ( $data === null ) {
909 //not json - assume html
910 $html = new simple_html_dom();
911 $html->load_file($_FILES['file']['tmp_name']);
912 $data = array();
913 $read = 0;
914 foreach (array('ol','ul') as $list) {
915 foreach ($html->find($list) as $ul) {
916 foreach ($ul->find('li') as $li) {
917 $tmpEntry = array();
918 $a = $li->find('a');
919 $tmpEntry['url'] = $a[0]->href;
920 $tmpEntry['tags'] = $a[0]->tags;
921 $tmpEntry['is_read'] = $read;
922 if ($tmpEntry['url']) {
923 $data[] = $tmpEntry;
924 }
925 }
926 # the second <ol/ul> is for read links
927 $read = ((sizeof($data) && $read)?0:1);
928 }
929 }
930 }
931
932 //for readability structure
933 foreach ($data as $record) {
934 if (is_array($record)) {
935 $data[] = $record;
936 foreach ($record as $record2) {
937 if (is_array($record2)) {
938 $data[] = $record2;
939 }
940 }
941 }
942 }
943
944 $urlsInserted = array(); //urls of articles inserted
945 foreach ($data as $record) {
946 $url = trim( isset($record['article__url']) ? $record['article__url'] : (isset($record['url']) ? $record['url'] : '') );
947 if ( $url and !in_array($url, $urlsInserted) ) {
948 $title = (isset($record['title']) ? $record['title'] : _('Untitled - Import - ').'</a> <a href="./?import">'._('click to finish import').'</a><a>');
949 $body = (isset($record['content']) ? $record['content'] : '');
950 $isRead = (isset($record['is_read']) ? intval($record['is_read']) : (isset($record['archive'])?intval($record['archive']):0));
951 $isFavorite = (isset($record['is_fav']) ? intval($record['is_fav']) : (isset($record['favorite'])?intval($record['favorite']):0) );
952 //insert new record
953 $id = $this->store->add($url, $title, $body, $this->user->getId(), $isFavorite, $isRead);
954 if ( $id ) {
955 $urlsInserted[] = $url; //add
956
957 if ( isset($record['tags']) && trim($record['tags']) ) {
958 //@TODO: set tags
959
960 }
961 }
962 }
963 }
964
965 $i = sizeof($urlsInserted);
966 if ( $i > 0 ) {
967 $this->messages->add('s', _('Articles inserted: ').$i._('. Please note, that some may be marked as "read".'));
968 }
969 Tools::logm('Import of articles finished: '.$i.' articles added (w/o content if not provided).');
970 }
971 //file parsing finished here
972
973 //now download article contents if any
974
975 //check if we need to download any content
976 $recordsDownloadRequired = $this->store->retrieveUnfetchedEntriesCount($this->user->getId());
977 if ( $recordsDownloadRequired == 0 ) {
978 //nothing to download
979 $this->messages->add('s', _('Import finished.'));
980 Tools::logm('Import finished completely');
981 Tools::redirect();
982 }
983 else {
984 //if just inserted - don't download anything, download will start in next reload
985 if ( !isset($_FILES['file']) ) {
986 //download next batch
987 Tools::logm('Fetching next batch of articles...');
988 $items = $this->store->retrieveUnfetchedEntries($this->user->getId(), IMPORT_LIMIT);
989
990 $purifier = $this->getPurifier();
991
992 foreach ($items as $item) {
993 $url = new Url(base64_encode($item['url']));
994 Tools::logm('Fetching article '.$item['id']);
995 $content = Tools::getPageContent($url);
996
997 $title = (($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'));
998 $body = (($content['rss']['channel']['item']['description'] != '') ? $content['rss']['channel']['item']['description'] : _('Undefined'));
999
1000 //clean content to prevent xss attack
1001 $title = $purifier->purify($title);
1002 $body = $purifier->purify($body);
1003
1004 $this->store->updateContentAndTitle($item['id'], $title, $body, $this->user->getId());
1005 Tools::logm('Article '.$item['id'].' updated.');
1006 }
1007
1008 }
1009 }
1010
1011 return array('includeImport'=>true, 'import'=>array('recordsDownloadRequired'=>$recordsDownloadRequired, 'recordsUnderDownload'=> IMPORT_LIMIT, 'delay'=> IMPORT_DELAY * 1000) );
1012 }
1013
1014 /**
1015 * export poche entries in json
1016 * @return json all poche entries
1017 */
1018 public function export() {
1019 $filename = "wallabag-export-".$this->user->getId()."-".date("Y-m-d").".json";
1020 header('Content-Disposition: attachment; filename='.$filename);
1021
1022 $entries = $this->store->retrieveAll($this->user->getId());
1023 echo $this->tpl->render('export.twig', array(
1024 'export' => Tools::renderJson($entries),
1025 ));
1026 Tools::logm('export view');
1027 }
1028
1029 /**
1030 * Checks online the latest version of poche and cache it
1031 * @param string $which 'prod' or 'dev'
1032 * @return string latest $which version
1033 */
1034 private function getPocheVersion($which = 'prod') {
1035 $cache_file = CACHE . '/' . $which;
1036 $check_time = time();
1037
1038 # checks if the cached version file exists
1039 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
1040 $version = file_get_contents($cache_file);
1041 $check_time = filemtime($cache_file);
1042 } else {
1043 $version = file_get_contents('http://static.wallabag.org/versions/' . $which);
1044 file_put_contents($cache_file, $version, LOCK_EX);
1045 }
1046 return array($version, $check_time);
1047 }
1048
1049 public function generateToken()
1050 {
1051 if (ini_get('open_basedir') === '') {
1052 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1053 echo 'This is a server using Windows!';
1054 // alternative to /dev/urandom for Windows
1055 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1056 } else {
1057 $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
1058 }
1059 }
1060 else {
1061 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1062 }
1063
1064 $token = str_replace('+', '', $token);
1065 $this->store->updateUserConfig($this->user->getId(), 'token', $token);
1066 $currentConfig = $_SESSION['poche_user']->config;
1067 $currentConfig['token'] = $token;
1068 $_SESSION['poche_user']->setConfig($currentConfig);
1069 Tools::redirect();
1070 }
1071
1072 public function generateFeeds($token, $user_id, $tag_id, $type = 'home')
1073 {
1074 $allowed_types = array('home', 'fav', 'archive', 'tag');
1075 $config = $this->store->getConfigUser($user_id);
1076
1077 if ($config == null) {
1078 die(sprintf(_('User with this id (%d) does not exist.'), $user_id));
1079 }
1080
1081 if (!in_array($type, $allowed_types) || $token != $config['token']) {
1082 die(_('Uh, there is a problem while generating feeds.'));
1083 }
1084 // Check the token
1085
1086 $feed = new FeedWriter(RSS2);
1087 $feed->setTitle('wallabag — ' . $type . ' feed');
1088 $feed->setLink(Tools::getPocheUrl());
1089 $feed->setChannelElement('pubDate', date(DATE_RSS , time()));
1090 $feed->setChannelElement('generator', 'wallabag');
1091 $feed->setDescription('wallabag ' . $type . ' elements');
1092
1093 if ($type == 'tag') {
1094 $entries = $this->store->retrieveEntriesByTag($tag_id, $user_id);
1095 }
1096 else {
1097 $entries = $this->store->getEntriesByView($type, $user_id);
1098 }
1099
1100 if (count($entries) > 0) {
1101 foreach ($entries as $entry) {
1102 $newItem = $feed->createNewItem();
1103 $newItem->setTitle($entry['title']);
1104 $newItem->setSource(Tools::getPocheUrl() . '?view=view&amp;id=' . $entry['id']);
1105 $newItem->setLink($entry['url']);
1106 $newItem->setDate(time());
1107 $newItem->setDescription($entry['content']);
1108 $feed->addItem($newItem);
1109 }
1110 }
1111
1112 $feed->genarateFeed();
1113 exit;
1114 }
1115
1116 public function emptyCache() {
1117 $files = new RecursiveIteratorIterator(
1118 new RecursiveDirectoryIterator(CACHE, RecursiveDirectoryIterator::SKIP_DOTS),
1119 RecursiveIteratorIterator::CHILD_FIRST
1120 );
1121
1122 foreach ($files as $fileinfo) {
1123 $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
1124 $todo($fileinfo->getRealPath());
1125 }
1126
1127 Tools::logm('empty cache');
1128 $this->messages->add('s', _('Cache deleted.'));
1129 Tools::redirect();
1130 }
1131
1132 /**
1133 * return new purifier object with actual config
1134 */
1135 protected function getPurifier() {
1136 $config = HTMLPurifier_Config::createDefault();
1137 $config->set('Cache.SerializerPath', CACHE);
1138 $config->set('HTML.SafeIframe', true);
1139
1140 //allow YouTube, Vimeo and dailymotion videos
1141 $config->set('URI.SafeIframeRegexp', '%^(https?:)?//(www\.youtube(?:-nocookie)?\.com/embed/|player\.vimeo\.com/video/|www\.dailymotion\.com/embed/video/)%');
1142
1143 return new HTMLPurifier($config);
1144 }
1145
1146 /**
1147 * handle epub
1148 */
1149 public function createEpub() {
1150
1151 switch ($_GET['method']) {
1152 case 'id':
1153 $entryID = filter_var($_GET['id'],FILTER_SANITIZE_NUMBER_INT);
1154 $entry = $this->store->retrieveOneById($entryID, $this->user->getId());
1155 $entries = array($entry);
1156 $bookTitle = $entry['title'];
1157 $bookFileName = substr($bookTitle, 0, 200);
1158 break;
1159 case 'all':
1160 $entries = $this->store->retrieveAll($this->user->getId());
1161 $bookTitle = sprintf(_('All my articles on '), date(_('d.m.y'))); #translatable because each country has it's own date format system
1162 $bookFileName = _('Allarticles') . date(_('dmY'));
1163 break;
1164 case 'tag':
1165 $tag = filter_var($_GET['tag'],FILTER_SANITIZE_STRING);
1166 $tags_id = $this->store->retrieveAllTags($this->user->getId(),$tag);
1167 $tag_id = $tags_id[0]["id"]; // we take the first result, which is supposed to match perfectly. There must be a workaround.
1168 $entries = $this->store->retrieveEntriesByTag($tag_id,$this->user->getId());
1169 $bookTitle = sprintf(_('Articles tagged %s'),$tag);
1170 $bookFileName = substr(sprintf(_('Tag %s'),$tag), 0, 200);
1171 break;
1172 case 'category':
1173 $category = filter_var($_GET['category'],FILTER_SANITIZE_STRING);
1174 $entries = $this->store->getEntriesByView($category,$this->user->getId());
1175 $bookTitle = sprintf(_('All articles in category %s'), $category);
1176 $bookFileName = substr(sprintf(_('Category %s'),$category), 0, 200);
1177 break;
1178 case 'search':
1179 $search = filter_var($_GET['search'],FILTER_SANITIZE_STRING);
1180 $entries = $this->store->search($search,$this->user->getId());
1181 $bookTitle = sprintf(_('All articles for search %s'), $search);
1182 $bookFileName = substr(sprintf(_('Search %s'), $search), 0, 200);
1183 break;
1184 case 'default':
1185 die(_('Uh, there is a problem while generating epub.'));
1186
1187 }
1188
1189 $content_start =
1190 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1191 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
1192 . "<head>"
1193 . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
1194 . "<title>wallabag articles book</title>\n"
1195 . "</head>\n"
1196 . "<body>\n";
1197
1198 $bookEnd = "</body>\n</html>\n";
1199
1200 $log = new Logger("wallabag", TRUE);
1201 $fileDir = CACHE;
1202
1203 $book = new EPub(EPub::BOOK_VERSION_EPUB3, DEBUG_POCHE);
1204 $log->logLine("new EPub()");
1205 $log->logLine("EPub class version: " . EPub::VERSION);
1206 $log->logLine("EPub Req. Zip version: " . EPub::REQ_ZIP_VERSION);
1207 $log->logLine("Zip version: " . Zip::VERSION);
1208 $log->logLine("getCurrentServerURL: " . $book->getCurrentServerURL());
1209 $log->logLine("getCurrentPageURL..: " . $book->getCurrentPageURL());
1210
1211 $book->setTitle(_('wallabag\'s articles'));
1212 $book->setIdentifier("http://$_SERVER[HTTP_HOST]", EPub::IDENTIFIER_URI); // Could also be the ISBN number, prefered for published books, or a UUID.
1213 //$book->setLanguage("en"); // Not needed, but included for the example, Language is mandatory, but EPub defaults to "en". Use RFC3066 Language codes, such as "en", "da", "fr" etc.
1214 $book->setDescription(_("Some articles saved on my wallabag"));
1215 $book->setAuthor("wallabag","wallabag");
1216 $book->setPublisher("wallabag","wallabag"); // I hope this is a non existant address :)
1217 $book->setDate(time()); // Strictly not needed as the book date defaults to time().
1218 //$book->setRights("Copyright and licence information specific for the book."); // As this is generated, this _could_ contain the name or licence information of the user who purchased the book, if needed. If this is used that way, the identifier must also be made unique for the book.
1219 $book->setSourceURL("http://$_SERVER[HTTP_HOST]");
1220
1221 $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "PHP");
1222 $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "wallabag");
1223
1224 $cssData = "body {\n margin-left: .5em;\n margin-right: .5em;\n text-align: justify;\n}\n\np {\n font-family: serif;\n font-size: 10pt;\n text-align: justify;\n text-indent: 1em;\n margin-top: 0px;\n margin-bottom: 1ex;\n}\n\nh1, h2 {\n font-family: sans-serif;\n font-style: italic;\n text-align: center;\n background-color: #6b879c;\n color: white;\n width: 100%;\n}\n\nh1 {\n margin-bottom: 2px;\n}\n\nh2 {\n margin-top: -2px;\n margin-bottom: 2px;\n}\n";
1225
1226 $log->logLine("Add Cover");
1227
1228 $fullTitle = "<h1> " . $bookTitle . "</h1>\n";
1229
1230 $book->setCoverImage("Cover.png", file_get_contents("themes/baggy/img/apple-touch-icon-152.png"), "image/png", $fullTitle);
1231
1232 $cover = $content_start . '<div style="text-align:center;"><p>' . _('Produced by wallabag with PHPePub') . '</p><p>'. _('Please open <a href="https://github.com/wallabag/wallabag/issues" >an issue</a> if you have trouble with the display of this E-Book on your device.') . '</p></div>' . $bookEnd;
1233
1234 //$book->addChapter("Table of Contents", "TOC.xhtml", NULL, false, EPub::EXTERNAL_REF_IGNORE);
1235 $book->addChapter("Notices", "Cover2.html", $cover);
1236
1237 $book->buildTOC();
1238
1239 foreach ($entries as $entry) { //set tags as subjects
1240 $tags = $this->store->retrieveTagsByEntry($entry['id']);
1241 foreach ($tags as $tag) {
1242 $book->setSubject($tag['value']);
1243 }
1244
1245 $log->logLine("Set up parameters");
1246
1247 $chapter = $content_start . $entry['content'] . $bookEnd;
1248 $book->addChapter($entry['title'], htmlspecialchars($entry['title']) . ".html", $chapter, true, EPub::EXTERNAL_REF_ADD);
1249 $log->logLine("Added chapter " . $entry['title']);
1250 }
1251
1252 if (DEBUG_POCHE) {
1253 $epuplog = $book->getLog();
1254 $book->addChapter("Log", "Log.html", $content_start . $log->getLog() . "\n</pre>" . $bookEnd); // log generation
1255 }
1256 $book->finalize();
1257 $zipData = $book->sendBook($bookFileName);
1258 }
1259 }