From 99c9c9541b012eebdb84ee33a765bb18a62fb0d7 Mon Sep 17 00:00:00 2001 From: Seb Sauvage Date: Thu, 6 Oct 2011 16:56:16 +0200 Subject: [PATCH] =?utf8?q?Version=200.0.24=20beta:=20-=20Corrected:=20Now?= =?utf8?q?=20thumnails=20also=20work=20for=20imgur=20gallery=20links=20(/g?= =?utf8?q?allery/=E2=80=A6)=20(Thanks=20to=20Accent=20Grave=20for=20the=20?= =?utf8?q?correction)=20-=20Corrected:=20Removed=20useless=20debugging=20i?= =?utf8?q?nformation=20in=20log.=20-=20Corrected:=20The=20filter=20in=20RS?= =?utf8?q?S/ATOM=20feed=20now=20works=20again=20properly=20(it=20was=20bro?= =?utf8?q?ken=20in=200.0.17=20beta)=20-=20New:=20As=20per=20request,=20you?= =?utf8?q?=20can=20now=20post=20an=20entry=20without=20a=20link.=20(You=20?= =?utf8?q?can=20use=20Shaarli=20as=20a=20kind=20of=20=E2=80=9Cpersonal=20t?= =?utf8?q?witter=E2=80=9D).=20-=20New:=20Each=20Shaarli=20entry=20now=20ha?= =?utf8?q?s=20a=20short=20link=20(just=20click=20on=20the=20date=20of=20a?= =?utf8?q?=20link).=20Now=20you=20can=20send=20a=20link=20that=20points=20?= =?utf8?q?to=20a=20single=20entry=20in=20your=20Shaarli.=20-=20New:=20In?= =?utf8?q?=20descriptions,=20URLs=20are=20now=20clickable.=20-=20New:=20Th?= =?utf8?q?umbnails=20will=20be=20generated=20for=20all=20link=20pointing?= =?utf8?q?=20to=20.jpg/png/gif=20(as=20long=20as=20the=20images=20are=20le?= =?utf8?q?ss=20than=204=20Mb=20and=20take=20less=20than=2030=20seconds=20t?= =?utf8?q?o=20download).?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- index.php | 129 ++++++++++++++++++++++++++++++++++++++++++++++------ shaarli.css | 7 ++- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/index.php b/index.php index a9491ed3..6e7e638c 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,5 @@ yZH23w + Small hashes: + - are unique (well, as unique as crc32, at last) + - are always 6 characters long. + - only use the following characters: a-z A-Z 0-9 - _ @ + - are NOT cryptographically secure (they CAN be forged) + In Shaarli, they are used as a tinyurl-like link to individual entries. +*/ +function smallHash($text) +{ + $t = rtrim(base64_encode(hash('crc32',$text,true)),'='); + $t = str_replace('+','-',$t); // Get rid of characters which need encoding in URLs. + $t = str_replace('/','_',$t); + $t = str_replace('=','@',$t); + return $t; +} + +// Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 +function text2clickable($url) +{ + return preg_replace('!((?:https?|ftp)://\S+[[:alnum:]]/?)!si','$1 ',$url); +} // ------------------------------------------------------------------------------------------ // Sniff browser language to display dates in the right format automatically. // (Note that is may not work on your server if the corresponding local is not installed.) @@ -381,7 +404,7 @@ function getHTTP($url,$timeout=30) { $options = array('http'=>array('method'=>'GET','timeout' => $timeout)); // Force network timeout $context = stream_context_create($options); - $data=file_get_contents($url,false,$context,-1, 2000000); // We download at most 2 Mb from source. + $data=file_get_contents($url,false,$context,-1, 4000000); // We download at most 4 Mb from source. if (!$data) { $lasterror=error_get_last(); return array($lasterror['message'],array(),''); } $httpStatus=$http_response_header[0]; // eg. "HTTP/1.1 200 OK" $responseHeaders=http_parse_headers($http_response_header); @@ -567,6 +590,22 @@ class linkdb implements Iterator, Countable, ArrayAccess krsort($filtered); return $filtered; } + + // Filter by smallHash. + // Only 1 article is returned. + public function filterSmallHash($smallHash) + { + $filtered=array(); + foreach($this->links as $l) + { + if ($smallHash==smallHash($l['linkdate'])) // Yes, this is ugly and slow + { + $filtered[$l['linkdate']] = $l; + return $filtered; + } + } + return $filtered; + } // Returns the list of all tags // Output: associative array key=tags, value=0 @@ -1011,7 +1050,7 @@ HTML; $linkdate = strval(date('Ymd_His')); $title = (empty($_GET['title']) ? '' : $_GET['title'] ); // Get title if it was provided in URL (by the bookmarklet). $description=''; $tags=''; $private=0; - if (parse_url($url,PHP_URL_SCHEME)=='') $url = 'http://'.$url; + if (($url!='') && parse_url($url,PHP_URL_SCHEME)=='') $url = 'http://'.$url; // If this is an HTTP link, we try go get the page to extact the title (otherwise we will to straight to the edit form.) if (empty($title) && parse_url($url,PHP_URL_SCHEME)=='http') { @@ -1019,6 +1058,7 @@ HTML; // FIXME: Decode charset according to specified in either 1) HTTP response headers or 2) in html if (strpos($status,'200 OK')) $title=html_entity_decode(html_extract_title($data),ENT_QUOTES,'UTF-8'); } + if ($url=='') $url='?'.smallHash($linkdate); // In case of empty URL, this is just a text (with a link that point to itself) $link = array('linkdate'=>$linkdate,'title'=>$title,'url'=>$url,'description'=>$description,'tags'=>$tags,'private'=>0); } list($editform,$onload)=templateEditForm($link,$link_is_new); @@ -1236,7 +1276,7 @@ HTML; function templateLinkList() { global $LINKSDB; - + // Search according to entered search terms: $linksToDisplay=array(); $searched=''; @@ -1251,6 +1291,10 @@ function templateLinkList() $tagshtml=''; foreach(explode(' ',trim($_GET['searchtags'])) as $tag) $tagshtml.=''.htmlspecialchars($tag).' x '; $searched=' '.count($linksToDisplay).' results for tags '.$tagshtml.':'; } + elseif (preg_match('/[a-zA-Z0-9-_@]{6}/',$_SERVER["QUERY_STRING"])) // Detect smallHashes in URL + { + $linksToDisplay = $LINKSDB->filterSmallHash($_SERVER["QUERY_STRING"]); + } else $linksToDisplay = $LINKSDB; // otherwise, display without filtering. @@ -1273,7 +1317,7 @@ function templateLinkList() while ($i<$end && $i'; @@ -1281,9 +1325,10 @@ function templateLinkList() if ($link['tags']!='') foreach(explode(' ',$link['tags']) as $tag) { $tags.=''.htmlspecialchars($tag).' '; } $linklist.='
  • '.thumbnail($link['url']); $linklist.='
  • \n"; + if ($description!='') $linklist.='
    '.nl2br($description).'

    '; + if (!HIDE_TIMESTAMPS || isLoggedIn()) $linklist.=''.htmlspecialchars(linkdate2locale($link['linkdate'])).' - '; + else $linklist.='link - '; + $linklist.=''.htmlspecialchars($link['url']).'
    '.$tags."\n"; $i++; } @@ -1323,6 +1368,7 @@ function thumbnail($url) { $path = parse_url($url,PHP_URL_PATH); if (substr_count($path,'/')==1) return '
    '; + if (strpos($path,'/gallery/')==0) return '
    '; } if ($domain=='i.imgur.com') { @@ -1356,6 +1402,15 @@ function thumbnail($url) return '
    '; } + // For all other, we try to make a thumbnail of links ending with .jpg/jpeg/png/gif + // Technically speaking, we should download ALL links and check their Content-Type to see if they are images. + // But using the extension will do. + $ext=strtolower(pathinfo($url,PATHINFO_EXTENSION)); + if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif') + { + $sign = hash_hmac('sha256', $url, $GLOBALS['salt']); // We use the salt to sign data (it's random, secret, and specific to each installation) + return '
    '; + } return ''; // No thumbnail. } @@ -1406,8 +1461,10 @@ $(document).ready(function() JS; } $feedurl=htmlspecialchars(serverUrl().$_SERVER['SCRIPT_NAME']); - if (!empty($_GET['searchtags'])) $feedurl.='&searchtags='.$_GET['searchtags']; - elseif (!empty($_GET['searchterm'])) $feedurl.='&searchterm='.$_GET['searchterm']; + $feedsearch=''; + if (!empty($_GET['searchtags'])) $feedsearch.='&searchtags='.$_GET['searchtags']; + elseif (!empty($_GET['searchterm'])) $feedsearch.='&searchterm='.$_GET['searchterm']; + $version=shaarli_version; $title = htmlspecialchars( $GLOBALS['title'] ); echo << {$title} - + {$jsincludes} {$newversion} @@ -1645,7 +1702,6 @@ function genThumbnail() // Is this a link to an image, or to a flickr page ? $imageurl=''; - logm('url: '.$url); if (endswith(parse_url($url,PHP_URL_PATH),'.jpg')) { // This is a direct link to an image. eg. http://farm1.static.flickr.com/5/5921913_ac83ed27bd_o.jpg preg_match('!(http://farm\d+.static.flickr.com/\d+/\d+_\w+_)\w.jpg!',$url,$matches); @@ -1671,7 +1727,6 @@ function genThumbnail() return; } } - else logm('unkown flickr url: '.$url); } if ($domain=='vimeo.com' ) @@ -1695,6 +1750,20 @@ function genThumbnail() } } } + + // For all other domains, we try to download the image and make a thumbnail. + list($httpstatus,$headers,$data) = getHTTP($url,30); // We allow 30 seconds max to download (and downloads are limited to 4 Mb) + if (strpos($httpstatus,'200 OK')) + { + $filepath=CACHEDIR.'/'.$thumbname; + file_put_contents($filepath,$data); // Save image to cache. + if (resizeImage($filepath)) + { + header('Content-Type: image/jpeg'); + echo file_get_contents($filepath); + return; + } + } // Otherwise, return an empty image (8x8 transparent gif) $blankgif = base64_decode('R0lGODlhCAAIAIAAAP///////yH5BAEKAAEALAAAAAAIAAgAAAIHjI+py+1dAAA7'); @@ -1703,6 +1772,36 @@ function genThumbnail() echo $blankgif; } +// Make a thumbnail of the image (to width: 120 pixels) +// Returns true if success, false otherwise. +function resizeImage($filepath) +{ + if (!function_exists('imagecreatefromjpeg')) return false; // GD not present: no thumbnail possible. + + // Trick: some stupid people rename GIF as JPEG... or else. + // So we really try to open each image type whatever the extension is. + $header=file_get_contents($filepath,false,NULL,0,256); // Read first 256 bytes and try to sniff file type. + $im=false; + if (strpos($header,'GIF8')==0) $im = imagecreatefromgif($filepath); // Well this is crude, but it should be enough. + if (strpos($header,'PNG')==1) $im = imagecreatefrompng($filepath); + if (strpos($header,'JFIF')) $im = imagecreatefromjpeg($filepath); + if (!$im) return false; // Unable to open image (corrupted or not an image) + $w = imagesx($im); + $h = imagesy($im); + $nw = 120; // Desired width + $nh = floor(($h*$nw)/$w); // Compute new width/height while keeping ratio + // Resize image: + $im2 = imagecreatetruecolor($nw,$nh); + imagecopyresampled($im2, $im, 0, 0, 0, 0, $nw, $nh, $w, $h); + imageinterlace($im2,true); // For progressive JPEG. + $tempname=$filepath.'_TEMP.jpg'; + imagejpeg($im2, $tempname, 90); + imagedestroy($im); + imagedestroy($im2); + rename($tempname,$filepath); // Overwrite original picture with thumbnail. + return true; +} + // Invalidate caches when the database is changed or the user logs out. // (eg. tags cache). function invalidateCaches() diff --git a/shaarli.css b/shaarli.css index 3136755b..a3b9434b 100644 --- a/shaarli.css +++ b/shaarli.css @@ -37,7 +37,7 @@ margin: auto; #pageheader a:visited { color:#bbb; text-decoration:none;} #pageheader a:hover { color:#FFFFC9; text-decoration:none;} #pageheader a:active { color:#bbb; text-decoration:none;} -.paging { background-color:#777; color:#ccc; text-align:center; padding:0 0 3 0;} +.paging { background-color:#777; color:#ccc; text-align:center; padding:0 0 3 0; clear:both;} .paging a:link { color:#ccc; text-decoration:none;} .paging a:visited { color:#ccc; } .paging a:hover { color:#FFFFC9; } @@ -51,11 +51,16 @@ margin: auto; .linktitle a:hover { text-decoration: underline; } .linktitle a:visited { color:#0000BB; } .linkdate { font-size:8pt; color:#888; } +.linkdate a { text-decoration: none; color:#888; } +.linkdate a:hover { text-decoration: underline; } .linkurl { font-size:8pt; color:#4BAA74; } .linkdescription { color:#000; margin-top:0px; margin-bottom:0px; font-weight:normal; } .linktag { font-size:9pt; color:#777; background-color:#ddd; padding:0 6 0 6; -moz-box-shadow: inset 2px 2px 3px #ffffff; -webkit-box-shadow: inset 2px 2px 3px #ffffff; box-shadow: inset 2px 2px 3px ffffff; border-bottom:1px solid #aaa; border-right:1px solid #aaa; } .linktag a { color:#777; text-decoration:none; } +.linkshort { font-size:8pt; color:#888; } +.linkshort a { text-decoration: none; color:#393964; } +.linkshort a:hover { text-decoration: underline; } .buttoneditform { display:inline; } #footer { font-size:8pt; text-align:center; border-top:1px solid #ddd; color: #888; } #newversion { background-color: #FFFFA0; color:#000; position:absolute; top:0;right:0; padding:2 7 2 7; font-size:9pt;} -- 2.41.0