Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/api-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ jobs:
check-code: false
force: ${{ matrix.experimental }}

- name: Allow local remote servers
working-directory: ../server
run: ./occ config:system:set allow_local_remote_servers --type=boolean --value=true

- name: Run API tests
working-directory: ../server
run: |
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/updater-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ jobs:
check-code: false
force: ${{ matrix.experimental }}

- name: Allow local remote servers
working-directory: ../server
run: ./occ config:system:set allow_local_remote_servers --type=boolean --value=true

- name: Install composer install php-feed-generator
working-directory: ../server
run: composer install -d apps/news/tests/test_helper/php-feed-generator
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# Unreleased
### Changed

- Refactor FaviconDataAccess to use Nextclouds IClientService (#3671)

Check failure on line 9 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Nextclouds'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Nextclouds'?", "location": {"path": "CHANGELOG.md", "range": {"start": {"line": 9, "column": 37}}}, "severity": "ERROR"}
### Fixed

# Releases
Expand Down
3 changes: 2 additions & 1 deletion lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use OCP\Config\BeforePreferenceDeletedEvent;
use OCP\Config\BeforePreferenceSetEvent;
use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\Http\Client\IClientService;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

Expand Down Expand Up @@ -122,7 +123,7 @@ public function register(IRegistrationContext $context): void

$context->registerService(FaviconDataAccess::class, function (ContainerInterface $c): FaviconDataAccess {
$config = $c->get(FetcherConfig::class);
return new FaviconDataAccess($config);
return new FaviconDataAccess($config, $c->get(IClientService::class), $c->get(LoggerInterface::class));
});

$context->registerService(Favicon::class, function (ContainerInterface $c): Favicon {
Expand Down
71 changes: 47 additions & 24 deletions lib/Fetcher/FaviconDataAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,72 @@

use OCA\News\Vendor\Favicon\DataAccess;

use OCP\Http\Client\IClientService;

use OCA\News\Config\FetcherConfig;
use Psr\Log\LoggerInterface;

/**
* Modified version of DataAccess with a configurable user agent header.
*/
class FaviconDataAccess extends DataAccess
{
/**
* @var FetcherConfig
*/
private $fetcherConfig;

public function __construct(
FetcherConfig $fetcherConfig,
private readonly FetcherConfig $fetcherConfig,
private readonly IClientService $clientService,
private readonly LoggerInterface $logger
) {
$this->fetcherConfig = $fetcherConfig;
}

public function retrieveUrl($url)
{
$this->setContext();
return @file_get_contents($url);
try {
$response = $this->clientService->newClient()->get(
$url,
$this->getRequestOptions()
);
return $response->getBody();
} catch (\Throwable $e) {
$this->logger->warning(
'Could not fetch favicon URL {url}: {error}',
['url' => $url, 'error' => $e->getMessage()]
);
return false;
}
}

public function retrieveHeader($url)
{
$this->setContext();
$headers = @get_headers($url, 1);
return is_array($headers) ? array_change_key_case($headers) : [];
try {
$response = $this->clientService->newClient()->head(
$url,
$this->getRequestOptions()
);
$statusCode = $response->getStatusCode();
$headers = array_change_key_case($response->getHeaders());
// The Favicon library expects $headers[0] to be the HTTP status line,
// matching the format returned by PHP's get_headers().
// Prepend it to ensure correct ordering.
$headers = [0 => 'HTTP/1.1 ' . $statusCode] + $headers;
return $headers;
} catch (\Throwable $e) {
$this->logger->warning(
'Could not fetch favicon headers for {url}: {error}',
['url' => $url, 'error' => $e->getMessage()]
);
return [];
}
}

private function setContext()
private function getRequestOptions(): array
{
stream_context_set_default(
[
'http' => [
'method' => 'GET',
'follow_location' => 0,
'max_redirects' => 1,
'timeout' => 10,
'header' => 'User-Agent: ' . $this->fetcherConfig->getUserAgent(),
]
]
);
return [
'timeout' => 10,
'allow_redirects' => false,
'http_errors' => false,
'headers' => [
'User-Agent' => $this->fetcherConfig->getUserAgent(),
],
];
}
}
156 changes: 156 additions & 0 deletions tests/Unit/Fetcher/FaviconDataAccessTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

namespace OCA\News\Tests\Unit\Fetcher;

use OCA\News\Config\FetcherConfig;
use OCA\News\Fetcher\FaviconDataAccess;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class FaviconDataAccessTest extends TestCase
{
/** @var FetcherConfig|MockObject */
private $fetcherConfig;

/** @var IClientService|MockObject */
private $clientService;

/** @var IClient|MockObject */
private $client;

/** @var LoggerInterface|MockObject */
private $logger;

/** @var FaviconDataAccess */
private $class;

protected function setUp(): void
{
$this->fetcherConfig = $this->createMock(FetcherConfig::class);
$this->clientService = $this->createMock(IClientService::class);
$this->client = $this->createMock(IClient::class);
$this->logger = $this->createMock(LoggerInterface::class);

$this->clientService->method('newClient')
->willReturn($this->client);

$this->class = new FaviconDataAccess($this->fetcherConfig, $this->clientService, $this->logger);
}

public function testRetrieveUrlReturnsBodyOnSuccess(): void
{
$url = 'https://example.com/favicon.ico';
$userAgent = 'NextCloud-News/25.0.0';

$this->fetcherConfig->expects($this->once())
->method('getUserAgent')
->willReturn($userAgent);

$response = $this->createMock(IResponse::class);
$response->expects($this->once())
->method('getBody')
->willReturn('favicon-body');

$this->client->expects($this->once())
->method('get')
->with($url, [
'timeout' => 10,
'allow_redirects' => false,
'http_errors' => false,
'headers' => [
'User-Agent' => $userAgent,
],
])
->willReturn($response);

$this->assertSame('favicon-body', $this->class->retrieveUrl($url));
}

public function testRetrieveUrlReturnsFalseOnClientError(): void
{
$url = 'https://example.com/favicon.ico';

$this->fetcherConfig->expects($this->once())
->method('getUserAgent')
->willReturn('NextCloud-News/25.0.0');

$this->client->expects($this->once())
->method('get')
->willThrowException(new \Exception('request failed'));

$this->logger->expects($this->once())
->method('warning');

$this->assertFalse($this->class->retrieveUrl($url));
}

public function testRetrieveHeaderReturnsLowercaseHeadersOnSuccess(): void
{
$url = 'https://example.com/favicon.ico';
$userAgent = 'NextCloud-News/25.0.0';

$this->fetcherConfig->expects($this->once())
->method('getUserAgent')
->willReturn($userAgent);

$response = $this->createMock(IResponse::class);
$response->expects($this->once())
->method('getHeaders')
->willReturn([
'Content-Type' => ['image/x-icon'],
'X-Test' => ['1'],
]);
$response->expects($this->once())
->method('getStatusCode')
->willReturn(200);

$this->client->expects($this->once())
->method('head')
->with($url, [
'timeout' => 10,
'allow_redirects' => false,
'http_errors' => false,
'headers' => [
'User-Agent' => $userAgent,
],
])
->willReturn($response);

$this->assertSame([
0 => 'HTTP/1.1 200',
'content-type' => ['image/x-icon'],
'x-test' => ['1'],
], $this->class->retrieveHeader($url));
}

public function testRetrieveHeaderReturnsEmptyArrayOnClientError(): void
{
$url = 'https://example.com/favicon.ico';
$userAgent = 'NextCloud-News/25.0.0';

$this->fetcherConfig->expects($this->once())
->method('getUserAgent')
->willReturn($userAgent);

$this->client->expects($this->once())
->method('head')
->with($url, [
'timeout' => 10,
'allow_redirects' => false,
'http_errors' => false,
'headers' => [
'User-Agent' => $userAgent,
],
])
->willThrowException(new \Exception('request failed'));

$this->logger->expects($this->once())
->method('warning');

$this->assertSame([], $this->class->retrieveHeader($url));
}
}
Loading