diff options
-rw-r--r-- | application/PageBuilder.php | 5 | ||||
-rw-r--r-- | application/Thumbnailer.php | 49 | ||||
-rw-r--r-- | application/config/ConfigManager.php | 4 | ||||
-rw-r--r-- | assets/vintage/css/shaarli.css | 8 | ||||
-rw-r--r-- | composer.json | 1 | ||||
-rw-r--r-- | inc/web-thumbnailer.json | 9 | ||||
-rw-r--r-- | index.php | 490 | ||||
-rw-r--r-- | tests/ThumbnailerTest.php | 51 | ||||
-rw-r--r-- | tests/utils/config/configJson.json.php | 3 | ||||
-rw-r--r-- | tpl/vintage/configure.html | 12 | ||||
-rw-r--r-- | tpl/vintage/linklist.html | 11 | ||||
-rw-r--r-- | tpl/vintage/picwall.html | 6 |
12 files changed, 222 insertions, 427 deletions
diff --git a/application/PageBuilder.php b/application/PageBuilder.php index a4483870..3dba7677 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php | |||
@@ -105,6 +105,11 @@ class PageBuilder | |||
105 | if ($this->linkDB !== null) { | 105 | if ($this->linkDB !== null) { |
106 | $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); | 106 | $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); |
107 | } | 107 | } |
108 | |||
109 | $this->tpl->assign('thumbnails_enabled', $this->conf->get('thumbnails.enabled')); | ||
110 | $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); | ||
111 | $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); | ||
112 | |||
108 | // To be removed with a proper theme configuration. | 113 | // To be removed with a proper theme configuration. |
109 | $this->tpl->assign('conf', $this->conf); | 114 | $this->tpl->assign('conf', $this->conf); |
110 | } | 115 | } |
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php new file mode 100644 index 00000000..b669adae --- /dev/null +++ b/application/Thumbnailer.php | |||
@@ -0,0 +1,49 @@ | |||
1 | <?php | ||
2 | |||
3 | use WebThumbnailer\WebThumbnailer; | ||
4 | |||
5 | /** | ||
6 | * Class Thumbnailer | ||
7 | * | ||
8 | * Utility class used to retrieve thumbnails using web-thumbnailer dependency. | ||
9 | */ | ||
10 | class Thumbnailer | ||
11 | { | ||
12 | /** | ||
13 | * @var WebThumbnailer instance. | ||
14 | */ | ||
15 | protected $wt; | ||
16 | |||
17 | /** | ||
18 | * @var ConfigManager instance. | ||
19 | */ | ||
20 | protected $conf; | ||
21 | |||
22 | /** | ||
23 | * Thumbnailer constructor. | ||
24 | * | ||
25 | * @param ConfigManager $conf instance. | ||
26 | */ | ||
27 | public function __construct($conf) | ||
28 | { | ||
29 | $this->conf = $conf; | ||
30 | $this->wt = new WebThumbnailer(); | ||
31 | \WebThumbnailer\Application\ConfigManager::addFile('inc/web-thumbnailer.json'); | ||
32 | $this->wt->maxWidth($this->conf->get('thumbnails.width')) | ||
33 | ->maxHeight($this->conf->get('thumbnails.height')) | ||
34 | ->crop(true) | ||
35 | ->debug($this->conf->get('dev.debug', false)); | ||
36 | } | ||
37 | |||
38 | /** | ||
39 | * Retrieve a thumbnail for given URL | ||
40 | * | ||
41 | * @param string $url where to look for a thumbnail. | ||
42 | * | ||
43 | * @return bool|string The thumbnail relative cache file path, or false if none has been found. | ||
44 | */ | ||
45 | public function get($url) | ||
46 | { | ||
47 | return $this->wt->thumbnail($url); | ||
48 | } | ||
49 | } | ||
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index 82f4a368..124fedc2 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php | |||
@@ -319,6 +319,10 @@ class ConfigManager | |||
319 | $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); | 319 | $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); |
320 | $this->setEmpty('general.default_note_title', 'Note: '); | 320 | $this->setEmpty('general.default_note_title', 'Note: '); |
321 | 321 | ||
322 | $this->setEmpty('thumbnails.enabled', true); | ||
323 | $this->setEmpty('thumbnails.width', 120); | ||
324 | $this->setEmpty('thumbnails.height', 120); | ||
325 | |||
322 | $this->setEmpty('updates.check_updates', false); | 326 | $this->setEmpty('updates.check_updates', false); |
323 | $this->setEmpty('updates.check_updates_branch', 'stable'); | 327 | $this->setEmpty('updates.check_updates_branch', 'stable'); |
324 | $this->setEmpty('updates.check_updates_interval', 86400); | 328 | $this->setEmpty('updates.check_updates_interval', 86400); |
diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css index c919339b..05e2c17e 100644 --- a/assets/vintage/css/shaarli.css +++ b/assets/vintage/css/shaarli.css | |||
@@ -701,8 +701,8 @@ a.bigbutton, #pageheader a.bigbutton { | |||
701 | position: relative; | 701 | position: relative; |
702 | display: table-cell; | 702 | display: table-cell; |
703 | vertical-align: middle; | 703 | vertical-align: middle; |
704 | width: 90px; | 704 | width: 120px; |
705 | height: 90px; | 705 | height: 120px; |
706 | overflow: hidden; | 706 | overflow: hidden; |
707 | text-align: center; | 707 | text-align: center; |
708 | float: left; | 708 | float: left; |
@@ -739,9 +739,9 @@ a.bigbutton, #pageheader a.bigbutton { | |||
739 | position: absolute; | 739 | position: absolute; |
740 | top: 0; | 740 | top: 0; |
741 | left: 0; | 741 | left: 0; |
742 | width: 90px; | 742 | width: 120px; |
743 | font-weight: bold; | 743 | font-weight: bold; |
744 | font-size: 8pt; | 744 | font-size: 9pt; |
745 | color: #fff; | 745 | color: #fff; |
746 | text-align: left; | 746 | text-align: left; |
747 | background-color: transparent; | 747 | background-color: transparent; |
diff --git a/composer.json b/composer.json index 0d4c623c..983bf9e9 100644 --- a/composer.json +++ b/composer.json | |||
@@ -19,6 +19,7 @@ | |||
19 | "shaarli/netscape-bookmark-parser": "^2.0", | 19 | "shaarli/netscape-bookmark-parser": "^2.0", |
20 | "erusev/parsedown": "^1.6", | 20 | "erusev/parsedown": "^1.6", |
21 | "slim/slim": "^3.0", | 21 | "slim/slim": "^3.0", |
22 | "arthurhoaro/web-thumbnailer": "dev-master", | ||
22 | "pubsubhubbub/publisher": "dev-master", | 23 | "pubsubhubbub/publisher": "dev-master", |
23 | "gettext/gettext": "^4.4" | 24 | "gettext/gettext": "^4.4" |
24 | }, | 25 | }, |
diff --git a/inc/web-thumbnailer.json b/inc/web-thumbnailer.json new file mode 100644 index 00000000..8a19f070 --- /dev/null +++ b/inc/web-thumbnailer.json | |||
@@ -0,0 +1,9 @@ | |||
1 | { | ||
2 | "settings": { | ||
3 | "default": { | ||
4 | "_comment": "infinite cache", | ||
5 | "cache_duration": -1, | ||
6 | "timeout": 60 | ||
7 | } | ||
8 | } | ||
9 | } \ No newline at end of file | ||
@@ -74,6 +74,7 @@ require_once 'application/Url.php'; | |||
74 | require_once 'application/Utils.php'; | 74 | require_once 'application/Utils.php'; |
75 | require_once 'application/PluginManager.php'; | 75 | require_once 'application/PluginManager.php'; |
76 | require_once 'application/Router.php'; | 76 | require_once 'application/Router.php'; |
77 | require_once 'application/Thumbnailer.php'; | ||
77 | require_once 'application/Updater.php'; | 78 | require_once 'application/Updater.php'; |
78 | use \Shaarli\Languages; | 79 | use \Shaarli\Languages; |
79 | use \Shaarli\ThemeUtils; | 80 | use \Shaarli\ThemeUtils; |
@@ -601,22 +602,54 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
601 | // -------- Picture wall | 602 | // -------- Picture wall |
602 | if ($targetPage == Router::$PAGE_PICWALL) | 603 | if ($targetPage == Router::$PAGE_PICWALL) |
603 | { | 604 | { |
605 | if (! $conf->get('thumbnails.enabled')) { | ||
606 | header('Location: ?'); | ||
607 | exit; | ||
608 | } | ||
609 | |||
604 | // Optionally filter the results: | 610 | // Optionally filter the results: |
605 | $links = $LINKSDB->filterSearch($_GET); | 611 | $links = $LINKSDB->filterSearch($_GET); |
606 | $linksToDisplay = array(); | 612 | $linksToDisplay = array(); |
607 | 613 | ||
614 | $thumbnailer = new Thumbnailer($conf); | ||
615 | |||
616 | |||
617 | $cpt = 0; | ||
608 | // Get only links which have a thumbnail. | 618 | // Get only links which have a thumbnail. |
609 | foreach($links as $link) | 619 | foreach($links as $link) |
610 | { | 620 | { |
611 | $permalink='?'.$link['shorturl']; | 621 | $permalink='?'.$link['shorturl']; |
612 | $thumb=lazyThumbnail($conf, $link['url'],$permalink); | 622 | // Not a note, |
613 | if ($thumb!='') // Only output links which have a thumbnail. | 623 | // and (never retrieved yet or no valid cache file) |
614 | { | 624 | if ($link['url'][0] != '?' |
615 | $link['thumbnail']=$thumb; // Thumbnail HTML code. | 625 | && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail']))) |
616 | $linksToDisplay[]=$link; // Add to array. | 626 | ) { |
627 | $link['thumbnail'] = $thumbnailer->get($link['url']); | ||
628 | // FIXME! we really need to get rid of ArrayAccess... | ||
629 | $item = $LINKSDB[$link['linkdate']]; | ||
630 | $item['thumbnail'] = $link['thumbnail']; | ||
631 | $LINKSDB[$link['linkdate']] = $item; | ||
632 | $updateDB = true; | ||
633 | $cpt++; | ||
634 | } | ||
635 | |||
636 | if (isset($link['thumbnail']) && $link['thumbnail'] !== false) { | ||
637 | $linksToDisplay[] = $link; // Add to array. | ||
638 | } | ||
639 | |||
640 | // If we retrieved new thumbnails, we update the database every 20 links. | ||
641 | // Downloading everything the first time may take a very long time | ||
642 | if (!empty($updateDB) && $cpt == 20) { | ||
643 | $LINKSDB->save($conf->get('resource.page_cache')); | ||
644 | $updateDB = false; | ||
645 | $cpt = 0; | ||
617 | } | 646 | } |
618 | } | 647 | } |
619 | 648 | ||
649 | if (!empty($updateDB)) { | ||
650 | $LINKSDB->save($conf->get('resource.page_cache')); | ||
651 | } | ||
652 | |||
620 | $data = array( | 653 | $data = array( |
621 | 'linksToDisplay' => $linksToDisplay, | 654 | 'linksToDisplay' => $linksToDisplay, |
622 | ); | 655 | ); |
@@ -1008,6 +1041,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
1008 | $conf->set('api.enabled', !empty($_POST['enableApi'])); | 1041 | $conf->set('api.enabled', !empty($_POST['enableApi'])); |
1009 | $conf->set('api.secret', escape($_POST['apiSecret'])); | 1042 | $conf->set('api.secret', escape($_POST['apiSecret'])); |
1010 | $conf->set('translation.language', escape($_POST['language'])); | 1043 | $conf->set('translation.language', escape($_POST['language'])); |
1044 | $conf->set('thumbnails.enabled', !empty($_POST['enableThumbnails'])); | ||
1011 | 1045 | ||
1012 | try { | 1046 | try { |
1013 | $conf->write($loginManager->isLoggedIn()); | 1047 | $conf->write($loginManager->isLoggedIn()); |
@@ -1148,6 +1182,11 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
1148 | $link['title'] = $link['url']; | 1182 | $link['title'] = $link['url']; |
1149 | } | 1183 | } |
1150 | 1184 | ||
1185 | if ($conf->get('thumbnails.enabled')) { | ||
1186 | $thumbnailer = new Thumbnailer($conf); | ||
1187 | $link['thumbnail'] = $thumbnailer->get($url); | ||
1188 | } | ||
1189 | |||
1151 | $pluginManager->executeHooks('save_link', $link); | 1190 | $pluginManager->executeHooks('save_link', $link); |
1152 | 1191 | ||
1153 | $LINKSDB[$id] = $link; | 1192 | $LINKSDB[$id] = $link; |
@@ -1549,6 +1588,11 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) | |||
1549 | // Start index. | 1588 | // Start index. |
1550 | $i = ($page-1) * $_SESSION['LINKS_PER_PAGE']; | 1589 | $i = ($page-1) * $_SESSION['LINKS_PER_PAGE']; |
1551 | $end = $i + $_SESSION['LINKS_PER_PAGE']; | 1590 | $end = $i + $_SESSION['LINKS_PER_PAGE']; |
1591 | |||
1592 | if ($conf->get('thumbnails.enabled')) { | ||
1593 | $thumbnailer = new Thumbnailer($conf); | ||
1594 | } | ||
1595 | |||
1552 | $linkDisp = array(); | 1596 | $linkDisp = array(); |
1553 | while ($i<$end && $i<count($keys)) | 1597 | while ($i<$end && $i<count($keys)) |
1554 | { | 1598 | { |
@@ -1569,9 +1613,22 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) | |||
1569 | $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY); | 1613 | $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY); |
1570 | uasort($taglist, 'strcasecmp'); | 1614 | uasort($taglist, 'strcasecmp'); |
1571 | $link['taglist'] = $taglist; | 1615 | $link['taglist'] = $taglist; |
1616 | |||
1617 | // Thumbnails enabled, not a note, | ||
1618 | // and (never retrieved yet or no valid cache file) | ||
1619 | if ($conf->get('thumbnails.enabled') && $link['url'][0] != '?' | ||
1620 | && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail']))) | ||
1621 | ) { | ||
1622 | $link['thumbnail'] = $thumbnailer->get($link['url']); | ||
1623 | // FIXME! we really need to get rid of ArrayAccess... | ||
1624 | $item = $LINKSDB[$keys[$i]]; | ||
1625 | $item['thumbnail'] = $link['thumbnail']; | ||
1626 | $LINKSDB[$keys[$i]] = $item; | ||
1627 | $updateDB = true; | ||
1628 | } | ||
1629 | |||
1572 | // Check for both signs of a note: starting with ? and 7 chars long. | 1630 | // Check for both signs of a note: starting with ? and 7 chars long. |
1573 | if ($link['url'][0] === '?' && | 1631 | if ($link['url'][0] === '?' && strlen($link['url']) === 7) { |
1574 | strlen($link['url']) === 7) { | ||
1575 | $link['url'] = index_url($_SERVER) . $link['url']; | 1632 | $link['url'] = index_url($_SERVER) . $link['url']; |
1576 | } | 1633 | } |
1577 | 1634 | ||
@@ -1579,6 +1636,11 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) | |||
1579 | $i++; | 1636 | $i++; |
1580 | } | 1637 | } |
1581 | 1638 | ||
1639 | // If we retrieved new thumbnails, we update the database. | ||
1640 | if (!empty($updateDB)) { | ||
1641 | $LINKSDB->save($conf->get('resource.page_cache')); | ||
1642 | } | ||
1643 | |||
1582 | // Compute paging navigation | 1644 | // Compute paging navigation |
1583 | $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); | 1645 | $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); |
1584 | $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); | 1646 | $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); |
@@ -1630,194 +1692,6 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) | |||
1630 | } | 1692 | } |
1631 | 1693 | ||
1632 | /** | 1694 | /** |
1633 | * Compute the thumbnail for a link. | ||
1634 | * | ||
1635 | * With a link to the original URL. | ||
1636 | * Understands various services (youtube.com...) | ||
1637 | * Input: $url = URL for which the thumbnail must be found. | ||
1638 | * $href = if provided, this URL will be followed instead of $url | ||
1639 | * Returns an associative array with thumbnail attributes (src,href,width,height,style,alt) | ||
1640 | * Some of them may be missing. | ||
1641 | * Return an empty array if no thumbnail available. | ||
1642 | * | ||
1643 | * @param ConfigManager $conf Configuration Manager instance. | ||
1644 | * @param string $url | ||
1645 | * @param string|bool $href | ||
1646 | * | ||
1647 | * @return array | ||
1648 | */ | ||
1649 | function computeThumbnail($conf, $url, $href = false) | ||
1650 | { | ||
1651 | if (!$conf->get('thumbnail.enable_thumbnails')) return array(); | ||
1652 | if ($href==false) $href=$url; | ||
1653 | |||
1654 | // For most hosts, the URL of the thumbnail can be easily deduced from the URL of the link. | ||
1655 | // (e.g. http://www.youtube.com/watch?v=spVypYk4kto ---> http://img.youtube.com/vi/spVypYk4kto/default.jpg ) | ||
1656 | // ^^^^^^^^^^^ ^^^^^^^^^^^ | ||
1657 | $domain = parse_url($url,PHP_URL_HOST); | ||
1658 | if ($domain=='youtube.com' || $domain=='www.youtube.com') | ||
1659 | { | ||
1660 | parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract video ID and get thumbnail | ||
1661 | if (!empty($params['v'])) return array('src'=>'https://img.youtube.com/vi/'.$params['v'].'/default.jpg', | ||
1662 | 'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail'); | ||
1663 | } | ||
1664 | if ($domain=='youtu.be') // Youtube short links | ||
1665 | { | ||
1666 | $path = parse_url($url,PHP_URL_PATH); | ||
1667 | return array('src'=>'https://img.youtube.com/vi'.$path.'/default.jpg', | ||
1668 | 'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail'); | ||
1669 | } | ||
1670 | if ($domain=='pix.toile-libre.org') // pix.toile-libre.org image hosting | ||
1671 | { | ||
1672 | parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract image filename. | ||
1673 | if (!empty($params) && !empty($params['img'])) return array('src'=>'http://pix.toile-libre.org/upload/thumb/'.urlencode($params['img']), | ||
1674 | 'href'=>$href,'style'=>'max-width:120px; max-height:150px','alt'=>'pix.toile-libre.org thumbnail'); | ||
1675 | } | ||
1676 | |||
1677 | if ($domain=='imgur.com') | ||
1678 | { | ||
1679 | $path = parse_url($url,PHP_URL_PATH); | ||
1680 | if (startsWith($path,'/a/')) return array(); // Thumbnails for albums are not available. | ||
1681 | if (startsWith($path,'/r/')) return array('src'=>'https://i.imgur.com/'.basename($path).'s.jpg', | ||
1682 | 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail'); | ||
1683 | if (startsWith($path,'/gallery/')) return array('src'=>'https://i.imgur.com'.substr($path,8).'s.jpg', | ||
1684 | 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail'); | ||
1685 | |||
1686 | if (substr_count($path,'/')==1) return array('src'=>'https://i.imgur.com/'.substr($path,1).'s.jpg', | ||
1687 | 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail'); | ||
1688 | } | ||
1689 | if ($domain=='i.imgur.com') | ||
1690 | { | ||
1691 | $pi = pathinfo(parse_url($url,PHP_URL_PATH)); | ||
1692 | if (!empty($pi['filename'])) return array('src'=>'https://i.imgur.com/'.$pi['filename'].'s.jpg', | ||
1693 | 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail'); | ||
1694 | } | ||
1695 | if ($domain=='dailymotion.com' || $domain=='www.dailymotion.com') | ||
1696 | { | ||
1697 | if (strpos($url,'dailymotion.com/video/')!==false) | ||
1698 | { | ||
1699 | $thumburl=str_replace('dailymotion.com/video/','dailymotion.com/thumbnail/video/',$url); | ||
1700 | return array('src'=>$thumburl, | ||
1701 | 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'DailyMotion thumbnail'); | ||
1702 | } | ||
1703 | } | ||
1704 | if (endsWith($domain,'.imageshack.us')) | ||
1705 | { | ||
1706 | $ext=strtolower(pathinfo($url,PATHINFO_EXTENSION)); | ||
1707 | if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif') | ||
1708 | { | ||
1709 | $thumburl = substr($url,0,strlen($url)-strlen($ext)).'th.'.$ext; | ||
1710 | return array('src'=>$thumburl, | ||
1711 | 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'imageshack.us thumbnail'); | ||
1712 | } | ||
1713 | } | ||
1714 | |||
1715 | // Some other hosts are SLOW AS HELL and usually require an extra HTTP request to get the thumbnail URL. | ||
1716 | // So we deport the thumbnail generation in order not to slow down page generation | ||
1717 | // (and we also cache the thumbnail) | ||
1718 | |||
1719 | if (! $conf->get('thumbnail.enable_localcache')) return array(); // If local cache is disabled, no thumbnails for services which require the use a local cache. | ||
1720 | |||
1721 | if ($domain=='flickr.com' || endsWith($domain,'.flickr.com') | ||
1722 | || $domain=='vimeo.com' | ||
1723 | || $domain=='ted.com' || endsWith($domain,'.ted.com') | ||
1724 | || $domain=='xkcd.com' || endsWith($domain,'.xkcd.com') | ||
1725 | ) | ||
1726 | { | ||
1727 | if ($domain=='vimeo.com') | ||
1728 | { // Make sure this vimeo URL points to a video (/xxx... where xxx is numeric) | ||
1729 | $path = parse_url($url,PHP_URL_PATH); | ||
1730 | if (!preg_match('!/\d+.+?!',$path)) return array(); // This is not a single video URL. | ||
1731 | } | ||
1732 | if ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com')) | ||
1733 | { // Make sure this URL points to a single comic (/xxx... where xxx is numeric) | ||
1734 | $path = parse_url($url,PHP_URL_PATH); | ||
1735 | if (!preg_match('!/\d+.+?!',$path)) return array(); | ||
1736 | } | ||
1737 | if ($domain=='ted.com' || endsWith($domain,'.ted.com')) | ||
1738 | { // Make sure this TED URL points to a video (/talks/...) | ||
1739 | $path = parse_url($url,PHP_URL_PATH); | ||
1740 | if ("/talks/" !== substr($path,0,7)) return array(); // This is not a single video URL. | ||
1741 | } | ||
1742 | $sign = hash_hmac('sha256', $url, $conf->get('credentials.salt')); // We use the salt to sign data (it's random, secret, and specific to each installation) | ||
1743 | return array('src'=>index_url($_SERVER).'?do=genthumbnail&hmac='.$sign.'&url='.urlencode($url), | ||
1744 | 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail'); | ||
1745 | } | ||
1746 | |||
1747 | // For all other, we try to make a thumbnail of links ending with .jpg/jpeg/png/gif | ||
1748 | // Technically speaking, we should download ALL links and check their Content-Type to see if they are images. | ||
1749 | // But using the extension will do. | ||
1750 | $ext=strtolower(pathinfo($url,PATHINFO_EXTENSION)); | ||
1751 | if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif') | ||
1752 | { | ||
1753 | $sign = hash_hmac('sha256', $url, $conf->get('credentials.salt')); // We use the salt to sign data (it's random, secret, and specific to each installation) | ||
1754 | return array('src'=>index_url($_SERVER).'?do=genthumbnail&hmac='.$sign.'&url='.urlencode($url), | ||
1755 | 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail'); | ||
1756 | } | ||
1757 | return array(); // No thumbnail. | ||
1758 | |||
1759 | } | ||
1760 | |||
1761 | |||
1762 | // Returns the HTML code to display a thumbnail for a link | ||
1763 | // with a link to the original URL. | ||
1764 | // Understands various services (youtube.com...) | ||
1765 | // Input: $url = URL for which the thumbnail must be found. | ||
1766 | // $href = if provided, this URL will be followed instead of $url | ||
1767 | // Returns '' if no thumbnail available. | ||
1768 | function thumbnail($url,$href=false) | ||
1769 | { | ||
1770 | // FIXME! | ||
1771 | global $conf; | ||
1772 | $t = computeThumbnail($conf, $url,$href); | ||
1773 | if (count($t)==0) return ''; // Empty array = no thumbnail for this URL. | ||
1774 | |||
1775 | $html='<a href="'.escape($t['href']).'"><img src="'.escape($t['src']).'"'; | ||
1776 | if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"'; | ||
1777 | if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"'; | ||
1778 | if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"'; | ||
1779 | if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"'; | ||
1780 | $html.='></a>'; | ||
1781 | return $html; | ||
1782 | } | ||
1783 | |||
1784 | // Returns the HTML code to display a thumbnail for a link | ||
1785 | // for the picture wall (using lazy image loading) | ||
1786 | // Understands various services (youtube.com...) | ||
1787 | // Input: $url = URL for which the thumbnail must be found. | ||
1788 | // $href = if provided, this URL will be followed instead of $url | ||
1789 | // Returns '' if no thumbnail available. | ||
1790 | function lazyThumbnail($conf, $url,$href=false) | ||
1791 | { | ||
1792 | // FIXME! | ||
1793 | global $conf; | ||
1794 | $t = computeThumbnail($conf, $url,$href); | ||
1795 | if (count($t)==0) return ''; // Empty array = no thumbnail for this URL. | ||
1796 | |||
1797 | $html='<a href="'.escape($t['href']).'">'; | ||
1798 | |||
1799 | // Lazy image | ||
1800 | $html.='<img class="b-lazy" src="#" data-src="'.escape($t['src']).'"'; | ||
1801 | |||
1802 | if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"'; | ||
1803 | if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"'; | ||
1804 | if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"'; | ||
1805 | if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"'; | ||
1806 | $html.='>'; | ||
1807 | |||
1808 | // No-JavaScript fallback. | ||
1809 | $html.='<noscript><img src="'.escape($t['src']).'"'; | ||
1810 | if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"'; | ||
1811 | if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"'; | ||
1812 | if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"'; | ||
1813 | if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"'; | ||
1814 | $html.='></noscript></a>'; | ||
1815 | |||
1816 | return $html; | ||
1817 | } | ||
1818 | |||
1819 | |||
1820 | /** | ||
1821 | * Installation | 1695 | * Installation |
1822 | * This function should NEVER be called if the file data/config.php exists. | 1696 | * This function should NEVER be called if the file data/config.php exists. |
1823 | * | 1697 | * |
@@ -1917,232 +1791,6 @@ function install($conf, $sessionManager, $loginManager) { | |||
1917 | exit; | 1791 | exit; |
1918 | } | 1792 | } |
1919 | 1793 | ||
1920 | /** | ||
1921 | * Because some f*cking services like flickr require an extra HTTP request to get the thumbnail URL, | ||
1922 | * I have deported the thumbnail URL code generation here, otherwise this would slow down page generation. | ||
1923 | * The following function takes the URL a link (e.g. a flickr page) and return the proper thumbnail. | ||
1924 | * This function is called by passing the URL: | ||
1925 | * http://mywebsite.com/shaarli/?do=genthumbnail&hmac=[HMAC]&url=[URL] | ||
1926 | * [URL] is the URL of the link (e.g. a flickr page) | ||
1927 | * [HMAC] is the signature for the [URL] (so that these URL cannot be forged). | ||
1928 | * The function below will fetch the image from the webservice and store it in the cache. | ||
1929 | * | ||
1930 | * @param ConfigManager $conf Configuration Manager instance, | ||
1931 | */ | ||
1932 | function genThumbnail($conf) | ||
1933 | { | ||
1934 | // Make sure the parameters in the URL were generated by us. | ||
1935 | $sign = hash_hmac('sha256', $_GET['url'], $conf->get('credentials.salt')); | ||
1936 | if ($sign!=$_GET['hmac']) die('Naughty boy!'); | ||
1937 | |||
1938 | $cacheDir = $conf->get('resource.thumbnails_cache', 'cache'); | ||
1939 | // Let's see if we don't already have the image for this URL in the cache. | ||
1940 | $thumbname=hash('sha1',$_GET['url']).'.jpg'; | ||
1941 | if (is_file($cacheDir .'/'. $thumbname)) | ||
1942 | { // We have the thumbnail, just serve it: | ||
1943 | header('Content-Type: image/jpeg'); | ||
1944 | echo file_get_contents($cacheDir .'/'. $thumbname); | ||
1945 | return; | ||
1946 | } | ||
1947 | // We may also serve a blank image (if service did not respond) | ||
1948 | $blankname=hash('sha1',$_GET['url']).'.gif'; | ||
1949 | if (is_file($cacheDir .'/'. $blankname)) | ||
1950 | { | ||
1951 | header('Content-Type: image/gif'); | ||
1952 | echo file_get_contents($cacheDir .'/'. $blankname); | ||
1953 | return; | ||
1954 | } | ||
1955 | |||
1956 | // Otherwise, generate the thumbnail. | ||
1957 | $url = $_GET['url']; | ||
1958 | $domain = parse_url($url,PHP_URL_HOST); | ||
1959 | |||
1960 | if ($domain=='flickr.com' || endsWith($domain,'.flickr.com')) | ||
1961 | { | ||
1962 | // Crude replacement to handle new flickr domain policy (They prefer www. now) | ||
1963 | $url = str_replace('http://flickr.com/','http://www.flickr.com/',$url); | ||
1964 | |||
1965 | // Is this a link to an image, or to a flickr page ? | ||
1966 | $imageurl=''; | ||
1967 | if (endsWith(parse_url($url, PHP_URL_PATH), '.jpg')) | ||
1968 | { // This is a direct link to an image. e.g. http://farm1.staticflickr.com/5/5921913_ac83ed27bd_o.jpg | ||
1969 | preg_match('!(http://farm\d+\.staticflickr\.com/\d+/\d+_\w+_)\w.jpg!',$url,$matches); | ||
1970 | if (!empty($matches[1])) $imageurl=$matches[1].'m.jpg'; | ||
1971 | } | ||
1972 | else // This is a flickr page (html) | ||
1973 | { | ||
1974 | // Get the flickr html page. | ||
1975 | list($headers, $content) = get_http_response($url, 20); | ||
1976 | if (strpos($headers[0], '200 OK') !== false) | ||
1977 | { | ||
1978 | // flickr now nicely provides the URL of the thumbnail in each flickr page. | ||
1979 | preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!', $content, $matches); | ||
1980 | if (!empty($matches[1])) $imageurl=$matches[1]; | ||
1981 | |||
1982 | // In albums (and some other pages), the link rel="image_src" is not provided, | ||
1983 | // but flickr provides: | ||
1984 | // <meta property="og:image" content="http://farm4.staticflickr.com/3398/3239339068_25d13535ff_z.jpg" /> | ||
1985 | if ($imageurl=='') | ||
1986 | { | ||
1987 | preg_match('!<meta property=\"og:image\" content=\"(.+?)\"!', $content, $matches); | ||
1988 | if (!empty($matches[1])) $imageurl=$matches[1]; | ||
1989 | } | ||
1990 | } | ||
1991 | } | ||
1992 | |||
1993 | if ($imageurl!='') | ||
1994 | { // Let's download the image. | ||
1995 | // Image is 240x120, so 10 seconds to download should be enough. | ||
1996 | list($headers, $content) = get_http_response($imageurl, 10); | ||
1997 | if (strpos($headers[0], '200 OK') !== false) { | ||
1998 | // Save image to cache. | ||
1999 | file_put_contents($cacheDir .'/'. $thumbname, $content); | ||
2000 | header('Content-Type: image/jpeg'); | ||
2001 | echo $content; | ||
2002 | return; | ||
2003 | } | ||
2004 | } | ||
2005 | } | ||
2006 | |||
2007 | elseif ($domain=='vimeo.com' ) | ||
2008 | { | ||
2009 | // This is more complex: we have to perform a HTTP request, then parse the result. | ||
2010 | // Maybe we should deport this to JavaScript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098 | ||
2011 | $vid = substr(parse_url($url,PHP_URL_PATH),1); | ||
2012 | list($headers, $content) = get_http_response('https://vimeo.com/api/v2/video/'.escape($vid).'.php', 5); | ||
2013 | if (strpos($headers[0], '200 OK') !== false) { | ||
2014 | $t = unserialize($content); | ||
2015 | $imageurl = $t[0]['thumbnail_medium']; | ||
2016 | // Then we download the image and serve it to our client. | ||
2017 | list($headers, $content) = get_http_response($imageurl, 10); | ||
2018 | if (strpos($headers[0], '200 OK') !== false) { | ||
2019 | // Save image to cache. | ||
2020 | file_put_contents($cacheDir .'/'. $thumbname, $content); | ||
2021 | header('Content-Type: image/jpeg'); | ||
2022 | echo $content; | ||
2023 | return; | ||
2024 | } | ||
2025 | } | ||
2026 | } | ||
2027 | |||
2028 | elseif ($domain=='ted.com' || endsWith($domain,'.ted.com')) | ||
2029 | { | ||
2030 | // The thumbnail for TED talks is located in the <link rel="image_src" [...]> tag on that page | ||
2031 | // http://www.ted.com/talks/mikko_hypponen_fighting_viruses_defending_the_net.html | ||
2032 | // <link rel="image_src" href="http://images.ted.com/images/ted/28bced335898ba54d4441809c5b1112ffaf36781_389x292.jpg" /> | ||
2033 | list($headers, $content) = get_http_response($url, 5); | ||
2034 | if (strpos($headers[0], '200 OK') !== false) { | ||
2035 | // Extract the link to the thumbnail | ||
2036 | preg_match('!link rel="image_src" href="(http://images.ted.com/images/ted/.+_\d+x\d+\.jpg)"!', $content, $matches); | ||
2037 | if (!empty($matches[1])) | ||
2038 | { // Let's download the image. | ||
2039 | $imageurl=$matches[1]; | ||
2040 | // No control on image size, so wait long enough | ||
2041 | list($headers, $content) = get_http_response($imageurl, 20); | ||
2042 | if (strpos($headers[0], '200 OK') !== false) { | ||
2043 | $filepath = $cacheDir .'/'. $thumbname; | ||
2044 | file_put_contents($filepath, $content); // Save image to cache. | ||
2045 | if (resizeImage($filepath)) | ||
2046 | { | ||
2047 | header('Content-Type: image/jpeg'); | ||
2048 | echo file_get_contents($filepath); | ||
2049 | return; | ||
2050 | } | ||
2051 | } | ||
2052 | } | ||
2053 | } | ||
2054 | } | ||
2055 | |||
2056 | elseif ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com')) | ||
2057 | { | ||
2058 | // There is no thumbnail available for xkcd comics, so download the whole image and resize it. | ||
2059 | // http://xkcd.com/327/ | ||
2060 | // <img src="http://imgs.xkcd.com/comics/exploits_of_a_mom.png" title="<BLABLA>" alt="<BLABLA>" /> | ||
2061 | list($headers, $content) = get_http_response($url, 5); | ||
2062 | if (strpos($headers[0], '200 OK') !== false) { | ||
2063 | // Extract the link to the thumbnail | ||
2064 | preg_match('!<img src="(http://imgs.xkcd.com/comics/.*)" title="[^s]!', $content, $matches); | ||
2065 | if (!empty($matches[1])) | ||
2066 | { // Let's download the image. | ||
2067 | $imageurl=$matches[1]; | ||
2068 | // No control on image size, so wait long enough | ||
2069 | list($headers, $content) = get_http_response($imageurl, 20); | ||
2070 | if (strpos($headers[0], '200 OK') !== false) { | ||
2071 | $filepath = $cacheDir.'/'.$thumbname; | ||
2072 | // Save image to cache. | ||
2073 | file_put_contents($filepath, $content); | ||
2074 | if (resizeImage($filepath)) | ||
2075 | { | ||
2076 | header('Content-Type: image/jpeg'); | ||
2077 | echo file_get_contents($filepath); | ||
2078 | return; | ||
2079 | } | ||
2080 | } | ||
2081 | } | ||
2082 | } | ||
2083 | } | ||
2084 | |||
2085 | else | ||
2086 | { | ||
2087 | // For all other domains, we try to download the image and make a thumbnail. | ||
2088 | // We allow 30 seconds max to download (and downloads are limited to 4 Mb) | ||
2089 | list($headers, $content) = get_http_response($url, 30); | ||
2090 | if (strpos($headers[0], '200 OK') !== false) { | ||
2091 | $filepath = $cacheDir .'/'.$thumbname; | ||
2092 | // Save image to cache. | ||
2093 | file_put_contents($filepath, $content); | ||
2094 | if (resizeImage($filepath)) | ||
2095 | { | ||
2096 | header('Content-Type: image/jpeg'); | ||
2097 | echo file_get_contents($filepath); | ||
2098 | return; | ||
2099 | } | ||
2100 | } | ||
2101 | } | ||
2102 | |||
2103 | |||
2104 | // Otherwise, return an empty image (8x8 transparent gif) | ||
2105 | $blankgif = base64_decode('R0lGODlhCAAIAIAAAP///////yH5BAEKAAEALAAAAAAIAAgAAAIHjI+py+1dAAA7'); | ||
2106 | // Also put something in cache so that this URL is not requested twice. | ||
2107 | file_put_contents($cacheDir .'/'. $blankname, $blankgif); | ||
2108 | header('Content-Type: image/gif'); | ||
2109 | echo $blankgif; | ||
2110 | } | ||
2111 | |||
2112 | // Make a thumbnail of the image (to width: 120 pixels) | ||
2113 | // Returns true if success, false otherwise. | ||
2114 | function resizeImage($filepath) | ||
2115 | { | ||
2116 | if (!function_exists('imagecreatefromjpeg')) return false; // GD not present: no thumbnail possible. | ||
2117 | |||
2118 | // Trick: some stupid people rename GIF as JPEG... or else. | ||
2119 | // So we really try to open each image type whatever the extension is. | ||
2120 | $header=file_get_contents($filepath,false,NULL,0,256); // Read first 256 bytes and try to sniff file type. | ||
2121 | $im=false; | ||
2122 | $i=strpos($header,'GIF8'); if (($i!==false) && ($i==0)) $im = imagecreatefromgif($filepath); // Well this is crude, but it should be enough. | ||
2123 | $i=strpos($header,'PNG'); if (($i!==false) && ($i==1)) $im = imagecreatefrompng($filepath); | ||
2124 | $i=strpos($header,'JFIF'); if ($i!==false) $im = imagecreatefromjpeg($filepath); | ||
2125 | if (!$im) return false; // Unable to open image (corrupted or not an image) | ||
2126 | $w = imagesx($im); | ||
2127 | $h = imagesy($im); | ||
2128 | $ystart = 0; $yheight=$h; | ||
2129 | if ($h>$w) { $ystart= ($h/2)-($w/2); $yheight=$w/2; } | ||
2130 | $nw = 120; // Desired width | ||
2131 | $nh = min(floor(($h*$nw)/$w),120); // Compute new width/height, but maximum 120 pixels height. | ||
2132 | // Resize image: | ||
2133 | $im2 = imagecreatetruecolor($nw,$nh); | ||
2134 | imagecopyresampled($im2, $im, 0, 0, 0, $ystart, $nw, $nh, $w, $yheight); | ||
2135 | imageinterlace($im2,true); // For progressive JPEG. | ||
2136 | $tempname=$filepath.'_TEMP.jpg'; | ||
2137 | imagejpeg($im2, $tempname, 90); | ||
2138 | imagedestroy($im); | ||
2139 | imagedestroy($im2); | ||
2140 | unlink($filepath); | ||
2141 | rename($tempname,$filepath); // Overwrite original picture with thumbnail. | ||
2142 | return true; | ||
2143 | } | ||
2144 | |||
2145 | if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=genthumbnail')) { genThumbnail($conf); exit; } // Thumbnail generation/cache does not need the link database. | ||
2146 | if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) { showDailyRSS($conf); exit; } | 1794 | if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) { showDailyRSS($conf); exit; } |
2147 | if (!isset($_SESSION['LINKS_PER_PAGE'])) { | 1795 | if (!isset($_SESSION['LINKS_PER_PAGE'])) { |
2148 | $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); | 1796 | $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); |
diff --git a/tests/ThumbnailerTest.php b/tests/ThumbnailerTest.php new file mode 100644 index 00000000..db109321 --- /dev/null +++ b/tests/ThumbnailerTest.php | |||
@@ -0,0 +1,51 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'application/Thumbnailer.php'; | ||
4 | require_once 'application/config/ConfigManager.php'; | ||
5 | |||
6 | /** | ||
7 | * Class ThumbnailerTest | ||
8 | * | ||
9 | * We only make 1 thumb test because: | ||
10 | * | ||
11 | * 1. the thumbnailer library is itself tested | ||
12 | * 2. we don't want to make too many external requests during the tests | ||
13 | */ | ||
14 | class ThumbnailerTest extends PHPUnit_Framework_TestCase | ||
15 | { | ||
16 | /** | ||
17 | * Test a thumbnail with a custom size. | ||
18 | */ | ||
19 | public function testThumbnailValid() | ||
20 | { | ||
21 | $conf = new ConfigManager('tests/utils/config/configJson'); | ||
22 | $width = 200; | ||
23 | $height = 200; | ||
24 | $conf->set('thumbnails.width', $width); | ||
25 | $conf->set('thumbnails.height', $height); | ||
26 | |||
27 | $thumbnailer = new Thumbnailer($conf); | ||
28 | $thumb = $thumbnailer->get('https://github.com/shaarli/Shaarli/'); | ||
29 | $this->assertNotFalse($thumb); | ||
30 | $image = imagecreatefromstring(file_get_contents($thumb)); | ||
31 | $this->assertEquals($width, imagesx($image)); | ||
32 | $this->assertEquals($height, imagesy($image)); | ||
33 | } | ||
34 | |||
35 | /** | ||
36 | * Test a thumbnail that can't be retrieved. | ||
37 | * | ||
38 | * @expectedException WebThumbnailer\Exception\ThumbnailNotFoundException | ||
39 | */ | ||
40 | public function testThumbnailNotValid() | ||
41 | { | ||
42 | $oldlog = ini_get('error_log'); | ||
43 | ini_set('error_log', '/dev/null'); | ||
44 | |||
45 | $thumbnailer = new Thumbnailer(new ConfigManager()); | ||
46 | $thumb = $thumbnailer->get('nope'); | ||
47 | $this->assertFalse($thumb); | ||
48 | |||
49 | ini_set('error_log', $oldlog); | ||
50 | } | ||
51 | } | ||
diff --git a/tests/utils/config/configJson.json.php b/tests/utils/config/configJson.json.php index 9c9288f3..3101b225 100644 --- a/tests/utils/config/configJson.json.php +++ b/tests/utils/config/configJson.json.php | |||
@@ -29,6 +29,9 @@ | |||
29 | }, | 29 | }, |
30 | "plugins": { | 30 | "plugins": { |
31 | "WALLABAG_VERSION": 1 | 31 | "WALLABAG_VERSION": 1 |
32 | }, | ||
33 | "dev": { | ||
34 | "debug": true | ||
32 | } | 35 | } |
33 | } | 36 | } |
34 | */ ?> | 37 | */ ?> |
diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html index 479284eb..e47c71ad 100644 --- a/tpl/vintage/configure.html +++ b/tpl/vintage/configure.html | |||
@@ -128,6 +128,18 @@ | |||
128 | <input type="text" name="apiSecret" id="apiSecret" size="50" value="{$api_secret}" /> | 128 | <input type="text" name="apiSecret" id="apiSecret" size="50" value="{$api_secret}" /> |
129 | </td> | 129 | </td> |
130 | </tr> | 130 | </tr> |
131 | <tr> | ||
132 | <td valign="top"><b>Enable thumbnails</b></td> | ||
133 | <td> | ||
134 | <input type="checkbox" name="enableThumbnails" id="enableThumbnails" | ||
135 | {if="$thumbnails_enabled"}checked{/if}/> | ||
136 | <label for="enableThumbnails"> | ||
137 | <strong>Warning:</strong> | ||
138 | If you have a large database, the first retrieval may take a few minutes. | ||
139 | It's recommended to visit the picture wall after enabling this feature | ||
140 | </label> | ||
141 | </td> | ||
142 | </tr> | ||
131 | 143 | ||
132 | <tr> | 144 | <tr> |
133 | <td></td> | 145 | <td></td> |
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html index 1ca51be3..9bdafa8c 100644 --- a/tpl/vintage/linklist.html +++ b/tpl/vintage/linklist.html | |||
@@ -80,7 +80,16 @@ | |||
80 | {loop="$links"} | 80 | {loop="$links"} |
81 | <li{if="$value.class"} class="{$value.class}"{/if}> | 81 | <li{if="$value.class"} class="{$value.class}"{/if}> |
82 | <a id="{$value.shorturl}"></a> | 82 | <a id="{$value.shorturl}"></a> |
83 | <div class="thumbnail">{$value.url|thumbnail}</div> | 83 | {if="$thumbnails_enabled && !empty($value.thumbnail)"} |
84 | <div class="thumbnail"> | ||
85 | <a href="{$value.real_url}"> | ||
86 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} | ||
87 | <img data-src="{$value.thumbnail}#" class="b-lazy" | ||
88 | src="#" | ||
89 | alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" /> | ||
90 | </a> | ||
91 | </div> | ||
92 | {/if} | ||
84 | <div class="linkcontainer"> | 93 | <div class="linkcontainer"> |
85 | {if="$is_logged_in"} | 94 | {if="$is_logged_in"} |
86 | <div class="linkeditbuttons"> | 95 | <div class="linkeditbuttons"> |
diff --git a/tpl/vintage/picwall.html b/tpl/vintage/picwall.html index 29688914..2ac11ec2 100644 --- a/tpl/vintage/picwall.html +++ b/tpl/vintage/picwall.html | |||
@@ -15,7 +15,11 @@ | |||
15 | <div id="picwall_container"> | 15 | <div id="picwall_container"> |
16 | {loop="$linksToDisplay"} | 16 | {loop="$linksToDisplay"} |
17 | <div class="picwall_pictureframe"> | 17 | <div class="picwall_pictureframe"> |
18 | {$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a> | 18 | {ignore}RainTPL hack: put the 2 src on two different line to avoid path replace bug{/ignore} |
19 | <img data-src="{$value.thumbnail}#" class="b-lazy" | ||
20 | src="#" | ||
21 | alt="thumbnail" width="{$thumbnails_width}" height="{$thumbnails_height}" /> | ||
22 | <a href="{$value.real_url}"><span class="info">{$value.title}</span></a> | ||
19 | {loop="$value.picwall_plugin"} | 23 | {loop="$value.picwall_plugin"} |
20 | {$value} | 24 | {$value} |
21 | {/loop} | 25 | {/loop} |