1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Brendt\Stitcher\Site\Http; |
4
|
|
|
|
5
|
|
|
use Brendt\Stitcher\Exception\ConfigurationException; |
6
|
|
|
use Brendt\Stitcher\Site\Page; |
7
|
|
|
use Symfony\Component\Filesystem\Filesystem; |
8
|
|
|
use Tivie\HtaccessParser\HtaccessContainer; |
9
|
|
|
use Tivie\HtaccessParser\Parser; |
10
|
|
|
use Tivie\HtaccessParser\Token\Block; |
11
|
|
|
use Tivie\HtaccessParser\Token\Directive; |
12
|
|
|
use Tivie\HtaccessParser\Token\WhiteLine; |
13
|
|
|
|
14
|
|
|
class Htaccess |
15
|
|
|
{ |
16
|
|
|
/** |
17
|
|
|
* @var Filesystem |
18
|
|
|
*/ |
19
|
|
|
private $fs; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var Parser |
23
|
|
|
*/ |
24
|
|
|
private $parser; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var array|\ArrayAccess|HtaccessContainer |
28
|
|
|
*/ |
29
|
|
|
private $contents; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var bool |
33
|
|
|
*/ |
34
|
|
|
private $redirectHttps = false; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var bool |
38
|
|
|
*/ |
39
|
|
|
private $redirectWww = false; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var array |
43
|
|
|
*/ |
44
|
|
|
private $redirects = []; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Htaccess constructor. |
48
|
|
|
* |
49
|
|
|
* @param string $path |
50
|
|
|
* |
51
|
|
|
* @throws ConfigurationException |
52
|
|
|
*/ |
53
|
|
|
public function __construct(string $path) { |
54
|
|
|
$this->fs = new Filesystem(); |
55
|
|
|
|
56
|
|
|
if (!$this->fs->exists($path)) { |
57
|
|
|
$this->fs->dumpFile($path, file_get_contents(__DIR__ . '/../../../../.htaccess')); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
$this->parser = new Parser(new \SplFileObject($path)); |
61
|
|
|
$this->parser->ignoreWhitelines(false); |
62
|
|
|
$this->contents = $this->parser->parse(); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @param bool $redirectHttps |
67
|
|
|
* |
68
|
|
|
* @return Htaccess |
69
|
|
|
*/ |
70
|
|
|
public function setRedirectHttps(bool $redirectHttps = false) : Htaccess { |
71
|
|
|
$this->redirectHttps = $redirectHttps; |
72
|
|
|
|
73
|
|
|
return $this; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @param bool $redirectWww |
78
|
|
|
* |
79
|
|
|
* @return Htaccess |
80
|
|
|
*/ |
81
|
|
|
public function setRedirectWww(bool $redirectWww = false) : Htaccess { |
82
|
|
|
$this->redirectWww = $redirectWww; |
83
|
|
|
|
84
|
|
|
return $this; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Add custom redirects handles by htaccess |
89
|
|
|
* |
90
|
|
|
* @param string $from |
91
|
|
|
* @param string $to |
92
|
|
|
* |
93
|
|
|
* @return Htaccess |
94
|
|
|
*/ |
95
|
|
|
public function addRedirect(string $from, string $to) : Htaccess { |
96
|
|
|
$this->redirects[$from] = $to; |
97
|
|
|
|
98
|
|
|
return $this; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Parse the modified .htaccess |
103
|
|
|
* |
104
|
|
|
* @return string |
105
|
|
|
*/ |
106
|
|
|
public function parse() : string { |
107
|
|
|
$this->clearRewriteBlock(); |
108
|
|
|
|
109
|
|
|
if ($this->redirectWww) { |
110
|
|
|
$this->rewriteWww(); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
if ($this->redirectHttps) { |
114
|
|
|
$this->rewriteHttps(); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
$this->rewriteCustomRedirects(); |
118
|
|
|
$this->rewriteHtml(); |
119
|
|
|
|
120
|
|
|
return (string) $this->contents; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Get or create the headers block |
125
|
|
|
* |
126
|
|
|
* @return Block |
127
|
|
|
*/ |
128
|
|
View Code Duplication |
public function &getHeaderBlock() : Block { |
|
|
|
|
129
|
|
|
$headerBlock = $this->findHeaderBlockByModName('mod_headers.c'); |
130
|
|
|
|
131
|
|
|
if (!$headerBlock) { |
132
|
|
|
$headerBlock = new Block('ifmodule'); |
133
|
|
|
$headerBlock->addArgument('mod_headers.c'); |
134
|
|
|
|
135
|
|
|
if ($this->contents instanceof HtaccessContainer) { |
136
|
|
|
$this->contents->append($headerBlock); |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
return $headerBlock; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Get or create a page block within the headers block |
145
|
|
|
* |
146
|
|
|
* @param Page $page |
147
|
|
|
* |
148
|
|
|
* @return Block |
149
|
|
|
*/ |
150
|
|
|
public function &getPageBlock(Page $page) : Block { |
151
|
|
|
$headerBlock = $this->getHeaderBlock(); |
152
|
|
|
$pageId = trim($page->getId(), '/') ?? 'index'; |
153
|
|
|
$pageId = pathinfo($pageId !== '' ? "{$pageId}" : 'index', PATHINFO_BASENAME); |
154
|
|
|
$pageName = '"^' . $pageId . '\.html$"'; |
155
|
|
|
|
156
|
|
|
$pageBlock = $this->findPageBlockByParentAndName($headerBlock, $pageName); |
157
|
|
|
|
158
|
|
|
if (!$pageBlock) { |
159
|
|
|
$pageBlock = new Block('filesmatch'); |
160
|
|
|
$pageBlock->addArgument($pageName); |
161
|
|
|
$headerBlock->addChild($pageBlock); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
return $pageBlock; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Clear all page header blocks |
169
|
|
|
*/ |
170
|
|
|
public function clearPageBlocks() { |
171
|
|
|
$headerBlock = $this->getHeaderBlock(); |
172
|
|
|
|
173
|
|
|
foreach ($headerBlock as $content) { |
174
|
|
|
if ($content instanceof Block && strtolower($content->getName()) === 'filesmatch') { |
|
|
|
|
175
|
|
|
$headerBlock->removeChild($content); |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Clear all rewrites |
182
|
|
|
*/ |
183
|
|
|
public function clearRewriteBlock() { |
184
|
|
|
$rewriteBlock = $this->getRewriteBlock(); |
185
|
|
|
|
186
|
|
|
foreach ($rewriteBlock as $content) { |
187
|
|
|
if ( |
188
|
|
|
$content instanceof WhiteLine || |
189
|
|
|
($content instanceof Directive && $content->getName() !== 'RewriteEngine' && $content->getName() !== 'DirectorySlash') |
|
|
|
|
190
|
|
|
) { |
191
|
|
|
$rewriteBlock->removeChild($content); |
192
|
|
|
} |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Get or create the rewrite block |
198
|
|
|
* |
199
|
|
|
* @return Block |
200
|
|
|
*/ |
201
|
|
View Code Duplication |
public function &getRewriteBlock() : Block { |
|
|
|
|
202
|
|
|
$rewriteBlock = $this->findHeaderBlockByModName('mod_rewrite.c'); |
203
|
|
|
|
204
|
|
|
if (!$rewriteBlock) { |
205
|
|
|
$rewriteBlock = new Block('ifmodule'); |
206
|
|
|
$rewriteBlock->addArgument('mod_rewrite.c'); |
207
|
|
|
|
208
|
|
|
if ($this->contents instanceof HtaccessContainer) { |
209
|
|
|
$this->contents->append($rewriteBlock); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
return $rewriteBlock; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* @param Block $headerBlock |
218
|
|
|
* @param string $pageName |
219
|
|
|
* |
220
|
|
|
* @return null|Block |
221
|
|
|
*/ |
222
|
|
|
private function findPageBlockByParentAndName(Block $headerBlock, string $pageName) { |
223
|
|
|
foreach ($headerBlock as $content) { |
224
|
|
|
$arguments = $content->getArguments(); |
225
|
|
|
|
226
|
|
|
if (reset($arguments) === $pageName) { |
227
|
|
|
return $content; |
228
|
|
|
} |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
return null; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Add www rewrite |
236
|
|
|
*/ |
237
|
|
|
private function rewriteWww() { |
238
|
|
|
$rewriteBlock = $this->getRewriteBlock(); |
239
|
|
|
|
240
|
|
|
$this->createConditionalRewrite($rewriteBlock, '%{HTTP_HOST} !^www\.', '^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]'); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Add https rewrite |
245
|
|
|
*/ |
246
|
|
|
private function rewriteHttps() { |
247
|
|
|
$rewriteBlock = $this->getRewriteBlock(); |
248
|
|
|
|
249
|
|
|
$this->createConditionalRewrite($rewriteBlock, '%{HTTPS} off', '(.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]'); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Add custom rewrites |
254
|
|
|
*/ |
255
|
|
|
private function rewriteCustomRedirects() { |
256
|
|
|
$rewriteBlock = $this->getRewriteBlock(); |
257
|
|
|
$rewriteBlock->addLineBreak(1); |
258
|
|
|
|
259
|
|
|
foreach ($this->redirects as $from => $to) { |
260
|
|
|
$ruleLine = new Directive(); |
261
|
|
|
$ruleLine->setName("RedirectMatch 301 ^{$from}$ {$to}"); |
262
|
|
|
$rewriteBlock->addChild($ruleLine); |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Add .html rewrite |
268
|
|
|
*/ |
269
|
|
|
private function rewriteHtml() { |
270
|
|
|
$rewriteBlock = $this->getRewriteBlock(); |
271
|
|
|
|
272
|
|
|
$this->createConditionalRewrite($rewriteBlock, '%{DOCUMENT_ROOT}/$1.html -f', '^(.+?)/?$ /$1.html [L]'); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* @param Block $rewriteBlock |
277
|
|
|
* @param string $condition |
278
|
|
|
* @param string $rule |
279
|
|
|
*/ |
280
|
|
|
private function createConditionalRewrite(Block &$rewriteBlock, string $condition, string $rule) { |
281
|
|
|
$rewriteBlock->addLineBreak(1); |
282
|
|
|
|
283
|
|
|
$conditionLine = new Directive(); |
284
|
|
|
$conditionLine->setName("RewriteCond {$condition}"); |
285
|
|
|
$rewriteBlock->addChild($conditionLine); |
286
|
|
|
|
287
|
|
|
$ruleLine = new Directive(); |
288
|
|
|
$ruleLine->setName("RewriteRule {$rule}"); |
289
|
|
|
$rewriteBlock->addChild($ruleLine); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* @param string $modName |
294
|
|
|
* |
295
|
|
|
* @return null|Block |
296
|
|
|
*/ |
297
|
|
|
private function findHeaderBlockByModName(string $modName) { |
298
|
|
|
foreach ($this->contents as $content) { |
299
|
|
|
$arguments = $content->getArguments(); |
300
|
|
|
if (reset($arguments) === $modName) { |
301
|
|
|
return $content; |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return null; |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.