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 (#1508)
by Dan
04:48
created

Template   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
eloc 133
dl 0
loc 276
rs 3.6
c 3
b 0
f 1
wmc 60

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getInstance() 0 2 1
A addJavascriptAlert() 0 4 2
A doAn() 0 3 2
A hasTemplateVar() 0 2 1
A checkDisableAJAX() 0 2 1
A addJavascriptSource() 0 2 1
B display() 0 35 7
A getTemplateLocation() 0 21 6
A includeTemplate() 0 15 5
F convertHtmlToAjaxXml() 0 90 23
A assign() 0 6 2
A unassign() 0 2 1
A doDamageTypeReductionDisplay() 0 7 3
A addJavascriptForAjax() 0 16 5

How to fix   Complexity   

Complex Class

Complex classes like Template often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Template, and based on these observations, apply Extract Interface, too.

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