PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / view / View.php

View.php in 404 Solution trunk, at includes/view/View.php

496 lines 21.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /* Turns data into an html display and vice versa.
9 * Houses all displayed pages. Logs, options page, captured 404s, stats, etc. */
10
11 require_once __DIR__ . '/ViewComponent.php';
12 require_once __DIR__ . '/View_Shared.php';
13 require_once __DIR__ . '/View_UI.php';
14 require_once __DIR__ . '/View_AdminChrome.php';
15 require_once __DIR__ . '/View_OptionsPresenter.php';
16 require_once __DIR__ . '/../diagnostics/ToolsDiagnostics.php';
17 require_once __DIR__ . '/View_Stats.php';
18 require_once __DIR__ . '/View_Tools.php';
19 require_once __DIR__ . '/View_Settings.php';
20 require_once __DIR__ . '/View_SettingsSections.php';
21 require_once __DIR__ . '/View_SimpleSettings.php';
22 require_once __DIR__ . '/View_Redirects.php';
23 require_once __DIR__ . '/View_RedirectsTable.php';
24 require_once __DIR__ . '/View_CapturedURLsTable.php';
25 require_once __DIR__ . '/View_RedirectForms.php';
26 require_once __DIR__ . '/View_ListTableChrome.php';
27 require_once __DIR__ . '/View_RedirectTypeUI.php';
28 require_once __DIR__ . '/View_RedirectConditions.php';
29 require_once __DIR__ . '/View_Logs.php';
30
31 /**
32 * Magic-method dispatch surface for the View facade. Each entry below points
33 * at a method that lives on one of the ABJ_404_Solution_View_* components
34 * (Shared, UI, AdminChrome, OptionsPresenter, Stats, Tools, Settings, SettingsSections,
35 * SimpleSettings, Redirects, RedirectsTable,
36 * RedirectTypeUI, RedirectConditions, Logs). __call routes the call to whichever component
37 * owns the method. PHPStan needs these annotations because it cannot trace
38 * __call dispatch through the component array.
39 *
40 * @method mixed buildRedirectRowHTML(array<mixed> $row, string $sub, array<mixed> $tableOptions, array<mixed> $deadDestIds, int $y)
41 * @method mixed buildRedirectToDropdownHtml(string $pageTitle, string $pageIDAndType)
42 * @method mixed buildRedirectsColumnDefs(array<mixed> $tableOptions)
43 * @method mixed buildScoreCell($rawScore, string $rowEngine, int $rowStatus)
44 * @method mixed buildTableActionLinks($row, $sub, $tableOptions, $isCapturedPage = false)
45 * @method mixed echoAddManualRedirect($tableOptions)
46 * @method mixed echoAddRedirectModal($tableOptions)
47 * @method mixed echoAdminCapturedURLsPage()
48 * @method mixed echoAdminDebugFile()
49 * @method mixed echoAdminEditRedirectPage()
50 * @method mixed echoAdminFooter()
51 * @method mixed echoAdminLogsPage()
52 * @method mixed echoAdminOptionsPage()
53 * @method mixed echoAdminRedirectsPage()
54 * @method mixed echoAdminToolsPage()
55 * @method mixed echoBrokenInternalLinksSection()
56 * @method mixed echoChosenAdminTab($action, $sub, $message)
57 * @method mixed echoConditionRow($index, array<mixed> $cond)
58 * @method mixed echoConditionsJavaScript()
59 * @method mixed echoConfidenceDistributionSection()
60 * @method string buildSubsubsubFilters(string $sub, array<int, array{0: int|string, 1: string}> $items, array<string, mixed> $tableOptions)
61 * @method mixed echoEditRedirect(ABJ_404_Solution_EditRedirectFormContext $ctx)
62 * @method mixed echoExpandCollapseButton($showSuggestions = true)
63 * @method mixed echoFileContents($fileName)
64 * @method mixed echoInlineModeToggle()
65 * @method mixed echoOptionsSection(ABJ_404_Solution_OptionsSectionView $section)
66 * @method mixed echoPostBox($id, $title, $content)
67 * @method mixed echoRedirectConditionsSection()
68 * @method mixed echoRedirectDestinationOptionsCatsTags($dest)
69 * @method mixed echoRedirectDestinationOptionsDefaults($currentlySelected)
70 * @method mixed echoRedirectDestinationOptionsOthers($dest, $rows)
71 * @method mixed echoRedirectTypeButtonGrid(string $selectedCode)
72 * @method mixed echoRestoreDefaultsModal()
73 * @method mixed echoSettingsModeToggle($currentMode)
74 * @method mixed echoSimpleModeOptions($options)
75 * @method mixed echoStickySaveBar()
76 * @method mixed echoToastNotification()
77 * @method mixed echoTrendsSection()
78 * @method mixed fillRedirectRowTemplate(array<mixed> $replacements)
79 * @method string getAdminLogsPageTable($sub, array<string, mixed> $tableOptionOverrides = array())
80 * @method mixed getAdminOptionsPageAdvancedContent($options)
81 * @method mixed getAdminOptionsPageAdvancedLogging($options)
82 * @method mixed getAdminOptionsPageAdvancedSystem($options)
83 * @method mixed getAdminOptionsPageAutoRedirects($options)
84 * @method mixed getAdminOptionsPageGeneralSettings($options)
85 * @method string getAdminRedirectsPageTable($sub, array<string, mixed> $tableOptionOverrides = array())
86 * @method mixed getBehaviorTilesHTML($options)
87 * @method mixed getBulkOperationsFormURL($sub, $tableOptions)
88 * @method string getCapturedURLSPageTable($sub, array<string, mixed> $tableOptionOverrides = array())
89 * @method string getCardIcon(string $iconName)
90 * @method mixed getCheckedAttr($options, $key)
91 * @method string computeTableDataSignature($sub, array<string, mixed> $tableOptionOverrides = array())
92 * @method string getCurrentTableDataSignature($sub)
93 * @method mixed getDashboardNotificationCaptured($captured)
94 * @method mixed getFallbackOptionDefaults()
95 * @method mixed getHeaderSortState($tableOptions, $orderby, $preferDescOnFirstClick = false)
96 * @method mixed getMigrateFromPluginMarkup()
97 * @method mixed getOptionsWithDefaults()
98 * @method string getPaginationLinks($sub, array<string, mixed> $tableOptionOverrides = array())
99 * @method mixed getSignatureFieldsForSubpage($sub, $row)
100 * @method mixed getSubSubSub($sub)
101 * @method mixed getSuggestedDestination(string $url, array<mixed> $options)
102 * @method mixed getTabFilters($sub, $tableOptions)
103 * @method mixed getTableColumns($sub, $columns)
104 * @method mixed getToolsDiagnosticsMarkup()
105 * @method mixed getToolsDiagnosticsRows()
106 * @method mixed humanizeEngineName(string $rawName)
107 * @method mixed normalizeOptionsForView($options)
108 * @method mixed normalizeSignatureValue($value)
109 * @method mixed optStr($options, $key, $default = '')
110 * @method mixed outputAdminHeaderTabs($sub = 'list', $message = '')
111 * @method mixed outputAdminStatsPage()
112 * @method mixed rememberTableDataSignature($sub, $rows)
113 * @method mixed renderBulkRedirectFormFields(array<mixed> $context, array<mixed> $recnums_multiple)
114 * @method mixed renderRegexAutoPromoteNotice(array<mixed> $notice)
115 * @method mixed renderSuggestionBlock(array<mixed> $suggestion)
116 * @method mixed resolveDestinationWarnings(ABJ_404_Solution_RedirectDestinationWarningContext $ctx)
117 * @method mixed resolveRedirectDestLink($rowType, string $rowFinalDest)
118 * @method mixed resolveRedirectDestinationInfo(array<mixed> $redirect, array<mixed> $options)
119 * @method mixed traceOutcomeClass(string $outcome)
120 * @method mixed translateTraceLabel(string $text)
121 * @method mixed viewGetPostOrGetSanitize($name, $defaultValue = null)
122 */
123 class ABJ_404_Solution_View {
124
125 /** @var self|null */
126 private static $instance = null;
127 /**
128 * Test seam: install or clear the cached singleton instance without
129 * private-field reflection. Pass null to reset between tests; pass a
130 * configured instance (or double) to install it. Mirrors the setInstance()
131 * contract on DataAccess / PluginLogic (M105 singleton-reset seam).
132 *
133 * @param self|null $instance
134 * @return void
135 */
136 public static function setInstance($instance) {
137 self::$instance = $instance;
138 }
139
140
141 /** @var ABJ_404_Solution_Functions */
142 private $f;
143
144 /** @var ABJ_404_Solution_PluginLogic */
145 private $logic;
146
147 /** @var ABJ_404_Solution_Logging */
148 private $logger;
149
150 /** @var ABJ_404_Solution_ViewReadServiceInterface */
151 private $viewReadService;
152
153 /** @var ABJ_404_Solution_LogsRepositoryInterface */
154 private $logsRepository;
155
156 /** @var ABJ_404_Solution_RedirectsRepositoryInterface */
157 private $redirectsRepository;
158
159 /** @var ABJ_404_Solution_ContentRepositoryInterface */
160 private $contentRepository;
161
162 /** @var ABJ_404_Solution_StatsRepositoryInterface */
163 private $statsRepository;
164
165 /** @var array<int, ABJ_404_Solution_ViewComponent> */
166 private $components = array();
167
168 /**
169 * Constructor with dependency injection.
170 *
171 * @param ABJ_404_Solution_Functions|null $functions String manipulation utilities
172 * @param ABJ_404_Solution_PluginLogic|null $pluginLogic Business logic service
173 * @param mixed $dataAccessOrViewReadService Data access (legacy) or ViewReadService
174 * @param ABJ_404_Solution_Logging|null $logging Logging service
175 * @param ABJ_404_Solution_StatsRepositoryInterface|null $statsRepository Stats repository
176 */
177 public function __construct($functions = null, $pluginLogic = null, $dataAccessOrViewReadService = null, $logging = null, $statsRepository = null) {
178 $this->f = $functions !== null ? $functions : abj_service('functions');
179 $this->logic = $pluginLogic !== null ? $pluginLogic : abj_service('plugin_logic');
180 $this->logger = $logging !== null ? $logging : abj_service('logging');
181
182 // When a DataAccess facade is injected (legacy callers / tests),
183 // use it directly for all module interfaces -- it delegates to the
184 // real modules internally.
185 $dao = $dataAccessOrViewReadService;
186 $isLegacyDao = ($dao !== null && is_object($dao)
187 && !($dao instanceof ABJ_404_Solution_ViewReadServiceInterface)
188 && (method_exists($dao, 'getRedirectsForView') || $dao instanceof ABJ_404_Solution_DataAccess));
189
190 if ($isLegacyDao) {
191 /** @var ABJ_404_Solution_ViewReadServiceInterface&ABJ_404_Solution_RedirectsRepositoryInterface&ABJ_404_Solution_StatsRepositoryInterface $dao */
192 $this->viewReadService = $dao;
193 // Resolve typed repository surfaces off the DataAccess facade so the
194 // View talks to the real LogsRepository / ContentRepository objects.
195 // Pass-throughs for these repos have been removed from DataAccess,
196 // so resolve the typed repositories from the facade when available.
197 $this->logsRepository = (is_object($dao) && method_exists($dao, 'getLogsRepo'))
198 ? $dao->getLogsRepo()
199 : $dao;
200 $this->redirectsRepository = $dao;
201 $this->contentRepository = (is_object($dao) && method_exists($dao, 'getContentRepo'))
202 ? $dao->getContentRepo()
203 : $dao;
204 $this->statsRepository = $this->resolveStatsRepository($statsRepository, $dao);
205 } else {
206 /** @var ABJ_404_Solution_ViewReadServiceInterface $vrs */
207 $vrs = abj_service('view_read_service');
208 $this->viewReadService = $vrs;
209 /** @var ABJ_404_Solution_LogsRepositoryInterface $lr */
210 $lr = abj_service('logs_repository');
211 $this->logsRepository = $lr;
212 /** @var ABJ_404_Solution_RedirectsRepositoryInterface $rr */
213 $rr = abj_service('redirects_repository');
214 $this->redirectsRepository = $rr;
215 /** @var ABJ_404_Solution_ContentRepositoryInterface $cr */
216 $cr = abj_service('content_repository');
217 $this->contentRepository = $cr;
218 /** @var ABJ_404_Solution_StatsRepositoryInterface $sr */
219 $sr = $statsRepository !== null ? $statsRepository : $this->resolveStatsRepository(null, null);
220 $this->statsRepository = $sr;
221 }
222
223 $this->components = $this->buildComponents();
224 }
225
226 /** @return self */
227 public static function getInstance() {
228 if (self::$instance !== null) {
229 return self::$instance;
230 }
231
232 // If the DI container is initialized, prefer it.
233 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
234 $resolved = ABJ_404_Solution_ServiceContainer::safeGet('view');
235 if ($resolved instanceof self) {
236 self::$instance = $resolved;
237 return self::$instance;
238 }
239 }
240
241 self::$instance = new ABJ_404_Solution_View();
242
243 return self::$instance;
244 }
245
246 /**
247 * @param ABJ_404_Solution_StatsRepositoryInterface|null $provided
248 * @param mixed $legacyDao
249 * @return ABJ_404_Solution_StatsRepositoryInterface
250 */
251 private function resolveStatsRepository($provided, $legacyDao): ABJ_404_Solution_StatsRepositoryInterface {
252 if ($provided instanceof ABJ_404_Solution_StatsRepositoryInterface) {
253 return $provided;
254 }
255
256 $service = class_exists('ABJ_404_Solution_ServiceContainer')
257 ? ABJ_404_Solution_ServiceContainer::safeGet('stats_repository')
258 : null;
259 if ($service instanceof ABJ_404_Solution_StatsRepositoryInterface) {
260 return $service;
261 }
262
263 return ABJ_404_Solution_StatsRepositoryResolver::resolve(__CLASS__);
264 }
265
266 /**
267 * @return array<int, ABJ_404_Solution_ViewComponent>
268 */
269 private function buildComponents() {
270 $args = array(
271 $this,
272 $this->f,
273 $this->logic,
274 $this->logger,
275 $this->viewReadService,
276 $this->logsRepository,
277 $this->redirectsRepository,
278 $this->contentRepository,
279 $this->statsRepository,
280 );
281
282 $shared = new ABJ_404_Solution_View_Shared(...$args);
283 $ui = new ABJ_404_Solution_View_UI(...$args);
284 $adminChrome = new ABJ_404_Solution_View_AdminChrome(...$args);
285 $optionsPresenter = new ABJ_404_Solution_View_OptionsPresenter(...$args);
286 $stats = new ABJ_404_Solution_View_Stats(...$args);
287 $tools = new ABJ_404_Solution_View_Tools(...$args);
288 $settings = new ABJ_404_Solution_View_Settings(...$args);
289 $settingsSections = new ABJ_404_Solution_View_SettingsSections(...$args);
290 $simpleSettings = new ABJ_404_Solution_View_SimpleSettings(...$args);
291 $redirects = new ABJ_404_Solution_View_Redirects(...$args);
292 $redirectsTable = new ABJ_404_Solution_View_RedirectsTable(...$args);
293 $capturedURLsTable = new ABJ_404_Solution_View_CapturedURLsTable(...$args);
294 $redirectForms = new ABJ_404_Solution_View_RedirectForms(...$args);
295 $listTableChrome = new ABJ_404_Solution_View_ListTableChrome(...$args);
296 $redirectTypeUI = new ABJ_404_Solution_View_RedirectTypeUI(...$args);
297 $redirectConditions = new ABJ_404_Solution_View_RedirectConditions(...$args);
298 $logs = new ABJ_404_Solution_View_Logs(...$args);
299
300 $components = array(
301 $shared, $ui, $adminChrome, $optionsPresenter, $stats, $tools,
302 $settings, $settingsSections, $simpleSettings, $redirects, $redirectsTable,
303 $capturedURLsTable, $redirectForms, $listTableChrome,
304 $redirectTypeUI, $redirectConditions, $logs,
305 );
306 foreach ($components as $component) {
307 $component->setSiblingComponents(
308 $shared, $ui, $adminChrome, $optionsPresenter, $stats, $tools,
309 $settings, $settingsSections, $simpleSettings, $redirects,
310 $redirectsTable, $capturedURLsTable, $redirectForms, $listTableChrome,
311 $redirectTypeUI, $redirectConditions, $logs
312 );
313 }
314
315 return $components;
316 }
317
318 /**
319 * Magic method dispatch. Routes the call to whichever component owns the
320 * named method. Replaces the trait-based composition with explicit
321 * delegation while preserving the existing call surface ($view->methodX()).
322 *
323 * @param string $name
324 * @param array<int,mixed> $arguments
325 * @return mixed
326 */
327 public function __call($name, $arguments) {
328 foreach ($this->components as $component) {
329 if (method_exists($component, $name)) {
330 return $component->{$name}(...$arguments);
331 }
332 }
333
334 throw new BadMethodCallException(
335 'ABJ_404_Solution_View has no method ' . $name
336 );
337 }
338
339 /**
340 * Static forwarder for the renderer of error-notice + support-button HTML.
341 * Used by callers that don't have the View instance handy.
342 *
343 * @param string $messageHtml
344 * @param string $triggeredFrom
345 * @param string|null $contextSummary
346 * @return string
347 */
348 public static function renderErrorNoticeWithSupportButton(string $messageHtml,
349 string $triggeredFrom, ?string $contextSummary = null): string {
350 return ABJ_404_Solution_View_UI::renderErrorNoticeWithSupportButton(
351 $messageHtml,
352 $triggeredFrom,
353 $contextSummary
354 );
355 }
356
357 /**
358 * Maintain the logs_hits rollup when a plugin admin page renders. The policy
359 * records staleness and schedules a rebuild only when the rollup is missing
360 * or behind the source log table.
361 *
362 * @return void
363 */
364 private function maintainLogsHitsRollupOnAdminPageLoad(): void {
365 try {
366 $this->viewReadService->maybeUpdateRedirectsForViewHitsTable();
367 } catch (\Throwable $e) {
368 $this->logger->warn(
369 'logs_hits rollup maintenance failed during admin page render: '
370 . get_class($e) . ' code=' . (string)$e->getCode() . ' message=' . $e->getMessage()
371 );
372 }
373 }
374
375 /**
376 * Static entry point for WP admin page rendering. Routes admin POST actions
377 * (trash / delete / ignore / edit / import) to PluginLogic and then renders
378 * the selected subpage. Needs to live on the View facade itself because it
379 * touches the facade's private $logic / $logger / $components fields, which
380 * View_UI (the rest of the UI surface lives there) cannot reach from
381 * outside the class scope.
382 *
383 * @return void
384 */
385 public static function handleMainAdminPageActionAndDisplay() {
386 global $abj404view;
387 $instance = self::getInstance();
388
389 try {
390 $action = (string)$instance->viewGetPostOrGetSanitize('action');
391
392 if (!is_admin() || !abj_service('admin_access_policy')->isPluginAdmin()) {
393 $instance->logger->logUserCapabilities("handleMainAdminPageActionAndDisplay (" .
394 esc_html($action == '' ? '(none)' : $action) . ")");
395
396 $permMessageTpl = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/adminPagePermissionDeniedMessage.html');
397 $permMessage = str_replace(
398 ['{permission_denied_label}', '{access_explanation}', '{verify_role_prefix}', '{capability_word}', '{security_plugin_note}'],
399 [
400 esc_html__('Permission denied.', '404-solution'),
401 esc_html__('Your user account does not have permission to access this page.', '404-solution'),
402 esc_html__('Please verify that your WordPress role has the', '404-solution'),
403 esc_html__('capability.', '404-solution'),
404 esc_html__('If you have a security plugin installed, it may be restricting access to this page.', '404-solution'),
405 ],
406 $permMessageTpl
407 );
408 $subpageForContext = (string)$instance->viewGetPostOrGetSanitize('subpage');
409 $triggerForPerm = ($subpageForContext === 'abj404_captured')
410 ? 'captured_404s_page' : 'redirects_page';
411 $permNotice = self::renderErrorNoticeWithSupportButton(
412 $permMessage,
413 $triggerForPerm,
414 'Permission denied on plugin admin page (action=' .
415 ($action == '' ? '(none)' : $action) . ')'
416 );
417 $permWrap = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/adminPagePermissionDeniedWrap.html');
418 echo str_replace(
419 ['{plugin_name}', '{notice}'],
420 [esc_html(PLUGIN_NAME), $permNotice],
421 $permWrap
422 );
423 return;
424 }
425
426 // Close any missing plugin table before drawing anything. The bounded
427 // counterpart to the boot prologue, and it contains its own failures;
428 // see DatabaseUpgradeSelfHeal::repairMissingTablesForRequest() for why
429 // a render has to ask for this rather than cause it by accident.
430 // After the authorization gate on purpose: an unauthorized visitor
431 // must not be able to make the site issue DDL.
432 abj_service('database_upgrades')->repairMissingTablesForRequest();
433
434 $instance->maintainLogsHitsRollupOnAdminPageLoad();
435
436 $sub = "";
437
438 // Handle post actions.
439 $instance->logger->debugMessage("Processing request for action: " .
440 esc_html($action == '' ? '(none)' : $action));
441
442 $message = "";
443 $message .= $instance->logic->adminActions()->handlePluginAction($action, $sub);
444 $message .= $instance->logic->adminActions()->hanldeTrashAction();
445 $message .= $instance->logic->adminActions()->handleDeleteAction();
446 $message .= $instance->logic->adminActions()->handleIgnoreAction();
447 $message .= $instance->logic->adminActions()->handleLaterAction();
448 $message .= $instance->logic->adminActions()->handleActionEdit($sub, $action);
449 $message .= $instance->logic->adminActions()->handleActionImportRedirects();
450 $instance->logic->adminActions()->handleActionChangeItemsPerRow();
451 $message .= $instance->logic->adminActions()->handleActionImportFile();
452
453 if ($action !== '' && $message !== '') {
454 $instance->logger->debugMessage("Admin action completed: " .
455 esc_html($action) . " => " . esc_html(substr($message, 0, 200)));
456 }
457
458 // Output the correct subpage.
459 $abj404view->echoChosenAdminTab($action, $sub, $message);
460
461 } catch (\Throwable $e) {
462 $encodedEx = json_encode($e);
463 $encodedContext = is_string($encodedEx) ? stripcslashes(wp_kses_post($encodedEx)) : '';
464 $renderFailureMessage = "Caught exception (" . get_class($e) . "): " . $e->getMessage()
465 . ($encodedContext !== '' ? " | context=" . $encodedContext : '');
466 // An admin page rendered while WordPress is replacing this plugin's
467 // own files cannot resolve a class the incoming release added
468 // (production report 266). The admin still sees the render-error
469 // panel built below; only the severity changes, so a self-healing
470 // hosting event stops emailing the maintainer.
471 if (function_exists('abj404_logCallbackFailure')) {
472 abj404_logCallbackFailure($instance->logger, $renderFailureMessage, $e);
473 } else {
474 $instance->logger->errorMessage($renderFailureMessage);
475 }
476 $subpageForContext = (string)$instance->viewGetPostOrGetSanitize('subpage');
477 $triggerForRenderError = ($subpageForContext === 'abj404_captured')
478 ? 'captured_404s_page' : 'redirects_page';
479 $renderErrorMessageTpl = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/adminPageRenderErrorMessage.html');
480 $renderErrorMessage = str_replace(
481 '{escaped_details}',
482 esc_html($e->getMessage() . "\n" . $e->getTraceAsString()),
483 $renderErrorMessageTpl
484 );
485 $renderErrorNotice = self::renderErrorNoticeWithSupportButton(
486 $renderErrorMessage,
487 $triggerForRenderError,
488 'Render error: ' . substr($e->getMessage(), 0, 200)
489 );
490 $renderErrorWrap = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/adminPageRenderErrorWrap.html');
491 echo str_replace('{notice}', $renderErrorNotice, $renderErrorWrap);
492 }
493 }
494
495 }
496