index.php 73 KB

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