pax_global_header 0000666 0000000 0000000 00000000064 13760020320 0014503 g ustar 00root root 0000000 0000000 52 comment=b746ca600b60098fa09d414a09a70a76fb198956
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/ 0000775 0000000 0000000 00000000000 13760020320 0020632 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/.gitignore 0000664 0000000 0000000 00000000071 13760020320 0022620 0 ustar 00root root 0000000 0000000 cache/*
workdir/*
!.gitkeep
coverage
vendor
composer.lock php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/.travis.yml 0000664 0000000 0000000 00000001252 13760020320 0022743 0 ustar 00root root 0000000 0000000 matrix:
include:
- language: php
php: nightly
install:
- composer self-update --2
- composer update --ignore-platform-req=php
- language: php
php: 7.4
- language: php
php: 7.3
- language: php
php: 7.2
- language: php
php: 7.1
cache:
directories:
- $HOME/.composer/cache
install:
- composer self-update
- composer install --prefer-dist
script:
- ./vendor/bin/phpcs src
- ./vendor/bin/phpunit --bootstrap tests/bootstrap.php --configuration phpunit.xml.dist tests
after_success:
- travis_retry php vendor/bin/php-coveralls -v -x coverage/logs/clover.xml -o coverage/logs/coveralls-upload.json
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/CHANGELOG.md 0000664 0000000 0000000 00000004336 13760020320 0022451 0 ustar 00root root 0000000 0000000 # Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## v1.3.1 - 2018-08-11
### Removed
* Remove PHP extension dependecies in `composer.json` to prevent docker multi stage build failure
## v1.3.0 - 2018-08-11
### Added
* Add a setting to force Apache version for htaccess syntax
### Fixed
* PHPDocs improvements
### Removed
* Parameter `PATH_TYPE` has been removed
* WebThumbnailer no longer try to resolve relative path to thumbnails, it now relies on provided `path.cache` setting
## v1.2.1 - 2018-07-17
### Fixed
* Fix a issue where download_mode from JSON config has no effect
## v1.2.0 - 2018-06-30
### Added
* Path type parameter, to retrieve either a relative or an absolute path to the thumbnail cache file
* `.htaccess` files are now created in cache folders (denied for `finder` and granted for `thumb`)
### Changed
* The relative path to the thumbnail cache file is now retrieved using `SCRIPT_FILENAME`.
## v1.1.3 - 2018-06-13
### Fixed
* Fix an issue with thumbs path with Apache alias where DOCUMENT_ROOT is not set properly
## v1.1.2 - 2018-05-05
### Added
* Support redirection in cURL download callback
### Fixed
* Fix an issue preventing the relative path to work properly in a subfolder
* Decode HTML entities on thumb urls (e.g. &)
* Fixed an issue where an empty cache folder where created
## v1.1.1 - 2018-05-01
### Fixed
* Fixed dev dependency
## v1.1.0 - 2018-05-01
> **Warning**: this release will invalidates existing cache.
### Added
* An exception is now thrown if PHP extension requirements are not satisfied.
* CI:
- Coverall PR check added
- Scrutinizer PR check added
- PHP CodeSniffer PSR-2 syntax is now run along unit tests
### Changed
* Image cache files are now stored as JPEG instead of PNG to save disk space.
* Image cache domain folders are now stored using a hash instead of the raw domain name.
### Fixed
* Make Github ignore HTML test files for language detection.
## v1.0.1 - 2017-11-27
First public release.
## v1.0.0 - 2017-11-27
First release. php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/LICENSE.md 0000664 0000000 0000000 00000002052 13760020320 0022235 0 ustar 00root root 0000000 0000000 MIT LICENSE
Copyright 2016 Arthur Hoareau
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/README.md 0000664 0000000 0000000 00000016270 13760020320 0022117 0 ustar 00root root 0000000 0000000 # Web Thumbnailer

[](https://coveralls.io/github/ArthurHoaro/web-thumbnailer?branch=master)
[](https://scrutinizer-ci.com/g/ArthurHoaro/web-thumbnailer/?branch=master)
PHP library which will retrieve a thumbnail for any given URL, if available.
## Features
- Support various specific website features: Imgur, FlickR, Youtube, XKCD, etc.
- Work with any website supporting [OpenGraph](http://ogp.me/) (tag meta `og:image`)
- Or use direct link to images
- Local cache
- Resizing and/or cropping according to given settings
## Requirements
Mandatory:
- PHP 7.1 (`v2.0.0`+) - PHP 5.6 (`v1.x.x`)
- PHP GD extension
(Highly) Recommended:
- PHP cURL extension: it let you retrieve thumbnails without downloading the whole remote page
## Installation
Using [Composer](https://getcomposer.org/):
```bash
composer require arthurhoaro/web-thumbnailer
```
## Usage
Using WebThumbnailer is pretty straight forward:
```php
require_once 'vendor/autoload.php';
$wt = new WebThumbnailer();
// Very basic usage
$thumb = $wt->thumbnail('https://github.com/ArthurHoaro');
// Using a bit of configuration
$thumb2 = $wt->maxHeight(180)
->maxWidth(320)
->crop(true)
->thumbnail('https://github.com/ArthurHoaro');
echo '
';
echo '
';
// returns false
$wt->thumbnail('bad url');
```
Result:
> 
> 
## Thumbnail configuration
There are 2 ways to set up thumbnail configuration:
* using `WebThumbnailer` helper functions as shown in *Usage* section.
* passing an array of settings to `thumbnail()` while getting a thumbnail.
This will override any setting setup with the helper functions.
Example:
```php
$conf = [
WebThumbnailer::MAX_HEIGHT => 320,
WebThumbnailer::MAX_WIDTH => 320,
WebThumbnailer::CROP => true
];
$wt->thumbnail('https://github.com/ArthurHoaro', $conf);
```
### Download mode
There are 3 download modes, only one can be used at once:
* Download (default): it will download thumbnail, resize it and save it in the cache folder.
* Hotlink: it will use [image hotlinking](https://en.wikipedia.org/wiki/Inline_linking) if the domain authorize it, download it otherwise.
* Hotlink strict: it will use image hotlinking if the domain authorize it, fail otherwise.
Usage:
```php
// Download (default value)
$wt = $wt->modeDownload();
$conf = [WebThumbnailer::DOWNLOAD];
// Hotlink
$wt = $wt->modeHotlink();
$conf = [WebThumbnailer::HOTLINK];
// Hotlink Strict
$wt = $wt->modeHotlinkStrict();
$conf = [WebThumbnailer::HOTLINK_STRICT];
```
> **Warning**: hotlinking means that thumbnails won't be resized, and have to be downloaded as their original size.
### Image Size
In download mode, thumbnails size can be defined using max width/height settings:
* with max height and max width, the thumbnail will be resized to match the first reached limit.
* with max height only, the thumbnail will be resized to the given height no matter its width.
* with max width only, the thumbnail will be resized to the given width no matter its height.
* if no size is provided, the default settings will apply (see Settings section).
Usage:
```php
// Sizes are given in number of pixels as an integer
$wt = $wt->maxHeight(180);
$conf = [WebThumbnailer::MAX_HEIGHT => 180];
$wt = $wt->maxWidth(320);
$conf = [WebThumbnailer::MAX_WIDTH => 180];
```
> **Bonus feature**: for websites which support an open API regarding their thumbnail size (e.g. Imgur, FlickR),
WebThumbnailer makes sure to download the smallest image matching size requirements.
### Image Crop
Image resizing might not be enough, and thumbnails might have to have a fixed size.
This option will crop the image (from its center) to match given dimensions.
> Note: max width AND height **must** be provided to use image crop.
Usage:
```php
// Sizes are given in number of pixels as an integer
$wt = $wt->crop(true);
$conf = [WebThumbnailer::CROP => true];
```
### Miscellaneous
* **NOCACHE**: Force the thumbnail to be resolved and downloaded instead of using cache files.
* **DEBUG**: Will throw an exception if an error occurred or if no thumbnail is found, instead of returning `false`.
* **VERBOSE**: Will log an entry in error log if a thumbnail could not be retrieved.
* **DOWNLOAD_TIMEOUT**: Override download timeout setting (in seconds).
* **DOWNLOAD_MAX_SIZE**: Override download max size setting (in bytes).
Usage:
```php
$wt = $wt
->noCache(true)
->debug(true)
->verbose(true)
->downloadTimeout(30)
->downloadMaxSize(4194304)
;
$conf = [
WebThumbnailer::NOCACHE => true,
WebThumbnailer::DEBUG => true,
WebThumbnailer::VERBOSE => true,
WebThumbnailer::DOWNLOAD_TIMEOUT => 30,
WebThumbnailer::DOWNLOAD_MAX_SIZE => 4194304,
];
```
## Settings
Settings are stored in JSON, and can be overrode using a custom JSON file:
```php
use WebThumbnailer\Application\ConfigManager;
ConfigManager::addFile('conf/mysettings.json');
```
Available settings:
* `default`:
* `download_mode`: default download mode (`DOWNLOAD`, `HOTLINK` or `HOTLINK_STRICT`).
* `timeout`: default download timeout, in seconds.
* `max_img_dl`: default download max size, in bytes.
* `max_width`: default max width if no size requirement is provided.
* `max_height`: default max height if no size requirement is provided.
* `cache_duration`: cache validity duration, in seconds (use a negative value for infinite cache).
* `path`:
* `cache`: cache path.
* `apache_version`: force `.htaccess` syntax depending on Apache's version, otherwise it uses `mod_version`
(allowed values: `2.2` or `2.4`).
## Thumbnails path
In download mode, the path to the thumbnail returned by WebThumbnailer library will depend on what's provided
to the `path.cache` setting. If an absolute path is set, thumbnails will be attached to an absolute path,
same for relative.
Relative path will depend on the entry point of the execution. For example, if your entry point for all request
is an `index.php` file in your project root directory, the default `cache/` setting will create a `cache/` folder
in the root directory. Another example, for Symfony, the cache folder will be relative to the `web/` directory,
which is the entry point with `app.php`.
If you don't have a single entry point in your project folder structure, you should provide an absolute path
and process the path yourself.
## Contributing
WebThumbnailer can easily support more website by adding new rules in `rules.json` using one of the default Finders,
or by writing a new Finder for specific cases.
Please report any issue you might encounter.
Also, feel free to correct any horrible English mistake I may have made in this README.
## License
MIT license, see LICENSE.md
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/cache/ 0000775 0000000 0000000 00000000000 13760020320 0021675 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/cache/.gitkeep 0000664 0000000 0000000 00000000000 13760020320 0023314 0 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/composer.json 0000664 0000000 0000000 00000001234 13760020320 0023354 0 ustar 00root root 0000000 0000000 {
"name": "arthurhoaro/web-thumbnailer",
"description": "PHP library which will retrieve a thumbnail for any given URL",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Arthur Hoaro",
"homepage": "http://hoa.ro"
}
],
"require": {
"php": ">=7.1",
"phpunit/php-text-template": "^1.2 || ^2.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"php-coveralls/php-coveralls": "^2.0",
"squizlabs/php_codesniffer": "^3.0",
"gskema/phpcs-type-sniff": "^0.13.1",
"phpstan/phpstan": "^0.12.9"
},
"autoload": {
"psr-0": {
"WebThumbnailer\\": ["src/", "tests/"]
}
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/phpcs.xml.dist 0000664 0000000 0000000 00000001243 13760020320 0023433 0 ustar 00root root 0000000 0000000
src
tests
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/phpunit.xml.dist 0000664 0000000 0000000 00000001732 13760020320 0024010 0 ustar 00root root 0000000 0000000
src/WebThumbnailer/
tests/WebThumbnailer/
tests
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/ 0000775 0000000 0000000 00000000000 13760020320 0021421 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/ 0000775 0000000 0000000 00000000000 13760020320 0024331 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/ 0000775 0000000 0000000 00000000000 13760020320 0026574 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/CacheManager.php 0000664 0000000 0000000 00000020341 13760020320 0031603 0 ustar 00root root 0000000 0000000 thumbnail url resolution is also cached.
* Cache files are organized by domains name, and have a unique name
* based on their URL, max-width and max-height.
*
* Cache duration is defined in JSON settings.
*/
class CacheManager
{
/** Thumbnails image cache. */
public const TYPE_THUMB = 'thumb';
/** Finder cache. */
public const TYPE_FINDER = 'finder';
/** @var string Clean filename, used to clean directories periodically. */
protected static $CLEAN_FILE = '.clean';
/**
* Returns the cache path according to the given type.
*
* @param string $type Type of cache.
* @param bool $rebuilt Flag to tell if a rebuild tentative has been done.
*
* @return string Cache path.
*
* @throws IOException Type not found.
* @throws CacheException
* @throws BadRulesException
*/
public static function getCachePath(string $type, bool $rebuilt = false): string
{
static::checkCacheType($type);
$cache = ConfigManager::get('settings.path.cache', 'cache/');
$path = FileUtils::getPath($cache, $type);
if (!$path && !$rebuilt) {
static::rebuildCacheFolders();
return static::getCachePath($type, true);
} elseif (!$path) {
throw new IOException('Cache folders are not writable: ' . $cache);
}
return $path;
}
/**
* Get a thumb cache file absolute path.
*
* @param string $url URL of the thumbnail (unique file per URL).
* @param string $domain Domain concerned.
* @param string $type Type of cache.
* @param int|string $width User setting for image width.
* @param int|string $height User setting for image height.
* @param bool|null $crop Crop enabled or not.
*
* @return string Absolute file path.
*
* @throws IOException
* @throws CacheException
* @throws BadRulesException
*/
public static function getCacheFilePath(
string $url,
string $domain,
string $type,
$width = 0,
$height = 0,
?bool $crop = false
): string {
$domainHash = static::getDomainHash($domain);
static::createDomainThumbCacheFolder($domainHash, $type);
$domainFolder = FileUtils::getPath(static::getCachePath($type), $domainHash);
if ($domainFolder === false) {
throw new CacheException(sprintf(
'Could not find cache path for type %s and domain hash %s',
$type,
$domainHash
));
}
if ($type === static::TYPE_THUMB) {
$suffix = $width . $height . ($crop ? '1' : '0') . '.jpg';
} else {
$suffix = $width . $height;
}
return $domainFolder . static::getThumbFilename($url) . $suffix;
}
/**
* Check whether a valid cache file exists or not.
* Also check that that file is still valid.
*
* Support endless cache using a negative value.
*
* @param string $cacheFile Cache file path.
* @param string $domain Domain concerned.
* @param string $type Type of cache.
*
* @return bool true if valid cache exists, false otherwise.
*
* @throws CacheException
* @throws IOException
* @throws BadRulesException
*/
public static function isCacheValid(string $cacheFile, string $domain, string $type): bool
{
$out = false;
$cacheDuration = ConfigManager::get('settings.cache_duration', 3600 * 24 * 31);
if (
is_readable($cacheFile)
&& ($cacheDuration < 0 || (time() - filemtime($cacheFile)) < $cacheDuration)
) {
$out = true;
} else {
static::createDomainThumbCacheFolder(static::getDomainHash($domain), $type);
}
return $out;
}
/**
* Create the domains folder for thumb cache if it doesn't exists.
*
* @param string $domain Domain used.
* @param string $type Type of cache.
*
* @throws CacheException
* @throws IOException
* @throws BadRulesException
*/
protected static function createDomainThumbCacheFolder(string $domain, string $type): void
{
$cachePath = static::getCachePath($type);
$domainFolder = $cachePath . $domain;
if (!file_exists($domainFolder)) {
mkdir($domainFolder, 0775, false);
touch($domainFolder . '/' . static::$CLEAN_FILE);
}
static::createHtaccessFile($cachePath, $type === static::TYPE_THUMB);
}
/**
* Create a .htaccess file for Apache webserver if it doesn't exists.
* The folder should be allowed for thumbs, and denied for finder's cache.
*
* @param string $path Cache directory path
* @param bool $allowed Weather the access is allowed or not
*
* @throws BadRulesException
* @throws IOException
*/
protected static function createHtaccessFile(string $path, bool $allowed = false): void
{
$apacheVersion = ConfigManager::get('settings.apache_version', '');
$htaccessFile = $path . '.htaccess';
if (file_exists($htaccessFile)) {
return;
}
$templateFile = file_exists(FileUtils::RESOURCES_PATH . 'htaccess' . $apacheVersion . '_template')
? FileUtils::RESOURCES_PATH . 'htaccess' . $apacheVersion . '_template'
: FileUtils::RESOURCES_PATH . 'htaccess_template';
$template = TemplatePolyfill::get($templateFile);
$template->setVar([
'new_all' => $allowed ? 'granted' : 'denied',
'old_allow' => $allowed ? 'all' : 'none',
'old_deny' => $allowed ? 'none' : 'all',
]);
file_put_contents($htaccessFile, $template->render());
}
/**
* Get the cache filename according to the given URL.
* Using a sha1 hash to get unique valid filenames.
*
* @param string $url Thumbnail URL.
*
* @return string Thumb filename.
*/
protected static function getThumbFilename(string $url): string
{
return hash('sha1', $url);
}
/**
* Make sure that the cache type exists.
*
* @param string $type Cache type.
*
* @return bool True if the check was successful.
*
* @throws CacheException Cache type doesn't exists.
*/
protected static function checkCacheType(string $type): bool
{
if ($type != static::TYPE_THUMB && $type != static::TYPE_FINDER) {
throw new CacheException('Unknown cache type ' . $type);
}
return true;
}
/**
* Recreates cache folders just in case the user delete them.
*
* @throws BadRulesException
* @throws IOException
*/
protected static function rebuildCacheFolders(): void
{
$mainFolder = ConfigManager::get('settings.path.cache', 'cache/');
if (! is_dir($mainFolder)) {
mkdir($mainFolder, 0755);
}
if (! is_dir($mainFolder . static::TYPE_THUMB)) {
mkdir($mainFolder . static::TYPE_THUMB, 0755);
}
if (! is_readable($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep')) {
touch($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep');
}
if (! is_dir($mainFolder . static::TYPE_FINDER)) {
mkdir($mainFolder . static::TYPE_FINDER, 0755);
}
if (! is_readable($mainFolder . static::TYPE_THUMB . DIRECTORY_SEPARATOR . '.gitkeep')) {
touch($mainFolder . static::TYPE_FINDER . DIRECTORY_SEPARATOR . '.gitkeep');
}
}
/**
* Return the hashed folder name for a given domain.
*
* @param string $domain name
*
* @return string hash
*/
protected static function getDomainHash(string $domain): string
{
return md5($domain);
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/ConfigManager.php 0000664 0000000 0000000 00000006724 13760020320 0032016 0 ustar 00root root 0000000 0000000 0) {
return static::getConfig($settings, $config[$setting]);
}
return $config[$setting];
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/Thumbnailer.php 0000664 0000000 0000000 00000030377 13760020320 0031571 0 ustar 00root root 0000000 0000000 url = $url;
$this->server = $server;
$this->finder = FinderFactory::getFinder($url);
$this->finder->setUserOptions($options);
$this->setOptions($options);
}
/**
* Set Thumbnailer options from user input.
*
* @param mixed[] $options User options array.
*
* @throws BadRulesException
* @throws IOException
*/
protected function setOptions(array $options): void
{
static::checkOptions($options);
$this->options[static::DL_OPTION] = ConfigManager::get('settings.default.download_mode', 'DOWNLOAD');
foreach ($options as $key => $value) {
// Download option.
if (
$value === WebThumbnailer::DOWNLOAD
|| $value === WebThumbnailer::HOTLINK
|| $value === WebThumbnailer::HOTLINK_STRICT
) {
$this->options[static::DL_OPTION] = $value;
break;
}
}
// DL size option
if (
isset($options[WebThumbnailer::DOWNLOAD_MAX_SIZE])
&& is_int($options[WebThumbnailer::DOWNLOAD_MAX_SIZE])
) {
$this->options[WebThumbnailer::DOWNLOAD_MAX_SIZE] = $options[WebThumbnailer::DOWNLOAD_MAX_SIZE];
} else {
$maxdl = ConfigManager::get('settings.default.max_img_dl', 4194304);
$this->options[WebThumbnailer::DOWNLOAD_MAX_SIZE] = $maxdl;
}
// DL timeout option
if (
isset($options[WebThumbnailer::DOWNLOAD_TIMEOUT])
&& is_int($options[WebThumbnailer::DOWNLOAD_TIMEOUT])
) {
$this->options[WebThumbnailer::DOWNLOAD_TIMEOUT] = $options[WebThumbnailer::DOWNLOAD_TIMEOUT];
} else {
$timeout = ConfigManager::get('settings.default.timeout', 30);
$this->options[WebThumbnailer::DOWNLOAD_TIMEOUT] = $timeout;
}
if (isset($options[WebThumbnailer::NOCACHE])) {
$this->options[WebThumbnailer::NOCACHE] = $options[WebThumbnailer::NOCACHE];
}
if (isset($options[WebThumbnailer::CROP])) {
$this->options[WebThumbnailer::CROP] = $options[WebThumbnailer::CROP];
} else {
$this->options[WebThumbnailer::CROP] = false;
}
if (isset($options[WebThumbnailer::DEBUG])) {
$this->options[WebThumbnailer::DEBUG] = $options[WebThumbnailer::DEBUG];
} else {
$this->options[WebThumbnailer::DEBUG] = false;
}
// Image size
$this->setSizeOptions($options);
}
/**
* Make sure user options are coherent.
* - Only one thumb mode can be defined.
*
* @param mixed[] $options User options array.
*
* @return bool True if the check is successful.
*
* @throws BadRulesException Invalid options.
*/
protected static function checkOptions(array $options): bool
{
$incompatibleFlagsList = [
[WebThumbnailer::DOWNLOAD, WebThumbnailer::HOTLINK, WebThumbnailer::HOTLINK_STRICT],
];
foreach ($incompatibleFlagsList as $incompatibleFlags) {
if (count(array_intersect($incompatibleFlags, $options)) > 1) {
$error = 'Only one of these flags can be set between: ';
foreach ($incompatibleFlags as $flag) {
$error .= $flag . ' ';
}
throw new BadRulesException($error);
}
}
return true;
}
/**
* Set specific size option, allowing 'meta' size SMALL, MEDIUM, etc.
*
* @param mixed[] $options User options array.
*
* @throws BadRulesException
* @throws IOException
*/
protected function setSizeOptions(array $options): void
{
foreach ([WebThumbnailer::MAX_WIDTH, WebThumbnailer::MAX_HEIGHT] as $parameter) {
$value = 0;
if (!empty($options[$parameter])) {
if (SizeUtils::isMetaSize((string) $options[$parameter])) {
$value = SizeUtils::getMetaSize((string) $options[$parameter]);
} elseif (is_int($options[$parameter]) || ctype_digit($options[$parameter])) {
$value = $options[$parameter];
}
}
$this->options[$parameter] = $value;
}
if ($this->options[WebThumbnailer::MAX_WIDTH] == 0 && $this->options[WebThumbnailer::MAX_HEIGHT] == 0) {
$maxwidth = ConfigManager::get('settings.default.max_width', 160);
$this->options[WebThumbnailer::MAX_WIDTH] = $maxwidth;
$maxheight = ConfigManager::get('settings.default.max_height', 160);
$this->options[WebThumbnailer::MAX_HEIGHT] = $maxheight;
}
}
/**
* Get the thumbnail according to download mode:
* - HOTLINK_STRICT: will only try to get hotlink thumb.
* - HOTLINK: will retrieve hotlink if available, or download otherwise.
* - DOWNLOAD: will download the thumb, resize it, and store it in cache.
*
* Default mode: DOWNLOAD.
*
* @return string|false The thumbnail URL (relative if downloaded), or false if no thumb found.
*
* @throws DownloadException
* @throws ImageConvertException
* @throws NotAnImageException
* @throws ThumbnailNotFoundException
* @throws IOException
* @throws CacheException
* @throws BadRulesException
*/
public function getThumbnail()
{
$cache = CacheManager::getCacheFilePath(
$this->url,
$this->finder->getDomain(),
CacheManager::TYPE_FINDER,
$this->options[WebThumbnailer::MAX_WIDTH],
$this->options[WebThumbnailer::MAX_HEIGHT]
);
// Loading Finder result from cache if enabled and valid to prevent useless requests.
if (
empty($this->options[WebThumbnailer::NOCACHE])
&& CacheManager::isCacheValid($cache, $this->finder->getDomain(), CacheManager::TYPE_FINDER)
) {
$thumbUrl = file_get_contents($cache);
} else {
$thumbUrl = $this->finder->find();
$thumbUrl = $thumbUrl !== false ? html_entity_decode($thumbUrl) : $thumbUrl;
file_put_contents($cache, $thumbUrl);
}
if (empty($thumbUrl)) {
$error = 'No thumbnail could be found using ' . $this->finder->getName() . ' finder: ' . $this->url;
throw new ThumbnailNotFoundException($error);
}
// Only hotlink, find() is enough.
if ($this->options[static::DL_OPTION] === WebThumbnailer::HOTLINK_STRICT) {
return $this->thumbnailStrictHotlink($thumbUrl);
}
// Hotlink if available, download otherwise.
if ($this->options[static::DL_OPTION] === WebThumbnailer::HOTLINK) {
return $this->thumbnailHotlink($thumbUrl);
} else { // Download
return $this->thumbnailDownload($thumbUrl);
}
}
/**
* Get thumbnails in HOTLINK_STRICT mode.
* Won't work for domains which doesn't allow hotlinking.
*
* @param string $thumbUrl Thumbnail URL, generated by the Finder.
*
* @return string The thumbnail URL.
*
* @throws ThumbnailNotFoundException Hotlink is disabled for this domains.
*/
protected function thumbnailStrictHotlink(string $thumbUrl): string
{
if (!$this->finder->isHotlinkAllowed()) {
throw new ThumbnailNotFoundException('Hotlink is not supported for this URL.');
}
return $thumbUrl;
}
/**
* Get thumbnails in HOTLINK mode.
*
* @param string $thumbUrl Thumbnail URL, generated by the Finder.
*
* @return string|false The thumbnail URL, or false if no thumb found.
*
* @throws DownloadException
* @throws ImageConvertException
* @throws NotAnImageException
* @throws IOException
* @throws CacheException
* @throws BadRulesException
*/
protected function thumbnailHotlink(string $thumbUrl)
{
if (!$this->finder->isHotlinkAllowed()) {
return $this->thumbnailDownload($thumbUrl);
}
return $thumbUrl;
}
/**
* Get thumbnails in HOTLINK mode.
*
* @param string $thumbUrl Thumbnail URL, generated by the Finder.
*
* @return string|false The thumbnail URL, or false if no thumb found.
*
* @throws DownloadException Couldn't download the image
* @throws ImageConvertException Thumbnail not generated
* @throws NotAnImageException
* @throws IOException
* @throws CacheException
* @throws BadRulesException
*/
protected function thumbnailDownload(string $thumbUrl)
{
// Cache file path.
$thumbPath = CacheManager::getCacheFilePath(
$thumbUrl,
$this->finder->getDomain(),
CacheManager::TYPE_THUMB,
$this->options[WebThumbnailer::MAX_WIDTH],
$this->options[WebThumbnailer::MAX_HEIGHT],
$this->options[WebThumbnailer::CROP]
);
// If the cache is valid, serve it.
if (
empty($this->options[WebThumbnailer::NOCACHE])
&& CacheManager::isCacheValid(
$thumbPath,
$this->finder->getDomain(),
CacheManager::TYPE_THUMB
)
) {
return $thumbPath;
}
$webaccess = WebAccessFactory::getWebAccess($thumbUrl);
// Download the thumb.
list($headers, $data) = $webaccess->getContent(
$thumbUrl,
$this->options[WebThumbnailer::DOWNLOAD_TIMEOUT],
$this->options[WebThumbnailer::DOWNLOAD_MAX_SIZE]
);
if (strpos($headers[0], '200') === false) {
throw new DownloadException(
'Unreachable thumbnail URL. HTTP ' . $headers[0] . '.' . PHP_EOL .
' - thumbnail URL: ' . $thumbUrl
);
}
if (empty($data)) {
throw new DownloadException('Couldn\'t download the thumbnail at ' . $thumbUrl);
}
// Resize and save it locally.
ImageUtils::generateThumbnail(
$data,
$thumbPath,
$this->options[WebThumbnailer::MAX_WIDTH],
$this->options[WebThumbnailer::MAX_HEIGHT],
$this->options[WebThumbnailer::CROP]
);
if (!is_file($thumbPath)) {
throw new ImageConvertException('Thumbnail was not generated.');
}
return $thumbPath;
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/WebAccess/ 0000775 0000000 0000000 00000000000 13760020320 0030433 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/WebAccess/WebAccess.php 0000664 0000000 0000000 00000003013 13760020320 0033000 0 ustar 00root root 0000000 0000000 '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,
['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);
curl_setopt($ch, CURLOPT_COOKIESESSION, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie);
// Max download size management
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024 * 16);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt(
$ch,
CURLOPT_PROGRESSFUNCTION,
function ($arg0, $arg1, $arg2) use ($maxBytes) {
$downloaded = $arg2;
// Non-zero return stops downloading
return ($downloaded > $maxBytes) ? 1 : 0;
}
);
if (is_callable($dlCallback)) {
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $dlCallback);
curl_exec($ch);
$response = $dlContent;
} else {
$response = curl_exec($ch);
}
$errorNo = curl_errno($ch);
$errorStr = curl_error($ch);
$headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
if (!is_string($response)) {
return [[0 => 'curl_exec() error #' . $errorNo . ': ' . $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 = [];
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 [$headers, $content];
}
}
WebAccessFactory.php 0000664 0000000 0000000 00000001470 13760020320 0034256 0 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Application/WebAccess getContext($timeout, false);
stream_context_set_default($context);
list($headers, $finalUrl) = $this->getRedirectedHeaders($url, $timeout, $maxRedr);
if (! $headers || strpos($headers[0], '200 OK') === false) {
$context = $this->getContext($timeout, true);
stream_context_set_default($context);
list($headers, $finalUrl) = $this->getRedirectedHeaders($url, $timeout, $maxRedr);
}
if (! $headers) {
return array($headers, false);
}
$context = stream_context_create($context);
$content = file_get_contents($finalUrl, false, $context, 0, $maxBytes);
return array($headers, $content);
}
/**
* Download URL HTTP headers and follow redirections (HTTP 30x) if necessary.
*
* @param string $url URL to download.
* @param int $timeout network timeout (in seconds)
* @param int $redirectionLimit Stop trying to follow redrection if this number is reached.
*
* @return mixed[] containing HTTP headers.
*/
protected function getRedirectedHeaders(string $url, int $timeout, int $redirectionLimit = 3): array
{
stream_context_set_default($this->getContext($timeout));
$headers = @get_headers($url, 1);
// Some hosts don't like fulluri request, some requires it...
if ($headers === false) {
stream_context_set_default($this->getContext($timeout, false));
$headers = @get_headers($url, 1);
}
// Headers found, redirection found, and limit not reached.
if (
$redirectionLimit-- > 0
&& !empty($headers)
&& (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false)
&& !empty($headers['Location'])
) {
$redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location'];
if ($redirection != $url) {
return $this->getRedirectedHeaders($redirection, $timeout, $redirectionLimit);
}
}
return [$headers, $url];
}
/**
* Create a valid context for PHP HTTP functions.
*
* @param int $timeout network timeout (in seconds)
* @param bool $fulluri this is required by some hosts, rejected by others, so option.
*
* @return mixed[] context.
*/
protected function getContext(int $timeout, bool $fulluri = true): array
{
return [
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:45.0; WebThumbnailer) Gecko/20100101 Firefox/45.0',
'request_fulluri' => $fulluri,
]
];
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Exception/ 0000775 0000000 0000000 00000000000 13760020320 0026267 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Exception/BadRegexException.php 0000664 0000000 0000000 00000000203 13760020320 0032333 0 ustar 00root root 0000000 0000000 webAccess = WebAccessFactory::getWebAccess($url);
$this->url = $url;
$this->domain = $domain;
}
/**
* Generic finder.
*
* @inheritdoc
*/
public function find()
{
if (ImageUtils::isImageExtension(UrlUtils::getUrlFileExtension($this->url))) {
return $this->url;
}
$content = $thumbnail = null;
$callback = $this->webAccess instanceof WebAccessCUrl
? $this->getCurlCallback($content, $thumbnail)
: null;
list($headers, $content) = $this->webAccess->getContent(
$this->url,
(int) ConfigManager::get('settings.default.timeout', 30),
(int) ConfigManager::get('settings.default.max_img_dl', 16777216),
$callback,
$content
);
if (empty($thumbnail) && ImageUtils::isImageString($content)) {
return $this->url;
}
if (empty($thumbnail) && ! empty($headers) && strpos($headers[0], '200') === false) {
return false;
}
// With curl, the thumb is extracted during the download
if ($this->webAccess instanceof WebAccessCUrl && ! empty($thumbnail)) {
return $thumbnail;
}
return ! empty($content) ? static::extractMetaTag($content) : false;
}
/**
* Get a callback for curl write function.
*
* @param string|null $content A variable reference in which the downloaded content should be stored.
* @param string|null $thumbnail A variable reference in which extracted thumb URL should be stored.
*
* @return callable CURLOPT_WRITEFUNCTION callback
*/
protected function getCurlCallback(?string &$content, ?string &$thumbnail): callable
{
$url = $this->url;
$isRedirected = false;
/**
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
*
* While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
* Then we extract the title and the charset and stop the download when it's done.
*
* Note that when using CURLOPT_WRITEFUNCTION, we have to manually handle the content retrieved,
* hence the $content reference variable.
*
* @param resource $ch cURL resource
* @param string $data chunk of data being downloaded
*
* @return int|false length of $data or false if we need to stop the download
*/
return function ($ch, $data) use ($url, &$content, &$thumbnail, &$isRedirected) {
$content .= $data;
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
$isRedirected = true;
return strlen($data);
}
if (!empty($responseCode) && $responseCode !== 200) {
return false;
}
// After a redirection, the content type will keep the previous request value
// until it finds the next content-type header.
if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
}
// we look for image, and ignore application/octet-stream,
// which is a the default content type for any binary
// @see https://developer.mozilla.org/fr/docs/Web/HTTP/Basics_of_HTTP/MIME_types
if (
!empty($contentType)
&& strpos($contentType, 'image/') !== false
&& strpos($contentType, 'application/octet-stream') === false
) {
$thumbnail = $url;
return false;
} elseif (
!empty($contentType)
&& strpos($contentType, 'text/html') === false
&& strpos($contentType, 'application/octet-stream') === false
) {
return false;
}
if (empty($thumbnail)) {
$thumbnail = DefaultFinder::extractMetaTag($data);
}
// We got everything we want, stop the download.
if (!empty($responseCode) && !empty($contentType) && !empty($thumbnail)) {
return false;
}
return strlen($data);
};
}
/**
* Applies the regexp on the HTML $content to extract the thumb URL.
*
* @param string $content Downloaded HTML content
*
* @return string|false Extracted thumb URL or false if not found.
*/
public static function extractMetaTag(string $content)
{
$propertiesKey = ['property', 'name', 'itemprop'];
$properties = implode('|', $propertiesKey);
// Try to retrieve OpenGraph image.
$ogRegex = '#]+(?:' . $properties . ')=["\']?og:image["\'\s][^>]*content=["\']?(.*?)["\'\s>]#';
// If the attributes are not in the order property => content (e.g. Github)
// New regex to keep this readable... more or less.
$ogRegexReverse = '#]+content=["\']?([^"\'\s]+)[^>]+(?:' . $properties . ')=["\']?og:image["\'\s/>]#';
if (
preg_match($ogRegex, $content, $matches) > 0
|| preg_match($ogRegexReverse, $content, $matches) > 0
) {
return $matches[1];
}
return false;
}
/** @inheritdoc */
public function isHotlinkAllowed(): bool
{
return true;
}
/** @inheritdoc */
public function checkRules(?array $rules): bool
{
return true;
}
/** @inheritdoc */
public function loadRules(?array $rules): void
{
}
/** @inheritdoc */
public function getName(): string
{
return 'default';
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Finder/Finder.php 0000664 0000000 0000000 00000004704 13760020320 0027465 0 ustar 00root root 0000000 0000000 getOptionValue($option);
return str_replace('${' . $option . '}', $chosenOption, $thumbnailUrl);
}
/**
* @param string $option to retrieve
*
* @return mixed Found option value
*
* @throws BadRulesException
*/
protected function getOptionValue(string $option)
{
// If the provided option is not defined in the Finder rules.
if (empty($this->finderOptions) || ! in_array($option, array_keys($this->finderOptions))) {
throw new BadRulesException('Unknown option "' . $option . '" for the finder "' . $this->getName() . '"');
}
// User option is defined.
// Any defined option must provide a replacement value in rules under the `param` key.
if (
! empty($this->userOptions[$option])
&& is_string($this->userOptions[$option])
&& isset($this->finderOptions[$option][$this->userOptions[$option]]['param'])
) {
return $this->finderOptions[$option][$this->userOptions[$option]]['param'];
}
// If no user option has been found, and no default value is provided: error.
if (! isset($this->finderOptions[$option]['default'])) {
$error = 'No default set for option "' . $option . '" for the finder "' . $this->getName() . '"';
throw new BadRulesException($error);
}
// Use default option replacement.
$default = $this->finderOptions[$option]['default'];
if (!isset($this->finderOptions[$option][$default]['param'])) {
$error = 'No default parameter set for option "' . $option . '" for the finder "' . $this->getName() . '"';
throw new BadRulesException($error);
}
return $this->finderOptions[$option][$default]['param'];
}
/** @inheritdoc */
public function isHotlinkAllowed(): bool
{
if (! isset($this->finderOptions['hotlink_allowed']) || $this->finderOptions['hotlink_allowed'] === true) {
return true;
}
return false;
}
/** @inheritdoc */
public function setUserOptions(?array $userOptions): void
{
$this->userOptions = $userOptions;
$this->setSizeOption();
}
/**
* Set size parameter properly.
*
* If something goes wrong, we just ignore it.
* The size user setting can be set to small, medium, etc. or a pixel value (int).
*
* We retrieve the thumbnail size bigger than the minimal size asked.
*/
protected function setSizeOption(): void
{
// If no option has been provided, we'll use default values.
if (
empty($this->userOptions[WebThumbnailer::MAX_HEIGHT])
&& empty($this->userOptions[WebThumbnailer::MAX_WIDTH])
) {
return;
}
// If the rules doesn't specify anything about size, abort.
if (empty($this->finderOptions[static::SIZE_KEY])) {
return;
}
// Load height user option.
if (!empty($this->userOptions[WebThumbnailer::MAX_HEIGHT])) {
$height = $this->userOptions[WebThumbnailer::MAX_HEIGHT];
if (SizeUtils::isMetaSize((string) $height)) {
$height = SizeUtils::getMetaSize((string) $height);
}
}
// Load width user option.
if (!empty($this->userOptions[WebThumbnailer::MAX_WIDTH])) {
$width = $this->userOptions[WebThumbnailer::MAX_WIDTH];
if (SizeUtils::isMetaSize((string) $width)) {
$width = SizeUtils::getMetaSize((string) $width);
}
}
// Trying to find a resolution higher than the one asked.
foreach ($this->finderOptions[static::SIZE_KEY] as $sizeOption => $value) {
if ($sizeOption == 'default') {
continue;
}
if (
(empty($value['maxwidth']) || empty($width) || $value['maxwidth'] >= $width)
&& (empty($value['maxheight']) || empty($height) || $value['maxheight'] >= $height)
) {
$this->userOptions[static::SIZE_KEY] = $sizeOption;
break;
}
}
// If the resolution asked hasn't been reached, take the highest resolution we have.
if ((!empty($width) || !empty($height)) && empty($this->userOptions[static::SIZE_KEY])) {
$ref = array_keys($this->finderOptions[static::SIZE_KEY]);
$this->userOptions[static::SIZE_KEY] = end($ref);
}
}
/** @inheritdoc */
public function getDomain(): string
{
return $this->domain;
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Finder/FinderFactory.php 0000664 0000000 0000000 00000007763 13760020320 0031025 0 ustar 00root root 0000000 0000000 getOptionValue('size');
// One size is actually no suffix...
$size = ! empty($size) ? '_' . $size : '';
$thumb = preg_replace('#(.*)_\w(\.\w+)$#i', '$1' . $size . '$2', $thumb);
return $thumb ?? false;
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Finder/QueryRegexFinder.php 0000664 0000000 0000000 00000015477 13760020320 0031517 0 ustar 00root root 0000000 0000000 webAccess = WebAccessFactory::getWebAccess($url);
$this->url = $url;
$this->domain = $domain;
$this->loadRules($rules);
$this->finderOptions = $options;
}
/**
* This finder downloads target URL page, and apply the regex given in rules on its content
* to extract the thumbnail image.
* The thumb URL must include ${number} to be replaced from the regex match.
* Also replace eventual URL options.
*
* @inheritdoc
*
* @throws BadRulesException
*/
public function find()
{
$thumbnail = $content = null;
$callback = $this->webAccess instanceof WebAccessCUrl
? $this->getCurlCallback($content, $thumbnail)
: null;
list($headers, $content) = $this->webAccess->getContent(
$this->url,
(int) ConfigManager::get('settings.default.timeout', 30),
(int) ConfigManager::get('settings.default.max_img_dl', 16777216),
$callback,
$content
);
if (
empty($content)
|| empty($headers)
|| (empty($thumbnail) && strpos($headers[0], '200') === false)
) {
return false;
}
// With curl, the thumb is extracted during the download
if ($this->webAccess instanceof WebAccessCUrl && ! empty($thumbnail)) {
return $thumbnail;
}
return $this->extractThumbContent($content);
}
/**
* Get a callback for curl write function.
*
* @param string|null $content A variable reference in which the downloaded content should be stored.
* @param string|null $thumbnail A variable reference in which extracted thumb URL should be stored.
*
* @return callable CURLOPT_WRITEFUNCTION callback
*/
protected function getCurlCallback(?string &$content, ?string &$thumbnail): callable
{
$isRedirected = false;
/**
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
*
* While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
* Then we extract the title and the charset and stop the download when it's done.
*
* Note that when using CURLOPT_WRITEFUNCTION, we have to manually handle the content retrieved,
* hence the $content reference variable.
*
* @param resource $ch cURL resource
* @param string $data chunk of data being downloaded
*
* @return int|false length of $data or false if we need to stop the download
*/
return function ($ch, $data) use (&$content, &$thumbnail, &$isRedirected) {
$content .= $data;
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
$isRedirected = true;
return strlen($data);
}
if (!empty($responseCode) && $responseCode !== 200) {
return false;
}
// After a redirection, the content type will keep the previous request value
// until it finds the next content-type header.
if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
}
if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
return false;
}
if (empty($thumbnail)) {
$thumbnail = $this->extractThumbContent($data);
}
// We got everything we want, stop the download.
if (!empty($responseCode) && !empty($contentType) && !empty($thumbnail)) {
return false;
}
return strlen($data);
};
}
/**
* @param string $content to extract thumb from
*
* @return string|false Thumbnail URL or false if not found
*
* @throws BadRulesException
*/
public function extractThumbContent(string $content)
{
$thumbnailUrl = $this->thumbnailUrlFormat;
if (preg_match($this->urlRegex, $content, $matches) !== 0) {
$total = count($matches);
for ($i = 1; $i < $total; $i++) {
$thumbnailUrl = str_replace('${' . $i . '}', $matches[$i], $thumbnailUrl);
}
// Match only options (not ${number})
if (preg_match_all('/\${((?!\d)\w+?)}/', $thumbnailUrl, $optionsMatch, PREG_PATTERN_ORDER)) {
foreach ($optionsMatch[1] as $value) {
$thumbnailUrl = $this->replaceOption($thumbnailUrl, $value);
}
}
return $thumbnailUrl;
}
return false;
}
/** @inheritdoc */
public function checkRules(?array $rules): bool
{
if (count($rules ?? []) > 0 && !FinderUtils::checkMandatoryRules($rules, ['image_regex', 'thumbnail_url'])) {
throw new BadRulesException();
}
return true;
}
/**
* @inheritdoc
*
* @throws BadRulesException
*/
public function loadRules(?array $rules): void
{
$this->checkRules($rules);
$this->urlRegex = FinderUtils::buildRegex($rules['image_regex'], 'im');
$this->thumbnailUrlFormat = $rules['thumbnail_url'];
}
/** @inheritdoc */
public function getName(): string
{
return 'Query Regex';
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Finder/UrlRegexFinder.php 0000664 0000000 0000000 00000005666 13760020320 0031153 0 ustar 00root root 0000000 0000000 url = $url;
$this->domain = $domain;
$this->loadRules($rules);
$this->finderOptions = $options;
}
/**
* Will replace ${number} in URL format to regex match.
* Also replace eventual URL options.
*
* {@inheritdoc}
*
* @throws BadRulesException
*/
public function find()
{
$this->thumbnailUrl = $this->thumbnailUrlFormat;
if (preg_match($this->urlRegex, $this->url, $matches) !== 0) {
$total = count($matches);
for ($i = 1; $i < $total; $i++) {
$this->thumbnailUrl = str_replace('${' . $i . '}', $matches[$i], $this->thumbnailUrl);
}
// Match only options (not ${number})
if (preg_match_all('/\${((?!\d)\w+?)}/', $this->thumbnailUrl, $optionsMatch, PREG_PATTERN_ORDER)) {
foreach ($optionsMatch[1] as $value) {
$this->thumbnailUrl = $this->replaceOption($this->thumbnailUrl, $value);
}
}
return $this->thumbnailUrl;
}
return false;
}
/**
* Mandatory rules:
* - url_regex
* - thumbnail_url
*
* {@inheritdoc}
*/
public function checkRules(?array $rules): bool
{
$mandatoryRules = [
'url_regex',
'thumbnail_url',
];
foreach ($mandatoryRules as $mandatoryRule) {
if (empty($rules[$mandatoryRule])) {
throw new BadRulesException();
}
}
return true;
}
/** @inheritdoc */
public function loadRules(?array $rules): void
{
$this->checkRules($rules);
$this->urlRegex = FinderUtils::buildRegex($rules['url_regex'], 'i');
$this->thumbnailUrlFormat = $rules['thumbnail_url'];
}
/** @inheritdoc */
public function getName(): string
{
return 'URL regex';
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/ 0000775 0000000 0000000 00000000000 13760020320 0025431 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/ApplicationUtils.php 0000664 0000000 0000000 00000002717 13760020320 0031435 0 ustar 00root root 0000000 0000000 isDir() ? rmdir($value->getRealPath()) : unlink($value->getRealPath());
}
return rmdir($path);
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/FinderUtils.php 0000664 0000000 0000000 00000002663 13760020320 0030401 0 ustar 00root root 0000000 0000000 $value) {
if (is_array($value)) {
if (isset($rules[$key])) {
return static::checkMandatoryRules($rules[$key], $value);
} else {
return false;
}
} else {
if (! isset($rules[$value])) {
return false;
}
}
}
return true;
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/ImageUtils.php 0000664 0000000 0000000 00000015554 13760020320 0030217 0 ustar 00root root 0000000 0000000 $originalWidth) {
$maxWidth = $originalWidth;
}
if ($maxHeight > $originalHeight) {
$maxHeight = $originalHeight;
}
list($finalWidth, $finalHeight) = static::calcNewSize(
$originalWidth,
$originalHeight,
$maxWidth,
$maxHeight,
$crop
);
$targetImg = imagecreatetruecolor($finalWidth, $finalHeight);
if ($targetImg === false) {
throw new ImageConvertException('Could not generate the thumbnail from source image.');
}
if (
!imagecopyresized(
$targetImg,
$sourceImg,
0,
0,
0,
0,
$finalWidth,
$finalHeight,
$originalWidth,
$originalHeight
)
) {
static::imageDestroy($sourceImg);
static::imageDestroy($targetImg);
throw new ImageConvertException('Could not generate the thumbnail from source image.');
}
if ($crop) {
$targetImg = imagecrop($targetImg, [
'x' => $finalWidth >= $finalHeight ? (int) floor(($finalWidth - $maxWidth) / 2) : 0,
'y' => $finalHeight <= $finalWidth ? (int) floor(($finalHeight - $maxHeight) / 2) : 0,
'width' => $maxWidth,
'height' => $maxHeight
]);
}
if (false === $targetImg) {
throw new ImageConvertException('Could not generate the thumbnail.');
}
imagedestroy($sourceImg);
imagejpeg($targetImg, $target);
imagedestroy($targetImg);
}
/**
* Calculate image new size to keep proportions depending on actual image size
* and max width/height settings.
*
* @param int $originalWidth Image original width
* @param int $originalHeight Image original height
* @param int $maxWidth Target image maximum width
* @param int $maxHeight Target image maximum height
* @param bool $crop Is cropping enabled
*
* @return int[] [final width, final height]
*
* @throws ImageConvertException At least maxwidth or maxheight needs to be defined
*/
public static function calcNewSize(
int $originalWidth,
int $originalHeight,
int $maxWidth,
int $maxHeight,
bool $crop
): array {
if (empty($maxHeight) && empty($maxWidth)) {
throw new ImageConvertException('At least maxwidth or maxheight needs to be defined.');
}
$diffWidth = !empty($maxWidth) ? $originalWidth - $maxWidth : false;
$diffHeight = !empty($maxHeight) ? $originalHeight - $maxHeight : false;
if (
($diffHeight === false && $diffWidth !== false)
|| ($diffWidth > $diffHeight && !$crop)
|| ($diffWidth < $diffHeight && $crop)
) {
$finalWidth = $maxWidth;
$finalHeight = $originalHeight * ($finalWidth / $originalWidth);
} else {
$finalHeight = $maxHeight;
$finalWidth = $originalWidth * ($finalHeight / $originalHeight);
}
return [(int) floor($finalWidth), (int) floor($finalHeight)];
}
/**
* Check if a file extension is an image.
*
* @param string $ext file extension.
*
* @return bool true if it's an image extension, false otherwise.
*/
public static function isImageExtension(string $ext): bool
{
$supportedImageFormats = ['png', 'jpg', 'jpeg', 'svg'];
return in_array($ext, $supportedImageFormats);
}
/**
* Check if a string is an image.
*
* @param string $content String to check.
*
* @return bool True if the content is image, false otherwise.
*/
public static function isImageString(string $content): bool
{
return static::imageCreateFromString($content) !== false;
}
/**
* With custom error handlers, @ does not stop the warning to being thrown.
*
* @param string $content
*
* @return resource|false
*/
protected static function imageCreateFromString(string $content)
{
try {
return @imagecreatefromstring($content);
} catch (\Throwable $e) {
// Avoid raising PHP exceptions here with custom error handler, we want to raise our own.
}
return false;
}
/**
* With custom error handlers, @ does not stop the warning to being thrown.
*
* @param resource $image
*
* @return bool
*/
// resource can't be type hinted:
// phpcs:ignore Gskema.Sniffs.CompositeCodeElement.FqcnMethodSniff
protected static function imageDestroy($image): bool
{
try {
return @imagedestroy($image);
} catch (\Throwable $e) {
// Avoid raising PHP exceptions here with custom error handler, we want to raise our own.
}
return false;
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/Utils/SizeUtils.php 0000664 0000000 0000000 00000003053 13760020320 0030076 0 ustar 00root root 0000000 0000000 0) {
return strtolower($match[1]);
}
return '';
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/WebThumbnailer.php 0000664 0000000 0000000 00000014376 13760020320 0027765 0 ustar 00root root 0000000 0000000
*
* @param string $url User URL.
* @param mixed[] $options Options array. See the documentation for more infos.
*
* @return string|false Thumbnail URL, false if not found.
*
* @throws WebThumbnailerException Only throw exception in debug mode.
*/
public function thumbnail(string $url, array $options = [])
{
$url = trim($url);
if (empty($url)) {
return false;
}
$options = array_merge(
[
static::DEBUG => $this->debug,
static::VERBOSE => $this->verbose,
static::NOCACHE => $this->nocache,
static::MAX_WIDTH => $this->maxWidth,
static::MAX_HEIGHT => $this->maxHeight,
static::DOWNLOAD_TIMEOUT => $this->downloadTimeout,
static::DOWNLOAD_MAX_SIZE => $this->downloadMaxSize,
static::CROP => $this->crop,
$this->downloadMode
],
$options
);
try {
$downloader = new Thumbnailer($url, $options, $_SERVER);
return $downloader->getThumbnail();
} catch (MissingRequirementException $e) {
throw $e;
} catch (WebThumbnailerException $e) {
if (isset($options[static::VERBOSE]) && $options[static::VERBOSE] === true) {
error_log($e->getMessage());
}
if (isset($options[static::DEBUG]) && $options[static::DEBUG] === true) {
throw $e;
}
return false;
}
}
/**
* @param int|string $maxWidth Either number of pixels or SIZE_SMALL|SIZE_MEDIUM|SIZE_LARGE.
*
* @return WebThumbnailer self instance.
*/
public function maxWidth($maxWidth): self
{
$this->maxWidth = (int) $maxWidth;
return $this;
}
/**
* @param int|string $maxHeight Either number of pixels or SIZE_SMALL|SIZE_MEDIUM|SIZE_LARGE.
*
* @return WebThumbnailer self instance.
*/
public function maxHeight($maxHeight): self
{
$this->maxHeight = (int) $maxHeight;
return $this;
}
/**
* @param bool $debug
*
* @return WebThumbnailer self instance.
*/
public function debug(bool $debug): self
{
$this->debug = $debug;
return $this;
}
/**
* @param bool $verbose
*
* @return WebThumbnailer self instance.
*/
public function verbose(bool $verbose): self
{
$this->verbose = $verbose;
return $this;
}
/**
* @param bool $nocache
*
* @return WebThumbnailer self instance.
*/
public function noCache(bool $nocache): self
{
$this->nocache = $nocache;
return $this;
}
/**
* @param bool $crop
*
* @return WebThumbnailer $this
*/
public function crop(bool $crop): self
{
$this->crop = $crop;
return $this;
}
/**
* @param int $downloadTimeout in seconds
*
* @return WebThumbnailer $this
*/
public function downloadTimeout(int $downloadTimeout): self
{
$this->downloadTimeout = $downloadTimeout;
return $this;
}
/**
* @param int $downloadMaxSize in bytes
*
* @return WebThumbnailer $this
*/
public function downloadMaxSize(int $downloadMaxSize): self
{
$this->downloadMaxSize = $downloadMaxSize;
return $this;
}
/**
* Enable download mode
* It will download thumbnail, resize it and save it in the cache folder.
*
* @return WebThumbnailer $this
*/
public function modeDownload(): self
{
$this->downloadMode = static::DOWNLOAD;
return $this;
}
/**
* Enable hotlink mode
* It will use image hotlinking if the domain authorize it, download it otherwise.
*
* @return WebThumbnailer $this
*/
public function modeHotlink(): self
{
$this->downloadMode = static::HOTLINK;
return $this;
}
/**
* Enable strict hotlink mode
* It will use image hotlinking if the domain authorize it, fail otherwise.
*
* @return WebThumbnailer $this
*/
public function modeHotlinkStrict(): self
{
$this->downloadMode = static::HOTLINK_STRICT;
return $this;
}
}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/ 0000775 0000000 0000000 00000000000 13760020320 0025374 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/finder/ 0000775 0000000 0000000 00000000000 13760020320 0026643 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/finder/.gitkeep 0000664 0000000 0000000 00000000000 13760020320 0030262 0 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/thumb/ 0000775 0000000 0000000 00000000000 13760020320 0026513 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/cache/thumb/.gitkeep 0000664 0000000 0000000 00000000000 13760020320 0030132 0 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/ 0000775 0000000 0000000 00000000000 13760020320 0026343 5 ustar 00root root 0000000 0000000 php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/htaccess2.2_template 0000664 0000000 0000000 00000000054 13760020320 0032177 0 ustar 00root root 0000000 0000000 Allow from {old_allow}
Deny from {old_deny}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/htaccess2.4_template 0000664 0000000 0000000 00000000026 13760020320 0032200 0 ustar 00root root 0000000 0000000 Require all {new_all}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/htaccess_template 0000664 0000000 0000000 00000000400 13760020320 0031750 0 ustar 00root root 0000000 0000000
= 2.4>
Require all {new_all}
Allow from {old_allow}
Deny from {old_deny}
Require all {new_all}
php-arthurhoaro-web-thumbnailer-2.0.3+dfsg/src/WebThumbnailer/resources/rules.json 0000664 0000000 0000000 00000020066 13760020320 0030374 0 ustar 00root root 0000000 0000000 {
"flickR": {
"domains": ["flickr.com"],
"finder": "FlickR",
"options": {
"hotlink_allowed": true,
"size": {
"default": "medium",
"thumb": {
"param": "t",
"maxwidth": "100",
"maxheight": "100"
},
"small": {
"param": "m",
"maxwidth": "240",
"maxheight": "240"
},
"smallplus": {
"param": "n",
"maxwidth": "320",
"maxheight": "320"
},
"medium": {
"param": "",
"maxwidth": "500",
"maxheight": "500"
},
"mediumplus": {
"param": "z",
"maxwidth": "640",
"maxheight": "640"
},
"large": {
"param": "c",
"maxwidth": "800",
"maxheight": "800"
},
"xlarge": {
"param": "b",
"maxwidth": "1024",
"maxheight": "1024"
},
"xxlarge": {
"param": "h",
"maxwidth": "1600",
"maxheight": "1600"
},
"xxxlarge": {
"param": "k",
"maxwidth": "2048",
"maxheight": "2048"
},
"_comment": "Original size is disabled because users may deny access to original size..."
}
},
"rules": {
"image_regex": "