<?php
/**
* GET an HTTP URL to retrieve its content
+ * Uses the cURL library or a fallback method
*
* @param string $url URL to get (http://...)
* @param int $timeout network timeout (in seconds)
* echo 'There was an error: '.htmlspecialchars($headers[0]);
* }
*
- * @see http://php.net/manual/en/function.file-get-contents.php
- * @see http://php.net/manual/en/function.stream-context-create.php
- * @see http://php.net/manual/en/function.get-headers.php
+ * @see https://secure.php.net/manual/en/ref.curl.php
+ * @see https://secure.php.net/manual/en/functions.anonymous.php
+ * @see https://secure.php.net/manual/en/function.preg-split.php
+ * @see https://secure.php.net/manual/en/function.explode.php
+ * @see http://stackoverflow.com/q/17641073
+ * @see http://stackoverflow.com/q/9183178
+ * @see http://stackoverflow.com/q/1462720
*/
function get_http_response($url, $timeout = 30, $maxBytes = 4194304)
{
$urlObj = new Url($url);
$cleanUrl = $urlObj->idnToAscii();
- if (! filter_var($cleanUrl, FILTER_VALIDATE_URL) || ! $urlObj->isHttp()) {
+ if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) {
return array(array(0 => 'Invalid HTTP Url'), false);
}
+ $userAgent =
+ 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)'
+ . ' Gecko/20100101 Firefox/45.0';
+ $acceptLanguage =
+ substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3';
+ $maxRedirs = 3;
+
+ if (!function_exists('curl_init')) {
+ return get_http_response_fallback(
+ $cleanUrl,
+ $timeout,
+ $maxBytes,
+ $userAgent,
+ $acceptLanguage,
+ $maxRedirs
+ );
+ }
+
+ $ch = curl_init($cleanUrl);
+ if ($ch === false) {
+ return array(array(0 => 'curl_init() error'), false);
+ }
+
+ // General cURL settings
+ curl_setopt($ch, CURLOPT_AUTOREFERER, true);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_HEADER, true);
+ curl_setopt(
+ $ch,
+ CURLOPT_HTTPHEADER,
+ array('Accept-Language: ' . $acceptLanguage)
+ );
+ curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
+ curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
+
+ // Max download size management
+ curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024);
+ curl_setopt($ch, CURLOPT_NOPROGRESS, false);
+ curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
+ function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes)
+ {
+ if (version_compare(phpversion(), '5.5', '<')) {
+ // PHP version lower than 5.5
+ // Callback has 4 arguments
+ $downloaded = $arg1;
+ } else {
+ // Callback has 5 arguments
+ $downloaded = $arg2;
+ }
+ // Non-zero return stops downloading
+ return ($downloaded > $maxBytes) ? 1 : 0;
+ }
+ );
+
+ $response = curl_exec($ch);
+ $errorNo = curl_errno($ch);
+ $errorStr = curl_error($ch);
+ $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+ curl_close($ch);
+
+ if ($response === false) {
+ if ($errorNo == CURLE_COULDNT_RESOLVE_HOST) {
+ /*
+ * Workaround to match fallback method behaviour
+ * Removing this would require updating
+ * GetHttpUrlTest::testGetInvalidRemoteUrl()
+ */
+ return array(false, false);
+ }
+ return array(array(0 => 'curl_exec() error: ' . $errorStr), false);
+ }
+
+ // Formatting output like the fallback method
+ $rawHeaders = substr($response, 0, $headSize);
+
+ // Keep only headers from latest redirection
+ $rawHeadersArrayRedirs = explode("\r\n\r\n", trim($rawHeaders));
+ $rawHeadersLastRedir = end($rawHeadersArrayRedirs);
+
+ $content = substr($response, $headSize);
+ $headers = array();
+ foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) {
+ if (empty($line) or ctype_space($line)) {
+ continue;
+ }
+ $splitLine = explode(': ', $line, 2);
+ if (count($splitLine) > 1) {
+ $key = $splitLine[0];
+ $value = $splitLine[1];
+ if (array_key_exists($key, $headers)) {
+ if (!is_array($headers[$key])) {
+ $headers[$key] = array(0 => $headers[$key]);
+ }
+ $headers[$key][] = $value;
+ } else {
+ $headers[$key] = $value;
+ }
+ } else {
+ $headers[] = $splitLine[0];
+ }
+ }
+
+ return array($headers, $content);
+}
+
+/**
+ * GET an HTTP URL to retrieve its content (fallback method)
+ *
+ * @param string $cleanUrl URL to get (http://... valid and in ASCII form)
+ * @param int $timeout network timeout (in seconds)
+ * @param int $maxBytes maximum downloaded bytes
+ * @param string $userAgent "User-Agent" header
+ * @param string $acceptLanguage "Accept-Language" header
+ * @param int $maxRedr maximum amount of redirections followed
+ *
+ * @return array HTTP response headers, downloaded content
+ *
+ * Output format:
+ * [0] = associative array containing HTTP response headers
+ * [1] = URL content (downloaded data)
+ *
+ * @see http://php.net/manual/en/function.file-get-contents.php
+ * @see http://php.net/manual/en/function.stream-context-create.php
+ * @see http://php.net/manual/en/function.get-headers.php
+ */
+function get_http_response_fallback(
+ $cleanUrl,
+ $timeout,
+ $maxBytes,
+ $userAgent,
+ $acceptLanguage,
+ $maxRedr
+) {
$options = array(
'http' => array(
'method' => 'GET',
'timeout' => $timeout,
- 'user_agent' => 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)'
- .' Gecko/20100101 Firefox/45.0',
- 'accept_language' => substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3',
+ 'user_agent' => $userAgent,
+ 'header' => "Accept: */*\r\n"
+ . 'Accept-Language: ' . $acceptLanguage
)
);
stream_context_set_default($options);
- list($headers, $finalUrl) = get_redirected_headers($cleanUrl);
+ list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
if (! $headers || strpos($headers[0], '200 OK') === false) {
$options['http']['request_fulluri'] = true;
stream_context_set_default($options);
- list($headers, $finalUrl) = get_redirected_headers($cleanUrl);
+ list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
}
- if (! $headers || strpos($headers[0], '200 OK') === false) {
+ if (! $headers) {
return array($headers, false);
}
}
return index_url($server);
}
+
+ /**
+ * Retrieve the initial IP forwarded by the reverse proxy.
+ *
+ * Inspired from: https://github.com/zendframework/zend-http/blob/master/src/PhpEnvironment/RemoteAddress.php
+ *
+ * @param array $server $_SERVER array which contains HTTP headers.
+ * @param array $trustedIps List of trusted IP from the configuration.
+ *
+ * @return string|bool The forwarded IP, or false if none could be extracted.
+ */
+ function getIpAddressFromProxy($server, $trustedIps)
+ {
+ $forwardedIpHeader = 'HTTP_X_FORWARDED_FOR';
+ if (empty($server[$forwardedIpHeader])) {
+ return false;
+ }
+
+ $ips = preg_split('/\s*,\s*/', $server[$forwardedIpHeader]);
+ $ips = array_diff($ips, $trustedIps);
+ if (empty($ips)) {
+ return false;
+ }
+
+ return array_pop($ips);
+ }
<?php
/**
- * Shaarli v0.7.0 - Shaare your links...
+ * Shaarli v0.8.0 - Shaare your links...
*
* The personal, minimalist, super-fast, database free, bookmarking service.
*
/*
* PHP configuration
*/
-define('shaarli_version', '0.7.0');
+define('shaarli_version', '0.8.0');
// http://server.com/x/shaarli --> /shaarli/
define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
//error_reporting(-1);
+// 3rd-party libraries
+if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
+ header('Content-Type: text/plain; charset=utf-8');
+ echo "Error: missing Composer configuration\n\n"
+ ."If you installed Shaarli through Git or using the development branch,\n"
+ ."please refer to the installation documentation to install PHP"
+ ." dependencies using Composer:\n"
+ ."- https://github.com/shaarli/Shaarli/wiki/Server-requirements\n"
+ ."- https://github.com/shaarli/Shaarli/wiki/Download-and-Installation";
+ exit;
+}
+require_once 'inc/rain.tpl.class.php';
+require_once __DIR__ . '/vendor/autoload.php';
+
// Shaarli library
require_once 'application/ApplicationUtils.php';
require_once 'application/Cache.php';
require_once 'application/FeedBuilder.php';
require_once 'application/FileUtils.php';
require_once 'application/HttpUtils.php';
+require_once 'application/Languages.php';
require_once 'application/LinkDB.php';
require_once 'application/LinkFilter.php';
require_once 'application/LinkUtils.php';
require_once 'application/PluginManager.php';
require_once 'application/Router.php';
require_once 'application/Updater.php';
-require_once 'inc/rain.tpl.class.php';
// Ensure the PHP version is supported
try {
function ban_loginFailed($conf)
{
$ip = $_SERVER['REMOTE_ADDR'];
+ $trusted = $conf->get('security.trusted_proxies', array());
+ if (in_array($ip, $trusted)) {
+ $ip = getIpAddressFromProxy($_SERVER, $trusted);
+ if (!$ip) {
+ return;
+ }
+ }
$gb = $GLOBALS['IPBANS'];
- if (!isset($gb['FAILURES'][$ip])) $gb['FAILURES'][$ip]=0;
+ if (! isset($gb['FAILURES'][$ip])) {
+ $gb['FAILURES'][$ip]=0;
+ }
$gb['FAILURES'][$ip]++;
if ($gb['FAILURES'][$ip] > ($conf->get('security.ban_after') - 1))
{
if ($targetPage == Router::$PAGE_LOGIN)
{
if ($conf->get('security.open_shaarli')) { header('Location: ?'); exit; } // No need to login for open Shaarli
- $token=''; if (ban_canLogin($conf)) $token=getToken($conf); // Do not waste token generation if not useful.
- $PAGE->assign('token',$token);
if (isset($_GET['username'])) {
$PAGE->assign('username', escape($_GET['username']));
}
}
else // show the change password form.
{
- $PAGE->assign('token',getToken($conf));
$PAGE->renderPage('changepassword');
exit;
}
}
else // Show the configuration form.
{
- $PAGE->assign('token',getToken($conf));
$PAGE->assign('title', $conf->get('general.title'));
$PAGE->assign('redirector', $conf->get('redirector.url'));
list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone'));
if ($targetPage == Router::$PAGE_CHANGETAG)
{
if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
- $PAGE->assign('token', getToken($conf));
$PAGE->assign('tags', $LINKSDB->allTags());
$PAGE->renderPage('changetag');
exit;
$data = array(
'link' => $link,
'link_is_new' => false,
- 'token' => getToken($conf),
'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
'tags' => $LINKSDB->allTags(),
);
$data = array(
'link' => $link,
'link_is_new' => $link_is_new,
- 'token' => getToken($conf), // XSRF protection.
'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
'tags' => $LINKSDB->allTags(),
- 'default_private_links' => $conf->get('default_private_links', false),
+ 'default_private_links' => $conf->get('privacy.default_private_links', false),
);
$pluginManager->executeHooks('render_editlink', $data);
exit;
}
- // -------- User is uploading a file for import
- if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=upload'))
- {
- // If file is too big, some form field may be missing.
- if (!isset($_POST['token']) || (!isset($_FILES)) || (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size']==0))
- {
- $returnurl = ( empty($_SERVER['HTTP_REFERER']) ? '?' : $_SERVER['HTTP_REFERER'] );
- echo '<script>alert("The file you are trying to upload is probably bigger than what this webserver can accept ('.getMaxFileSize().' bytes). Please upload in smaller chunks.");document.location=\''.escape($returnurl).'\';</script>';
+ if ($targetPage == Router::$PAGE_IMPORT) {
+ // Upload a Netscape bookmark dump to import its contents
+
+ if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
+ // Show import dialog
+ $PAGE->assign('maxfilesize', getMaxFileSize());
+ $PAGE->renderPage('import');
exit;
}
- if (!tokenOk($_POST['token'])) die('Wrong token.');
- importFile($LINKSDB);
- exit;
- }
- // -------- Show upload/import dialog:
- if ($targetPage == Router::$PAGE_IMPORT)
- {
- $PAGE->assign('token',getToken($conf));
- $PAGE->assign('maxfilesize',getMaxFileSize());
- $PAGE->renderPage('import');
+ // Import bookmarks from an uploaded file
+ if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
+ // The file is too big or some form field may be missing.
+ echo '<script>alert("The file you are trying to upload is probably'
+ .' bigger than what this webserver can accept ('
+ .getMaxFileSize().' bytes).'
+ .' Please upload in smaller chunks.");document.location=\'?do='
+ .Router::$PAGE_IMPORT .'\';</script>';
+ exit;
+ }
+ if (! tokenOk($_POST['token'])) {
+ die('Wrong token.');
+ }
+ $status = NetscapeBookmarkUtils::import(
+ $_POST,
+ $_FILES,
+ $LINKSDB,
+ $conf->get('resource.page_cache')
+ );
+ echo '<script>alert("'.$status.'");document.location=\'?do='
+ .Router::$PAGE_IMPORT .'\';</script>';
exit;
}
exit;
}
-/**
- * Process the import file form.
- *
- * @param LinkDB $LINKSDB Loaded LinkDB instance.
- * @param ConfigManager $conf Configuration Manager instance.
- */
-function importFile($LINKSDB, $conf)
-{
- if (!isLoggedIn()) { die('Not allowed.'); }
-
- $filename=$_FILES['filetoupload']['name'];
- $filesize=$_FILES['filetoupload']['size'];
- $data=file_get_contents($_FILES['filetoupload']['tmp_name']);
- $private = (empty($_POST['private']) ? 0 : 1); // Should the links be imported as private?
- $overwrite = !empty($_POST['overwrite']) ; // Should the imported links overwrite existing ones?
- $import_count=0;
-
- // Sniff file type:
- $type='unknown';
- if (startsWith($data,'<!DOCTYPE NETSCAPE-Bookmark-file-1>')) $type='netscape'; // Netscape bookmark file (aka Firefox).
-
- // Then import the bookmarks.
- if ($type=='netscape')
- {
- // This is a standard Netscape-style bookmark file.
- // This format is supported by all browsers (except IE, of course), also Delicious, Diigo and others.
- foreach(explode('<DT>',$data) as $html) // explode is very fast
- {
- $link = array('linkdate'=>'','title'=>'','url'=>'','description'=>'','tags'=>'','private'=>0);
- $d = explode('<DD>',$html);
- if (startsWith($d[0], '<A '))
- {
- $link['description'] = (isset($d[1]) ? html_entity_decode(trim($d[1]),ENT_QUOTES,'UTF-8') : ''); // Get description (optional)
- preg_match('!<A .*?>(.*?)</A>!i',$d[0],$matches); $link['title'] = (isset($matches[1]) ? trim($matches[1]) : ''); // Get title
- $link['title'] = html_entity_decode($link['title'],ENT_QUOTES,'UTF-8');
- preg_match_all('! ([A-Z_]+)=\"(.*?)"!i',$html,$matches,PREG_SET_ORDER); // Get all other attributes
- $raw_add_date=0;
- foreach($matches as $m)
- {
- $attr=$m[1]; $value=$m[2];
- if ($attr=='HREF') $link['url']=html_entity_decode($value,ENT_QUOTES,'UTF-8');
- elseif ($attr=='ADD_DATE')
- {
- $raw_add_date=intval($value);
- if ($raw_add_date>30000000000) $raw_add_date/=1000; //If larger than year 2920, then was likely stored in milliseconds instead of seconds
- }
- elseif ($attr=='PRIVATE') $link['private']=($value=='0'?0:1);
- elseif ($attr=='TAGS') $link['tags']=html_entity_decode(str_replace(',',' ',$value),ENT_QUOTES,'UTF-8');
- }
- if ($link['url']!='')
- {
- if ($private==1) $link['private']=1;
- $dblink = $LINKSDB->getLinkFromUrl($link['url']); // See if the link is already in database.
- if ($dblink==false)
- { // Link not in database, let's import it...
- if (empty($raw_add_date)) $raw_add_date=time(); // In case of shitty bookmark file with no ADD_DATE
-
- // Make sure date/time is not already used by another link.
- // (Some bookmark files have several different links with the same ADD_DATE)
- // We increment date by 1 second until we find a date which is not used in DB.
- // (so that links that have the same date/time are more or less kept grouped by date, but do not conflict.)
- while (!empty($LINKSDB[date('Ymd_His',$raw_add_date)])) { $raw_add_date++; }// Yes, I know it's ugly.
- $link['linkdate']=date('Ymd_His',$raw_add_date);
- $LINKSDB[$link['linkdate']] = $link;
- $import_count++;
- }
- else // Link already present in database.
- {
- if ($overwrite)
- { // If overwrite is required, we import link data, except date/time.
- $link['linkdate']=$dblink['linkdate'];
- $LINKSDB[$link['linkdate']] = $link;
- $import_count++;
- }
- }
-
- }
- }
- }
- $LINKSDB->savedb($conf->get('resource.page_cache'));
-
- echo '<script>alert("File '.json_encode($filename).' ('.$filesize.' bytes) was successfully processed: '.$import_count.' links imported.");document.location=\'?\';</script>';
- }
- else
- {
- echo '<script>alert("File '.json_encode($filename).' ('.$filesize.' bytes) has an unknown file format. Nothing was imported.");document.location=\'?\';</script>';
- }
-}
-
/**
* Template for the list of links (<div id="linklist">)
* This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
'search_term' => $searchterm,
'search_tags' => $searchtags,
'redirector' => $conf->get('redirector.url'), // Optional redirector URL.
- 'token' => $token,
'links' => $linkDisp,
'tags' => $LINKSDB->allTags(),
);