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 (#1446)
by Dan
06:16
created

Template   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 265
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 125
c 2
b 0
f 1
dl 0
loc 265
rs 5.04
wmc 57

14 Methods

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

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 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
		$gameDirs = array_unique([get_game_dir(), 'Default/']);
104
105
		foreach ($gameDirs as $gameDir) {
106
			foreach ($templateDirs as $templateDir) {
107
				$filePath = TEMPLATES . $templateDir . 'engine/' . $gameDir . $templateName;
108
				if (is_file($filePath)) {
109
					return $filePath;
110
				}
111
			}
112
		}
113
		foreach ($templateDirs as $templateDir) {
114
			$filePath = TEMPLATES . $templateDir . $templateName;
115
			if (is_file($filePath)) {
116
				return $filePath;
117
			}
118
		}
119
		throw new Exception('No template found for ' . $templateName);
120
	}
121
122
	/**
123
	 * @param array<string, mixed> $assignVars
124
	 */
125
	protected function includeTemplate(string $templateName, array $assignVars = null): void {
126
		if ($this->nestedIncludes > 15) {
127
			throw new Exception('Nested more than 15 template includes, is something wrong?');
128
		}
129
		foreach ($this->data as $key => $value) {
130
			$$key = $value;
131
		}
132
		if ($assignVars !== null) {
133
			foreach ($assignVars as $key => $value) {
134
				$$key = $value;
135
			}
136
		}
137
		$this->nestedIncludes++;
138
		require($this->getTemplateLocation($templateName));
139
		$this->nestedIncludes--;
140
	}
141
142
	/**
143
	 * Check if the HTML includes input elements where the user is able to
144
	 * input data (i.e. we don't want to AJAX update a form that they may
145
	 * have already started filling out).
146
	 */
147
	protected function checkDisableAJAX(string $html): bool {
148
		return preg_match('/<input (?![^>]*(submit|hidden|image))/i', $html) != 0;
149
	}
150
151
	protected function doDamageTypeReductionDisplay(int &$damageTypes): void {
152
		if ($damageTypes == 3) {
153
			echo ', ';
154
		} elseif ($damageTypes == 2) {
155
			echo ' and ';
156
		}
157
		$damageTypes--;
158
	}
159
160
	protected function doAn(string $wordAfter): string {
161
		$char = strtoupper($wordAfter[0]);
162
		return str_contains('AEIOU', $char) ? 'an' : 'a';
163
	}
164
165
	/*
166
	 * EVAL is special (well, will be when needed and implemented in the javascript).
167
	 */
168
	public function addJavascriptForAjax(string $varName, mixed $obj): string {
169
		if ($varName == 'EVAL') {
170
			if (!isset($this->ajaxJS['EVAL'])) {
171
				return $this->ajaxJS['EVAL'] = $obj;
172
			}
173
			return $this->ajaxJS['EVAL'] .= ';' . $obj;
174
		}
175
176
		if (isset($this->ajaxJS[$varName])) {
177
			throw new Exception('Trying to set javascript val twice: ' . $varName);
178
		}
179
		return $this->ajaxJS[$varName] = json_encode($obj);
180
	}
181
182
	protected function addJavascriptAlert(string $string): void {
183
		$session = Session::getInstance();
184
		if (!$session->addAjaxReturns('ALERT:' . $string, $string)) {
185
			$this->jsAlerts[] = $string;
186
		}
187
	}
188
189
	/**
190
	 * Registers a JS target for inclusion at the end of the HTML body.
191
	 */
192
	protected function addJavascriptSource(string $src): void {
193
		$this->jsSources[] = $src;
194
	}
195
196
	protected function convertHtmlToAjaxXml(string $str, bool $returnXml): string {
197
		if (empty($str)) {
198
			return '';
199
		}
200
201
		$session = Session::getInstance();
202
203
		$getInnerHTML = function(DOMNode $node): string {
204
			$innerHTML = '';
205
			foreach ($node->childNodes as $child) {
206
				$innerHTML .= $child->ownerDocument->saveHTML($child);
207
			}
208
			return $innerHTML;
209
		};
210
211
		// Helper function to canonicalize making an XML element,
212
		// with its inner content properly escaped.
213
		$xmlify = function(string $id, string $str): string {
214
			return '<' . $id . '>' . htmlspecialchars($str, ENT_XML1, 'utf-8') . '</' . $id . '>';
215
		};
216
217
		$xml = '';
218
		$dom = new DOMDocument();
219
		$dom->loadHTML($str);
220
		$xpath = new DOMXPath($dom);
221
222
		// Use relative xpath selectors so that they can be reused when we
223
		// pass the middle panel as the xpath query's context node.
224
		$ajaxSelectors = ['.//span[@id]', './/*[contains(@class,"ajax")]'];
225
226
		foreach ($ajaxSelectors as $selector) {
227
			$matchNodes = $xpath->query($selector);
228
			foreach ($matchNodes as $node) {
229
				$id = $node->getAttribute('id');
230
				$inner = $getInnerHTML($node);
231
				if (!$session->addAjaxReturns($id, $inner) && $returnXml) {
232
					$xml .= $xmlify($id, $inner);
233
				}
234
			}
235
		}
236
237
		// Determine if we should do ajax updates on the middle panel div
238
		$mid = $dom->getElementById('middle_panel');
239
		$doAjaxMiddle = true;
240
		if ($mid === null) {
241
			// Skip if there is no middle_panel.
242
			$doAjaxMiddle = false;
243
		} else {
244
			// Skip if middle_panel has ajax-enabled children.
245
			foreach ($ajaxSelectors as $selector) {
246
				if (count($xpath->query($selector, $mid)) > 0) {
247
					$doAjaxMiddle = false;
248
					break;
249
				}
250
			}
251
		}
252
253
		if ($doAjaxMiddle) {
254
			$inner = $getInnerHTML($mid);
255
			if (!$this->checkDisableAJAX($inner)) {
256
				$id = $mid->getAttribute('id');
257
				if (!$session->addAjaxReturns($id, $inner) && $returnXml) {
258
					$xml .= $xmlify($id, $inner);
259
				}
260
			}
261
		}
262
263
		$js = '';
264
		foreach ($this->ajaxJS as $varName => $JSON) {
265
			if (!$session->addAjaxReturns('JS:' . $varName, $JSON) && $returnXml) {
266
				$js .= $xmlify($varName, $JSON);
267
			}
268
		}
269
		if ($returnXml && count($this->jsAlerts) > 0) {
270
			$js = '<ALERT>' . json_encode($this->jsAlerts) . '</ALERT>';
271
		}
272
		if (strlen($js) > 0) {
273
			$xml .= '<JS>' . $js . '</JS>';
274
		}
275
		return $xml;
276
	}
277
278
}
279