Add a setting to retrieve bookmark metadata asynchrounously
- There is a new standalone script (metadata.js) which requests a new controller to get bookmark metadata and fill the form async - This feature is enabled with the new setting: general.enable_async_metadata (enabled by default) - general.retrieve_description is now enabled by default - A small rotating loader animation has a been added to bookmark inputs when metadata is being retrieved (default template) - Custom JS htmlentities has been removed and mathiasbynens/he library is used instead Fixes #1563
This commit is contained in:
parent
f34554c6c2
commit
4cf3564d28
19 changed files with 447 additions and 75 deletions
1
Makefile
1
Makefile
|
@ -175,6 +175,7 @@ translate:
|
|||
eslint:
|
||||
@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/common/js/
|
||||
|
||||
### Run CSSLint check against Shaarli's SCSS files
|
||||
sasslint:
|
||||
|
|
|
@ -366,7 +366,8 @@ class ConfigManager
|
|||
$this->setEmpty('general.links_per_page', 20);
|
||||
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
|
||||
$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_branch', 'stable');
|
||||
|
|
|
@ -14,6 +14,7 @@ use Shaarli\Front\Controller\Visitor\ErrorController;
|
|||
use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
|
||||
use Shaarli\History;
|
||||
use Shaarli\Http\HttpAccess;
|
||||
use Shaarli\Http\MetadataRetriever;
|
||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Render\PageBuilder;
|
||||
|
@ -90,6 +91,10 @@ class ContainerBuilder
|
|||
);
|
||||
};
|
||||
|
||||
$container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever {
|
||||
return new MetadataRetriever($container->conf, $container->httpAccess);
|
||||
};
|
||||
|
||||
$container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
|
||||
return new PageBuilder(
|
||||
$container->conf,
|
||||
|
|
|
@ -10,6 +10,7 @@ use Shaarli\Feed\FeedBuilder;
|
|||
use Shaarli\Formatter\FormatterFactory;
|
||||
use Shaarli\History;
|
||||
use Shaarli\Http\HttpAccess;
|
||||
use Shaarli\Http\MetadataRetriever;
|
||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Render\PageBuilder;
|
||||
|
@ -35,6 +36,7 @@ use Slim\Container;
|
|||
* @property History $history
|
||||
* @property HttpAccess $httpAccess
|
||||
* @property LoginManager $loginManager
|
||||
* @property MetadataRetriever $metadataRetriever
|
||||
* @property NetscapeBookmarkUtils $netscapeBookmarkUtils
|
||||
* @property callable $notFoundHandler Overrides default Slim exception display
|
||||
* @property PageBuilder $pageBuilder
|
||||
|
|
|
@ -53,36 +53,22 @@ class ManageShaareController extends ShaarliAdminController
|
|||
|
||||
// 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.)
|
||||
if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
|
||||
$retrieveDescription = $this->container->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->container->httpAccess->getHttpResponse(
|
||||
$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 (true !== $this->container->conf->get('general.enable_async_metadata', true)
|
||||
&& empty($title)
|
||||
&& strpos(get_url_scheme($url) ?: '', 'http') !== false
|
||||
) {
|
||||
$metadata = $this->container->metadataRetriever->retrieve($url);
|
||||
}
|
||||
|
||||
if (empty($url) && empty($title)) {
|
||||
$title = $this->container->conf->get('general.default_note_title', t('Note: '));
|
||||
if (empty($url)) {
|
||||
$metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: '));
|
||||
}
|
||||
|
||||
$link = [
|
||||
'title' => $title,
|
||||
'title' => $title ?? $metadata['title'] ?? '',
|
||||
'url' => $url ?? '',
|
||||
'description' => $description ?? '',
|
||||
'tags' => $tags ?? '',
|
||||
'description' => $description ?? $metadata['description'] ?? '',
|
||||
'tags' => $tags ?? $metadata['tags'] ?? '',
|
||||
'private' => $private,
|
||||
];
|
||||
} else {
|
||||
|
@ -352,6 +338,8 @@ class ManageShaareController extends ShaarliAdminController
|
|||
'source' => $request->getParam('source') ?? '',
|
||||
'tags' => $tags,
|
||||
'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);
|
||||
|
|
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([]);
|
||||
}
|
||||
}
|
68
application/http/MetadataRetriever.php
Normal file
68
application/http/MetadataRetriever.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?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->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 he from 'he';
|
||||
|
||||
/**
|
||||
* Find a parent element according to its tag and its attributes
|
||||
|
@ -95,15 +96,6 @@ function updateAwesompleteList(selector, tags, 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.
|
||||
*
|
||||
|
@ -569,7 +561,7 @@ function init(description) {
|
|||
input.setAttribute('name', totag);
|
||||
input.setAttribute('value', totag);
|
||||
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
|
||||
.querySelector('a.tag-link')
|
||||
.setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
|
||||
|
|
|
@ -1269,6 +1269,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-form-container {
|
||||
.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).
|
||||
- **enabled_plugins**: List of enabled plugins.
|
||||
- **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.
|
||||
- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@ $app->group('/admin', function () {
|
|||
$this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
|
||||
$this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
|
||||
$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');
|
||||
})->add('\Shaarli\Front\ShaarliAdminMiddleware');
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"awesomplete": "^1.1.2",
|
||||
"blazy": "^1.8.2",
|
||||
"fork-awesome": "^1.1.7",
|
||||
"he": "^1.2.0",
|
||||
"pure-extras": "^1.0.0",
|
||||
"purecss": "^1.0.0"
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ use Shaarli\Front\Controller\Visitor\ErrorController;
|
|||
use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
|
||||
use Shaarli\History;
|
||||
use Shaarli\Http\HttpAccess;
|
||||
use Shaarli\Http\MetadataRetriever;
|
||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||
use Shaarli\Plugin\PluginManager;
|
||||
use Shaarli\Render\PageBuilder;
|
||||
|
@ -72,6 +73,7 @@ class ContainerBuilderTest extends TestCase
|
|||
static::assertInstanceOf(History::class, $container->history);
|
||||
static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
|
||||
static::assertInstanceOf(LoginManager::class, $container->loginManager);
|
||||
static::assertInstanceOf(MetadataRetriever::class, $container->metadataRetriever);
|
||||
static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
|
||||
static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
|
||||
static::assertInstanceOf(PageCacheManager::class, $container->pageCacheManager);
|
||||
|
|
|
@ -9,6 +9,7 @@ use Shaarli\Config\ConfigManager;
|
|||
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
|
||||
use Shaarli\Front\Controller\Admin\ManageShaareController;
|
||||
use Shaarli\Http\HttpAccess;
|
||||
use Shaarli\Http\MetadataRetriever;
|
||||
use Shaarli\TestCase;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
@ -25,6 +26,7 @@ class DisplayCreateFormTest extends TestCase
|
|||
$this->createContainer();
|
||||
|
||||
$this->container->httpAccess = $this->createMock(HttpAccess::class);
|
||||
$this->container->metadataRetriever = $this->createMock(MetadataRetriever::class);
|
||||
$this->controller = new ManageShaareController($this->container);
|
||||
}
|
||||
|
||||
|
@ -32,7 +34,7 @@ class DisplayCreateFormTest extends TestCase
|
|||
* Test displaying bookmark create form
|
||||
* Ensure that every step of the standard workflow works properly.
|
||||
*/
|
||||
public function testDisplayCreateFormWithUrl(): void
|
||||
public function testDisplayCreateFormWithUrlAndWithMetadataRetrieval(): void
|
||||
{
|
||||
$this->container->environment = [
|
||||
'HTTP_REFERER' => $referer = 'http://shaarli/subfolder/controller/?searchtag=abc'
|
||||
|
@ -53,40 +55,20 @@ class DisplayCreateFormTest extends TestCase
|
|||
});
|
||||
$response = new Response();
|
||||
|
||||
$this->container->httpAccess
|
||||
->expects(static::once())
|
||||
->method('getCurlDownloadCallback')
|
||||
->willReturnCallback(
|
||||
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;
|
||||
};
|
||||
}
|
||||
)
|
||||
;
|
||||
$this->container->httpAccess
|
||||
->expects(static::once())
|
||||
->method('getHttpResponse')
|
||||
->with($expectedUrl, 30, 4194304)
|
||||
->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
|
||||
$callback();
|
||||
})
|
||||
;
|
||||
$this->container->conf = $this->createMock(ConfigManager::class);
|
||||
$this->container->conf->method('get')->willReturnCallback(function (string $param, $default) {
|
||||
if ($param === 'general.enable_async_metadata') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $default;
|
||||
});
|
||||
|
||||
$this->container->metadataRetriever->expects(static::once())->method('retrieve')->willReturn([
|
||||
'title' => $remoteTitle,
|
||||
'description' => $remoteDesc,
|
||||
'tags' => $remoteTags,
|
||||
]);
|
||||
|
||||
$this->container->bookmarkService
|
||||
->expects(static::once())
|
||||
|
@ -127,6 +109,72 @@ class DisplayCreateFormTest extends TestCase
|
|||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
123
tests/http/MetadataRetrieverTest.php
Normal file
123
tests/http/MetadataRetrieverTest.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?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';
|
||||
|
||||
$expectedResult = [
|
||||
'title' => $remoteTitle,
|
||||
'description' => $remoteDesc,
|
||||
'tags' => $remoteTags,
|
||||
];
|
||||
|
||||
$this->httpAccess
|
||||
->expects(static::once())
|
||||
->method('getCurlDownloadCallback')
|
||||
->willReturnCallback(
|
||||
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;
|
||||
};
|
||||
}
|
||||
)
|
||||
;
|
||||
$this->httpAccess
|
||||
->expects(static::once())
|
||||
->method('getHttpResponse')
|
||||
->with($url, 30, 4194304)
|
||||
->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
|
||||
$callback();
|
||||
})
|
||||
;
|
||||
|
||||
$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 (&$charset, &$title, &$description, &$tags): callable {
|
||||
return function () use (&$charset, &$title, &$description, &$tags): void {};
|
||||
}
|
||||
)
|
||||
;
|
||||
$this->httpAccess
|
||||
->expects(static::once())
|
||||
->method('getHttpResponse')
|
||||
->with($url, 30, 4194304)
|
||||
->willReturnCallback(function($url, $timeout, $maxBytes, $callback): void {
|
||||
$callback();
|
||||
})
|
||||
;
|
||||
|
||||
$result = $this->retriever->retrieve($url);
|
||||
|
||||
static::assertSame($expectedResult, $result);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@
|
|||
action="{$base_path}/admin/shaare"
|
||||
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">
|
||||
{if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
|
||||
</h2>
|
||||
|
@ -28,21 +30,32 @@
|
|||
<div>
|
||||
<label for="lf_title">{'Title'|t}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input autofocus">
|
||||
<div class="{$asyncLoadClass}">
|
||||
<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>
|
||||
<label for="lf_description">{'Description'|t}</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}">
|
||||
<textarea name="lf_description" id="lf_description" class="autofocus">{$link.description}</textarea>
|
||||
<div class="icon-container">
|
||||
<i class="loader"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lf_tags">{'Tags'|t}</label>
|
||||
</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"
|
||||
data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" >
|
||||
<div class="icon-container">
|
||||
<i class="loader"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -88,5 +101,6 @@
|
|||
</form>
|
||||
</div>
|
||||
{include="page.footer"}
|
||||
{if="$link_is_new && $async_metadata"}<script src="{$asset_path}/js/metadata.min.js?v={$version_hash}#"></script>{/if}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -20,6 +20,7 @@ module.exports = [
|
|||
entry: {
|
||||
thumbnails: './assets/common/js/thumbnails.js',
|
||||
thumbnails_update: './assets/common/js/thumbnails-update.js',
|
||||
metadata: './assets/common/js/metadata.js',
|
||||
pluginsadmin: './assets/default/js/plugins-admin.js',
|
||||
shaarli: [
|
||||
'./assets/default/js/base.js',
|
||||
|
@ -99,6 +100,7 @@ module.exports = [
|
|||
].concat(glob.sync('./assets/vintage/img/*')),
|
||||
markdown: './assets/common/css/markdown.css',
|
||||
thumbnails: './assets/common/js/thumbnails.js',
|
||||
metadata: './assets/common/js/metadata.js',
|
||||
thumbnails_update: './assets/common/js/thumbnails-update.js',
|
||||
},
|
||||
output: {
|
||||
|
|
|
@ -2912,6 +2912,11 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
|||
inherits "^2.0.3"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
|
|
Loading…
Reference in a new issue