/shaarli/ define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0))); // High execution time in case of problematic imports/exports. ini_set('max_input_time', '60'); // Try to set max upload file size and read ini_set('memory_limit', '128M'); ini_set('post_max_size', '16M'); ini_set('upload_max_filesize', '16M'); // See all error except warnings error_reporting(E_ALL^E_WARNING); // 3rd-party libraries if (! file_exists(__DIR__ . '/vendor/autoload.php')) { header('Content-Type: text/plain; charset=utf-8'); echo "Error: missing Composer configuration\n\n" ."If you installed Shaarli through Git or using the development branch,\n" ."please refer to the installation documentation to install PHP" ." dependencies using Composer:\n" ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n" ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/"; exit; } require_once 'inc/rain.tpl.class.php'; require_once __DIR__ . '/vendor/autoload.php'; // Shaarli library require_once 'application/bookmark/LinkUtils.php'; require_once 'application/config/ConfigPlugin.php'; require_once 'application/http/HttpUtils.php'; require_once 'application/http/UrlUtils.php'; require_once 'application/updater/UpdaterUtils.php'; require_once 'application/FileUtils.php'; require_once 'application/TimeZone.php'; require_once 'application/Utils.php'; use Shaarli\ApplicationUtils; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFilter; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Config\ConfigManager; use Shaarli\Container\ContainerBuilder; use Shaarli\Feed\CachedPage; use Shaarli\Feed\FeedBuilder; use Shaarli\Formatter\BookmarkMarkdownFormatter; use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Languages; use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; use Shaarli\Render\PageCacheManager; use Shaarli\Render\ThemeUtils; use Shaarli\Router; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; use Shaarli\Updater\Updater; use Shaarli\Updater\UpdaterUtils; use Slim\App; // Ensure the PHP version is supported try { ApplicationUtils::checkPHPVersion('7.1', PHP_VERSION); } catch (Exception $exc) { header('Content-Type: text/plain; charset=utf-8'); echo $exc->getMessage(); exit; } define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); // Force cookie path (but do not change lifetime) $cookie = session_get_cookie_params(); $cookiedir = ''; if (dirname($_SERVER['SCRIPT_NAME']) != '/') { $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/'; } // Set default cookie expiration and path. session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); // Set session parameters on server side. // Use cookies to store session. ini_set('session.use_cookies', 1); // Force cookies for session (phpsessionID forbidden in URL). ini_set('session.use_only_cookies', 1); // Prevent PHP form using sessionID in URL if cookies are disabled. ini_set('session.use_trans_sid', false); session_name('shaarli'); // Start session if needed (Some server auto-start sessions). if (session_status() == PHP_SESSION_NONE) { session_start(); } // Regenerate session ID if invalid or not defined in cookie. if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) { session_regenerate_id(true); $_COOKIE['shaarli'] = session_id(); } $conf = new ConfigManager(); // In dev mode, throw exception on any warning if ($conf->get('dev.debug', false)) { // See all errors (for debugging only) error_reporting(-1); set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) { throw new ErrorException($errstr, 0, $errno, $errfile, $errline); }); } $sessionManager = new SessionManager($_SESSION, $conf); $loginManager = new LoginManager($conf, $sessionManager); $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); $clientIpId = client_ip_id($_SERVER); // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. if (! defined('LC_MESSAGES')) { define('LC_MESSAGES', LC_COLLATE); } // Sniff browser language and set date format accordingly. if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']); } new Languages(setlocale(LC_MESSAGES, 0), $conf); $conf->setEmpty('general.timezone', date_default_timezone_get()); $conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER))); RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory $pluginManager = new PluginManager($conf); $pluginManager->load($conf->get('general.enabled_plugins')); date_default_timezone_set($conf->get('general.timezone', 'UTC')); ob_start(); // Output buffering for the page cache. // Prevent caching on client side or proxy: (yes, it's ugly) header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); if (! is_file($conf->getConfigFileExt())) { // Ensure Shaarli has proper access to its resources $errors = ApplicationUtils::checkResourcePermissions($conf); if ($errors != array()) { $message = '

'. t('Insufficient permissions:') .'

'; header('Content-Type: text/html; charset=utf-8'); echo $message; exit; } // Display the installation form if no existing config is found install($conf, $sessionManager, $loginManager); } $loginManager->checkLoginState($_COOKIE, $clientIpId); /** * Adapter function to ensure compatibility with third-party templates * * @see https://github.com/shaarli/Shaarli/pull/1086 * * @return bool true when the user is logged in, false otherwise */ function isLoggedIn() { global $loginManager; return $loginManager->isLoggedIn(); } // ------------------------------------------------------------------------------------------ // Process login form: Check if login/password is correct. if (isset($_POST['login'])) { if (! $loginManager->canLogin($_SERVER)) { die(t('I said: NO. You are banned for the moment. Go away.')); } if (isset($_POST['password']) && $sessionManager->checkToken($_POST['token']) && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password']) ) { $loginManager->handleSuccessfulLogin($_SERVER); $cookiedir = ''; if (dirname($_SERVER['SCRIPT_NAME']) != '/') { // Note: Never forget the trailing slash on the cookie path! $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/'; } if (!empty($_POST['longlastingsession'])) { // Keep the session cookie even after the browser closes $sessionManager->setStaySignedIn(true); $expirationTime = $sessionManager->extendSession(); setcookie( $loginManager::$STAY_SIGNED_IN_COOKIE, $loginManager->getStaySignedInToken(), $expirationTime, WEB_PATH ); } else { // Standard session expiration (=when browser closes) $expirationTime = 0; } // Send cookie with the new expiration date to the browser session_destroy(); session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']); session_start(); session_regenerate_id(true); // Optional redirect after login: if (isset($_GET['post'])) { $uri = './?post='. urlencode($_GET['post']); foreach (array('description', 'source', 'title', 'tags') as $param) { if (!empty($_GET[$param])) { $uri .= '&'.$param.'='.urlencode($_GET[$param]); } } header('Location: '. $uri); exit; } if (isset($_GET['edit_link'])) { header('Location: ./?edit_link='. escape($_GET['edit_link'])); exit; } if (isset($_POST['returnurl'])) { // Prevent loops over login screen. if (strpos($_POST['returnurl'], '/login') === false) { header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST'])); exit; } } header('Location: ./?'); exit; } else { $loginManager->handleFailedLogin($_SERVER); $redir = '?username='. urlencode($_POST['login']); if (isset($_GET['post'])) { $redir .= '&post=' . urlencode($_GET['post']); foreach (array('description', 'source', 'title', 'tags') as $param) { if (!empty($_GET[$param])) { $redir .= '&' . $param . '=' . urlencode($_GET[$param]); } } } // Redirect to login screen. echo ''; exit; } } // ------------------------------------------------------------------------------------------ // Token management for XSRF protection // Token should be used in any form which acts on data (create,update,delete,import...). if (!isset($_SESSION['tokens'])) { $_SESSION['tokens']=array(); // Token are attached to the session. } /** * Renders the linklist * * @param pageBuilder $PAGE pageBuilder instance. * @param BookmarkServiceInterface $linkDb instance. * @param ConfigManager $conf Configuration Manager instance. * @param PluginManager $pluginManager Plugin Manager instance. */ function showLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager) { buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager); $PAGE->renderPage('linklist'); } /** * Render HTML page (according to URL parameters and user rights) * * @param ConfigManager $conf Configuration Manager instance. * @param PluginManager $pluginManager Plugin Manager instance, * @param BookmarkServiceInterface $bookmarkService * @param History $history instance * @param SessionManager $sessionManager SessionManager instance * @param LoginManager $loginManager LoginManager instance */ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionManager, $loginManager) { $pageCacheManager = new PageCacheManager($conf->get('resource.page_cache'), $loginManager->isLoggedIn()); $updater = new Updater( UpdaterUtils::read_updates_file($conf->get('resource.updates')), $bookmarkService, $conf, $loginManager->isLoggedIn() ); try { $newUpdates = $updater->update(); if (! empty($newUpdates)) { UpdaterUtils::write_updates_file( $conf->get('resource.updates'), $updater->getDoneUpdates() ); $pageCacheManager->invalidateCaches(); } } catch (Exception $e) { die($e->getMessage()); } $PAGE = new PageBuilder($conf, $_SESSION, $bookmarkService, $sessionManager->generateToken(), $loginManager->isLoggedIn()); $PAGE->assign('linkcount', $bookmarkService->count(BookmarkFilter::$ALL)); $PAGE->assign('privateLinkcount', $bookmarkService->count(BookmarkFilter::$PRIVATE)); $PAGE->assign('plugin_errors', $pluginManager->getErrors()); // Determine which page will be rendered. $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn()); if (// if the user isn't logged in !$loginManager->isLoggedIn() && // and Shaarli doesn't have public content... $conf->get('privacy.hide_public_links') && // and is configured to enforce the login $conf->get('privacy.force_login') && // and the current page isn't already the login page $targetPage !== Router::$PAGE_LOGIN && // and the user is not requesting a feed (which would lead to a different content-type as expected) $targetPage !== Router::$PAGE_FEED_ATOM && $targetPage !== Router::$PAGE_FEED_RSS ) { // force current page to be the login page $targetPage = Router::$PAGE_LOGIN; } // Call plugin hooks for header, footer and includes, specifying which page will be rendered. // Then assign generated data to RainTPL. $common_hooks = array( 'includes', 'header', 'footer', ); foreach ($common_hooks as $name) { $plugin_data = array(); $pluginManager->executeHooks( 'render_' . $name, $plugin_data, array( 'target' => $targetPage, 'loggedin' => $loginManager->isLoggedIn() ) ); $PAGE->assign('plugins_' . $name, $plugin_data); } // -------- Display login form. if ($targetPage == Router::$PAGE_LOGIN) { header('Location: ./login'); exit; } // -------- User wants to logout. if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) { header('Location: ./logout'); exit; } // -------- Picture wall if ($targetPage == Router::$PAGE_PICWALL) { header('Location: ./picture-wall'); exit; } // -------- Tag cloud if ($targetPage == Router::$PAGE_TAGCLOUD) { header('Location: ./tags/cloud'); exit; } // -------- Tag list if ($targetPage == Router::$PAGE_TAGLIST) { header('Location: ./tags/list'); exit; } // Daily page. if ($targetPage == Router::$PAGE_DAILY) { $dayParam = !empty($_GET['day']) ? '?day=' . escape($_GET['day']) : ''; header('Location: ./daily'. $dayParam); exit; } // ATOM and RSS feed. if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) { $feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM; header('Location: ./feed/'. $feedType .'?'. http_build_query($_GET)); exit; } // Display opensearch plugin (XML) if ($targetPage == Router::$PAGE_OPENSEARCH) { header('Location: ./open-search'); exit; } // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...) if (isset($_GET['addtag'])) { header('Location: ./add-tag/'. $_GET['addtag']); exit; } // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...) if (isset($_GET['removetag'])) { header('Location: ./remove-tag/'. $_GET['removetag']); exit; } // -------- User wants to change the number of bookmarks per page (linksperpage=...) if (isset($_GET['linksperpage'])) { header('Location: ./links-per-page?nb='. $_GET['linksperpage']); exit; } // -------- User wants to see only private bookmarks (toggle) if (isset($_GET['visibility'])) { header('Location: ./visibility/'. $_GET['visibility']); exit; } // -------- User wants to see only untagged bookmarks (toggle) if (isset($_GET['untaggedonly'])) { header('Location: ./untagged-only'); exit; } // -------- Handle other actions allowed for non-logged in users: if (!$loginManager->isLoggedIn()) { // User tries to post new link but is not logged in: // Show login screen, then redirect to ?post=... if (isset($_GET['post'])) { header( // Redirect to login page, then back to post link. 'Location: ./login?post='.urlencode($_GET['post']). (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):''). (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):''). (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):''). (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'') ); exit; } showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager); if (isset($_GET['edit_link'])) { header('Location: ./login?edit_link='. escape($_GET['edit_link'])); exit; } exit; // Never remove this one! All operations below are reserved for logged in user. } // -------- All other functions are reserved for the registered user: // TODO: Remove legacy admin route redirections. We'll only keep public URL. // -------- Display the Tools menu if requested (import/export/bookmarklet...) if ($targetPage == Router::$PAGE_TOOLS) { header('Location: ./admin/tools'); exit; } // -------- User wants to change his/her password. if ($targetPage == Router::$PAGE_CHANGEPASSWORD) { header('Location: ./admin/password'); exit; } // -------- User wants to change configuration if ($targetPage == Router::$PAGE_CONFIGURE) { header('Location: ./admin/configure'); exit; } // -------- User wants to rename a tag or delete it if ($targetPage == Router::$PAGE_CHANGETAG) { header('Location: ./admin/tags'); exit; } // -------- User wants to add a link without using the bookmarklet: Show form. if ($targetPage == Router::$PAGE_ADDLINK) { header('Location: ./admin/shaare'); exit; } // -------- User clicked the "Save" button when editing a link: Save link to database. if (isset($_POST['save_edit'])) { // This route is no longer supported in legacy mode header('Location: ./'); exit; } // -------- User clicked the "Delete" button when editing a link: Delete link from database. if ($targetPage == Router::$PAGE_DELETELINK) { $ids = $_GET['lf_linkdate'] ?? ''; $token = $_GET['token'] ?? ''; header('Location: ./admin/shaare/delete?id=' . $ids . '&token=' . $token); exit; } // -------- User clicked either "Set public" or "Set private" bulk operation if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) { header('Location: ./admin/shaare/visibility?id=' . $_GET['token']); exit; } // -------- User clicked the "EDIT" button on a link: Display link edit form. if (isset($_GET['edit_link'])) { $id = (int) escape($_GET['edit_link']); header('Location: ./admin/shaare/' . $id); exit; } // -------- User want to post a new link: Display link edit form. if (isset($_GET['post'])) { header('Location: ./admin/shaare?' . http_build_query($_GET)); exit; } if ($targetPage == Router::$PAGE_PINLINK) { // This route is no longer supported in legacy mode header('Location: ./'); exit; } if ($targetPage == Router::$PAGE_EXPORT) { // Export bookmarks as a Netscape Bookmarks file if (empty($_GET['selection'])) { $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli')); $PAGE->renderPage('export'); exit; } // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html $selection = $_GET['selection']; if (isset($_GET['prepend_note_url'])) { $prependNoteUrl = $_GET['prepend_note_url']; } else { $prependNoteUrl = false; } try { $factory = new FormatterFactory($conf, $loginManager->isLoggedIn()); $formatter = $factory->getFormatter('raw'); $PAGE->assign( 'links', NetscapeBookmarkUtils::filterAndFormat( $bookmarkService, $formatter, $selection, $prependNoteUrl, index_url($_SERVER) ) ); } catch (Exception $exc) { header('Content-Type: text/plain; charset=utf-8'); echo $exc->getMessage(); exit; } $now = new DateTime(); header('Content-Type: text/html; charset=utf-8'); header( 'Content-disposition: attachment; filename=bookmarks_' .$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' ); $PAGE->assign('date', $now->format(DateTime::RFC822)); $PAGE->assign('eol', PHP_EOL); $PAGE->assign('selection', $selection); $PAGE->renderPage('export.bookmarks'); exit; } if ($targetPage == Router::$PAGE_IMPORT) { // Upload a Netscape bookmark dump to import its contents if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { // Show import dialog $PAGE->assign( 'maxfilesize', get_max_upload_size( ini_get('post_max_size'), ini_get('upload_max_filesize'), false ) ); $PAGE->assign( 'maxfilesizeHuman', get_max_upload_size( ini_get('post_max_size'), ini_get('upload_max_filesize'), true ) ); $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli')); $PAGE->renderPage('import'); exit; } // Import bookmarks from an uploaded file if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) { // The file is too big or some form field may be missing. $msg = sprintf( t( 'The file you are trying to upload is probably bigger than what this webserver can accept' .' (%s). Please upload in smaller chunks.' ), get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) ); echo ''; exit; } if (! $sessionManager->checkToken($_POST['token'])) { die('Wrong token.'); } $netscapeBookmarkUtils = new NetscapeBookmarkUtils($bookmarkService, $conf, $history); $status = $netscapeBookmarkUtils->import($_POST, $_FILES); echo ''; exit; } // Plugin administration page if ($targetPage == Router::$PAGE_PLUGINSADMIN) { $pluginMeta = $pluginManager->getPluginsMeta(); // Split plugins into 2 arrays: ordered enabled plugins and disabled. $enabledPlugins = array_filter($pluginMeta, function ($v) { return $v['order'] !== false; }); // Load parameters. $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array())); uasort( $enabledPlugins, function ($a, $b) { return $a['order'] - $b['order']; } ); $disabledPlugins = array_filter($pluginMeta, function ($v) { return $v['order'] === false; }); $PAGE->assign('enabledPlugins', $enabledPlugins); $PAGE->assign('disabledPlugins', $disabledPlugins); $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli')); $PAGE->renderPage('pluginsadmin'); exit; } // Plugin administration form action if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) { try { if (isset($_POST['parameters_form'])) { $pluginManager->executeHooks('save_plugin_parameters', $_POST); unset($_POST['parameters_form']); foreach ($_POST as $param => $value) { $conf->set('plugins.'. $param, escape($value)); } } else { $conf->set('general.enabled_plugins', save_plugin_config($_POST)); } $conf->write($loginManager->isLoggedIn()); $history->updateSettings(); } catch (Exception $e) { error_log( 'ERROR while saving plugin configuration:.' . PHP_EOL . $e->getMessage() ); // TODO: do not handle exceptions/errors in JS. echo ''; exit; } header('Location: ./?do='. Router::$PAGE_PLUGINSADMIN); exit; } // Get a fresh token if ($targetPage == Router::$GET_TOKEN) { header('Content-Type:text/plain'); echo $sessionManager->generateToken(); exit; } // -------- Thumbnails Update if ($targetPage == Router::$PAGE_THUMBS_UPDATE) { $ids = []; foreach ($bookmarkService->search() as $bookmark) { // A note or not HTTP(S) if ($bookmark->isNote() || ! startsWith(strtolower($bookmark->getUrl()), 'http')) { continue; } $ids[] = $bookmark->getId(); } $PAGE->assign('ids', $ids); $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli')); $PAGE->renderPage('thumbnails'); exit; } // -------- Single Thumbnail Update if ($targetPage == Router::$AJAX_THUMB_UPDATE) { if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) { http_response_code(400); exit; } $id = (int) $_POST['id']; if (! $bookmarkService->exists($id)) { http_response_code(404); exit; } $thumbnailer = new Thumbnailer($conf); $bookmark = $bookmarkService->get($id); $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl())); $bookmarkService->set($bookmark); $factory = new FormatterFactory($conf, $loginManager->isLoggedIn()); echo json_encode($factory->getFormatter('raw')->format($bookmark)); exit; } // -------- Otherwise, simply display search form and bookmarks: showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager); exit; } /** * Template for the list of bookmarks (