index.php 74 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923
  1. <?php
  2. /**
  3. * Shaarli - The personal, minimalist, super-fast, database free, bookmarking service.
  4. *
  5. * Friendly fork by the Shaarli community:
  6. * - https://github.com/shaarli/Shaarli
  7. *
  8. * Original project by sebsauvage.net:
  9. * - http://sebsauvage.net/wiki/doku.php?id=php:shaarli
  10. * - https://github.com/sebsauvage/Shaarli
  11. *
  12. * Licence: http://www.opensource.org/licenses/zlib-license.php
  13. *
  14. * Requires: PHP 5.5.x
  15. */
  16. // Set 'UTC' as the default timezone if it is not defined in php.ini
  17. // See http://php.net/manual/en/datetime.configuration.php#ini.date.timezone
  18. if (date_default_timezone_get() == '') {
  19. date_default_timezone_set('UTC');
  20. }
  21. /*
  22. * PHP configuration
  23. */
  24. // http://server.com/x/shaarli --> /shaarli/
  25. define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
  26. // High execution time in case of problematic imports/exports.
  27. ini_set('max_input_time', '60');
  28. // Try to set max upload file size and read
  29. ini_set('memory_limit', '128M');
  30. ini_set('post_max_size', '16M');
  31. ini_set('upload_max_filesize', '16M');
  32. // See all error except warnings
  33. error_reporting(E_ALL^E_WARNING);
  34. // See all errors (for debugging only)
  35. //error_reporting(-1);
  36. // 3rd-party libraries
  37. if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
  38. header('Content-Type: text/plain; charset=utf-8');
  39. echo "Error: missing Composer configuration\n\n"
  40. ."If you installed Shaarli through Git or using the development branch,\n"
  41. ."please refer to the installation documentation to install PHP"
  42. ." dependencies using Composer:\n"
  43. ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
  44. ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
  45. exit;
  46. }
  47. require_once 'inc/rain.tpl.class.php';
  48. require_once __DIR__ . '/vendor/autoload.php';
  49. // Shaarli library
  50. require_once 'application/ApplicationUtils.php';
  51. require_once 'application/Cache.php';
  52. require_once 'application/CachedPage.php';
  53. require_once 'application/config/ConfigPlugin.php';
  54. require_once 'application/FeedBuilder.php';
  55. require_once 'application/FileUtils.php';
  56. require_once 'application/History.php';
  57. require_once 'application/HttpUtils.php';
  58. require_once 'application/LinkDB.php';
  59. require_once 'application/LinkFilter.php';
  60. require_once 'application/LinkUtils.php';
  61. require_once 'application/NetscapeBookmarkUtils.php';
  62. require_once 'application/PageBuilder.php';
  63. require_once 'application/TimeZone.php';
  64. require_once 'application/Url.php';
  65. require_once 'application/Utils.php';
  66. require_once 'application/PluginManager.php';
  67. require_once 'application/Router.php';
  68. require_once 'application/Updater.php';
  69. use \Shaarli\Config\ConfigManager;
  70. use \Shaarli\Languages;
  71. use \Shaarli\Security\LoginManager;
  72. use \Shaarli\Security\SessionManager;
  73. use \Shaarli\ThemeUtils;
  74. use \Shaarli\Thumbnailer;
  75. // Ensure the PHP version is supported
  76. try {
  77. ApplicationUtils::checkPHPVersion('5.5', PHP_VERSION);
  78. } catch (Exception $exc) {
  79. header('Content-Type: text/plain; charset=utf-8');
  80. echo $exc->getMessage();
  81. exit;
  82. }
  83. define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
  84. // Force cookie path (but do not change lifetime)
  85. $cookie = session_get_cookie_params();
  86. $cookiedir = '';
  87. if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
  88. $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
  89. }
  90. // Set default cookie expiration and path.
  91. session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
  92. // Set session parameters on server side.
  93. // Use cookies to store session.
  94. ini_set('session.use_cookies', 1);
  95. // Force cookies for session (phpsessionID forbidden in URL).
  96. ini_set('session.use_only_cookies', 1);
  97. // Prevent PHP form using sessionID in URL if cookies are disabled.
  98. ini_set('session.use_trans_sid', false);
  99. session_name('shaarli');
  100. // Start session if needed (Some server auto-start sessions).
  101. if (session_status() == PHP_SESSION_NONE) {
  102. session_start();
  103. }
  104. // Regenerate session ID if invalid or not defined in cookie.
  105. if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
  106. session_regenerate_id(true);
  107. $_COOKIE['shaarli'] = session_id();
  108. }
  109. $conf = new ConfigManager();
  110. $sessionManager = new SessionManager($_SESSION, $conf);
  111. $loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
  112. $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
  113. $clientIpId = client_ip_id($_SERVER);
  114. // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
  115. if (! defined('LC_MESSAGES')) {
  116. define('LC_MESSAGES', LC_COLLATE);
  117. }
  118. // Sniff browser language and set date format accordingly.
  119. if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  120. autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
  121. }
  122. new Languages(setlocale(LC_MESSAGES, 0), $conf);
  123. $conf->setEmpty('general.timezone', date_default_timezone_get());
  124. $conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
  125. RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
  126. RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
  127. $pluginManager = new PluginManager($conf);
  128. $pluginManager->load($conf->get('general.enabled_plugins'));
  129. date_default_timezone_set($conf->get('general.timezone', 'UTC'));
  130. ob_start(); // Output buffering for the page cache.
  131. // Prevent caching on client side or proxy: (yes, it's ugly)
  132. header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
  133. header("Cache-Control: no-store, no-cache, must-revalidate");
  134. header("Cache-Control: post-check=0, pre-check=0", false);
  135. header("Pragma: no-cache");
  136. if (! is_file($conf->getConfigFileExt())) {
  137. // Ensure Shaarli has proper access to its resources
  138. $errors = ApplicationUtils::checkResourcePermissions($conf);
  139. if ($errors != array()) {
  140. $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
  141. foreach ($errors as $error) {
  142. $message .= '<li>'.$error.'</li>';
  143. }
  144. $message .= '</ul>';
  145. header('Content-Type: text/html; charset=utf-8');
  146. echo $message;
  147. exit;
  148. }
  149. // Display the installation form if no existing config is found
  150. install($conf, $sessionManager, $loginManager);
  151. }
  152. $loginManager->checkLoginState($_COOKIE, $clientIpId);
  153. /**
  154. * Adapter function to ensure compatibility with third-party templates
  155. *
  156. * @see https://github.com/shaarli/Shaarli/pull/1086
  157. *
  158. * @return bool true when the user is logged in, false otherwise
  159. */
  160. function isLoggedIn()
  161. {
  162. global $loginManager;
  163. return $loginManager->isLoggedIn();
  164. }
  165. // ------------------------------------------------------------------------------------------
  166. // Process login form: Check if login/password is correct.
  167. if (isset($_POST['login'])) {
  168. if (! $loginManager->canLogin($_SERVER)) {
  169. die(t('I said: NO. You are banned for the moment. Go away.'));
  170. }
  171. if (isset($_POST['password'])
  172. && $sessionManager->checkToken($_POST['token'])
  173. && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
  174. ) {
  175. $loginManager->handleSuccessfulLogin($_SERVER);
  176. $cookiedir = '';
  177. if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
  178. // Note: Never forget the trailing slash on the cookie path!
  179. $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
  180. }
  181. if (!empty($_POST['longlastingsession'])) {
  182. // Keep the session cookie even after the browser closes
  183. $sessionManager->setStaySignedIn(true);
  184. $expirationTime = $sessionManager->extendSession();
  185. setcookie(
  186. $loginManager::$STAY_SIGNED_IN_COOKIE,
  187. $loginManager->getStaySignedInToken(),
  188. $expirationTime,
  189. WEB_PATH
  190. );
  191. } else {
  192. // Standard session expiration (=when browser closes)
  193. $expirationTime = 0;
  194. }
  195. // Send cookie with the new expiration date to the browser
  196. session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
  197. session_regenerate_id(true);
  198. // Optional redirect after login:
  199. if (isset($_GET['post'])) {
  200. $uri = '?post='. urlencode($_GET['post']);
  201. foreach (array('description', 'source', 'title', 'tags') as $param) {
  202. if (!empty($_GET[$param])) {
  203. $uri .= '&'.$param.'='.urlencode($_GET[$param]);
  204. }
  205. }
  206. header('Location: '. $uri);
  207. exit;
  208. }
  209. if (isset($_GET['edit_link'])) {
  210. header('Location: ?edit_link='. escape($_GET['edit_link']));
  211. exit;
  212. }
  213. if (isset($_POST['returnurl'])) {
  214. // Prevent loops over login screen.
  215. if (strpos($_POST['returnurl'], 'do=login') === false) {
  216. header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
  217. exit;
  218. }
  219. }
  220. header('Location: ?');
  221. exit;
  222. } else {
  223. $loginManager->handleFailedLogin($_SERVER);
  224. $redir = '&username='. urlencode($_POST['login']);
  225. if (isset($_GET['post'])) {
  226. $redir .= '&post=' . urlencode($_GET['post']);
  227. foreach (array('description', 'source', 'title', 'tags') as $param) {
  228. if (!empty($_GET[$param])) {
  229. $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
  230. }
  231. }
  232. }
  233. // Redirect to login screen.
  234. echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
  235. exit;
  236. }
  237. }
  238. // ------------------------------------------------------------------------------------------
  239. // Token management for XSRF protection
  240. // Token should be used in any form which acts on data (create,update,delete,import...).
  241. if (!isset($_SESSION['tokens'])) {
  242. $_SESSION['tokens']=array(); // Token are attached to the session.
  243. }
  244. /**
  245. * Daily RSS feed: 1 RSS entry per day giving all the links on that day.
  246. * Gives the last 7 days (which have links).
  247. * This RSS feed cannot be filtered.
  248. *
  249. * @param ConfigManager $conf Configuration Manager instance
  250. * @param LoginManager $loginManager LoginManager instance
  251. */
  252. function showDailyRSS($conf, $loginManager)
  253. {
  254. // Cache system
  255. $query = $_SERVER['QUERY_STRING'];
  256. $cache = new CachedPage(
  257. $conf->get('config.PAGE_CACHE'),
  258. page_url($_SERVER),
  259. startsWith($query, 'do=dailyrss') && !$loginManager->isLoggedIn()
  260. );
  261. $cached = $cache->cachedVersion();
  262. if (!empty($cached)) {
  263. echo $cached;
  264. exit;
  265. }
  266. // If cached was not found (or not usable), then read the database and build the response:
  267. // Read links from database (and filter private links if used it not logged in).
  268. $LINKSDB = new LinkDB(
  269. $conf->get('resource.datastore'),
  270. $loginManager->isLoggedIn(),
  271. $conf->get('privacy.hide_public_links'),
  272. $conf->get('redirector.url'),
  273. $conf->get('redirector.encode_url')
  274. );
  275. /* Some Shaarlies may have very few links, so we need to look
  276. back in time until we have enough days ($nb_of_days).
  277. */
  278. $nb_of_days = 7; // We take 7 days.
  279. $today = date('Ymd');
  280. $days = array();
  281. foreach ($LINKSDB as $link) {
  282. $day = $link['created']->format('Ymd'); // Extract day (without time)
  283. if (strcmp($day, $today) < 0) {
  284. if (empty($days[$day])) {
  285. $days[$day] = array();
  286. }
  287. $days[$day][] = $link;
  288. }
  289. if (count($days) > $nb_of_days) {
  290. break; // Have we collected enough days?
  291. }
  292. }
  293. // Build the RSS feed.
  294. header('Content-Type: application/rss+xml; charset=utf-8');
  295. $pageaddr = escape(index_url($_SERVER));
  296. echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">';
  297. echo '<channel>';
  298. echo '<title>Daily - '. $conf->get('general.title') . '</title>';
  299. echo '<link>'. $pageaddr .'</link>';
  300. echo '<description>Daily shared links</description>';
  301. echo '<language>en-en</language>';
  302. echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
  303. // For each day.
  304. foreach ($days as $day => $links) {
  305. $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
  306. $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
  307. // We pre-format some fields for proper output.
  308. foreach ($links as &$link) {
  309. $link['formatedDescription'] = format_description(
  310. $link['description'],
  311. $conf->get('redirector.url'),
  312. $conf->get('redirector.encode_url')
  313. );
  314. $link['timestamp'] = $link['created']->getTimestamp();
  315. if (startsWith($link['url'], '?')) {
  316. $link['url'] = index_url($_SERVER) . $link['url']; // make permalink URL absolute
  317. }
  318. }
  319. // Then build the HTML for this day:
  320. $tpl = new RainTPL;
  321. $tpl->assign('title', $conf->get('general.title'));
  322. $tpl->assign('daydate', $dayDate->getTimestamp());
  323. $tpl->assign('absurl', $absurl);
  324. $tpl->assign('links', $links);
  325. $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
  326. $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
  327. $tpl->assign('index_url', $pageaddr);
  328. $html = $tpl->draw('dailyrss', true);
  329. echo $html . PHP_EOL;
  330. }
  331. echo '</channel></rss><!-- Cached version of '. escape(page_url($_SERVER)) .' -->';
  332. $cache->cache(ob_get_contents());
  333. ob_end_flush();
  334. exit;
  335. }
  336. /**
  337. * Show the 'Daily' page.
  338. *
  339. * @param PageBuilder $pageBuilder Template engine wrapper.
  340. * @param LinkDB $LINKSDB LinkDB instance.
  341. * @param ConfigManager $conf Configuration Manager instance.
  342. * @param PluginManager $pluginManager Plugin Manager instance.
  343. * @param LoginManager $loginManager Login Manager instance
  344. */
  345. function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
  346. {
  347. $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
  348. if (isset($_GET['day'])) {
  349. $day = $_GET['day'];
  350. }
  351. $days = $LINKSDB->days();
  352. $i = array_search($day, $days);
  353. if ($i === false && count($days)) {
  354. // no links for day, but at least one day with links
  355. $i = count($days) - 1;
  356. $day = $days[$i];
  357. }
  358. $previousday = '';
  359. $nextday = '';
  360. if ($i !== false) {
  361. if ($i >= 1) {
  362. $previousday=$days[$i - 1];
  363. }
  364. if ($i < count($days) - 1) {
  365. $nextday = $days[$i + 1];
  366. }
  367. }
  368. try {
  369. $linksToDisplay = $LINKSDB->filterDay($day);
  370. } catch (Exception $exc) {
  371. error_log($exc);
  372. $linksToDisplay = array();
  373. }
  374. // We pre-format some fields for proper output.
  375. foreach ($linksToDisplay as $key => $link) {
  376. $taglist = explode(' ', $link['tags']);
  377. uasort($taglist, 'strcasecmp');
  378. $linksToDisplay[$key]['taglist']=$taglist;
  379. $linksToDisplay[$key]['formatedDescription'] = format_description(
  380. $link['description'],
  381. $conf->get('redirector.url'),
  382. $conf->get('redirector.encode_url')
  383. );
  384. $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
  385. }
  386. $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
  387. $data = array(
  388. 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
  389. 'linksToDisplay' => $linksToDisplay,
  390. 'day' => $dayDate->getTimestamp(),
  391. 'dayDate' => $dayDate,
  392. 'previousday' => $previousday,
  393. 'nextday' => $nextday,
  394. );
  395. /* Hook is called before column construction so that plugins don't have
  396. to deal with columns. */
  397. $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
  398. /* We need to spread the articles on 3 columns.
  399. I did not want to use a JavaScript lib like http://masonry.desandro.com/
  400. so I manually spread entries with a simple method: I roughly evaluate the
  401. height of a div according to title and description length.
  402. */
  403. $columns = array(array(), array(), array()); // Entries to display, for each column.
  404. $fill = array(0, 0, 0); // Rough estimate of columns fill.
  405. foreach ($data['linksToDisplay'] as $key => $link) {
  406. // Roughly estimate length of entry (by counting characters)
  407. // Title: 30 chars = 1 line. 1 line is 30 pixels height.
  408. // Description: 836 characters gives roughly 342 pixel height.
  409. // This is not perfect, but it's usually OK.
  410. $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836;
  411. if ($link['thumbnail']) {
  412. $length += 100; // 1 thumbnails roughly takes 100 pixels height.
  413. }
  414. // Then put in column which is the less filled:
  415. $smallest = min($fill); // find smallest value in array.
  416. $index = array_search($smallest, $fill); // find index of this smallest value.
  417. array_push($columns[$index], $link); // Put entry in this column.
  418. $fill[$index] += $length;
  419. }
  420. $data['cols'] = $columns;
  421. foreach ($data as $key => $value) {
  422. $pageBuilder->assign($key, $value);
  423. }
  424. $pageBuilder->assign('pagetitle', t('Daily') .' - '. $conf->get('general.title', 'Shaarli'));
  425. $pageBuilder->renderPage('daily');
  426. exit;
  427. }
  428. /**
  429. * Renders the linklist
  430. *
  431. * @param pageBuilder $PAGE pageBuilder instance.
  432. * @param LinkDB $LINKSDB LinkDB instance.
  433. * @param ConfigManager $conf Configuration Manager instance.
  434. * @param PluginManager $pluginManager Plugin Manager instance.
  435. */
  436. function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
  437. {
  438. buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
  439. $PAGE->renderPage('linklist');
  440. }
  441. /**
  442. * Render HTML page (according to URL parameters and user rights)
  443. *
  444. * @param ConfigManager $conf Configuration Manager instance.
  445. * @param PluginManager $pluginManager Plugin Manager instance,
  446. * @param LinkDB $LINKSDB
  447. * @param History $history instance
  448. * @param SessionManager $sessionManager SessionManager instance
  449. * @param LoginManager $loginManager LoginManager instance
  450. */
  451. function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $loginManager)
  452. {
  453. $updater = new Updater(
  454. read_updates_file($conf->get('resource.updates')),
  455. $LINKSDB,
  456. $conf,
  457. $loginManager->isLoggedIn(),
  458. $_SESSION
  459. );
  460. try {
  461. $newUpdates = $updater->update();
  462. if (! empty($newUpdates)) {
  463. write_updates_file(
  464. $conf->get('resource.updates'),
  465. $updater->getDoneUpdates()
  466. );
  467. }
  468. } catch (Exception $e) {
  469. die($e->getMessage());
  470. }
  471. $PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
  472. $PAGE->assign('linkcount', count($LINKSDB));
  473. $PAGE->assign('privateLinkcount', count_private($LINKSDB));
  474. $PAGE->assign('plugin_errors', $pluginManager->getErrors());
  475. // Determine which page will be rendered.
  476. $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
  477. $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
  478. if (// if the user isn't logged in
  479. !$loginManager->isLoggedIn() &&
  480. // and Shaarli doesn't have public content...
  481. $conf->get('privacy.hide_public_links') &&
  482. // and is configured to enforce the login
  483. $conf->get('privacy.force_login') &&
  484. // and the current page isn't already the login page
  485. $targetPage !== Router::$PAGE_LOGIN &&
  486. // and the user is not requesting a feed (which would lead to a different content-type as expected)
  487. $targetPage !== Router::$PAGE_FEED_ATOM &&
  488. $targetPage !== Router::$PAGE_FEED_RSS
  489. ) {
  490. // force current page to be the login page
  491. $targetPage = Router::$PAGE_LOGIN;
  492. }
  493. // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
  494. // Then assign generated data to RainTPL.
  495. $common_hooks = array(
  496. 'includes',
  497. 'header',
  498. 'footer',
  499. );
  500. foreach ($common_hooks as $name) {
  501. $plugin_data = array();
  502. $pluginManager->executeHooks(
  503. 'render_' . $name,
  504. $plugin_data,
  505. array(
  506. 'target' => $targetPage,
  507. 'loggedin' => $loginManager->isLoggedIn()
  508. )
  509. );
  510. $PAGE->assign('plugins_' . $name, $plugin_data);
  511. }
  512. // -------- Display login form.
  513. if ($targetPage == Router::$PAGE_LOGIN) {
  514. if ($conf->get('security.open_shaarli')) {
  515. header('Location: ?');
  516. exit;
  517. } // No need to login for open Shaarli
  518. if (isset($_GET['username'])) {
  519. $PAGE->assign('username', escape($_GET['username']));
  520. }
  521. $PAGE->assign('returnurl', (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
  522. // add default state of the 'remember me' checkbox
  523. $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
  524. $PAGE->assign('user_can_login', $loginManager->canLogin($_SERVER));
  525. $PAGE->assign('pagetitle', t('Login') .' - '. $conf->get('general.title', 'Shaarli'));
  526. $PAGE->renderPage('loginform');
  527. exit;
  528. }
  529. // -------- User wants to logout.
  530. if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) {
  531. invalidateCaches($conf->get('resource.page_cache'));
  532. $sessionManager->logout();
  533. setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
  534. header('Location: ?');
  535. exit;
  536. }
  537. // -------- Picture wall
  538. if ($targetPage == Router::$PAGE_PICWALL) {
  539. $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
  540. if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
  541. $PAGE->assign('linksToDisplay', []);
  542. $PAGE->renderPage('picwall');
  543. exit;
  544. }
  545. // Optionally filter the results:
  546. $links = $LINKSDB->filterSearch($_GET);
  547. $linksToDisplay = array();
  548. // Get only links which have a thumbnail.
  549. // Note: we do not retrieve thumbnails here, the request is too heavy.
  550. foreach ($links as $key => $link) {
  551. if (isset($link['thumbnail']) && $link['thumbnail'] !== false) {
  552. $linksToDisplay[] = $link; // Add to array.
  553. }
  554. }
  555. $data = array(
  556. 'linksToDisplay' => $linksToDisplay,
  557. );
  558. $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
  559. foreach ($data as $key => $value) {
  560. $PAGE->assign($key, $value);
  561. }
  562. $PAGE->renderPage('picwall');
  563. exit;
  564. }
  565. // -------- Tag cloud
  566. if ($targetPage == Router::$PAGE_TAGCLOUD) {
  567. $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
  568. $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
  569. $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
  570. // We sort tags alphabetically, then choose a font size according to count.
  571. // First, find max value.
  572. $maxcount = 0;
  573. foreach ($tags as $value) {
  574. $maxcount = max($maxcount, $value);
  575. }
  576. alphabetical_sort($tags, false, true);
  577. $tagList = array();
  578. foreach ($tags as $key => $value) {
  579. if (in_array($key, $filteringTags)) {
  580. continue;
  581. }
  582. // Tag font size scaling:
  583. // default 15 and 30 logarithm bases affect scaling,
  584. // 22 and 6 are arbitrary font sizes for max and min sizes.
  585. $size = log($value, 15) / log($maxcount, 30) * 2.2 + 0.8;
  586. $tagList[$key] = array(
  587. 'count' => $value,
  588. 'size' => number_format($size, 2, '.', ''),
  589. );
  590. }
  591. $searchTags = implode(' ', escape($filteringTags));
  592. $data = array(
  593. 'search_tags' => $searchTags,
  594. 'tags' => $tagList,
  595. );
  596. $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
  597. foreach ($data as $key => $value) {
  598. $PAGE->assign($key, $value);
  599. }
  600. $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
  601. $PAGE->assign('pagetitle', $searchTags. t('Tag cloud') .' - '. $conf->get('general.title', 'Shaarli'));
  602. $PAGE->renderPage('tag.cloud');
  603. exit;
  604. }
  605. // -------- Tag list
  606. if ($targetPage == Router::$PAGE_TAGLIST) {
  607. $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
  608. $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
  609. $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
  610. foreach ($filteringTags as $tag) {
  611. if (array_key_exists($tag, $tags)) {
  612. unset($tags[$tag]);
  613. }
  614. }
  615. if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
  616. alphabetical_sort($tags, false, true);
  617. }
  618. $searchTags = implode(' ', escape($filteringTags));
  619. $data = [
  620. 'search_tags' => $searchTags,
  621. 'tags' => $tags,
  622. ];
  623. $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
  624. foreach ($data as $key => $value) {
  625. $PAGE->assign($key, $value);
  626. }
  627. $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
  628. $PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
  629. $PAGE->renderPage('tag.list');
  630. exit;
  631. }
  632. // Daily page.
  633. if ($targetPage == Router::$PAGE_DAILY) {
  634. showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
  635. }
  636. // ATOM and RSS feed.
  637. if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) {
  638. $feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
  639. header('Content-Type: application/'. $feedType .'+xml; charset=utf-8');
  640. // Cache system
  641. $query = $_SERVER['QUERY_STRING'];
  642. $cache = new CachedPage(
  643. $conf->get('resource.page_cache'),
  644. page_url($_SERVER),
  645. startsWith($query, 'do='. $targetPage) && !$loginManager->isLoggedIn()
  646. );
  647. $cached = $cache->cachedVersion();
  648. if (!empty($cached)) {
  649. echo $cached;
  650. exit;
  651. }
  652. // Generate data.
  653. $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
  654. $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
  655. $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
  656. $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
  657. $data = $feedGenerator->buildData();
  658. // Process plugin hook.
  659. $pluginManager->executeHooks('render_feed', $data, array(
  660. 'loggedin' => $loginManager->isLoggedIn(),
  661. 'target' => $targetPage,
  662. ));
  663. // Render the template.
  664. $PAGE->assignAll($data);
  665. $PAGE->renderPage('feed.'. $feedType);
  666. $cache->cache(ob_get_contents());
  667. ob_end_flush();
  668. exit;
  669. }
  670. // Display opensearch plugin (XML)
  671. if ($targetPage == Router::$PAGE_OPENSEARCH) {
  672. header('Content-Type: application/xml; charset=utf-8');
  673. $PAGE->assign('serverurl', index_url($_SERVER));
  674. $PAGE->renderPage('opensearch');
  675. exit;
  676. }
  677. // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
  678. if (isset($_GET['addtag'])) {
  679. // Get previous URL (http_referer) and add the tag to the searchtags parameters in query.
  680. if (empty($_SERVER['HTTP_REFERER'])) {
  681. // In case browser does not send HTTP_REFERER
  682. header('Location: ?searchtags='.urlencode($_GET['addtag']));
  683. exit;
  684. }
  685. parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
  686. // Prevent redirection loop
  687. if (isset($params['addtag'])) {
  688. unset($params['addtag']);
  689. }
  690. // Check if this tag is already in the search query and ignore it if it is.
  691. // Each tag is always separated by a space
  692. if (isset($params['searchtags'])) {
  693. $current_tags = explode(' ', $params['searchtags']);
  694. } else {
  695. $current_tags = array();
  696. }
  697. $addtag = true;
  698. foreach ($current_tags as $value) {
  699. if ($value === $_GET['addtag']) {
  700. $addtag = false;
  701. break;
  702. }
  703. }
  704. // Append the tag if necessary
  705. if (empty($params['searchtags'])) {
  706. $params['searchtags'] = trim($_GET['addtag']);
  707. } elseif ($addtag) {
  708. $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']);
  709. }
  710. // We also remove page (keeping the same page has no sense, since the
  711. // results are different)
  712. unset($params['page']);
  713. header('Location: ?'.http_build_query($params));
  714. exit;
  715. }
  716. // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...)
  717. if (isset($_GET['removetag'])) {
  718. // Get previous URL (http_referer) and remove the tag from the searchtags parameters in query.
  719. if (empty($_SERVER['HTTP_REFERER'])) {
  720. header('Location: ?');
  721. exit;
  722. }
  723. // In case browser does not send HTTP_REFERER
  724. parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
  725. // Prevent redirection loop
  726. if (isset($params['removetag'])) {
  727. unset($params['removetag']);
  728. }
  729. if (isset($params['searchtags'])) {
  730. $tags = explode(' ', $params['searchtags']);
  731. // Remove value from array $tags.
  732. $tags = array_diff($tags, array($_GET['removetag']));
  733. $params['searchtags'] = implode(' ', $tags);
  734. if (empty($params['searchtags'])) {
  735. unset($params['searchtags']);
  736. }
  737. // We also remove page (keeping the same page has no sense, since
  738. // the results are different)
  739. unset($params['page']);
  740. }
  741. header('Location: ?'.http_build_query($params));
  742. exit;
  743. }
  744. // -------- User wants to change the number of links per page (linksperpage=...)
  745. if (isset($_GET['linksperpage'])) {
  746. if (is_numeric($_GET['linksperpage'])) {
  747. $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
  748. }
  749. if (! empty($_SERVER['HTTP_REFERER'])) {
  750. $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
  751. } else {
  752. $location = '?';
  753. }
  754. header('Location: '. $location);
  755. exit;
  756. }
  757. // -------- User wants to see only private links (toggle)
  758. if (isset($_GET['visibility'])) {
  759. if ($_GET['visibility'] === 'private') {
  760. // Visibility not set or not already private, set private, otherwise reset it
  761. if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
  762. // See only private links
  763. $_SESSION['visibility'] = 'private';
  764. } else {
  765. unset($_SESSION['visibility']);
  766. }
  767. } elseif ($_GET['visibility'] === 'public') {
  768. if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
  769. // See only public links
  770. $_SESSION['visibility'] = 'public';
  771. } else {
  772. unset($_SESSION['visibility']);
  773. }
  774. }
  775. if (! empty($_SERVER['HTTP_REFERER'])) {
  776. $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
  777. } else {
  778. $location = '?';
  779. }
  780. header('Location: '. $location);
  781. exit;
  782. }
  783. // -------- User wants to see only untagged links (toggle)
  784. if (isset($_GET['untaggedonly'])) {
  785. $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
  786. if (! empty($_SERVER['HTTP_REFERER'])) {
  787. $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
  788. } else {
  789. $location = '?';
  790. }
  791. header('Location: '. $location);
  792. exit;
  793. }
  794. // -------- Handle other actions allowed for non-logged in users:
  795. if (!$loginManager->isLoggedIn()) {
  796. // User tries to post new link but is not logged in:
  797. // Show login screen, then redirect to ?post=...
  798. if (isset($_GET['post'])) {
  799. header( // Redirect to login page, then back to post link.
  800. 'Location: ?do=login&post='.urlencode($_GET['post']).
  801. (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
  802. (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
  803. (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
  804. (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
  805. );
  806. exit;
  807. }
  808. showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
  809. if (isset($_GET['edit_link'])) {
  810. header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
  811. exit;
  812. }
  813. exit; // Never remove this one! All operations below are reserved for logged in user.
  814. }
  815. // -------- All other functions are reserved for the registered user:
  816. // -------- Display the Tools menu if requested (import/export/bookmarklet...)
  817. if ($targetPage == Router::$PAGE_TOOLS) {
  818. $data = [
  819. 'pageabsaddr' => index_url($_SERVER),
  820. 'sslenabled' => is_https($_SERVER),
  821. ];
  822. $pluginManager->executeHooks('render_tools', $data);
  823. foreach ($data as $key => $value) {
  824. $PAGE->assign($key, $value);
  825. }
  826. $PAGE->assign('pagetitle', t('Tools') .' - '. $conf->get('general.title', 'Shaarli'));
  827. $PAGE->renderPage('tools');
  828. exit;
  829. }
  830. // -------- User wants to change his/her password.
  831. if ($targetPage == Router::$PAGE_CHANGEPASSWORD) {
  832. if ($conf->get('security.open_shaarli')) {
  833. die(t('You are not supposed to change a password on an Open Shaarli.'));
  834. }
  835. if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) {
  836. if (!$sessionManager->checkToken($_POST['token'])) {
  837. die(t('Wrong token.')); // Go away!
  838. }
  839. // Make sure old password is correct.
  840. $oldhash = sha1(
  841. $_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')
  842. );
  843. if ($oldhash != $conf->get('credentials.hash')) {
  844. echo '<script>alert("'
  845. . t('The old password is not correct.')
  846. .'");document.location=\'?do=changepasswd\';</script>';
  847. exit;
  848. }
  849. // Save new password
  850. // Salt renders rainbow-tables attacks useless.
  851. $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
  852. $conf->set(
  853. 'credentials.hash',
  854. sha1(
  855. $_POST['setpassword']
  856. . $conf->get('credentials.login')
  857. . $conf->get('credentials.salt')
  858. )
  859. );
  860. try {
  861. $conf->write($loginManager->isLoggedIn());
  862. } catch (Exception $e) {
  863. error_log(
  864. 'ERROR while writing config file after changing password.' . PHP_EOL .
  865. $e->getMessage()
  866. );
  867. // TODO: do not handle exceptions/errors in JS.
  868. echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
  869. exit;
  870. }
  871. echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
  872. exit;
  873. } else {
  874. // show the change password form.
  875. $PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
  876. $PAGE->renderPage('changepassword');
  877. exit;
  878. }
  879. }
  880. // -------- User wants to change configuration
  881. if ($targetPage == Router::$PAGE_CONFIGURE) {
  882. if (!empty($_POST['title'])) {
  883. if (!$sessionManager->checkToken($_POST['token'])) {
  884. die(t('Wrong token.')); // Go away!
  885. }
  886. $tz = 'UTC';
  887. if (!empty($_POST['continent']) && !empty($_POST['city'])
  888. && isTimeZoneValid($_POST['continent'], $_POST['city'])
  889. ) {
  890. $tz = $_POST['continent'] . '/' . $_POST['city'];
  891. }
  892. $conf->set('general.timezone', $tz);
  893. $conf->set('general.title', escape($_POST['title']));
  894. $conf->set('general.header_link', escape($_POST['titleLink']));
  895. $conf->set('resource.theme', escape($_POST['theme']));
  896. $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
  897. $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
  898. $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
  899. $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
  900. $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
  901. $conf->set('api.enabled', !empty($_POST['enableApi']));
  902. $conf->set('api.secret', escape($_POST['apiSecret']));
  903. $conf->set('translation.language', escape($_POST['language']));
  904. $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
  905. if ($thumbnailsMode !== Thumbnailer::MODE_NONE
  906. && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
  907. ) {
  908. $_SESSION['warnings'][] = t(
  909. 'You have enabled or changed thumbnails mode. '
  910. .'<a href="?do=thumbs_update">Please synchronize them</a>.'
  911. );
  912. }
  913. $conf->set('thumbnails.mode', $thumbnailsMode);
  914. try {
  915. $conf->write($loginManager->isLoggedIn());
  916. $history->updateSettings();
  917. invalidateCaches($conf->get('resource.page_cache'));
  918. } catch (Exception $e) {
  919. error_log(
  920. 'ERROR while writing config file after configuration update.' . PHP_EOL .
  921. $e->getMessage()
  922. );
  923. // TODO: do not handle exceptions/errors in JS.
  924. echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
  925. exit;
  926. }
  927. echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
  928. exit;
  929. } else {
  930. // Show the configuration form.
  931. $PAGE->assign('title', $conf->get('general.title'));
  932. $PAGE->assign('theme', $conf->get('resource.theme'));
  933. $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
  934. list($continents, $cities) = generateTimeZoneData(
  935. timezone_identifiers_list(),
  936. $conf->get('general.timezone')
  937. );
  938. $PAGE->assign('continents', $continents);
  939. $PAGE->assign('cities', $cities);
  940. $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
  941. $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
  942. $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
  943. $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
  944. $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
  945. $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
  946. $PAGE->assign('api_secret', $conf->get('api.secret'));
  947. $PAGE->assign('languages', Languages::getAvailableLanguages());
  948. $PAGE->assign('language', $conf->get('translation.language'));
  949. $PAGE->assign('gd_enabled', extension_loaded('gd'));
  950. $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
  951. $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
  952. $PAGE->renderPage('configure');
  953. exit;
  954. }
  955. }
  956. // -------- User wants to rename a tag or delete it
  957. if ($targetPage == Router::$PAGE_CHANGETAG) {
  958. if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
  959. $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
  960. $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
  961. $PAGE->renderPage('changetag');
  962. exit;
  963. }
  964. if (!$sessionManager->checkToken($_POST['token'])) {
  965. die(t('Wrong token.'));
  966. }
  967. $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
  968. $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), $toTag);
  969. $LINKSDB->save($conf->get('resource.page_cache'));
  970. foreach ($alteredLinks as $link) {
  971. $history->updateLink($link);
  972. }
  973. $delete = empty($_POST['totag']);
  974. $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
  975. $count = count($alteredLinks);
  976. $alert = $delete
  977. ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
  978. : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
  979. echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
  980. exit;
  981. }
  982. // -------- User wants to add a link without using the bookmarklet: Show form.
  983. if ($targetPage == Router::$PAGE_ADDLINK) {
  984. $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
  985. $PAGE->renderPage('addlink');
  986. exit;
  987. }
  988. // -------- User clicked the "Save" button when editing a link: Save link to database.
  989. if (isset($_POST['save_edit'])) {
  990. // Go away!
  991. if (! $sessionManager->checkToken($_POST['token'])) {
  992. die(t('Wrong token.'));
  993. }
  994. // lf_id should only be present if the link exists.
  995. $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
  996. // Linkdate is kept here to:
  997. // - use the same permalink for notes as they're displayed when creating them
  998. // - let users hack creation date of their posts
  999. // See: https://shaarli.readthedocs.io/en/master/guides/various-hacks/#changing-the-timestamp-for-a-shaare
  1000. $linkdate = escape($_POST['lf_linkdate']);
  1001. if (isset($LINKSDB[$id])) {
  1002. // Edit
  1003. $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
  1004. $updated = new DateTime();
  1005. $shortUrl = $LINKSDB[$id]['shorturl'];
  1006. $new = false;
  1007. } else {
  1008. // New link
  1009. $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
  1010. $updated = null;
  1011. $shortUrl = link_small_hash($created, $id);
  1012. $new = true;
  1013. }
  1014. // Remove multiple spaces.
  1015. $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
  1016. // Remove first '-' char in tags.
  1017. $tags = preg_replace('/(^| )\-/', '$1', $tags);
  1018. // Remove duplicates.
  1019. $tags = implode(' ', array_unique(explode(' ', $tags)));
  1020. if (empty(trim($_POST['lf_url']))) {
  1021. $_POST['lf_url'] = '?' . smallHash($linkdate . $id);
  1022. }
  1023. $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols'));
  1024. $link = array(
  1025. 'id' => $id,
  1026. 'title' => trim($_POST['lf_title']),
  1027. 'url' => $url,
  1028. 'description' => $_POST['lf_description'],
  1029. 'private' => (isset($_POST['lf_private']) ? 1 : 0),
  1030. 'created' => $created,
  1031. 'updated' => $updated,
  1032. 'tags' => str_replace(',', ' ', $tags),
  1033. 'shorturl' => $shortUrl,
  1034. );
  1035. // If title is empty, use the URL as title.
  1036. if ($link['title'] == '') {
  1037. $link['title'] = $link['url'];
  1038. }
  1039. if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE) {
  1040. $thumbnailer = new Thumbnailer($conf);
  1041. $link['thumbnail'] = $thumbnailer->get($url);
  1042. }
  1043. $pluginManager->executeHooks('save_link', $link);
  1044. $LINKSDB[$id] = $link;
  1045. $LINKSDB->save($conf->get('resource.page_cache'));
  1046. if ($new) {
  1047. $history->addLink($link);
  1048. } else {
  1049. $history->updateLink($link);
  1050. }
  1051. // If we are called from the bookmarklet, we must close the popup:
  1052. if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
  1053. echo '<script>self.close();</script>';
  1054. exit;
  1055. }
  1056. $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
  1057. $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
  1058. // Scroll to the link which has been edited.
  1059. $location .= '#' . $link['shorturl'];
  1060. // After saving the link, redirect to the page the user was on.
  1061. header('Location: '. $location);
  1062. exit;
  1063. }
  1064. // -------- User clicked the "Cancel" button when editing a link.
  1065. if (isset($_POST['cancel_edit'])) {
  1066. $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false;
  1067. if (! isset($LINKSDB[$id])) {
  1068. header('Location: ?');
  1069. }
  1070. // If we are called from the bookmarklet, we must close the popup:
  1071. if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
  1072. echo '<script>self.close();</script>';
  1073. exit;
  1074. }
  1075. $link = $LINKSDB[$id];
  1076. $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
  1077. // Scroll to the link which has been edited.
  1078. $returnurl .= '#'. $link['shorturl'];
  1079. $returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
  1080. header('Location: '.$returnurl); // After canceling, redirect to the page the user was on.
  1081. exit;
  1082. }
  1083. // -------- User clicked the "Delete" button when editing a link: Delete link from database.
  1084. if ($targetPage == Router::$PAGE_DELETELINK) {
  1085. if (! $sessionManager->checkToken($_GET['token'])) {
  1086. die(t('Wrong token.'));
  1087. }
  1088. $ids = trim($_GET['lf_linkdate']);
  1089. if (strpos($ids, ' ') !== false) {
  1090. // multiple, space-separated ids provided
  1091. $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
  1092. } else {
  1093. // only a single id provided
  1094. $ids = [$ids];
  1095. }
  1096. // assert at least one id is given
  1097. if (!count($ids)) {
  1098. die('no id provided');
  1099. }
  1100. foreach ($ids as $id) {
  1101. $id = (int) escape($id);
  1102. $link = $LINKSDB[$id];
  1103. $pluginManager->executeHooks('delete_link', $link);
  1104. $history->deleteLink($link);
  1105. unset($LINKSDB[$id]);
  1106. }
  1107. $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
  1108. // If we are called from the bookmarklet, we must close the popup:
  1109. if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
  1110. echo '<script>self.close();</script>';
  1111. exit;
  1112. }
  1113. $location = '?';
  1114. if (isset($_SERVER['HTTP_REFERER'])) {
  1115. // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404.
  1116. $location = generateLocation(
  1117. $_SERVER['HTTP_REFERER'],
  1118. $_SERVER['HTTP_HOST'],
  1119. ['delete_link', 'edit_link', $link['shorturl']]
  1120. );
  1121. }
  1122. header('Location: ' . $location); // After deleting the link, redirect to appropriate location
  1123. exit;
  1124. }
  1125. // -------- User clicked the "EDIT" button on a link: Display link edit form.
  1126. if (isset($_GET['edit_link'])) {
  1127. $id = (int) escape($_GET['edit_link']);
  1128. $link = $LINKSDB[$id]; // Read database
  1129. if (!$link) {
  1130. header('Location: ?');
  1131. exit;
  1132. } // Link not found in database.
  1133. $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
  1134. $data = array(
  1135. 'link' => $link,
  1136. 'link_is_new' => false,
  1137. 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
  1138. 'tags' => $LINKSDB->linksCountPerTag(),
  1139. );
  1140. $pluginManager->executeHooks('render_editlink', $data);
  1141. foreach ($data as $key => $value) {
  1142. $PAGE->assign($key, $value);
  1143. }
  1144. $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
  1145. $PAGE->renderPage('editlink');
  1146. exit;
  1147. }
  1148. // -------- User want to post a new link: Display link edit form.
  1149. if (isset($_GET['post'])) {
  1150. $url = cleanup_url($_GET['post']);
  1151. $link_is_new = false;
  1152. // Check if URL is not already in database (in this case, we will edit the existing link)
  1153. $link = $LINKSDB->getLinkFromUrl($url);
  1154. if (! $link) {
  1155. $link_is_new = true;
  1156. $linkdate = strval(date(LinkDB::LINK_DATE_FORMAT));
  1157. // Get title if it was provided in URL (by the bookmarklet).
  1158. $title = empty($_GET['title']) ? '' : escape($_GET['title']);
  1159. // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
  1160. $description = empty($_GET['description']) ? '' : escape($_GET['description']);
  1161. $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
  1162. $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
  1163. // If this is an HTTP(S) link, we try go get the page to extract
  1164. // the title (otherwise we will to straight to the edit form.)
  1165. if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
  1166. // Short timeout to keep the application responsive
  1167. // The callback will fill $charset and $title with data from the downloaded page.
  1168. get_http_response(
  1169. $url,
  1170. $conf->get('general.download_timeout', 30),
  1171. $conf->get('general.download_max_size', 4194304),
  1172. get_curl_download_callback($charset, $title)
  1173. );
  1174. if (! empty($title) && strtolower($charset) != 'utf-8') {
  1175. $title = mb_convert_encoding($title, 'utf-8', $charset);
  1176. }
  1177. }
  1178. if ($url == '') {
  1179. $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
  1180. $title = $conf->get('general.default_note_title', t('Note: '));
  1181. }
  1182. $url = escape($url);
  1183. $title = escape($title);
  1184. $link = array(
  1185. 'linkdate' => $linkdate,
  1186. 'title' => $title,
  1187. 'url' => $url,
  1188. 'description' => $description,
  1189. 'tags' => $tags,
  1190. 'private' => $private,
  1191. );
  1192. } else {
  1193. $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
  1194. }
  1195. $data = array(
  1196. 'link' => $link,
  1197. 'link_is_new' => $link_is_new,
  1198. 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
  1199. 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
  1200. 'tags' => $LINKSDB->linksCountPerTag(),
  1201. 'default_private_links' => $conf->get('privacy.default_private_links', false),
  1202. );
  1203. $pluginManager->executeHooks('render_editlink', $data);
  1204. foreach ($data as $key => $value) {
  1205. $PAGE->assign($key, $value);
  1206. }
  1207. $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
  1208. $PAGE->renderPage('editlink');
  1209. exit;
  1210. }
  1211. if ($targetPage == Router::$PAGE_PINLINK) {
  1212. if (! isset($_GET['id']) || empty($LINKSDB[$_GET['id']])) {
  1213. // FIXME! Use a proper error system.
  1214. $msg = t('Invalid link ID provided');
  1215. echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>';
  1216. exit;
  1217. }
  1218. if (! $sessionManager->checkToken($_GET['token'])) {
  1219. die('Wrong token.');
  1220. }
  1221. $link = $LINKSDB[$_GET['id']];
  1222. $link['sticky'] = ! $link['sticky'];
  1223. $LINKSDB[(int) $_GET['id']] = $link;
  1224. $LINKSDB->save($conf->get('resource.page_cache'));
  1225. header('Location: '.index_url($_SERVER));
  1226. exit;
  1227. }
  1228. if ($targetPage == Router::$PAGE_EXPORT) {
  1229. // Export links as a Netscape Bookmarks file
  1230. if (empty($_GET['selection'])) {
  1231. $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
  1232. $PAGE->renderPage('export');
  1233. exit;
  1234. }
  1235. // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
  1236. $selection = $_GET['selection'];
  1237. if (isset($_GET['prepend_note_url'])) {
  1238. $prependNoteUrl = $_GET['prepend_note_url'];
  1239. } else {
  1240. $prependNoteUrl = false;
  1241. }
  1242. try {
  1243. $PAGE->assign(
  1244. 'links',
  1245. NetscapeBookmarkUtils::filterAndFormat(
  1246. $LINKSDB,
  1247. $selection,
  1248. $prependNoteUrl,
  1249. index_url($_SERVER)
  1250. )
  1251. );
  1252. } catch (Exception $exc) {
  1253. header('Content-Type: text/plain; charset=utf-8');
  1254. echo $exc->getMessage();
  1255. exit;
  1256. }
  1257. $now = new DateTime();
  1258. header('Content-Type: text/html; charset=utf-8');
  1259. header(
  1260. 'Content-disposition: attachment; filename=bookmarks_'
  1261. .$selection.'_'.$now->format(LinkDB::LINK_DATE_FORMAT).'.html'
  1262. );
  1263. $PAGE->assign('date', $now->format(DateTime::RFC822));
  1264. $PAGE->assign('eol', PHP_EOL);
  1265. $PAGE->assign('selection', $selection);
  1266. $PAGE->renderPage('export.bookmarks');
  1267. exit;
  1268. }
  1269. if ($targetPage == Router::$PAGE_IMPORT) {
  1270. // Upload a Netscape bookmark dump to import its contents
  1271. if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
  1272. // Show import dialog
  1273. $PAGE->assign(
  1274. 'maxfilesize',
  1275. get_max_upload_size(
  1276. ini_get('post_max_size'),
  1277. ini_get('upload_max_filesize'),
  1278. false
  1279. )
  1280. );
  1281. $PAGE->assign(
  1282. 'maxfilesizeHuman',
  1283. get_max_upload_size(
  1284. ini_get('post_max_size'),
  1285. ini_get('upload_max_filesize'),
  1286. true
  1287. )
  1288. );
  1289. $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli'));
  1290. $PAGE->renderPage('import');
  1291. exit;
  1292. }
  1293. // Import bookmarks from an uploaded file
  1294. if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
  1295. // The file is too big or some form field may be missing.
  1296. $msg = sprintf(
  1297. t(
  1298. 'The file you are trying to upload is probably bigger than what this webserver can accept'
  1299. .' (%s). Please upload in smaller chunks.'
  1300. ),
  1301. get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
  1302. );
  1303. echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
  1304. exit;
  1305. }
  1306. if (! $sessionManager->checkToken($_POST['token'])) {
  1307. die('Wrong token.');
  1308. }
  1309. $status = NetscapeBookmarkUtils::import(
  1310. $_POST,
  1311. $_FILES,
  1312. $LINKSDB,
  1313. $conf,
  1314. $history
  1315. );
  1316. echo '<script>alert("'.$status.'");document.location=\'?do='
  1317. .Router::$PAGE_IMPORT .'\';</script>';
  1318. exit;
  1319. }
  1320. // Plugin administration page
  1321. if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
  1322. $pluginMeta = $pluginManager->getPluginsMeta();
  1323. // Split plugins into 2 arrays: ordered enabled plugins and disabled.
  1324. $enabledPlugins = array_filter($pluginMeta, function ($v) {
  1325. return $v['order'] !== false;
  1326. });
  1327. // Load parameters.
  1328. $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array()));
  1329. uasort(
  1330. $enabledPlugins,
  1331. function ($a, $b) {
  1332. return $a['order'] - $b['order'];
  1333. }
  1334. );
  1335. $disabledPlugins = array_filter($pluginMeta, function ($v) {
  1336. return $v['order'] === false;
  1337. });
  1338. $PAGE->assign('enabledPlugins', $enabledPlugins);
  1339. $PAGE->assign('disabledPlugins', $disabledPlugins);
  1340. $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
  1341. $PAGE->renderPage('pluginsadmin');
  1342. exit;
  1343. }
  1344. // Plugin administration form action
  1345. if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
  1346. try {
  1347. if (isset($_POST['parameters_form'])) {
  1348. unset($_POST['parameters_form']);
  1349. foreach ($_POST as $param => $value) {
  1350. $conf->set('plugins.'. $param, escape($value));
  1351. }
  1352. } else {
  1353. $conf->set('general.enabled_plugins', save_plugin_config($_POST));
  1354. }
  1355. $conf->write($loginManager->isLoggedIn());
  1356. $history->updateSettings();
  1357. } catch (Exception $e) {
  1358. error_log(
  1359. 'ERROR while saving plugin configuration:.' . PHP_EOL .
  1360. $e->getMessage()
  1361. );
  1362. // TODO: do not handle exceptions/errors in JS.
  1363. echo '<script>alert("'
  1364. . $e->getMessage()
  1365. .'");document.location=\'?do='
  1366. . Router::$PAGE_PLUGINSADMIN
  1367. .'\';</script>';
  1368. exit;
  1369. }
  1370. header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
  1371. exit;
  1372. }
  1373. // Get a fresh token
  1374. if ($targetPage == Router::$GET_TOKEN) {
  1375. header('Content-Type:text/plain');
  1376. echo $sessionManager->generateToken($conf);
  1377. exit;
  1378. }
  1379. // -------- Thumbnails Update
  1380. if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
  1381. $ids = [];
  1382. foreach ($LINKSDB as $link) {
  1383. // A note or not HTTP(S)
  1384. if ($link['url'][0] === '?' || ! startsWith(strtolower($link['url']), 'http')) {
  1385. continue;
  1386. }
  1387. $ids[] = $link['id'];
  1388. }
  1389. $PAGE->assign('ids', $ids);
  1390. $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
  1391. $PAGE->renderPage('thumbnails');
  1392. exit;
  1393. }
  1394. // -------- Single Thumbnail Update
  1395. if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
  1396. if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
  1397. http_response_code(400);
  1398. exit;
  1399. }
  1400. $id = (int) $_POST['id'];
  1401. if (empty($LINKSDB[$id])) {
  1402. http_response_code(404);
  1403. exit;
  1404. }
  1405. $thumbnailer = new Thumbnailer($conf);
  1406. $link = $LINKSDB[$id];
  1407. $link['thumbnail'] = $thumbnailer->get($link['url']);
  1408. $LINKSDB[$id] = $link;
  1409. $LINKSDB->save($conf->get('resource.page_cache'));
  1410. echo json_encode($link);
  1411. exit;
  1412. }
  1413. // -------- Otherwise, simply display search form and links:
  1414. showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
  1415. exit;
  1416. }
  1417. /**
  1418. * Template for the list of links (<div id="linklist">)
  1419. * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
  1420. *
  1421. * @param pageBuilder $PAGE pageBuilder instance.
  1422. * @param LinkDB $LINKSDB LinkDB instance.
  1423. * @param ConfigManager $conf Configuration Manager instance.
  1424. * @param PluginManager $pluginManager Plugin Manager instance.
  1425. * @param LoginManager $loginManager LoginManager instance
  1426. */
  1427. function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
  1428. {
  1429. // Used in templates
  1430. if (isset($_GET['searchtags'])) {
  1431. if (! empty($_GET['searchtags'])) {
  1432. $searchtags = escape(normalize_spaces($_GET['searchtags']));
  1433. } else {
  1434. $searchtags = false;
  1435. }
  1436. } else {
  1437. $searchtags = '';
  1438. }
  1439. $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : '';
  1440. // Smallhash filter
  1441. if (! empty($_SERVER['QUERY_STRING'])
  1442. && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
  1443. try {
  1444. $linksToDisplay = $LINKSDB->filterHash($_SERVER['QUERY_STRING']);
  1445. } catch (LinkNotFoundException $e) {
  1446. $PAGE->render404($e->getMessage());
  1447. exit;
  1448. }
  1449. } else {
  1450. // Filter links according search parameters.
  1451. $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
  1452. $request = [
  1453. 'searchtags' => $searchtags,
  1454. 'searchterm' => $searchterm,
  1455. ];
  1456. $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility, !empty($_SESSION['untaggedonly']));
  1457. }
  1458. // ---- Handle paging.
  1459. $keys = array();
  1460. foreach ($linksToDisplay as $key => $value) {
  1461. $keys[] = $key;
  1462. }
  1463. // Select articles according to paging.
  1464. $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
  1465. $pagecount = $pagecount == 0 ? 1 : $pagecount;
  1466. $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
  1467. $page = $page < 1 ? 1 : $page;
  1468. $page = $page > $pagecount ? $pagecount : $page;
  1469. // Start index.
  1470. $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
  1471. $end = $i + $_SESSION['LINKS_PER_PAGE'];
  1472. $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
  1473. if ($thumbnailsEnabled) {
  1474. $thumbnailer = new Thumbnailer($conf);
  1475. }
  1476. $linkDisp = array();
  1477. while ($i<$end && $i<count($keys)) {
  1478. $link = $linksToDisplay[$keys[$i]];
  1479. $link['description'] = format_description(
  1480. $link['description'],
  1481. $conf->get('redirector.url'),
  1482. $conf->get('redirector.encode_url')
  1483. );
  1484. $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight';
  1485. $link['class'] = $link['private'] == 0 ? $classLi : 'private';
  1486. $link['timestamp'] = $link['created']->getTimestamp();
  1487. if (! empty($link['updated'])) {
  1488. $link['updated_timestamp'] = $link['updated']->getTimestamp();
  1489. } else {
  1490. $link['updated_timestamp'] = '';
  1491. }
  1492. $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
  1493. uasort($taglist, 'strcasecmp');
  1494. $link['taglist'] = $taglist;
  1495. // Logged in, thumbnails enabled, not a note,
  1496. // and (never retrieved yet or no valid cache file)
  1497. if ($loginManager->isLoggedIn() && $thumbnailsEnabled && $link['url'][0] != '?'
  1498. && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail'])))
  1499. ) {
  1500. $elem = $LINKSDB[$keys[$i]];
  1501. $elem['thumbnail'] = $thumbnailer->get($link['url']);
  1502. $LINKSDB[$keys[$i]] = $elem;
  1503. $updateDB = true;
  1504. $link['thumbnail'] = $elem['thumbnail'];
  1505. }
  1506. // Check for both signs of a note: starting with ? and 7 chars long.
  1507. if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
  1508. $link['url'] = index_url($_SERVER) . $link['url'];
  1509. }
  1510. $linkDisp[$keys[$i]] = $link;
  1511. $i++;
  1512. }
  1513. // If we retrieved new thumbnails, we update the database.
  1514. if (!empty($updateDB)) {
  1515. $LINKSDB->save($conf->get('resource.page_cache'));
  1516. }
  1517. // Compute paging navigation
  1518. $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
  1519. $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
  1520. $previous_page_url = '';
  1521. if ($i != count($keys)) {
  1522. $previous_page_url = '?page=' . ($page+1) . $searchtermUrl . $searchtagsUrl;
  1523. }
  1524. $next_page_url='';
  1525. if ($page>1) {
  1526. $next_page_url = '?page=' . ($page-1) . $searchtermUrl . $searchtagsUrl;
  1527. }
  1528. // Fill all template fields.
  1529. $data = array(
  1530. 'previous_page_url' => $previous_page_url,
  1531. 'next_page_url' => $next_page_url,
  1532. 'page_current' => $page,
  1533. 'page_max' => $pagecount,
  1534. 'result_count' => count($linksToDisplay),
  1535. 'search_term' => $searchterm,
  1536. 'search_tags' => $searchtags,
  1537. 'visibility' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '',
  1538. 'redirector' => $conf->get('redirector.url'), // Optional redirector URL.
  1539. 'links' => $linkDisp,
  1540. );
  1541. // If there is only a single link, we change on-the-fly the title of the page.
  1542. if (count($linksToDisplay) == 1) {
  1543. $data['pagetitle'] = $linksToDisplay[$keys[0]]['title'] .' - '. $conf->get('general.title');
  1544. } elseif (! empty($searchterm) || ! empty($searchtags)) {
  1545. $data['pagetitle'] = t('Search: ');
  1546. $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
  1547. $bracketWrap = function ($tag) {
  1548. return '['. $tag .']';
  1549. };
  1550. $data['pagetitle'] .= ! empty($searchtags)
  1551. ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchtags))).' '
  1552. : '';
  1553. $data['pagetitle'] .= '- '. $conf->get('general.title');
  1554. }
  1555. $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
  1556. foreach ($data as $key => $value) {
  1557. $PAGE->assign($key, $value);
  1558. }
  1559. return;
  1560. }
  1561. /**
  1562. * Installation
  1563. * This function should NEVER be called if the file data/config.php exists.
  1564. *
  1565. * @param ConfigManager $conf Configuration Manager instance.
  1566. * @param SessionManager $sessionManager SessionManager instance
  1567. * @param LoginManager $loginManager LoginManager instance
  1568. */
  1569. function install($conf, $sessionManager, $loginManager)
  1570. {
  1571. // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
  1572. if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) {
  1573. mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705);
  1574. }
  1575. // This part makes sure sessions works correctly.
  1576. // (Because on some hosts, session.save_path may not be set correctly,
  1577. // or we may not have write access to it.)
  1578. if (isset($_GET['test_session'])
  1579. && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) {
  1580. // Step 2: Check if data in session is correct.
  1581. $msg = t(
  1582. '<pre>Sessions do not seem to work correctly on your server.<br>'.
  1583. 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
  1584. 'and that you have write access to it.<br>'.
  1585. 'It currently points to %s.<br>'.
  1586. 'On some browsers, accessing your server via a hostname like \'localhost\' '.
  1587. 'or any custom hostname without a dot causes cookie storage to fail. '.
  1588. 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
  1589. );
  1590. $msg = sprintf($msg, session_save_path());
  1591. echo $msg;
  1592. echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
  1593. die;
  1594. }
  1595. if (!isset($_SESSION['session_tested'])) {
  1596. // Step 1 : Try to store data in session and reload page.
  1597. $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session.
  1598. header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data.
  1599. }
  1600. if (isset($_GET['test_session'])) {
  1601. // Step 3: Sessions are OK. Remove test parameter from URL.
  1602. header('Location: '.index_url($_SERVER));
  1603. }
  1604. if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) {
  1605. $tz = 'UTC';
  1606. if (!empty($_POST['continent']) && !empty($_POST['city'])
  1607. && isTimeZoneValid($_POST['continent'], $_POST['city'])
  1608. ) {
  1609. $tz = $_POST['continent'].'/'.$_POST['city'];
  1610. }
  1611. $conf->set('general.timezone', $tz);
  1612. $login = $_POST['setlogin'];
  1613. $conf->set('credentials.login', $login);
  1614. $salt = sha1(uniqid('', true) .'_'. mt_rand());
  1615. $conf->set('credentials.salt', $salt);
  1616. $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
  1617. if (!empty($_POST['title'])) {
  1618. $conf->set('general.title', escape($_POST['title']));
  1619. } else {
  1620. $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
  1621. }
  1622. $conf->set('translation.language', escape($_POST['language']));
  1623. $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
  1624. $conf->set('api.enabled', !empty($_POST['enableApi']));
  1625. $conf->set(
  1626. 'api.secret',
  1627. generate_api_secret(
  1628. $conf->get('credentials.login'),
  1629. $conf->get('credentials.salt')
  1630. )
  1631. );
  1632. try {
  1633. // Everything is ok, let's create config file.
  1634. $conf->write($loginManager->isLoggedIn());
  1635. } catch (Exception $e) {
  1636. error_log(
  1637. 'ERROR while writing config file after installation.' . PHP_EOL .
  1638. $e->getMessage()
  1639. );
  1640. // TODO: do not handle exceptions/errors in JS.
  1641. echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
  1642. exit;
  1643. }
  1644. echo '<script>alert('
  1645. .'"Shaarli is now configured. '
  1646. .'Please enter your login/password and start shaaring your links!"'
  1647. .');document.location=\'?do=login\';</script>';
  1648. exit;
  1649. }
  1650. $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
  1651. list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
  1652. $PAGE->assign('continents', $continents);
  1653. $PAGE->assign('cities', $cities);
  1654. $PAGE->assign('languages', Languages::getAvailableLanguages());
  1655. $PAGE->renderPage('install');
  1656. exit;
  1657. }
  1658. if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
  1659. showDailyRSS($conf, $loginManager);
  1660. exit;
  1661. }
  1662. if (!isset($_SESSION['LINKS_PER_PAGE'])) {
  1663. $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
  1664. }
  1665. try {
  1666. $history = new History($conf->get('resource.history'));
  1667. } catch (Exception $e) {
  1668. die($e->getMessage());
  1669. }
  1670. $linkDb = new LinkDB(
  1671. $conf->get('resource.datastore'),
  1672. $loginManager->isLoggedIn(),
  1673. $conf->get('privacy.hide_public_links'),
  1674. $conf->get('redirector.url'),
  1675. $conf->get('redirector.encode_url')
  1676. );
  1677. $container = new \Slim\Container();
  1678. $container['conf'] = $conf;
  1679. $container['plugins'] = $pluginManager;
  1680. $container['history'] = $history;
  1681. $app = new \Slim\App($container);
  1682. // REST API routes
  1683. $app->group('/api/v1', function () {
  1684. $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
  1685. $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
  1686. $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
  1687. $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
  1688. $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
  1689. $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
  1690. $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags');
  1691. $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag');
  1692. $this->put('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:putTag')->setName('putTag');
  1693. $this->delete('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:deleteTag')->setName('deleteTag');
  1694. $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory');
  1695. })->add('\Shaarli\Api\ApiMiddleware');
  1696. $response = $app->run(true);
  1697. // Hack to make Slim and Shaarli router work together:
  1698. // If a Slim route isn't found and NOT API call, we call renderPage().
  1699. if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
  1700. // We use UTF-8 for proper international characters handling.
  1701. header('Content-Type: text/html; charset=utf-8');
  1702. renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
  1703. } else {
  1704. $response = $response
  1705. ->withHeader('Access-Control-Allow-Origin', '*')
  1706. ->withHeader(
  1707. 'Access-Control-Allow-Headers',
  1708. 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
  1709. )
  1710. ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  1711. $app->respond($response);
  1712. }