Merge pull request #1567 from ArthurHoaro/feature/async-title-retrieval
This commit is contained in:
commit
9b3c1270bc
22 changed files with 689 additions and 257 deletions
1
Makefile
1
Makefile
|
@ -175,6 +175,7 @@ translate:
|
||||||
eslint:
|
eslint:
|
||||||
@yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/
|
@yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/
|
||||||
@yarn run eslint -c .dev/.eslintrc.js assets/default/js/
|
@yarn run eslint -c .dev/.eslintrc.js assets/default/js/
|
||||||
|
@yarn run eslint -c .dev/.eslintrc.js assets/common/js/
|
||||||
|
|
||||||
### Run CSSLint check against Shaarli's SCSS files
|
### Run CSSLint check against Shaarli's SCSS files
|
||||||
sasslint:
|
sasslint:
|
||||||
|
|
|
@ -366,7 +366,8 @@ protected function setDefaultValues()
|
||||||
$this->setEmpty('general.links_per_page', 20);
|
$this->setEmpty('general.links_per_page', 20);
|
||||||
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
|
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
|
||||||
$this->setEmpty('general.default_note_title', 'Note: ');
|
$this->setEmpty('general.default_note_title', 'Note: ');
|
||||||
$this->setEmpty('general.retrieve_description', false);
|
$this->setEmpty('general.retrieve_description', true);
|
||||||
|
$this->setEmpty('general.enable_async_metadata', true);
|
||||||
|
|
||||||
$this->setEmpty('updates.check_updates', false);
|
$this->setEmpty('updates.check_updates', false);
|
||||||
$this->setEmpty('updates.check_updates_branch', 'stable');
|
$this->setEmpty('updates.check_updates_branch', 'stable');
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
|
use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
|
||||||
use Shaarli\History;
|
use Shaarli\History;
|
||||||
use Shaarli\Http\HttpAccess;
|
use Shaarli\Http\HttpAccess;
|
||||||
|
use Shaarli\Http\MetadataRetriever;
|
||||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||||
use Shaarli\Plugin\PluginManager;
|
use Shaarli\Plugin\PluginManager;
|
||||||
use Shaarli\Render\PageBuilder;
|
use Shaarli\Render\PageBuilder;
|
||||||
|
@ -90,6 +91,10 @@ public function build(): ShaarliContainer
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever {
|
||||||
|
return new MetadataRetriever($container->conf, $container->httpAccess);
|
||||||
|
};
|
||||||
|
|
||||||
$container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
|
$container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
|
||||||
return new PageBuilder(
|
return new PageBuilder(
|
||||||
$container->conf,
|
$container->conf,
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
use Shaarli\Formatter\FormatterFactory;
|
use Shaarli\Formatter\FormatterFactory;
|
||||||
use Shaarli\History;
|
use Shaarli\History;
|
||||||
use Shaarli\Http\HttpAccess;
|
use Shaarli\Http\HttpAccess;
|
||||||
|
use Shaarli\Http\MetadataRetriever;
|
||||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||||
use Shaarli\Plugin\PluginManager;
|
use Shaarli\Plugin\PluginManager;
|
||||||
use Shaarli\Render\PageBuilder;
|
use Shaarli\Render\PageBuilder;
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
* @property History $history
|
* @property History $history
|
||||||
* @property HttpAccess $httpAccess
|
* @property HttpAccess $httpAccess
|
||||||
* @property LoginManager $loginManager
|
* @property LoginManager $loginManager
|
||||||
|
* @property MetadataRetriever $metadataRetriever
|
||||||
* @property NetscapeBookmarkUtils $netscapeBookmarkUtils
|
* @property NetscapeBookmarkUtils $netscapeBookmarkUtils
|
||||||
* @property callable $notFoundHandler Overrides default Slim exception display
|
* @property callable $notFoundHandler Overrides default Slim exception display
|
||||||
* @property PageBuilder $pageBuilder
|
* @property PageBuilder $pageBuilder
|
||||||
|
|
|
@ -53,36 +53,22 @@ public function displayCreateForm(Request $request, Response $response): Respons
|
||||||
|
|
||||||
// If this is an HTTP(S) link, we try go get the page to extract
|
// If this is an HTTP(S) link, we try go get the page to extract
|
||||||
// the title (otherwise we will to straight to the edit form.)
|
// the title (otherwise we will to straight to the edit form.)
|
||||||
if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
|
if (true !== $this->container->conf->get('general.enable_async_metadata', true)
|
||||||
$retrieveDescription = $this->container->conf->get('general.retrieve_description');
|
&& empty($title)
|
||||||
// Short timeout to keep the application responsive
|
&& strpos(get_url_scheme($url) ?: '', 'http') !== false
|
||||||
// The callback will fill $charset and $title with data from the downloaded page.
|
) {
|
||||||
$this->container->httpAccess->getHttpResponse(
|
$metadata = $this->container->metadataRetriever->retrieve($url);
|
||||||
$url,
|
|
||||||
$this->container->conf->get('general.download_timeout', 30),
|
|
||||||
$this->container->conf->get('general.download_max_size', 4194304),
|
|
||||||
$this->container->httpAccess->getCurlDownloadCallback(
|
|
||||||
$charset,
|
|
||||||
$title,
|
|
||||||
$description,
|
|
||||||
$tags,
|
|
||||||
$retrieveDescription
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (! empty($title) && strtolower($charset) !== 'utf-8' && mb_check_encoding($charset)) {
|
|
||||||
$title = mb_convert_encoding($title, 'utf-8', $charset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($url) && empty($title)) {
|
if (empty($url)) {
|
||||||
$title = $this->container->conf->get('general.default_note_title', t('Note: '));
|
$metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: '));
|
||||||
}
|
}
|
||||||
|
|
||||||
$link = [
|
$link = [
|
||||||
'title' => $title,
|
'title' => $title ?? $metadata['title'] ?? '',
|
||||||
'url' => $url ?? '',
|
'url' => $url ?? '',
|
||||||
'description' => $description ?? '',
|
'description' => $description ?? $metadata['description'] ?? '',
|
||||||
'tags' => $tags ?? '',
|
'tags' => $tags ?? $metadata['tags'] ?? '',
|
||||||
'private' => $private,
|
'private' => $private,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
|
@ -352,6 +338,8 @@ protected function displayForm(array $link, bool $isNew, Request $request, Respo
|
||||||
'source' => $request->getParam('source') ?? '',
|
'source' => $request->getParam('source') ?? '',
|
||||||
'tags' => $tags,
|
'tags' => $tags,
|
||||||
'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
|
'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
|
||||||
|
'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true),
|
||||||
|
'retrieve_description' => $this->container->conf->get('general.retrieve_description', false),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
|
$this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
|
||||||
|
|
29
application/front/controller/admin/MetadataController.php
Normal file
29
application/front/controller/admin/MetadataController.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Front\Controller\Admin;
|
||||||
|
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used to retrieve/update bookmark's metadata.
|
||||||
|
*/
|
||||||
|
class MetadataController extends ShaarliAdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* GET /admin/metadata/{url} - Attempt to retrieve the bookmark title from provided URL.
|
||||||
|
*/
|
||||||
|
public function ajaxRetrieveTitle(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$url = $request->getParam('url');
|
||||||
|
|
||||||
|
// Only try to extract metadata from URL with HTTP(s) scheme
|
||||||
|
if (!empty($url) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
|
||||||
|
return $response->withJson($this->container->metadataRetriever->retrieve($url));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response->withJson([]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,9 +14,14 @@
|
||||||
*/
|
*/
|
||||||
class HttpAccess
|
class HttpAccess
|
||||||
{
|
{
|
||||||
public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
|
public function getHttpResponse(
|
||||||
{
|
$url,
|
||||||
return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction);
|
$timeout = 30,
|
||||||
|
$maxBytes = 4194304,
|
||||||
|
$curlHeaderFunction = null,
|
||||||
|
$curlWriteFunction = null
|
||||||
|
) {
|
||||||
|
return get_http_response($url, $timeout, $maxBytes, $curlHeaderFunction, $curlWriteFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCurlDownloadCallback(
|
public function getCurlDownloadCallback(
|
||||||
|
@ -24,16 +29,19 @@ public function getCurlDownloadCallback(
|
||||||
&$title,
|
&$title,
|
||||||
&$description,
|
&$description,
|
||||||
&$keywords,
|
&$keywords,
|
||||||
$retrieveDescription,
|
$retrieveDescription
|
||||||
$curlGetInfo = 'curl_getinfo'
|
|
||||||
) {
|
) {
|
||||||
return get_curl_download_callback(
|
return get_curl_download_callback(
|
||||||
$charset,
|
$charset,
|
||||||
$title,
|
$title,
|
||||||
$description,
|
$description,
|
||||||
$keywords,
|
$keywords,
|
||||||
$retrieveDescription,
|
$retrieveDescription
|
||||||
$curlGetInfo
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCurlHeaderCallback(&$charset, $curlGetInfo = 'curl_getinfo')
|
||||||
|
{
|
||||||
|
return get_curl_header_callback($charset, $curlGetInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
* @param string $url URL to get (http://...)
|
* @param string $url URL to get (http://...)
|
||||||
* @param int $timeout network timeout (in seconds)
|
* @param int $timeout network timeout (in seconds)
|
||||||
* @param int $maxBytes maximum downloaded bytes (default: 4 MiB)
|
* @param int $maxBytes maximum downloaded bytes (default: 4 MiB)
|
||||||
|
* @param callable|string $curlHeaderFunction Optional callback called during the download of headers
|
||||||
|
* (CURLOPT_HEADERFUNCTION)
|
||||||
* @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION).
|
* @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION).
|
||||||
* Can be used to add download conditions on the
|
* Can be used to add download conditions on the
|
||||||
* headers (response code, content type, etc.).
|
* headers (response code, content type, etc.).
|
||||||
|
@ -35,8 +37,13 @@
|
||||||
* @see http://stackoverflow.com/q/9183178
|
* @see http://stackoverflow.com/q/9183178
|
||||||
* @see http://stackoverflow.com/q/1462720
|
* @see http://stackoverflow.com/q/1462720
|
||||||
*/
|
*/
|
||||||
function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
|
function get_http_response(
|
||||||
{
|
$url,
|
||||||
|
$timeout = 30,
|
||||||
|
$maxBytes = 4194304,
|
||||||
|
$curlHeaderFunction = null,
|
||||||
|
$curlWriteFunction = null
|
||||||
|
) {
|
||||||
$urlObj = new Url($url);
|
$urlObj = new Url($url);
|
||||||
$cleanUrl = $urlObj->idnToAscii();
|
$cleanUrl = $urlObj->idnToAscii();
|
||||||
|
|
||||||
|
@ -70,7 +77,8 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
|
||||||
// General cURL settings
|
// General cURL settings
|
||||||
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
|
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
|
||||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
// Default header download if the $curlHeaderFunction is not defined
|
||||||
|
curl_setopt($ch, CURLOPT_HEADER, !is_callable($curlHeaderFunction));
|
||||||
curl_setopt(
|
curl_setopt(
|
||||||
$ch,
|
$ch,
|
||||||
CURLOPT_HTTPHEADER,
|
CURLOPT_HTTPHEADER,
|
||||||
|
@ -81,25 +89,21 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||||
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
|
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
|
||||||
|
|
||||||
if (is_callable($curlWriteFunction)) {
|
|
||||||
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max download size management
|
// Max download size management
|
||||||
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
|
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
|
||||||
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
|
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
|
||||||
|
if (is_callable($curlHeaderFunction)) {
|
||||||
|
curl_setopt($ch, CURLOPT_HEADERFUNCTION, $curlHeaderFunction);
|
||||||
|
}
|
||||||
|
if (is_callable($curlWriteFunction)) {
|
||||||
|
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
|
||||||
|
}
|
||||||
curl_setopt(
|
curl_setopt(
|
||||||
$ch,
|
$ch,
|
||||||
CURLOPT_PROGRESSFUNCTION,
|
CURLOPT_PROGRESSFUNCTION,
|
||||||
function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
|
function ($arg0, $arg1, $arg2, $arg3, $arg4) use ($maxBytes) {
|
||||||
if (version_compare(phpversion(), '5.5', '<')) {
|
|
||||||
// PHP version lower than 5.5
|
|
||||||
// Callback has 4 arguments
|
|
||||||
$downloaded = $arg1;
|
|
||||||
} else {
|
|
||||||
// Callback has 5 arguments
|
|
||||||
$downloaded = $arg2;
|
$downloaded = $arg2;
|
||||||
}
|
|
||||||
// Non-zero return stops downloading
|
// Non-zero return stops downloading
|
||||||
return ($downloaded > $maxBytes) ? 1 : 0;
|
return ($downloaded > $maxBytes) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
@ -493,53 +497,22 @@ function is_https($server)
|
||||||
* Get cURL callback function for CURLOPT_WRITEFUNCTION
|
* Get cURL callback function for CURLOPT_WRITEFUNCTION
|
||||||
*
|
*
|
||||||
* @param string $charset to extract from the downloaded page (reference)
|
* @param string $charset to extract from the downloaded page (reference)
|
||||||
* @param string $title to extract from the downloaded page (reference)
|
|
||||||
* @param string $description to extract from the downloaded page (reference)
|
|
||||||
* @param string $keywords to extract from the downloaded page (reference)
|
|
||||||
* @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
|
|
||||||
* @param string $curlGetInfo Optionally overrides curl_getinfo function
|
* @param string $curlGetInfo Optionally overrides curl_getinfo function
|
||||||
*
|
*
|
||||||
* @return Closure
|
* @return Closure
|
||||||
*/
|
*/
|
||||||
function get_curl_download_callback(
|
function get_curl_header_callback(
|
||||||
&$charset,
|
&$charset,
|
||||||
&$title,
|
|
||||||
&$description,
|
|
||||||
&$keywords,
|
|
||||||
$retrieveDescription,
|
|
||||||
$curlGetInfo = 'curl_getinfo'
|
$curlGetInfo = 'curl_getinfo'
|
||||||
) {
|
) {
|
||||||
$isRedirected = false;
|
$isRedirected = false;
|
||||||
$currentChunk = 0;
|
|
||||||
$foundChunk = null;
|
|
||||||
|
|
||||||
/**
|
return function ($ch, $data) use ($curlGetInfo, &$charset, &$isRedirected) {
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* @param resource $ch cURL resource
|
|
||||||
* @param string $data chunk of data being downloaded
|
|
||||||
*
|
|
||||||
* @return int|bool length of $data or false if we need to stop the download
|
|
||||||
*/
|
|
||||||
return function (&$ch, $data) use (
|
|
||||||
$retrieveDescription,
|
|
||||||
$curlGetInfo,
|
|
||||||
&$charset,
|
|
||||||
&$title,
|
|
||||||
&$description,
|
|
||||||
&$keywords,
|
|
||||||
&$isRedirected,
|
|
||||||
&$currentChunk,
|
|
||||||
&$foundChunk
|
|
||||||
) {
|
|
||||||
$currentChunk++;
|
|
||||||
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
|
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
|
$chunkLength = strlen($data);
|
||||||
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
|
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
|
||||||
$isRedirected = true;
|
$isRedirected = true;
|
||||||
return strlen($data);
|
return $chunkLength;
|
||||||
}
|
}
|
||||||
if (!empty($responseCode) && $responseCode !== 200) {
|
if (!empty($responseCode) && $responseCode !== 200) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -555,6 +528,56 @@ function get_curl_download_callback(
|
||||||
if (!empty($contentType) && empty($charset)) {
|
if (!empty($contentType) && empty($charset)) {
|
||||||
$charset = header_extract_charset($contentType);
|
$charset = header_extract_charset($contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $chunkLength;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cURL callback function for CURLOPT_WRITEFUNCTION
|
||||||
|
*
|
||||||
|
* @param string $charset to extract from the downloaded page (reference)
|
||||||
|
* @param string $title to extract from the downloaded page (reference)
|
||||||
|
* @param string $description to extract from the downloaded page (reference)
|
||||||
|
* @param string $keywords to extract from the downloaded page (reference)
|
||||||
|
* @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
|
||||||
|
* @param string $curlGetInfo Optionally overrides curl_getinfo function
|
||||||
|
*
|
||||||
|
* @return Closure
|
||||||
|
*/
|
||||||
|
function get_curl_download_callback(
|
||||||
|
&$charset,
|
||||||
|
&$title,
|
||||||
|
&$description,
|
||||||
|
&$keywords,
|
||||||
|
$retrieveDescription
|
||||||
|
) {
|
||||||
|
$currentChunk = 0;
|
||||||
|
$foundChunk = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param resource $ch cURL resource
|
||||||
|
* @param string $data chunk of data being downloaded
|
||||||
|
*
|
||||||
|
* @return int|bool length of $data or false if we need to stop the download
|
||||||
|
*/
|
||||||
|
return function ($ch, $data) use (
|
||||||
|
$retrieveDescription,
|
||||||
|
&$charset,
|
||||||
|
&$title,
|
||||||
|
&$description,
|
||||||
|
&$keywords,
|
||||||
|
&$currentChunk,
|
||||||
|
&$foundChunk
|
||||||
|
) {
|
||||||
|
$chunkLength = strlen($data);
|
||||||
|
$currentChunk++;
|
||||||
|
|
||||||
if (empty($charset)) {
|
if (empty($charset)) {
|
||||||
$charset = html_extract_charset($data);
|
$charset = html_extract_charset($data);
|
||||||
}
|
}
|
||||||
|
@ -562,6 +585,10 @@ function get_curl_download_callback(
|
||||||
$title = html_extract_title($data);
|
$title = html_extract_title($data);
|
||||||
$foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
|
$foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
|
||||||
}
|
}
|
||||||
|
if (empty($title)) {
|
||||||
|
$title = html_extract_tag('title', $data);
|
||||||
|
$foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
|
||||||
|
}
|
||||||
if ($retrieveDescription && empty($description)) {
|
if ($retrieveDescription && empty($description)) {
|
||||||
$description = html_extract_tag('description', $data);
|
$description = html_extract_tag('description', $data);
|
||||||
$foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
|
$foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
|
||||||
|
@ -591,6 +618,6 @@ function get_curl_download_callback(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return strlen($data);
|
return $chunkLength;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
69
application/http/MetadataRetriever.php
Normal file
69
application/http/MetadataRetriever.php
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Http;
|
||||||
|
|
||||||
|
use Shaarli\Config\ConfigManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Tool used to extract metadata from external URL (title, description, etc.).
|
||||||
|
*/
|
||||||
|
class MetadataRetriever
|
||||||
|
{
|
||||||
|
/** @var ConfigManager */
|
||||||
|
protected $conf;
|
||||||
|
|
||||||
|
/** @var HttpAccess */
|
||||||
|
protected $httpAccess;
|
||||||
|
|
||||||
|
public function __construct(ConfigManager $conf, HttpAccess $httpAccess)
|
||||||
|
{
|
||||||
|
$this->conf = $conf;
|
||||||
|
$this->httpAccess = $httpAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve metadata for given URL.
|
||||||
|
*
|
||||||
|
* @return array [
|
||||||
|
* 'title' => <remote title>,
|
||||||
|
* 'description' => <remote description>,
|
||||||
|
* 'tags' => <remote keywords>,
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
public function retrieve(string $url): array
|
||||||
|
{
|
||||||
|
$charset = null;
|
||||||
|
$title = null;
|
||||||
|
$description = null;
|
||||||
|
$tags = null;
|
||||||
|
$retrieveDescription = $this->conf->get('general.retrieve_description');
|
||||||
|
|
||||||
|
// Short timeout to keep the application responsive
|
||||||
|
// The callback will fill $charset and $title with data from the downloaded page.
|
||||||
|
$this->httpAccess->getHttpResponse(
|
||||||
|
$url,
|
||||||
|
$this->conf->get('general.download_timeout', 30),
|
||||||
|
$this->conf->get('general.download_max_size', 4194304),
|
||||||
|
$this->httpAccess->getCurlHeaderCallback($charset),
|
||||||
|
$this->httpAccess->getCurlDownloadCallback(
|
||||||
|
$charset,
|
||||||
|
$title,
|
||||||
|
$description,
|
||||||
|
$tags,
|
||||||
|
$retrieveDescription
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($title) && strtolower($charset) !== 'utf-8') {
|
||||||
|
$title = mb_convert_encoding($title, 'utf-8', $charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $title,
|
||||||
|
'description' => $description,
|
||||||
|
'tags' => $tags,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
39
assets/common/js/metadata.js
Normal file
39
assets/common/js/metadata.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import he from 'he';
|
||||||
|
|
||||||
|
function clearLoaders(loaders) {
|
||||||
|
if (loaders != null && loaders.length > 0) {
|
||||||
|
[...loaders].forEach((loader) => {
|
||||||
|
loader.classList.remove('loading-input');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const loaders = document.querySelectorAll('.loading-input');
|
||||||
|
const inputTitle = document.querySelector('input[name="lf_title"]');
|
||||||
|
if (inputTitle != null && inputTitle.value.length > 0) {
|
||||||
|
clearLoaders(loaders);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = document.querySelector('input[name="lf_url"]').value;
|
||||||
|
const basePath = document.querySelector('input[name="js_base_path"]').value;
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
xhr.onload = () => {
|
||||||
|
const result = JSON.parse(xhr.response);
|
||||||
|
Object.keys(result).forEach((key) => {
|
||||||
|
if (result[key] !== null && result[key].length) {
|
||||||
|
const element = document.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`);
|
||||||
|
if (element != null && element.value.length === 0) {
|
||||||
|
element.value = he.decode(result[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clearLoaders(loaders);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send();
|
||||||
|
})();
|
|
@ -1,4 +1,5 @@
|
||||||
import Awesomplete from 'awesomplete';
|
import Awesomplete from 'awesomplete';
|
||||||
|
import he from 'he';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a parent element according to its tag and its attributes
|
* Find a parent element according to its tag and its attributes
|
||||||
|
@ -95,15 +96,6 @@ function updateAwesompleteList(selector, tags, instances) {
|
||||||
return instances;
|
return instances;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* html_entities in JS
|
|
||||||
*
|
|
||||||
* @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
|
|
||||||
*/
|
|
||||||
function htmlEntities(str) {
|
|
||||||
return str.replace(/[\u00A0-\u9999<>&]/gim, (i) => `&#${i.charCodeAt(0)};`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the class 'hidden' to city options not attached to the current selected continent.
|
* Add the class 'hidden' to city options not attached to the current selected continent.
|
||||||
*
|
*
|
||||||
|
@ -569,7 +561,7 @@ function init(description) {
|
||||||
input.setAttribute('name', totag);
|
input.setAttribute('name', totag);
|
||||||
input.setAttribute('value', totag);
|
input.setAttribute('value', totag);
|
||||||
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
|
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
|
||||||
block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
|
block.querySelector('a.tag-link').innerHTML = he.encode(totag);
|
||||||
block
|
block
|
||||||
.querySelector('a.tag-link')
|
.querySelector('a.tag-link')
|
||||||
.setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
|
.setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
|
||||||
|
|
|
@ -1273,6 +1273,57 @@ form {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-input {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@keyframes around {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 60px;
|
||||||
|
top: calc(50% - 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: relative;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
animation: around 5.4s infinite;
|
||||||
|
|
||||||
|
&::after,
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
background: $form-input-background;
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: #333 #333 transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
animation: around 0.7s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
animation: around 0.7s ease-in-out 0.1s infinite;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LOGIN
|
// LOGIN
|
||||||
.login-form-container {
|
.login-form-container {
|
||||||
.remember-me {
|
.remember-me {
|
||||||
|
|
|
@ -150,6 +150,7 @@ _These settings should not be edited_
|
||||||
- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).
|
- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).
|
||||||
- **enabled_plugins**: List of enabled plugins.
|
- **enabled_plugins**: List of enabled plugins.
|
||||||
- **default_note_title**: Default title of a new note.
|
- **default_note_title**: Default title of a new note.
|
||||||
|
- **enable_async_metadata** (boolean): Retrieve external bookmark metadata asynchronously to prevent bookmark creation slowdown.
|
||||||
- **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags.
|
- **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags.
|
||||||
- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
|
- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
$this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
|
$this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
|
||||||
$this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
|
$this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
|
||||||
$this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
|
$this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
|
||||||
|
$this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle');
|
||||||
$this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
|
$this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
|
||||||
})->add('\Shaarli\Front\ShaarliAdminMiddleware');
|
})->add('\Shaarli\Front\ShaarliAdminMiddleware');
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"awesomplete": "^1.1.2",
|
"awesomplete": "^1.1.2",
|
||||||
"blazy": "^1.8.2",
|
"blazy": "^1.8.2",
|
||||||
"fork-awesome": "^1.1.7",
|
"fork-awesome": "^1.1.7",
|
||||||
|
"he": "^1.2.0",
|
||||||
"pure-extras": "^1.0.0",
|
"pure-extras": "^1.0.0",
|
||||||
"purecss": "^1.0.0"
|
"purecss": "^1.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -216,60 +216,91 @@ public function testHtmlExtractNonExistentOgTag()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the download callback with valid value
|
* Test the header callback with valid value
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackOk()
|
public function testCurlHeaderCallbackOk(): void
|
||||||
{
|
{
|
||||||
$callback = get_curl_download_callback(
|
$callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ok');
|
||||||
$charset,
|
|
||||||
$title,
|
|
||||||
$desc,
|
|
||||||
$keywords,
|
|
||||||
false,
|
|
||||||
'ut_curl_getinfo_ok'
|
|
||||||
);
|
|
||||||
$data = [
|
$data = [
|
||||||
'HTTP/1.1 200 OK',
|
'HTTP/1.1 200 OK',
|
||||||
'Server: GitHub.com',
|
'Server: GitHub.com',
|
||||||
'Date: Sat, 28 Oct 2017 12:01:33 GMT',
|
'Date: Sat, 28 Oct 2017 12:01:33 GMT',
|
||||||
'Content-Type: text/html; charset=utf-8',
|
'Content-Type: text/html; charset=utf-8',
|
||||||
'Status: 200 OK',
|
'Status: 200 OK',
|
||||||
'end' => 'th=device-width">'
|
|
||||||
. '<title>Refactoring · GitHub</title>'
|
|
||||||
. '<link rel="search" type="application/opensea',
|
|
||||||
'<title>ignored</title>'
|
|
||||||
. '<meta name="description" content="desc" />'
|
|
||||||
. '<meta name="keywords" content="key1,key2" />',
|
|
||||||
];
|
];
|
||||||
foreach ($data as $key => $line) {
|
|
||||||
$ignore = null;
|
foreach ($data as $chunk) {
|
||||||
$expected = $key !== 'end' ? strlen($line) : false;
|
static::assertIsInt($callback(null, $chunk));
|
||||||
$this->assertEquals($expected, $callback($ignore, $line));
|
|
||||||
if ($expected === false) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
$this->assertEquals('utf-8', $charset);
|
static::assertSame('utf-8', $charset);
|
||||||
$this->assertEquals('Refactoring · GitHub', $title);
|
|
||||||
$this->assertEmpty($desc);
|
|
||||||
$this->assertEmpty($keywords);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the download callback with valid values and no charset
|
* Test the download callback with valid value
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackOkNoCharset()
|
public function testCurlDownloadCallbackOk(): void
|
||||||
{
|
{
|
||||||
|
$charset = 'utf-8';
|
||||||
$callback = get_curl_download_callback(
|
$callback = get_curl_download_callback(
|
||||||
$charset,
|
$charset,
|
||||||
$title,
|
$title,
|
||||||
$desc,
|
$desc,
|
||||||
$keywords,
|
$keywords,
|
||||||
false,
|
false
|
||||||
'ut_curl_getinfo_no_charset'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'th=device-width">'
|
||||||
|
. '<title>Refactoring · GitHub</title>'
|
||||||
|
. '<link rel="search" type="application/opensea',
|
||||||
|
'<title>ignored</title>'
|
||||||
|
. '<meta name="description" content="desc" />'
|
||||||
|
. '<meta name="keywords" content="key1,key2" />',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($data as $chunk) {
|
||||||
|
static::assertSame(strlen($chunk), $callback(null, $chunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
static::assertSame('utf-8', $charset);
|
||||||
|
static::assertSame('Refactoring · GitHub', $title);
|
||||||
|
static::assertEmpty($desc);
|
||||||
|
static::assertEmpty($keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the header callback with valid value
|
||||||
|
*/
|
||||||
|
public function testCurlHeaderCallbackNoCharset(): void
|
||||||
|
{
|
||||||
|
$callback = get_curl_header_callback($charset, 'ut_curl_getinfo_no_charset');
|
||||||
$data = [
|
$data = [
|
||||||
'HTTP/1.1 200 OK',
|
'HTTP/1.1 200 OK',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($data as $chunk) {
|
||||||
|
static::assertSame(strlen($chunk), $callback(null, $chunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
static::assertFalse($charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the download callback with valid values and no charset
|
||||||
|
*/
|
||||||
|
public function testCurlDownloadCallbackOkNoCharset(): void
|
||||||
|
{
|
||||||
|
$charset = null;
|
||||||
|
$callback = get_curl_download_callback(
|
||||||
|
$charset,
|
||||||
|
$title,
|
||||||
|
$desc,
|
||||||
|
$keywords,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = [
|
||||||
'end' => 'th=device-width">'
|
'end' => 'th=device-width">'
|
||||||
. '<title>Refactoring · GitHub</title>'
|
. '<title>Refactoring · GitHub</title>'
|
||||||
. '<link rel="search" type="application/opensea',
|
. '<link rel="search" type="application/opensea',
|
||||||
|
@ -277,10 +308,11 @@ public function testCurlDownloadCallbackOkNoCharset()
|
||||||
. '<meta name="description" content="desc" />'
|
. '<meta name="description" content="desc" />'
|
||||||
. '<meta name="keywords" content="key1,key2" />',
|
. '<meta name="keywords" content="key1,key2" />',
|
||||||
];
|
];
|
||||||
foreach ($data as $key => $line) {
|
|
||||||
$ignore = null;
|
foreach ($data as $chunk) {
|
||||||
$this->assertEquals(strlen($line), $callback($ignore, $line));
|
static::assertSame(strlen($chunk), $callback(null, $chunk));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertEmpty($charset);
|
$this->assertEmpty($charset);
|
||||||
$this->assertEquals('Refactoring · GitHub', $title);
|
$this->assertEquals('Refactoring · GitHub', $title);
|
||||||
$this->assertEmpty($desc);
|
$this->assertEmpty($desc);
|
||||||
|
@ -290,18 +322,18 @@ public function testCurlDownloadCallbackOkNoCharset()
|
||||||
/**
|
/**
|
||||||
* Test the download callback with valid values and no charset
|
* Test the download callback with valid values and no charset
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackOkHtmlCharset()
|
public function testCurlDownloadCallbackOkHtmlCharset(): void
|
||||||
{
|
{
|
||||||
|
$charset = null;
|
||||||
$callback = get_curl_download_callback(
|
$callback = get_curl_download_callback(
|
||||||
$charset,
|
$charset,
|
||||||
$title,
|
$title,
|
||||||
$desc,
|
$desc,
|
||||||
$keywords,
|
$keywords,
|
||||||
false,
|
false
|
||||||
'ut_curl_getinfo_no_charset'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'HTTP/1.1 200 OK',
|
|
||||||
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
|
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
|
||||||
'end' => 'th=device-width">'
|
'end' => 'th=device-width">'
|
||||||
. '<title>Refactoring · GitHub</title>'
|
. '<title>Refactoring · GitHub</title>'
|
||||||
|
@ -310,14 +342,10 @@ public function testCurlDownloadCallbackOkHtmlCharset()
|
||||||
. '<meta name="description" content="desc" />'
|
. '<meta name="description" content="desc" />'
|
||||||
. '<meta name="keywords" content="key1,key2" />',
|
. '<meta name="keywords" content="key1,key2" />',
|
||||||
];
|
];
|
||||||
foreach ($data as $key => $line) {
|
foreach ($data as $chunk) {
|
||||||
$ignore = null;
|
static::assertSame(strlen($chunk), $callback(null, $chunk));
|
||||||
$expected = $key !== 'end' ? strlen($line) : false;
|
|
||||||
$this->assertEquals($expected, $callback($ignore, $line));
|
|
||||||
if ($expected === false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertEquals('utf-8', $charset);
|
$this->assertEquals('utf-8', $charset);
|
||||||
$this->assertEquals('Refactoring · GitHub', $title);
|
$this->assertEquals('Refactoring · GitHub', $title);
|
||||||
$this->assertEmpty($desc);
|
$this->assertEmpty($desc);
|
||||||
|
@ -327,25 +355,26 @@ public function testCurlDownloadCallbackOkHtmlCharset()
|
||||||
/**
|
/**
|
||||||
* Test the download callback with valid values and no title
|
* Test the download callback with valid values and no title
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackOkNoTitle()
|
public function testCurlDownloadCallbackOkNoTitle(): void
|
||||||
{
|
{
|
||||||
|
$charset = 'utf-8';
|
||||||
$callback = get_curl_download_callback(
|
$callback = get_curl_download_callback(
|
||||||
$charset,
|
$charset,
|
||||||
$title,
|
$title,
|
||||||
$desc,
|
$desc,
|
||||||
$keywords,
|
$keywords,
|
||||||
false,
|
false
|
||||||
'ut_curl_getinfo_ok'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'HTTP/1.1 200 OK',
|
|
||||||
'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea',
|
'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea',
|
||||||
'ignored',
|
'ignored',
|
||||||
];
|
];
|
||||||
foreach ($data as $key => $line) {
|
|
||||||
$ignore = null;
|
foreach ($data as $chunk) {
|
||||||
$this->assertEquals(strlen($line), $callback($ignore, $line));
|
static::assertSame(strlen($chunk), $callback(null, $chunk));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertEquals('utf-8', $charset);
|
$this->assertEquals('utf-8', $charset);
|
||||||
$this->assertEmpty($title);
|
$this->assertEmpty($title);
|
||||||
$this->assertEmpty($desc);
|
$this->assertEmpty($desc);
|
||||||
|
@ -353,81 +382,55 @@ public function testCurlDownloadCallbackOkNoTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the download callback with an invalid content type.
|
* Test the header callback with an invalid content type.
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackInvalidContentType()
|
public function testCurlHeaderCallbackInvalidContentType(): void
|
||||||
{
|
{
|
||||||
$callback = get_curl_download_callback(
|
$callback = get_curl_header_callback($charset, 'ut_curl_getinfo_ct_ko');
|
||||||
$charset,
|
$data = [
|
||||||
$title,
|
'HTTP/1.1 200 OK',
|
||||||
$desc,
|
];
|
||||||
$keywords,
|
|
||||||
false,
|
static::assertFalse($callback(null, $data[0]));
|
||||||
'ut_curl_getinfo_ct_ko'
|
static::assertNull($charset);
|
||||||
);
|
|
||||||
$ignore = null;
|
|
||||||
$this->assertFalse($callback($ignore, ''));
|
|
||||||
$this->assertEmpty($charset);
|
|
||||||
$this->assertEmpty($title);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the download callback with an invalid response code.
|
* Test the header callback with an invalid response code.
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackInvalidResponseCode()
|
public function testCurlHeaderCallbackInvalidResponseCode(): void
|
||||||
{
|
{
|
||||||
$callback = $callback = get_curl_download_callback(
|
$callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rc_ko');
|
||||||
$charset,
|
|
||||||
$title,
|
static::assertFalse($callback(null, ''));
|
||||||
$desc,
|
static::assertNull($charset);
|
||||||
$keywords,
|
|
||||||
false,
|
|
||||||
'ut_curl_getinfo_rc_ko'
|
|
||||||
);
|
|
||||||
$ignore = null;
|
|
||||||
$this->assertFalse($callback($ignore, ''));
|
|
||||||
$this->assertEmpty($charset);
|
|
||||||
$this->assertEmpty($title);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the download callback with an invalid content type and response code.
|
* Test the header callback with an invalid content type and response code.
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackInvalidContentTypeAndResponseCode()
|
public function testCurlHeaderCallbackInvalidContentTypeAndResponseCode(): void
|
||||||
{
|
{
|
||||||
$callback = $callback = get_curl_download_callback(
|
$callback = get_curl_header_callback($charset, 'ut_curl_getinfo_rs_ct_ko');
|
||||||
$charset,
|
|
||||||
$title,
|
static::assertFalse($callback(null, ''));
|
||||||
$desc,
|
static::assertNull($charset);
|
||||||
$keywords,
|
|
||||||
false,
|
|
||||||
'ut_curl_getinfo_rs_ct_ko'
|
|
||||||
);
|
|
||||||
$ignore = null;
|
|
||||||
$this->assertFalse($callback($ignore, ''));
|
|
||||||
$this->assertEmpty($charset);
|
|
||||||
$this->assertEmpty($title);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the download callback with valid value, and retrieve_description option enabled.
|
* Test the download callback with valid value, and retrieve_description option enabled.
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackOkWithDesc()
|
public function testCurlDownloadCallbackOkWithDesc(): void
|
||||||
{
|
{
|
||||||
|
$charset = 'utf-8';
|
||||||
$callback = get_curl_download_callback(
|
$callback = get_curl_download_callback(
|
||||||
$charset,
|
$charset,
|
||||||
$title,
|
$title,
|
||||||
$desc,
|
$desc,
|
||||||
$keywords,
|
$keywords,
|
||||||
true,
|
true
|
||||||
'ut_curl_getinfo_ok'
|
|
||||||
);
|
);
|
||||||
$data = [
|
$data = [
|
||||||
'HTTP/1.1 200 OK',
|
|
||||||
'Server: GitHub.com',
|
|
||||||
'Date: Sat, 28 Oct 2017 12:01:33 GMT',
|
|
||||||
'Content-Type: text/html; charset=utf-8',
|
|
||||||
'Status: 200 OK',
|
|
||||||
'th=device-width">'
|
'th=device-width">'
|
||||||
. '<title>Refactoring · GitHub</title>'
|
. '<title>Refactoring · GitHub</title>'
|
||||||
. '<link rel="search" type="application/opensea',
|
. '<link rel="search" type="application/opensea',
|
||||||
|
@ -435,14 +438,11 @@ public function testCurlDownloadCallbackOkWithDesc()
|
||||||
. '<meta name="description" content="link desc" />'
|
. '<meta name="description" content="link desc" />'
|
||||||
. '<meta name="keywords" content="key1,key2" />',
|
. '<meta name="keywords" content="key1,key2" />',
|
||||||
];
|
];
|
||||||
foreach ($data as $key => $line) {
|
|
||||||
$ignore = null;
|
foreach ($data as $chunk) {
|
||||||
$expected = $key !== 'end' ? strlen($line) : false;
|
static::assertSame(strlen($chunk), $callback(null, $chunk));
|
||||||
$this->assertEquals($expected, $callback($ignore, $line));
|
|
||||||
if ($expected === false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertEquals('utf-8', $charset);
|
$this->assertEquals('utf-8', $charset);
|
||||||
$this->assertEquals('Refactoring · GitHub', $title);
|
$this->assertEquals('Refactoring · GitHub', $title);
|
||||||
$this->assertEquals('link desc', $desc);
|
$this->assertEquals('link desc', $desc);
|
||||||
|
@ -453,8 +453,9 @@ public function testCurlDownloadCallbackOkWithDesc()
|
||||||
* Test the download callback with valid value, and retrieve_description option enabled,
|
* Test the download callback with valid value, and retrieve_description option enabled,
|
||||||
* but no desc or keyword defined in the page.
|
* but no desc or keyword defined in the page.
|
||||||
*/
|
*/
|
||||||
public function testCurlDownloadCallbackOkWithDescNotFound()
|
public function testCurlDownloadCallbackOkWithDescNotFound(): void
|
||||||
{
|
{
|
||||||
|
$charset = 'utf-8';
|
||||||
$callback = get_curl_download_callback(
|
$callback = get_curl_download_callback(
|
||||||
$charset,
|
$charset,
|
||||||
$title,
|
$title,
|
||||||
|
@ -464,24 +465,16 @@ public function testCurlDownloadCallbackOkWithDescNotFound()
|
||||||
'ut_curl_getinfo_ok'
|
'ut_curl_getinfo_ok'
|
||||||
);
|
);
|
||||||
$data = [
|
$data = [
|
||||||
'HTTP/1.1 200 OK',
|
|
||||||
'Server: GitHub.com',
|
|
||||||
'Date: Sat, 28 Oct 2017 12:01:33 GMT',
|
|
||||||
'Content-Type: text/html; charset=utf-8',
|
|
||||||
'Status: 200 OK',
|
|
||||||
'th=device-width">'
|
'th=device-width">'
|
||||||
. '<title>Refactoring · GitHub</title>'
|
. '<title>Refactoring · GitHub</title>'
|
||||||
. '<link rel="search" type="application/opensea',
|
. '<link rel="search" type="application/opensea',
|
||||||
'end' => '<title>ignored</title>',
|
'end' => '<title>ignored</title>',
|
||||||
];
|
];
|
||||||
foreach ($data as $key => $line) {
|
|
||||||
$ignore = null;
|
foreach ($data as $chunk) {
|
||||||
$expected = $key !== 'end' ? strlen($line) : false;
|
static::assertSame(strlen($chunk), $callback(null, $chunk));
|
||||||
$this->assertEquals($expected, $callback($ignore, $line));
|
|
||||||
if ($expected === false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertEquals('utf-8', $charset);
|
$this->assertEquals('utf-8', $charset);
|
||||||
$this->assertEquals('Refactoring · GitHub', $title);
|
$this->assertEquals('Refactoring · GitHub', $title);
|
||||||
$this->assertEmpty($desc);
|
$this->assertEmpty($desc);
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
|
use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
|
||||||
use Shaarli\History;
|
use Shaarli\History;
|
||||||
use Shaarli\Http\HttpAccess;
|
use Shaarli\Http\HttpAccess;
|
||||||
|
use Shaarli\Http\MetadataRetriever;
|
||||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||||
use Shaarli\Plugin\PluginManager;
|
use Shaarli\Plugin\PluginManager;
|
||||||
use Shaarli\Render\PageBuilder;
|
use Shaarli\Render\PageBuilder;
|
||||||
|
@ -72,6 +73,7 @@ public function testBuildContainer(): void
|
||||||
static::assertInstanceOf(History::class, $container->history);
|
static::assertInstanceOf(History::class, $container->history);
|
||||||
static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
|
static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
|
||||||
static::assertInstanceOf(LoginManager::class, $container->loginManager);
|
static::assertInstanceOf(LoginManager::class, $container->loginManager);
|
||||||
|
static::assertInstanceOf(MetadataRetriever::class, $container->metadataRetriever);
|
||||||
static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
|
static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
|
||||||
static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
|
static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
|
||||||
static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager);
|
static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager);
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
|
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
|
||||||
use Shaarli\Front\Controller\Admin\ManageShaareController;
|
use Shaarli\Front\Controller\Admin\ManageShaareController;
|
||||||
use Shaarli\Http\HttpAccess;
|
use Shaarli\Http\HttpAccess;
|
||||||
|
use Shaarli\Http\MetadataRetriever;
|
||||||
use Shaarli\TestCase;
|
use Shaarli\TestCase;
|
||||||
use Slim\Http\Request;
|
use Slim\Http\Request;
|
||||||
use Slim\Http\Response;
|
use Slim\Http\Response;
|
||||||
|
@ -25,6 +26,7 @@ public function setUp(): void
|
||||||
$this->createContainer();
|
$this->createContainer();
|
||||||
|
|
||||||
$this->container->httpAccess = $this->createMock(HttpAccess::class);
|
$this->container->httpAccess = $this->createMock(HttpAccess::class);
|
||||||
|
$this->container->metadataRetriever = $this->createMock(MetadataRetriever::class);
|
||||||
$this->controller = new ManageShaareController($this->container);
|
$this->controller = new ManageShaareController($this->container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ public function setUp(): void
|
||||||
* Test displaying bookmark create form
|
* Test displaying bookmark create form
|
||||||
* Ensure that every step of the standard workflow works properly.
|
* Ensure that every step of the standard workflow works properly.
|
||||||
*/
|
*/
|
||||||
public function testDisplayCreateFormWithUrl(): void
|
public function testDisplayCreateFormWithUrlAndWithMetadataRetrieval(): void
|
||||||
{
|
{
|
||||||
$this->container->environment = [
|
$this->container->environment = [
|
||||||
'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
|
'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
|
||||||
|
@ -53,40 +55,20 @@ public function testDisplayCreateFormWithUrl(): void
|
||||||
});
|
});
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
|
|
||||||
$this->container->httpAccess
|
$this->container->conf = $this->createMock(ConfigManager::class);
|
||||||
->expects(static::once())
|
$this->container->conf->method('get')->willReturnCallback(function (string $param, $default) {
|
||||||
->method('getCurlDownloadCallback')
|
if ($param === 'general.enable_async_metadata') {
|
||||||
->willReturnCallback(
|
return false;
|
||||||
function (&$charset, &$title, &$description, &$tags) use (
|
|
||||||
$remoteTitle,
|
|
||||||
$remoteDesc,
|
|
||||||
$remoteTags
|
|
||||||
): callable {
|
|
||||||
return function () use (
|
|
||||||
&$charset,
|
|
||||||
&$title,
|
|
||||||
&$description,
|
|
||||||
&$tags,
|
|
||||||
$remoteTitle,
|
|
||||||
$remoteDesc,
|
|
||||||
$remoteTags
|
|
||||||
): void {
|
|
||||||
$charset = 'ISO-8859-1';
|
|
||||||
$title = $remoteTitle;
|
|
||||||
$description = $remoteDesc;
|
|
||||||
$tags = $remoteTags;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
;
|
return $default;
|
||||||
$this->container->httpAccess
|
});
|
||||||
->expects(static::once())
|
|
||||||
->method('getHttpResponse')
|
$this->container->metadataRetriever->expects(static::once())->method('retrieve')->willReturn([
|
||||||
->with($expectedUrl, 30, 4194304)
|
'title' => $remoteTitle,
|
||||||
->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
|
'description' => $remoteDesc,
|
||||||
$callback();
|
'tags' => $remoteTags,
|
||||||
})
|
]);
|
||||||
;
|
|
||||||
|
|
||||||
$this->container->bookmarkService
|
$this->container->bookmarkService
|
||||||
->expects(static::once())
|
->expects(static::once())
|
||||||
|
@ -127,6 +109,72 @@ function (&$charset, &$title, &$description, &$tags) use (
|
||||||
static::assertSame($tags, $assignedVariables['tags']);
|
static::assertSame($tags, $assignedVariables['tags']);
|
||||||
static::assertArrayHasKey('source', $assignedVariables);
|
static::assertArrayHasKey('source', $assignedVariables);
|
||||||
static::assertArrayHasKey('default_private_links', $assignedVariables);
|
static::assertArrayHasKey('default_private_links', $assignedVariables);
|
||||||
|
static::assertArrayHasKey('async_metadata', $assignedVariables);
|
||||||
|
static::assertArrayHasKey('retrieve_description', $assignedVariables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test displaying bookmark create form without any external metadata retrieval attempt
|
||||||
|
*/
|
||||||
|
public function testDisplayCreateFormWithUrlAndWithoutMetadata(): void
|
||||||
|
{
|
||||||
|
$this->container->environment = [
|
||||||
|
'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
|
||||||
|
];
|
||||||
|
|
||||||
|
$assignedVariables = [];
|
||||||
|
$this->assignTemplateVars($assignedVariables);
|
||||||
|
|
||||||
|
$url = 'http://url.tld/other?part=3&utm_ad=pay#hash';
|
||||||
|
$expectedUrl = str_replace('&utm_ad=pay', '', $url);
|
||||||
|
|
||||||
|
$request = $this->createMock(Request::class);
|
||||||
|
$request->method('getParam')->willReturnCallback(function (string $key) use ($url): ?string {
|
||||||
|
return $key === 'post' ? $url : null;
|
||||||
|
});
|
||||||
|
$response = new Response();
|
||||||
|
|
||||||
|
$this->container->metadataRetriever->expects(static::never())->method('retrieve');
|
||||||
|
|
||||||
|
$this->container->bookmarkService
|
||||||
|
->expects(static::once())
|
||||||
|
->method('bookmarksCountPerTag')
|
||||||
|
->willReturn($tags = ['tag1' => 2, 'tag2' => 1])
|
||||||
|
;
|
||||||
|
|
||||||
|
// Make sure that PluginManager hook is triggered
|
||||||
|
$this->container->pluginManager
|
||||||
|
->expects(static::at(0))
|
||||||
|
->method('executeHooks')
|
||||||
|
->willReturnCallback(function (string $hook, array $data): array {
|
||||||
|
static::assertSame('render_editlink', $hook);
|
||||||
|
static::assertSame('', $data['link']['title']);
|
||||||
|
static::assertSame('', $data['link']['description']);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
})
|
||||||
|
;
|
||||||
|
|
||||||
|
$result = $this->controller->displayCreateForm($request, $response);
|
||||||
|
|
||||||
|
static::assertSame(200, $result->getStatusCode());
|
||||||
|
static::assertSame('editlink', (string) $result->getBody());
|
||||||
|
|
||||||
|
static::assertSame('Shaare - Shaarli', $assignedVariables['pagetitle']);
|
||||||
|
|
||||||
|
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
|
||||||
|
static::assertSame('', $assignedVariables['link']['title']);
|
||||||
|
static::assertSame('', $assignedVariables['link']['description']);
|
||||||
|
static::assertSame('', $assignedVariables['link']['tags']);
|
||||||
|
static::assertFalse($assignedVariables['link']['private']);
|
||||||
|
|
||||||
|
static::assertTrue($assignedVariables['link_is_new']);
|
||||||
|
static::assertSame($referer, $assignedVariables['http_referer']);
|
||||||
|
static::assertSame($tags, $assignedVariables['tags']);
|
||||||
|
static::assertArrayHasKey('source', $assignedVariables);
|
||||||
|
static::assertArrayHasKey('default_private_links', $assignedVariables);
|
||||||
|
static::assertArrayHasKey('async_metadata', $assignedVariables);
|
||||||
|
static::assertArrayHasKey('retrieve_description', $assignedVariables);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
154
tests/http/MetadataRetrieverTest.php
Normal file
154
tests/http/MetadataRetrieverTest.php
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Http;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shaarli\Config\ConfigManager;
|
||||||
|
|
||||||
|
class MetadataRetrieverTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var MetadataRetriever */
|
||||||
|
protected $retriever;
|
||||||
|
|
||||||
|
/** @var ConfigManager */
|
||||||
|
protected $conf;
|
||||||
|
|
||||||
|
/** @var HttpAccess */
|
||||||
|
protected $httpAccess;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->conf = $this->createMock(ConfigManager::class);
|
||||||
|
$this->httpAccess = $this->createMock(HttpAccess::class);
|
||||||
|
$this->retriever = new MetadataRetriever($this->conf, $this->httpAccess);
|
||||||
|
|
||||||
|
$this->conf->method('get')->willReturnCallback(function (string $param, $default) {
|
||||||
|
return $default === null ? $param : $default;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test metadata retrieve() with values returned
|
||||||
|
*/
|
||||||
|
public function testFullRetrieval(): void
|
||||||
|
{
|
||||||
|
$url = 'https://domain.tld/link';
|
||||||
|
$remoteTitle = 'Remote Title ';
|
||||||
|
$remoteDesc = 'Sometimes the meta description is relevant.';
|
||||||
|
$remoteTags = 'abc def';
|
||||||
|
$remoteCharset = 'utf-8';
|
||||||
|
|
||||||
|
$expectedResult = [
|
||||||
|
'title' => $remoteTitle,
|
||||||
|
'description' => $remoteDesc,
|
||||||
|
'tags' => $remoteTags,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->httpAccess
|
||||||
|
->expects(static::once())
|
||||||
|
->method('getCurlHeaderCallback')
|
||||||
|
->willReturnCallback(
|
||||||
|
function (&$charset) use (
|
||||||
|
$remoteCharset
|
||||||
|
): callable {
|
||||||
|
return function () use (
|
||||||
|
&$charset,
|
||||||
|
$remoteCharset
|
||||||
|
): void {
|
||||||
|
$charset = $remoteCharset;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
$this->httpAccess
|
||||||
|
->expects(static::once())
|
||||||
|
->method('getCurlDownloadCallback')
|
||||||
|
->willReturnCallback(
|
||||||
|
function (&$charset, &$title, &$description, &$tags) use (
|
||||||
|
$remoteCharset,
|
||||||
|
$remoteTitle,
|
||||||
|
$remoteDesc,
|
||||||
|
$remoteTags
|
||||||
|
): callable {
|
||||||
|
return function () use (
|
||||||
|
&$charset,
|
||||||
|
&$title,
|
||||||
|
&$description,
|
||||||
|
&$tags,
|
||||||
|
$remoteCharset,
|
||||||
|
$remoteTitle,
|
||||||
|
$remoteDesc,
|
||||||
|
$remoteTags
|
||||||
|
): void {
|
||||||
|
static::assertSame($remoteCharset, $charset);
|
||||||
|
|
||||||
|
$title = $remoteTitle;
|
||||||
|
$description = $remoteDesc;
|
||||||
|
$tags = $remoteTags;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
$this->httpAccess
|
||||||
|
->expects(static::once())
|
||||||
|
->method('getHttpResponse')
|
||||||
|
->with($url, 30, 4194304)
|
||||||
|
->willReturnCallback(function($url, $timeout, $maxBytes, $headerCallback, $dlCallback): void {
|
||||||
|
$headerCallback();
|
||||||
|
$dlCallback();
|
||||||
|
})
|
||||||
|
;
|
||||||
|
|
||||||
|
$result = $this->retriever->retrieve($url);
|
||||||
|
|
||||||
|
static::assertSame($expectedResult, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test metadata retrieve() without any value
|
||||||
|
*/
|
||||||
|
public function testEmptyRetrieval(): void
|
||||||
|
{
|
||||||
|
$url = 'https://domain.tld/link';
|
||||||
|
|
||||||
|
$expectedResult = [
|
||||||
|
'title' => null,
|
||||||
|
'description' => null,
|
||||||
|
'tags' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->httpAccess
|
||||||
|
->expects(static::once())
|
||||||
|
->method('getCurlDownloadCallback')
|
||||||
|
->willReturnCallback(
|
||||||
|
function (): callable {
|
||||||
|
return function (): void {};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
$this->httpAccess
|
||||||
|
->expects(static::once())
|
||||||
|
->method('getCurlHeaderCallback')
|
||||||
|
->willReturnCallback(
|
||||||
|
function (): callable {
|
||||||
|
return function (): void {};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
;
|
||||||
|
$this->httpAccess
|
||||||
|
->expects(static::once())
|
||||||
|
->method('getHttpResponse')
|
||||||
|
->with($url, 30, 4194304)
|
||||||
|
->willReturnCallback(function($url, $timeout, $maxBytes, $headerCallback, $dlCallback): void {
|
||||||
|
$headerCallback();
|
||||||
|
$dlCallback();
|
||||||
|
})
|
||||||
|
;
|
||||||
|
|
||||||
|
$result = $this->retriever->retrieve($url);
|
||||||
|
|
||||||
|
static::assertSame($expectedResult, $result);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,8 @@
|
||||||
action="{$base_path}/admin/shaare"
|
action="{$base_path}/admin/shaare"
|
||||||
class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"
|
class="page-form pure-u-lg-3-5 pure-u-22-24 page-form page-form-light"
|
||||||
>
|
>
|
||||||
|
{$asyncLoadClass=$link_is_new && $async_metadata && empty($link.title) ? 'loading-input' : ''}
|
||||||
|
|
||||||
<h2 class="window-title">
|
<h2 class="window-title">
|
||||||
{if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
|
{if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -28,21 +30,32 @@ <h2 class="window-title">
|
||||||
<div>
|
<div>
|
||||||
<label for="lf_title">{'Title'|t}</label>
|
<label for="lf_title">{'Title'|t}</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="{$asyncLoadClass}">
|
||||||
<input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input autofocus">
|
<input type="text" name="lf_title" id="lf_title" value="{$link.title}"
|
||||||
|
class="lf_input {if="!$async_metadata"}autofocus{/if}"
|
||||||
|
>
|
||||||
|
<div class="icon-container">
|
||||||
|
<i class="loader"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="lf_description">{'Description'|t}</label>
|
<label for="lf_description">{'Description'|t}</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}">
|
||||||
<textarea name="lf_description" id="lf_description" class="autofocus">{$link.description}</textarea>
|
<textarea name="lf_description" id="lf_description" class="autofocus">{$link.description}</textarea>
|
||||||
|
<div class="icon-container">
|
||||||
|
<i class="loader"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="lf_tags">{'Tags'|t}</label>
|
<label for="lf_tags">{'Tags'|t}</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}">
|
||||||
<input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input autofocus"
|
<input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input autofocus"
|
||||||
data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" >
|
data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" >
|
||||||
|
<div class="icon-container">
|
||||||
|
<i class="loader"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -88,5 +101,6 @@ <h2 class="window-title">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{include="page.footer"}
|
{include="page.footer"}
|
||||||
|
{if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -20,6 +20,7 @@ module.exports = [
|
||||||
entry: {
|
entry: {
|
||||||
thumbnails: './assets/common/js/thumbnails.js',
|
thumbnails: './assets/common/js/thumbnails.js',
|
||||||
thumbnails_update: './assets/common/js/thumbnails-update.js',
|
thumbnails_update: './assets/common/js/thumbnails-update.js',
|
||||||
|
metadata: './assets/common/js/metadata.js',
|
||||||
pluginsadmin: './assets/default/js/plugins-admin.js',
|
pluginsadmin: './assets/default/js/plugins-admin.js',
|
||||||
shaarli: [
|
shaarli: [
|
||||||
'./assets/default/js/base.js',
|
'./assets/default/js/base.js',
|
||||||
|
@ -99,6 +100,7 @@ module.exports = [
|
||||||
].concat(glob.sync('./assets/vintage/img/*')),
|
].concat(glob.sync('./assets/vintage/img/*')),
|
||||||
markdown: './assets/common/css/markdown.css',
|
markdown: './assets/common/css/markdown.css',
|
||||||
thumbnails: './assets/common/js/thumbnails.js',
|
thumbnails: './assets/common/js/thumbnails.js',
|
||||||
|
metadata: './assets/common/js/metadata.js',
|
||||||
thumbnails_update: './assets/common/js/thumbnails-update.js',
|
thumbnails_update: './assets/common/js/thumbnails-update.js',
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|
|
@ -2912,6 +2912,11 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
minimalistic-assert "^1.0.1"
|
minimalistic-assert "^1.0.1"
|
||||||
|
|
||||||
|
he@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||||
|
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||||
|
|
||||||
hmac-drbg@^1.0.0:
|
hmac-drbg@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||||
|
|
Loading…
Reference in a new issue