]> git.immae.eu Git - github/wallabag/wallabag.git/blob - inc/poche/Poche.class.php
Fix bugs and improved epub rendering
[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 $tags = explode(',', $_POST['value']);
515 $entry_id = $_POST['entry_id'];
516 $entry = $this->store->retrieveOneById($entry_id, $this->user->getId());
517 if (!$entry) {
518 $this->messages->add('e', _('Article not found!'));
519 Tools::logm('error : article not found');
520 Tools::redirect();
521 }
522 //get all already set tags to preven duplicates
523 $already_set_tags = array();
524 $entry_tags = $this->store->retrieveTagsByEntry($entry_id);
525 foreach ($entry_tags as $tag) {
526 $already_set_tags[] = $tag['value'];
527 }
528 foreach($tags as $key => $tag_value) {
529 $value = trim($tag_value);
530 if ($value && !in_array($value, $already_set_tags)) {
531 $tag = $this->store->retrieveTagByValue($value);
532
533 if (is_null($tag)) {
534 # we create the tag
535 $tag = $this->store->createTag($value);
536 $sequence = '';
537 if (STORAGE == 'postgres') {
538 $sequence = 'tags_id_seq';
539 }
540 $tag_id = $this->store->getLastId($sequence);
541 }
542 else {
543 $tag_id = $tag['id'];
544 }
545
546 # we assign the tag to the article
547 $this->store->setTagToEntry($tag_id, $entry_id);
548 }
549 }
550 Tools::redirect();
551 break;
552 case 'remove_tag' :
553 $tag_id = $_GET['tag_id'];
554 $entry = $this->store->retrieveOneById($id, $this->user->getId());
555 if (!$entry) {
556 $this->messages->add('e', _('Article not found!'));
557 Tools::logm('error : article not found');
558 Tools::redirect();
559 }
560 $this->store->removeTagForEntry($id, $tag_id);
561 Tools::redirect();
562 break;
563 default:
564 break;
565 }
566 }
567
568 function displayView($view, $id = 0)
569 {
570 $tpl_vars = array();
571
572 switch ($view)
573 {
574 case 'config':
575 $dev_infos = $this->getPocheVersion('dev');
576 $dev = trim($dev_infos[0]);
577 $check_time_dev = date('d-M-Y H:i', $dev_infos[1]);
578 $prod_infos = $this->getPocheVersion('prod');
579 $prod = trim($prod_infos[0]);
580 $check_time_prod = date('d-M-Y H:i', $prod_infos[1]);
581 $compare_dev = version_compare(POCHE, $dev);
582 $compare_prod = version_compare(POCHE, $prod);
583 $themes = $this->getInstalledThemes();
584 $languages = $this->getInstalledLanguages();
585 $token = $this->user->getConfigValue('token');
586 $http_auth = (isset($_SERVER['PHP_AUTH_USER']) || isset($_SERVER['REMOTE_USER'])) ? true : false;
587 $only_user = ($this->store->listUsers() > 1) ? false : true;
588 $tpl_vars = array(
589 'themes' => $themes,
590 'languages' => $languages,
591 'dev' => $dev,
592 'prod' => $prod,
593 'check_time_dev' => $check_time_dev,
594 'check_time_prod' => $check_time_prod,
595 'compare_dev' => $compare_dev,
596 'compare_prod' => $compare_prod,
597 'token' => $token,
598 'user_id' => $this->user->getId(),
599 'http_auth' => $http_auth,
600 'only_user' => $only_user
601 );
602 Tools::logm('config view');
603 break;
604 case 'edit-tags':
605 # tags
606 $entry = $this->store->retrieveOneById($id, $this->user->getId());
607 if (!$entry) {
608 $this->messages->add('e', _('Article not found!'));
609 Tools::logm('error : article not found');
610 Tools::redirect();
611 }
612 $tags = $this->store->retrieveTagsByEntry($id);
613 $tpl_vars = array(
614 'entry_id' => $id,
615 'tags' => $tags,
616 'entry' => $entry,
617 );
618 break;
619 case 'tags':
620 $token = $this->user->getConfigValue('token');
621 //if term is set - search tags for this term
622 $term = Tools::checkVar('term');
623 $tags = $this->store->retrieveAllTags($this->user->getId(), $term);
624 if (Tools::isAjaxRequest()) {
625 $result = array();
626 foreach ($tags as $tag) {
627 $result[] = $tag['value'];
628 }
629 echo json_encode($result);
630 exit;
631 }
632 $tpl_vars = array(
633 'token' => $token,
634 'user_id' => $this->user->getId(),
635 'tags' => $tags,
636 );
637 break;
638 case 'search':
639 if (isset($_GET['search'])) {
640 $search = filter_var($_GET['search'], FILTER_SANITIZE_STRING);
641 $tpl_vars['entries'] = $this->store->search($search, $this->user->getId());
642 $count = count($tpl_vars['entries']);
643 $this->pagination->set_total($count);
644 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
645 $this->pagination->page_links('?view=' . $view . '?search=' . $search . '&sort=' . $_SESSION['sort'] . '&' ));
646 $tpl_vars['page_links'] = $page_links;
647 $tpl_vars['nb_results'] = $count;
648 $tpl_vars['search_term'] = $search;
649 }
650 break;
651 case 'view':
652 $entry = $this->store->retrieveOneById($id, $this->user->getId());
653 if ($entry != NULL) {
654 Tools::logm('view link #' . $id);
655 $content = $entry['content'];
656 if (function_exists('tidy_parse_string')) {
657 $tidy = tidy_parse_string($content, array('indent'=>true, 'show-body-only' => true), 'UTF8');
658 $tidy->cleanRepair();
659 $content = $tidy->value;
660 }
661
662 # flattr checking
663 $flattr = new FlattrItem();
664 $flattr->checkItem($entry['url'], $entry['id']);
665
666 # tags
667 $tags = $this->store->retrieveTagsByEntry($entry['id']);
668
669 $tpl_vars = array(
670 'entry' => $entry,
671 'content' => $content,
672 'flattr' => $flattr,
673 'tags' => $tags
674 );
675 }
676 else {
677 Tools::logm('error in view call : entry is null');
678 }
679 break;
680 default: # home, favorites, archive and tag views
681 $tpl_vars = array(
682 'entries' => '',
683 'page_links' => '',
684 'nb_results' => '',
685 'listmode' => (isset($_COOKIE['listmode']) ? true : false),
686 );
687
688 //if id is given - we retrive entries by tag: id is tag id
689 if ($id) {
690 $tpl_vars['tag'] = $this->store->retrieveTag($id, $this->user->getId());
691 $tpl_vars['id'] = intval($id);
692 }
693
694 $count = $this->store->getEntriesByViewCount($view, $this->user->getId(), $id);
695
696 if ($count > 0) {
697 $this->pagination->set_total($count);
698 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
699 $this->pagination->page_links('?view=' . $view . '&sort=' . $_SESSION['sort'] . (($id)?'&id='.$id:'') . '&' ));
700 $tpl_vars['entries'] = $this->store->getEntriesByView($view, $this->user->getId(), $this->pagination->get_limit(), $id);
701 $tpl_vars['page_links'] = $page_links;
702 $tpl_vars['nb_results'] = $count;
703 }
704 Tools::logm('display ' . $view . ' view');
705 break;
706 }
707
708 return $tpl_vars;
709 }
710
711 /**
712 * update the password of the current user.
713 * if MODE_DEMO is TRUE, the password can't be updated.
714 * @todo add the return value
715 * @todo set the new password in function header like this updatePassword($newPassword)
716 * @return boolean
717 */
718 public function updatePassword()
719 {
720 if (MODE_DEMO) {
721 $this->messages->add('i', _('in demo mode, you can\'t update your password'));
722 Tools::logm('in demo mode, you can\'t do this');
723 Tools::redirect('?view=config');
724 }
725 else {
726 if (isset($_POST['password']) && isset($_POST['password_repeat'])) {
727 if ($_POST['password'] == $_POST['password_repeat'] && $_POST['password'] != "") {
728 $this->messages->add('s', _('your password has been updated'));
729 $this->store->updatePassword($this->user->getId(), Tools::encodeString($_POST['password'] . $this->user->getUsername()));
730 Session::logout();
731 Tools::logm('password updated');
732 Tools::redirect();
733 }
734 else {
735 $this->messages->add('e', _('the two fields have to be filled & the password must be the same in the two fields'));
736 Tools::redirect('?view=config');
737 }
738 }
739 }
740 }
741
742 public function updateTheme()
743 {
744 # no data
745 if (empty($_POST['theme'])) {
746 }
747
748 # we are not going to change it to the current theme...
749 if ($_POST['theme'] == $this->getTheme()) {
750 $this->messages->add('w', _('still using the "' . $this->getTheme() . '" theme!'));
751 Tools::redirect('?view=config');
752 }
753
754 $themes = $this->getInstalledThemes();
755 $actualTheme = false;
756
757 foreach (array_keys($themes) as $theme) {
758 if ($theme == $_POST['theme']) {
759 $actualTheme = true;
760 break;
761 }
762 }
763
764 if (! $actualTheme) {
765 $this->messages->add('e', _('that theme does not seem to be installed'));
766 Tools::redirect('?view=config');
767 }
768
769 $this->store->updateUserConfig($this->user->getId(), 'theme', $_POST['theme']);
770 $this->messages->add('s', _('you have changed your theme preferences'));
771
772 $currentConfig = $_SESSION['poche_user']->config;
773 $currentConfig['theme'] = $_POST['theme'];
774
775 $_SESSION['poche_user']->setConfig($currentConfig);
776
777 $this->emptyCache();
778
779 Tools::redirect('?view=config');
780 }
781
782 public function updateLanguage()
783 {
784 # no data
785 if (empty($_POST['language'])) {
786 }
787
788 # we are not going to change it to the current language...
789 if ($_POST['language'] == $this->getLanguage()) {
790 $this->messages->add('w', _('still using the "' . $this->getLanguage() . '" language!'));
791 Tools::redirect('?view=config');
792 }
793
794 $languages = $this->getInstalledLanguages();
795 $actualLanguage = false;
796
797 foreach ($languages as $language) {
798 if ($language['value'] == $_POST['language']) {
799 $actualLanguage = true;
800 break;
801 }
802 }
803
804 if (! $actualLanguage) {
805 $this->messages->add('e', _('that language does not seem to be installed'));
806 Tools::redirect('?view=config');
807 }
808
809 $this->store->updateUserConfig($this->user->getId(), 'language', $_POST['language']);
810 $this->messages->add('s', _('you have changed your language preferences'));
811
812 $currentConfig = $_SESSION['poche_user']->config;
813 $currentConfig['language'] = $_POST['language'];
814
815 $_SESSION['poche_user']->setConfig($currentConfig);
816
817 $this->emptyCache();
818
819 Tools::redirect('?view=config');
820 }
821 /**
822 * get credentials from differents sources
823 * it redirects the user to the $referer link
824 * @return array
825 */
826 private function credentials() {
827 if(isset($_SERVER['PHP_AUTH_USER'])) {
828 return array($_SERVER['PHP_AUTH_USER'],'php_auth',true);
829 }
830 if(!empty($_POST['login']) && !empty($_POST['password'])) {
831 return array($_POST['login'],$_POST['password'],false);
832 }
833 if(isset($_SERVER['REMOTE_USER'])) {
834 return array($_SERVER['REMOTE_USER'],'http_auth',true);
835 }
836
837 return array(false,false,false);
838 }
839
840 /**
841 * checks if login & password are correct and save the user in session.
842 * it redirects the user to the $referer link
843 * @param string $referer the url to redirect after login
844 * @todo add the return value
845 * @return boolean
846 */
847 public function login($referer)
848 {
849 list($login,$password,$isauthenticated)=$this->credentials();
850 if($login === false || $password === false) {
851 $this->messages->add('e', _('login failed: you have to fill all fields'));
852 Tools::logm('login failed');
853 Tools::redirect();
854 }
855 if (!empty($login) && !empty($password)) {
856 $user = $this->store->login($login, Tools::encodeString($password . $login), $isauthenticated);
857 if ($user != array()) {
858 # Save login into Session
859 $longlastingsession = isset($_POST['longlastingsession']);
860 $passwordTest = ($isauthenticated) ? $user['password'] : Tools::encodeString($password . $login);
861 Session::login($user['username'], $user['password'], $login, $passwordTest, $longlastingsession, array('poche_user' => new User($user)));
862 $this->messages->add('s', _('welcome to your wallabag'));
863 Tools::logm('login successful');
864 Tools::redirect($referer);
865 }
866 $this->messages->add('e', _('login failed: bad login or password'));
867 Tools::logm('login failed');
868 Tools::redirect();
869 }
870 }
871
872 /**
873 * log out the poche user. It cleans the session.
874 * @todo add the return value
875 * @return boolean
876 */
877 public function logout()
878 {
879 $this->user = array();
880 Session::logout();
881 Tools::logm('logout');
882 Tools::redirect();
883 }
884
885 /**
886 * import datas into your poche
887 * @return boolean
888 */
889 public function import() {
890
891 if ( isset($_FILES['file']) ) {
892 Tools::logm('Import stated: parsing file');
893
894 // assume, that file is in json format
895 $str_data = file_get_contents($_FILES['file']['tmp_name']);
896 $data = json_decode($str_data, true);
897
898 if ( $data === null ) {
899 //not json - assume html
900 $html = new simple_html_dom();
901 $html->load_file($_FILES['file']['tmp_name']);
902 $data = array();
903 $read = 0;
904 foreach (array('ol','ul') as $list) {
905 foreach ($html->find($list) as $ul) {
906 foreach ($ul->find('li') as $li) {
907 $tmpEntry = array();
908 $a = $li->find('a');
909 $tmpEntry['url'] = $a[0]->href;
910 $tmpEntry['tags'] = $a[0]->tags;
911 $tmpEntry['is_read'] = $read;
912 if ($tmpEntry['url']) {
913 $data[] = $tmpEntry;
914 }
915 }
916 # the second <ol/ul> is for read links
917 $read = ((sizeof($data) && $read)?0:1);
918 }
919 }
920 }
921
922 //for readability structure
923 foreach ($data as $record) {
924 if (is_array($record)) {
925 $data[] = $record;
926 foreach ($record as $record2) {
927 if (is_array($record2)) {
928 $data[] = $record2;
929 }
930 }
931 }
932 }
933
934 $urlsInserted = array(); //urls of articles inserted
935 foreach ($data as $record) {
936 $url = trim( isset($record['article__url']) ? $record['article__url'] : (isset($record['url']) ? $record['url'] : '') );
937 if ( $url and !in_array($url, $urlsInserted) ) {
938 $title = (isset($record['title']) ? $record['title'] : _('Untitled - Import - ').'</a> <a href="./?import">'._('click to finish import').'</a><a>');
939 $body = (isset($record['content']) ? $record['content'] : '');
940 $isRead = (isset($record['is_read']) ? intval($record['is_read']) : (isset($record['archive'])?intval($record['archive']):0));
941 $isFavorite = (isset($record['is_fav']) ? intval($record['is_fav']) : (isset($record['favorite'])?intval($record['favorite']):0) );
942 //insert new record
943 $id = $this->store->add($url, $title, $body, $this->user->getId(), $isFavorite, $isRead);
944 if ( $id ) {
945 $urlsInserted[] = $url; //add
946
947 if ( isset($record['tags']) && trim($record['tags']) ) {
948 //@TODO: set tags
949
950 }
951 }
952 }
953 }
954
955 $i = sizeof($urlsInserted);
956 if ( $i > 0 ) {
957 $this->messages->add('s', _('Articles inserted: ').$i._('. Please note, that some may be marked as "read".'));
958 }
959 Tools::logm('Import of articles finished: '.$i.' articles added (w/o content if not provided).');
960 }
961 //file parsing finished here
962
963 //now download article contents if any
964
965 //check if we need to download any content
966 $recordsDownloadRequired = $this->store->retrieveUnfetchedEntriesCount($this->user->getId());
967 if ( $recordsDownloadRequired == 0 ) {
968 //nothing to download
969 $this->messages->add('s', _('Import finished.'));
970 Tools::logm('Import finished completely');
971 Tools::redirect();
972 }
973 else {
974 //if just inserted - don't download anything, download will start in next reload
975 if ( !isset($_FILES['file']) ) {
976 //download next batch
977 Tools::logm('Fetching next batch of articles...');
978 $items = $this->store->retrieveUnfetchedEntries($this->user->getId(), IMPORT_LIMIT);
979
980 $purifier = $this->getPurifier();
981
982 foreach ($items as $item) {
983 $url = new Url(base64_encode($item['url']));
984 Tools::logm('Fetching article '.$item['id']);
985 $content = Tools::getPageContent($url);
986
987 $title = (($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'));
988 $body = (($content['rss']['channel']['item']['description'] != '') ? $content['rss']['channel']['item']['description'] : _('Undefined'));
989
990 //clean content to prevent xss attack
991 $title = $purifier->purify($title);
992 $body = $purifier->purify($body);
993
994 $this->store->updateContentAndTitle($item['id'], $title, $body, $this->user->getId());
995 Tools::logm('Article '.$item['id'].' updated.');
996 }
997
998 }
999 }
1000
1001 return array('includeImport'=>true, 'import'=>array('recordsDownloadRequired'=>$recordsDownloadRequired, 'recordsUnderDownload'=> IMPORT_LIMIT, 'delay'=> IMPORT_DELAY * 1000) );
1002 }
1003
1004 /**
1005 * export poche entries in json
1006 * @return json all poche entries
1007 */
1008 public function export() {
1009 $filename = "wallabag-export-".$this->user->getId()."-".date("Y-m-d").".json";
1010 header('Content-Disposition: attachment; filename='.$filename);
1011
1012 $entries = $this->store->retrieveAll($this->user->getId());
1013 echo $this->tpl->render('export.twig', array(
1014 'export' => Tools::renderJson($entries),
1015 ));
1016 Tools::logm('export view');
1017 }
1018
1019 /**
1020 * Checks online the latest version of poche and cache it
1021 * @param string $which 'prod' or 'dev'
1022 * @return string latest $which version
1023 */
1024 private function getPocheVersion($which = 'prod') {
1025 $cache_file = CACHE . '/' . $which;
1026 $check_time = time();
1027
1028 # checks if the cached version file exists
1029 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
1030 $version = file_get_contents($cache_file);
1031 $check_time = filemtime($cache_file);
1032 } else {
1033 $version = file_get_contents('http://static.wallabag.org/versions/' . $which);
1034 file_put_contents($cache_file, $version, LOCK_EX);
1035 }
1036 return array($version, $check_time);
1037 }
1038
1039 public function generateToken()
1040 {
1041 if (ini_get('open_basedir') === '') {
1042 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1043 echo 'This is a server using Windows!';
1044 // alternative to /dev/urandom for Windows
1045 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1046 } else {
1047 $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
1048 }
1049 }
1050 else {
1051 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1052 }
1053
1054 $token = str_replace('+', '', $token);
1055 $this->store->updateUserConfig($this->user->getId(), 'token', $token);
1056 $currentConfig = $_SESSION['poche_user']->config;
1057 $currentConfig['token'] = $token;
1058 $_SESSION['poche_user']->setConfig($currentConfig);
1059 Tools::redirect();
1060 }
1061
1062 public function generateFeeds($token, $user_id, $tag_id, $type = 'home')
1063 {
1064 $allowed_types = array('home', 'fav', 'archive', 'tag');
1065 $config = $this->store->getConfigUser($user_id);
1066
1067 if ($config == null) {
1068 die(_('User with this id (' . $user_id . ') does not exist.'));
1069 }
1070
1071 if (!in_array($type, $allowed_types) ||
1072 $token != $config['token']) {
1073 die(_('Uh, there is a problem while generating feeds.'));
1074 }
1075 // Check the token
1076
1077 $feed = new FeedWriter(RSS2);
1078 $feed->setTitle('wallabag — ' . $type . ' feed');
1079 $feed->setLink(Tools::getPocheUrl());
1080 $feed->setChannelElement('pubDate', date(DATE_RSS , time()));
1081 $feed->setChannelElement('generator', 'wallabag');
1082 $feed->setDescription('wallabag ' . $type . ' elements');
1083
1084 if ($type == 'tag') {
1085 $entries = $this->store->retrieveEntriesByTag($tag_id, $user_id);
1086 }
1087 else {
1088 $entries = $this->store->getEntriesByView($type, $user_id);
1089 }
1090
1091 if (count($entries) > 0) {
1092 foreach ($entries as $entry) {
1093 $newItem = $feed->createNewItem();
1094 $newItem->setTitle($entry['title']);
1095 $newItem->setSource(Tools::getPocheUrl() . '?view=view&amp;id=' . $entry['id']);
1096 $newItem->setLink($entry['url']);
1097 $newItem->setDate(time());
1098 $newItem->setDescription($entry['content']);
1099 $feed->addItem($newItem);
1100 }
1101 }
1102
1103 $feed->genarateFeed();
1104 exit;
1105 }
1106
1107 public function emptyCache() {
1108 $files = new RecursiveIteratorIterator(
1109 new RecursiveDirectoryIterator(CACHE, RecursiveDirectoryIterator::SKIP_DOTS),
1110 RecursiveIteratorIterator::CHILD_FIRST
1111 );
1112
1113 foreach ($files as $fileinfo) {
1114 $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
1115 $todo($fileinfo->getRealPath());
1116 }
1117
1118 Tools::logm('empty cache');
1119 $this->messages->add('s', _('Cache deleted.'));
1120 Tools::redirect();
1121 }
1122
1123 /**
1124 * return new purifier object with actual config
1125 */
1126 protected function getPurifier() {
1127 $config = HTMLPurifier_Config::createDefault();
1128 $config->set('Cache.SerializerPath', CACHE);
1129 $config->set('HTML.SafeIframe', true);
1130 $config->set('URI.SafeIframeRegexp', '%^(https?:)?//(www\.youtube(?:-nocookie)?\.com/embed/|player\.vimeo\.com/video/)%'); //allow YouTube and Vimeo$purifier = new HTMLPurifier($config);
1131
1132 return new HTMLPurifier($config);
1133 }
1134
1135 /**
1136 * handle epub
1137 */
1138 public function createEpub() {
1139
1140 switch ($_GET['method']) {
1141 case 'id':
1142 $entryID = filter_var($_GET['id'],FILTER_SANITIZE_NUMBER_INT);
1143 $entry = $this->store->retrieveOneById($entryID, $this->user->getId());
1144 $entries = array($entry);
1145 $bookTitle = $entry['title'];
1146 break;
1147 case 'all':
1148 $entries = $this->store->retrieveAll($this->user->getId());
1149 $bookTitle = _('All my articles');
1150 break;
1151 case 'tag':
1152 $tag = filter_var($_GET['tag'],FILTER_SANITIZE_STRING);
1153 $tags_id = $this->store->retrieveAllTags($this->user->getId(),$tag);
1154 $tag_id = $tags_id[0]["id"]; // we take the first result, which is supposed to match perfectly. There must be a workaround.
1155 $entries = $this->store->retrieveEntriesByTag($tag_id,$this->user->getId());
1156 $bookTitle = sprintf(_('Articles related to %s'),$tag);
1157 break;
1158 case 'category':
1159 $category = filter_var($_GET['category'],FILTER_SANITIZE_STRING);
1160 $entries = $this->store->getEntriesByView($category,$this->user->getId());
1161 $bookTitle = sprintf(_('All my articles in category %s'), $category);
1162 break;
1163 case 'search':
1164 $search = filter_var($_GET['search'],FILTER_SANITIZE_STRING);
1165 $entries = $this->store->search($search,$this->user->getId());
1166 $bookTitle = sprintf(_('All my articles for search %s'), $search);
1167 break;
1168 case 'default':
1169 die(_('Uh, there is a problem while generating epub.'));
1170
1171 }
1172
1173 $content_start =
1174 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1175 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
1176 . "<head>"
1177 . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
1178 . "<title>wallabag articles book</title>\n"
1179 . "</head>\n"
1180 . "<body>\n";
1181
1182 $bookEnd = "</body>\n</html>\n";
1183
1184 $log = new Logger("wallabag", TRUE);
1185 $fileDir = CACHE;
1186
1187
1188 $book = new EPub(EPub::BOOK_VERSION_EPUB3);
1189 $log->logLine("new EPub()");
1190 $log->logLine("EPub class version: " . EPub::VERSION);
1191 $log->logLine("EPub Req. Zip version: " . EPub::REQ_ZIP_VERSION);
1192 $log->logLine("Zip version: " . Zip::VERSION);
1193 $log->logLine("getCurrentServerURL: " . $book->getCurrentServerURL());
1194 $log->logLine("getCurrentPageURL..: " . $book->getCurrentPageURL());
1195
1196 $book->setTitle(_('wallabag\'s articles'));
1197 $book->setIdentifier("http://$_SERVER[HTTP_HOST]", EPub::IDENTIFIER_URI); // Could also be the ISBN number, prefered for published books, or a UUID.
1198 //$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.
1199 $book->setDescription(_("Some articles saved on my wallabag"));
1200 $book->setAuthor("wallabag","wallabag");
1201 $book->setPublisher("wallabag","wallabag"); // I hope this is a non existant address :)
1202 $book->setDate(time()); // Strictly not needed as the book date defaults to time().
1203 //$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.
1204 $book->setSourceURL("http://$_SERVER[HTTP_HOST]");
1205
1206 $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "PHP");
1207 $book->addDublinCoreMetadata(DublinCore::CONTRIBUTOR, "wallabag");
1208
1209 $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";
1210
1211 $log->logLine("Add Cover");
1212
1213 $fullTitle = "<h1> " . $bookTitle . "</h1>\n";
1214
1215 $book->setCoverImage("Cover.png", file_get_contents("themes/baggy/img/apple-touch-icon-152.png"), "image/png", $fullTitle);
1216
1217 $cover = $content_start . _('<span style="text-align:center;display:block;">Produced by wallabag with PHPePub</span>') . $bookEnd;
1218
1219 //$book->addChapter("Table of Contents", "TOC.xhtml", NULL, false, EPub::EXTERNAL_REF_IGNORE);
1220 $book->addChapter("Notices", "Cover2.html", $cover);
1221
1222 $book->buildTOC();
1223
1224 foreach ($entries as $entry) {
1225 $tags = $this->store->retrieveTagsByEntry($entry['id']);
1226 foreach ($tags as $tag) {
1227 $book->setSubject($tag['value']);
1228 }
1229
1230 $log->logLine("Set up parameters");
1231
1232 $chapter = $content_start . $entry['content'] . $bookEnd;
1233 $book->addChapter($entry['title'], htmlspecialchars($entry['title']) . ".html", $chapter, true, EPub::EXTERNAL_REF_ADD);
1234 $log->logLine("Added chapter " . $entry['title']);
1235 }
1236
1237 if (DEBUG_POCHE) {
1238 $epuplog = $book->getLog();
1239 $book->addChapter("Log", "Log.html", $content_start . $log->getLog() . "\n</pre>" . $bookEnd); // generation log
1240 // Only used in case we need to debug EPub.php.
1241 //$book->addChapter("ePubLog", "ePubLog.html", $content_start . $epuplog . "\n</pre>" . $bookEnd);
1242 }
1243 $book->finalize();
1244 $zipData = $book->sendBook(_('wallabag\'s articles'));
1245 }
1246 }