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 ( 705f36...a28172 )
by Dan
32s queued 21s
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