Skip to content

Commit 0bd05d3

Browse files
[FEATURE] Refactor gravatar loading (#161)
* [FEATURE] Refactor gravatar loading * [TASK] Do not break existing public api * [TASK] Remove superfluous CacheManager instance * [BUGFIX] Reverse if empty logic * [TASK] Create deriveFileTypeFromContentType method * [BUGFIX] Use try catch blog instead of null comparison * [TASK] Introduce extension configuration option * [TASK] Restructure new files * [TASK] Add missing dependencies to composer.json * [BUGFIX] GravatarViewHelper must create author object * [BUGFIX] Revert argument name * [TASK] Deactivate DI ¯\_(ツ)_/¯ * [TASK] Do not resolve gravatar urls for comments by default * [TASK] Code cleanup * [TASK] Make PR TYPO3 9.5 compatible * [TASK] Add unit test for GravatarUriBuilder * [TASK] Add unit test for GravatarResourceResolver * [BUGFIX] Fix unit test for TYPO3 9.5 * [TASK] Introduce functional test for GravatarProvider * [TASK] Register functional tests as composer command
1 parent d79d63d commit 0bd05d3

File tree

20 files changed

+753
-19
lines changed

20 files changed

+753
-19
lines changed

.ddev/docker-compose.environment.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ services:
44
web:
55
environment:
66
- TYPO3_CONTEXT=Development
7+
- typo3DatabaseHost=db
8+
- typo3DatabaseName=t3func
9+
- typo3DatabasePassword=root
10+
- typo3DatabaseUsername=root

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ jobs:
66
build:
77

88
runs-on: ubuntu-latest
9+
910
strategy:
1011
matrix:
1112
typo3: [^9.5, ^10.4]
1213
php: ['7.2', '7.3', '7.4']
1314

1415
steps:
16+
- name: Start database server
17+
run: sudo /etc/init.d/mysql start
1518

1619
- name: Checkout
1720
uses: actions/checkout@v2
@@ -37,3 +40,11 @@ jobs:
3740

3841
- name: Unit Tests
3942
run: composer t3g:test:php:unit
43+
44+
- name: Functional Tests
45+
run: composer t3g:test:php:functional;
46+
env:
47+
typo3DatabaseHost: 127.0.0.1
48+
typo3DatabaseName: t3func
49+
typo3DatabasePassword: root
50+
typo3DatabaseUsername: root

Build/FunctionalTests.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit
3+
backupGlobals="true"
4+
bootstrap="../.build/vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
5+
colors="true"
6+
convertErrorsToExceptions="true"
7+
convertWarningsToExceptions="true"
8+
forceCoversAnnotation="false"
9+
stopOnError="false"
10+
stopOnFailure="false"
11+
stopOnIncomplete="false"
12+
stopOnSkipped="false"
13+
verbose="false"
14+
beStrictAboutTestsThatDoNotTestAnything="false"
15+
>
16+
<testsuites>
17+
<testsuite name="Blog Extension">
18+
<directory>../Tests/Functional</directory>
19+
</testsuite>
20+
</testsuites>
21+
<php>
22+
<const name="TYPO3_MODE" value="BE" />
23+
<ini name="display_errors" value="1" />
24+
<env name="TYPO3_CONTEXT" value="Testing" />
25+
</php>
26+
<filter>
27+
<whitelist processUncoveredFilesFromWhitelist="true">
28+
<directory suffix=".php">../Classes/</directory>
29+
</whitelist>
30+
</filter>
31+
</phpunit>

Classes/AvatarProvider/GravatarProvider.php

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,106 @@
1212

1313
use T3G\AgencyPack\Blog\AvatarProviderInterface;
1414
use T3G\AgencyPack\Blog\Domain\Model\Author;
15+
use T3G\AgencyPack\Blog\Http\Client;
16+
use T3G\AgencyPack\Blog\Http\RequestFactory;
17+
use T3G\AgencyPack\Blog\Http\UriFactory;
18+
use T3G\AgencyPack\Blog\Service\Avatar\AvatarResourceResolverInterface;
19+
use T3G\AgencyPack\Blog\Service\Avatar\Gravatar\GravatarResourceResolver;
20+
use T3G\AgencyPack\Blog\Service\Avatar\Gravatar\GravatarUriBuilder;
21+
use T3G\AgencyPack\Blog\Service\Avatar\Gravatar\GravatarUriBuilderInterface;
22+
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
23+
use TYPO3\CMS\Core\Core\Environment;
24+
use TYPO3\CMS\Core\SingletonInterface;
1525
use TYPO3\CMS\Core\Utility\GeneralUtility;
26+
use TYPO3\CMS\Core\Utility\PathUtility;
1627
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
1728
use TYPO3\CMS\Extbase\Object\ObjectManager;
1829

19-
class GravatarProvider implements AvatarProviderInterface
30+
class GravatarProvider implements AvatarProviderInterface, SingletonInterface
2031
{
32+
/**
33+
* @var GravatarUriBuilderInterface
34+
*/
35+
private $gravatarUriBuilder;
36+
37+
/**
38+
* @var AvatarResourceResolverInterface
39+
*/
40+
private $avatarResourceResolver;
41+
42+
/**
43+
* @var bool
44+
*/
45+
private $proxyGravatarImage;
46+
47+
final public function __construct()
48+
{
49+
$this->gravatarUriBuilder = GeneralUtility::makeInstance(
50+
GravatarUriBuilder::class,
51+
GeneralUtility::makeInstance(UriFactory::class)
52+
);
53+
$this->avatarResourceResolver = GeneralUtility::makeInstance(
54+
GravatarResourceResolver::class,
55+
GeneralUtility::makeInstance(Client::class, GeneralUtility::makeInstance(\GuzzleHttp\Client::class)),
56+
GeneralUtility::makeInstance(RequestFactory::class)
57+
);
58+
59+
/** @var ExtensionConfiguration $extensionConfiguration */
60+
$extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class);
61+
$this->proxyGravatarImage = (bool)($extensionConfiguration->get('blog', 'enableGravatarProxy') ?? false);
62+
}
63+
2164
public function getAvatarUrl(Author $author): string
2265
{
2366
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
2467
$configurationManager = $objectManager->get(ConfigurationManagerInterface::class);
2568
$settings = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS, 'blog');
2669

27-
$defaultSize = 32;
28-
$defaultDefault = 'mm';
29-
$defaultRating = 'g';
30-
$size = ($settings['authors']['avatar']['provider']['size'] ?? $defaultSize) ?: $defaultSize;
31-
$default = ($settings['authors']['avatar']['provider']['default'] ?? $defaultDefault) ?: $defaultDefault;
32-
$rating = ($settings['authors']['avatar']['provider']['rating'] ?? $defaultRating) ?: $defaultRating;
70+
$size = empty($size = (string)($settings['authors']['avatar']['provider']['size'] ?? '')) ? null : (int)$size;
71+
$rating = empty($rating = (string)($settings['authors']['avatar']['provider']['rating'] ?? '')) ? null : $rating;
72+
$default = empty($default = (string)($settings['authors']['avatar']['provider']['default'] ?? '')) ? null : $default;
73+
74+
$gravatarUri = $this->gravatarUriBuilder->getUri(
75+
$author->getEmail(),
76+
$size,
77+
$rating,
78+
$default
79+
);
80+
81+
if (!$this->proxyGravatarImage) {
82+
return (string)$gravatarUri;
83+
}
3384

34-
$avatarUrl = 'https://www.gravatar.com/avatar/' . md5($author->getEmail())
35-
. '?s=' . $size
36-
. '&d=' . urlencode($default)
37-
. '&r=' . $rating;
85+
try {
86+
$gravatar = $this->avatarResourceResolver->resolve($gravatarUri);
87+
} catch (\Throwable $e) {
88+
// something went wrong, no need to deal with caching
89+
return '';
90+
}
3891

39-
return $avatarUrl;
92+
$fileType = $this->deriveFileTypeFromContentType($gravatar->getContentType());
93+
$filePath = Environment::getPublicPath() . '/typo3temp/assets/t3g/blog/gravatar/' . md5($gravatar->getContent()) . '.' . $fileType;
94+
95+
$absoluteWebPath = PathUtility::getAbsoluteWebPath($filePath);
96+
97+
if (file_exists($filePath)) {
98+
if (hash_equals(md5_file($filePath), md5($gravatar->getContent()))) {
99+
return $absoluteWebPath;
100+
}
101+
102+
unlink($filePath);
103+
}
104+
105+
$errorMessage = GeneralUtility::writeFileToTypo3tempDir($filePath, $gravatar->getContent());
106+
if ($errorMessage !== null && !file_exists($filePath)) {
107+
throw new \RuntimeException($errorMessage, 1597674070);
108+
}
109+
110+
return $absoluteWebPath;
111+
}
112+
113+
private function deriveFileTypeFromContentType(string $contentType): string
114+
{
115+
return substr($contentType, (int)strrpos($contentType, '/') + 1);
40116
}
41117
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
/*
5+
* This file is part of the package t3g/blog.
6+
*
7+
* For the full copyright and license information, please read the
8+
* LICENSE file that was distributed with this source code.
9+
*/
10+
11+
namespace T3G\AgencyPack\Blog\DataTransferObject;
12+
13+
use Psr\Http\Message\UriInterface;
14+
15+
interface AvatarResource
16+
{
17+
public function getUri(): UriInterface;
18+
19+
public function getContentType(): string;
20+
21+
public function getContent(): string;
22+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
/*
5+
* This file is part of the package t3g/blog.
6+
*
7+
* For the full copyright and license information, please read the
8+
* LICENSE file that was distributed with this source code.
9+
*/
10+
11+
namespace T3G\AgencyPack\Blog\DataTransferObject;
12+
13+
use Psr\Http\Message\UriInterface;
14+
15+
class Gravatar implements AvatarResource
16+
{
17+
/**
18+
* @var UriInterface
19+
*/
20+
private $uri;
21+
22+
/**
23+
* @var string
24+
*/
25+
private $contentType;
26+
27+
/**
28+
* @var string
29+
*/
30+
private $content;
31+
32+
public function __construct(UriInterface $uri, string $contentType, string $content)
33+
{
34+
$this->uri = $uri;
35+
$this->contentType = $contentType;
36+
$this->content = $content;
37+
}
38+
39+
public function getUri(): UriInterface
40+
{
41+
return $this->uri;
42+
}
43+
44+
public function getContentType(): string
45+
{
46+
return $this->contentType;
47+
}
48+
49+
public function getContent(): string
50+
{
51+
return $this->content;
52+
}
53+
}

Classes/Http/Client.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the package t3g/blog.
7+
*
8+
* For the full copyright and license information, please read the
9+
* LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace T3G\AgencyPack\Blog\Http;
13+
14+
use GuzzleHttp\ClientInterface as GuzzleClientInterface;
15+
use GuzzleHttp\Exception\ConnectException;
16+
use GuzzleHttp\Exception\GuzzleException;
17+
use GuzzleHttp\Exception\RequestException;
18+
use GuzzleHttp\RequestOptions;
19+
use Psr\Http\Client\ClientExceptionInterface;
20+
use Psr\Http\Client\ClientInterface;
21+
use Psr\Http\Message\RequestInterface;
22+
use Psr\Http\Message\ResponseInterface;
23+
24+
/**
25+
* Backport of https://github.com/TYPO3-CMS/core/blob/10.4/Classes/Http/Client.php
26+
*
27+
* Must be removed as soon as t3g/blog's minimum requirement is typo3/cms-core:^10.4
28+
*/
29+
class Client implements ClientInterface
30+
{
31+
/**
32+
* @var GuzzleClientInterface
33+
*/
34+
private $guzzle;
35+
36+
public function __construct(GuzzleClientInterface $guzzle)
37+
{
38+
$this->guzzle = $guzzle;
39+
}
40+
41+
/**
42+
* Sends a PSR-7 request and returns a PSR-7 response.
43+
*
44+
* @param RequestInterface $request
45+
* @return ResponseInterface
46+
* @throws ClientExceptionInterface If an error happens while processing the request.
47+
*/
48+
public function sendRequest(RequestInterface $request): ResponseInterface
49+
{
50+
$clientException = new class extends \Exception implements ClientExceptionInterface {
51+
};
52+
53+
try {
54+
return $this->guzzle->send($request, [
55+
RequestOptions::HTTP_ERRORS => false,
56+
RequestOptions::ALLOW_REDIRECTS => false,
57+
]);
58+
} catch (ConnectException $e) {
59+
throw new $clientException($e->getMessage(), 1566909446, $e->getRequest(), $e);
60+
} catch (RequestException $e) {
61+
throw new $clientException($e->getMessage(), 1566909447, $e->getRequest(), $e);
62+
} catch (GuzzleException $e) {
63+
throw new $clientException($e->getMessage(), 1566909448, $e);
64+
}
65+
}
66+
}

Classes/Http/RequestFactory.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the package t3g/blog.
7+
*
8+
* For the full copyright and license information, please read the
9+
* LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace T3G\AgencyPack\Blog\Http;
13+
14+
use GuzzleHttp\HandlerStack;
15+
use Psr\Http\Message\RequestFactoryInterface;
16+
use Psr\Http\Message\RequestInterface;
17+
use Psr\Http\Message\ResponseInterface;
18+
use Psr\Http\Message\UriInterface;
19+
use TYPO3\CMS\Core\Http\Request;
20+
use TYPO3\CMS\Core\Utility\GeneralUtility;
21+
22+
/**
23+
* Backport of https://github.com/TYPO3-CMS/core/blob/10.4/Classes/Http/RequestFactory.php
24+
*
25+
* Must be removed as soon as t3g/blog's minimum requirement is typo3/cms-core:^10.4
26+
*/
27+
class RequestFactory implements RequestFactoryInterface
28+
{
29+
/**
30+
* Create a new request.
31+
*
32+
* @param string $method The HTTP method associated with the request.
33+
* @param UriInterface|string $uri The URI associated with the request.
34+
* @return RequestInterface
35+
*/
36+
public function createRequest(string $method, $uri): RequestInterface
37+
{
38+
return new Request($uri, $method);
39+
}
40+
41+
/**
42+
* Create a guzzle request object with our custom implementation
43+
*
44+
* @param string $uri the URI to request
45+
* @param string $method the HTTP method (defaults to GET)
46+
* @param array $options custom options for this request
47+
* @return ResponseInterface
48+
*/
49+
public function request(string $uri, string $method = 'GET', array $options = []): ResponseInterface
50+
{
51+
$httpOptions = $GLOBALS['TYPO3_CONF_VARS']['HTTP'];
52+
$httpOptions['verify'] = filter_var($httpOptions['verify'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $httpOptions['verify'];
53+
54+
if (isset($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler']) && is_array($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'])) {
55+
$stack = HandlerStack::create();
56+
foreach ($GLOBALS['TYPO3_CONF_VARS']['HTTP']['handler'] ?? [] as $handler) {
57+
$stack->push($handler);
58+
}
59+
$httpOptions['handler'] = $stack;
60+
}
61+
62+
/** @var $client */
63+
$client = GeneralUtility::makeInstance(Client::class, $httpOptions);
64+
return $client->request($method, $uri, $options);
65+
}
66+
}

0 commit comments

Comments
 (0)