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());
237 public function createNewUser() {
238 if (isset($_GET['newuser'])){
239 if ($_POST['newusername'] != "" && $_POST['password4newuser'] != ""){
240 $newusername = filter_var($_POST['newusername'], FILTER_SANITIZE_STRING
);
241 if (!$this->store
->userExists($newusername)){
242 if ($this->store
->install($newusername, Tools
::encodeString($_POST['password4newuser'] . $newusername))) {
243 Tools
::logm('The new user '.$newusername.' has been installed');
244 $this->messages
->add('s', sprintf(_('The new user %s has been installed. Do you want to <a href="?logout">logout ?</a>'),$newusername));
248 Tools
::logm('error during adding new user');
253 $this->messages
->add('e', sprintf(_('Error : An user with the name %s already exists !'),$newusername));
254 Tools
::logm('An user with the name '.$newusername.' already exists !');
261 public function deleteUser(){
262 if (isset($_GET['deluser'])){
263 if ($this->store
->listUsers() > 1) {
264 if (Tools
::encodeString($_POST['password4deletinguser'].$this->user
->getUsername()) == $this->store
->getUserPassword($this->user
->getId())) {
265 $username = $this->user
->getUsername();
266 $this->store
->deleteUserConfig($this->user
->getId());
267 Tools
::logm('The configuration for user '. $username .' has been deleted !');
268 $this->store
->deleteTagsEntriesAndEntries($this->user
->getId());
269 Tools
::logm('The entries for user '. $username .' has been deleted !');
270 $this->store
->deleteUser($this->user
->getId());
271 Tools
::logm('User '. $username .' has been completely deleted !');
273 Tools
::logm('logout');
275 $this->messages
->add('s', sprintf(_('User %s has been successfully deleted !'),$newusername));
278 Tools
::logm('Bad password !');
279 $this->messages
->add('e', _('Error : The password is wrong !'));
283 Tools
::logm('Only user !');
284 $this->messages
->add('e', _('Error : You are the only user, you cannot delete your account !'));
289 private function install()
291 Tools
::logm('poche still not installed');
292 echo $this->tpl
->render('install.twig', array(
293 'token' => Session
::getToken(),
294 'theme' => $this->getTheme(),
295 'poche_url' => Tools
::getPocheUrl()
297 if (isset($_GET['install'])) {
298 if (($_POST['password'] == $_POST['password_repeat'])
299 && $_POST['password'] != "" && $_POST['login'] != "") {
300 # let's rock, install poche baby !
301 if ($this->store
->install($_POST['login'], Tools
::encodeString($_POST['password'] . $_POST['login'])))
304 Tools
::logm('poche is now installed');
309 Tools
::logm('error during installation');
316 public function getTheme() {
317 return $this->currentTheme
;
321 * Provides theme information by parsing theme.ini file if present in the theme's root directory.
322 * In all cases, the following data will be returned:
323 * - name: theme's name, or key if the theme is unnamed,
324 * - current: boolean informing if the theme is the current user theme.
326 * @param string $theme Theme key (directory name)
327 * @return array|boolean Theme information, or false if the theme doesn't exist.
329 public function getThemeInfo($theme) {
330 if (!is_dir(THEME
. '/' . $theme)) {
334 $themeIniFile = THEME
. '/' . $theme . '/theme.ini';
335 $themeInfo = array();
337 if (is_file($themeIniFile) && is_readable($themeIniFile)) {
338 $themeInfo = parse_ini_file($themeIniFile);
341 if ($themeInfo === false) {
342 $themeInfo = array();
344 if (!isset($themeInfo['name'])) {
345 $themeInfo['name'] = $theme;
347 $themeInfo['current'] = ($theme === $this->getTheme());
352 public function getInstalledThemes() {
353 $handle = opendir(THEME
);
356 while (($theme = readdir($handle)) !== false) {
357 # Themes are stored in a directory, so all directory names are themes
358 # @todo move theme installation data to database
359 if (!is_dir(THEME
. '/' . $theme) || in_array($theme, array('.', '..'))) {
363 $themes[$theme] = $this->getThemeInfo($theme);
371 public function getLanguage() {
372 return $this->currentLanguage
;
375 public function getInstalledLanguages() {
376 $handle = opendir(LOCALE
);
377 $languages = array();
379 while (($language = readdir($handle)) !== false) {
380 # Languages are stored in a directory, so all directory names are languages
381 # @todo move language installation data to database
382 if (! is_dir(LOCALE
. '/' . $language) || in_array($language, array('..', '.', 'tools'))) {
388 if ($language === $this->getLanguage()) {
392 $languages[] = array('name' => (isset($this->language_names
[$language]) ? $this->language_names
[$language] : $language), 'value' => $language, 'current' => $current);
398 public function getDefaultConfig()
401 'pager' => PAGINATION
,
403 'theme' => DEFAULT_THEME
408 * Call action (mark as fav, archive, delete, etc.)
410 public function action($action, Url
$url, $id = 0, $import = FALSE, $autoclose = FALSE, $tags = null)
415 $content = Tools
::getPageContent($url);
416 $title = ($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled');
417 $body = $content['rss']['channel']['item']['description'];
419 // clean content from prevent xss attack
420 $purifier = $this->getPurifier();
421 $title = $purifier->purify($title);
422 $body = $purifier->purify($body);
424 //search for possible duplicate
426 $duplicate = $this->store
->retrieveOneByURL($url->getUrl(), $this->user
->getId());
428 $last_id = $this->store
->add($url->getUrl(), $title, $body, $this->user
->getId());
430 Tools
::logm('add link ' . $url->getUrl());
431 if (DOWNLOAD_PICTURES
) {
432 $content = filtre_picture($body, $url->getUrl(), $last_id);
433 Tools
::logm('updating content article');
434 $this->store
->updateContent($last_id, $content, $this->user
->getId());
437 if ($duplicate != NULL) {
438 // duplicate exists, so, older entry needs to be deleted (as new entry should go to the top of list), BUT favorite mark and tags should be preserved
439 Tools
::logm('link ' . $url->getUrl() . ' is a duplicate');
440 // 1) - preserve tags and favorite, then drop old entry
441 $this->store
->reassignTags($duplicate['id'], $last_id);
442 if ($duplicate['is_fav']) {
443 $this->store
->favoriteById($last_id, $this->user
->getId());
445 if ($this->store
->deleteById($duplicate['id'], $this->user
->getId())) {
446 Tools
::logm('previous link ' . $url->getUrl() .' entry deleted');
450 $this->messages
->add('s', _('the link has been added successfully'));
453 $this->messages
->add('e', _('error during insertion : the link wasn\'t added'));
454 Tools
::logm('error during insertion : the link wasn\'t added ' . $url->getUrl());
457 if ($autoclose == TRUE) {
458 Tools
::redirect('?view=home');
460 Tools
::redirect('?view=home&closewin=true');
464 $msg = 'delete link #' . $id;
465 if ($this->store
->deleteById($id, $this->user
->getId())) {
466 if (DOWNLOAD_PICTURES
) {
467 remove_directory(ABS_PATH
. $id);
469 $this->messages
->add('s', _('the link has been deleted successfully'));
472 $this->messages
->add('e', _('the link wasn\'t deleted'));
473 $msg = 'error : can\'t delete link #' . $id;
476 Tools
::redirect('?');
479 $this->store
->favoriteById($id, $this->user
->getId());
480 Tools
::logm('mark as favorite link #' . $id);
481 if ( Tools
::isAjaxRequest() ) {
489 case 'toggle_archive' :
490 $this->store
->archiveById($id, $this->user
->getId());
491 Tools
::logm('archive link #' . $id);
492 if ( Tools
::isAjaxRequest() ) {
501 $this->store
->archiveAll($this->user
->getId());
502 Tools
::logm('archive all links');
506 if (isset($_GET['search'])) {
507 //when we want to apply a tag to a search
508 $tags = array($_GET['search']);
509 $allentry_ids = $this->store
->search($tags[0], $this->user
->getId());
510 $entry_ids = array();
511 foreach ($allentry_ids as $eachentry) {
512 $entry_ids[] = $eachentry[0];
514 } else { //add a tag to a single article
515 $tags = explode(',', $_POST['value']);
516 $entry_ids = array($_POST['entry_id']);
518 foreach($entry_ids as $entry_id) {
519 $entry = $this->store
->retrieveOneById($entry_id, $this->user
->getId());
521 $this->messages
->add('e', _('Article not found!'));
522 Tools
::logm('error : article not found');
525 //get all already set tags to preven duplicates
526 $already_set_tags = array();
527 $entry_tags = $this->store
->retrieveTagsByEntry($entry_id);
528 foreach ($entry_tags as $tag) {
529 $already_set_tags[] = $tag['value'];
531 foreach($tags as $key => $tag_value) {
532 $value = trim($tag_value);
533 if ($value && !in_array($value, $already_set_tags)) {
534 $tag = $this->store
->retrieveTagByValue($value);
537 $tag = $this->store
->createTag($value);
539 if (STORAGE
== 'postgres') {
540 $sequence = 'tags_id_seq';
542 $tag_id = $this->store
->getLastId($sequence);
545 $tag_id = $tag['id'];
548 # we assign the tag to the article
549 $this->store
->setTagToEntry($tag_id, $entry_id);
553 $this->messages
->add('s', _('The tag has been applied successfully'));
554 Tools
::logm('The tag has been applied successfully');
558 $tag_id = $_GET['tag_id'];
559 $entry = $this->store
->retrieveOneById($id, $this->user
->getId());
561 $this->messages
->add('e', _('Article not found!'));
562 Tools
::logm('error : article not found');
565 $this->store
->removeTagForEntry($id, $tag_id);
566 Tools
::logm('tag entry deleted');
567 if ($this->store
->cleanUnusedTag($tag_id)) {
568 Tools
::logm('tag deleted');
570 $this->messages
->add('s', _('The tag has been successfully deleted'));
578 function displayView($view, $id = 0)
585 $dev_infos = $this->getPocheVersion('dev');
586 $dev = trim($dev_infos[0]);
587 $check_time_dev = date('d-M-Y H:i', $dev_infos[1]);
588 $prod_infos = $this->getPocheVersion('prod');
589 $prod = trim($prod_infos[0]);
590 $check_time_prod = date('d-M-Y H:i', $prod_infos[1]);
591 $compare_dev = version_compare(POCHE
, $dev);
592 $compare_prod = version_compare(POCHE
, $prod);
593 $themes = $this->getInstalledThemes();
594 $languages = $this->getInstalledLanguages();
595 $token = $this->user
->getConfigValue('token');
596 $http_auth = (isset($_SERVER['PHP_AUTH_USER']) || isset($_SERVER['REMOTE_USER'])) ? true : false;
597 $only_user = ($this->store
->listUsers() > 1) ? false : true;
600 'languages' => $languages,
603 'check_time_dev' => $check_time_dev,
604 'check_time_prod' => $check_time_prod,
605 'compare_dev' => $compare_dev,
606 'compare_prod' => $compare_prod,
608 'user_id' => $this->user
->getId(),
609 'http_auth' => $http_auth,
610 'only_user' => $only_user
612 Tools
::logm('config view');
616 $entry = $this->store
->retrieveOneById($id, $this->user
->getId());
618 $this->messages
->add('e', _('Article not found!'));
619 Tools
::logm('error : article not found');
622 $tags = $this->store
->retrieveTagsByEntry($id);
630 $token = $this->user
->getConfigValue('token');
631 //if term is set - search tags for this term
632 $term = Tools
::checkVar('term');
633 $tags = $this->store
->retrieveAllTags($this->user
->getId(), $term);
634 if (Tools
::isAjaxRequest()) {
636 foreach ($tags as $tag) {
637 $result[] = $tag['value'];
639 echo json_encode($result);
644 'user_id' => $this->user
->getId(),
649 if (isset($_GET['search'])) {
650 $search = filter_var($_GET['search'], FILTER_SANITIZE_STRING
);
651 $tpl_vars['entries'] = $this->store
->search($search, $this->user
->getId());
652 $count = count($tpl_vars['entries']);
653 $this->pagination
->set_total($count);
654 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
655 $this->pagination
->page_links('?view=' . $view . '?search=' . $search . '&sort=' . $_SESSION['sort'] . '&' ));
656 $tpl_vars['page_links'] = $page_links;
657 $tpl_vars['nb_results'] = $count;
658 $tpl_vars['search_term'] = $search;
662 $entry = $this->store
->retrieveOneById($id, $this->user
->getId());
663 if ($entry != NULL) {
664 Tools
::logm('view link #' . $id);
665 $content = $entry['content'];
666 if (function_exists('tidy_parse_string')) {
667 $tidy = tidy_parse_string($content, array('indent'=>true, 'show-body-only' => true), 'UTF8');
668 $tidy->cleanRepair();
669 $content = $tidy->value
;
673 $flattr = new FlattrItem();
674 $flattr->checkItem($entry['url'], $entry['id']);
677 $tags = $this->store
->retrieveTagsByEntry($entry['id']);
681 'content' => $content,
687 Tools
::logm('error in view call : entry is null');
690 default: # home, favorites, archive and tag views
695 'listmode' => (isset($_COOKIE['listmode']) ? true : false),
698 //if id is given - we retrive entries by tag: id is tag id
700 $tpl_vars['tag'] = $this->store
->retrieveTag($id, $this->user
->getId());
701 $tpl_vars['id'] = intval($id);
704 $count = $this->store
->getEntriesByViewCount($view, $this->user
->getId(), $id);
707 $this->pagination
->set_total($count);
708 $page_links = str_replace(array('previous', 'next'), array(_('previous'), _('next')),
709 $this->pagination
->page_links('?view=' . $view . '&sort=' . $_SESSION['sort'] . (($id)?'&id='.$id:'') . '&' ));
710 $tpl_vars['entries'] = $this->store
->getEntriesByView($view, $this->user
->getId(), $this->pagination
->get_limit(), $id);
711 $tpl_vars['page_links'] = $page_links;
712 $tpl_vars['nb_results'] = $count;
714 Tools
::logm('display ' . $view . ' view');
722 * update the password of the current user.
723 * if MODE_DEMO is TRUE, the password can't be updated.
724 * @todo add the return value
725 * @todo set the new password in function header like this updatePassword($newPassword)
728 public function updatePassword()
731 $this->messages
->add('i', _('in demo mode, you can\'t update your password'));
732 Tools
::logm('in demo mode, you can\'t do this');
733 Tools
::redirect('?view=config');
736 if (isset($_POST['password']) && isset($_POST['password_repeat'])) {
737 if ($_POST['password'] == $_POST['password_repeat'] && $_POST['password'] != "") {
738 $this->messages
->add('s', _('your password has been updated'));
739 $this->store
->updatePassword($this->user
->getId(), Tools
::encodeString($_POST['password'] . $this->user
->getUsername()));
741 Tools
::logm('password updated');
745 $this->messages
->add('e', _('the two fields have to be filled & the password must be the same in the two fields'));
746 Tools
::redirect('?view=config');
752 public function updateTheme()
755 if (empty($_POST['theme'])) {
758 # we are not going to change it to the current theme...
759 if ($_POST['theme'] == $this->getTheme()) {
760 $this->messages
->add('w', _('still using the "' . $this->getTheme() . '" theme!'));
761 Tools
::redirect('?view=config');
764 $themes = $this->getInstalledThemes();
765 $actualTheme = false;
767 foreach (array_keys($themes) as $theme) {
768 if ($theme == $_POST['theme']) {
774 if (! $actualTheme) {
775 $this->messages
->add('e', _('that theme does not seem to be installed'));
776 Tools
::redirect('?view=config');
779 $this->store
->updateUserConfig($this->user
->getId(), 'theme', $_POST['theme']);
780 $this->messages
->add('s', _('you have changed your theme preferences'));
782 $currentConfig = $_SESSION['poche_user']->config
;
783 $currentConfig['theme'] = $_POST['theme'];
785 $_SESSION['poche_user']->setConfig($currentConfig);
789 Tools
::redirect('?view=config');
792 public function updateLanguage()
795 if (empty($_POST['language'])) {
798 # we are not going to change it to the current language...
799 if ($_POST['language'] == $this->getLanguage()) {
800 $this->messages
->add('w', _('still using the "' . $this->getLanguage() . '" language!'));
801 Tools
::redirect('?view=config');
804 $languages = $this->getInstalledLanguages();
805 $actualLanguage = false;
807 foreach ($languages as $language) {
808 if ($language['value'] == $_POST['language']) {
809 $actualLanguage = true;
814 if (! $actualLanguage) {
815 $this->messages
->add('e', _('that language does not seem to be installed'));
816 Tools
::redirect('?view=config');
819 $this->store
->updateUserConfig($this->user
->getId(), 'language', $_POST['language']);
820 $this->messages
->add('s', _('you have changed your language preferences'));
822 $currentConfig = $_SESSION['poche_user']->config
;
823 $currentConfig['language'] = $_POST['language'];
825 $_SESSION['poche_user']->setConfig($currentConfig);
829 Tools
::redirect('?view=config');
832 * get credentials from differents sources
833 * it redirects the user to the $referer link
836 private function credentials() {
837 if(isset($_SERVER['PHP_AUTH_USER'])) {
838 return array($_SERVER['PHP_AUTH_USER'],'php_auth',true);
840 if(!empty($_POST['login']) && !empty($_POST['password'])) {
841 return array($_POST['login'],$_POST['password'],false);
843 if(isset($_SERVER['REMOTE_USER'])) {
844 return array($_SERVER['REMOTE_USER'],'http_auth',true);
847 return array(false,false,false);
851 * checks if login & password are correct and save the user in session.
852 * it redirects the user to the $referer link
853 * @param string $referer the url to redirect after login
854 * @todo add the return value
857 public function login($referer)
859 list($login,$password,$isauthenticated)=$this->credentials();
860 if($login === false || $password === false) {
861 $this->messages
->add('e', _('login failed: you have to fill all fields'));
862 Tools
::logm('login failed');
865 if (!empty($login) && !empty($password)) {
866 $user = $this->store
->login($login, Tools
::encodeString($password . $login), $isauthenticated);
867 if ($user != array()) {
868 # Save login into Session
869 $longlastingsession = isset($_POST['longlastingsession']);
870 $passwordTest = ($isauthenticated) ? $user['password'] : Tools
::encodeString($password . $login);
871 Session
::login($user['username'], $user['password'], $login, $passwordTest, $longlastingsession, array('poche_user' => new User($user)));
872 $this->messages
->add('s', _('welcome to your wallabag'));
873 Tools
::logm('login successful');
874 Tools
::redirect($referer);
876 $this->messages
->add('e', _('login failed: bad login or password'));
877 Tools
::logm('login failed');
883 * log out the poche user. It cleans the session.
884 * @todo add the return value
887 public function logout()
889 $this->user
= array();
891 Tools
::logm('logout');
896 * import datas into your poche
899 public function import() {
901 if ( isset($_FILES['file']) ) {
902 Tools
::logm('Import stated: parsing file');
904 // assume, that file is in json format
905 $str_data = file_get_contents($_FILES['file']['tmp_name']);
906 $data = json_decode($str_data, true);
908 if ( $data === null ) {
909 //not json - assume html
910 $html = new simple_html_dom();
911 $html->load_file($_FILES['file']['tmp_name']);
914 foreach (array('ol','ul') as $list) {
915 foreach ($html->find($list) as $ul) {
916 foreach ($ul->find('li') as $li) {
919 $tmpEntry['url'] = $a[0]->href
;
920 $tmpEntry['tags'] = $a[0]->tags
;
921 $tmpEntry['is_read'] = $read;
922 if ($tmpEntry['url']) {
926 # the second <ol/ul> is for read links
927 $read = ((sizeof($data) && $read)?0:1);
932 //for readability structure
933 foreach ($data as $record) {
934 if (is_array($record)) {
936 foreach ($record as $record2) {
937 if (is_array($record2)) {
944 $urlsInserted = array(); //urls of articles inserted
945 foreach ($data as $record) {
946 $url = trim( isset($record['article__url']) ? $record['article__url'] : (isset($record['url']) ? $record['url'] : '') );
947 if ( $url and !in_array($url, $urlsInserted) ) {
948 $title = (isset($record['title']) ? $record['title'] : _('Untitled - Import - ').'</a> <a href="./?import">'._('click to finish import').'</a><a>');
949 $body = (isset($record['content']) ? $record['content'] : '');
950 $isRead = (isset($record['is_read']) ? intval($record['is_read']) : (isset($record['archive'])?intval($record['archive']):0));
951 $isFavorite = (isset($record['is_fav']) ? intval($record['is_fav']) : (isset($record['favorite'])?intval($record['favorite']):0) );
953 $id = $this->store
->add($url, $title, $body, $this->user
->getId(), $isFavorite, $isRead);
955 $urlsInserted[] = $url; //add
957 if ( isset($record['tags']) && trim($record['tags']) ) {
965 $i = sizeof($urlsInserted);
967 $this->messages
->add('s', _('Articles inserted: ').$i._('. Please note, that some may be marked as "read".'));
969 Tools
::logm('Import of articles finished: '.$i.' articles added (w/o content if not provided).');
971 //file parsing finished here
973 //now download article contents if any
975 //check if we need to download any content
976 $recordsDownloadRequired = $this->store
->retrieveUnfetchedEntriesCount($this->user
->getId());
977 if ( $recordsDownloadRequired == 0 ) {
978 //nothing to download
979 $this->messages
->add('s', _('Import finished.'));
980 Tools
::logm('Import finished completely');
984 //if just inserted - don't download anything, download will start in next reload
985 if ( !isset($_FILES['file']) ) {
986 //download next batch
987 Tools
::logm('Fetching next batch of articles...');
988 $items = $this->store
->retrieveUnfetchedEntries($this->user
->getId(), IMPORT_LIMIT
);
990 $purifier = $this->getPurifier();
992 foreach ($items as $item) {
993 $url = new Url(base64_encode($item['url']));
994 Tools
::logm('Fetching article '.$item['id']);
995 $content = Tools
::getPageContent($url);
997 $title = (($content['rss']['channel']['item']['title'] != '') ? $content['rss']['channel']['item']['title'] : _('Untitled'));
998 $body = (($content['rss']['channel']['item']['description'] != '') ? $content['rss']['channel']['item']['description'] : _('Undefined'));
1000 //clean content to prevent xss attack
1001 $title = $purifier->purify($title);
1002 $body = $purifier->purify($body);
1004 $this->store
->updateContentAndTitle($item['id'], $title, $body, $this->user
->getId());
1005 Tools
::logm('Article '.$item['id'].' updated.');
1011 return array('includeImport'=>true, 'import'=>array('recordsDownloadRequired'=>$recordsDownloadRequired, 'recordsUnderDownload'=> IMPORT_LIMIT
, 'delay'=> IMPORT_DELAY
* 1000) );
1015 * export poche entries in json
1016 * @return json all poche entries
1018 public function export() {
1019 $filename = "wallabag-export-".$this->user
->getId()."-".date("Y-m-d").".json";
1020 header('Content-Disposition: attachment; filename='.$filename);
1022 $entries = $this->store
->retrieveAll($this->user
->getId());
1023 echo $this->tpl
->render('export.twig', array(
1024 'export' => Tools
::renderJson($entries),
1026 Tools
::logm('export view');
1030 * Checks online the latest version of poche and cache it
1031 * @param string $which 'prod' or 'dev'
1032 * @return string latest $which version
1034 private function getPocheVersion($which = 'prod') {
1035 $cache_file = CACHE
. '/' . $which;
1036 $check_time = time();
1038 # checks if the cached version file exists
1039 if (file_exists($cache_file) && (filemtime($cache_file) > (time() - 86400 ))) {
1040 $version = file_get_contents($cache_file);
1041 $check_time = filemtime($cache_file);
1043 $version = file_get_contents('http://static.wallabag.org/versions/' . $which);
1044 file_put_contents($cache_file, $version, LOCK_EX
);
1046 return array($version, $check_time);
1049 public function generateToken()
1051 if (ini_get('open_basedir') === '') {
1052 if (strtoupper(substr(PHP_OS
, 0, 3)) === 'WIN') {
1053 echo 'This is a server using Windows!';
1054 // alternative to /dev/urandom for Windows
1055 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1057 $token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
1061 $token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
1064 $token = str_replace('+', '', $token);
1065 $this->store
->updateUserConfig($this->user
->getId(), 'token', $token);
1066 $currentConfig = $_SESSION['poche_user']->config
;
1067 $currentConfig['token'] = $token;
1068 $_SESSION['poche_user']->setConfig($currentConfig);
1072 public function generateFeeds($token, $user_id, $tag_id, $type = 'home')
1074 $allowed_types = array('home', 'fav', 'archive', 'tag');
1075 $config = $this->store
->getConfigUser($user_id);
1077 if ($config == null) {
1078 die(sprintf(_('User with this id (%d) does not exist.'), $user_id));
1081 if (!in_array($type, $allowed_types) || $token != $config['token']) {
1082 die(_('Uh, there is a problem while generating feeds.'));
1086 $feed = new FeedWriter(RSS2
);
1087 $feed->setTitle('wallabag — ' . $type . ' feed');
1088 $feed->setLink(Tools
::getPocheUrl());
1089 $feed->setChannelElement('pubDate', date(DATE_RSS
, time()));
1090 $feed->setChannelElement('generator', 'wallabag');
1091 $feed->setDescription('wallabag ' . $type . ' elements');
1093 if ($type == 'tag') {
1094 $entries = $this->store
->retrieveEntriesByTag($tag_id, $user_id);
1097 $entries = $this->store
->getEntriesByView($type, $user_id);
1100 if (count($entries) > 0) {
1101 foreach ($entries as $entry) {
1102 $newItem = $feed->createNewItem();
1103 $newItem->setTitle($entry['title']);
1104 $newItem->setSource(Tools
::getPocheUrl() . '?view=view&id=' . $entry['id']);
1105 $newItem->setLink($entry['url']);
1106 $newItem->setDate(time());
1107 $newItem->setDescription($entry['content']);
1108 $feed->addItem($newItem);
1112 $feed->genarateFeed();
1116 public function emptyCache() {
1117 $files = new RecursiveIteratorIterator(
1118 new RecursiveDirectoryIterator(CACHE
, RecursiveDirectoryIterator
::SKIP_DOTS
),
1119 RecursiveIteratorIterator
::CHILD_FIRST
1122 foreach ($files as $fileinfo) {
1123 $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
1124 $todo($fileinfo->getRealPath());
1127 Tools
::logm('empty cache');
1128 $this->messages
->add('s', _('Cache deleted.'));
1133 * return new purifier object with actual config
1135 protected function getPurifier() {
1136 $config = HTMLPurifier_Config
::createDefault();
1137 $config->set('Cache.SerializerPath', CACHE
);
1138 $config->set('HTML.SafeIframe', true);
1140 //allow YouTube, Vimeo and dailymotion videos
1141 $config->set('URI.SafeIframeRegexp', '%^(https?:)?//(www\.youtube(?:-nocookie)?\.com/embed/|player\.vimeo\.com/video/|www\.dailymotion\.com/embed/video/)%');
1143 return new HTMLPurifier($config);
1149 public function createEpub() {
1151 switch ($_GET['method']) {
1153 $entryID = filter_var($_GET['id'],FILTER_SANITIZE_NUMBER_INT
);
1154 $entry = $this->store
->retrieveOneById($entryID, $this->user
->getId());
1155 $entries = array($entry);
1156 $bookTitle = $entry['title'];
1157 $bookFileName = substr($bookTitle, 0, 200);
1160 $entries = $this->store
->retrieveAll($this->user
->getId());
1161 $bookTitle = sprintf(_('All my articles on '), date(_('d.m.y'))); #translatable because each country has it's own date format system
1162 $bookFileName = _('Allarticles') . date(_('dmY'));
1165 $tag = filter_var($_GET['tag'],FILTER_SANITIZE_STRING
);
1166 $tags_id = $this->store
->retrieveAllTags($this->user
->getId(),$tag);
1167 $tag_id = $tags_id[0]["id"]; // we take the first result, which is supposed to match perfectly. There must be a workaround.
1168 $entries = $this->store
->retrieveEntriesByTag($tag_id,$this->user
->getId());
1169 $bookTitle = sprintf(_('Articles tagged %s'),$tag);
1170 $bookFileName = substr(sprintf(_('Tag %s'),$tag), 0, 200);
1173 $category = filter_var($_GET['category'],FILTER_SANITIZE_STRING
);
1174 $entries = $this->store
->getEntriesByView($category,$this->user
->getId());
1175 $bookTitle = sprintf(_('All articles in category %s'), $category);
1176 $bookFileName = substr(sprintf(_('Category %s'),$category), 0, 200);
1179 $search = filter_var($_GET['search'],FILTER_SANITIZE_STRING
);
1180 $entries = $this->store
->search($search,$this->user
->getId());
1181 $bookTitle = sprintf(_('All articles for search %s'), $search);
1182 $bookFileName = substr(sprintf(_('Search %s'), $search), 0, 200);
1185 die(_('Uh, there is a problem while generating epub.'));
1190 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1191 . "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n"
1193 . "<meta http-equiv=\"Default-Style\" content=\"text/html; charset=utf-8\" />\n"
1194 . "<title>wallabag articles book</title>\n"
1198 $bookEnd = "</body>\n</html>\n";
1200 $log = new Logger("wallabag", TRUE);
1203 $book = new EPub(EPub
::BOOK_VERSION_EPUB3
, DEBUG_POCHE
);
1204 $log->logLine("new EPub()");
1205 $log->logLine("EPub class version: " . EPub
::VERSION
);
1206 $log->logLine("EPub Req. Zip version: " . EPub
::REQ_ZIP_VERSION
);
1207 $log->logLine("Zip version: " . Zip
::VERSION
);
1208 $log->logLine("getCurrentServerURL: " . $book->getCurrentServerURL());
1209 $log->logLine("getCurrentPageURL..: " . $book->getCurrentPageURL());
1211 $book->setTitle(_('wallabag\'s articles'));
1212 $book->setIdentifier("http://$_SERVER[HTTP_HOST]", EPub
::IDENTIFIER_URI
); // Could also be the ISBN number, prefered for published books, or a UUID.
1213 //$book->setLanguage("en"); // Not needed, but included for the example, Language is mandatory, but EPub defaults to "en". Use RFC3066 Language codes, such as "en", "da", "fr" etc.
1214 $book->setDescription(_("Some articles saved on my wallabag"));
1215 $book->setAuthor("wallabag","wallabag");
1216 $book->setPublisher("wallabag","wallabag"); // I hope this is a non existant address :)
1217 $book->setDate(time()); // Strictly not needed as the book date defaults to time().
1218 //$book->setRights("Copyright and licence information specific for the book."); // As this is generated, this _could_ contain the name or licence information of the user who purchased the book, if needed. If this is used that way, the identifier must also be made unique for the book.
1219 $book->setSourceURL("http://$_SERVER[HTTP_HOST]");
1221 $book->addDublinCoreMetadata(DublinCore
::CONTRIBUTOR
, "PHP");
1222 $book->addDublinCoreMetadata(DublinCore
::CONTRIBUTOR
, "wallabag");
1224 $cssData = "body {\n margin-left: .5em;\n margin-right: .5em;\n text-align: justify;\n}\n\np {\n font-family: serif;\n font-size: 10pt;\n text-align: justify;\n text-indent: 1em;\n margin-top: 0px;\n margin-bottom: 1ex;\n}\n\nh1, h2 {\n font-family: sans-serif;\n font-style: italic;\n text-align: center;\n background-color: #6b879c;\n color: white;\n width: 100%;\n}\n\nh1 {\n margin-bottom: 2px;\n}\n\nh2 {\n margin-top: -2px;\n margin-bottom: 2px;\n}\n";
1226 $log->logLine("Add Cover");
1228 $fullTitle = "<h1> " . $bookTitle . "</h1>\n";
1230 $book->setCoverImage("Cover.png", file_get_contents("themes/baggy/img/apple-touch-icon-152.png"), "image/png", $fullTitle);
1232 $cover = $content_start . '<div style="text-align:center;"><p>' . _('Produced by wallabag with PHPePub') . '</p><p>'. _('Please open <a href="https://github.com/wallabag/wallabag/issues" >an issue</a> if you have trouble with the display of this E-Book on your device.') . '</p></div>' . $bookEnd;
1234 //$book->addChapter("Table of Contents", "TOC.xhtml", NULL, false, EPub::EXTERNAL_REF_IGNORE);
1235 $book->addChapter("Notices", "Cover2.html", $cover);
1239 foreach ($entries as $entry) { //set tags as subjects
1240 $tags = $this->store
->retrieveTagsByEntry($entry['id']);
1241 foreach ($tags as $tag) {
1242 $book->setSubject($tag['value']);
1245 $log->logLine("Set up parameters");
1247 $chapter = $content_start . $entry['content'] . $bookEnd;
1248 $book->addChapter($entry['title'], htmlspecialchars($entry['title']) . ".html", $chapter, true, EPub
::EXTERNAL_REF_ADD
);
1249 $log->logLine("Added chapter " . $entry['title']);
1253 $epuplog = $book->getLog();
1254 $book->addChapter("Log", "Log.html", $content_start . $log->getLog() . "\n</pre>" . $bookEnd); // log generation
1257 $zipData = $book->sendBook($bookFileName);