Completed
Push — master ( 33496d...9e3f76 )
by Daniel
12:54
created

SSViewerTest::testIncludeWithArguments()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 79
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 50
nc 1
nop 0
dl 0
loc 79
rs 8.8701
c 1
b 1
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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() {
0 ignored issues
show
Coding Style introduced by
setUp uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
17
		parent::setUp();
18
		Config::inst()->update('SSViewer', 'source_file_comments', false);
19
		Config::inst()->update('SSViewer_FromString', 'cache_template', false);
20
		AssetStoreTest_SpyStore::activate('SSViewerTest');
21
		$this->oldServer = $_SERVER;
22
	}
23
24
	public function tearDown() {
0 ignored issues
show
Coding Style introduced by
tearDown uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
25
		$_SERVER = $this->oldServer;
26
		AssetStoreTest_SpyStore::reset();
27
		parent::tearDown();
28
	}
29
30
	/**
31
	 * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
32
	 * of user defined themes via {@link SiteConfig} and default theme
33
	 * when no user themes are defined.
34
	 */
35
	public function testCurrentTheme() {
36
		//TODO: SiteConfig moved to CMS
37
		Config::inst()->update('SSViewer', 'theme', 'mytheme');
38
		$this->assertEquals('mytheme', Config::inst()->get('SSViewer', 'theme'),
39
			'Current theme is the default - user has not defined one');
40
	}
41
42
	/**
43
	 * Test that a template without a <head> tag still renders.
44
	 */
45
	public function testTemplateWithoutHeadRenders() {
46
		$data = new ArrayData(array(
47
			'Var' => 'var value'
48
		));
49
50
		$result = $data->renderWith("SSViewerTestPartialTemplate");
51
		$this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U",'',$result)));
52
	}
53
54
	public function testIncludeScopeInheritance() {
55
		$data = $this->getScopeInheritanceTestData();
56
		$expected = array(
57
			'Item 1 - First-ODD top:Item 1',
58
			'Item 2 - EVEN top:Item 2',
59
			'Item 3 - ODD top:Item 3',
60
			'Item 4 - EVEN top:Item 4',
61
			'Item 5 - ODD top:Item 5',
62
			'Item 6 - Last-EVEN top:Item 6',
63
		);
64
65
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
66
		$this->assertExpectedStrings($result, $expected);
67
68
		// reset results for the tests that include arguments (the title is passed as an arg)
69
		$expected = array(
70
			'Item 1 _ Item 1 - First-ODD top:Item 1',
71
			'Item 2 _ Item 2 - EVEN top:Item 2',
72
			'Item 3 _ Item 3 - ODD top:Item 3',
73
			'Item 4 _ Item 4 - EVEN top:Item 4',
74
			'Item 5 _ Item 5 - ODD top:Item 5',
75
			'Item 6 _ Item 6 - Last-EVEN top:Item 6',
76
		);
77
78
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
79
		$this->assertExpectedStrings($result, $expected);
80
	}
81
82
	public function testIncludeTruthyness() {
83
		$data = new ArrayData(array(
84
			'Title' => 'TruthyTest',
85
			'Items' => new ArrayList(array(
86
				new ArrayData(array('Title' => 'Item 1')),
87
				new ArrayData(array('Title' => '')),
88
				new ArrayData(array('Title' => true)),
89
				new ArrayData(array('Title' => false)),
90
				new ArrayData(array('Title' => null)),
91
				new ArrayData(array('Title' => 0)),
92
				new ArrayData(array('Title' => 7))
93
			))
94
		));
95
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
96
97
		// We should not end up with empty values appearing as empty
98
		$expected = array(
99
			'Item 1 _ Item 1 - First-ODD top:Item 1',
100
			'Untitled - EVEN top:',
101
			'1 _ 1 - ODD top:1',
102
			'Untitled - EVEN top:',
103
			'Untitled - ODD top:',
104
			'Untitled - EVEN top:0',
105
			'7 _ 7 - Last-ODD top:7'
106
		);
107
		$this->assertExpectedStrings($result, $expected);
108
	}
109
110
	private function getScopeInheritanceTestData() {
111
		return new ArrayData(array(
112
			'Title' => 'TopTitleValue',
113
			'Items' => new ArrayList(array(
114
				new ArrayData(array('Title' => 'Item 1')),
115
				new ArrayData(array('Title' => 'Item 2')),
116
				new ArrayData(array('Title' => 'Item 3')),
117
				new ArrayData(array('Title' => 'Item 4')),
118
				new ArrayData(array('Title' => 'Item 5')),
119
				new ArrayData(array('Title' => 'Item 6'))
120
			))
121
		));
122
	}
123
124
	private function assertExpectedStrings($result, $expected) {
125
		foreach ($expected as $expectedStr) {
126
			$this->assertTrue(
127
				(boolean) preg_match("/{$expectedStr}/", $result),
128
				"Didn't find '{$expectedStr}' in:\n{$result}"
129
			);
130
		}
131
	}
132
133
	/**
134
	 * Small helper to render templates from strings
135
	 */
136
	public function render($templateString, $data = null, $cacheTemplate = false) {
137
		$t = SSViewer::fromString($templateString, $cacheTemplate);
138
		if(!$data) $data = new SSViewerTestFixture();
139
		return trim(''.$t->process($data));
140
	}
141
142
	public function testRequirements() {
143
		$requirements = $this->getMock("Requirements_Backend", array("javascript", "css"));
144
		$jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
145
		$cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
146
147
		$requirements->expects($this->once())->method('javascript')->with($jsFile);
148
		$requirements->expects($this->once())->method('css')->with($cssFile);
149
150
		$origReq = Requirements::backend();
151
		Requirements::set_backend($requirements);
152
		$template = $this->render("<% require javascript($jsFile) %>
153
		<% require css($cssFile) %>");
154
		Requirements::set_backend($origReq);
155
156
		$this->assertFalse((bool)trim($template), "Should be no content in this return.");
157
	}
158
159
	public function testRequirementsCombine(){
160
		$testBackend = Injector::inst()->create('Requirements_Backend');
161
		$testBackend->setSuffixRequirements(false);
162
		//$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
163
164
		$jsFile = FRAMEWORK_DIR . '/tests/view/themes/javascript/bad.js';
165
		$jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
166
		$testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
167
168
		// first make sure that our test js file causes an exception to be thrown
169
		try{
170
			require_once('thirdparty/jsmin/jsmin.php');
171
			$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...
172
			$this->fail('JSMin did not throw exception on minify bad file: ');
173
		} catch(Exception $e) {
174
			// exception thrown... good
175
		}
176
177
		// secondly, make sure that requirements is generated, even though minification failed
178
		$testBackend->processCombinedFiles();
179
		$js = $testBackend->getJavascript();
180
		$combinedTestFilePath = BASE_PATH . reset($js);
181
		$this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
182
183
		// and make sure the combined content matches the input content, i.e. no loss of functionality
184
		if(!file_exists($combinedTestFilePath)) {
185
			$this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
186
		}
187
		$combinedTestFileContents = file_get_contents($combinedTestFilePath);
188
		$this->assertContains($jsFileContents, $combinedTestFileContents);
189
	}
190
191
192
193
	public function testComments() {
194
		$output = $this->render(<<<SS
195
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
196
<%-- Alone multi
197
	line comment --%>
198
Some more content
199
Mixing content and <%-- multi
200
	line comment --%> Final final
201
content
202
SS
203
);
204
		$shouldbe = <<<SS
205
This is my templateThis is some contentFinal content
206
207
Some more content
208
Mixing content and  Final final
209
content
210
SS;
211
212
		$this->assertEquals($shouldbe, $output);
213
	}
214
215
	public function testBasicText() {
216
		$this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
217
		$this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
218
		$this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
219
		$this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
220
	}
221
222
	public function testBasicInjection() {
223
		$this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
224
		$this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
225
		$this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
226
		$this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
227
228
		$this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
229
		$this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
230
		$this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
231
232
		$this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
233
		$this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'),
234
			'Escapes before injections are correctly unescaped');
235
	}
236
237
238
	public function testGlobalVariableCalls() {
239
		$this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
240
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
241
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
242
	}
243
244
	public function testGlobalVariableCallsWithArguments() {
245
		$this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
246
		$this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
247
		$this->assertEquals('zFoo:Bar:Bazz',
248
			$this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")'));
249
		$this->assertEquals('zreferencez',
250
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)'));
251
	}
252
253
	public function testGlobalVariablesAreEscaped() {
254
		$this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
255
		$this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
256
257
		$this->assertEquals('z<div></div>z',
258
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)'));
259
		$this->assertEquals('z&lt;div&gt;&lt;/div&gt;z',
260
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)'));
261
	}
262
263
	public function testCoreGlobalVariableCalls() {
264
		$this->assertEquals(Director::absoluteBaseURL(),
265
			$this->render('{$absoluteBaseURL}'), 'Director::absoluteBaseURL can be called from within template');
266
		$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$AbsoluteBaseURL}'),
267
			'Upper-case %AbsoluteBaseURL can be called from within template');
268
269
		$this->assertEquals(Director::is_ajax(), $this->render('{$isAjax}'),
270
			'All variations of is_ajax result in the correct call');
271
		$this->assertEquals(Director::is_ajax(), $this->render('{$IsAjax}'),
272
			'All variations of is_ajax result in the correct call');
273
		$this->assertEquals(Director::is_ajax(), $this->render('{$is_ajax}'),
274
			'All variations of is_ajax result in the correct call');
275
		$this->assertEquals(Director::is_ajax(), $this->render('{$Is_ajax}'),
276
			'All variations of is_ajax result in the correct call');
277
278
		$this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'),
279
			'i18n template functions result correct result');
280
		$this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'),
281
			'i18n template functions result correct result');
282
283
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'),
284
			'Member template functions result correct result');
285
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'),
286
			'Member template functions result correct result');
287
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'),
288
			'Member template functions result correct result');
289
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentUser}'),
290
			'Member template functions result correct result');
291
292
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'),
293
			'SecurityToken template functions result correct result');
294
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'),
295
			'SecurityToken template functions result correct result');
296
297
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
298
			'Permissions template functions result correct result');
299
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
300
			'Permissions template functions result correct result');
301
	}
302
303
	public function testNonFieldCastingHelpersNotUsedInHasValue() {
304
		// check if Link without $ in front of variable
305
		$result = $this->render(
306
			'A<% if Link %>$Link<% end_if %>B', new SSViewerTest_Object());
307
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
308
309
		// check if Link with $ in front of variable
310
		$result = $this->render(
311
			'A<% if $Link %>$Link<% end_if %>B', new SSViewerTest_Object());
312
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
313
	}
314
315
	public function testLocalFunctionsTakePriorityOverGlobals() {
316
		$data = new ArrayData(array(
317
			'Page' => new SSViewerTest_Object()
318
		));
319
320
		//call method with lots of arguments
321
		$result = $this->render(
322
			'<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',$data);
323
		$this->assertEquals("abcdefghijk",$result, "public function can accept up to 11 arguments");
324
325
		//call method that does not exist
326
		$result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>',$data);
327
		$this->assertEquals("",$result, "Method does not exist - empty result");
328
329
		//call if that does not exist
330
		$result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>',$data);
331
		$this->assertEquals("",$result, "Method does not exist - empty result");
332
333
		//call method with same name as a global method (local call should take priority)
334
		$result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>',$data);
335
		$this->assertEquals("testLocalFunctionPriorityCalled",$result,
336
			"Local Object's public function called. Did not return the actual baseURL of the current site");
337
	}
338
339
	public function testCurrentScopeLoopWith() {
340
		// Data to run the loop tests on - one sequence of three items, each with a subitem
341
		$data = new ArrayData(array(
342
			'Foo' => new ArrayList(array(
343
				'Subocean' => new ArrayData(array(
344
						'Name' => 'Higher'
345
					)),
346
				new ArrayData(array(
347
					'Sub' => new ArrayData(array(
348
						'Name' => 'SubKid1'
349
					))
350
				)),
351
				new ArrayData(array(
352
					'Sub' => new ArrayData(array(
353
						'Name' => 'SubKid2'
354
					))
355
				)),
356
				new SSViewerTest_Object('Number6')
357
			))
358
		));
359
360
		$result = $this->render(
361
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
362
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
363
364
		$result = $this->render(
365
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
366
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
367
368
		$result = $this->render('<% with Foo %>$Count<% end_with %>',$data);
369
		$this->assertEquals("4",$result, "4 items in the DataObjectSet");
370
371
		$result = $this->render('<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
372
			. '<% end_if %><% end_loop %><% end_with %>',$data);
373
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in with Up.Foo scope works");
374
375
		$result = $this->render('<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
376
			. '<% end_if %><% end_loop %><% end_with %>',$data);
377
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in current scope works");
378
	}
379
380
	public function testObjectDotArguments() {
381
		$this->assertEquals(
382
			'[out:TestObject.methodWithOneArgument(one)]
383
				[out:TestObject.methodWithTwoArguments(one,two)]
384
				[out:TestMethod(Arg1,Arg2).Bar.Val]
385
				[out:TestMethod(Arg1,Arg2).Bar]
386
				[out:TestMethod(Arg1,Arg2)]
387
				[out:TestMethod(Arg1).Bar.Val]
388
				[out:TestMethod(Arg1).Bar]
389
				[out:TestMethod(Arg1)]',
390
			$this->render('$TestObject.methodWithOneArgument(one)
391
				$TestObject.methodWithTwoArguments(one,two)
392
				$TestMethod(Arg1, Arg2).Bar.Val
393
				$TestMethod(Arg1, Arg2).Bar
394
				$TestMethod(Arg1, Arg2)
395
				$TestMethod(Arg1).Bar.Val
396
				$TestMethod(Arg1).Bar
397
				$TestMethod(Arg1)')
398
		);
399
	}
400
401
	public function testEscapedArguments() {
402
		$this->assertEquals(
403
			'[out:Foo(Arg1,Arg2).Bar.Val].Suffix
404
				[out:Foo(Arg1,Arg2).Val]_Suffix
405
				[out:Foo(Arg1,Arg2)]/Suffix
406
				[out:Foo(Arg1).Bar.Val]textSuffix
407
				[out:Foo(Arg1).Bar].Suffix
408
				[out:Foo(Arg1)].Suffix
409
				[out:Foo.Bar.Val].Suffix
410
				[out:Foo.Bar].Suffix
411
				[out:Foo].Suffix',
412
			$this->render('{$Foo(Arg1, Arg2).Bar.Val}.Suffix
413
				{$Foo(Arg1, Arg2).Val}_Suffix
414
				{$Foo(Arg1, Arg2)}/Suffix
415
				{$Foo(Arg1).Bar.Val}textSuffix
416
				{$Foo(Arg1).Bar}.Suffix
417
				{$Foo(Arg1)}.Suffix
418
				{$Foo.Bar.Val}.Suffix
419
				{$Foo.Bar}.Suffix
420
				{$Foo}.Suffix')
421
		);
422
	}
423
424
	public function testLoopWhitespace() {
425
		$this->assertEquals(
426
			'before[out:SingleItem.Test]after
427
				beforeTestafter',
428
			$this->render('before<% loop SingleItem %>$Test<% end_loop %>after
429
				before<% loop SingleItem %>Test<% end_loop %>after')
430
		);
431
432
		// The control tags are removed from the output, but no whitespace
433
		// This is a quirk that could be changed, but included in the test to make the current
434
		// behaviour explicit
435
		$this->assertEquals(
436
			'before
437
438
[out:SingleItem.ItemOnItsOwnLine]
439
440
after',
441
			$this->render('before
442
<% loop SingleItem %>
443
$ItemOnItsOwnLine
444
<% end_loop %>
445
after')
446
		);
447
448
		// The whitespace within the control tags is preserve in a loop
449
		// This is a quirk that could be changed, but included in the test to make the current
450
		// behaviour explicit
451
		$this->assertEquals(
452
			'before
453
454
[out:Loop3.ItemOnItsOwnLine]
455
456
[out:Loop3.ItemOnItsOwnLine]
457
458
[out:Loop3.ItemOnItsOwnLine]
459
460
after',
461
			$this->render('before
462
<% loop Loop3 %>
463
$ItemOnItsOwnLine
464
<% end_loop %>
465
after')
466
		);
467
	}
468
469
	public function testControls() {
470
		// Single item controls
471
		$this->assertEquals(
472
			'a[out:Foo.Bar.Item]b
473
				[out:Foo.Bar(Arg1).Item]
474
				[out:Foo(Arg1).Item]
475
				[out:Foo(Arg1,Arg2).Item]
476
				[out:Foo(Arg1,Arg2,Arg3).Item]',
477
			$this->render('<% with Foo.Bar %>a{$Item}b<% end_with %>
478
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
479
				<% with Foo(Arg1) %>$Item<% end_with %>
480
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
481
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>')
482
		);
483
484
		// Loop controls
485
		$this->assertEquals('a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
486
			$this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>'));
487
488
		$this->assertEquals('[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
489
			$this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>'));
490
491
		$this->assertEquals('[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
492
			$this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>'));
493
494
		$this->assertEquals('[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
495
			$this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>'));
496
497
		$this->assertEquals('[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
498
			$this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>'));
499
500
	}
501
502
	public function testIfBlocks() {
503
		// Basic test
504
		$this->assertEquals('AC',
505
			$this->render('A<% if NotSet %>B$NotSet<% end_if %>C'));
506
507
		// Nested test
508
		$this->assertEquals('AB1C',
509
			$this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C'));
510
511
		// else_if
512
		$this->assertEquals('ACD',
513
			$this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
514
		$this->assertEquals('AD',
515
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D'));
516
		$this->assertEquals('ADE',
517
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
518
519
		$this->assertEquals('ADE',
520
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
521
522
		// Dot syntax
523
		$this->assertEquals('ACD',
524
			$this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D'));
525
		$this->assertEquals('ACD',
526
			$this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D'));
527
528
		// Params
529
		$this->assertEquals('ACD',
530
			$this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D'));
531
		$this->assertEquals('ABD',
532
			$this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D'));
533
534
		// Negation
535
		$this->assertEquals('AC',
536
			$this->render('A<% if not IsSet %>B<% end_if %>C'));
537
		$this->assertEquals('ABC',
538
			$this->render('A<% if not NotSet %>B<% end_if %>C'));
539
540
		// Or
541
		$this->assertEquals('ABD',
542
			$this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D'));
543
		$this->assertEquals('ACD',
544
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D'));
545
		$this->assertEquals('AD',
546
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D'));
547
		$this->assertEquals('ACD',
548
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D'));
549
		$this->assertEquals('AD',
550
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D'));
551
552
		// Negated Or
553
		$this->assertEquals('ACD',
554
			$this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
555
		$this->assertEquals('ABD',
556
			$this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
557
		$this->assertEquals('ABD',
558
			$this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
559
560
		// And
561
		$this->assertEquals('ABD',
562
			$this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D'));
563
		$this->assertEquals('ACD',
564
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
565
		$this->assertEquals('AD',
566
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D'));
567
		$this->assertEquals('ACD',
568
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D'));
569
		$this->assertEquals('AD',
570
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D'));
571
572
		// Equality
573
		$this->assertEquals('ABC',
574
			$this->render('A<% if RawVal == RawVal %>B<% end_if %>C'));
575
		$this->assertEquals('ACD',
576
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D'));
577
		$this->assertEquals('ABC',
578
			$this->render('A<% if Right != Wrong %>B<% end_if %>C'));
579
		$this->assertEquals('AD',
580
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D'));
581
582
		// test inequalities with simple numbers
583
		$this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
584
		$this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
585
		$this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
586
		$this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
587
588
		$this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
589
		$this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
590
		$this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
591
		$this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
592
593
		$this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
594
		$this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
595
		$this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
596
		$this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
597
598
		// empty else_if and else tags, if this would not be supported,
599
		// the output would stop after A, thereby failing the assert
600
		$this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
601
		$this->assertEquals('AD',
602
			$this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D'));
603
		$this->assertEquals('AD',
604
			$this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D'));
605
606
		// Bare words with ending space
607
		$this->assertEquals('ABC',
608
			$this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C'));
609
610
		// Else
611
		$this->assertEquals('ADE',
612
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E'));
613
614
		// Empty if with else
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
615
		$this->assertEquals('ABC',
616
			$this->render('A<% if NotSet %><% else %>B<% end_if %>C'));
617
	}
618
619
	public function testBaseTagGeneration() {
620
		// XHTML wil have a closed base tag
621
		$tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
622
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
623
				. ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
624
			<html>
625
				<head><% base_tag %></head>
626
				<body><p>test</p><body>
627
			</html>';
628
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
629
630
		// HTML4 and 5 will only have it for IE
631
		$tmpl2 = '<!DOCTYPE html>
632
			<html>
633
				<head><% base_tag %></head>
634
				<body><p>test</p><body>
635
			</html>';
636
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
637
			$this->render($tmpl2));
638
639
640
		$tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
641
			<html>
642
				<head><% base_tag %></head>
643
				<body><p>test</p><body>
644
			</html>';
645
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
646
			$this->render($tmpl3));
647
648
		// Check that the content negotiator converts to the equally legal formats
649
		$negotiator = new ContentNegotiator();
650
651
		$response = new SS_HTTPResponse($this->render($tmpl1));
652
		$negotiator->html($response);
653
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
654
			$response->getBody());
655
656
		$response = new SS_HTTPResponse($this->render($tmpl1));
657
		$negotiator->xhtml($response);
658
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
659
	}
660
661
	public function testIncludeWithArguments() {
662
		$this->assertEquals(
663
			$this->render('<% include SSViewerTestIncludeWithArguments %>'),
664
			'<p>[out:Arg1]</p><p>[out:Arg2]</p>'
665
		);
666
667
		$this->assertEquals(
668
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
669
			'<p>A</p><p>[out:Arg2]</p>'
670
		);
671
672
		$this->assertEquals(
673
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
674
			'<p>A</p><p>B</p>'
675
		);
676
677
		$this->assertEquals(
678
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
679
			'<p>A Bare String</p><p>B Bare String</p>'
680
		);
681
682
		$this->assertEquals(
683
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
684
				new ArrayData(array('B' => 'Bar'))),
685
			'<p>A</p><p>Bar</p>'
686
		);
687
688
		$this->assertEquals(
689
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
690
				new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))),
691
			'<p>A</p><p>Bar</p>'
692
		);
693
694
		$this->assertEquals(
695
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
696
				new ArrayData(array('Items' => new ArrayList(array(
697
					new ArrayData(array('Title' => 'Foo')),
698
					new ArrayData(array('Title' => 'Bar'))
699
				))))),
700
			'SomeArg - Foo - Bar - SomeArg'
701
		);
702
703
		$this->assertEquals(
704
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
705
				new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))),
706
			'A - B - A'
707
		);
708
709
		$this->assertEquals(
710
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
711
				new ArrayData(array(
712
					'Item' => new ArrayData(array(
713
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
714
					)))
715
				)),
716
			'A - B - C - B - A'
717
		);
718
719
		$this->assertEquals(
720
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
721
				new ArrayData(array(
722
					'Item' => new ArrayData(array(
723
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
724
					)))
725
				)),
726
			'A - A - A'
727
		);
728
729
		$data = new ArrayData(array(
730
			'Nested' => new ArrayData(array(
731
				'Object' => new ArrayData(array('Key' => 'A'))
732
			)),
733
			'Object' => new ArrayData(array('Key' => 'B'))
734
		));
735
736
		$tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
737
		$res  = $tmpl->process($data);
738
		$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...
739
	}
740
741
	public function testNamespaceInclude() {
742
		$data = new ArrayData([]);
743
744
		$this->assertEquals(
745
			"tests:( NamespaceInclude\n )",
746
			$this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
747
			'Backslashes work for namespace references in includes'
748
		);
749
750
		$this->assertEquals(
751
			"tests:( NamespaceInclude\n )",
752
			$this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
753
			'Forward slashes work for namespace references in includes'
754
		);
755
756
		$this->assertEquals(
757
			"tests:( NamespaceInclude\n )",
758
			$this->render('tests:( <% include NamespaceInclude %> )', $data),
759
			'Namespace can be missed for a namespaed include'
760
		);
761
	}
762
763
764
	public function testRecursiveInclude() {
765
		$view = new SSViewer(array('SSViewerTestRecursiveInclude'));
766
767
		$data = new ArrayData(array(
768
			'Title' => 'A',
769
			'Children' => new ArrayList(array(
770
				new ArrayData(array(
771
					'Title' => 'A1',
772
					'Children' => new ArrayList(array(
773
						new ArrayData(array( 'Title' => 'A1 i', )),
774
						new ArrayData(array( 'Title' => 'A1 ii', )),
775
					)),
776
				)),
777
				new ArrayData(array( 'Title' => 'A2', )),
778
				new ArrayData(array( 'Title' => 'A3', )),
779
			)),
780
		));
781
782
		$result = $view->process($data);
783
		// We don't care about whitespace
784
		$rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
785
786
		$this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
787
	}
788
789
	public function assertEqualIgnoringWhitespace($a, $b) {
790
		$this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b));
791
	}
792
793
	/**
794
	 * See {@link ViewableDataTest} for more extensive casting tests,
795
	 * this test just ensures that basic casting is correctly applied during template parsing.
796
	 */
797
	public function testCastingHelpers() {
798
		$vd = new SSViewerTest_ViewableData();
799
		$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...
800
		$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...
801
		$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...
802
803
		// Value casted as "Text"
804
		$this->assertEquals(
805
			'&lt;b&gt;html&lt;/b&gt;',
806
			$t = SSViewer::fromString('$TextValue')->process($vd)
807
		);
808
		$this->assertEquals(
809
			'<b>html</b>',
810
			$t = SSViewer::fromString('$TextValue.RAW')->process($vd)
811
		);
812
		$this->assertEquals(
813
			'&lt;b&gt;html&lt;/b&gt;',
814
			$t = SSViewer::fromString('$TextValue.XML')->process($vd)
815
		);
816
817
		// Value casted as "HTMLText"
818
		$this->assertEquals(
819
			'<b>html</b>',
820
			$t = SSViewer::fromString('$HTMLValue')->process($vd)
821
		);
822
		$this->assertEquals(
823
			'<b>html</b>',
824
			$t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
825
		);
826
		$this->assertEquals(
827
			'&lt;b&gt;html&lt;/b&gt;',
828
			$t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
829
		);
830
831
		// Uncasted value (falls back to ViewableData::$default_cast="HTMLText")
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
			'<b>html</b>',
836
			$t = SSViewer::fromString('$UncastedValue')->process($vd)
837
		);
838
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
839
		$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...
840
		$this->assertEquals(
841
			'<b>html</b>',
842
			$t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
843
		);
844
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
845
		$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...
846
		$this->assertEquals(
847
			'&lt;b&gt;html&lt;/b&gt;',
848
			$t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
849
		);
850
	}
851
852
	public function testSSViewerBasicIteratorSupport() {
853
		$data = new ArrayData(array(
854
			'Set' => new ArrayList(array(
855
				new SSViewerTest_Object("1"),
856
				new SSViewerTest_Object("2"),
857
				new SSViewerTest_Object("3"),
858
				new SSViewerTest_Object("4"),
859
				new SSViewerTest_Object("5"),
860
				new SSViewerTest_Object("6"),
861
				new SSViewerTest_Object("7"),
862
				new SSViewerTest_Object("8"),
863
				new SSViewerTest_Object("9"),
864
				new SSViewerTest_Object("10"),
865
			))
866
		));
867
868
		//base test
869
		$result = $this->render('<% loop Set %>$Number<% end_loop %>',$data);
870
		$this->assertEquals("12345678910",$result,"Numbers rendered in order");
871
872
		//test First
873
		$result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>',$data);
874
		$this->assertEquals("1",$result,"Only the first number is rendered");
875
876
		//test Last
877
		$result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>',$data);
878
		$this->assertEquals("10",$result,"Only the last number is rendered");
879
880
		//test Even
881
		$result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>',$data);
882
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
883
884
		//test Even with quotes
885
		$result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>',$data);
886
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
887
888
		//test Even without quotes
889
		$result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>',$data);
890
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
891
892
		//test Even with zero-based start index
893
		$result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>',$data);
894
		$this->assertEquals("13579",$result,"Even (with zero-based index) numbers rendered in order");
895
896
		//test Odd
897
		$result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>',$data);
898
		$this->assertEquals("13579",$result,"Odd numbers rendered in order");
899
900
		//test FirstLast
901
		$result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>',$data);
902
		$this->assertEquals("1first10last",$result,"First and last numbers rendered in order");
903
904
		//test Middle
905
		$result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>',$data);
906
		$this->assertEquals("23456789",$result,"Middle numbers rendered in order");
907
908
		//test MiddleString
909
		$result = $this->render('<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
910
			. '<% end_loop %>',$data);
911
		$this->assertEquals("2middle3middle4middle5middle6middle7middle8middle9middle",$result,
912
			"Middle numbers rendered in order");
913
914
		//test EvenOdd
915
		$result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>',$data);
916
		$this->assertEquals("oddevenoddevenoddevenoddevenoddeven",$result,
917
			"Even and Odd is returned in sequence numbers rendered in order");
918
919
		//test Pos
920
		$result = $this->render('<% loop Set %>$Pos<% end_loop %>',$data);
921
		$this->assertEquals("12345678910", $result, '$Pos is rendered in order');
922
923
		//test Pos
924
		$result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>',$data);
925
		$this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
926
927
		//test FromEnd
928
		$result = $this->render('<% loop Set %>$FromEnd<% end_loop %>',$data);
929
		$this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
930
931
		//test FromEnd
932
		$result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>',$data);
933
		$this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
934
935
		//test Total
936
		$result = $this->render('<% loop Set %>$TotalItems<% end_loop %>',$data);
937
		$this->assertEquals("10101010101010101010",$result,"10 total items X 10 are returned");
938
939
		//test Modulus
940
		$result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>',$data);
941
		$this->assertEquals("1010101010",$result,"1-indexed pos modular divided by 2 rendered in order");
942
943
		//test MultipleOf 3
944
		$result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>',$data);
945
		$this->assertEquals("369",$result,"Only numbers that are multiples of 3 are returned");
946
947
		//test MultipleOf 4
948
		$result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>',$data);
949
		$this->assertEquals("48",$result,"Only numbers that are multiples of 4 are returned");
950
951
		//test MultipleOf 5
952
		$result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>',$data);
953
		$this->assertEquals("510",$result,"Only numbers that are multiples of 5 are returned");
954
955
		//test MultipleOf 10
956
		$result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>',$data);
957
		$this->assertEquals("10",$result,"Only numbers that are multiples of 10 (with 1-based indexing) are returned");
958
959
		//test MultipleOf 9 zero-based
960
		$result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>',$data);
961
		$this->assertEquals("110",$result,
962
			"Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)");
963
964
		//test MultipleOf 11
965
		$result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>',$data);
966
		$this->assertEquals("",$result,"Only numbers that are multiples of 11 are returned. I.e. nothing returned");
967
	}
968
969
	/**
970
	 * Test $Up works when the scope $Up refers to was entered with a "with" block
971
	 */
972
	public function testUpInWith() {
973
974
		// Data to run the loop tests on - three levels deep
975
		$data = new ArrayData(array(
976
			'Name' => 'Top',
977
			'Foo' => new ArrayData(array(
978
				'Name' => 'Foo',
979
				'Bar' => new ArrayData(array(
980
					'Name' => 'Bar',
981
					'Baz' => new ArrayData(array(
982
						'Name' => 'Baz'
983
					)),
984
					'Qux' => new ArrayData(array(
985
						'Name' => 'Qux'
986
					))
987
				))
988
			))
989
		));
990
991
		// Basic functionality
992
		$this->assertEquals('BarFoo',
993
			$this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data));
994
995
		// Two level with block, up refers to internally referenced Bar
996
		$this->assertEquals('BarFoo',
997
			$this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data));
998
999
		// Stepping up & back down the scope tree
1000
		$this->assertEquals('BazBarQux',
1001
			$this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data));
1002
1003
		// Using $Up in a with block
1004
		$this->assertEquals('BazBarQux',
1005
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
1006
				.'<% end_with %>', $data));
1007
1008
		// Stepping up & back down the scope tree with with blocks
1009
		$this->assertEquals('BazBarQuxBarBaz',
1010
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
1011
				. '{$Name}<% end_with %>{$Name}<% end_with %>', $data));
1012
1013
		// Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1014
		$this->assertEquals('Foo',
1015
			$this->render('<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
1016
				. '<% end_with %>', $data));
1017
1018
		// Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
1019
		$this->assertEquals('Foo',
1020
			$this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data));
1021
	}
1022
1023
	/**
1024
	 * Test $Up works when the scope $Up refers to was entered with a "loop" block
1025
	 */
1026
	public function testUpInLoop(){
1027
1028
		// Data to run the loop tests on - one sequence of three items, each with a subitem
1029
		$data = new ArrayData(array(
1030
			'Name' => 'Top',
1031
			'Foo' => new ArrayList(array(
1032
				new ArrayData(array(
1033
					'Name' => '1',
1034
					'Sub' => new ArrayData(array(
1035
						'Name' => 'Bar'
1036
					))
1037
				)),
1038
				new ArrayData(array(
1039
					'Name' => '2',
1040
					'Sub' => new ArrayData(array(
1041
						'Name' => 'Baz'
1042
					))
1043
				)),
1044
				new ArrayData(array(
1045
					'Name' => '3',
1046
					'Sub' => new ArrayData(array(
1047
						'Name' => 'Qux'
1048
					))
1049
				))
1050
			))
1051
		));
1052
1053
		// Make sure inside a loop, $Up refers to the current item of the loop
1054
		$this->assertEqualIgnoringWhitespace(
1055
			'111 222 333',
1056
			$this->render(
1057
				'<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1058
				$data
1059
			)
1060
		);
1061
1062
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1063
		// and doesn't interfere with the original iterator
1064
		$this->assertEqualIgnoringWhitespace(
1065
			'1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1066
			$this->render(
1067
				'<% loop $Foo %>
1068
					$Name
1069
					<% with $Sub %>
1070
						$Name
1071
						<% loop $Up %>$Name<% end_loop %>
1072
						$Name
1073
					<% end_with %>
1074
					$Name
1075
				<% end_loop %>',
1076
				$data
1077
			)
1078
		);
1079
1080
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1081
		// and doesn't interfere with the original iterator or local lookups
1082
		$this->assertEqualIgnoringWhitespace(
1083
			'1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1084
			$this->render(
1085
				'<% loop $Foo %>
1086
					$Name
1087
					<% with $Sub %>
1088
						{$Name}{$Up.Name}
1089
						<% loop $Up %>$Name<% end_loop %>
1090
						{$Up.Name}{$Name}
1091
					<% end_with %>
1092
					$Name
1093
				<% end_loop %>',
1094
				$data
1095
			)
1096
		);
1097
	}
1098
1099
	/**
1100
	 * Test that nested loops restore the loop variables correctly when pushing and popping states
1101
	 */
1102
	public function testNestedLoops(){
1103
1104
		// Data to run the loop tests on - one sequence of three items, one with child elements
1105
		// (of a different size to the main sequence)
1106
		$data = new ArrayData(array(
1107
			'Foo' => new ArrayList(array(
1108
				new ArrayData(array(
1109
					'Name' => '1',
1110
					'Children' => new ArrayList(array(
1111
						new ArrayData(array(
1112
							'Name' => 'a'
1113
						)),
1114
						new ArrayData(array(
1115
							'Name' => 'b'
1116
						)),
1117
					)),
1118
				)),
1119
				new ArrayData(array(
1120
					'Name' => '2',
1121
					'Children' => new ArrayList(),
1122
				)),
1123
				new ArrayData(array(
1124
					'Name' => '3',
1125
					'Children' => new ArrayList(),
1126
				)),
1127
			)),
1128
		));
1129
1130
		// Make sure that including a loop inside a loop will not destroy the internal count of
1131
		// items, checked by using "Last"
1132
		$this->assertEqualIgnoringWhitespace(
1133
			'1ab23last',
1134
			$this->render('<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1135
				. '<% end_loop %>', $data
1136
			)
1137
		);
1138
	}
1139
1140
	public function testLayout() {
1141
		$self = $this;
1142
1143
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1144
			$template = new SSViewer(array('Page'));
1145
			$self->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1146
1147
			$template = new SSViewer(array('Shortcodes', 'Page'));
1148
			$self->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1149
		});
1150
	}
1151
1152
	/**
1153
	 * @covers SSViewer::get_templates_by_class()
1154
	 */
1155
	public function testGetTemplatesByClass() {
1156
		$self = $this;
1157
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1158
			// Test passing a string
1159
			$templates = SSViewer::get_templates_by_class(
1160
				'TestNamespace\SSViewerTest_Controller',
1161
				'',
1162
				'Controller'
1163
			);
1164
			$self->assertEquals([
1165
				'TestNamespace\SSViewerTest_Controller',
1166
    			'Controller',
1167
			], $templates);
1168
1169
			// Test to ensure we're stopping at the base class.
1170
			$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTest_Controller', '', 'TestNamespace\SSViewerTest_Controller');
1171
			$self->assertCount(1, $templates);
1172
1173
			// Make sure we can filter our templates by suffix.
1174
			$templates = SSViewer::get_templates_by_class('SSViewerTest', '_Controller');
1175
			$self->assertCount(1, $templates);
1176
1177
			// Let's throw something random in there.
1178
			$self->setExpectedException('InvalidArgumentException');
1179
			$templates = SSViewer::get_templates_by_class(array());
1180
		});
1181
	}
1182
1183
	/**
1184
	 * @covers SSViewer::get_themes()
1185
	 */
1186
	public function testThemeRetrieval() {
1187
		$ds = DIRECTORY_SEPARATOR;
1188
		$testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes';
1189
1190
		if(file_exists($testThemeBaseDir)) Filesystem::removeFolder($testThemeBaseDir);
1191
1192
		mkdir($testThemeBaseDir);
1193
		mkdir($testThemeBaseDir . $ds . 'blackcandy');
1194
		mkdir($testThemeBaseDir . $ds . 'blackcandy_blog');
1195
		mkdir($testThemeBaseDir . $ds . 'darkshades');
1196
		mkdir($testThemeBaseDir . $ds . 'darkshades_blog');
1197
1198
		$this->assertEquals(array(
1199
			'blackcandy' => 'blackcandy',
1200
			'darkshades' => 'darkshades'
1201
		), SSViewer::get_themes($testThemeBaseDir), 'Our test theme directory contains 2 themes');
1202
1203
		$this->assertEquals(array(
1204
			'blackcandy' => 'blackcandy',
1205
			'blackcandy_blog' => 'blackcandy_blog',
1206
			'darkshades' => 'darkshades',
1207
			'darkshades_blog' => 'darkshades_blog'
1208
		), SSViewer::get_themes($testThemeBaseDir, true),
1209
			'Our test theme directory contains 2 themes and 2 sub-themes');
1210
1211
		// Remove all the test themes we created
1212
		Filesystem::removeFolder($testThemeBaseDir);
1213
	}
1214
1215
	public function testRewriteHashlinks() {
0 ignored issues
show
Coding Style introduced by
testRewriteHashlinks uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1216
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1217
		Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
1218
1219
		$_SERVER['HTTP_HOST'] = 'www.mysite.com';
1220
		$_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1221
1222
		// Emulate SSViewer::process()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1223
		// Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1224
		// as protocol-less absolute urls
1225
		$base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1226
1227
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1228
1229
		// Note: SSViewer_FromString doesn't rewrite hash links.
1230
		file_put_contents($tmplFile, '<!DOCTYPE html>
1231
			<html>
1232
				<head><% base_tag %></head>
1233
				<body>
1234
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1235
				$ExternalInsertedLink
1236
				<a class="inline" href="#anchor">InlineLink</a>
1237
				$InsertedLink
1238
				<svg><use xlink:href="#sprite"></use></svg>
1239
				<body>
1240
			</html>');
1241
		$tmpl = new SSViewer($tmplFile);
1242
		$obj = new ViewableData();
1243
		$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...
1244
		$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...
1245
		$result = $tmpl->process($obj);
1246
		$this->assertContains(
1247
			'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1248
			$result
1249
		);
1250
		$this->assertContains(
1251
			'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1252
			$result
1253
		);
1254
		$this->assertContains(
1255
			'<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1256
			$result
1257
		);
1258
		$this->assertContains(
1259
			'<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1260
			$result
1261
		);
1262
		$this->assertContains(
1263
			'<svg><use xlink:href="#sprite"></use></svg>',
1264
			$result,
1265
			'SSTemplateParser should only rewrite anchor hrefs'
1266
		);
1267
1268
		unlink($tmplFile);
1269
1270
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1271
	}
1272
1273
	public function testRewriteHashlinksInPhpMode() {
1274
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1275
		Config::inst()->update('SSViewer', 'rewrite_hash_links', 'php');
1276
1277
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1278
1279
		// Note: SSViewer_FromString doesn't rewrite hash links.
1280
		file_put_contents($tmplFile, '<!DOCTYPE html>
1281
			<html>
1282
				<head><% base_tag %></head>
1283
				<body>
1284
				<a class="inline" href="#anchor">InlineLink</a>
1285
				$InsertedLink
1286
				<svg><use xlink:href="#sprite"></use></svg>
1287
				<body>
1288
			</html>');
1289
		$tmpl = new SSViewer($tmplFile);
1290
		$obj = new ViewableData();
1291
		$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...
1292
		$result = $tmpl->process($obj);
1293
1294
		$code = <<<'EOC'
1295
<a class="inserted" href="<?php echo Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1296
EOC;
1297
		$this->assertContains($code, $result);
1298
		// TODO Fix inline links in PHP mode
1299
		// $this->assertContains(
1300
		// 	'<a class="inline" href="<?php echo str_replace(',
1301
		// 	$result
1302
		// );
1303
		$this->assertContains(
1304
			'<svg><use xlink:href="#sprite"></use></svg>',
1305
			$result,
1306
			'SSTemplateParser should only rewrite anchor hrefs'
1307
		);
1308
1309
		unlink($tmplFile);
1310
1311
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1312
	}
1313
1314
	public function testRenderWithSourceFileComments() {
1315
		$origEnv = Config::inst()->get('Director', 'environment_type');
1316
		Config::inst()->update('Director', 'environment_type', 'dev');
1317
		Config::inst()->update('SSViewer', 'source_file_comments', true);
1318
		$f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments';
1319
		$templates = array(
1320
			array(
1321
				'name' => 'SSViewerTestCommentsFullSource',
1322
				'expected' => ""
1323
					. "<!doctype html>"
1324
					. "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1325
					. "<html>"
1326
					. "\t<head></head>"
1327
					. "\t<body></body>"
1328
					. "</html>"
1329
					. "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1330
			),
1331
			array(
1332
				'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1333
				'expected' => ""
1334
					. "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1335
					. "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1336
					. "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1337
					. "<html>"
1338
					. "\t<head></head>"
1339
					. "\t<body></body>"
1340
					. "</html>"
1341
					. "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1342
			),
1343
			array(
1344
				'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1345
				'expected' => ""
1346
					. "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1347
					. "\t<head></head>"
1348
					. "\t<body></body>"
1349
					. "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1350
			),
1351
			array(
1352
				'name' => 'SSViewerTestCommentsFullSourceIfIE',
1353
				'expected' => ""
1354
					. "<!doctype html>"
1355
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1356
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1357
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1358
					. "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1359
					. "\t<head></head>"
1360
					. "\t<body></body>"
1361
					. "</html>"
1362
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1363
			),
1364
			array(
1365
				'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1366
				'expected' => ""
1367
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1368
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1369
					. "<!--[if !IE]><!--> <html class='no-ie'>"
1370
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1371
					. " <!--<![endif]-->"
1372
					. "\t<head></head>"
1373
					. "\t<body></body>"
1374
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1375
			),
1376
			array(
1377
				'name' => 'SSViewerTestCommentsPartialSource',
1378
				'expected' =>
1379
				"<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1380
					. "<div class='typography'></div>"
1381
					. "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1382
			),
1383
			array(
1384
				'name' => 'SSViewerTestCommentsWithInclude',
1385
				'expected' =>
1386
				"<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1387
					. "<div class='typography'>"
1388
					. "<!-- include 'SSViewerTestCommentsInclude' -->"
1389
					. "<!-- template $f/SSViewerTestCommentsInclude.ss -->"
1390
					. "Included"
1391
					. "<!-- end template $f/SSViewerTestCommentsInclude.ss -->"
1392
					. "<!-- end include 'SSViewerTestCommentsInclude' -->"
1393
					. "</div>"
1394
					. "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1395
			),
1396
		);
1397
		foreach ($templates as $template) {
1398
			$this->_renderWithSourceFileComments($template['name'], $template['expected']);
1399
		}
1400
		Config::inst()->update('SSViewer', 'source_file_comments', false);
1401
		Config::inst()->update('Director', 'environment_type', $origEnv);
1402
	}
1403
	private function _renderWithSourceFileComments($name, $expected) {
1404
		$viewer = new SSViewer(array($name));
1405
		$data = new ArrayData(array());
1406
		$result = $viewer->process($data);
1407
		$expected = str_replace(array("\r", "\n"), '', $expected);
1408
		$result = str_replace(array("\r", "\n"), '', $result);
1409
		$this->assertEquals($result, $expected);
1410
	}
1411
1412
	public function testLoopIteratorIterator() {
1413
		$list = new PaginatedList(new ArrayList());
1414
		$viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1415
		$result = $viewer->process(new ArrayData(array('List' => $list)));
1416
		$this->assertEquals($result, '');
1417
	}
1418
1419
	public function testProcessOnlyIncludesRequirementsOnce() {
1420
		$template = new SSViewer(array('SSViewerTestProcess'));
1421
		$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
1422
1423
		$backend = Injector::inst()->create('Requirements_Backend');
1424
		$backend->setCombinedFilesEnabled(false);
1425
		$backend->combineFiles(
1426
			'RequirementsTest_ab.css',
1427
			array(
1428
				$basePath . '/RequirementsTest_a.css',
1429
				$basePath . '/RequirementsTest_b.css'
1430
			)
1431
		);
1432
1433
		Requirements::set_backend($backend);
1434
1435
		$this->assertEquals(1, substr_count($template->process(array()), "a.css"));
1436
		$this->assertEquals(1, substr_count($template->process(array()), "b.css"));
1437
1438
		// if we disable the requirements then we should get nothing
1439
		$template->includeRequirements(false);
1440
		$this->assertEquals(0, substr_count($template->process(array()), "a.css"));
1441
		$this->assertEquals(0, substr_count($template->process(array()), "b.css"));
1442
	}
1443
1444
	public function testRequireCallInTemplateInclude() {
1445
		//TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1446
		if(FRAMEWORK_DIR === 'framework') {
1447
			$template = new SSViewer(array('SSViewerTestProcess'));
1448
1449
			Requirements::set_suffix_requirements(false);
1450
1451
			$this->assertEquals(1, substr_count(
1452
				$template->process(array()),
1453
				"tests/forms/RequirementsTest_a.js"
1454
			));
1455
		}
1456
		else {
1457
			$this->markTestSkipped('Requirement will always fail if the framework dir is not '.
1458
				'named \'framework\', since templates require hard coded paths');
1459
		}
1460
	}
1461
1462
	public function testCallsWithArguments() {
1463
		$data = new ArrayData(array(
1464
			'Set' => new ArrayList(array(
1465
				new SSViewerTest_Object("1"),
1466
				new SSViewerTest_Object("2"),
1467
				new SSViewerTest_Object("3"),
1468
				new SSViewerTest_Object("4"),
1469
				new SSViewerTest_Object("5"),
1470
			)),
1471
			'Level' => new SSViewerTest_LevelTest(1),
1472
			'Nest' => array(
1473
				'Level' => new SSViewerTest_LevelTest(2),
1474
			),
1475
		));
1476
1477
		$tests = array(
1478
			'$Level.output(1)' => '1-1',
1479
			'$Nest.Level.output($Set.First.Number)' => '2-1',
1480
			'<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1481
			'<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1482
			'<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1483
			'<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1484
			'<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1485
			'<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1486
			'<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1487
			'<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1488
			'<% with $Nest %>
1489
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1490
			<% end_with %>' => '1-hi',
1491
			'<% with $Nest %>
1492
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1493
			<% end_with %>' => '!0!1!2!3!4',
1494
		);
1495
1496
		foreach($tests as $template => $expected) {
1497
			$this->assertEquals($expected, trim($this->render($template, $data)));
1498
		}
1499
	}
1500
1501
	public function testClosedBlockExtension() {
1502
		$count = 0;
1503
		$parser = new SSTemplateParser();
1504
		$parser->addClosedBlock(
1505
			'test',
1506
			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...
1507
				$count++;
1508
			}
1509
		);
1510
1511
		$template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
1512
		$template->process(new SSViewerTestFixture());
1513
1514
		$this->assertEquals(1, $count);
1515
	}
1516
1517
	public function testOpenBlockExtension() {
1518
		$count = 0;
1519
		$parser = new SSTemplateParser();
1520
		$parser->addOpenBlock(
1521
			'test',
1522
			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...
1523
				$count++;
1524
			}
1525
		);
1526
1527
		$template = new SSViewer_FromString("<% test %>", $parser);
1528
		$template->process(new SSViewerTestFixture());
1529
1530
		$this->assertEquals(1, $count);
1531
	}
1532
1533
	/**
1534
	 * Tests if caching for SSViewer_FromString is working
1535
	 */
1536
	public function testFromStringCaching() {
1537
		$content = 'Test content';
1538
		$cacheFile = TEMP_FOLDER . '/.cache.' . sha1($content);
1539
		if (file_exists($cacheFile)) {
1540
			unlink($cacheFile);
1541
		}
1542
1543
		// Test global behaviors
1544
		$this->render($content, null, null);
1545
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1546
1547
		Config::inst()->update('SSViewer_FromString', 'cache_template', true);
1548
		$this->render($content, null, null);
1549
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1550
		unlink($cacheFile);
1551
1552
		// Test instance behaviors
1553
		$this->render($content, null, false);
1554
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1555
1556
		$this->render($content, null, true);
1557
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1558
		unlink($cacheFile);
1559
	}
1560
}
1561
1562
/**
1563
 * A test fixture that will echo back the template item
1564
 */
1565
class SSViewerTestFixture extends ViewableData {
1566
	protected $name;
1567
1568
	public function __construct($name = null) {
1569
		$this->name = $name;
1570
		parent::__construct();
1571
	}
1572
1573
1574
	private function argedName($fieldName, $arguments) {
1575
		$childName = $this->name ? "$this->name.$fieldName" : $fieldName;
1576
		if($arguments) return $childName . '(' . implode(',', $arguments) . ')';
1577
		else return $childName;
1578
	}
1579
	public function obj($fieldName, $arguments=null, $forceReturnedObject=true, $cache=false, $cacheName=null) {
1580
		$childName = $this->argedName($fieldName, $arguments);
1581
1582
		// Special field name Loop### to create a list
1583
		if(preg_match('/^Loop([0-9]+)$/', $fieldName, $matches)) {
1584
			$output = new ArrayList();
1585
			for($i=0;$i<$matches[1];$i++) $output->push(new SSViewerTestFixture($childName));
1586
			return $output;
1587
1588
		} else if(preg_match('/NotSet/i', $fieldName)) {
1589
			return new ViewableData();
1590
1591
		} else {
1592
			return new SSViewerTestFixture($childName);
1593
		}
1594
	}
1595
1596
1597
	public function XML_val($fieldName, $arguments = null, $cache = false) {
1598
		if(preg_match('/NotSet/i', $fieldName)) {
1599
			return '';
1600
		} else if(preg_match('/Raw/i', $fieldName)) {
1601
			return $fieldName;
1602
		} else {
1603
			return '[out:' . $this->argedName($fieldName, $arguments) . ']';
1604
		}
1605
	}
1606
1607
	public function hasValue($fieldName, $arguments = null, $cache = true) {
1608
		return (bool)$this->XML_val($fieldName, $arguments);
1609
	}
1610
}
1611
1612
class SSViewerTest_ViewableData extends ViewableData implements TestOnly {
1613
1614
	private static $casting = array(
1615
		'TextValue' => 'Text',
1616
		'HTMLValue' => 'HTMLText'
1617
	);
1618
1619
	public function methodWithOneArgument($arg1) {
1620
		return "arg1:{$arg1}";
1621
	}
1622
1623
	public function methodWithTwoArguments($arg1, $arg2) {
1624
		return "arg1:{$arg1},arg2:{$arg2}";
1625
	}
1626
}
1627
1628
class SSViewerTest_Object extends DataObject implements TestOnly {
1629
1630
	public $number = null;
1631
1632
	private static $casting = array(
1633
		'Link' => 'Text',
1634
	);
1635
1636
1637
	public function __construct($number = null) {
1638
		parent::__construct();
1639
		$this->number = $number;
1640
	}
1641
1642
	public function Number() {
1643
		return $this->number;
1644
	}
1645
1646
	public function absoluteBaseURL() {
1647
		return "testLocalFunctionPriorityCalled";
1648
	}
1649
1650
	public function lotsOfArguments11($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
1651
		return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
1652
	}
1653
1654
	public function Link() {
1655
		return 'some/url.html';
1656
	}
1657
}
1658
1659
class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly {
1660
1661
	public static function get_template_global_variables() {
1662
		return array(
1663
			'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html', 'casting' => 'HTMLText'),
1664
			'SSViewerTest_GlobalHTMLEscaped' => array('method' => 'get_html'),
1665
1666
			'SSViewerTest_GlobalAutomatic',
1667
			'SSViewerTest_GlobalReferencedByString' => 'get_reference',
1668
			'SSViewerTest_GlobalReferencedInArray' => array('method' => 'get_reference'),
1669
1670
			'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix', 'casting' => 'HTMLText')
1671
1672
		);
1673
	}
1674
1675
	public static function get_html() {
1676
		return '<div></div>';
1677
	}
1678
1679
	public static function SSViewerTest_GlobalAutomatic() {
1680
		return 'automatic';
1681
	}
1682
1683
	public static function get_reference() {
1684
		return 'reference';
1685
	}
1686
1687
	public static function get_argmix() {
1688
		$args = func_get_args();
1689
		return 'z' . implode(':', $args) . 'z';
1690
	}
1691
1692
}
1693
1694
class SSViewerTest_LevelTest extends ViewableData implements TestOnly {
1695
	protected $depth;
1696
1697
	public function __construct($depth = 1) {
1698
		$this->depth = $depth;
1699
	}
1700
1701
	public function output($val) {
1702
		return "$this->depth-$val";
1703
	}
1704
1705
	public function forLoop($number) {
1706
		$ret = array();
1707
		for($i = 0; $i < (int)$number; ++$i) {
1708
			$ret[] = new SSViewerTest_Object("!$i");
1709
		}
1710
		return new ArrayList($ret);
1711
	}
1712
1713
	public function forWith($number) {
1714
		return new self($number);
1715
	}
1716
}
1717