Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Failed Conditions
Pull Request — main (#1487)
by Dan
06:01
created

Template::hasTemplateVar()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 2
rs 10
1
<?php declare(strict_types=1);
2
3
namespace Smr;
4
5
use DOMDocument;
6
use DOMNode;
7
use DOMXPath;
8
use Exception;
9
use Smr\Container\DiContainer;
10
11
class Template {
12
13
	/** @var array<string, mixed> */
14
	private array $data = [];
15
	private int $nestedIncludes = 0;
16
	/** @var array<string, mixed> */
17
	private array $ajaxJS = [];
18
	/** @var array<string> */
19
	protected array $jsAlerts = [];
20
	/** @var array<string> */
21
	protected array $jsSources = [];
22
23
	/**
24
	 * Defines a listjs_include.js function to call at the end of the HTML body.
25
	 */
26
	public ?string $listjsInclude = null;
27
28
	/**
29
	 * Return the Smr\Template in the DI container.
30
	 * If one does not exist yet, it will be created.
31
	 * This is the intended way to construct this class.
32
	 */
33
	public static function getInstance(): self {
34
		return DiContainer::get(self::class);
35
	}
36
37
	public function hasTemplateVar(string $var): bool {
38
		return isset($this->data[$var]);
39
	}
40
41
	public function assign(string $var, mixed $value): void {
42
		if (!isset($this->data[$var])) {
43
			$this->data[$var] = $value;
44
		} else {
45
			// We insist that template variables not change once they are set
46
			throw new Exception("Cannot re-assign template variable '$var'!");
47
		}
48
	}
49
50
	public function unassign(string $var): void {
51
		unset($this->data[$var]);
52
	}
53
54
	/**
55
	 * Displays the template HTML. Stores any ajax-enabled elements for future
56
	 * comparison, and outputs modified elements in XML for ajax if requested.
57
	 */
58
	public function display(string $templateName, bool $outputXml = false): void {
59
		// If we already started output buffering before calling `display`,
60
		// we may have unwanted content in the buffer that we need to remove
61
		// before we send the Content-Type headers below.
62
		// Skip this for debug builds to help discover offending output.
63
		if (!ENABLE_DEBUG) {
64
			if (ob_get_length() > 0) {
65
				ob_clean();
66
			}
67
		}
68
		ob_start();
69
		$this->includeTemplate($templateName);
70
		$output = ob_get_clean();
71
		if ($output === false) {
72
			throw new Exception('Output buffering is not active!');
73
		}
74
75
		$ajaxEnabled = ($this->data['AJAX_ENABLE_REFRESH'] ?? false) !== false;
76
		if ($ajaxEnabled) {
77
			$ajaxXml = $this->convertHtmlToAjaxXml($output, $outputXml);
78
			if ($outputXml) {
79
				/* Left out for size: <?xml version="1.0" encoding="ISO-8859-1"?>*/
80
				$output = '<all>' . $ajaxXml . '</all>';
81
			}
82
			$session = Session::getInstance();
83
			$session->saveAjaxReturns();
84
		}
85
86
		// Now that we are completely done processing, we can output
87
		if ($outputXml) {
88
			header('Content-Type: text/xml; charset=utf-8');
89
		} else {
90
			header('Content-Type: text/html; charset=utf-8');
91
		}
92
		echo $output;
93
	}
94
95
96
	protected function getTemplateLocation(string $templateName): string {
97
		if (isset($this->data['ThisAccount'])) {
98
			$templateDir = $this->data['ThisAccount']->getTemplate() . '/';
99
		} else {
100
			$templateDir = 'Default/';
101
		}
102
		$templateDirs = array_unique([$templateDir, 'Default/']);
103
104
		foreach ($templateDirs as $templateDir) {
105
			$filePath = TEMPLATES . $templateDir . 'engine/Default/' . $templateName;
106
			if (is_file($filePath)) {
107
				return $filePath;
108
			}
109
		}
110
		foreach ($templateDirs as $templateDir) {
111
			$filePath = TEMPLATES . $templateDir . $templateName;
112
			if (is_file($filePath)) {
113
				return $filePath;
114
			}
115
		}
116
		throw new Exception('No template found for ' . $templateName);
117
	}
118
119
	/**
120
	 * @param array<string, mixed> $assignVars
121
	 */
122
	protected function includeTemplate(string $templateName, array $assignVars = null): void {
123
		if ($this->nestedIncludes > 15) {
124
			throw new Exception('Nested more than 15 template includes, is something wrong?');
125
		}
126
		foreach ($this->data as $key => $value) {
127
			$$key = $value;
128
		}
129
		if ($assignVars !== null) {
130
			foreach ($assignVars as $key => $value) {
131
				$$key = $value;
132
			}
133
		}
134
		$this->nestedIncludes++;
135
		require($this->getTemplateLocation($templateName));
136
		$this->nestedIncludes--;
137
	}
138
139
	/**
140
	 * Check if the HTML includes input elements where the user is able to
141
	 * input data (i.e. we don't want to AJAX update a form that they may
142
	 * have already started filling out).
143
	 */
144
	protected function checkDisableAJAX(string $html): bool {
145
		return preg_match('/<input (?![^>]*(submit|hidden|image))/i', $html) != 0;
146
	}
147
148
	protected function doDamageTypeReductionDisplay(int &$damageTypes): void {
149
		if ($damageTypes == 3) {
150
			echo ', ';
151
		} elseif ($damageTypes == 2) {
152
			echo ' and ';
153
		}
154
		$damageTypes--;
155
	}
156
157
	protected function doAn(string $wordAfter): string {
158
		$char = strtoupper($wordAfter[0]);
159
		return str_contains('AEIOU', $char) ? 'an' : 'a';
160
	}
161
162
	/*
163
	 * EVAL is special (well, will be when needed and implemented in the javascript).
164
	 */
165
	public function addJavascriptForAjax(string $varName, mixed $obj): string {
166
		if ($varName == 'EVAL') {
167
			if (!isset($this->ajaxJS['EVAL'])) {
168
				return $this->ajaxJS['EVAL'] = $obj;
169
			}
170
			return $this->ajaxJS['EVAL'] .= ';' . $obj;
171
		}
172
173
		if (isset($this->ajaxJS[$varName])) {
174
			throw new Exception('Trying to set javascript val twice: ' . $varName);
175
		}
176
		return $this->ajaxJS[$varName] = json_encode($obj);
177
	}
178
179
	protected function addJavascriptAlert(string $string): void {
180
		$session = Session::getInstance();
181
		if (!$session->addAjaxReturns('ALERT:' . $string, $string)) {
182
			$this->jsAlerts[] = $string;
183
		}
184
	}
185
186
	/**
187
	 * Registers a JS target for inclusion at the end of the HTML body.
188
	 */
189
	protected function addJavascriptSource(string $src): void {
190
		$this->jsSources[] = $src;
191
	}
192
193
	protected function convertHtmlToAjaxXml(string $str, bool $returnXml): string {
194
		if (empty($str)) {
195
			return '';
196
		}
197
198
		$session = Session::getInstance();
199
200
		$getInnerHTML = function(DOMNode $node): string {
201
			$innerHTML = '';
202
			foreach ($node->childNodes as $child) {
203
				$innerHTML .= $child->ownerDocument->saveHTML($child);
204
			}
205
			return $innerHTML;
206
		};
207
208
		// Helper function to canonicalize making an XML element,
209
		// with its inner content properly escaped.
210
		$xmlify = function(string $id, string $str): string {
211
			return '<' . $id . '>' . htmlspecialchars($str, ENT_XML1, 'utf-8') . '</' . $id . '>';
212
		};
213
214
		$xml = '';
215
		$dom = new DOMDocument();
216
		$dom->loadHTML($str);
217
		$xpath = new DOMXPath($dom);
218
219
		// Use relative xpath selectors so that they can be reused when we
220
		// pass the middle panel as the xpath query's context node.
221
		$ajaxSelectors = ['.//span[@id]', './/*[contains(@class,"ajax")]'];
222
223
		foreach ($ajaxSelectors as $selector) {
224
			$matchNodes = $xpath->query($selector);
225
			foreach ($matchNodes as $node) {
226
				$id = $node->getAttribute('id');
227
				$inner = $getInnerHTML($node);
228
				if (!$session->addAjaxReturns($id, $inner) && $returnXml) {
229
					$xml .= $xmlify($id, $inner);
230
				}
231
			}
232
		}
233
234
		// Determine if we should do ajax updates on the middle panel div
235
		$mid = $dom->getElementById('middle_panel');
236
		$doAjaxMiddle = true;
237
		if ($mid === null) {
238
			// Skip if there is no middle_panel.
239
			$doAjaxMiddle = false;
240
		} else {
241
			// Skip if middle_panel has ajax-enabled children.
242
			foreach ($ajaxSelectors as $selector) {
243
				if (count($xpath->query($selector, $mid)) > 0) {
244
					$doAjaxMiddle = false;
245
					break;
246
				}
247
			}
248
		}
249
250
		if ($doAjaxMiddle) {
251
			$inner = $getInnerHTML($mid);
252
			if (!$this->checkDisableAJAX($inner)) {
253
				$id = $mid->getAttribute('id');
254
				if (!$session->addAjaxReturns($id, $inner) && $returnXml) {
255
					$xml .= $xmlify($id, $inner);
256
				}
257
			}
258
		}
259
260
		$js = '';
261
		foreach ($this->ajaxJS as $varName => $JSON) {
262
			if (!$session->addAjaxReturns('JS:' . $varName, $JSON) && $returnXml) {
263
				$js .= $xmlify($varName, $JSON);
264
			}
265
		}
266
		if ($returnXml && count($this->jsAlerts) > 0) {
267
			$js = '<ALERT>' . json_encode($this->jsAlerts) . '</ALERT>';
268
		}
269
		if (strlen($js) > 0) {
270
			$xml .= '<JS>' . $js . '</JS>';
271
		}
272
		return $xml;
273
	}
274
275
}
276