Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

SSViewerTest   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 1546
Duplicated Lines 1.94 %

Coupling/Cohesion

Components 5
Dependencies 25

Importance

Changes 2
Bugs 2 Features 0
Metric Value
c 2
b 2
f 0
dl 30
loc 1546
rs 0.5217
wmc 61
lcom 5
cbo 25

49 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 6 1
A tearDown() 0 4 1
A testCurrentTheme() 0 6 1
A testTemplateWithoutHeadRenders() 0 8 1
B testIncludeScopeInheritance() 0 27 1
B testIncludeTruthyness() 0 27 1
A getScopeInheritanceTestData() 0 13 1
A assertExpectedStrings() 0 8 2
A render() 0 5 2
A testRequirements() 0 14 1
B testRequirementsCombine() 0 45 6
A testComments() 0 21 1
A testBasicText() 0 6 1
A testBasicInjection() 0 14 1
A testGlobalVariableCalls() 0 5 1
A testGlobalVariableCallsWithArguments() 0 8 1
A testGlobalVariablesAreEscaped() 0 9 1
B testCoreGlobalVariableCalls() 0 39 1
A testNonFieldCastingHelpersNotUsedInHasValue() 0 11 1
A testLocalFunctionsTakePriorityOverGlobals() 0 23 1
B testCurrentScopeLoopWith() 0 40 1
A testObjectDotArguments() 0 20 1
A testEscapedArguments() 0 22 1
B testLoopWhitespace() 0 44 1
B testControls() 0 32 1
B testIfBlocks() 0 116 1
B testBaseTagGeneration() 0 41 1
B testIncludeWithArguments() 0 79 1
B testRecursiveInclude() 0 24 1
A assertEqualIgnoringWhitespace() 0 3 1
A testCastingHelpers() 0 54 1
B testSSViewerBasicIteratorSupport() 0 116 1
A testUpInWith() 0 50 1
A testUpInLoop() 0 72 1
B testNestedLoops() 0 37 1
A testLayout() 0 11 1
B testGetTemplatesByClass() 0 27 1
B testThemeRetrieval() 0 28 2
A testRewriteHashlinks() 0 57 1
B testRewriteHashlinksInPhpMode() 0 40 1
B testRenderWithSourceFileComments() 0 89 2
A _renderWithSourceFileComments() 0 8 1
A testLoopIteratorIterator() 0 6 1
B testProcessOnlyIncludesRequirementsOnce() 0 24 1
A testRequireCallInTemplateInclude() 0 17 2
B testCallsWithArguments() 0 38 2
A testClosedBlockExtension() 15 15 1
A testOpenBlockExtension() 15 15 1
B testFromStringCaching() 0 24 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SSViewerTest 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 SSViewerTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
class SSViewerTest extends SapphireTest {
4
5
	/**
6
	 * Backup of $_SERVER global
7
	 *
8
	 * @var array
9
	 */
10
	protected $oldServer = array();
11
12
	protected $extraDataObjects = array(
13
		'SSViewerTest_Object',
14
	);
15
16
	public function setUp() {
17
		parent::setUp();
18
		Config::inst()->update('SSViewer', 'source_file_comments', false);
19
		Config::inst()->update('SSViewer_FromString', 'cache_template', false);
20
		$this->oldServer = $_SERVER;
21
	}
22
23
	public function tearDown() {
24
		$_SERVER = $this->oldServer;
25
		parent::tearDown();
26
	}
27
28
	/**
29
	 * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
30
	 * of user defined themes via {@link SiteConfig} and default theme
31
	 * when no user themes are defined.
32
	 */
33
	public function testCurrentTheme() {
34
		//TODO: SiteConfig moved to CMS
35
		Config::inst()->update('SSViewer', 'theme', 'mytheme');
36
		$this->assertEquals('mytheme', Config::inst()->get('SSViewer', 'theme'),
37
			'Current theme is the default - user has not defined one');
38
	}
39
40
	/**
41
	 * Test that a template without a <head> tag still renders.
42
	 */
43
	public function testTemplateWithoutHeadRenders() {
44
		$data = new ArrayData(array(
45
			'Var' => 'var value'
46
		));
47
48
		$result = $data->renderWith("SSViewerTestPartialTemplate");
49
		$this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U",'',$result)));
50
	}
51
52
	public function testIncludeScopeInheritance() {
53
		$data = $this->getScopeInheritanceTestData();
54
		$expected = array(
55
			'Item 1 - First-ODD top:Item 1',
56
			'Item 2 - EVEN top:Item 2',
57
			'Item 3 - ODD top:Item 3',
58
			'Item 4 - EVEN top:Item 4',
59
			'Item 5 - ODD top:Item 5',
60
			'Item 6 - Last-EVEN top:Item 6',
61
		);
62
63
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
64
		$this->assertExpectedStrings($result, $expected);
65
66
		// reset results for the tests that include arguments (the title is passed as an arg)
67
		$expected = array(
68
			'Item 1 _ Item 1 - First-ODD top:Item 1',
69
			'Item 2 _ Item 2 - EVEN top:Item 2',
70
			'Item 3 _ Item 3 - ODD top:Item 3',
71
			'Item 4 _ Item 4 - EVEN top:Item 4',
72
			'Item 5 _ Item 5 - ODD top:Item 5',
73
			'Item 6 _ Item 6 - Last-EVEN top:Item 6',
74
		);
75
76
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
77
		$this->assertExpectedStrings($result, $expected);
78
	}
79
80
	public function testIncludeTruthyness() {
81
		$data = new ArrayData(array(
82
			'Title' => 'TruthyTest',
83
			'Items' => new ArrayList(array(
84
				new ArrayData(array('Title' => 'Item 1')),
85
				new ArrayData(array('Title' => '')),
86
				new ArrayData(array('Title' => true)),
87
				new ArrayData(array('Title' => false)),
88
				new ArrayData(array('Title' => null)),
89
				new ArrayData(array('Title' => 0)),
90
				new ArrayData(array('Title' => 7))
91
			))
92
		));
93
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
94
95
		// We should not end up with empty values appearing as empty
96
		$expected = array(
97
			'Item 1 _ Item 1 - First-ODD top:Item 1',
98
			'Untitled - EVEN top:',
99
			'1 _ 1 - ODD top:1',
100
			'Untitled - EVEN top:',
101
			'Untitled - ODD top:',
102
			'Untitled - EVEN top:0',
103
			'7 _ 7 - Last-ODD top:7'
104
		);
105
		$this->assertExpectedStrings($result, $expected);
106
	}
107
108
	private function getScopeInheritanceTestData() {
109
		return new ArrayData(array(
110
			'Title' => 'TopTitleValue',
111
			'Items' => new ArrayList(array(
112
				new ArrayData(array('Title' => 'Item 1')),
113
				new ArrayData(array('Title' => 'Item 2')),
114
				new ArrayData(array('Title' => 'Item 3')),
115
				new ArrayData(array('Title' => 'Item 4')),
116
				new ArrayData(array('Title' => 'Item 5')),
117
				new ArrayData(array('Title' => 'Item 6'))
118
			))
119
		));
120
	}
121
122
	private function assertExpectedStrings($result, $expected) {
123
		foreach ($expected as $expectedStr) {
124
			$this->assertTrue(
125
				(boolean) preg_match("/{$expectedStr}/", $result),
126
				"Didn't find '{$expectedStr}' in:\n{$result}"
127
			);
128
		}
129
	}
130
131
	/**
132
	 * Small helper to render templates from strings
133
	 */
134
	public function render($templateString, $data = null, $cacheTemplate = false) {
135
		$t = SSViewer::fromString($templateString, $cacheTemplate);
136
		if(!$data) $data = new SSViewerTestFixture();
137
		return trim(''.$t->process($data));
138
	}
139
140
	public function testRequirements() {
141
		$requirements = $this->getMock("Requirements_Backend", array("javascript", "css"));
142
		$jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
143
		$cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
144
145
		$requirements->expects($this->once())->method('javascript')->with($jsFile);
146
		$requirements->expects($this->once())->method('css')->with($cssFile);
147
148
		Requirements::set_backend($requirements);
149
150
		$template = $this->render("<% require javascript($jsFile) %>
151
		<% require css($cssFile) %>");
152
		$this->assertFalse((bool)trim($template), "Should be no content in this return.");
153
	}
154
155
	public function testRequirementsCombine(){
156
		$oldBackend = Requirements::backend();
157
		$testBackend = new Requirements_Backend();
158
		Requirements::set_backend($testBackend);
159
		$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
160
161
		$jsFile = FRAMEWORK_DIR . '/tests/view/themes/javascript/bad.js';
162
		$jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
163
		Requirements::combine_files('testRequirementsCombine.js', array($jsFile));
164
		require_once('thirdparty/jsmin/jsmin.php');
165
166
		// first make sure that our test js file causes an exception to be thrown
167
		try{
168
			$content = JSMin::minify($content);
0 ignored issues
show
Bug introduced by
The variable $content seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Unused Code introduced by
$content is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
169
			Requirements::set_backend($oldBackend);
170
			$this->fail('JSMin did not throw exception on minify bad file: ');
171
		}catch(Exception $e){
172
			// exception thrown... good
173
		}
174
175
		// secondly, make sure that requirements combine throws the correct warning, and only that warning
176
		@unlink($combinedTestFilePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
177
		try{
178
			Requirements::process_combined_files();
179
		}catch(PHPUnit_Framework_Error_Warning $e){
180
			if(strstr($e->getMessage(), 'Failed to minify') === false){
181
				Requirements::set_backend($oldBackend);
182
				$this->fail('Requirements::process_combined_files raised a warning, which is good, but this is not the expected warning ("Failed to minify..."): '.$e);
183
			}
184
		}catch(Exception $e){
185
			Requirements::set_backend($oldBackend);
186
			$this->fail('Requirements::process_combined_files did not catch exception caused by minifying bad js file: '.$e);
187
		}
188
189
		// and make sure the combined content matches the input content, i.e. no loss of functionality
190
		if(!file_exists($combinedTestFilePath)){
191
			Requirements::set_backend($oldBackend);
192
			$this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
193
		}
194
		$combinedTestFileContents = file_get_contents($combinedTestFilePath);
195
		$this->assertContains($jsFileContents, $combinedTestFileContents);
196
197
		// reset
198
		Requirements::set_backend($oldBackend);
199
	}
200
201
202
203
	public function testComments() {
204
		$output = $this->render(<<<SS
205
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
206
<%-- Alone multi
207
	line comment --%>
208
Some more content
209
Mixing content and <%-- multi
210
	line comment --%> Final final
211
content
212
SS
213
);
214
		$shouldbe = <<<SS
215
This is my templateThis is some contentFinal content
216
217
Some more content
218
Mixing content and  Final final
219
content
220
SS;
221
222
		$this->assertEquals($shouldbe, $output);
223
	}
224
225
	public function testBasicText() {
226
		$this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
227
		$this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
228
		$this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
229
		$this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
230
	}
231
232
	public function testBasicInjection() {
233
		$this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
234
		$this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
235
		$this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
236
		$this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
237
238
		$this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
239
		$this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
240
		$this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
241
242
		$this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
243
		$this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'),
244
			'Escapes before injections are correctly unescaped');
245
	}
246
247
248
	public function testGlobalVariableCalls() {
249
		$this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
250
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
251
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
252
	}
253
254
	public function testGlobalVariableCallsWithArguments() {
255
		$this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
256
		$this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
257
		$this->assertEquals('zFoo:Bar:Bazz',
258
			$this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")'));
259
		$this->assertEquals('zreferencez',
260
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)'));
261
	}
262
263
	public function testGlobalVariablesAreEscaped() {
264
		$this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
265
		$this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
266
267
		$this->assertEquals('z<div></div>z',
268
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)'));
269
		$this->assertEquals('z&lt;div&gt;&lt;/div&gt;z',
270
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)'));
271
	}
272
273
	public function testCoreGlobalVariableCalls() {
274
		$this->assertEquals(Director::absoluteBaseURL(),
275
			$this->render('{$absoluteBaseURL}'), 'Director::absoluteBaseURL can be called from within template');
276
		$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$AbsoluteBaseURL}'),
277
			'Upper-case %AbsoluteBaseURL can be called from within template');
278
279
		$this->assertEquals(Director::is_ajax(), $this->render('{$isAjax}'),
280
			'All variations of is_ajax result in the correct call');
281
		$this->assertEquals(Director::is_ajax(), $this->render('{$IsAjax}'),
282
			'All variations of is_ajax result in the correct call');
283
		$this->assertEquals(Director::is_ajax(), $this->render('{$is_ajax}'),
284
			'All variations of is_ajax result in the correct call');
285
		$this->assertEquals(Director::is_ajax(), $this->render('{$Is_ajax}'),
286
			'All variations of is_ajax result in the correct call');
287
288
		$this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'),
289
			'i18n template functions result correct result');
290
		$this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'),
291
			'i18n template functions result correct result');
292
293
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'),
294
			'Member template functions result correct result');
295
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'),
296
			'Member template functions result correct result');
297
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'),
298
			'Member template functions result correct result');
299
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentUser}'),
300
			'Member template functions result correct result');
301
302
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'),
303
			'SecurityToken template functions result correct result');
304
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'),
305
			'SecurityToken template functions result correct result');
306
307
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
308
			'Permissions template functions result correct result');
309
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
310
			'Permissions template functions result correct result');
311
	}
312
313
	public function testNonFieldCastingHelpersNotUsedInHasValue() {
314
		// check if Link without $ in front of variable
315
		$result = $this->render(
316
			'A<% if Link %>$Link<% end_if %>B', new SSViewerTest_Object());
317
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
318
319
		// check if Link with $ in front of variable
320
		$result = $this->render(
321
			'A<% if $Link %>$Link<% end_if %>B', new SSViewerTest_Object());
322
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
323
	}
324
325
	public function testLocalFunctionsTakePriorityOverGlobals() {
326
		$data = new ArrayData(array(
327
			'Page' => new SSViewerTest_Object()
328
		));
329
330
		//call method with lots of arguments
331
		$result = $this->render(
332
			'<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',$data);
333
		$this->assertEquals("abcdefghijk",$result, "public function can accept up to 11 arguments");
334
335
		//call method that does not exist
336
		$result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>',$data);
337
		$this->assertEquals("",$result, "Method does not exist - empty result");
338
339
		//call if that does not exist
340
		$result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>',$data);
341
		$this->assertEquals("",$result, "Method does not exist - empty result");
342
343
		//call method with same name as a global method (local call should take priority)
344
		$result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>',$data);
345
		$this->assertEquals("testLocalFunctionPriorityCalled",$result,
346
			"Local Object's public function called. Did not return the actual baseURL of the current site");
347
	}
348
349
	public function testCurrentScopeLoopWith() {
350
		// Data to run the loop tests on - one sequence of three items, each with a subitem
351
		$data = new ArrayData(array(
352
			'Foo' => new ArrayList(array(
353
				'Subocean' => new ArrayData(array(
354
						'Name' => 'Higher'
355
					)),
356
				new ArrayData(array(
357
					'Sub' => new ArrayData(array(
358
						'Name' => 'SubKid1'
359
					))
360
				)),
361
				new ArrayData(array(
362
					'Sub' => new ArrayData(array(
363
						'Name' => 'SubKid2'
364
					))
365
				)),
366
				new SSViewerTest_Object('Number6')
367
			))
368
		));
369
370
		$result = $this->render(
371
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
372
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
373
374
		$result = $this->render(
375
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
376
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
377
378
		$result = $this->render('<% with Foo %>$Count<% end_with %>',$data);
379
		$this->assertEquals("4",$result, "4 items in the DataObjectSet");
380
381
		$result = $this->render('<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
382
			. '<% end_if %><% end_loop %><% end_with %>',$data);
383
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in with Up.Foo scope works");
384
385
		$result = $this->render('<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
386
			. '<% end_if %><% end_loop %><% end_with %>',$data);
387
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in current scope works");
388
	}
389
390
	public function testObjectDotArguments() {
391
		$this->assertEquals(
392
			'[out:TestObject.methodWithOneArgument(one)]
393
				[out:TestObject.methodWithTwoArguments(one,two)]
394
				[out:TestMethod(Arg1,Arg2).Bar.Val]
395
				[out:TestMethod(Arg1,Arg2).Bar]
396
				[out:TestMethod(Arg1,Arg2)]
397
				[out:TestMethod(Arg1).Bar.Val]
398
				[out:TestMethod(Arg1).Bar]
399
				[out:TestMethod(Arg1)]',
400
			$this->render('$TestObject.methodWithOneArgument(one)
401
				$TestObject.methodWithTwoArguments(one,two)
402
				$TestMethod(Arg1, Arg2).Bar.Val
403
				$TestMethod(Arg1, Arg2).Bar
404
				$TestMethod(Arg1, Arg2)
405
				$TestMethod(Arg1).Bar.Val
406
				$TestMethod(Arg1).Bar
407
				$TestMethod(Arg1)')
408
		);
409
	}
410
411
	public function testEscapedArguments() {
412
		$this->assertEquals(
413
			'[out:Foo(Arg1,Arg2).Bar.Val].Suffix
414
				[out:Foo(Arg1,Arg2).Val]_Suffix
415
				[out:Foo(Arg1,Arg2)]/Suffix
416
				[out:Foo(Arg1).Bar.Val]textSuffix
417
				[out:Foo(Arg1).Bar].Suffix
418
				[out:Foo(Arg1)].Suffix
419
				[out:Foo.Bar.Val].Suffix
420
				[out:Foo.Bar].Suffix
421
				[out:Foo].Suffix',
422
			$this->render('{$Foo(Arg1, Arg2).Bar.Val}.Suffix
423
				{$Foo(Arg1, Arg2).Val}_Suffix
424
				{$Foo(Arg1, Arg2)}/Suffix
425
				{$Foo(Arg1).Bar.Val}textSuffix
426
				{$Foo(Arg1).Bar}.Suffix
427
				{$Foo(Arg1)}.Suffix
428
				{$Foo.Bar.Val}.Suffix
429
				{$Foo.Bar}.Suffix
430
				{$Foo}.Suffix')
431
		);
432
	}
433
434
	public function testLoopWhitespace() {
435
		$this->assertEquals(
436
			'before[out:SingleItem.Test]after
437
				beforeTestafter',
438
			$this->render('before<% loop SingleItem %>$Test<% end_loop %>after
439
				before<% loop SingleItem %>Test<% end_loop %>after')
440
		);
441
442
		// The control tags are removed from the output, but no whitespace
443
		// This is a quirk that could be changed, but included in the test to make the current
444
		// behaviour explicit
445
		$this->assertEquals(
446
			'before
447
448
[out:SingleItem.ItemOnItsOwnLine]
449
450
after',
451
			$this->render('before
452
<% loop SingleItem %>
453
$ItemOnItsOwnLine
454
<% end_loop %>
455
after')
456
		);
457
458
		// The whitespace within the control tags is preserve in a loop
459
		// This is a quirk that could be changed, but included in the test to make the current
460
		// behaviour explicit
461
		$this->assertEquals(
462
			'before
463
464
[out:Loop3.ItemOnItsOwnLine]
465
466
[out:Loop3.ItemOnItsOwnLine]
467
468
[out:Loop3.ItemOnItsOwnLine]
469
470
after',
471
			$this->render('before
472
<% loop Loop3 %>
473
$ItemOnItsOwnLine
474
<% end_loop %>
475
after')
476
		);
477
	}
478
479
	public function testControls() {
480
		// Single item controls
481
		$this->assertEquals(
482
			'a[out:Foo.Bar.Item]b
483
				[out:Foo.Bar(Arg1).Item]
484
				[out:Foo(Arg1).Item]
485
				[out:Foo(Arg1,Arg2).Item]
486
				[out:Foo(Arg1,Arg2,Arg3).Item]',
487
			$this->render('<% with Foo.Bar %>a{$Item}b<% end_with %>
488
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
489
				<% with Foo(Arg1) %>$Item<% end_with %>
490
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
491
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>')
492
		);
493
494
		// Loop controls
495
		$this->assertEquals('a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
496
			$this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>'));
497
498
		$this->assertEquals('[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
499
			$this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>'));
500
501
		$this->assertEquals('[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
502
			$this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>'));
503
504
		$this->assertEquals('[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
505
			$this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>'));
506
507
		$this->assertEquals('[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
508
			$this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>'));
509
510
	}
511
512
	public function testIfBlocks() {
513
		// Basic test
514
		$this->assertEquals('AC',
515
			$this->render('A<% if NotSet %>B$NotSet<% end_if %>C'));
516
517
		// Nested test
518
		$this->assertEquals('AB1C',
519
			$this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C'));
520
521
		// else_if
522
		$this->assertEquals('ACD',
523
			$this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
524
		$this->assertEquals('AD',
525
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D'));
526
		$this->assertEquals('ADE',
527
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
528
529
		$this->assertEquals('ADE',
530
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
531
532
		// Dot syntax
533
		$this->assertEquals('ACD',
534
			$this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D'));
535
		$this->assertEquals('ACD',
536
			$this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D'));
537
538
		// Params
539
		$this->assertEquals('ACD',
540
			$this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D'));
541
		$this->assertEquals('ABD',
542
			$this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D'));
543
544
		// Negation
545
		$this->assertEquals('AC',
546
			$this->render('A<% if not IsSet %>B<% end_if %>C'));
547
		$this->assertEquals('ABC',
548
			$this->render('A<% if not NotSet %>B<% end_if %>C'));
549
550
		// Or
551
		$this->assertEquals('ABD',
552
			$this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D'));
553
		$this->assertEquals('ACD',
554
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D'));
555
		$this->assertEquals('AD',
556
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D'));
557
		$this->assertEquals('ACD',
558
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D'));
559
		$this->assertEquals('AD',
560
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D'));
561
562
		// Negated Or
563
		$this->assertEquals('ACD',
564
			$this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
565
		$this->assertEquals('ABD',
566
			$this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
567
		$this->assertEquals('ABD',
568
			$this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
569
570
		// And
571
		$this->assertEquals('ABD',
572
			$this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D'));
573
		$this->assertEquals('ACD',
574
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
575
		$this->assertEquals('AD',
576
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D'));
577
		$this->assertEquals('ACD',
578
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D'));
579
		$this->assertEquals('AD',
580
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D'));
581
582
		// Equality
583
		$this->assertEquals('ABC',
584
			$this->render('A<% if RawVal == RawVal %>B<% end_if %>C'));
585
		$this->assertEquals('ACD',
586
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D'));
587
		$this->assertEquals('ABC',
588
			$this->render('A<% if Right != Wrong %>B<% end_if %>C'));
589
		$this->assertEquals('AD',
590
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D'));
591
592
		// test inequalities with simple numbers
593
		$this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
594
		$this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
595
		$this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
596
		$this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
597
598
		$this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
599
		$this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
600
		$this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
601
		$this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
602
603
		$this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
604
		$this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
605
		$this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
606
		$this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
607
608
		// empty else_if and else tags, if this would not be supported,
609
		// the output would stop after A, thereby failing the assert
610
		$this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
611
		$this->assertEquals('AD',
612
			$this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D'));
613
		$this->assertEquals('AD',
614
			$this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D'));
615
616
		// Bare words with ending space
617
		$this->assertEquals('ABC',
618
			$this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C'));
619
620
		// Else
621
		$this->assertEquals('ADE',
622
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E'));
623
624
		// Empty if with else
625
		$this->assertEquals('ABC',
626
			$this->render('A<% if NotSet %><% else %>B<% end_if %>C'));
627
	}
628
629
	public function testBaseTagGeneration() {
630
		// XHTML wil have a closed base tag
631
		$tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
632
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
633
				. ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
634
			<html>
635
				<head><% base_tag %></head>
636
				<body><p>test</p><body>
637
			</html>';
638
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
639
640
		// HTML4 and 5 will only have it for IE
641
		$tmpl2 = '<!DOCTYPE html>
642
			<html>
643
				<head><% base_tag %></head>
644
				<body><p>test</p><body>
645
			</html>';
646
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
647
			$this->render($tmpl2));
648
649
650
		$tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
651
			<html>
652
				<head><% base_tag %></head>
653
				<body><p>test</p><body>
654
			</html>';
655
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
656
			$this->render($tmpl3));
657
658
		// Check that the content negotiator converts to the equally legal formats
659
		$negotiator = new ContentNegotiator();
660
661
		$response = new SS_HTTPResponse($this->render($tmpl1));
662
		$negotiator->html($response);
663
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
664
			$response->getBody());
665
666
		$response = new SS_HTTPResponse($this->render($tmpl1));
667
		$negotiator->xhtml($response);
668
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
669
	}
670
671
	public function testIncludeWithArguments() {
672
		$this->assertEquals(
673
			$this->render('<% include SSViewerTestIncludeWithArguments %>'),
674
			'<p>[out:Arg1]</p><p>[out:Arg2]</p>'
675
		);
676
677
		$this->assertEquals(
678
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
679
			'<p>A</p><p>[out:Arg2]</p>'
680
		);
681
682
		$this->assertEquals(
683
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
684
			'<p>A</p><p>B</p>'
685
		);
686
687
		$this->assertEquals(
688
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
689
			'<p>A Bare String</p><p>B Bare String</p>'
690
		);
691
692
		$this->assertEquals(
693
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
694
				new ArrayData(array('B' => 'Bar'))),
695
			'<p>A</p><p>Bar</p>'
696
		);
697
698
		$this->assertEquals(
699
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
700
				new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))),
701
			'<p>A</p><p>Bar</p>'
702
		);
703
704
		$this->assertEquals(
705
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
706
				new ArrayData(array('Items' => new ArrayList(array(
707
					new ArrayData(array('Title' => 'Foo')),
708
					new ArrayData(array('Title' => 'Bar'))
709
				))))),
710
			'SomeArg - Foo - Bar - SomeArg'
711
		);
712
713
		$this->assertEquals(
714
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
715
				new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))),
716
			'A - B - A'
717
		);
718
719
		$this->assertEquals(
720
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
721
				new ArrayData(array(
722
					'Item' => new ArrayData(array(
723
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
724
					)))
725
				)),
726
			'A - B - C - B - A'
727
		);
728
729
		$this->assertEquals(
730
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
731
				new ArrayData(array(
732
					'Item' => new ArrayData(array(
733
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
734
					)))
735
				)),
736
			'A - A - A'
737
		);
738
739
		$data = new ArrayData(array(
740
			'Nested' => new ArrayData(array(
741
				'Object' => new ArrayData(array('Key' => 'A'))
742
			)),
743
			'Object' => new ArrayData(array('Key' => 'B'))
744
		));
745
746
		$tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
747
		$res  = $tmpl->process($data);
748
		$this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
0 ignored issues
show
Unused Code introduced by
The call to SSViewerTest::assertEqualIgnoringWhitespace() has too many arguments starting with 'Objects can be passed as named arguments'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
749
	}
750
751
752
	public function testRecursiveInclude() {
753
		$view = new SSViewer(array('SSViewerTestRecursiveInclude'));
754
755
		$data = new ArrayData(array(
756
			'Title' => 'A',
757
			'Children' => new ArrayList(array(
758
				new ArrayData(array(
759
					'Title' => 'A1',
760
					'Children' => new ArrayList(array(
761
						new ArrayData(array( 'Title' => 'A1 i', )),
762
						new ArrayData(array( 'Title' => 'A1 ii', )),
763
					)),
764
				)),
765
				new ArrayData(array( 'Title' => 'A2', )),
766
				new ArrayData(array( 'Title' => 'A3', )),
767
			)),
768
		));
769
770
		$result = $view->process($data);
771
		// We don't care about whitespace
772
		$rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
773
774
		$this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
775
	}
776
777
	public function assertEqualIgnoringWhitespace($a, $b) {
778
		$this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b));
779
	}
780
781
	/**
782
	 * See {@link ViewableDataTest} for more extensive casting tests,
783
	 * this test just ensures that basic casting is correctly applied during template parsing.
784
	 */
785
	public function testCastingHelpers() {
786
		$vd = new SSViewerTest_ViewableData();
787
		$vd->TextValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property TextValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
788
		$vd->HTMLValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property HTMLValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
789
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
790
791
		// Value casted as "Text"
792
		$this->assertEquals(
793
			'&lt;b&gt;html&lt;/b&gt;',
794
			$t = SSViewer::fromString('$TextValue')->process($vd)
795
		);
796
		$this->assertEquals(
797
			'<b>html</b>',
798
			$t = SSViewer::fromString('$TextValue.RAW')->process($vd)
799
		);
800
		$this->assertEquals(
801
			'&lt;b&gt;html&lt;/b&gt;',
802
			$t = SSViewer::fromString('$TextValue.XML')->process($vd)
803
		);
804
805
		// Value casted as "HTMLText"
806
		$this->assertEquals(
807
			'<b>html</b>',
808
			$t = SSViewer::fromString('$HTMLValue')->process($vd)
809
		);
810
		$this->assertEquals(
811
			'<b>html</b>',
812
			$t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
813
		);
814
		$this->assertEquals(
815
			'&lt;b&gt;html&lt;/b&gt;',
816
			$t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
817
		);
818
819
		// Uncasted value (falls back to ViewableData::$default_cast="HTMLText")
820
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
821
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
822
		$this->assertEquals(
823
			'<b>html</b>',
824
			$t = SSViewer::fromString('$UncastedValue')->process($vd)
825
		);
826
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
827
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
828
		$this->assertEquals(
829
			'<b>html</b>',
830
			$t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
831
		);
832
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
833
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
834
		$this->assertEquals(
835
			'&lt;b&gt;html&lt;/b&gt;',
836
			$t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
837
		);
838
	}
839
840
	public function testSSViewerBasicIteratorSupport() {
841
		$data = new ArrayData(array(
842
			'Set' => new ArrayList(array(
843
				new SSViewerTest_Object("1"),
844
				new SSViewerTest_Object("2"),
845
				new SSViewerTest_Object("3"),
846
				new SSViewerTest_Object("4"),
847
				new SSViewerTest_Object("5"),
848
				new SSViewerTest_Object("6"),
849
				new SSViewerTest_Object("7"),
850
				new SSViewerTest_Object("8"),
851
				new SSViewerTest_Object("9"),
852
				new SSViewerTest_Object("10"),
853
			))
854
		));
855
856
		//base test
857
		$result = $this->render('<% loop Set %>$Number<% end_loop %>',$data);
858
		$this->assertEquals("12345678910",$result,"Numbers rendered in order");
859
860
		//test First
861
		$result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>',$data);
862
		$this->assertEquals("1",$result,"Only the first number is rendered");
863
864
		//test Last
865
		$result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>',$data);
866
		$this->assertEquals("10",$result,"Only the last number is rendered");
867
868
		//test Even
869
		$result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>',$data);
870
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
871
872
		//test Even with quotes
873
		$result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>',$data);
874
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
875
876
		//test Even without quotes
877
		$result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>',$data);
878
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
879
880
		//test Even with zero-based start index
881
		$result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>',$data);
882
		$this->assertEquals("13579",$result,"Even (with zero-based index) numbers rendered in order");
883
884
		//test Odd
885
		$result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>',$data);
886
		$this->assertEquals("13579",$result,"Odd numbers rendered in order");
887
888
		//test FirstLast
889
		$result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>',$data);
890
		$this->assertEquals("1first10last",$result,"First and last numbers rendered in order");
891
892
		//test Middle
893
		$result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>',$data);
894
		$this->assertEquals("23456789",$result,"Middle numbers rendered in order");
895
896
		//test MiddleString
897
		$result = $this->render('<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
898
			. '<% end_loop %>',$data);
899
		$this->assertEquals("2middle3middle4middle5middle6middle7middle8middle9middle",$result,
900
			"Middle numbers rendered in order");
901
902
		//test EvenOdd
903
		$result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>',$data);
904
		$this->assertEquals("oddevenoddevenoddevenoddevenoddeven",$result,
905
			"Even and Odd is returned in sequence numbers rendered in order");
906
907
		//test Pos
908
		$result = $this->render('<% loop Set %>$Pos<% end_loop %>',$data);
909
		$this->assertEquals("12345678910", $result, '$Pos is rendered in order');
910
911
		//test Pos
912
		$result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>',$data);
913
		$this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
914
915
		//test FromEnd
916
		$result = $this->render('<% loop Set %>$FromEnd<% end_loop %>',$data);
917
		$this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
918
919
		//test FromEnd
920
		$result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>',$data);
921
		$this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
922
923
		//test Total
924
		$result = $this->render('<% loop Set %>$TotalItems<% end_loop %>',$data);
925
		$this->assertEquals("10101010101010101010",$result,"10 total items X 10 are returned");
926
927
		//test Modulus
928
		$result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>',$data);
929
		$this->assertEquals("1010101010",$result,"1-indexed pos modular divided by 2 rendered in order");
930
931
		//test MultipleOf 3
932
		$result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>',$data);
933
		$this->assertEquals("369",$result,"Only numbers that are multiples of 3 are returned");
934
935
		//test MultipleOf 4
936
		$result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>',$data);
937
		$this->assertEquals("48",$result,"Only numbers that are multiples of 4 are returned");
938
939
		//test MultipleOf 5
940
		$result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>',$data);
941
		$this->assertEquals("510",$result,"Only numbers that are multiples of 5 are returned");
942
943
		//test MultipleOf 10
944
		$result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>',$data);
945
		$this->assertEquals("10",$result,"Only numbers that are multiples of 10 (with 1-based indexing) are returned");
946
947
		//test MultipleOf 9 zero-based
948
		$result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>',$data);
949
		$this->assertEquals("110",$result,
950
			"Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)");
951
952
		//test MultipleOf 11
953
		$result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>',$data);
954
		$this->assertEquals("",$result,"Only numbers that are multiples of 11 are returned. I.e. nothing returned");
955
	}
956
957
	/**
958
	 * Test $Up works when the scope $Up refers to was entered with a "with" block
959
	 */
960
	public function testUpInWith() {
961
962
		// Data to run the loop tests on - three levels deep
963
		$data = new ArrayData(array(
964
			'Name' => 'Top',
965
			'Foo' => new ArrayData(array(
966
				'Name' => 'Foo',
967
				'Bar' => new ArrayData(array(
968
					'Name' => 'Bar',
969
					'Baz' => new ArrayData(array(
970
						'Name' => 'Baz'
971
					)),
972
					'Qux' => new ArrayData(array(
973
						'Name' => 'Qux'
974
					))
975
				))
976
			))
977
		));
978
979
		// Basic functionality
980
		$this->assertEquals('BarFoo',
981
			$this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data));
982
983
		// Two level with block, up refers to internally referenced Bar
984
		$this->assertEquals('BarFoo',
985
			$this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data));
986
987
		// Stepping up & back down the scope tree
988
		$this->assertEquals('BazBarQux',
989
			$this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data));
990
991
		// Using $Up in a with block
992
		$this->assertEquals('BazBarQux',
993
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
994
				.'<% end_with %>', $data));
995
996
		// Stepping up & back down the scope tree with with blocks
997
		$this->assertEquals('BazBarQuxBarBaz',
998
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
999
				. '{$Name}<% end_with %>{$Name}<% end_with %>', $data));
1000
1001
		// Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1002
		$this->assertEquals('Foo',
1003
			$this->render('<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
1004
				. '<% end_with %>', $data));
1005
1006
		// Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
1007
		$this->assertEquals('Foo',
1008
			$this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data));
1009
	}
1010
1011
	/**
1012
	 * Test $Up works when the scope $Up refers to was entered with a "loop" block
1013
	 */
1014
	public function testUpInLoop(){
1015
1016
		// Data to run the loop tests on - one sequence of three items, each with a subitem
1017
		$data = new ArrayData(array(
1018
			'Name' => 'Top',
1019
			'Foo' => new ArrayList(array(
1020
				new ArrayData(array(
1021
					'Name' => '1',
1022
					'Sub' => new ArrayData(array(
1023
						'Name' => 'Bar'
1024
					))
1025
				)),
1026
				new ArrayData(array(
1027
					'Name' => '2',
1028
					'Sub' => new ArrayData(array(
1029
						'Name' => 'Baz'
1030
					))
1031
				)),
1032
				new ArrayData(array(
1033
					'Name' => '3',
1034
					'Sub' => new ArrayData(array(
1035
						'Name' => 'Qux'
1036
					))
1037
				))
1038
			))
1039
		));
1040
1041
		// Make sure inside a loop, $Up refers to the current item of the loop
1042
		$this->assertEqualIgnoringWhitespace(
1043
			'111 222 333',
1044
			$this->render(
1045
				'<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1046
				$data
1047
			)
1048
		);
1049
1050
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1051
		// and doesn't interfere with the original iterator
1052
		$this->assertEqualIgnoringWhitespace(
1053
			'1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1054
			$this->render(
1055
				'<% loop $Foo %>
1056
					$Name
1057
					<% with $Sub %>
1058
						$Name
1059
						<% loop $Up %>$Name<% end_loop %>
1060
						$Name
1061
					<% end_with %>
1062
					$Name
1063
				<% end_loop %>',
1064
				$data
1065
			)
1066
		);
1067
1068
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1069
		// and doesn't interfere with the original iterator or local lookups
1070
		$this->assertEqualIgnoringWhitespace(
1071
			'1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1072
			$this->render(
1073
				'<% loop $Foo %>
1074
					$Name
1075
					<% with $Sub %>
1076
						{$Name}{$Up.Name}
1077
						<% loop $Up %>$Name<% end_loop %>
1078
						{$Up.Name}{$Name}
1079
					<% end_with %>
1080
					$Name
1081
				<% end_loop %>',
1082
				$data
1083
			)
1084
		);
1085
	}
1086
1087
	/**
1088
	 * Test that nested loops restore the loop variables correctly when pushing and popping states
1089
	 */
1090
	public function testNestedLoops(){
1091
1092
		// Data to run the loop tests on - one sequence of three items, one with child elements
1093
		// (of a different size to the main sequence)
1094
		$data = new ArrayData(array(
1095
			'Foo' => new ArrayList(array(
1096
				new ArrayData(array(
1097
					'Name' => '1',
1098
					'Children' => new ArrayList(array(
1099
						new ArrayData(array(
1100
							'Name' => 'a'
1101
						)),
1102
						new ArrayData(array(
1103
							'Name' => 'b'
1104
						)),
1105
					)),
1106
				)),
1107
				new ArrayData(array(
1108
					'Name' => '2',
1109
					'Children' => new ArrayList(),
1110
				)),
1111
				new ArrayData(array(
1112
					'Name' => '3',
1113
					'Children' => new ArrayList(),
1114
				)),
1115
			)),
1116
		));
1117
1118
		// Make sure that including a loop inside a loop will not destroy the internal count of
1119
		// items, checked by using "Last"
1120
		$this->assertEqualIgnoringWhitespace(
1121
			'1ab23last',
1122
			$this->render('<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1123
				. '<% end_loop %>', $data
1124
			)
1125
		);
1126
	}
1127
1128
	public function testLayout() {
1129
		$self = $this;
1130
1131
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1132
			$template = new SSViewer(array('Page'));
1133
			$self->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1134
1135
			$template = new SSViewer(array('Shortcodes', 'Page'));
1136
			$self->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1137
		});
1138
	}
1139
1140
	/**
1141
	 * @covers SSViewer::get_templates_by_class()
1142
	 */
1143
	public function testGetTemplatesByClass() {
1144
		$self = $this;
1145
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1146
			// Test passing a string
1147
			$templates = SSViewer::get_templates_by_class('SSViewerTest_Controller', '', 'Controller');
1148
			$self->assertCount(2, $templates);
1149
1150
			// Test to ensure we're stopping at the base class.
1151
			$templates = SSViewer::get_templates_by_class('SSViewerTest_Controller', '', 'SSViewerTest_Controller');
1152
			$self->assertCount(1, $templates);
1153
1154
			// Make sure we can filter our templates by suffix.
1155
			$templates = SSViewer::get_templates_by_class('SSViewerTest', '_Controller');
1156
			$self->assertCount(1, $templates);
1157
1158
			// Test passing a valid object
1159
			$templates = SSViewer::get_templates_by_class("SSViewerTest_Controller", '', 'Controller');
1160
1161
			// Test that templates are returned in the correct order
1162
			$self->assertEquals('SSViewerTest_Controller', array_shift($templates));
1163
			$self->assertEquals('Controller', array_shift($templates));
1164
1165
			// Let's throw something random in there.
1166
			$self->setExpectedException('InvalidArgumentException');
1167
			$templates = SSViewer::get_templates_by_class(array());
0 ignored issues
show
Unused Code introduced by
$templates is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1168
		});
1169
	}
1170
1171
	/**
1172
	 * @covers SSViewer::get_themes()
1173
	 */
1174
	public function testThemeRetrieval() {
1175
		$ds = DIRECTORY_SEPARATOR;
1176
		$testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes';
1177
1178
		if(file_exists($testThemeBaseDir)) Filesystem::removeFolder($testThemeBaseDir);
1179
1180
		mkdir($testThemeBaseDir);
1181
		mkdir($testThemeBaseDir . $ds . 'blackcandy');
1182
		mkdir($testThemeBaseDir . $ds . 'blackcandy_blog');
1183
		mkdir($testThemeBaseDir . $ds . 'darkshades');
1184
		mkdir($testThemeBaseDir . $ds . 'darkshades_blog');
1185
1186
		$this->assertEquals(array(
1187
			'blackcandy' => 'blackcandy',
1188
			'darkshades' => 'darkshades'
1189
		), SSViewer::get_themes($testThemeBaseDir), 'Our test theme directory contains 2 themes');
1190
1191
		$this->assertEquals(array(
1192
			'blackcandy' => 'blackcandy',
1193
			'blackcandy_blog' => 'blackcandy_blog',
1194
			'darkshades' => 'darkshades',
1195
			'darkshades_blog' => 'darkshades_blog'
1196
		), SSViewer::get_themes($testThemeBaseDir, true),
1197
			'Our test theme directory contains 2 themes and 2 sub-themes');
1198
1199
		// Remove all the test themes we created
1200
		Filesystem::removeFolder($testThemeBaseDir);
1201
	}
1202
1203
	public function testRewriteHashlinks() {
1204
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1205
		Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
1206
1207
		$_SERVER['HTTP_HOST'] = 'www.mysite.com';
1208
		$_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1209
1210
		// Emulate SSViewer::process()
1211
		// Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1212
		// as protocol-less absolute urls
1213
		$base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1214
1215
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1216
1217
		// Note: SSViewer_FromString doesn't rewrite hash links.
1218
		file_put_contents($tmplFile, '<!DOCTYPE html>
1219
			<html>
1220
				<head><% base_tag %></head>
1221
				<body>
1222
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1223
				$ExternalInsertedLink
1224
				<a class="inline" href="#anchor">InlineLink</a>
1225
				$InsertedLink
1226
				<svg><use xlink:href="#sprite"></use></svg>
1227
				<body>
1228
			</html>');
1229
		$tmpl = new SSViewer($tmplFile);
1230
		$obj = new ViewableData();
1231
		$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
0 ignored issues
show
Documentation introduced by
The property InsertedLink does not exist on object<ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1232
		$obj->ExternalInsertedLink = '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>';
0 ignored issues
show
Documentation introduced by
The property ExternalInsertedLink does not exist on object<ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1233
		$result = $tmpl->process($obj);
1234
		$this->assertContains(
1235
			'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1236
			$result
1237
		);
1238
		$this->assertContains(
1239
			'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1240
			$result
1241
		);
1242
		$this->assertContains(
1243
			'<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1244
			$result
1245
		);
1246
		$this->assertContains(
1247
			'<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1248
			$result
1249
		);
1250
		$this->assertContains(
1251
			'<svg><use xlink:href="#sprite"></use></svg>',
1252
			$result,
1253
			'SSTemplateParser should only rewrite anchor hrefs'
1254
		);
1255
1256
		unlink($tmplFile);
1257
1258
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1259
	}
1260
1261
	public function testRewriteHashlinksInPhpMode() {
1262
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1263
		Config::inst()->update('SSViewer', 'rewrite_hash_links', 'php');
1264
1265
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1266
1267
		// Note: SSViewer_FromString doesn't rewrite hash links.
1268
		file_put_contents($tmplFile, '<!DOCTYPE html>
1269
			<html>
1270
				<head><% base_tag %></head>
1271
				<body>
1272
				<a class="inline" href="#anchor">InlineLink</a>
1273
				$InsertedLink
1274
				<svg><use xlink:href="#sprite"></use></svg>
1275
				<body>
1276
			</html>');
1277
		$tmpl = new SSViewer($tmplFile);
1278
		$obj = new ViewableData();
1279
		$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
0 ignored issues
show
Documentation introduced by
The property InsertedLink does not exist on object<ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1280
		$result = $tmpl->process($obj);
1281
1282
		$code = <<<'EOC'
1283
<a class="inserted" href="<?php echo Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1284
EOC;
1285
		$this->assertContains($code, $result);
1286
		// TODO Fix inline links in PHP mode
1287
		// $this->assertContains(
1288
		// 	'<a class="inline" href="<?php echo str_replace(',
1289
		// 	$result
1290
		// );
1291
		$this->assertContains(
1292
			'<svg><use xlink:href="#sprite"></use></svg>',
1293
			$result,
1294
			'SSTemplateParser should only rewrite anchor hrefs'
1295
		);
1296
1297
		unlink($tmplFile);
1298
1299
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1300
	}
1301
1302
	public function testRenderWithSourceFileComments() {
1303
		$origEnv = Config::inst()->get('Director', 'environment_type');
1304
		Config::inst()->update('Director', 'environment_type', 'dev');
1305
		Config::inst()->update('SSViewer', 'source_file_comments', true);
1306
		$f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments';
1307
		$templates = array(
1308
			array(
1309
				'name' => 'SSViewerTestCommentsFullSource',
1310
				'expected' => ""
1311
					. "<!doctype html>"
1312
					. "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1313
					. "<html>"
1314
					. "\t<head></head>"
1315
					. "\t<body></body>"
1316
					. "</html>"
1317
					. "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1318
			),
1319
			array(
1320
				'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1321
				'expected' => ""
1322
					. "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1323
					. "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1324
					. "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1325
					. "<html>"
1326
					. "\t<head></head>"
1327
					. "\t<body></body>"
1328
					. "</html>"
1329
					. "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1330
			),
1331
			array(
1332
				'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1333
				'expected' => ""
1334
					. "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1335
					. "\t<head></head>"
1336
					. "\t<body></body>"
1337
					. "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1338
			),
1339
			array(
1340
				'name' => 'SSViewerTestCommentsFullSourceIfIE',
1341
				'expected' => ""
1342
					. "<!doctype html>"
1343
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1344
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1345
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1346
					. "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1347
					. "\t<head></head>"
1348
					. "\t<body></body>"
1349
					. "</html>"
1350
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1351
			),
1352
			array(
1353
				'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1354
				'expected' => ""
1355
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1356
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1357
					. "<!--[if !IE]><!--> <html class='no-ie'>"
1358
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1359
					. " <!--<![endif]-->"
1360
					. "\t<head></head>"
1361
					. "\t<body></body>"
1362
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1363
			),
1364
			array(
1365
				'name' => 'SSViewerTestCommentsPartialSource',
1366
				'expected' =>
1367
				"<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1368
					. "<div class='typography'></div>"
1369
					. "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1370
			),
1371
			array(
1372
				'name' => 'SSViewerTestCommentsWithInclude',
1373
				'expected' =>
1374
				"<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1375
					. "<div class='typography'>"
1376
					. "<!-- include 'SSViewerTestCommentsInclude' -->"
1377
					. "<!-- template $f/SSViewerTestCommentsInclude.ss -->"
1378
					. "Included"
1379
					. "<!-- end template $f/SSViewerTestCommentsInclude.ss -->"
1380
					. "<!-- end include 'SSViewerTestCommentsInclude' -->"
1381
					. "</div>"
1382
					. "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1383
			),
1384
		);
1385
		foreach ($templates as $template) {
1386
			$this->_renderWithSourceFileComments($template['name'], $template['expected']);
1387
		}
1388
		Config::inst()->update('SSViewer', 'source_file_comments', false);
1389
		Config::inst()->update('Director', 'environment_type', $origEnv);
1390
	}
1391
	private function _renderWithSourceFileComments($name, $expected) {
1392
		$viewer = new SSViewer(array($name));
1393
		$data = new ArrayData(array());
1394
		$result = $viewer->process($data);
1395
		$expected = str_replace(array("\r", "\n"), '', $expected);
1396
		$result = str_replace(array("\r", "\n"), '', $result);
1397
		$this->assertEquals($result, $expected);
1398
	}
1399
1400
	public function testLoopIteratorIterator() {
1401
		$list = new PaginatedList(new ArrayList());
1402
		$viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1403
		$result = $viewer->process(new ArrayData(array('List' => $list)));
1404
		$this->assertEquals($result, '');
1405
	}
1406
1407
	public function testProcessOnlyIncludesRequirementsOnce() {
1408
		$template = new SSViewer(array('SSViewerTestProcess'));
1409
		$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
1410
1411
		$backend = new Requirements_Backend;
1412
		$backend->set_combined_files_enabled(false);
1413
		$backend->combine_files(
1414
			'RequirementsTest_ab.css',
1415
			array(
1416
				$basePath . '/RequirementsTest_a.css',
1417
				$basePath . '/RequirementsTest_b.css'
1418
			)
1419
		);
1420
1421
		Requirements::set_backend($backend);
1422
1423
		$this->assertEquals(1, substr_count($template->process(array()), "a.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1424
		$this->assertEquals(1, substr_count($template->process(array()), "b.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1425
1426
		// if we disable the requirements then we should get nothing
1427
		$template->includeRequirements(false);
1428
		$this->assertEquals(0, substr_count($template->process(array()), "a.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1429
		$this->assertEquals(0, substr_count($template->process(array()), "b.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1430
	}
1431
1432
	public function testRequireCallInTemplateInclude() {
1433
		//TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1434
		if(FRAMEWORK_DIR === 'framework') {
1435
			$template = new SSViewer(array('SSViewerTestProcess'));
1436
1437
			Requirements::set_suffix_requirements(false);
1438
1439
			$this->assertEquals(1, substr_count(
1440
				$template->process(array()),
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1441
				"tests/forms/RequirementsTest_a.js"
1442
			));
1443
		}
1444
		else {
1445
			$this->markTestSkipped('Requirement will always fail if the framework dir is not '.
1446
				'named \'framework\', since templates require hard coded paths');
1447
		}
1448
	}
1449
1450
	public function testCallsWithArguments() {
1451
		$data = new ArrayData(array(
1452
			'Set' => new ArrayList(array(
1453
				new SSViewerTest_Object("1"),
1454
				new SSViewerTest_Object("2"),
1455
				new SSViewerTest_Object("3"),
1456
				new SSViewerTest_Object("4"),
1457
				new SSViewerTest_Object("5"),
1458
			)),
1459
			'Level' => new SSViewerTest_LevelTest(1),
1460
			'Nest' => array(
1461
				'Level' => new SSViewerTest_LevelTest(2),
1462
			),
1463
		));
1464
1465
		$tests = array(
1466
			'$Level.output(1)' => '1-1',
1467
			'$Nest.Level.output($Set.First.Number)' => '2-1',
1468
			'<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1469
			'<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1470
			'<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1471
			'<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1472
			'<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1473
			'<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1474
			'<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1475
			'<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1476
			'<% with $Nest %>
1477
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1478
			<% end_with %>' => '1-hi',
1479
			'<% with $Nest %>
1480
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1481
			<% end_with %>' => '!0!1!2!3!4',
1482
		);
1483
1484
		foreach($tests as $template => $expected) {
1485
			$this->assertEquals($expected, trim($this->render($template, $data)));
1486
		}
1487
	}
1488
1489 View Code Duplication
	public function testClosedBlockExtension() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1490
		$count = 0;
1491
		$parser = new SSTemplateParser();
1492
		$parser->addClosedBlock(
1493
			'test',
1494
			function (&$res) use (&$count) {
0 ignored issues
show
Unused Code introduced by
The parameter $res is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1495
				$count++;
1496
			}
1497
		);
1498
1499
		$template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
1500
		$template->process(new SSViewerTestFixture());
1501
1502
		$this->assertEquals(1, $count);
1503
	}
1504
1505 View Code Duplication
	public function testOpenBlockExtension() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1506
		$count = 0;
1507
		$parser = new SSTemplateParser();
1508
		$parser->addOpenBlock(
1509
			'test',
1510
			function (&$res) use (&$count) {
0 ignored issues
show
Unused Code introduced by
The parameter $res is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1511
				$count++;
1512
			}
1513
		);
1514
1515
		$template = new SSViewer_FromString("<% test %>", $parser);
1516
		$template->process(new SSViewerTestFixture());
1517
1518
		$this->assertEquals(1, $count);
1519
	}
1520
1521
	/**
1522
	 * Tests if caching for SSViewer_FromString is working
1523
	 */
1524
	public function testFromStringCaching() {
1525
		$content = 'Test content';
1526
		$cacheFile = TEMP_FOLDER . '/.cache.' . sha1($content);
1527
		if (file_exists($cacheFile)) {
1528
			unlink($cacheFile);
1529
		}
1530
1531
		// Test global behaviors
1532
		$this->render($content, null, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1533
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1534
1535
		Config::inst()->update('SSViewer_FromString', 'cache_template', true);
1536
		$this->render($content, null, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1537
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1538
		unlink($cacheFile);
1539
1540
		// Test instance behaviors
1541
		$this->render($content, null, false);
1542
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1543
1544
		$this->render($content, null, true);
1545
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1546
		unlink($cacheFile);
1547
	}
1548
}
1549
1550
/**
1551
 * A test fixture that will echo back the template item
1552
 */
1553
class SSViewerTestFixture extends ViewableData {
1554
	protected $name;
1555
1556
	public function __construct($name = null) {
1557
		$this->name = $name;
1558
		parent::__construct();
1559
	}
1560
1561
1562
	private function argedName($fieldName, $arguments) {
1563
		$childName = $this->name ? "$this->name.$fieldName" : $fieldName;
1564
		if($arguments) return $childName . '(' . implode(',', $arguments) . ')';
1565
		else return $childName;
1566
	}
1567
	public function obj($fieldName, $arguments=null, $forceReturnedObject=true, $cache=false, $cacheName=null) {
1568
		$childName = $this->argedName($fieldName, $arguments);
1569
1570
		// Special field name Loop### to create a list
1571
		if(preg_match('/^Loop([0-9]+)$/', $fieldName, $matches)) {
1572
			$output = new ArrayList();
1573
			for($i=0;$i<$matches[1];$i++) $output->push(new SSViewerTestFixture($childName));
1574
			return $output;
1575
1576
		} else if(preg_match('/NotSet/i', $fieldName)) {
1577
			return new ViewableData();
1578
1579
		} else {
1580
			return new SSViewerTestFixture($childName);
1581
		}
1582
	}
1583
1584
1585
	public function XML_val($fieldName, $arguments = null, $cache = false) {
1586
		if(preg_match('/NotSet/i', $fieldName)) {
1587
			return '';
1588
		} else if(preg_match('/Raw/i', $fieldName)) {
1589
			return $fieldName;
1590
		} else {
1591
			return '[out:' . $this->argedName($fieldName, $arguments) . ']';
1592
		}
1593
	}
1594
1595
	public function hasValue($fieldName, $arguments = null, $cache = true) {
1596
		return (bool)$this->XML_val($fieldName, $arguments);
1597
	}
1598
}
1599
1600
class SSViewerTest_ViewableData extends ViewableData implements TestOnly {
1601
1602
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1603
		'TextValue' => 'Text',
1604
		'HTMLValue' => 'HTMLText'
1605
	);
1606
1607
	public function methodWithOneArgument($arg1) {
1608
		return "arg1:{$arg1}";
1609
	}
1610
1611
	public function methodWithTwoArguments($arg1, $arg2) {
1612
		return "arg1:{$arg1},arg2:{$arg2}";
1613
	}
1614
}
1615
1616
1617
class SSViewerTest_Controller extends Controller {
1618
1619
}
1620
1621
class SSViewerTest_Object extends DataObject implements TestOnly {
1622
1623
	public $number = null;
1624
1625
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1626
		'Link' => 'Text',
1627
	);
1628
1629
1630
	public function __construct($number = null) {
1631
		parent::__construct();
1632
		$this->number = $number;
1633
	}
1634
1635
	public function Number() {
1636
		return $this->number;
1637
	}
1638
1639
	public function absoluteBaseURL() {
1640
		return "testLocalFunctionPriorityCalled";
1641
	}
1642
1643
	public function lotsOfArguments11($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
1644
		return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
1645
	}
1646
1647
	public function Link() {
1648
		return 'some/url.html';
1649
	}
1650
}
1651
1652
class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly {
1653
1654
	public static function get_template_global_variables() {
1655
		return array(
1656
			'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html', 'casting' => 'HTMLText'),
1657
			'SSViewerTest_GlobalHTMLEscaped' => array('method' => 'get_html'),
1658
1659
			'SSViewerTest_GlobalAutomatic',
1660
			'SSViewerTest_GlobalReferencedByString' => 'get_reference',
1661
			'SSViewerTest_GlobalReferencedInArray' => array('method' => 'get_reference'),
1662
1663
			'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix', 'casting' => 'HTMLText')
1664
1665
		);
1666
	}
1667
1668
	public static function get_html() {
1669
		return '<div></div>';
1670
	}
1671
1672
	public static function SSViewerTest_GlobalAutomatic() {
1673
		return 'automatic';
1674
	}
1675
1676
	public static function get_reference() {
1677
		return 'reference';
1678
	}
1679
1680
	public static function get_argmix() {
1681
		$args = func_get_args();
1682
		return 'z' . implode(':', $args) . 'z';
1683
	}
1684
1685
}
1686
1687
class SSViewerTest_LevelTest extends ViewableData implements TestOnly {
1688
	protected $depth;
1689
1690
	public function __construct($depth = 1) {
1691
		$this->depth = $depth;
1692
	}
1693
1694
	public function output($val) {
1695
		return "$this->depth-$val";
1696
	}
1697
1698
	public function forLoop($number) {
1699
		$ret = array();
1700
		for($i = 0; $i < (int)$number; ++$i) {
1701
			$ret[] = new SSViewerTest_Object("!$i");
1702
		}
1703
		return new ArrayList($ret);
1704
	}
1705
1706
	public function forWith($number) {
1707
		return new self($number);
1708
	}
1709
}
1710
1711