1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PiedWeb\CMSBundle\Extension\StaticGenerator; |
4
|
|
|
|
5
|
|
|
use Doctrine\ORM\EntityManagerInterface; |
6
|
|
|
use PiedWeb\CMSBundle\Entity\PageInterface as Page; |
7
|
|
|
use PiedWeb\CMSBundle\Repository\PageRepository; |
8
|
|
|
use PiedWeb\CMSBundle\Service\AppConfigHelper as App; |
9
|
|
|
use PiedWeb\CMSBundle\Service\PageCanonicalService as PageCanonical; |
10
|
|
|
use PiedWeb\CMSBundle\Utils\GenerateLivePathForTrait; |
11
|
|
|
use PiedWeb\CMSBundle\Utils\KernelTrait; |
12
|
|
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; |
13
|
|
|
use Symfony\Component\Filesystem\Filesystem; |
14
|
|
|
use Symfony\Component\HttpFoundation\Request; |
15
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
16
|
|
|
use Symfony\Component\HttpKernel\KernelInterface; |
17
|
|
|
use Symfony\Component\Routing\RouterInterface; |
18
|
|
|
use Symfony\Contracts\Translation\TranslatorInterface; |
19
|
|
|
use Twig\Environment as Twig; |
20
|
|
|
use WyriHaximus\HtmlCompress\Factory as HtmlCompressor; |
21
|
|
|
use WyriHaximus\HtmlCompress\HtmlCompressorInterface; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Generate 1 App. |
25
|
|
|
*/ |
26
|
|
|
class StaticAppGenerator |
27
|
|
|
{ |
28
|
|
|
use GenerateLivePathForTrait; |
29
|
|
|
use KernelTrait; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Contain files relative to SEO wich will be hard copied. |
33
|
|
|
* |
34
|
|
|
* @var array |
35
|
|
|
*/ |
36
|
|
|
protected $robotsFiles = ['robots.txt']; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
protected $dontCopy = ['index.php', '.htaccess']; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var EntityManagerInterface |
45
|
|
|
*/ |
46
|
|
|
protected $em; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var Filesystem |
50
|
|
|
*/ |
51
|
|
|
protected $filesystem; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var Twig |
55
|
|
|
*/ |
56
|
|
|
protected $twig; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var string |
60
|
|
|
*/ |
61
|
|
|
protected $webDir; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var array |
65
|
|
|
*/ |
66
|
|
|
protected $apps; |
67
|
|
|
protected $app; |
68
|
|
|
protected $staticDomain; |
69
|
|
|
protected $mustGetPagesWithoutHost = true; |
70
|
|
|
|
71
|
|
|
/** var @string */ |
72
|
|
|
protected $staticDir; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @var RequestStack |
76
|
|
|
*/ |
77
|
|
|
protected $requesStack; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @var \PiedWeb\CMSBundle\Service\PageCanonicalService |
81
|
|
|
*/ |
82
|
|
|
protected $pageCanonical; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @var TranslatorInterface |
86
|
|
|
*/ |
87
|
|
|
protected $translator; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @var HtmlCompressorInterface |
91
|
|
|
*/ |
92
|
|
|
protected $parser; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* @var ParameterBagInterface |
96
|
|
|
*/ |
97
|
|
|
protected $params; |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* @var RouterInterface |
101
|
|
|
*/ |
102
|
|
|
protected $router; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Used in .htaccess generation. |
106
|
|
|
* |
107
|
|
|
* @var string |
108
|
|
|
*/ |
109
|
|
|
protected $redirections = ''; |
110
|
|
|
|
111
|
|
|
public function __construct( |
112
|
|
|
EntityManagerInterface $em, |
113
|
|
|
Twig $twig, |
114
|
|
|
ParameterBagInterface $params, |
115
|
|
|
RequestStack $requesStack, |
116
|
|
|
PageCanonical $pageCanonical, |
117
|
|
|
TranslatorInterface $translator, |
118
|
|
|
RouterInterface $router, |
119
|
|
|
string $webDir, |
120
|
|
|
KernelInterface $kernel |
121
|
|
|
) { |
122
|
|
|
$this->em = $em; |
123
|
|
|
$this->filesystem = new Filesystem(); |
124
|
|
|
$this->twig = $twig; |
125
|
|
|
$this->params = $params; |
126
|
|
|
$this->requesStack = $requesStack; |
127
|
|
|
$this->webDir = $webDir; |
128
|
|
|
$this->pageCanonical = $pageCanonical; |
129
|
|
|
$this->translator = $translator; |
130
|
|
|
$this->router = $router; |
131
|
|
|
$this->apps = $this->params->get('pwc.apps'); |
132
|
|
|
$this->parser = HtmlCompressor::construct(); |
133
|
|
|
|
134
|
|
|
if (!method_exists($this->filesystem, 'dumpFile')) { |
135
|
|
|
throw new \RuntimeException('Method dumpFile() is not available. Upgrade your Filesystem.'); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
static::loadKernel($kernel); |
139
|
|
|
$this->kernel = $kernel; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
public function generateAll($filter = null) |
143
|
|
|
{ |
144
|
|
|
foreach ($this->apps as $app) { |
145
|
|
|
if ($filter && !\in_array($filter, $app->getHost())) { |
146
|
|
|
continue; |
147
|
|
|
} |
148
|
|
|
$this->generate($app, $this->mustGetPagesWithoutHost); |
149
|
|
|
//$this->generateStaticApp($app); |
150
|
|
|
|
151
|
|
|
$this->mustGetPagesWithoutHost = false; |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
public function generateFromHost($host) |
156
|
|
|
{ |
157
|
|
|
return $this->generateAll($host); |
|
|
|
|
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Main Logic is here. |
162
|
|
|
* |
163
|
|
|
* @throws \RuntimeException |
164
|
|
|
* @throws \LogicException |
165
|
|
|
*/ |
166
|
|
|
public function generate($app, $mustGetPagesWithoutHost = false) |
167
|
|
|
{ |
168
|
|
|
$this->app = new App($app['hosts'][0], [$app]); |
169
|
|
|
$this->mustGetPagesWithoutHost = $mustGetPagesWithoutHost; |
170
|
|
|
|
171
|
|
|
$this->filesystem->remove($this->app->getStaticDir()); |
172
|
|
|
$this->generatePages(); |
173
|
|
|
$this->generateSitemaps(); |
174
|
|
|
$this->generateErrorPages(); |
175
|
|
|
$this->copyRobotsFiles(); |
176
|
|
|
$this->generateServerManagerFile(); |
177
|
|
|
$this->copyAssets(); |
178
|
|
|
$this->copyMediaToDownload(); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Symlink doesn't work on github page, symlink only for apache if conf say OK to symlink. |
183
|
|
|
*/ |
184
|
|
|
protected function mustSymlink() |
185
|
|
|
{ |
186
|
|
|
return $this->app->get('static_generateForApache') ? $this->app->get('static_symlinkMedia') : false; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Generate .htaccess for Apache or CNAME for github |
191
|
|
|
* Must be run after generatePages() !! |
192
|
|
|
*/ |
193
|
|
|
protected function generateServerManagerFile() |
194
|
|
|
{ |
195
|
|
|
if ($this->app->get('static_generateForApache')) { |
196
|
|
|
$this->generateHtaccess(); |
197
|
|
|
} else { //if ($this->app['static_generateForGithubPages'])) { |
198
|
|
|
$this->generateCname(); |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* Copy files relative to SEO (robots, sitemaps, etc.). |
204
|
|
|
*/ |
205
|
|
|
protected function copyRobotsFiles(): void |
206
|
|
|
{ |
207
|
|
|
array_map([$this, 'copy'], $this->robotsFiles); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
// todo |
211
|
|
|
// docs |
212
|
|
|
// https://help.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site |
213
|
|
|
protected function generateCname() |
214
|
|
|
{ |
215
|
|
|
$this->filesystem->dumpFile($this->app->getStaticDir().'/CNAME', $this->app->getMainHost()); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
protected function generateHtaccess() |
219
|
|
|
{ |
220
|
|
|
$htaccess = $this->twig->render('@PiedWebCMS/static/htaccess.twig', [ |
221
|
|
|
'domain' => $this->app->getMainHost(), |
222
|
|
|
'redirections' => $this->redirections, |
223
|
|
|
]); |
224
|
|
|
$this->filesystem->dumpFile($this->app->getStaticDir().'/.htaccess', $htaccess); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
protected function copy(string $file): void |
228
|
|
|
{ |
229
|
|
|
if (file_exists($file)) { |
230
|
|
|
copy( |
231
|
|
|
str_replace($this->params->get('kernel.project_dir').'/', '../', $this->webDir.'/'.$file), |
232
|
|
|
$this->app->getStaticDir().'/'.$file |
233
|
|
|
); |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Copy (or symlink) for all assets in public |
239
|
|
|
* (and media previously generated by liip in public). |
240
|
|
|
*/ |
241
|
|
|
protected function copyAssets(): void |
242
|
|
|
{ |
243
|
|
|
$symlink = $this->mustSymlink(); |
244
|
|
|
|
245
|
|
|
$dir = dir($this->webDir); |
246
|
|
|
while (false !== $entry = $dir->read()) { |
247
|
|
|
if ('.' == $entry || '..' == $entry) { |
248
|
|
|
continue; |
249
|
|
|
} |
250
|
|
|
if (!\in_array($entry, $this->robotsFiles) && !\in_array($entry, $this->dontCopy)) { |
251
|
|
|
//$this->symlink( |
252
|
|
|
if (true === $symlink) { |
253
|
|
|
$this->filesystem->symlink( |
254
|
|
|
str_replace($this->params->get('kernel.project_dir').'/', '../', $this->webDir.'/'.$entry), |
255
|
|
|
$this->app->getStaticDir().'/'.$entry |
256
|
|
|
); |
257
|
|
|
} else { |
258
|
|
|
$action = is_file($this->webDir.'/'.$entry) ? 'copy' : 'mirror'; |
259
|
|
|
$this->filesystem->$action($this->webDir.'/'.$entry, $this->app->getStaticDir().'/'.$entry); |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
$dir->close(); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Copy or Symlink "not image" media to download folder. |
268
|
|
|
* |
269
|
|
|
* @return void |
270
|
|
|
*/ |
271
|
|
|
protected function copyMediaToDownload() |
272
|
|
|
{ |
273
|
|
|
$symlink = $this->mustSymlink(); |
274
|
|
|
|
275
|
|
|
if (!file_exists($this->app->getStaticDir().'/download')) { |
276
|
|
|
$this->filesystem->mkdir($this->app->getStaticDir().'/download/'); |
277
|
|
|
$this->filesystem->mkdir($this->app->getStaticDir().'/download/media'); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
$dir = dir($this->webDir.'/../media'); |
281
|
|
|
while (false !== $entry = $dir->read()) { |
282
|
|
|
if ('.' == $entry || '..' == $entry) { |
283
|
|
|
continue; |
284
|
|
|
} |
285
|
|
|
// if the file is an image, it's ever exist (maybe it's slow to check every files) |
286
|
|
|
if (!file_exists($this->webDir.'/media/default/'.$entry)) { |
287
|
|
|
if (true === $symlink) { |
288
|
|
|
$this->filesystem->symlink( |
289
|
|
|
'../../../media/'.$entry, |
290
|
|
|
$this->app->getStaticDir().'/download/media/'.$entry |
291
|
|
|
); |
292
|
|
|
} else { |
293
|
|
|
$this->filesystem->copy( |
294
|
|
|
$this->webDir.'/../media/'.$entry, |
295
|
|
|
$this->app->getStaticDir().'/download/media/'.$entry |
296
|
|
|
); |
297
|
|
|
} |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
//$this->filesystem->$action($this->webDir.'/../media', $this->app->getStaticDir().'/download/media'); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
protected function generateSitemaps(): void |
305
|
|
|
{ |
306
|
|
|
foreach (explode('|', $this->params->get('pwc.locales')) as $locale) { |
307
|
|
|
foreach (['txt', 'xml'] as $format) { |
308
|
|
|
$this->generateSitemap($locale, $format); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
$this->generateFeed($locale); |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
protected function generateSitemap($locale, $format) |
316
|
|
|
{ |
317
|
|
|
$liveUri = $this->generateLivePathFor( |
318
|
|
|
$this->app->getMainHost(), |
319
|
|
|
'piedweb_cms_page_sitemap', |
320
|
|
|
['locale' => $locale, '_format' => $format] |
321
|
|
|
); |
322
|
|
|
$staticFile = $this->app->getStaticDir().'/sitemap'.$locale.'.'.$format; // todo get it from URI removing host |
323
|
|
|
$this->saveAsStatic($liveUri, $staticFile); |
324
|
|
|
|
325
|
|
|
if ($this->params->get('locale') == $locale ? '' : '.'.$locale) { |
326
|
|
|
$staticFile = $this->app->getStaticDir().'/sitemap.'.$format; |
327
|
|
|
$this->saveAsStatic($liveUri, $staticFile); |
328
|
|
|
} |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
protected function generateFeed($locale) |
332
|
|
|
{ |
333
|
|
|
$liveUri = $this->generateLivePathFor( |
334
|
|
|
$this->app->getMainHost(), |
335
|
|
|
'piedweb_cms_page_main_feed', |
336
|
|
|
['locale' => $locale] |
337
|
|
|
); |
338
|
|
|
$staticFile = $this->app->getStaticDir().'/feed'.$locale.'.xml'; |
339
|
|
|
$this->saveAsStatic($liveUri, $staticFile); |
340
|
|
|
|
341
|
|
|
if ($this->params->get('locale') == $locale ? '' : '.'.$locale) { |
342
|
|
|
$staticFile = $this->app->getStaticDir().'/feed.xml'; |
343
|
|
|
$this->saveAsStatic($liveUri, $staticFile); |
344
|
|
|
} |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* The function cache redirection found during generatePages and |
349
|
|
|
* format in self::$redirection the content for the .htaccess. |
350
|
|
|
* |
351
|
|
|
* @return void |
352
|
|
|
*/ |
353
|
|
|
protected function addRedirection(Page $page) |
354
|
|
|
{ |
355
|
|
|
$this->redirections .= 'Redirect '; |
356
|
|
|
$this->redirections .= $page->getRedirectionCode().' '; |
357
|
|
|
$this->redirections .= $this->pageCanonical->generatePathForPage($page->getRealSlug()); |
358
|
|
|
$this->redirections .= ' '.$page->getRedirection(); |
359
|
|
|
$this->redirections .= PHP_EOL; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
protected function generatePages(): void |
363
|
|
|
{ |
364
|
|
|
$qb = $this->getPageRepository()->getQueryToFindPublished('p'); |
365
|
|
|
$qb = $this->getPageRepository()->andHost($qb, $this->app->getMainHost(), $this->mustGetPagesWithoutHost); |
366
|
|
|
$pages = $qb->getQuery()->getResult(); |
367
|
|
|
|
368
|
|
|
foreach ($pages as $page) { |
369
|
|
|
$this->generatePage($page); |
370
|
|
|
//if ($page->getRealSlug()) $this->generateFeedFor($page); |
371
|
|
|
} |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
protected function generatePage(Page $page) |
375
|
|
|
{ |
376
|
|
|
// check if it's a redirection |
377
|
|
|
if (false !== $page->getRedirection()) { |
378
|
|
|
$this->addRedirection($page); |
379
|
|
|
|
380
|
|
|
return; |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
$this->saveAsStatic($this->generateLivePathFor($page), $this->generateFilePath($page)); |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
protected function saveAsStatic($liveUri, $destination) |
387
|
|
|
{ |
388
|
|
|
$request = Request::create($liveUri); |
389
|
|
|
|
390
|
|
|
$response = static::$appKernel->handle($request); |
391
|
|
|
|
392
|
|
|
if ($response->isRedirect()) { |
393
|
|
|
// todo |
394
|
|
|
//$this->addRedirection($liveUri, getRedirectUri) |
395
|
|
|
return; |
396
|
|
|
} elseif (200 != $response->getStatusCode()) { |
397
|
|
|
//$this->kernel = static::$appKernel; |
398
|
|
|
if (500 === $response->getStatusCode() && 'dev' == $this->kernel->getEnvironment()) { |
399
|
|
|
exit($this->kernel->handle($request)); |
|
|
|
|
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
return; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
$content = $this->compress($response->getContent()); |
406
|
|
|
$this->filesystem->dumpFile($destination, $content); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
protected function compress($html) |
410
|
|
|
{ |
411
|
|
|
return $this->parser->compress($html); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
protected function generateFilePath(Page $page) |
415
|
|
|
{ |
416
|
|
|
$slug = '' == $page->getRealSlug() ? 'index' : $page->getRealSlug(); |
417
|
|
|
$route = $this->pageCanonical->generatePathForPage($slug); |
418
|
|
|
|
419
|
|
|
return $this->app->getStaticDir().$route.'.html'; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Generate static file for feed indexing children pages |
424
|
|
|
* (only if children pages exists). |
425
|
|
|
* |
426
|
|
|
* @return void |
427
|
|
|
*/ |
428
|
|
|
protected function generateFeedFor(Page $page) |
429
|
|
|
{ |
430
|
|
|
$liveUri = $this->generateLivePathFor($page, 'piedweb_cms_page_feed'); |
431
|
|
|
$staticFile = preg_replace('/.html$/', '.xml', $this->generateFilePath($page)); |
432
|
|
|
$this->saveAsStatic($liveUri, $staticFile); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
protected function generateErrorPages(): void |
436
|
|
|
{ |
437
|
|
|
$this->generateErrorPage(); |
438
|
|
|
|
439
|
|
|
// todo i18n error in .htaccess |
440
|
|
|
$locales = explode('|', $this->params->get('pwc.locales')); |
441
|
|
|
|
442
|
|
|
foreach ($locales as $locale) { |
443
|
|
|
$this->filesystem->mkdir($this->app->getStaticDir().'/'.$locale); |
444
|
|
|
$this->generateErrorPage($locale); |
445
|
|
|
} |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
protected function generateErrorPage($locale = null, $uri = '404.html') |
449
|
|
|
{ |
450
|
|
|
if (null !== $locale) { |
451
|
|
|
$request = new Request(); |
452
|
|
|
$request->setLocale($locale); |
453
|
|
|
$this->requesStack->push($request); |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
$dump = $this->parser->compress($this->twig->render('@Twig/Exception/error.html.twig')); |
457
|
|
|
$this->filesystem->dumpFile($this->app->getStaticDir().(null !== $locale ? '/'.$locale : '').'/'.$uri, $dump); |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
protected function getPageRepository(): PageRepository |
461
|
|
|
{ |
462
|
|
|
return $this->em->getRepository($this->params->get('pwc.entity_page')); |
463
|
|
|
} |
464
|
|
|
} |
465
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.