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
Push — main ( d9cfb9...10f5c7 )
by Dan
32s queued 21s
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