3 * Shaarli - The personal, minimalist, super-fast, database free, bookmarking service.
5 * Friendly fork by the Shaarli community:
6 * - https://github.com/shaarli/Shaarli
8 * Original project by sebsauvage.net:
9 * - http://sebsauvage.net/wiki/doku.php?id=php:shaarli
10 * - https://github.com/sebsauvage/Shaarli
12 * Licence: http://www.opensource.org/licenses/zlib-license.php
15 // Set 'UTC' as the default timezone if it is not defined in php.ini
16 // See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
17 if (date_default_timezone_get() == '') {
18 date_default_timezone_set('UTC');
25 // http://server.com/x/shaarli --> /shaarli/
26 define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+
strrpos($_SERVER['REQUEST_URI'], '/', 0)));
28 // High execution time in case of problematic imports/exports.
29 ini_set('max_input_time', '60');
31 // Try to set max upload file size and read
32 ini_set('memory_limit', '128M');
33 ini_set('post_max_size', '16M');
34 ini_set('upload_max_filesize', '16M');
36 // See all error except warnings
37 error_reporting(E_ALL^E_WARNING
);
39 // 3rd-party libraries
40 if (! file_exists(__DIR__
. '/vendor/autoload.php')) {
41 header('Content-Type: text/plain; charset=utf-8');
42 echo "Error: missing Composer configuration\n\n"
43 ."If you installed Shaarli through Git or using the development branch,\n"
44 ."please refer to the installation documentation to install PHP"
45 ." dependencies using Composer:\n"
46 ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
47 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
50 require_once 'inc/rain.tpl.class.php';
51 require_once __DIR__
. '/vendor/autoload.php';
54 require_once 'application/bookmark/LinkUtils.php';
55 require_once 'application/config/ConfigPlugin.php';
56 require_once 'application/http/HttpUtils.php';
57 require_once 'application/http/UrlUtils.php';
58 require_once 'application/updater/UpdaterUtils.php';
59 require_once 'application/FileUtils.php';
60 require_once 'application/TimeZone.php';
61 require_once 'application/Utils.php';
63 use Shaarli\ApplicationUtils
;
64 use Shaarli\Bookmark\Bookmark
;
65 use Shaarli\Bookmark\BookmarkFileService
;
66 use Shaarli\Bookmark\BookmarkFilter
;
67 use Shaarli\Bookmark\BookmarkServiceInterface
;
68 use Shaarli\Bookmark\Exception\BookmarkNotFoundException
;
69 use Shaarli\Config\ConfigManager
;
70 use Shaarli\Container\ContainerBuilder
;
71 use Shaarli\Feed\CachedPage
;
72 use Shaarli\Feed\FeedBuilder
;
73 use Shaarli\Formatter\BookmarkMarkdownFormatter
;
74 use Shaarli\Formatter\FormatterFactory
;
76 use Shaarli\Languages
;
77 use Shaarli\Netscape\NetscapeBookmarkUtils
;
78 use Shaarli\Plugin\PluginManager
;
79 use Shaarli\Render\PageBuilder
;
80 use Shaarli\Render\PageCacheManager
;
81 use Shaarli\Render\ThemeUtils
;
83 use Shaarli\Security\LoginManager
;
84 use Shaarli\Security\SessionManager
;
85 use Shaarli\Thumbnailer
;
86 use Shaarli\Updater\Updater
;
87 use Shaarli\Updater\UpdaterUtils
;
90 // Ensure the PHP version is supported
92 ApplicationUtils
::checkPHPVersion('7.1', PHP_VERSION
);
93 } catch (Exception
$exc) {
94 header('Content-Type: text/plain; charset=utf-8');
95 echo $exc->getMessage();
99 define('SHAARLI_VERSION', ApplicationUtils
::getVersion(__DIR__
.'/'. ApplicationUtils
::$VERSION_FILE));
101 // Force cookie path (but do not change lifetime)
102 $cookie = session_get_cookie_params();
104 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
105 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
107 // Set default cookie expiration and path.
108 session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
109 // Set session parameters on server side.
110 // Use cookies to store session.
111 ini_set('session.use_cookies', 1);
112 // Force cookies for session (phpsessionID forbidden in URL).
113 ini_set('session.use_only_cookies', 1);
114 // Prevent PHP form using sessionID in URL if cookies are disabled.
115 ini_set('session.use_trans_sid', false);
117 session_name('shaarli');
118 // Start session if needed (Some server auto-start sessions).
119 if (session_status() == PHP_SESSION_NONE
) {
123 // Regenerate session ID if invalid or not defined in cookie.
124 if (isset($_COOKIE['shaarli']) && !SessionManager
::checkId($_COOKIE['shaarli'])) {
125 session_regenerate_id(true);
126 $_COOKIE['shaarli'] = session_id();
129 $conf = new ConfigManager();
131 // In dev mode, throw exception on any warning
132 if ($conf->get('dev.debug', false)) {
133 // See all errors (for debugging only)
136 set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) {
137 throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
141 $sessionManager = new SessionManager($_SESSION, $conf);
142 $loginManager = new LoginManager($conf, $sessionManager);
143 $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
144 $clientIpId = client_ip_id($_SERVER);
146 // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
147 if (! defined('LC_MESSAGES')) {
148 define('LC_MESSAGES', LC_COLLATE
);
151 // Sniff browser language and set date format accordingly.
152 if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
153 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
156 new Languages(setlocale(LC_MESSAGES
, 0), $conf);
158 $conf->setEmpty('general.timezone', date_default_timezone_get());
159 $conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER)));
160 RainTPL
::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
161 RainTPL
::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
163 $pluginManager = new PluginManager($conf);
164 $pluginManager->load($conf->get('general.enabled_plugins'));
166 date_default_timezone_set($conf->get('general.timezone', 'UTC'));
168 ob_start(); // Output buffering for the page cache.
170 // Prevent caching on client side or proxy: (yes, it's ugly)
171 header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
172 header("Cache-Control: no-store, no-cache, must-revalidate");
173 header("Cache-Control: post-check=0, pre-check=0", false);
174 header("Pragma: no-cache");
176 if (! is_file($conf->getConfigFileExt())) {
177 // Ensure Shaarli has proper access to its resources
178 $errors = ApplicationUtils
::checkResourcePermissions($conf);
180 if ($errors != array()) {
181 $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
183 foreach ($errors as $error) {
184 $message .= '<li>'.$error.'</li>';
188 header('Content-Type: text/html; charset=utf-8');
193 // Display the installation form if no existing config is found
194 install($conf, $sessionManager, $loginManager);
197 $loginManager->checkLoginState($_COOKIE, $clientIpId);
200 * Adapter function to ensure compatibility with third-party templates
202 * @see https://github.com/shaarli/Shaarli/pull/1086
204 * @return bool true when the user is logged in, false otherwise
206 function isLoggedIn()
208 global $loginManager;
209 return $loginManager->isLoggedIn();
213 // ------------------------------------------------------------------------------------------
214 // Process login form: Check if login/password is correct.
215 if (isset($_POST['login'])) {
216 if (! $loginManager->canLogin($_SERVER)) {
217 die(t('I said: NO. You are banned for the moment. Go away.'));
219 if (isset($_POST['password'])
220 && $sessionManager->checkToken($_POST['token'])
221 && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
223 $loginManager->handleSuccessfulLogin($_SERVER);
226 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
227 // Note: Never forget the trailing slash on the cookie path!
228 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
231 if (!empty($_POST['longlastingsession'])) {
232 // Keep the session cookie even after the browser closes
233 $sessionManager->setStaySignedIn(true);
234 $expirationTime = $sessionManager->extendSession();
237 $loginManager::$STAY_SIGNED_IN_COOKIE,
238 $loginManager->getStaySignedInToken(),
243 // Standard session expiration (=when browser closes)
247 // Send cookie with the new expiration date to the browser
249 session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
251 session_regenerate_id(true);
253 // Optional redirect after login:
254 if (isset($_GET['post'])) {
255 $uri = './?post='. urlencode($_GET['post']);
256 foreach (array('description', 'source', 'title', 'tags') as $param) {
257 if (!empty($_GET[$param])) {
258 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
261 header('Location: '. $uri);
265 if (isset($_GET['edit_link'])) {
266 header('Location: ./?edit_link='. escape($_GET['edit_link']));
270 if (isset($_POST['returnurl'])) {
271 // Prevent loops over login screen.
272 if (strpos($_POST['returnurl'], '/login') === false) {
273 header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
277 header('Location: ./?');
280 $loginManager->handleFailedLogin($_SERVER);
281 $redir = '?username='. urlencode($_POST['login']);
282 if (isset($_GET['post'])) {
283 $redir .= '&post=' . urlencode($_GET['post']);
284 foreach (array('description', 'source', 'title', 'tags') as $param) {
285 if (!empty($_GET[$param])) {
286 $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
290 // Redirect to login screen.
291 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'./login'.$redir.'\';</script>';
296 // ------------------------------------------------------------------------------------------
297 // Token management for XSRF protection
298 // Token should be used in any form which acts on data (create,update,delete,import...).
299 if (!isset($_SESSION['tokens'])) {
300 $_SESSION['tokens']=array(); // Token are attached to the session.
304 * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
305 * Gives the last 7 days (which have bookmarks).
306 * This RSS feed cannot be filtered.
308 * @param BookmarkServiceInterface $bookmarkService
309 * @param ConfigManager $conf Configuration Manager instance
310 * @param LoginManager $loginManager LoginManager instance
312 function showDailyRSS($bookmarkService, $conf, $loginManager)
315 $query = $_SERVER['QUERY_STRING'];
316 $cache = new CachedPage(
317 $conf->get('config.PAGE_CACHE'),
319 startsWith($query, 'do=dailyrss') && !$loginManager->isLoggedIn()
321 $cached = $cache->cachedVersion();
322 if (!empty($cached)) {
327 /* Some Shaarlies may have very few bookmarks, so we need to look
328 back in time until we have enough days ($nb_of_days).
330 $nb_of_days = 7; // We take 7 days.
331 $today = date('Ymd');
334 foreach ($bookmarkService->search() as $bookmark) {
335 $day = $bookmark->getCreated()->format('Ymd'); // Extract day (without time)
336 if (strcmp($day, $today) < 0) {
337 if (empty($days[$day])) {
338 $days[$day] = array();
340 $days[$day][] = $bookmark;
343 if (count($days) > $nb_of_days) {
344 break; // Have we collected enough days?
348 // Build the RSS feed.
349 header('Content-Type: application/rss+xml; charset=utf-8');
350 $pageaddr = escape(index_url($_SERVER));
351 echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">';
353 echo '<title>Daily - '. $conf->get('general.title') . '</title>';
354 echo '<link>'. $pageaddr .'</link>';
355 echo '<description>Daily shared bookmarks</description>';
356 echo '<language>en-en</language>';
357 echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL
;
359 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
360 $formatter = $factory->getFormatter();
361 $formatter->addContextData('index_url', index_url($_SERVER));
363 /** @var Bookmark[] $bookmarks */
364 foreach ($days as $day => $bookmarks) {
365 $formattedBookmarks = [];
366 $dayDate = DateTime
::createFromFormat(Bookmark
::LINK_DATE_FORMAT
, $day.'_000000');
367 $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
369 // We pre-format some fields for proper output.
370 foreach ($bookmarks as $key => $bookmark) {
371 $formattedBookmarks[$key] = $formatter->format($bookmark);
372 // This page is a bit specific, we need raw description to calculate the length
373 $formattedBookmarks[$key]['formatedDescription'] = $formattedBookmarks[$key]['description'];
374 $formattedBookmarks[$key]['description'] = $bookmark->getDescription();
376 if ($bookmark->isNote()) {
377 $link['url'] = index_url($_SERVER) . $bookmark->getUrl(); // make permalink URL absolute
381 // Then build the HTML for this day:
382 $tpl = new RainTPL();
383 $tpl->assign('title', $conf->get('general.title'));
384 $tpl->assign('daydate', $dayDate->getTimestamp());
385 $tpl->assign('absurl', $absurl);
386 $tpl->assign('links', $formattedBookmarks);
387 $tpl->assign('rssdate', escape($dayDate->format(DateTime
::RSS
)));
388 $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
389 $tpl->assign('index_url', $pageaddr);
390 $html = $tpl->draw('dailyrss', true);
392 echo $html . PHP_EOL
;
394 echo '</channel></rss><!-- Cached version of '. escape(page_url($_SERVER)) .' -->';
396 $cache->cache(ob_get_contents());
402 * Renders the linklist
404 * @param pageBuilder $PAGE pageBuilder instance.
405 * @param BookmarkServiceInterface $linkDb instance.
406 * @param ConfigManager $conf Configuration Manager instance.
407 * @param PluginManager $pluginManager Plugin Manager instance.
409 function showLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
411 buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager);
412 $PAGE->renderPage('linklist');
416 * Render HTML page (according to URL parameters and user rights)
418 * @param ConfigManager $conf Configuration Manager instance.
419 * @param PluginManager $pluginManager Plugin Manager instance,
420 * @param BookmarkServiceInterface $bookmarkService
421 * @param History $history instance
422 * @param SessionManager $sessionManager SessionManager instance
423 * @param LoginManager $loginManager LoginManager instance
425 function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionManager, $loginManager)
427 $pageCacheManager = new PageCacheManager($conf->get('resource.page_cache'));
428 $updater = new Updater(
429 UpdaterUtils
::read_updates_file($conf->get('resource.updates')),
432 $loginManager->isLoggedIn()
435 $newUpdates = $updater->update();
436 if (! empty($newUpdates)) {
437 UpdaterUtils
::write_updates_file(
438 $conf->get('resource.updates'),
439 $updater->getDoneUpdates()
442 $pageCacheManager->invalidateCaches();
444 } catch (Exception
$e) {
445 die($e->getMessage());
448 $PAGE = new PageBuilder($conf, $_SESSION, $bookmarkService, $sessionManager->generateToken(), $loginManager->isLoggedIn());
449 $PAGE->assign('linkcount', $bookmarkService->count(BookmarkFilter
::$ALL));
450 $PAGE->assign('privateLinkcount', $bookmarkService->count(BookmarkFilter
::$PRIVATE));
451 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
453 // Determine which page will be rendered.
454 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
455 $targetPage = Router
::findPage($query, $_GET, $loginManager->isLoggedIn());
457 if (// if the user isn't logged in
458 !$loginManager->isLoggedIn() &&
459 // and Shaarli doesn't have public content...
460 $conf->get('privacy.hide_public_links') &&
461 // and is configured to enforce the login
462 $conf->get('privacy.force_login') &&
463 // and the current page isn't already the login page
464 $targetPage !== Router
::$PAGE_LOGIN &&
465 // and the user is not requesting a feed (which would lead to a different content-type as expected)
466 $targetPage !== Router
::$PAGE_FEED_ATOM &&
467 $targetPage !== Router
::$PAGE_FEED_RSS
469 // force current page to be the login page
470 $targetPage = Router
::$PAGE_LOGIN;
473 // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
474 // Then assign generated data to RainTPL.
475 $common_hooks = array(
481 foreach ($common_hooks as $name) {
482 $plugin_data = array();
483 $pluginManager->executeHooks(
487 'target' => $targetPage,
488 'loggedin' => $loginManager->isLoggedIn()
491 $PAGE->assign('plugins_' . $name, $plugin_data);
494 // -------- Display login form.
495 if ($targetPage == Router
::$PAGE_LOGIN) {
496 header('Location: ./login');
499 // -------- User wants to logout.
500 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) {
501 header('Location: ./logout');
505 // -------- Picture wall
506 if ($targetPage == Router
::$PAGE_PICWALL) {
507 header('Location: ./picture-wall');
511 // -------- Tag cloud
512 if ($targetPage == Router
::$PAGE_TAGCLOUD) {
513 header('Location: ./tag-cloud');
518 if ($targetPage == Router
::$PAGE_TAGLIST) {
519 header('Location: ./tag-list');
524 if ($targetPage == Router
::$PAGE_DAILY) {
525 $dayParam = !empty($_GET['day']) ? '?day=' . escape($_GET['day']) : '';
526 header('Location: ./daily'. $dayParam);
530 // ATOM and RSS feed.
531 if ($targetPage == Router
::$PAGE_FEED_ATOM || $targetPage == Router
::$PAGE_FEED_RSS) {
532 $feedType = $targetPage == Router
::$PAGE_FEED_RSS ? FeedBuilder
::$FEED_RSS : FeedBuilder
::$FEED_ATOM;
533 header('Content-Type: application/'. $feedType .'+xml; charset=utf-8');
536 $query = $_SERVER['QUERY_STRING'];
537 $cache = new CachedPage(
538 $conf->get('resource.page_cache'),
540 startsWith($query, 'do='. $targetPage) && !$loginManager->isLoggedIn()
542 $cached = $cache->cachedVersion();
543 if (!empty($cached)) {
548 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
550 $feedGenerator = new FeedBuilder(
552 $factory->getFormatter(),
556 $loginManager->isLoggedIn()
558 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE
, 0)));
559 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
560 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
561 $data = $feedGenerator->buildData();
563 // Process plugin hook.
564 $pluginManager->executeHooks('render_feed', $data, array(
565 'loggedin' => $loginManager->isLoggedIn(),
566 'target' => $targetPage,
569 // Render the template.
570 $PAGE->assignAll($data);
571 $PAGE->renderPage('feed.'. $feedType);
572 $cache->cache(ob_get_contents());
577 // Display opensearch plugin (XML)
578 if ($targetPage == Router
::$PAGE_OPENSEARCH) {
579 header('Content-Type: application/xml; charset=utf-8');
580 $PAGE->assign('serverurl', index_url($_SERVER));
581 $PAGE->renderPage('opensearch');
585 // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
586 if (isset($_GET['addtag'])) {
587 // Get previous URL (http_referer) and add the tag to the searchtags parameters in query.
588 if (empty($_SERVER['HTTP_REFERER'])) {
589 // In case browser does not send HTTP_REFERER
590 header('Location: ?searchtags='.urlencode($_GET['addtag']));
593 parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY
), $params);
595 // Prevent redirection loop
596 if (isset($params['addtag'])) {
597 unset($params['addtag']);
600 // Check if this tag is already in the search query and ignore it if it is.
601 // Each tag is always separated by a space
602 if (isset($params['searchtags'])) {
603 $current_tags = explode(' ', $params['searchtags']);
605 $current_tags = array();
608 foreach ($current_tags as $value) {
609 if ($value === $_GET['addtag']) {
614 // Append the tag if necessary
615 if (empty($params['searchtags'])) {
616 $params['searchtags'] = trim($_GET['addtag']);
618 $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']);
621 // We also remove page (keeping the same page has no sense, since the
622 // results are different)
623 unset($params['page']);
625 header('Location: ?'.http_build_query($params));
629 // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...)
630 if (isset($_GET['removetag'])) {
631 // Get previous URL (http_referer) and remove the tag from the searchtags parameters in query.
632 if (empty($_SERVER['HTTP_REFERER'])) {
633 header('Location: ?');
637 // In case browser does not send HTTP_REFERER
638 parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY
), $params);
640 // Prevent redirection loop
641 if (isset($params['removetag'])) {
642 unset($params['removetag']);
645 if (isset($params['searchtags'])) {
646 $tags = explode(' ', $params['searchtags']);
647 // Remove value from array $tags.
648 $tags = array_diff($tags, array($_GET['removetag']));
649 $params['searchtags'] = implode(' ', $tags);
651 if (empty($params['searchtags'])) {
652 unset($params['searchtags']);
655 // We also remove page (keeping the same page has no sense, since
656 // the results are different)
657 unset($params['page']);
659 header('Location: ?'.http_build_query($params));
663 // -------- User wants to change the number of bookmarks per page (linksperpage=...)
664 if (isset($_GET['linksperpage'])) {
665 if (is_numeric($_GET['linksperpage'])) {
666 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
669 if (! empty($_SERVER['HTTP_REFERER'])) {
670 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
674 header('Location: '. $location);
678 // -------- User wants to see only private bookmarks (toggle)
679 if (isset($_GET['visibility'])) {
680 if ($_GET['visibility'] === 'private') {
681 // Visibility not set or not already private, set private, otherwise reset it
682 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
683 // See only private bookmarks
684 $_SESSION['visibility'] = 'private';
686 unset($_SESSION['visibility']);
688 } elseif ($_GET['visibility'] === 'public') {
689 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
690 // See only public bookmarks
691 $_SESSION['visibility'] = 'public';
693 unset($_SESSION['visibility']);
697 if (! empty($_SERVER['HTTP_REFERER'])) {
698 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
702 header('Location: '. $location);
706 // -------- User wants to see only untagged bookmarks (toggle)
707 if (isset($_GET['untaggedonly'])) {
708 $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
710 if (! empty($_SERVER['HTTP_REFERER'])) {
711 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
715 header('Location: '. $location);
719 // -------- Handle other actions allowed for non-logged in users:
720 if (!$loginManager->isLoggedIn()) {
721 // User tries to post new link but is not logged in:
722 // Show login screen, then redirect to ?post=...
723 if (isset($_GET['post'])) {
724 header( // Redirect to login page, then back to post link.
725 'Location: ./login?post='.urlencode($_GET['post']).
726 (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
727 (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
728 (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
729 (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
734 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
735 if (isset($_GET['edit_link'])) {
736 header('Location: ./login?edit_link='. escape($_GET['edit_link']));
740 exit; // Never remove this one! All operations below are reserved for logged in user.
743 // -------- All other functions are reserved for the registered user:
745 // -------- Display the Tools menu if requested (import/export/bookmarklet...)
746 if ($targetPage == Router
::$PAGE_TOOLS) {
748 'pageabsaddr' => index_url($_SERVER),
749 'sslenabled' => is_https($_SERVER),
751 $pluginManager->executeHooks('render_tools', $data);
753 foreach ($data as $key => $value) {
754 $PAGE->assign($key, $value);
757 $PAGE->assign('pagetitle', t('Tools') .' - '. $conf->get('general.title', 'Shaarli'));
758 $PAGE->renderPage('tools');
762 // -------- User wants to change his/her password.
763 if ($targetPage == Router
::$PAGE_CHANGEPASSWORD) {
764 if ($conf->get('security.open_shaarli')) {
765 die(t('You are not supposed to change a password on an Open Shaarli.'));
768 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) {
769 if (!$sessionManager->checkToken($_POST['token'])) {
770 die(t('Wrong token.')); // Go away!
773 // Make sure old password is correct.
775 $_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')
777 if ($oldhash != $conf->get('credentials.hash')) {
778 echo '<script>alert("'
779 . t('The old password is not correct.')
780 .'");document.location=\'./?do=changepasswd\';</script>';
784 // Salt renders rainbow-tables attacks useless.
785 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
789 $_POST['setpassword']
790 . $conf->get('credentials.login')
791 . $conf->get('credentials.salt')
795 $conf->write($loginManager->isLoggedIn());
796 } catch (Exception
$e) {
798 'ERROR while writing config file after changing password.' . PHP_EOL
.
802 // TODO: do not handle exceptions/errors in JS.
803 echo '<script>alert("'. $e->getMessage() .'");document.location=\'./?do=tools\';</script>';
806 echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'./?do=tools\';</script>';
809 // show the change password form.
810 $PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
811 $PAGE->renderPage('changepassword');
816 // -------- User wants to change configuration
817 if ($targetPage == Router
::$PAGE_CONFIGURE) {
818 if (!empty($_POST['title'])) {
819 if (!$sessionManager->checkToken($_POST['token'])) {
820 die(t('Wrong token.')); // Go away!
823 if (!empty($_POST['continent']) && !empty($_POST['city'])
824 && isTimeZoneValid($_POST['continent'], $_POST['city'])
826 $tz = $_POST['continent'] . '/' . $_POST['city'];
828 $conf->set('general.timezone', $tz);
829 $conf->set('general.title', escape($_POST['title']));
830 $conf->set('general.header_link', escape($_POST['titleLink']));
831 $conf->set('general.retrieve_description', !empty($_POST['retrieveDescription']));
832 $conf->set('resource.theme', escape($_POST['theme']));
833 $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
834 $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
835 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
836 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
837 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
838 $conf->set('api.enabled', !empty($_POST['enableApi']));
839 $conf->set('api.secret', escape($_POST['apiSecret']));
840 $conf->set('formatter', escape($_POST['formatter']));
842 if (! empty($_POST['language'])) {
843 $conf->set('translation.language', escape($_POST['language']));
846 $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer
::MODE_NONE
;
847 if ($thumbnailsMode !== Thumbnailer
::MODE_NONE
848 && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer
::MODE_NONE
)
850 $_SESSION['warnings'][] = t(
851 'You have enabled or changed thumbnails mode. '
852 .'<a href="./?do=thumbs_update">Please synchronize them</a>.'
855 $conf->set('thumbnails.mode', $thumbnailsMode);
858 $conf->write($loginManager->isLoggedIn());
859 $history->updateSettings();
860 $pageCacheManager->invalidateCaches();
861 } catch (Exception
$e) {
863 'ERROR while writing config file after configuration update.' . PHP_EOL
.
867 // TODO: do not handle exceptions/errors in JS.
868 echo '<script>alert("'. $e->getMessage() .'");document.location=\'./?do=configure\';</script>';
871 echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'./?do=configure\';</script>';
874 // Show the configuration form.
875 $PAGE->assign('title', $conf->get('general.title'));
876 $PAGE->assign('theme', $conf->get('resource.theme'));
877 $PAGE->assign('theme_available', ThemeUtils
::getThemes($conf->get('resource.raintpl_tpl')));
878 $PAGE->assign('formatter_available', ['default', 'markdown']);
879 list($continents, $cities) = generateTimeZoneData(
880 timezone_identifiers_list(),
881 $conf->get('general.timezone')
883 $PAGE->assign('continents', $continents);
884 $PAGE->assign('cities', $cities);
885 $PAGE->assign('retrieve_description', $conf->get('general.retrieve_description'));
886 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
887 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
888 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
889 $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
890 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
891 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
892 $PAGE->assign('api_secret', $conf->get('api.secret'));
893 $PAGE->assign('languages', Languages
::getAvailableLanguages());
894 $PAGE->assign('gd_enabled', extension_loaded('gd'));
895 $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer
::MODE_NONE
));
896 $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
897 $PAGE->renderPage('configure');
902 // -------- User wants to rename a tag or delete it
903 if ($targetPage == Router
::$PAGE_CHANGETAG) {
904 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
905 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
906 $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
907 $PAGE->renderPage('changetag');
911 if (!$sessionManager->checkToken($_POST['token'])) {
912 die(t('Wrong token.'));
915 $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
916 $fromTag = escape($_POST['fromtag']);
918 $bookmarks = $bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter
::$ALL, true);
919 foreach ($bookmarks as $bookmark) {
921 $bookmark->renameTag($fromTag, $toTag);
923 $bookmark->deleteTag($fromTag);
925 $bookmarkService->set($bookmark, false);
926 $history->updateLink($bookmark);
929 $bookmarkService->save();
930 $delete = empty($_POST['totag']);
931 $redirect = $delete ? './do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
933 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d bookmarks.', $count), $count)
934 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d bookmarks.', $count), $count);
935 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
939 // -------- User wants to add a link without using the bookmarklet: Show form.
940 if ($targetPage == Router
::$PAGE_ADDLINK) {
941 $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
942 $PAGE->renderPage('addlink');
946 // -------- User clicked the "Save" button when editing a link: Save link to database.
947 if (isset($_POST['save_edit'])) {
949 if (! $sessionManager->checkToken($_POST['token'])) {
950 die(t('Wrong token.'));
953 // lf_id should only be present if the link exists.
954 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null;
955 if ($id && $bookmarkService->exists($id)) {
957 $bookmark = $bookmarkService->get($id);
960 $bookmark = new Bookmark();
963 $bookmark->setTitle($_POST['lf_title']);
964 $bookmark->setDescription($_POST['lf_description']);
965 $bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols'));
966 $bookmark->setPrivate(isset($_POST['lf_private']));
967 $bookmark->setTagsString($_POST['lf_tags']);
969 if ($conf->get('thumbnails.mode', Thumbnailer
::MODE_NONE
) !== Thumbnailer
::MODE_NONE
970 && ! $bookmark->isNote()
972 $thumbnailer = new Thumbnailer($conf);
973 $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
975 $bookmarkService->addOrSet($bookmark, false);
977 // To preserve backward compatibility with 3rd parties, plugins still use arrays
978 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
979 $formatter = $factory->getFormatter('raw');
980 $data = $formatter->format($bookmark);
981 $pluginManager->executeHooks('save_link', $data);
983 $bookmark->fromArray($data);
984 $bookmarkService->set($bookmark);
986 // If we are called from the bookmarklet, we must close the popup:
987 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
988 echo '<script>self.close();</script>';
992 $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
993 $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
994 // Scroll to the link which has been edited.
995 $location .= '#' . $bookmark->getShortUrl();
996 // After saving the link, redirect to the page the user was on.
997 header('Location: '. $location);
1001 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1002 if ($targetPage == Router
::$PAGE_DELETELINK) {
1003 if (! $sessionManager->checkToken($_GET['token'])) {
1004 die(t('Wrong token.'));
1007 $ids = trim($_GET['lf_linkdate']);
1008 if (strpos($ids, ' ') !== false) {
1009 // multiple, space-separated ids provided
1010 $ids = array_values(array_filter(
1011 preg_split('/\s+/', escape($ids)),
1013 return $item !== '';
1017 // only a single id provided
1018 $shortUrl = $bookmarkService->get($ids)->getShortUrl();
1021 // assert at least one id is given
1023 die('no id provided');
1025 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1026 $formatter = $factory->getFormatter('raw');
1027 foreach ($ids as $id) {
1028 $id = (int) escape($id);
1029 $bookmark = $bookmarkService->get($id);
1030 $data = $formatter->format($bookmark);
1031 $pluginManager->executeHooks('delete_link', $data);
1032 $bookmarkService->remove($bookmark, false);
1034 $bookmarkService->save();
1036 // If we are called from the bookmarklet, we must close the popup:
1037 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1038 echo '<script>self.close();</script>';
1043 if (isset($_SERVER['HTTP_REFERER'])) {
1044 // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404.
1045 $location = generateLocation(
1046 $_SERVER['HTTP_REFERER'],
1047 $_SERVER['HTTP_HOST'],
1048 ['delete_link', 'edit_link', ! empty($shortUrl) ? $shortUrl : null]
1052 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
1056 // -------- User clicked either "Set public" or "Set private" bulk operation
1057 if ($targetPage == Router
::$PAGE_CHANGE_VISIBILITY) {
1058 if (! $sessionManager->checkToken($_GET['token'])) {
1059 die(t('Wrong token.'));
1062 $ids = trim($_GET['ids']);
1063 if (strpos($ids, ' ') !== false) {
1064 // multiple, space-separated ids provided
1065 $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
1067 // only a single id provided
1071 // assert at least one id is given
1073 die('no id provided');
1075 // assert that the visibility is valid
1076 if (!isset($_GET['newVisibility']) || !in_array($_GET['newVisibility'], ['public', 'private'])) {
1077 die('invalid visibility');
1079 $private = $_GET['newVisibility'] === 'private';
1081 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1082 $formatter = $factory->getFormatter('raw');
1083 foreach ($ids as $id) {
1084 $id = (int) escape($id);
1085 $bookmark = $bookmarkService->get($id);
1086 $bookmark->setPrivate($private);
1088 // To preserve backward compatibility with 3rd parties, plugins still use arrays
1089 $data = $formatter->format($bookmark);
1090 $pluginManager->executeHooks('save_link', $data);
1091 $bookmark->fromArray($data);
1093 $bookmarkService->set($bookmark);
1095 $bookmarkService->save();
1098 if (isset($_SERVER['HTTP_REFERER'])) {
1099 $location = generateLocation(
1100 $_SERVER['HTTP_REFERER'],
1101 $_SERVER['HTTP_HOST']
1104 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
1108 // -------- User clicked the "EDIT" button on a link: Display link edit form.
1109 if (isset($_GET['edit_link'])) {
1110 $id = (int) escape($_GET['edit_link']);
1112 $link = $bookmarkService->get($id); // Read database
1113 } catch (BookmarkNotFoundException
$e) {
1114 // Link not found in database.
1115 header('Location: ?');
1119 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1120 $formatter = $factory->getFormatter('raw');
1121 $formattedLink = $formatter->format($link);
1122 $tags = $bookmarkService->bookmarksCountPerTag();
1123 if ($conf->get('formatter') === 'markdown') {
1124 $tags[BookmarkMarkdownFormatter
::NO_MD_TAG
] = 1;
1127 'link' => $formattedLink,
1128 'link_is_new' => false,
1129 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1132 $pluginManager->executeHooks('render_editlink', $data);
1134 foreach ($data as $key => $value) {
1135 $PAGE->assign($key, $value);
1138 $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1139 $PAGE->renderPage('editlink');
1143 // -------- User want to post a new link: Display link edit form.
1144 if (isset($_GET['post'])) {
1145 $url = cleanup_url($_GET['post']);
1147 $link_is_new = false;
1148 // Check if URL is not already in database (in this case, we will edit the existing link)
1149 $bookmark = $bookmarkService->findByUrl($url);
1151 $link_is_new = true;
1152 // Get title if it was provided in URL (by the bookmarklet).
1153 $title = empty($_GET['title']) ? '' : escape($_GET['title']);
1154 // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
1155 $description = empty($_GET['description']) ? '' : escape($_GET['description']);
1156 $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
1157 $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
1159 // If this is an HTTP(S) link, we try go get the page to extract
1160 // the title (otherwise we will to straight to the edit form.)
1161 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
1162 $retrieveDescription = $conf->get('general.retrieve_description');
1163 // Short timeout to keep the application responsive
1164 // The callback will fill $charset and $title with data from the downloaded page.
1167 $conf->get('general.download_timeout', 30),
1168 $conf->get('general.download_max_size', 4194304),
1169 get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
1171 if (! empty($title) && strtolower($charset) != 'utf-8') {
1172 $title = mb_convert_encoding($title, 'utf-8', $charset);
1177 $title = $conf->get('general.default_note_title', t('Note: '));
1179 $url = escape($url);
1180 $title = escape($title);
1185 'description' => $description,
1187 'private' => $private,
1190 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1191 $formatter = $factory->getFormatter('raw');
1192 $link = $formatter->format($bookmark);
1195 $tags = $bookmarkService->bookmarksCountPerTag();
1196 if ($conf->get('formatter') === 'markdown') {
1197 $tags[BookmarkMarkdownFormatter
::NO_MD_TAG
] = 1;
1201 'link_is_new' => $link_is_new,
1202 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1203 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
1205 'default_private_links' => $conf->get('privacy.default_private_links', false),
1207 $pluginManager->executeHooks('render_editlink', $data);
1209 foreach ($data as $key => $value) {
1210 $PAGE->assign($key, $value);
1213 $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1214 $PAGE->renderPage('editlink');
1218 if ($targetPage == Router
::$PAGE_PINLINK) {
1219 if (! isset($_GET['id']) || !$bookmarkService->exists($_GET['id'])) {
1220 // FIXME! Use a proper error system.
1221 $msg = t('Invalid link ID provided');
1222 echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>';
1225 if (! $sessionManager->checkToken($_GET['token'])) {
1226 die('Wrong token.');
1229 $link = $bookmarkService->get($_GET['id']);
1230 $link->setSticky(! $link->isSticky());
1231 $bookmarkService->set($link);
1232 header('Location: '.index_url($_SERVER));
1236 if ($targetPage == Router
::$PAGE_EXPORT) {
1237 // Export bookmarks as a Netscape Bookmarks file
1239 if (empty($_GET['selection'])) {
1240 $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
1241 $PAGE->renderPage('export');
1245 // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
1246 $selection = $_GET['selection'];
1247 if (isset($_GET['prepend_note_url'])) {
1248 $prependNoteUrl = $_GET['prepend_note_url'];
1250 $prependNoteUrl = false;
1254 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1255 $formatter = $factory->getFormatter('raw');
1258 NetscapeBookmarkUtils
::filterAndFormat(
1266 } catch (Exception
$exc) {
1267 header('Content-Type: text/plain; charset=utf-8');
1268 echo $exc->getMessage();
1271 $now = new DateTime();
1272 header('Content-Type: text/html; charset=utf-8');
1274 'Content-disposition: attachment; filename=bookmarks_'
1275 .$selection.'_'.$now->format(Bookmark
::LINK_DATE_FORMAT
).'.html'
1277 $PAGE->assign('date', $now->format(DateTime
::RFC822
));
1278 $PAGE->assign('eol', PHP_EOL
);
1279 $PAGE->assign('selection', $selection);
1280 $PAGE->renderPage('export.bookmarks');
1284 if ($targetPage == Router
::$PAGE_IMPORT) {
1285 // Upload a Netscape bookmark dump to import its contents
1287 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1288 // Show import dialog
1291 get_max_upload_size(
1292 ini_get('post_max_size'),
1293 ini_get('upload_max_filesize'),
1299 get_max_upload_size(
1300 ini_get('post_max_size'),
1301 ini_get('upload_max_filesize'),
1305 $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli'));
1306 $PAGE->renderPage('import');
1310 // Import bookmarks from an uploaded file
1311 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1312 // The file is too big or some form field may be missing.
1315 'The file you are trying to upload is probably bigger than what this webserver can accept'
1316 .' (%s). Please upload in smaller chunks.'
1318 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
1320 echo '<script>alert("'. $msg .'");document.location=\'./?do='.Router
::$PAGE_IMPORT .'\';</script>';
1323 if (! $sessionManager->checkToken($_POST['token'])) {
1324 die('Wrong token.');
1326 $status = NetscapeBookmarkUtils
::import(
1333 echo '<script>alert("'.$status.'");document.location=\'./?do='
1334 .Router
::$PAGE_IMPORT .'\';</script>';
1338 // Plugin administration page
1339 if ($targetPage == Router
::$PAGE_PLUGINSADMIN) {
1340 $pluginMeta = $pluginManager->getPluginsMeta();
1342 // Split plugins into 2 arrays: ordered enabled plugins and disabled.
1343 $enabledPlugins = array_filter($pluginMeta, function ($v) {
1344 return $v['order'] !== false;
1347 $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array()));
1351 return $a['order'] - $b['order'];
1354 $disabledPlugins = array_filter($pluginMeta, function ($v) {
1355 return $v['order'] === false;
1358 $PAGE->assign('enabledPlugins', $enabledPlugins);
1359 $PAGE->assign('disabledPlugins', $disabledPlugins);
1360 $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
1361 $PAGE->renderPage('pluginsadmin');
1365 // Plugin administration form action
1366 if ($targetPage == Router
::$PAGE_SAVE_PLUGINSADMIN) {
1368 if (isset($_POST['parameters_form'])) {
1369 $pluginManager->executeHooks('save_plugin_parameters', $_POST);
1370 unset($_POST['parameters_form']);
1371 foreach ($_POST as $param => $value) {
1372 $conf->set('plugins.'. $param, escape($value));
1375 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
1377 $conf->write($loginManager->isLoggedIn());
1378 $history->updateSettings();
1379 } catch (Exception
$e) {
1381 'ERROR while saving plugin configuration:.' . PHP_EOL
.
1385 // TODO: do not handle exceptions/errors in JS.
1386 echo '<script>alert("'
1388 .'");document.location=\'./?do='
1389 . Router
::$PAGE_PLUGINSADMIN
1393 header('Location: ./?do='. Router
::$PAGE_PLUGINSADMIN);
1397 // Get a fresh token
1398 if ($targetPage == Router
::$GET_TOKEN) {
1399 header('Content-Type:text/plain');
1400 echo $sessionManager->generateToken();
1404 // -------- Thumbnails Update
1405 if ($targetPage == Router
::$PAGE_THUMBS_UPDATE) {
1407 foreach ($bookmarkService->search() as $bookmark) {
1408 // A note or not HTTP(S)
1409 if ($bookmark->isNote() || ! startsWith(strtolower($bookmark->getUrl()), 'http')) {
1412 $ids[] = $bookmark->getId();
1414 $PAGE->assign('ids', $ids);
1415 $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
1416 $PAGE->renderPage('thumbnails');
1420 // -------- Single Thumbnail Update
1421 if ($targetPage == Router
::$AJAX_THUMB_UPDATE) {
1422 if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
1423 http_response_code(400);
1426 $id = (int) $_POST['id'];
1427 if (! $bookmarkService->exists($id)) {
1428 http_response_code(404);
1431 $thumbnailer = new Thumbnailer($conf);
1432 $bookmark = $bookmarkService->get($id);
1433 $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
1434 $bookmarkService->set($bookmark);
1436 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1437 echo json_encode($factory->getFormatter('raw')->format($bookmark));
1441 // -------- Otherwise, simply display search form and bookmarks:
1442 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
1447 * Template for the list of bookmarks (<div id="linklist">)
1448 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1450 * @param pageBuilder $PAGE pageBuilder instance.
1451 * @param BookmarkServiceInterface $linkDb LinkDB instance.
1452 * @param ConfigManager $conf Configuration Manager instance.
1453 * @param PluginManager $pluginManager Plugin Manager instance.
1454 * @param LoginManager $loginManager LoginManager instance
1456 function buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
1458 $factory = new FormatterFactory($conf, $loginManager->isLoggedIn());
1459 $formatter = $factory->getFormatter();
1461 // Used in templates
1462 if (isset($_GET['searchtags'])) {
1463 if (! empty($_GET['searchtags'])) {
1464 $searchtags = escape(normalize_spaces($_GET['searchtags']));
1466 $searchtags = false;
1471 $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : '';
1474 if (! empty($_SERVER['QUERY_STRING'])
1475 && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
1477 $linksToDisplay = $linkDb->findByHash($_SERVER['QUERY_STRING']);
1478 } catch (BookmarkNotFoundException
$e) {
1479 $PAGE->render404($e->getMessage());
1483 // Filter bookmarks according search parameters.
1484 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : null;
1486 'searchtags' => $searchtags,
1487 'searchterm' => $searchterm,
1489 $linksToDisplay = $linkDb->search($request, $visibility, false, !empty($_SESSION['untaggedonly']));
1492 // ---- Handle paging.
1494 foreach ($linksToDisplay as $key => $value) {
1498 // Select articles according to paging.
1499 $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
1500 $pagecount = $pagecount == 0 ? 1 : $pagecount;
1501 $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
1502 $page = $page < 1 ? 1 : $page;
1503 $page = $page > $pagecount ? $pagecount : $page;
1505 $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
1506 $end = $i +
$_SESSION['LINKS_PER_PAGE'];
1508 $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer
::MODE_NONE
) !== Thumbnailer
::MODE_NONE
;
1509 if ($thumbnailsEnabled) {
1510 $thumbnailer = new Thumbnailer($conf);
1513 $linkDisp = array();
1514 while ($i<$end && $i<count($keys)) {
1515 $link = $formatter->format($linksToDisplay[$keys[$i]]);
1517 // Logged in, thumbnails enabled, not a note,
1518 // and (never retrieved yet or no valid cache file)
1519 if ($loginManager->isLoggedIn()
1520 && $thumbnailsEnabled
1521 && !$linksToDisplay[$keys[$i]]->isNote()
1522 && $linksToDisplay[$keys[$i]]->getThumbnail() !== false
1523 && ! is_file($linksToDisplay[$keys[$i]]->getThumbnail())
1525 $linksToDisplay[$keys[$i]]->setThumbnail($thumbnailer->get($link['url']));
1526 $linkDb->set($linksToDisplay[$keys[$i]], false);
1528 $link['thumbnail'] = $linksToDisplay[$keys[$i]]->getThumbnail();
1531 // Check for both signs of a note: starting with ? and 7 chars long.
1532 // if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
1533 // $link['url'] = index_url($_SERVER) . $link['url'];
1536 $linkDisp[$keys[$i]] = $link;
1540 // If we retrieved new thumbnails, we update the database.
1541 if (!empty($updateDB)) {
1545 // Compute paging navigation
1546 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
1547 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
1548 $previous_page_url = '';
1549 if ($i != count($keys)) {
1550 $previous_page_url = '?page=' . ($page+
1) . $searchtermUrl . $searchtagsUrl;
1554 $next_page_url = '?page=' . ($page-1) . $searchtermUrl . $searchtagsUrl;
1557 // Fill all template fields.
1559 'previous_page_url' => $previous_page_url,
1560 'next_page_url' => $next_page_url,
1561 'page_current' => $page,
1562 'page_max' => $pagecount,
1563 'result_count' => count($linksToDisplay),
1564 'search_term' => $searchterm,
1565 'search_tags' => $searchtags,
1566 'visibility' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '',
1567 'links' => $linkDisp,
1570 // If there is only a single link, we change on-the-fly the title of the page.
1571 if (count($linksToDisplay) == 1) {
1572 $data['pagetitle'] = $linksToDisplay[$keys[0]]->getTitle() .' - '. $conf->get('general.title');
1573 } elseif (! empty($searchterm) || ! empty($searchtags)) {
1574 $data['pagetitle'] = t('Search: ');
1575 $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
1576 $bracketWrap = function ($tag) {
1577 return '['. $tag .']';
1579 $data['pagetitle'] .= ! empty($searchtags)
1580 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchtags))).' '
1582 $data['pagetitle'] .= '- '. $conf->get('general.title');
1585 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
1587 foreach ($data as $key => $value) {
1588 $PAGE->assign($key, $value);
1596 * This function should NEVER be called if the file data/config.php exists.
1598 * @param ConfigManager $conf Configuration Manager instance.
1599 * @param SessionManager $sessionManager SessionManager instance
1600 * @param LoginManager $loginManager LoginManager instance
1602 function install($conf, $sessionManager, $loginManager)
1604 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
1605 if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) {
1606 mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705);
1610 // This part makes sure sessions works correctly.
1611 // (Because on some hosts, session.save_path may not be set correctly,
1612 // or we may not have write access to it.)
1613 if (isset($_GET['test_session'])
1614 && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) {
1615 // Step 2: Check if data in session is correct.
1617 '<pre>Sessions do not seem to work correctly on your server.<br>'.
1618 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
1619 'and that you have write access to it.<br>'.
1620 'It currently points to %s.<br>'.
1621 'On some browsers, accessing your server via a hostname like \'localhost\' '.
1622 'or any custom hostname without a dot causes cookie storage to fail. '.
1623 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
1625 $msg = sprintf($msg, session_save_path());
1627 echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
1630 if (!isset($_SESSION['session_tested'])) {
1631 // Step 1 : Try to store data in session and reload page.
1632 $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session.
1633 header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data.
1635 if (isset($_GET['test_session'])) {
1636 // Step 3: Sessions are OK. Remove test parameter from URL.
1637 header('Location: '.index_url($_SERVER));
1641 if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) {
1643 if (!empty($_POST['continent']) && !empty($_POST['city'])
1644 && isTimeZoneValid($_POST['continent'], $_POST['city'])
1646 $tz = $_POST['continent'].'/'.$_POST['city'];
1648 $conf->set('general.timezone', $tz);
1649 $login = $_POST['setlogin'];
1650 $conf->set('credentials.login', $login);
1651 $salt = sha1(uniqid('', true) .'_'. mt_rand());
1652 $conf->set('credentials.salt', $salt);
1653 $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
1654 if (!empty($_POST['title'])) {
1655 $conf->set('general.title', escape($_POST['title']));
1657 $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER)));
1659 $conf->set('translation.language', escape($_POST['language']));
1660 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1661 $conf->set('api.enabled', !empty($_POST['enableApi']));
1664 generate_api_secret(
1665 $conf->get('credentials.login'),
1666 $conf->get('credentials.salt')
1670 // Everything is ok, let's create config file.
1671 $conf->write($loginManager->isLoggedIn());
1672 } catch (Exception
$e) {
1674 'ERROR while writing config file after installation.' . PHP_EOL
.
1678 // TODO: do not handle exceptions/errors in JS.
1679 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
1683 $history = new History($conf->get('resource.history'));
1684 $bookmarkService = new BookmarkFileService($conf, $history, true);
1685 if ($bookmarkService->count() === 0) {
1686 $bookmarkService->initialize();
1689 echo '<script>alert('
1690 .'"Shaarli is now configured. '
1691 .'Please enter your login/password and start shaaring your bookmarks!"'
1692 .');document.location=\'./login\';</script>';
1696 $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
1697 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
1698 $PAGE->assign('continents', $continents);
1699 $PAGE->assign('cities', $cities);
1700 $PAGE->assign('languages', Languages
::getAvailableLanguages());
1701 $PAGE->renderPage('install');
1705 if (!isset($_SESSION['LINKS_PER_PAGE'])) {
1706 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
1710 $history = new History($conf->get('resource.history'));
1711 } catch (Exception
$e) {
1712 die($e->getMessage());
1715 $linkDb = new BookmarkFileService($conf, $history, $loginManager->isLoggedIn());
1717 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
1718 showDailyRSS($linkDb, $conf, $loginManager);
1722 $containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager, WEB_PATH
);
1723 $container = $containerBuilder->build();
1724 $app = new App($container);
1727 $app->group('/api/v1', function () {
1728 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
1729 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
1730 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
1731 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
1732 $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
1733 $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
1735 $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags');
1736 $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag');
1737 $this->put('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:putTag')->setName('putTag');
1738 $this->delete('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:deleteTag')->setName('deleteTag');
1740 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
1741 })->add('\Shaarli\Api\ApiMiddleware');
1743 $app->group('', function () {
1744 $this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login');
1745 $this->get('/logout', '\Shaarli\Front\Controller\LogoutController:index')->setName('logout');
1746 $this->get('/picture-wall', '\Shaarli\Front\Controller\PictureWallController:index')->setName('picwall');
1747 $this->get('/tag-cloud', '\Shaarli\Front\Controller\TagCloudController:cloud')->setName('tagcloud');
1748 $this->get('/tag-list', '\Shaarli\Front\Controller\TagCloudController:list')->setName('taglist');
1749 $this->get('/daily', '\Shaarli\Front\Controller\DailyController:index')->setName('daily');
1751 $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\TagController:addTag')->setName('add-tag');
1752 })->add('\Shaarli\Front\ShaarliMiddleware');
1754 $response = $app->run(true);
1756 // Hack to make Slim and Shaarli router work together:
1757 // If a Slim route isn't found and NOT API call, we call renderPage().
1758 if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
1759 // We use UTF-8 for proper international characters handling.
1760 header('Content-Type: text/html; charset=utf-8');
1761 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
1763 $response = $response
1764 ->withHeader('Access-Control-Allow-Origin', '*')
1766 'Access-Control-Allow-Headers',
1767 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
1769 ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
1770 $app->respond($response);