Skip to content

Commit c83a0d0

Browse files
[TASK] Initial commit
0 parents  commit c83a0d0

19 files changed

+6818
-0
lines changed

.editorconfig

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
root = true
3+
4+
[*]
5+
charset = utf-8
6+
trim_trailing_whitespace = true
7+
end_of_line = lf
8+
insert_final_newline = true
9+
indent_style = space
10+
indent_size = 4
11+
12+
[Makefile]
13+
indent_style = tab
14+
15+
[*.{yml,yaml,conf,scss}]
16+
indent_size = 2
17+
18+
[*.rst]
19+
indent_size = 3
20+
21+
[*.md]
22+
trim_trailing_whitespace = false

.gitattributes

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
* text eol=lf
2+
3+
*.png binary
4+
*.jpg binary
5+
6+
/.github export-ignore
7+
/.editorconfig export-ignore
8+
/.gitattributes export-ignore
9+
/.gitignore export-ignore
10+
/.php-cs-fixer.php export-ignore
11+
/composer.lock export-ignore
12+
/Makefile export-ignore

.github/workflows/code-style.yml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
name: 'Code Style'
3+
4+
on: [ push, pull_request ]
5+
6+
jobs:
7+
code-style:
8+
name: Code Style Check
9+
runs-on: ubuntu-22.04
10+
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v3
14+
15+
- name: Setup PHP
16+
uses: nanasess/setup-php@v3
17+
with:
18+
php-version: 8.2
19+
20+
- name: Check code style
21+
run: |
22+
make lint

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
.idea
3+
.vscode
4+
.php-cs-fixer.cache
5+
/vendor

.php-cs-fixer.php

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Symfony\Component\Finder\Finder;
6+
7+
$finder = Finder::create()
8+
->name('/\\.php$/')
9+
->in(__DIR__)
10+
->notPath('vendor');
11+
12+
return (new PhpCsFixer\Config())
13+
->setUsingCache(false)
14+
->setRiskyAllowed(true)
15+
->setRules([
16+
'@DoctrineAnnotation' => true,
17+
'@PSR12' => true,
18+
'@PHP81Migration' => true,
19+
'@PHP80Migration:risky' => true,
20+
'@PHPUnit84Migration:risky' => true,
21+
'align_multiline_comment' => [
22+
'comment_type' => 'phpdocs_like',
23+
],
24+
'array_syntax' => ['syntax' => 'short'],
25+
'binary_operator_spaces' => [
26+
'default' => 'single_space',
27+
'operators' => [
28+
'=>' => 'align_single_space_minimal',
29+
],
30+
],
31+
'blank_line_before_statement' => [
32+
'statements' => ['if', 'try', 'return'],
33+
],
34+
'cast_spaces' => ['space' => 'none'],
35+
'concat_space' => ['spacing' => 'one'],
36+
'declare_equal_normalize' => ['space' => 'none'],
37+
'declare_strict_types' => true,
38+
'doctrine_annotation_array_assignment' => [
39+
'operator' => '=',
40+
],
41+
'dir_constant' => true,
42+
'ereg_to_preg' => true,
43+
'escape_implicit_backslashes' => [
44+
'double_quoted' => true,
45+
'heredoc_syntax' => true,
46+
'single_quoted' => true,
47+
],
48+
'explicit_indirect_variable' => true,
49+
'explicit_string_variable' => true,
50+
'function_typehint_space' => true,
51+
'linebreak_after_opening_tag' => true,
52+
'lowercase_cast' => true,
53+
'magic_constant_casing' => true,
54+
'modernize_types_casting' => true,
55+
'native_function_casing' => true,
56+
'new_with_braces' => true,
57+
'no_alias_functions' => true,
58+
'no_blank_lines_after_phpdoc' => true,
59+
'no_empty_comment' => true,
60+
'no_empty_phpdoc' => true,
61+
'no_empty_statement' => true,
62+
'no_extra_blank_lines' => true,
63+
'no_leading_import_slash' => true,
64+
'no_leading_namespace_whitespace' => true,
65+
'no_mixed_echo_print' => [
66+
'use' => 'echo',
67+
],
68+
'no_multiline_whitespace_around_double_arrow' => true,
69+
'no_null_property_initialization' => true,
70+
'no_php4_constructor' => true,
71+
'no_short_bool_cast' => true,
72+
'no_singleline_whitespace_before_semicolons' => true,
73+
'no_superfluous_elseif' => true,
74+
'no_spaces_after_function_name' => true,
75+
'no_spaces_around_offset' => [
76+
'positions' => ['inside', 'outside'],
77+
],
78+
'no_spaces_inside_parenthesis' => true,
79+
'no_trailing_comma_in_singleline' => true,
80+
'no_unneeded_control_parentheses' => true,
81+
'no_unused_imports' => true,
82+
'no_useless_else' => true,
83+
'no_useless_return' => true,
84+
'no_whitespace_in_blank_line' => true,
85+
'non_printable_character' => false,
86+
'ordered_class_elements' => true,
87+
'ordered_imports' => true,
88+
'phpdoc_indent' => true,
89+
'phpdoc_scalar' => true,
90+
'short_scalar_cast' => true,
91+
'single_line_comment_style' => true,
92+
'phpdoc_no_access' => true,
93+
'phpdoc_no_empty_return' => true,
94+
'phpdoc_no_package' => true,
95+
'phpdoc_trim' => true,
96+
'phpdoc_types' => true,
97+
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
98+
'return_type_declaration' => ['space_before' => 'none'],
99+
'single_quote' => [
100+
'strings_containing_single_quote_chars' => true,
101+
],
102+
'standardize_not_equals' => true,
103+
'visibility_required' => true,
104+
'ternary_operator_spaces' => true,
105+
'whitespace_after_comma_in_array' => true,
106+
])
107+
->setFinder($finder);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the TYPO3 CMS project.
7+
*
8+
* It is free software; you can redistribute it and/or modify it under
9+
* the terms of the GNU General Public License, either version 2
10+
* of the License, or any later version.
11+
*
12+
* For the full copyright and license information, please read the
13+
* LICENSE.txt file that was distributed with this source code.
14+
*
15+
* The TYPO3 project - inspiring people to share!
16+
*/
17+
18+
namespace Supseven\CanonicalFiles\Middleware;
19+
20+
use Psr\Http\Message\ResponseInterface;
21+
use Psr\Http\Message\ServerRequestInterface;
22+
use Psr\Http\Server\MiddlewareInterface;
23+
use Psr\Http\Server\RequestHandlerInterface;
24+
use Supseven\CanonicalFiles\Utility\CanonicalUri;
25+
use TYPO3\CMS\Core\Core\Environment;
26+
use TYPO3\CMS\Core\Resource\ResourceFactory;
27+
28+
/**
29+
* Middleware to add a canonical link header to files
30+
*
31+
* Insert the following snippet into your .htaccess file to configure, which files will be affected by this middleware.
32+
* Amend the conditions accordingly:
33+
*
34+
* RewriteCond %{REQUEST_URI} ^/fileadmin
35+
* RewriteCond %{REQUEST_FILENAME} \.(pdf|doc|docx|xls|xlsx|ppt|pptx)$
36+
* RewriteRule ^.*$ %{ENV:CWD}index.php [QSA,L]
37+
*/
38+
readonly class AddCanonicalFileHeader implements MiddlewareInterface
39+
{
40+
/**
41+
* Inject ResourceFactory
42+
*
43+
* @param \TYPO3\CMS\Core\Resource\ResourceFactory $resourceFactory
44+
*/
45+
public function __construct(
46+
private ResourceFactory $resourceFactory
47+
) {
48+
}
49+
50+
/**
51+
* If the current request handles a file, convert the URL of a file to a canonical URL
52+
* and add it as a link header to the response.
53+
*
54+
* See class description above about which files affected.
55+
*
56+
* @throws \TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException
57+
*/
58+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
59+
{
60+
// Get absolute file path (on file system) from request
61+
$uri = urldecode($request->getUri()->getPath());
62+
$requestedFile = Environment::getPublicPath() . $uri;
63+
64+
// Check if file exists
65+
// If not we handle this as a common page request
66+
if (file_exists($requestedFile) && is_file($requestedFile)) {
67+
68+
// Retrieve FAL from file path to get information about the file's storage
69+
$file = $this->resourceFactory->retrieveFileOrFolderObject($uri);
70+
71+
// Just in case...
72+
if (!$file) {
73+
return $handler->handle($request);
74+
}
75+
76+
// We could compare the file's storage location with the current site's base URL and only add
77+
// the canonical link header if the file's location is not located in the current site's storage.
78+
// But it's okay to add the canonical link header in any case, so we save pains and just add the header.
79+
// Get the site identifier from the file's storage configuration, if available:
80+
$canonisedFileUrl = CanonicalUri::getFileUri($file);
81+
82+
// As we force the file through TYPO3's index.php, we have to handle file headers manually
83+
$response = CanonicalUri::buildResponseForFile($requestedFile);
84+
85+
// Set the canonised header
86+
return $response->withHeader('Link', '<' . $canonisedFileUrl . '>; rel="canonical"');
87+
}
88+
89+
// Not a file request, so we handle this as a common page request
90+
return $handler->handle($request);
91+
}
92+
}

Classes/Utility/CanonicalUri.php

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the TYPO3 CMS project.
7+
*
8+
* It is free software; you can redistribute it and/or modify it under
9+
* the terms of the GNU General Public License, either version 2
10+
* of the License, or any later version.
11+
*
12+
* For the full copyright and license information, please read the
13+
* LICENSE.txt file that was distributed with this source code.
14+
*
15+
* The TYPO3 project - inspiring people to share!
16+
*/
17+
18+
namespace Supseven\CanonicalFiles\Utility;
19+
20+
use Symfony\Component\Mime\MimeTypes;
21+
use TYPO3\CMS\Core\Core\Environment;
22+
use TYPO3\CMS\Core\Http\Response;
23+
use TYPO3\CMS\Core\Http\Stream;
24+
use TYPO3\CMS\Core\Resource\FileInterface;
25+
use TYPO3\CMS\Core\Site\SiteFinder;
26+
use TYPO3\CMS\Core\Utility\GeneralUtility;
27+
28+
/**
29+
* Class to create a canonical file uri
30+
*/
31+
class CanonicalUri
32+
{
33+
/**
34+
* Simple runtime cache for base URI
35+
*
36+
* @var array
37+
*/
38+
protected static array $baseUriCache = [];
39+
40+
/**
41+
* Try to get the setting "tx_canonical_files_site_identifier" from the file storage record and create the
42+
* uri to the file accordingly.
43+
*
44+
* The key might be empty if not defined in the backend, or the file might come from a fallback storage. Then
45+
* we use the current base URL, if absolute is true.
46+
*
47+
* @param \TYPO3\CMS\Core\Resource\FileInterface $file
48+
* @return string
49+
*/
50+
public static function getFileUri(FileInterface $file): string
51+
{
52+
$fileUrl = $file->getPublicUrl();
53+
$fileStorage = $file->getStorage()->getStorageRecord();
54+
55+
// If configuration is available and set, create and use the canonical url...
56+
if (!empty($fileStorage['tx_canonical_files_site_identifier'])) {
57+
58+
return self::getBaseFromSiteIdentifier($fileStorage['tx_canonical_files_site_identifier']) . $fileUrl;
59+
}
60+
61+
// ...otherwise use the current base URL
62+
return GeneralUtility::locationHeaderUrl($fileUrl);
63+
}
64+
65+
/**
66+
* Extracts the base URL from the site configuration.
67+
*
68+
* Value is cached per site identifier.
69+
* URI is always returned without trailing slash, because the path to the file always starts with a slash.
70+
*
71+
* @param string $siteIdentifier
72+
* @return string
73+
*/
74+
public static function getBaseFromSiteIdentifier(string $siteIdentifier): string
75+
{
76+
if (isset(self::$baseUriCache[$siteIdentifier])) {
77+
return self::$baseUriCache[$siteIdentifier];
78+
}
79+
80+
$site = GeneralUtility::makeInstance(SiteFinder::class);
81+
$siteConfiguration = $site->getAllSites()[$siteIdentifier]->getConfiguration();
82+
$baseFromSiteIdentifier = '';
83+
84+
// First check base variants and use it if applicable
85+
foreach ($siteConfiguration['baseVariants'] as $base) {
86+
if (str_contains($base['condition'], Environment::toArray()['context'])) {
87+
$baseFromSiteIdentifier = $base['base'];
88+
}
89+
}
90+
91+
// Use default base, if no variants match the current environment
92+
if ($baseFromSiteIdentifier === '') {
93+
$baseFromSiteIdentifier = $siteConfiguration['base'];
94+
}
95+
96+
// Cache the result
97+
return self::$baseUriCache[$siteIdentifier] = rtrim($baseFromSiteIdentifier, '/');
98+
}
99+
100+
/**
101+
* Create file headers
102+
*
103+
* @param string $filePath
104+
* @return \TYPO3\CMS\Core\Http\Response
105+
*/
106+
public static function buildResponseForFile(string $filePath): Response
107+
{
108+
$mimeType = new MimeTypes();
109+
$mimeType = $mimeType->guessMimeType($filePath);
110+
111+
$response = new Response();
112+
113+
return $response
114+
->withHeader('Content-Length', (string)filesize($filePath))
115+
->withHeader('Content-Type', $mimeType)
116+
->withBody(new Stream($filePath));
117+
}
118+
}

0 commit comments

Comments
 (0)