index.php 73 KB

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