]> git.immae.eu Git - github/wallabag/wallabag.git/blob - inc/poche/Poche.class.php
fix bug #225: blank page on article page
[github/wallabag/wallabag.git] / inc / poche / Poche.class.php
1 <?php
2 /**
3 * poche, a read it later open source system
4 *
5 * @category poche
6 * @author Nicolas LÅ“uillet <support@inthepoche.com>
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 $notInstalledMessage = '';
24
25 # @todo make this dynamic (actually install themes and save them in the database including author information et cetera)
26 private $installedThemes = array(
27 'default' => array('requires' => array()),
28 'dark' => array('requires' => array('default')),
29 'dmagenta' => array('requires' => array('default')),
30 'solarized' => array('requires' => array('default')),
31 'solarized-dark' => array('requires' => array('default'))
32 );
33
34 public function __construct()
35 {
36 if (! $this->configFileIsAvailable()) {
37 return;
38 }
39
40 $this->init();
41
42 if (! $this->themeIsInstalled()) {
43 return;
44 }
45
46 $this->initTpl();
47
48 if (! $this->systemIsInstalled()) {
49 return;
50 }
51
52 $this->store = new Database();
53 $this->messages = new Messages();
54
55 # installation
56 if (! $this->store->isInstalled()) {
57 $this->install();
58 }
59 }
60
61 private function init()
62 {
63 Tools::initPhp();
64 Session::$sessionName = 'poche';
65 Session::init();
66
67 if (isset($_SESSION['poche_user']) && $_SESSION['poche_user'] != array()) {
68 $this->user = $_SESSION['poche_user'];
69 } else {
70 # fake user, just for install & login screens
71 $this->user = new User();
72 $this->user->setConfig($this->getDefaultConfig());
73 }
74
75 # l10n
76 $language = $this->user->getConfigValue('language');
77 putenv('LC_ALL=' . $language);
78 setlocale(LC_ALL, $language);
79 bindtextdomain($language, LOCALE);
80 textdomain($language);
81
82 # Pagination
83 $this->pagination = new Paginator($this->user->getConfigValue('pager'), 'p');
84
85 # Set up theme
86 $themeDirectory = $this->user->getConfigValue('theme');
87
88 if ($themeDirectory === false) {
89 $themeDirectory = DEFAULT_THEME;
90 }
91
92 $this->currentTheme = $themeDirectory;
93 }
94
95 public function configFileIsAvailable() {
96 if (! self::$configFileAvailable) {
97 $this->notInstalledMessage = 'You have to rename <strong>inc/poche/config.inc.php.new</strong> to <strong>inc/poche/config.inc.php</strong>.';
98
99 return false;
100 }
101
102 return true;
103 }
104
105 public function themeIsInstalled() {
106 # Twig is an absolute requirement for Poche to function. Abort immediately if the Composer installer hasn't been run yet
107 if (! self::$canRenderTemplates) {
108 $this->notInstalledMessage = 'Twig does not seem to be installed. Please initialize the Composer installation to automatically fetch dependencies. Have a look at <a href="http://inthepoche.com/?pages/Documentation">the documentation.</a>';
109
110 return false;
111 }
112
113 # Check if the selected theme and its requirements are present
114 if (! is_dir(THEME . '/' . $this->getTheme())) {
115 $this->notInstalledMessage = 'The currently selected theme (' . $this->getTheme() . ') does not seem to be properly installed (Missing directory: ' . THEME . '/' . $this->getTheme() . ')';
116
117 self::$canRenderTemplates = false;
118
119 return false;
120 }
121
122 foreach ($this->installedThemes[$this->getTheme()]['requires'] as $requiredTheme) {
123 if (! is_dir(THEME . '/' . $requiredTheme)) {
124 $this->notInstalledMessage = 'The required "' . $requiredTheme . '" theme is missing for the current theme (' . $this->getTheme() . ')';
125
126 self::$canRenderTemplates = false;
127
128 return false;
129 }
130 }
131
132 return true;
133 }
134
135 /**
136 * all checks before installation.
137 * @todo move HTML to template
138 * @return boolean
139 */
140 public function systemIsInstalled()
141 {
142 $msg = '';
143
144 $configSalt = defined('SALT') ? constant('SALT') : '';
145
146 if (empty($configSalt)) {
147 $msg = '<h1>error</h1><p>You have not yet filled in the SALT value in the config.inc.php file.</p>';
148 } else if (! is_writable(CACHE)) {
149 Tools::logm('you don\'t have write access on cache directory');
150 $msg = '<h1>error</h1><p>You don\'t have write access on cache directory.</p>';
151 } else if (STORAGE == 'sqlite' && ! file_exists(STORAGE_SQLITE)) {
152 Tools::logm('sqlite file doesn\'t exist');
153 $msg = '<h1>error</h1><p>sqlite file doesn\'t exist, you can find it in install folder. Copy it in /db folder.</p>';
154 } else if (file_exists(ROOT . '/install/update.php') && ! DEBUG_POCHE) {
155 $msg = '<h1>setup</h1><p><strong>It\'s your first time here?</strong> Please copy /install/poche.sqlite in db folder. Then, delete install folder.<br /><strong>If you have already installed poche</strong>, an update is needed <a href="install/update.php">by clicking here</a>.</p>';
156 } else if (is_dir(ROOT . '/install') && ! DEBUG_POCHE) {
157 $msg = '<h1>setup</h1><p><strong>If you want to update your poche</strong>, you just have to delete /install folder. <br /><strong>To install your poche with sqlite</strong>, copy /install/poche.sqlite in /db and delete the folder /install. you have to delete the /install folder before using poche.</p>';
158 } else if (STORAGE == 'sqlite' && ! is_writable(STORAGE_SQLITE)) {
159 Tools::logm('you don\'t have write access on sqlite file');
160 $msg = '<h1>error</h1><p>You don\'t have write access on sqlite file.</p>';
161 }
162
163 if (! empty($msg)) {
164 $this->notInstalledMessage = $msg;
165
166 return false;
167 }
168
169 return true;
170 }
171
172 public function getNotInstalledMessage() {
173 return $this->notInstalledMessage;
174 }
175
176 private function initTpl()
177 {
178 $loaderChain = new Twig_Loader_Chain();
179
180 # add the current theme as first to the loader chain so Twig will look there first for overridden template files
181 try {
182 $loaderChain->addLoader(new Twig_Loader_Filesystem(THEME . '/' . $this->getTheme()));
183 } catch (Twig_Error_Loader $e) {
184 # @todo isInstalled() should catch this, inject Twig later
185 die('The currently selected theme (' . $this->getTheme() . ') does not seem to be properly installed (' . THEME . '/' . $this->getTheme() .' is missing)');
186 }
187
188 # add all required themes to the loader chain
189 foreach ($this->installedThemes[$this->getTheme()]['requires'] as $requiredTheme) {
190 try {
191 $loaderChain->addLoader(new Twig_Loader_Filesystem(THEME . '/' . DEFAULT_THEME));
192 } catch (Twig_Error_Loader $e) {
193 # @todo isInstalled() should catch this, inject Twig later
194 die('The required "' . $requiredTheme . '" theme is missing for the current theme (' . $this->getTheme() . ')');
195 }
196 }
197
198 if (DEBUG_POCHE) {
199 $twig_params = array();
200 } else {
201 $twig_params = array('cache' => CACHE);
202 }
203
204 $this->tpl = new Twig_Environment($loaderChain, $twig_params);
205 $this->tpl->addExtension(new Twig_Extensions_Extension_I18n());
206
207 # filter to display domain name of an url
208 $filter = new Twig_SimpleFilter('getDomain', 'Tools::getDomain');
209 $this->tpl->addFilter($filter);
210
211 # filter for reading time
212 $filter = new Twig_SimpleFilter('getReadingTime', 'Tools::getReadingTime');
213 $this->tpl->addFilter($filter);
214
215 # filter for simple filenames in config view
216 $filter = new Twig_SimpleFilter('getPrettyFilename', function($string) { return str_replace(ROOT, '', $string); });
217 $this->tpl->addFilter($filter);
218 }
219
220 private function install()
221 {
222 Tools::logm('poche still not installed');
223 echo $this->tpl->render('install.twig', array(
224 'token' => Session::getToken(),
225 'theme' => $this->getTheme(),
226 'poche_url' => Tools::getPocheUrl()
227 ));
228 if (isset($_GET['install'])) {
229 if (($_POST['password'] == $_POST['password_repeat'])
230 && $_POST['password'] != "" && $_POST['login'] != "") {
231 # let's rock, install poche baby !
232 if ($this->store->install($_POST['login'], Tools::encodeString($_POST['password'] . $_POST['login'])))
233 {
234 Session::logout();
235 Tools::logm('poche is now installed');
236 Tools::redirect();
237 }
238 }
239 else {
240 Tools::logm('error during installation');
241 Tools::redirect();
242 }
243 }
244 exit();
245 }
246
247 public function getTheme() {
248 return $this->currentTheme;
249 }
250
251 public function getInstalledThemes() {
252 $handle = opendir(THEME);
253 $themes = array();
254
255 while (($theme = readdir($handle)) !== false) {
256 # Themes are stored in a directory, so all directory names are themes
257 # @todo move theme installation data to database
258 if (! is_dir(THEME . '/' . $theme) || in_array($theme, array('..', '.'))) {
259 continue;
260 }
261
262 $current = false;
263
264 if ($theme === $this->getTheme()) {
265 $current = true;
266 }
267
268 $themes[] = array('name' => $theme, 'current' => $current);
269 }
270
271 return $themes;
272 }
273
274 public function getDefaultConfig()
275 {
276 return array(
277 'pager' => PAGINATION,
278 'language' => LANG,
279 'theme' => DEFAULT_THEME
280 );
281 }
282
283 /**
284 * Call action (mark as fav, archive, delete, etc.)
285 */
286 public function action($action, Url $url, $id = 0, $import = FALSE)
287 {
288 switch ($action)
289 {
290 case 'add':
291 $content = $url->extract();
292
293 if ($this->store->add($url->getUrl(), $content['title'], $content['body'], $this->user->getId())) {
294 Tools::logm('add link ' . $url->getUrl());
295 $sequence = '';
296 if (STORAGE == 'postgres') {
297 $sequence = 'entries_id_seq';
298 }
299 $last_id = $this->store->getLastId($sequence);
300 if (DOWNLOAD_PICTURES) {
301 $content = filtre_picture($content['body'], $url->getUrl(), $last_id);
302 Tools::logm('updating content article');
303 $this->store->updateContent($last_id, $content, $this->user->getId());
304 }
305 if (!$import) {
306 $this->messages->add('s', _('the link has been added successfully'));
307 }
308 }
309 else {
310 if (!$import) {
311 $this->messages->add('e', _('error during insertion : the link wasn\'t added'));
312 Tools::logm('error during insertion : the link wasn\'t added ' . $url->getUrl());
313 }
314 }
315
316 if (!$import) {
317 Tools::redirect('?view=home');
318 }
319 break;
320 case 'delete':
321 $msg = 'delete link #' . $id;
322 if ($this->store->deleteById($id, $this->user->getId())) {
323 if (DOWNLOAD_PICTURES) {
324 remove_directory(ABS_PATH . $id);
325 }
326 $this->messages->add('s', _('the link has been deleted successfully'));
327 }
328 else {
329 $this->messages->add('e', _('the link wasn\'t deleted'));
330 $msg = 'error : can\'t delete link #' . $id;
331 }
332 Tools::logm($msg);
333 Tools::redirect('?');
334 break;
335 case 'toggle_fav' :
336 $this->store->favoriteById($id, $this->user->getId());
337 Tools::logm('mark as favorite link #' . $id);
338 if (!$import) {
339 Tools::redirect();
340 }
341 break;
342 case 'toggle_archive' :
343 $this->store->archiveById($id, $this->user->getId());
344 Tools::logm('archive link #' . $id);
345 if (!$import) {
346 Tools::redirect();
347 }
348 break;
349 default:
350 break;
351 }
352 }
353
354 function displayView($view, $id = 0)
355 {
356 $tpl_vars = array();
357
358 switch ($view)
359 {
360 case 'config':
361 $dev = $this->getPocheVersion('dev');
362 $prod = $this->getPocheVersion('prod');
363 $compare_dev = version_compare(POCHE_VERSION, $dev);
364 $compare_prod = version_compare(POCHE_VERSION, $prod);
365 $themes = $this->getInstalledThemes();
366 $tpl_vars = array(
367 'themes' => $themes,
368 'dev' => $dev,
369 'prod' => $prod,
370 'compare_dev' => $compare_dev,
371 'compare_prod' => $compare_prod,
372 );
373 Tools::logm('config view');
374 break;
375 case 'view':
376 $entry = $this->store->retrieveOneById($id, $this->user->getId());
377 if ($entry != NULL) {
378 Tools::logm('view link #' . $id);
379 $content = $entry['content'];
380 if (function_exists('tidy_parse_string')) {
381 $tidy = tidy_parse_string($content, array('indent'=>true, 'show-body-only' => true), 'UTF8');
382 $tidy->cleanRepair();
383 $content = $tidy->value;
384 }
385
386 # flattr checking
387 $flattr = new FlattrItem();
388 $flattr->checkItem($entry['url']);
389
390 $tpl_vars = array(
391 'entry' => $entry,
392 'content' => $content,
393 'flattr' => $flattr
394 );
395 }
396 else {
397 Tools::logm('error in view call : entry is null');
398 }
399 break;
400 default: # home, favorites and archive views
401 $entries = $this->store->getEntriesByView($view, $this->user->getId());
402 $tpl_vars = array(
403 'entries' => '',
404 'page_links' => '',
405 'nb_results' => '',
406 );
407 if (count($entries) > 0) {
408 $this->pagination->set_total(count($entries));
409 $page_links = $this->pagination->page_links('?view=' . $view . '&sort=' . $_SESSION['sort'] . '&');
410 $datas = $this->store->getEntriesByView($view, $this->user->getId(), $this->pagination->get_limit());
411 $tpl_vars['entries'] = $datas;
412 $tpl_vars['page_links'] = $page_links;
413 $tpl_vars['nb_results'] = count($entries);
414 }
415 Tools::logm('display ' . $view . ' view');
416 break;
417 }
418
419 return $tpl_vars;
420 }
421
422 /**
423 * update the password of the current user.
424 * if MODE_DEMO is TRUE, the password can't be updated.
425 * @todo add the return value
426 * @todo set the new password in function header like this updatePassword($newPassword)
427 * @return boolean
428 */
429 public function updatePassword()
430 {
431 if (MODE_DEMO) {
432 $this->messages->add('i', _('in demo mode, you can\'t update your password'));
433 Tools::logm('in demo mode, you can\'t do this');
434 Tools::redirect('?view=config');
435 }
436 else {
437 if (isset($_POST['password']) && isset($_POST['password_repeat'])) {
438 if ($_POST['password'] == $_POST['password_repeat'] && $_POST['password'] != "") {
439 $this->messages->add('s', _('your password has been updated'));
440 $this->store->updatePassword($this->user->getId(), Tools::encodeString($_POST['password'] . $this->user->getUsername()));
441 Session::logout();
442 Tools::logm('password updated');
443 Tools::redirect();
444 }
445 else {
446 $this->messages->add('e', _('the two fields have to be filled & the password must be the same in the two fields'));
447 Tools::redirect('?view=config');
448 }
449 }
450 }
451 }
452
453 public function updateTheme()
454 {
455 # no data
456 if (empty($_POST['theme'])) {
457 }
458
459 # we are not going to change it to the current theme...
460 if ($_POST['theme'] == $this->getTheme()) {
461 $this->messages->add('w', _('still using the "' . $this->getTheme() . '" theme!'));
462 Tools::redirect('?view=config');
463 }
464
465 $themes = $this->getInstalledThemes();
466 $actualTheme = false;
467
468 foreach ($themes as $theme) {
469 if ($theme['name'] == $_POST['theme']) {
470 $actualTheme = true;
471 break;
472 }
473 }
474
475 if (! $actualTheme) {
476 $this->messages->add('e', _('that theme does not seem to be installed'));
477 Tools::redirect('?view=config');
478 }
479
480 $this->store->updateUserConfig($this->user->getId(), 'theme', $_POST['theme']);
481 $this->messages->add('s', _('you have changed your theme preferences'));
482
483 $currentConfig = $_SESSION['poche_user']->config;
484 $currentConfig['theme'] = $_POST['theme'];
485
486 $_SESSION['poche_user']->setConfig($currentConfig);
487
488 Tools::redirect('?view=config');
489 }
490
491 /**
492 * checks if login & password are correct and save the user in session.
493 * it redirects the user to the $referer link
494 * @param string $referer the url to redirect after login
495 * @todo add the return value
496 * @return boolean
497 */
498 public function login($referer)
499 {
500 if (!empty($_POST['login']) && !empty($_POST['password'])) {
501 $user = $this->store->login($_POST['login'], Tools::encodeString($_POST['password'] . $_POST['login']));
502 if ($user != array()) {
503 # Save login into Session
504 Session::login($user['username'], $user['password'], $_POST['login'], Tools::encodeString($_POST['password'] . $_POST['login']), array('poche_user' => new User($user)));
505 $this->messages->add('s', _('welcome to your poche'));
506 Tools::logm('login successful');
507 Tools::redirect($referer);
508 }
509 $this->messages->add('e', _('login failed: bad login or password'));
510 Tools::logm('login failed');
511 Tools::redirect();
512 } else {
513 $this->messages->add('e', _('login failed: you have to fill all fields'));
514 Tools::logm('login failed');
515 Tools::redirect();
516 }
517 }
518
519 /**
520 * log out the poche user. It cleans the session.
521 * @todo add the return value
522 * @return boolean
523 */
524 public function logout()
525 {
526 $this->user = array();
527 Session::logout();
528 $this->messages->add('s', _('see you soon!'));
529 Tools::logm('logout');
530 Tools::redirect();
531 }
532
533 /**
534 * import from Instapaper. poche needs a ./instapaper-export.html file
535 * @todo add the return value
536 * @param string $targetFile the file used for importing
537 * @return boolean
538 */
539 private function importFromInstapaper($targetFile)
540 {
541 # TODO gestion des articles favs
542 $html = new simple_html_dom();
543 $html->load_file($targetFile);
544 Tools::logm('starting import from instapaper');
545
546 $read = 0;
547 $errors = array();
548 foreach($html->find('ol') as $ul)
549 {
550 foreach($ul->find('li') as $li)
551 {
552 $a = $li->find('a');
553 $url = new Url(base64_encode($a[0]->href));
554 $this->action('add', $url, 0, TRUE);
555 if ($read == '1') {
556 $sequence = '';
557 if (STORAGE == 'postgres') {
558 $sequence = 'entries_id_seq';
559 }
560 $last_id = $this->store->getLastId($sequence);
561 $this->action('toggle_archive', $url, $last_id, TRUE);
562 }
563 }
564
565 # the second <ol> is for read links
566 $read = 1;
567 }
568 $this->messages->add('s', _('import from instapaper completed'));
569 Tools::logm('import from instapaper completed');
570 Tools::redirect();
571 }
572
573 /**
574 * import from Pocket. poche needs a ./ril_export.html file
575 * @todo add the return value
576 * @param string $targetFile the file used for importing
577 * @return boolean
578 */
579 private function importFromPocket($targetFile)
580 {
581 # TODO gestion des articles favs
582 $html = new simple_html_dom();
583 $html->load_file($targetFile);
584 Tools::logm('starting import from pocket');
585
586 $read = 0;
587 $errors = array();
588 foreach($html->find('ul') as $ul)
589 {
590 foreach($ul->find('li') as $li)
591 {
592 $a = $li->find('a');
593 $url = new Url(base64_encode($a[0]->href));
594 $this->action('add', $url, 0, TRUE);
595 if ($read == '1') {
596 $sequence = '';
597 if (STORAGE == 'postgres') {
598 $sequence = 'entries_id_seq';
599 }
600 $last_id = $this->store->getLastId($sequence);
601 $this->action('toggle_archive', $url, $last_id, TRUE);
602 }
603 }
604
605 # the second <ul> is for read links
606 $read = 1;
607 }
608 $this->messages->add('s', _('import from pocket completed'));
609 Tools::logm('import from pocket completed');
610 Tools::redirect();
611 }
612
613 /**
614 * import from Readability. poche needs a ./readability file
615 * @todo add the return value
616 * @param string $targetFile the file used for importing
617 * @return boolean
618 */
619 private function importFromReadability($targetFile)
620 {
621 # TODO gestion des articles lus / favs
622 $str_data = file_get_contents($targetFile);
623 $data = json_decode($str_data,true);
624 Tools::logm('starting import from Readability');
625 $count = 0;
626 foreach ($data as $key => $value) {
627 $url = NULL;
628 $favorite = FALSE;
629 $archive = FALSE;
630 foreach ($value as $attr => $attr_value) {
631 if ($attr == 'article__url') {
632 $url = new Url(base64_encode($attr_value));
633 }
634 $sequence = '';
635 if (STORAGE == 'postgres') {
636 $sequence = 'entries_id_seq';
637 }
638 if ($attr_value == 'true') {
639 if ($attr == 'favorite') {
640 $favorite = TRUE;
641 }
642 if ($attr == 'archive') {
643 $archive = TRUE;
644 }
645 }
646 }
647 # we can add the url
648 if (!is_null($url) && $url->isCorrect()) {
649 $this->action('add', $url, 0, TRUE);
650 $count++;
651 if ($favorite) {
652 $last_id = $this->store->getLastId($sequence);
653 $this->action('toggle_fav', $url, $last_id, TRUE);
654 }
655 if ($archive) {
656 $last_id = $this->store->getLastId($sequence);
657 $this->action('toggle_archive', $url, $last_id, TRUE);
658 }
659 }
660 }
661 $this->messages->add('s', _('import from Readability completed. ' . $count . ' new links.'));
662 Tools::logm('import from Readability completed');
663 Tools::redirect();
664 }
665
666 /**
667 * import datas into your poche
668 * @param string $from name of the service to import : pocket, instapaper or readability
669 * @todo add the return value
670 * @return boolean
671 */
672 public function import($from)
673 {
674 $providers = array(
675 'pocket' => 'importFromPocket',
676 'readability' => 'importFromReadability',
677 'instapaper' => 'importFromInstapaper'
678 );
679
680 if (! isset($providers[$from])) {
681 $this->messages->add('e', _('Unknown import provider.'));
682 Tools::redirect();
683 }
684
685 $targetDefinition = 'IMPORT_' . strtoupper($from) . '_FILE';
686 $targetFile = constant($targetDefinition);
687
688 if (! defined($targetDefinition)) {
689 $this->messages->add('e', _('Incomplete inc/poche/define.inc.php file, please define "' . $targetDefinition . '".'));
690 Tools::redirect();
691 }
692
693 if (! file_exists($targetFile)) {
694 $this->messages->add('e', _('Could not find required "' . $targetFile . '" import file.'));
695 Tools::redirect();
696 }
697
698 $this->$providers[$from]($targetFile);
699 }
700
701 /**
702 * export poche entries in json
703 * @return json all poche entries
704 */
705 public function export()
706 {
707 $entries = $this->store->retrieveAll($this->user->getId());
708 echo $this->tpl->render('export.twig', array(
709 'export' => Tools::renderJson($entries),
710 ));
711 Tools::logm('export view');
712 }
713
714 /**
715 * Checks online the latest version of poche and cache it
716 * @param string $which 'prod' or 'dev'
717 * @return string latest $which version
718 */
719 private function getPocheVersion($which = 'prod')
720 {
721 $cache_file = CACHE . '/' . $which;
722
723 # checks if the cached version file exists
724 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
725 $version = file_get_contents($cache_file);
726 } else {
727 $version = file_get_contents('http://static.inthepoche.com/versions/' . $which);
728 file_put_contents($cache_file, $version, LOCK_EX);
729 }
730 return $version;
731 }
732 }