]> git.immae.eu Git - github/wallabag/wallabag.git/blob - inc/poche/Poche.class.php
fix to display the login successful message with the translation
[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 # filter to display domain name of an url
237 $filter = new Twig_SimpleFilter('getDomain', 'Tools::getDomain');
238 $this->tpl->addFilter($filter);
239
240 # filter for reading time
241 $filter = new Twig_SimpleFilter('getReadingTime', 'Tools::getReadingTime');
242 $this->tpl->addFilter($filter);
243 }
244
245 public function createNewUser() {
246 if (isset($_GET['newuser'])){
247 if ($_POST['newusername'] != "" && $_POST['password4newuser'] != ""){
248 $newusername = filter_var($_POST['newusername'], FILTER_SANITIZE_STRING);
249 if (!$this->store->userExists($newusername)){
250 if ($this->store->install($newusername, Tools::encodeString($_POST['password4newuser'] . $newusername))) {
251 Tools::logm('The new user '.$newusername.' has been installed');
252 $this->messages->add('s', sprintf(_('The new user %s has been installed. Do you want to <a href="?logout">logout ?</a>'),$newusername));
253 Tools::redirect();
254 }
255 else {
256 Tools::logm('error during adding new user');
257 Tools::redirect();
258 }
259 }
260 else {
261 $this->messages->add('e', sprintf(_('Error : An user with the name %s already exists !'),$newusername));
262 Tools::logm('An user with the name '.$newusername.' already exists !');
263 Tools::redirect();
264 }
265 }
266 }
267 }
268
269 public function deleteUser(){
270 if (isset($_GET['deluser'])){
271 if ($this->store->listUsers() > 1) {
272 if (Tools::encodeString($_POST['password4deletinguser'].$this->user->getUsername()) == $this->store->getUserPassword($this->user->getId())) {
273 $username = $this->user->getUsername();
274 $this->store->deleteUserConfig($this->user->getId());
275 Tools::logm('The configuration for user '. $username .' has been deleted !');
276 $this->store->deleteTagsEntriesAndEntries($this->user->getId());
277 Tools::logm('The entries for user '. $username .' has been deleted !');
278 $this->store->deleteUser($this->user->getId());
279 Tools::logm('User '. $username .' has been completely deleted !');
280 Session::logout();
281 Tools::logm('logout');
282 Tools::redirect();
283 $this->messages->add('s', sprintf(_('User %s has been successfully deleted !'),$newusername));
284 }
285 else {
286 Tools::logm('Bad password !');
287 $this->messages->add('e', _('Error : The password is wrong !'));
288 }
289 }
290 else {
291 Tools::logm('Only user !');
292 $this->messages->add('e', _('Error : You are the only user, you cannot delete your account !'));
293 }
294 }
295 }
296
297 private function install()
298 {
299 Tools::logm('poche still not installed');
300 echo $this->tpl->render('install.twig', array(
301 'token' => Session::getToken(),
302 'theme' => $this->getTheme(),
303 'poche_url' => Tools::getPocheUrl()
304 ));
305 if (isset($_GET['install'])) {
306 if (($_POST['password'] == $_POST['password_repeat'])
307 && $_POST['password'] != "" && $_POST['login'] != "") {
308 # let's rock, install poche baby !
309 if ($this->store->install($_POST['login'], Tools::encodeString($_POST['password'] . $_POST['login'])))
310 {
311 Session::logout();
312 Tools::logm('poche is now installed');
313 Tools::redirect();
314 }
315 }
316 else {
317 Tools::logm('error during installation');
318 Tools::redirect();
319 }
320 }
321 exit();
322 }
323
324 public function getTheme() {
325 return $this->currentTheme;
326 }
327
328 /**
329 * Provides theme information by parsing theme.ini file if present in the theme's root directory.
330 * In all cases, the following data will be returned:
331 * - name: theme's name, or key if the theme is unnamed,
332 * - current: boolean informing if the theme is the current user theme.
333 *
334 * @param string $theme Theme key (directory name)
335 * @return array|boolean Theme information, or false if the theme doesn't exist.
336 */
337 public function getThemeInfo($theme) {
338 if (!is_dir(THEME . '/' . $theme)) {
339 return false;
340 }
341
342 $themeIniFile = THEME . '/' . $theme . '/theme.ini';
343 $themeInfo = array();
344
345 if (is_file($themeIniFile) && is_readable($themeIniFile)) {
346 $themeInfo = parse_ini_file($themeIniFile);
347 }
348
349 if ($themeInfo === false) {
350 $themeInfo = array();
351 }
352 if (!isset($themeInfo['name'])) {
353 $themeInfo['name'] = $theme;
354 }
355 $themeInfo['current'] = ($theme === $this->getTheme());
356
357 return $themeInfo;
358 }
359
360 public function getInstalledThemes() {
361 $handle = opendir(THEME);
362 $themes = array();
363
364 while (($theme = readdir($handle)) !== false) {
365 # Themes are stored in a directory, so all directory names are themes
366 # @todo move theme installation data to database
367 if (!is_dir(THEME . '/' . $theme) || in_array($theme, array('.', '..'))) {
368 continue;
369 }
370
371 $themes[$theme] = $this->getThemeInfo($theme);
372 }
373
374 ksort($themes);
375
376 return $themes;
377 }
378
379 public function getLanguage() {
380 return $this->currentLanguage;
381 }
382
383 public function getInstalledLanguages() {
384 $handle = opendir(LOCALE);
385 $languages = array();
386
387 while (($language = readdir($handle)) !== false) {
388 # Languages are stored in a directory, so all directory names are languages
389 # @todo move language installation data to database
390 if (! is_dir(LOCALE . '/' . $language) || in_array($language, array('..', '.', 'tools'))) {
391 continue;
392 }
393
394 $current = false;
395
396 if ($language === $this->getLanguage()) {
397 $current = true;
398 }
399
400 $languages[] = array('name' => (isset($this->language_names[$language]) ? $this->language_names[$language] : $language), 'value' => $language, 'current' => $current);
401 }
402
403 return $languages;
404 }
405
406 public function getDefaultConfig()
407 {
408 return array(
409 'pager' => PAGINATION,
410 'language' => LANG,
411 'theme' => DEFAULT_THEME
412 );
413 }
414
415 /**
416 * Call action (mark as fav, archive, delete, etc.)
417 */
418 public function action($action, Url $url, $id = 0, $import = FALSE, $autoclose = FALSE, $tags = null)
419 {
420 switch ($action)
421 {
422 case 'add':
423 $content = Tools::getPageContent($url);
424 $title = ($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled');
425 $body = $content['rss']['channel']['item']['description'];
426
427 // clean content from prevent xss attack
428 $purifier = $this->getPurifier();
429 $title = $purifier->purify($title);
430 $body = $purifier->purify($body);
431
432 //search for possible duplicate
433 $duplicate = NULL;
434 $duplicate = $this->store->retrieveOneByURL($url->getUrl(), $this->user->getId());
435
436 $last_id = $this->store->add($url->getUrl(), $title, $body, $this->user->getId());
437 if ( $last_id ) {
438 Tools::logm('add link ' . $url->getUrl());
439 if (DOWNLOAD_PICTURES) {
440 $content = filtre_picture($body, $url->getUrl(), $last_id);
441 Tools::logm('updating content article');
442 $this->store->updateContent($last_id, $content, $this->user->getId());
443 }
444
445 if ($duplicate != NULL) {
446 // 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
447 Tools::logm('link ' . $url->getUrl() . ' is a duplicate');
448 // 1) - preserve tags and favorite, then drop old entry
449 $this->store->reassignTags($duplicate['id'], $last_id);
450 if ($duplicate['is_fav']) {
451 $this->store->favoriteById($last_id, $this->user->getId());
452 }
453 if ($this->store->deleteById($duplicate['id'], $this->user->getId())) {
454 Tools::logm('previous link ' . $url->getUrl() .' entry deleted');
455 }
456 }
457
458 $this->messages->add('s', _('the link has been added successfully'));
459 }
460 else {
461 $this->messages->add('e', _('error during insertion : the link wasn\'t added'));
462 Tools::logm('error during insertion : the link wasn\'t added ' . $url->getUrl());
463 }
464
465 if ($autoclose == TRUE) {
466 Tools::redirect('?view=home');
467 } else {
468 Tools::redirect('?view=home&closewin=true');
469 }
470 break;
471 case 'delete':
472 $msg = 'delete link #' . $id;
473 if ($this->store->deleteById($id, $this->user->getId())) {
474 if (DOWNLOAD_PICTURES) {
475 remove_directory(ABS_PATH . $id);
476 }
477 $this->messages->add('s', _('the link has been deleted successfully'));
478 }
479 else {
480 $this->messages->add('e', _('the link wasn\'t deleted'));
481 $msg = 'error : can\'t delete link #' . $id;
482 }
483 Tools::logm($msg);
484 Tools::redirect('?');
485 break;
486 case 'toggle_fav' :
487 $this->store->favoriteById($id, $this->user->getId());
488 Tools::logm('mark as favorite link #' . $id);
489 if ( Tools::isAjaxRequest() ) {
490 echo 1;
491 exit;
492 }
493 else {
494 Tools::redirect();
495 }
496 break;
497 case 'toggle_archive' :
498 $this->store->archiveById($id, $this->user->getId());
499 Tools::logm('archive link #' . $id);
500 if ( Tools::isAjaxRequest() ) {
501 echo 1;
502 exit;
503 }
504 else {
505 Tools::redirect();
506 }
507 break;
508 case 'archive_all' :
509 $this->store->archiveAll($this->user->getId());
510 Tools::logm('archive all links');
511 Tools::redirect();
512 break;
513 case 'add_tag' :
514 if (isset($_GET['search'])) {
515 //when we want to apply a tag to a search
516 $tags = array($_GET['search']);
517 $allentry_ids = $this->store->search($tags[0], $this->user->getId());
518 $entry_ids = array();
519 foreach ($allentry_ids as $eachentry) {
520 $entry_ids[] = $eachentry[0];
521 }
522 } else { //add a tag to a single article
523 $tags = explode(',', $_POST['value']);
524 $entry_ids = array($_POST['entry_id']);
525 }
526 foreach($entry_ids as $entry_id) {
527 $entry = $this->store->retrieveOneById($entry_id, $this->user->getId());
528 if (!$entry) {
529 $this->messages->add('e', _('Article not found!'));
530 Tools::logm('error : article not found');
531 Tools::redirect();
532 }
533 //get all already set tags to preven duplicates
534 $already_set_tags = array();
535 $entry_tags = $this->store->retrieveTagsByEntry($entry_id);
536 foreach ($entry_tags as $tag) {
537 $already_set_tags[] = $tag['value'];
538 }
539 foreach($tags as $key => $tag_value) {
540 $value = trim($tag_value);
541 if ($value && !in_array($value, $already_set_tags)) {
542 $tag = $this->store->retrieveTagByValue($value);
543 if (is_null($tag)) {
544 # we create the tag
545 $tag = $this->store->createTag($value);
546 $sequence = '';
547 if (STORAGE == 'postgres') {
548 $sequence = 'tags_id_seq';
549 }
550 $tag_id = $this->store->getLastId($sequence);
551 }
552 else {
553 $tag_id = $tag['id'];
554 }
555
556 # we assign the tag to the article
557 $this->store->setTagToEntry($tag_id, $entry_id);
558 }
559 }
560 }
561 $this->messages->add('s', _('The tag has been applied successfully'));
562 Tools::logm('The tag has been applied successfully');
563 Tools::redirect();
564 break;
565 case 'remove_tag' :
566 $tag_id = $_GET['tag_id'];
567 $entry = $this->store->retrieveOneById($id, $this->user->getId());
568 if (!$entry) {
569 $this->messages->add('e', _('Article not found!'));
570 Tools::logm('error : article not found');
571 Tools::redirect();
572 }
573 $this->store->removeTagForEntry($id, $tag_id);
574 Tools::logm('tag entry deleted');
575 if ($this->store->cleanUnusedTag($tag_id)) {
576 Tools::logm('tag deleted');
577 }
578 $this->messages->add('s', _('The tag has been successfully deleted'));
579 Tools::redirect();
580 break;
581 default:
582 break;
583 }
584 }
585
586 function displayView($view, $id = 0)
587 {
588 $tpl_vars = array();
589
590 switch ($view)
591 {
592 case 'config':
593 $dev_infos = $this->getPocheVersion('dev');
594 $dev = trim($dev_infos[0]);
595 $check_time_dev = date('d-M-Y H:i', $dev_infos[1]);
596 $prod_infos = $this->getPocheVersion('prod');
597 $prod = trim($prod_infos[0]);
598 $check_time_prod = date('d-M-Y H:i', $prod_infos[1]);
599 $compare_dev = version_compare(POCHE, $dev);
600 $compare_prod = version_compare(POCHE, $prod);
601 $themes = $this->getInstalledThemes();
602 $languages = $this->getInstalledLanguages();
603 $token = $this->user->getConfigValue('token');
604 $http_auth = (isset($_SERVER['PHP_AUTH_USER']) || isset($_SERVER['REMOTE_USER'])) ? true : false;
605 $only_user = ($this->store->listUsers() > 1) ? false : true;
606 $tpl_vars = array(
607 'themes' => $themes,
608 'languages' => $languages,
609 'dev' => $dev,
610 'prod' => $prod,
611 'check_time_dev' => $check_time_dev,
612 'check_time_prod' => $check_time_prod,
613 'compare_dev' => $compare_dev,
614 'compare_prod' => $compare_prod,
615 'token' => $token,
616 'user_id' => $this->user->getId(),
617 'http_auth' => $http_auth,
618 'only_user' => $only_user
619 );
620 Tools::logm('config view');
621 break;
622 case 'edit-tags':
623 # tags
624 $entry = $this->store->retrieveOneById($id, $this->user->getId());
625 if (!$entry) {
626 $this->messages->add('e', _('Article not found!'));
627 Tools::logm('error : article not found');
628 Tools::redirect();
629 }
630 $tags = $this->store->retrieveTagsByEntry($id);
631 $tpl_vars = array(
632 'entry_id' => $id,
633 'tags' => $tags,
634 'entry' => $entry,
635 );
636 break;
637 case 'tags':
638 $token = $this->user->getConfigValue('token');
639 //if term is set - search tags for this term
640 $term = Tools::checkVar('term');
641 $tags = $this->store->retrieveAllTags($this->user->getId(), $term);
642 if (Tools::isAjaxRequest()) {
643 $result = array();
644 foreach ($tags as $tag) {
645 $result[] = $tag['value'];
646 }
647 echo json_encode($result);
648 exit;
649 }
650 $tpl_vars = array(
651 'token' => $token,
652 'user_id' => $this->user->getId(),
653 'tags' => $tags,
654 );
655 break;
656 case 'search':
657 if (isset($_GET['search'])) {
658 $search = filter_var($_GET['search'], FILTER_SANITIZE_STRING);
659 $tpl_vars['entries'] = $this->store->search($search, $this->user->getId());
660 $count = count($tpl_vars['entries']);
661 $this->pagination->set_total($count);
662 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
663 $this->pagination->page_links('?view=' . $view . '?search=' . $search . '&sort=' . $_SESSION['sort'] . '&' ));
664 $tpl_vars['page_links'] = $page_links;
665 $tpl_vars['nb_results'] = $count;
666 $tpl_vars['search_term'] = $search;
667 }
668 break;
669 case 'view':
670 $entry = $this->store->retrieveOneById($id, $this->user->getId());
671 if ($entry != NULL) {
672 Tools::logm('view link #' . $id);
673 $content = $entry['content'];
674 if (function_exists('tidy_parse_string')) {
675 $tidy = tidy_parse_string($content, array('indent'=>true, 'show-body-only' => true), 'UTF8');
676 $tidy->cleanRepair();
677 $content = $tidy->value;
678 }
679
680 # flattr checking
681 $flattr = new FlattrItem();
682 $flattr->checkItem($entry['url'], $entry['id']);
683
684 # tags
685 $tags = $this->store->retrieveTagsByEntry($entry['id']);
686
687 $tpl_vars = array(
688 'entry' => $entry,
689 'content' => $content,
690 'flattr' => $flattr,
691 'tags' => $tags
692 );
693 }
694 else {
695 Tools::logm('error in view call : entry is null');
696 }
697 break;
698 default: # home, favorites, archive and tag views
699 $tpl_vars = array(
700 'entries' => '',
701 'page_links' => '',
702 'nb_results' => '',
703 'listmode' => (isset($_COOKIE['listmode']) ? true : false),
704 );
705
706 //if id is given - we retrive entries by tag: id is tag id
707 if ($id) {
708 $tpl_vars['tag'] = $this->store->retrieveTag($id, $this->user->getId());
709 $tpl_vars['id'] = intval($id);
710 }
711
712 $count = $this->store->getEntriesByViewCount($view, $this->user->getId(), $id);
713
714 if ($count > 0) {
715 $this->pagination->set_total($count);
716 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
717 $this->pagination->page_links('?view=' . $view . '&sort=' . $_SESSION['sort'] . (($id)?'&id='.$id:'') . '&' ));
718 $tpl_vars['entries'] = $this->store->getEntriesByView($view, $this->user->getId(), $this->pagination->get_limit(), $id);
719 $tpl_vars['page_links'] = $page_links;
720 $tpl_vars['nb_results'] = $count;
721 }
722 Tools::logm('display ' . $view . ' view');
723 break;
724 }
725
726 return $tpl_vars;
727 }
728
729 /**
730 * update the password of the current user.
731 * if MODE_DEMO is TRUE, the password can't be updated.
732 * @todo add the return value
733 * @todo set the new password in function header like this updatePassword($newPassword)
734 * @return boolean
735 */
736 public function updatePassword()
737 {
738 if (MODE_DEMO) {
739 $this->messages->add('i', _('in demo mode, you can\'t update your password'));
740 Tools::logm('in demo mode, you can\'t do this');
741 Tools::redirect('?view=config');
742 }
743 else {
744 if (isset($_POST['password']) && isset($_POST['password_repeat'])) {
745 if ($_POST['password'] == $_POST['password_repeat'] && $_POST['password'] != "") {
746 $this->messages->add('s', _('your password has been updated'));
747 $this->store->updatePassword($this->user->getId(), Tools::encodeString($_POST['password'] . $this->user->getUsername()));
748 Session::logout();
749 Tools::logm('password updated');
750 Tools::redirect();
751 }
752 else {
753 $this->messages->add('e', _('the two fields have to be filled & the password must be the same in the two fields'));
754 Tools::redirect('?view=config');
755 }
756 }
757 }
758 }
759
760 public function updateTheme()
761 {
762 # no data
763 if (empty($_POST['theme'])) {
764 }
765
766 # we are not going to change it to the current theme...
767 if ($_POST['theme'] == $this->getTheme()) {
768 $this->messages->add('w', _('still using the "' . $this->getTheme() . '" theme!'));
769 Tools::redirect('?view=config');
770 }
771
772 $themes = $this->getInstalledThemes();
773 $actualTheme = false;
774
775 foreach (array_keys($themes) as $theme) {
776 if ($theme == $_POST['theme']) {
777 $actualTheme = true;
778 break;
779 }
780 }
781
782 if (! $actualTheme) {
783 $this->messages->add('e', _('that theme does not seem to be installed'));
784 Tools::redirect('?view=config');
785 }
786
787 $this->store->updateUserConfig($this->user->getId(), 'theme', $_POST['theme']);
788 $this->messages->add('s', _('you have changed your theme preferences'));
789
790 $currentConfig = $_SESSION['poche_user']->config;
791 $currentConfig['theme'] = $_POST['theme'];
792
793 $_SESSION['poche_user']->setConfig($currentConfig);
794
795 $this->emptyCache();
796
797 Tools::redirect('?view=config');
798 }
799
800 public function updateLanguage()
801 {
802 # no data
803 if (empty($_POST['language'])) {
804 }
805
806 # we are not going to change it to the current language...
807 if ($_POST['language'] == $this->getLanguage()) {
808 $this->messages->add('w', _('still using the "' . $this->getLanguage() . '" language!'));
809 Tools::redirect('?view=config');
810 }
811
812 $languages = $this->getInstalledLanguages();
813 $actualLanguage = false;
814
815 foreach ($languages as $language) {
816 if ($language['value'] == $_POST['language']) {
817 $actualLanguage = true;
818 break;
819 }
820 }
821
822 if (! $actualLanguage) {
823 $this->messages->add('e', _('that language does not seem to be installed'));
824 Tools::redirect('?view=config');
825 }
826
827 $this->store->updateUserConfig($this->user->getId(), 'language', $_POST['language']);
828 $this->messages->add('s', _('you have changed your language preferences'));
829
830 $currentConfig = $_SESSION['poche_user']->config;
831 $currentConfig['language'] = $_POST['language'];
832
833 $_SESSION['poche_user']->setConfig($currentConfig);
834
835 $this->emptyCache();
836
837 Tools::redirect('?view=config');
838 }
839 /**
840 * get credentials from differents sources
841 * it redirects the user to the $referer link
842 * @return array
843 */
844 private function credentials() {
845 if(isset($_SERVER['PHP_AUTH_USER'])) {
846 return array($_SERVER['PHP_AUTH_USER'],'php_auth',true);
847 }
848 if(!empty($_POST['login']) && !empty($_POST['password'])) {
849 return array($_POST['login'],$_POST['password'],false);
850 }
851 if(isset($_SERVER['REMOTE_USER'])) {
852 return array($_SERVER['REMOTE_USER'],'http_auth',true);
853 }
854
855 return array(false,false,false);
856 }
857
858 /**
859 * checks if login & password are correct and save the user in session.
860 * it redirects the user to the $referer link
861 * @param string $referer the url to redirect after login
862 * @todo add the return value
863 * @return boolean
864 */
865 public function login($referer)
866 {
867 list($login,$password,$isauthenticated)=$this->credentials();
868 if($login === false || $password === false) {
869 $this->messages->add('e', _('login failed: you have to fill all fields'));
870 Tools::logm('login failed');
871 Tools::redirect();
872 }
873 if (!empty($login) && !empty($password)) {
874 $user = $this->store->login($login, Tools::encodeString($password . $login), $isauthenticated);
875 if ($user != array()) {
876 # Save login into Session
877 $longlastingsession = isset($_POST['longlastingsession']);
878 $passwordTest = ($isauthenticated) ? $user['password'] : Tools::encodeString($password . $login);
879 Session::login($user['username'], $user['password'], $login, $passwordTest, $longlastingsession, array('poche_user' => new User($user)));
880
881 # reload l10n
882 $language = $user['config']['language'];
883 @putenv('LC_ALL=' . $language);
884 setlocale(LC_ALL, $language);
885 bindtextdomain($language, LOCALE);
886 textdomain($language);
887
888 $this->messages->add('s', _('welcome to your wallabag'));
889 Tools::logm('login successful');
890 Tools::redirect($referer);
891 }
892 $this->messages->add('e', _('login failed: bad login or password'));
893 Tools::logm('login failed');
894 Tools::redirect();
895 }
896 }
897
898 /**
899 * log out the poche user. It cleans the session.
900 * @todo add the return value
901 * @return boolean
902 */
903 public function logout()
904 {
905 $this->user = array();
906 Session::logout();
907 Tools::logm('logout');
908 Tools::redirect();
909 }
910
911 /**
912 * import datas into your poche
913 * @return boolean
914 */
915 public function import() {
916
917 if ( isset($_FILES['file']) ) {
918 Tools::logm('Import stated: parsing file');
919
920 // assume, that file is in json format
921 $str_data = file_get_contents($_FILES['file']['tmp_name']);
922 $data = json_decode($str_data, true);
923
924 if ( $data === null ) {
925 //not json - assume html
926 $html = new simple_html_dom();
927 $html->load_file($_FILES['file']['tmp_name']);
928 $data = array();
929 $read = 0;
930 foreach (array('ol','ul') as $list) {
931 foreach ($html->find($list) as $ul) {
932 foreach ($ul->find('li') as $li) {
933 $tmpEntry = array();
934 $a = $li->find('a');
935 $tmpEntry['url'] = $a[0]->href;
936 $tmpEntry['tags'] = $a[0]->tags;
937 $tmpEntry['is_read'] = $read;
938 if ($tmpEntry['url']) {
939 $data[] = $tmpEntry;
940 }
941 }
942 # the second <ol/ul> is for read links
943 $read = ((sizeof($data) && $read)?0:1);
944 }
945 }
946 }
947
948 //for readability structure
949 foreach ($data as $record) {
950 if (is_array($record)) {
951 $data[] = $record;
952 foreach ($record as $record2) {
953 if (is_array($record2)) {
954 $data[] = $record2;
955 }
956 }
957 }
958 }
959
960 $urlsInserted = array(); //urls of articles inserted
961 foreach ($data as $record) {
962 $url = trim( isset($record['article__url']) ? $record['article__url'] : (isset($record['url']) ? $record['url'] : '') );
963 if ( $url and !in_array($url, $urlsInserted) ) {
964 $title = (isset($record['title']) ? $record['title'] : _('Untitled - Import - ').'</a> <a href="./?import">'._('click to finish import').'</a><a>');
965 $body = (isset($record['content']) ? $record['content'] : '');
966 $isRead = (isset($record['is_read']) ? intval($record['is_read']) : (isset($record['archive'])?intval($record['archive']):0));
967 $isFavorite = (isset($record['is_fav']) ? intval($record['is_fav']) : (isset($record['favorite'])?intval($record['favorite']):0) );
968 //insert new record
969 $id = $this->store->add($url, $title, $body, $this->user->getId(), $isFavorite, $isRead);
970 if ( $id ) {
971 $urlsInserted[] = $url; //add
972
973 if ( isset($record['tags']) && trim($record['tags']) ) {
974 //@TODO: set tags
975
976 }
977 }
978 }
979 }
980
981 $i = sizeof($urlsInserted);
982 if ( $i > 0 ) {
983 $this->messages->add('s', _('Articles inserted: ').$i._('. Please note, that some may be marked as "read".'));
984 }
985 Tools::logm('Import of articles finished: '.$i.' articles added (w/o content if not provided).');
986 }
987 //file parsing finished here
988
989 //now download article contents if any
990
991 //check if we need to download any content
992 $recordsDownloadRequired = $this->store->retrieveUnfetchedEntriesCount($this->user->getId());
993 if ( $recordsDownloadRequired == 0 ) {
994 //nothing to download
995 $this->messages->add('s', _('Import finished.'));
996 Tools::logm('Import finished completely');
997 Tools::redirect();
998 }
999 else {
1000 //if just inserted - don't download anything, download will start in next reload
1001 if ( !isset($_FILES['file']) ) {
1002 //download next batch
1003 Tools::logm('Fetching next batch of articles...');
1004 $items = $this->store->retrieveUnfetchedEntries($this->user->getId(), IMPORT_LIMIT);
1005
1006 $purifier = $this->getPurifier();
1007
1008 foreach ($items as $item) {
1009 $url = new Url(base64_encode($item['url']));
1010 Tools::logm('Fetching article '.$item['id']);
1011 $content = Tools::getPageContent($url);
1012
1013 $title = (($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'));
1014 $body = (($content['rss']['channel']['item']['description'] != '') ? $content['rss']['channel']['item']['description'] : _('Undefined'));
1015
1016 //clean content to prevent xss attack
1017 $title = $purifier->purify($title);
1018 $body = $purifier->purify($body);
1019
1020 $this->store->updateContentAndTitle($item['id'], $title, $body, $this->user->getId());
1021 Tools::logm('Article '.$item['id'].' updated.');
1022 }
1023
1024 }
1025 }
1026
1027 return array('includeImport'=>true, 'import'=>array('recordsDownloadRequired'=>$recordsDownloadRequired, 'recordsUnderDownload'=> IMPORT_LIMIT, 'delay'=> IMPORT_DELAY * 1000) );
1028 }
1029
1030 /**
1031 * export poche entries in json
1032 * @return json all poche entries
1033 */
1034 public function export() {
1035 $filename = "wallabag-export-".$this->user->getId()."-".date("Y-m-d").".json";
1036 header('Content-Disposition: attachment; filename='.$filename);
1037
1038 $entries = $this->store->retrieveAll($this->user->getId());
1039 echo $this->tpl->render('export.twig', array(
1040 'export' => Tools::renderJson($entries),
1041 ));
1042 Tools::logm('export view');
1043 }
1044
1045 /**
1046 * Checks online the latest version of poche and cache it
1047 * @param string $which 'prod' or 'dev'
1048 * @return string latest $which version
1049 */
1050 private function getPocheVersion($which = 'prod') {
1051 $cache_file = CACHE . '/' . $which;
1052 $check_time = time();
1053
1054 # checks if the cached version file exists
1055 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
1056 $version = file_get_contents($cache_file);
1057 $check_time = filemtime($cache_file);
1058 } else {
1059 $version = file_get_contents('http://static.wallabag.org/versions/' . $which);
1060 file_put_contents($cache_file, $version, LOCK_EX);
1061 }
1062 return array($version, $check_time);
1063 }
1064
1065 public function generateToken()
1066 {
1067 if (ini_get('open_basedir') === '') {
1068 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1069 echo 'This is a server using Windows!';
1070 // alternative to /dev/urandom for Windows
1071 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1072 } else {
1073 $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
1074 }
1075 }
1076 else {
1077 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1078 }
1079
1080 $token = str_replace('+', '', $token);
1081 $this->store->updateUserConfig($this->user->getId(), 'token', $token);
1082 $currentConfig = $_SESSION['poche_user']->config;
1083 $currentConfig['token'] = $token;
1084 $_SESSION['poche_user']->setConfig($currentConfig);
1085 Tools::redirect();
1086 }
1087
1088 public function generateFeeds($token, $user_id, $tag_id, $type = 'home')
1089 {
1090 $allowed_types = array('home', 'fav', 'archive', 'tag');
1091 $config = $this->store->getConfigUser($user_id);
1092
1093 if ($config == null) {
1094 die(sprintf(_('User with this id (%d) does not exist.'), $user_id));
1095 }
1096
1097 if (!in_array($type, $allowed_types) || $token != $config['token']) {
1098 die(_('Uh, there is a problem while generating feeds.'));
1099 }
1100 // Check the token
1101
1102 $feed = new FeedWriter(RSS2);
1103 $feed->setTitle('wallabag — ' . $type . ' feed');
1104 $feed->setLink(Tools::getPocheUrl());
1105 $feed->setChannelElement('pubDate', date(DATE_RSS , time()));
1106 $feed->setChannelElement('generator', 'wallabag');
1107 $feed->setDescription('wallabag ' . $type . ' elements');
1108
1109 if ($type == 'tag') {
1110 $entries = $this->store->retrieveEntriesByTag($tag_id, $user_id);
1111 }
1112 else {
1113 $entries = $this->store->getEntriesByView($type, $user_id);
1114 }
1115
1116 if (count($entries) > 0) {
1117 foreach ($entries as $entry) {
1118 $newItem = $feed->createNewItem();
1119 $newItem->setTitle($entry['title']);
1120 $newItem->setSource(Tools::getPocheUrl() . '?view=view&amp;id=' . $entry['id']);
1121 $newItem->setLink($entry['url']);
1122 $newItem->setDate(time());
1123 $newItem->setDescription($entry['content']);
1124 $feed->addItem($newItem);
1125 }
1126 }
1127
1128 $feed->genarateFeed();
1129 exit;
1130 }
1131
1132 public function emptyCache() {
1133 $files = new RecursiveIteratorIterator(
1134 new RecursiveDirectoryIterator(CACHE, RecursiveDirectoryIterator::SKIP_DOTS),
1135 RecursiveIteratorIterator::CHILD_FIRST
1136 );
1137
1138 foreach ($files as $fileinfo) {
1139 $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
1140 $todo($fileinfo->getRealPath());
1141 }
1142
1143 Tools::logm('empty cache');
1144 $this->messages->add('s', _('Cache deleted.'));
1145 Tools::redirect();
1146 }
1147
1148 /**
1149 * return new purifier object with actual config
1150 */
1151 protected function getPurifier() {
1152 $config = HTMLPurifier_Config::createDefault();
1153 $config->set('Cache.SerializerPath', CACHE);
1154 $config->set('HTML.SafeIframe', true);
1155
1156 //allow YouTube, Vimeo and dailymotion videos
1157 $config->set('URI.SafeIframeRegexp', '%^(https?:)?//(www\.youtube(?:-nocookie)?\.com/embed/|player\.vimeo\.com/video/|www\.dailymotion\.com/embed/video/)%');
1158
1159 return new HTMLPurifier($config);
1160 }
1161
1162 /**
1163 * handle epub
1164 */
1165 public function createEpub() {
1166
1167 switch ($_GET['method']) {
1168 case 'id':
1169 $entryID = filter_var($_GET['id'],FILTER_SANITIZE_NUMBER_INT);
1170 $entry = $this->store->retrieveOneById($entryID, $this->user->getId());
1171 $entries = array($entry);
1172 $bookTitle = $entry['title'];
1173 $bookFileName = substr($bookTitle, 0, 200);
1174 break;
1175 case 'all':
1176 $entries = $this->store->retrieveAll($this->user->getId());
1177 $bookTitle = sprintf(_('All my articles on '), date(_('d.m.y'))); #translatable because each country has it's own date format system
1178 $bookFileName = _('Allarticles') . date(_('dmY'));
1179 break;
1180 case 'tag':
1181 $tag = filter_var($_GET['tag'],FILTER_SANITIZE_STRING);
1182 $tags_id = $this->store->retrieveAllTags($this->user->getId(),$tag);
1183 $tag_id = $tags_id[0]["id"]; // we take the first result, which is supposed to match perfectly. There must be a workaround.
1184 $entries = $this->store->retrieveEntriesByTag($tag_id,$this->user->getId());
1185 $bookTitle = sprintf(_('Articles tagged %s'),$tag);
1186 $bookFileName = substr(sprintf(_('Tag %s'),$tag), 0, 200);
1187 break;
1188 case 'category':
1189 $category = filter_var($_GET['category'],FILTER_SANITIZE_STRING);
1190 $entries = $this->store->getEntriesByView($category,$this->user->getId());
1191 $bookTitle = sprintf(_('All articles in category %s'), $category);
1192 $bookFileName = substr(sprintf(_('Category %s'),$category), 0, 200);
1193 break;
1194 case 'search':
1195 $search = filter_var($_GET['search'],FILTER_SANITIZE_STRING);
1196 $entries = $this->store->search($search,$this->user->getId());
1197 $bookTitle = sprintf(_('All articles for search %s'), $search);
1198 $bookFileName = substr(sprintf(_('Search %s'), $search), 0, 200);
1199 break;
1200 case 'default':
1201 die(_('Uh, there is a problem while generating epub.'));
1202
1203 }
1204
1205 $content_start =
1206 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1207 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
1208 . "<head>"
1209 . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
1210 . "<title>wallabag articles book</title>\n"
1211 . "</head>\n"
1212 . "<body>\n";
1213
1214 $bookEnd = "</body>\n</html>\n";
1215
1216 $log = new Logger("wallabag", TRUE);
1217 $fileDir = CACHE;
1218
1219 $book = new EPub(EPub::BOOK_VERSION_EPUB3, DEBUG_POCHE);
1220 $log->logLine("new EPub()");
1221 $log->logLine("EPub class version: " . EPub::VERSION);
1222 $log->logLine("EPub Req. Zip version: " . EPub::REQ_ZIP_VERSION);
1223 $log->logLine("Zip version: " . Zip::VERSION);
1224 $log->logLine("getCurrentServerURL: " . $book->getCurrentServerURL());
1225 $log->logLine("getCurrentPageURL..: " . $book->getCurrentPageURL());
1226
1227 $book->setTitle(_('wallabag\'s articles'));
1228 $book->setIdentifier("http://$_SERVER[HTTP_HOST]", EPub::IDENTIFIER_URI); // Could also be the ISBN number, prefered for published books, or a UUID.
1229 //$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.
1230 $book->setDescription(_("Some articles saved on my wallabag"));
1231 $book->setAuthor("wallabag","wallabag");
1232 $book->setPublisher("wallabag","wallabag"); // I hope this is a non existant address :)
1233 $book->setDate(time()); // Strictly not needed as the book date defaults to time().
1234 //$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.
1235 $book->setSourceURL("http://$_SERVER[HTTP_HOST]");
1236
1237 $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "PHP");
1238 $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "wallabag");
1239
1240 $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";
1241
1242 $log->logLine("Add Cover");
1243
1244 $fullTitle = "<h1> " . $bookTitle . "</h1>\n";
1245
1246 $book->setCoverImage("Cover.png", file_get_contents("themes/baggy/img/apple-touch-icon-152.png"), "image/png", $fullTitle);
1247
1248 $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;
1249
1250 //$book->addChapter("Table of Contents", "TOC.xhtml", NULL, false, EPub::EXTERNAL_REF_IGNORE);
1251 $book->addChapter("Notices", "Cover2.html", $cover);
1252
1253 $book->buildTOC();
1254
1255 foreach ($entries as $entry) { //set tags as subjects
1256 $tags = $this->store->retrieveTagsByEntry($entry['id']);
1257 foreach ($tags as $tag) {
1258 $book->setSubject($tag['value']);
1259 }
1260
1261 $log->logLine("Set up parameters");
1262
1263 $chapter = $content_start . $entry['content'] . $bookEnd;
1264 $book->addChapter($entry['title'], htmlspecialchars($entry['title']) . ".html", $chapter, true, EPub::EXTERNAL_REF_ADD);
1265 $log->logLine("Added chapter " . $entry['title']);
1266 }
1267
1268 if (DEBUG_POCHE) {
1269 $epuplog = $book->getLog();
1270 $book->addChapter("Log", "Log.html", $content_start . $log->getLog() . "\n</pre>" . $bookEnd); // log generation
1271 }
1272 $book->finalize();
1273 $zipData = $book->sendBook($bookFileName);
1274 }
1275 }