3 * wallabag, self hostable application allowing you to not miss any content anymore
6 * @author Nicolas Lœuillet <nicolas@loeuillet.org>
8 * @license http://www.wtfpl.net/ see COPYING file
13 public static $canRenderTemplates = true;
14 public static $configFileAvailable = true;
22 private $currentTheme = '';
23 private $currentLanguage = '';
24 private $notInstalledMessage = array();
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' => 'Українська',
40 public function __construct()
42 if ($this->configFileIsAvailable()) {
46 if ($this->themeIsInstalled()) {
50 if ($this->systemIsInstalled()) {
51 $this->store
= new Database();
52 $this->messages
= new Messages();
54 if (! $this->store
->isInstalled()) {
57 $this->store
->checkTags();
61 private function init()
65 if (isset($_SESSION['poche_user']) && $_SESSION['poche_user'] != array()) {
66 $this->user
= $_SESSION['poche_user'];
68 # fake user, just for install & login screens
69 $this->user
= new User();
70 $this->user
->setConfig($this->getDefaultConfig());
74 $language = $this->user
->getConfigValue('language');
75 @putenv('LC_ALL=' . $language);
76 setlocale(LC_ALL
, $language);
77 bindtextdomain($language, LOCALE
);
78 textdomain($language);
81 $this->pagination
= new Paginator($this->user
->getConfigValue('pager'), 'p');
84 $themeDirectory = $this->user
->getConfigValue('theme');
86 if ($themeDirectory === false) {
87 $themeDirectory = DEFAULT_THEME
;
90 $this->currentTheme
= $themeDirectory;
93 $languageDirectory = $this->user
->getConfigValue('language');
95 if ($languageDirectory === false) {
96 $languageDirectory = DEFAULT_THEME
;
99 $this->currentLanguage
= $languageDirectory;
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.';
112 public function themeIsInstalled() {
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.';
120 if (! is_writable(CACHE
)) {
121 $this->notInstalledMessage
[] = 'You don\'t have write access on cache directory.';
123 self
::$canRenderTemplates = false;
128 # Check if the selected theme and its requirements are present
129 $theme = $this->getTheme();
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 . ')';
134 self
::$canRenderTemplates = false;
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 . ')';
145 self
::$canRenderTemplates = false;
161 * all checks before installation.
162 * @todo move HTML to template
165 public function systemIsInstalled()
169 $configSalt = defined('SALT') ? constant('SALT') : '';
171 if (empty($configSalt)) {
172 $this->notInstalledMessage
[] = 'You have not yet filled in the SALT value in the config.inc.php file.';
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.';
180 if (is_dir(ROOT
. '/install') && ! DEBUG_POCHE
) {
181 $this->notInstalledMessage
[] = 'you have to delete the /install folder before using poche.';
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.';
197 public function getNotInstalledMessage() {
198 return $this->notInstalledMessage
;
201 private function initTpl()
203 $loaderChain = new Twig_Loader_Chain();
204 $theme = $this->getTheme();
206 # add the current theme as first to the loader chain so Twig will look there first for overridden template files
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)');
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) {
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 . ')');
228 $twigParams = array();
230 $twigParams = array('cache' => CACHE
);
233 $this->tpl
= new Twig_Environment($loaderChain, $twigParams);
234 $this->tpl
->addExtension(new Twig_Extensions_Extension_I18n());
236 # filter to display domain name of an url
237 $filter = new Twig_SimpleFilter('getDomain', 'Tools::getDomain');
238 $this->tpl
->addFilter($filter);
240 # filter for reading time
241 $filter = new Twig_SimpleFilter('getReadingTime', 'Tools::getReadingTime');
242 $this->tpl
->addFilter($filter);
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));
256 Tools
::logm('error during adding new user');
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 !');
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 !');
281 Tools
::logm('logout');
283 $this->messages
->add('s', sprintf(_('User %s has been successfully deleted !'),$newusername));
286 Tools
::logm('Bad password !');
287 $this->messages
->add('e', _('Error : The password is wrong !'));
291 Tools
::logm('Only user !');
292 $this->messages
->add('e', _('Error : You are the only user, you cannot delete your account !'));
297 private function install()
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()
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'])))
312 Tools
::logm('poche is now installed');
317 Tools
::logm('error during installation');
324 public function getTheme() {
325 return $this->currentTheme
;
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.
334 * @param string $theme Theme key (directory name)
335 * @return array|boolean Theme information, or false if the theme doesn't exist.
337 public function getThemeInfo($theme) {
338 if (!is_dir(THEME
. '/' . $theme)) {
342 $themeIniFile = THEME
. '/' . $theme . '/theme.ini';
343 $themeInfo = array();
345 if (is_file($themeIniFile) && is_readable($themeIniFile)) {
346 $themeInfo = parse_ini_file($themeIniFile);
349 if ($themeInfo === false) {
350 $themeInfo = array();
352 if (!isset($themeInfo['name'])) {
353 $themeInfo['name'] = $theme;
355 $themeInfo['current'] = ($theme === $this->getTheme());
360 public function getInstalledThemes() {
361 $handle = opendir(THEME
);
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('.', '..'))) {
371 $themes[$theme] = $this->getThemeInfo($theme);
379 public function getLanguage() {
380 return $this->currentLanguage
;
383 public function getInstalledLanguages() {
384 $handle = opendir(LOCALE
);
385 $languages = array();
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'))) {
396 if ($language === $this->getLanguage()) {
400 $languages[] = array('name' => (isset($this->language_names
[$language]) ? $this->language_names
[$language] : $language), 'value' => $language, 'current' => $current);
406 public function getDefaultConfig()
409 'pager' => PAGINATION
,
411 'theme' => DEFAULT_THEME
416 * Call action (mark as fav, archive, delete, etc.)
418 public function action($action, Url
$url, $id = 0, $import = FALSE, $autoclose = FALSE, $tags = null)
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'];
427 // clean content from prevent xss attack
428 $purifier = $this->getPurifier();
429 $title = $purifier->purify($title);
430 $body = $purifier->purify($body);
432 //search for possible duplicate
434 $duplicate = $this->store
->retrieveOneByURL($url->getUrl(), $this->user
->getId());
436 $last_id = $this->store
->add($url->getUrl(), $title, $body, $this->user
->getId());
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());
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());
453 if ($this->store
->deleteById($duplicate['id'], $this->user
->getId())) {
454 Tools
::logm('previous link ' . $url->getUrl() .' entry deleted');
458 $this->messages
->add('s', _('the link has been added successfully'));
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());
465 if ($autoclose == TRUE) {
466 Tools
::redirect('?view=home');
468 Tools
::redirect('?view=home&closewin=true');
472 $msg = 'delete link #' . $id;
473 if ($this->store
->deleteById($id, $this->user
->getId())) {
474 if (DOWNLOAD_PICTURES
) {
475 remove_directory(ABS_PATH
. $id);
477 $this->messages
->add('s', _('the link has been deleted successfully'));
480 $this->messages
->add('e', _('the link wasn\'t deleted'));
481 $msg = 'error : can\'t delete link #' . $id;
484 Tools
::redirect('?');
487 $this->store
->favoriteById($id, $this->user
->getId());
488 Tools
::logm('mark as favorite link #' . $id);
489 if ( Tools
::isAjaxRequest() ) {
497 case 'toggle_archive' :
498 $this->store
->archiveById($id, $this->user
->getId());
499 Tools
::logm('archive link #' . $id);
500 if ( Tools
::isAjaxRequest() ) {
509 $this->store
->archiveAll($this->user
->getId());
510 Tools
::logm('archive all links');
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];
522 } else { //add a tag to a single article
523 $tags = explode(',', $_POST['value']);
524 $entry_ids = array($_POST['entry_id']);
526 foreach($entry_ids as $entry_id) {
527 $entry = $this->store
->retrieveOneById($entry_id, $this->user
->getId());
529 $this->messages
->add('e', _('Article not found!'));
530 Tools
::logm('error : article not found');
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'];
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);
545 $tag = $this->store
->createTag($value);
547 if (STORAGE
== 'postgres') {
548 $sequence = 'tags_id_seq';
550 $tag_id = $this->store
->getLastId($sequence);
553 $tag_id = $tag['id'];
556 # we assign the tag to the article
557 $this->store
->setTagToEntry($tag_id, $entry_id);
561 $this->messages
->add('s', _('The tag has been applied successfully'));
562 Tools
::logm('The tag has been applied successfully');
566 $tag_id = $_GET['tag_id'];
567 $entry = $this->store
->retrieveOneById($id, $this->user
->getId());
569 $this->messages
->add('e', _('Article not found!'));
570 Tools
::logm('error : article not found');
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');
578 $this->messages
->add('s', _('The tag has been successfully deleted'));
586 function displayView($view, $id = 0)
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;
608 'languages' => $languages,
611 'check_time_dev' => $check_time_dev,
612 'check_time_prod' => $check_time_prod,
613 'compare_dev' => $compare_dev,
614 'compare_prod' => $compare_prod,
616 'user_id' => $this->user
->getId(),
617 'http_auth' => $http_auth,
618 'only_user' => $only_user
620 Tools
::logm('config view');
624 $entry = $this->store
->retrieveOneById($id, $this->user
->getId());
626 $this->messages
->add('e', _('Article not found!'));
627 Tools
::logm('error : article not found');
630 $tags = $this->store
->retrieveTagsByEntry($id);
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()) {
644 foreach ($tags as $tag) {
645 $result[] = $tag['value'];
647 echo json_encode($result);
652 'user_id' => $this->user
->getId(),
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;
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
;
681 $flattr = new FlattrItem();
682 $flattr->checkItem($entry['url'], $entry['id']);
685 $tags = $this->store
->retrieveTagsByEntry($entry['id']);
689 'content' => $content,
695 Tools
::logm('error in view call : entry is null');
698 default: # home, favorites, archive and tag views
703 'listmode' => (isset($_COOKIE['listmode']) ? true : false),
706 //if id is given - we retrive entries by tag: id is tag id
708 $tpl_vars['tag'] = $this->store
->retrieveTag($id, $this->user
->getId());
709 $tpl_vars['id'] = intval($id);
712 $count = $this->store
->getEntriesByViewCount($view, $this->user
->getId(), $id);
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;
722 Tools
::logm('display ' . $view . ' view');
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)
736 public function updatePassword()
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');
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()));
749 Tools
::logm('password updated');
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');
760 public function updateTheme()
763 if (empty($_POST['theme'])) {
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');
772 $themes = $this->getInstalledThemes();
773 $actualTheme = false;
775 foreach (array_keys($themes) as $theme) {
776 if ($theme == $_POST['theme']) {
782 if (! $actualTheme) {
783 $this->messages
->add('e', _('that theme does not seem to be installed'));
784 Tools
::redirect('?view=config');
787 $this->store
->updateUserConfig($this->user
->getId(), 'theme', $_POST['theme']);
788 $this->messages
->add('s', _('you have changed your theme preferences'));
790 $currentConfig = $_SESSION['poche_user']->config
;
791 $currentConfig['theme'] = $_POST['theme'];
793 $_SESSION['poche_user']->setConfig($currentConfig);
797 Tools
::redirect('?view=config');
800 public function updateLanguage()
803 if (empty($_POST['language'])) {
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');
812 $languages = $this->getInstalledLanguages();
813 $actualLanguage = false;
815 foreach ($languages as $language) {
816 if ($language['value'] == $_POST['language']) {
817 $actualLanguage = true;
822 if (! $actualLanguage) {
823 $this->messages
->add('e', _('that language does not seem to be installed'));
824 Tools
::redirect('?view=config');
827 $this->store
->updateUserConfig($this->user
->getId(), 'language', $_POST['language']);
828 $this->messages
->add('s', _('you have changed your language preferences'));
830 $currentConfig = $_SESSION['poche_user']->config
;
831 $currentConfig['language'] = $_POST['language'];
833 $_SESSION['poche_user']->setConfig($currentConfig);
837 Tools
::redirect('?view=config');
840 * get credentials from differents sources
841 * it redirects the user to the $referer link
844 private function credentials() {
845 if(isset($_SERVER['PHP_AUTH_USER'])) {
846 return array($_SERVER['PHP_AUTH_USER'],'php_auth',true);
848 if(!empty($_POST['login']) && !empty($_POST['password'])) {
849 return array($_POST['login'],$_POST['password'],false);
851 if(isset($_SERVER['REMOTE_USER'])) {
852 return array($_SERVER['REMOTE_USER'],'http_auth',true);
855 return array(false,false,false);
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
865 public function login($referer)
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');
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 $this->messages
->add('s', _('welcome to your wallabag'));
881 Tools
::logm('login successful');
882 Tools
::redirect($referer);
884 $this->messages
->add('e', _('login failed: bad login or password'));
885 Tools
::logm('login failed');
891 * log out the poche user. It cleans the session.
892 * @todo add the return value
895 public function logout()
897 $this->user
= array();
899 Tools
::logm('logout');
904 * import datas into your poche
907 public function import() {
909 if ( isset($_FILES['file']) ) {
910 Tools
::logm('Import stated: parsing file');
912 // assume, that file is in json format
913 $str_data = file_get_contents($_FILES['file']['tmp_name']);
914 $data = json_decode($str_data, true);
916 if ( $data === null ) {
917 //not json - assume html
918 $html = new simple_html_dom();
919 $html->load_file($_FILES['file']['tmp_name']);
922 foreach (array('ol','ul') as $list) {
923 foreach ($html->find($list) as $ul) {
924 foreach ($ul->find('li') as $li) {
927 $tmpEntry['url'] = $a[0]->href
;
928 $tmpEntry['tags'] = $a[0]->tags
;
929 $tmpEntry['is_read'] = $read;
930 if ($tmpEntry['url']) {
934 # the second <ol/ul> is for read links
935 $read = ((sizeof($data) && $read)?0:1);
940 //for readability structure
941 foreach ($data as $record) {
942 if (is_array($record)) {
944 foreach ($record as $record2) {
945 if (is_array($record2)) {
952 $urlsInserted = array(); //urls of articles inserted
953 foreach ($data as $record) {
954 $url = trim( isset($record['article__url']) ? $record['article__url'] : (isset($record['url']) ? $record['url'] : '') );
955 if ( $url and !in_array($url, $urlsInserted) ) {
956 $title = (isset($record['title']) ? $record['title'] : _('Untitled - Import - ').'</a> <a href="./?import">'._('click to finish import').'</a><a>');
957 $body = (isset($record['content']) ? $record['content'] : '');
958 $isRead = (isset($record['is_read']) ? intval($record['is_read']) : (isset($record['archive'])?intval($record['archive']):0));
959 $isFavorite = (isset($record['is_fav']) ? intval($record['is_fav']) : (isset($record['favorite'])?intval($record['favorite']):0) );
961 $id = $this->store
->add($url, $title, $body, $this->user
->getId(), $isFavorite, $isRead);
963 $urlsInserted[] = $url; //add
965 if ( isset($record['tags']) && trim($record['tags']) ) {
973 $i = sizeof($urlsInserted);
975 $this->messages
->add('s', _('Articles inserted: ').$i._('. Please note, that some may be marked as "read".'));
977 Tools
::logm('Import of articles finished: '.$i.' articles added (w/o content if not provided).');
979 //file parsing finished here
981 //now download article contents if any
983 //check if we need to download any content
984 $recordsDownloadRequired = $this->store
->retrieveUnfetchedEntriesCount($this->user
->getId());
985 if ( $recordsDownloadRequired == 0 ) {
986 //nothing to download
987 $this->messages
->add('s', _('Import finished.'));
988 Tools
::logm('Import finished completely');
992 //if just inserted - don't download anything, download will start in next reload
993 if ( !isset($_FILES['file']) ) {
994 //download next batch
995 Tools
::logm('Fetching next batch of articles...');
996 $items = $this->store
->retrieveUnfetchedEntries($this->user
->getId(), IMPORT_LIMIT
);
998 $purifier = $this->getPurifier();
1000 foreach ($items as $item) {
1001 $url = new Url(base64_encode($item['url']));
1002 Tools
::logm('Fetching article '.$item['id']);
1003 $content = Tools
::getPageContent($url);
1005 $title = (($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'));
1006 $body = (($content['rss']['channel']['item']['description'] != '') ? $content['rss']['channel']['item']['description'] : _('Undefined'));
1008 //clean content to prevent xss attack
1009 $title = $purifier->purify($title);
1010 $body = $purifier->purify($body);
1012 $this->store
->updateContentAndTitle($item['id'], $title, $body, $this->user
->getId());
1013 Tools
::logm('Article '.$item['id'].' updated.');
1019 return array('includeImport'=>true, 'import'=>array('recordsDownloadRequired'=>$recordsDownloadRequired, 'recordsUnderDownload'=> IMPORT_LIMIT
, 'delay'=> IMPORT_DELAY
* 1000) );
1023 * export poche entries in json
1024 * @return json all poche entries
1026 public function export() {
1027 $filename = "wallabag-export-".$this->user
->getId()."-".date("Y-m-d").".json";
1028 header('Content-Disposition: attachment; filename='.$filename);
1030 $entries = $this->store
->retrieveAll($this->user
->getId());
1031 echo $this->tpl
->render('export.twig', array(
1032 'export' => Tools
::renderJson($entries),
1034 Tools
::logm('export view');
1038 * Checks online the latest version of poche and cache it
1039 * @param string $which 'prod' or 'dev'
1040 * @return string latest $which version
1042 private function getPocheVersion($which = 'prod') {
1043 $cache_file = CACHE
. '/' . $which;
1044 $check_time = time();
1046 # checks if the cached version file exists
1047 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
1048 $version = file_get_contents($cache_file);
1049 $check_time = filemtime($cache_file);
1051 $version = file_get_contents('http://static.wallabag.org/versions/' . $which);
1052 file_put_contents($cache_file, $version, LOCK_EX
);
1054 return array($version, $check_time);
1057 public function generateToken()
1059 if (ini_get('open_basedir') === '') {
1060 if (strtoupper(substr(PHP_OS
, 0, 3)) === 'WIN') {
1061 echo 'This is a server using Windows!';
1062 // alternative to /dev/urandom for Windows
1063 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1065 $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
1069 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1072 $token = str_replace('+', '', $token);
1073 $this->store
->updateUserConfig($this->user
->getId(), 'token', $token);
1074 $currentConfig = $_SESSION['poche_user']->config
;
1075 $currentConfig['token'] = $token;
1076 $_SESSION['poche_user']->setConfig($currentConfig);
1080 public function generateFeeds($token, $user_id, $tag_id, $type = 'home')
1082 $allowed_types = array('home', 'fav', 'archive', 'tag');
1083 $config = $this->store
->getConfigUser($user_id);
1085 if ($config == null) {
1086 die(sprintf(_('User with this id (%d) does not exist.'), $user_id));
1089 if (!in_array($type, $allowed_types) || $token != $config['token']) {
1090 die(_('Uh, there is a problem while generating feeds.'));
1094 $feed = new FeedWriter(RSS2
);
1095 $feed->setTitle('wallabag — ' . $type . ' feed');
1096 $feed->setLink(Tools
::getPocheUrl());
1097 $feed->setChannelElement('pubDate', date(DATE_RSS
, time()));
1098 $feed->setChannelElement('generator', 'wallabag');
1099 $feed->setDescription('wallabag ' . $type . ' elements');
1101 if ($type == 'tag') {
1102 $entries = $this->store
->retrieveEntriesByTag($tag_id, $user_id);
1105 $entries = $this->store
->getEntriesByView($type, $user_id);
1108 if (count($entries) > 0) {
1109 foreach ($entries as $entry) {
1110 $newItem = $feed->createNewItem();
1111 $newItem->setTitle($entry['title']);
1112 $newItem->setSource(Tools
::getPocheUrl() . '?view=view&id=' . $entry['id']);
1113 $newItem->setLink($entry['url']);
1114 $newItem->setDate(time());
1115 $newItem->setDescription($entry['content']);
1116 $feed->addItem($newItem);
1120 $feed->genarateFeed();
1124 public function emptyCache() {
1125 $files = new RecursiveIteratorIterator(
1126 new RecursiveDirectoryIterator(CACHE
, RecursiveDirectoryIterator
::SKIP_DOTS
),
1127 RecursiveIteratorIterator
::CHILD_FIRST
1130 foreach ($files as $fileinfo) {
1131 $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
1132 $todo($fileinfo->getRealPath());
1135 Tools
::logm('empty cache');
1136 $this->messages
->add('s', _('Cache deleted.'));
1141 * return new purifier object with actual config
1143 protected function getPurifier() {
1144 $config = HTMLPurifier_Config
::createDefault();
1145 $config->set('Cache.SerializerPath', CACHE
);
1146 $config->set('HTML.SafeIframe', true);
1148 //allow YouTube, Vimeo and dailymotion videos
1149 $config->set('URI.SafeIframeRegexp', '%^(https?:)?//(www\.youtube(?:-nocookie)?\.com/embed/|player\.vimeo\.com/video/|www\.dailymotion\.com/embed/video/)%');
1151 return new HTMLPurifier($config);
1157 public function createEpub() {
1159 switch ($_GET['method']) {
1161 $entryID = filter_var($_GET['id'],FILTER_SANITIZE_NUMBER_INT
);
1162 $entry = $this->store
->retrieveOneById($entryID, $this->user
->getId());
1163 $entries = array($entry);
1164 $bookTitle = $entry['title'];
1165 $bookFileName = substr($bookTitle, 0, 200);
1168 $entries = $this->store
->retrieveAll($this->user
->getId());
1169 $bookTitle = sprintf(_('All my articles on '), date(_('d.m.y'))); #translatable because each country has it's own date format system
1170 $bookFileName = _('Allarticles') . date(_('dmY'));
1173 $tag = filter_var($_GET['tag'],FILTER_SANITIZE_STRING
);
1174 $tags_id = $this->store
->retrieveAllTags($this->user
->getId(),$tag);
1175 $tag_id = $tags_id[0]["id"]; // we take the first result, which is supposed to match perfectly. There must be a workaround.
1176 $entries = $this->store
->retrieveEntriesByTag($tag_id,$this->user
->getId());
1177 $bookTitle = sprintf(_('Articles tagged %s'),$tag);
1178 $bookFileName = substr(sprintf(_('Tag %s'),$tag), 0, 200);
1181 $category = filter_var($_GET['category'],FILTER_SANITIZE_STRING
);
1182 $entries = $this->store
->getEntriesByView($category,$this->user
->getId());
1183 $bookTitle = sprintf(_('All articles in category %s'), $category);
1184 $bookFileName = substr(sprintf(_('Category %s'),$category), 0, 200);
1187 $search = filter_var($_GET['search'],FILTER_SANITIZE_STRING
);
1188 $entries = $this->store
->search($search,$this->user
->getId());
1189 $bookTitle = sprintf(_('All articles for search %s'), $search);
1190 $bookFileName = substr(sprintf(_('Search %s'), $search), 0, 200);
1193 die(_('Uh, there is a problem while generating epub.'));
1198 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1199 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
1201 . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
1202 . "<title>wallabag articles book</title>\n"
1206 $bookEnd = "</body>\n</html>\n";
1208 $log = new Logger("wallabag", TRUE);
1211 $book = new EPub(EPub
::BOOK_VERSION_EPUB3
, DEBUG_POCHE
);
1212 $log->logLine("new EPub()");
1213 $log->logLine("EPub class version: " . EPub
::VERSION
);
1214 $log->logLine("EPub Req. Zip version: " . EPub
::REQ_ZIP_VERSION
);
1215 $log->logLine("Zip version: " . Zip
::VERSION
);
1216 $log->logLine("getCurrentServerURL: " . $book->getCurrentServerURL());
1217 $log->logLine("getCurrentPageURL..: " . $book->getCurrentPageURL());
1219 $book->setTitle(_('wallabag\'s articles'));
1220 $book->setIdentifier("http://$_SERVER[HTTP_HOST]", EPub
::IDENTIFIER_URI
); // Could also be the ISBN number, prefered for published books, or a UUID.
1221 //$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.
1222 $book->setDescription(_("Some articles saved on my wallabag"));
1223 $book->setAuthor("wallabag","wallabag");
1224 $book->setPublisher("wallabag","wallabag"); // I hope this is a non existant address :)
1225 $book->setDate(time()); // Strictly not needed as the book date defaults to time().
1226 //$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.
1227 $book->setSourceURL("http://$_SERVER[HTTP_HOST]");
1229 $book->addDublinCoreMetadata(DublinCore
::CONTRIBUTOR
, "PHP");
1230 $book->addDublinCoreMetadata(DublinCore
::CONTRIBUTOR
, "wallabag");
1232 $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";
1234 $log->logLine("Add Cover");
1236 $fullTitle = "<h1> " . $bookTitle . "</h1>\n";
1238 $book->setCoverImage("Cover.png", file_get_contents("themes/baggy/img/apple-touch-icon-152.png"), "image/png", $fullTitle);
1240 $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;
1242 //$book->addChapter("Table of Contents", "TOC.xhtml", NULL, false, EPub::EXTERNAL_REF_IGNORE);
1243 $book->addChapter("Notices", "Cover2.html", $cover);
1247 foreach ($entries as $entry) { //set tags as subjects
1248 $tags = $this->store
->retrieveTagsByEntry($entry['id']);
1249 foreach ($tags as $tag) {
1250 $book->setSubject($tag['value']);
1253 $log->logLine("Set up parameters");
1255 $chapter = $content_start . $entry['content'] . $bookEnd;
1256 $book->addChapter($entry['title'], htmlspecialchars($entry['title']) . ".html", $chapter, true, EPub
::EXTERNAL_REF_ADD
);
1257 $log->logLine("Added chapter " . $entry['title']);
1261 $epuplog = $book->getLog();
1262 $book->addChapter("Log", "Log.html", $content_start . $log->getLog() . "\n</pre>" . $bookEnd); // log generation
1265 $zipData = $book->sendBook($bookFileName);