Completed
Push — master ( 45736e...f2ac6e )
by Daniel
24:18 queued 12:07
created

SSViewerTest::tearDown()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
use SilverStripe\Control\Director;
4
use SilverStripe\Control\ContentNegotiator;
5
use SilverStripe\Control\HTTPResponse;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, HTTPResponse.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
6
use SilverStripe\Core\Convert;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Dev\SapphireTest;
9
use SilverStripe\Dev\TestOnly;
10
use SilverStripe\i18n\i18n;
11
use SilverStripe\ORM\FieldType\DBField;
12
use SilverStripe\ORM\ArrayList;
13
use SilverStripe\ORM\DataObject;
14
use SilverStripe\ORM\PaginatedList;
15
use SilverStripe\Security\Member;
16
use SilverStripe\Security\SecurityToken;
17
use SilverStripe\Security\Permission;
18
use SilverStripe\View\ArrayData;
19
use SilverStripe\View\SSViewer;
20
use SilverStripe\View\Requirements;
21
use SilverStripe\View\ViewableData;
22
use SilverStripe\View\SSViewer_FromString;
23
use SilverStripe\View\SSTemplateParser;
24
use SilverStripe\View\TemplateGlobalProvider;
25
26
class SSViewerTest extends SapphireTest {
27
28
	/**
29
	 * Backup of $_SERVER global
30
	 *
31
	 * @var array
32
	 */
33
	protected $oldServer = array();
34
35
	protected $extraDataObjects = array(
36
		'SSViewerTest_Object',
37
	);
38
39
	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...
40
		parent::setUp();
41
		SSViewer::config()->update('source_file_comments', false);
42
		SSViewer_FromString::config()->update('cache_template', false);
43
		AssetStoreTest_SpyStore::activate('SSViewerTest');
44
		$this->oldServer = $_SERVER;
45
	}
46
47
	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...
48
		$_SERVER = $this->oldServer;
49
		AssetStoreTest_SpyStore::reset();
50
		parent::tearDown();
51
	}
52
53
	/**
54
	 * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
55
	 * of user defined themes via {@link SiteConfig} and default theme
56
	 * when no user themes are defined.
57
	 */
58
	public function testCurrentTheme() {
59
		//TODO: SiteConfig moved to CMS
60
		SSViewer::config()->update('theme', 'mytheme');
61
		$this->assertEquals(
62
			'mytheme',
63
			SSViewer::config()->get('theme'),
64
			'Current theme is the default - user has not defined one'
65
		);
66
	}
67
68
	/**
69
	 * Test that a template without a <head> tag still renders.
70
	 */
71
	public function testTemplateWithoutHeadRenders() {
72
		$data = new ArrayData(array(
73
			'Var' => 'var value'
74
		));
75
76
		$result = $data->renderWith("SSViewerTestPartialTemplate");
77
		$this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U",'',$result)));
78
	}
79
80
	public function testIncludeScopeInheritance() {
81
		$data = $this->getScopeInheritanceTestData();
82
		$expected = array(
83
			'Item 1 - First-ODD top:Item 1',
84
			'Item 2 - EVEN top:Item 2',
85
			'Item 3 - ODD top:Item 3',
86
			'Item 4 - EVEN top:Item 4',
87
			'Item 5 - ODD top:Item 5',
88
			'Item 6 - Last-EVEN top:Item 6',
89
		);
90
91
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
92
		$this->assertExpectedStrings($result, $expected);
93
94
		// reset results for the tests that include arguments (the title is passed as an arg)
95
		$expected = array(
96
			'Item 1 _ Item 1 - First-ODD top:Item 1',
97
			'Item 2 _ Item 2 - EVEN top:Item 2',
98
			'Item 3 _ Item 3 - ODD top:Item 3',
99
			'Item 4 _ Item 4 - EVEN top:Item 4',
100
			'Item 5 _ Item 5 - ODD top:Item 5',
101
			'Item 6 _ Item 6 - Last-EVEN top:Item 6',
102
		);
103
104
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
105
		$this->assertExpectedStrings($result, $expected);
106
	}
107
108
	public function testIncludeTruthyness() {
109
		$data = new ArrayData(array(
110
			'Title' => 'TruthyTest',
111
			'Items' => new ArrayList(array(
112
				new ArrayData(array('Title' => 'Item 1')),
113
				new ArrayData(array('Title' => '')),
114
				new ArrayData(array('Title' => true)),
115
				new ArrayData(array('Title' => false)),
116
				new ArrayData(array('Title' => null)),
117
				new ArrayData(array('Title' => 0)),
118
				new ArrayData(array('Title' => 7))
119
			))
120
		));
121
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
122
123
		// We should not end up with empty values appearing as empty
124
		$expected = array(
125
			'Item 1 _ Item 1 - First-ODD top:Item 1',
126
			'Untitled - EVEN top:',
127
			'1 _ 1 - ODD top:1',
128
			'Untitled - EVEN top:',
129
			'Untitled - ODD top:',
130
			'Untitled - EVEN top:0',
131
			'7 _ 7 - Last-ODD top:7'
132
		);
133
		$this->assertExpectedStrings($result, $expected);
134
	}
135
136
	private function getScopeInheritanceTestData() {
137
		return new ArrayData(array(
138
			'Title' => 'TopTitleValue',
139
			'Items' => new ArrayList(array(
140
				new ArrayData(array('Title' => 'Item 1')),
141
				new ArrayData(array('Title' => 'Item 2')),
142
				new ArrayData(array('Title' => 'Item 3')),
143
				new ArrayData(array('Title' => 'Item 4')),
144
				new ArrayData(array('Title' => 'Item 5')),
145
				new ArrayData(array('Title' => 'Item 6'))
146
			))
147
		));
148
	}
149
150
	private function assertExpectedStrings($result, $expected) {
151
		foreach ($expected as $expectedStr) {
152
			$this->assertTrue(
153
				(boolean) preg_match("/{$expectedStr}/", $result),
154
				"Didn't find '{$expectedStr}' in:\n{$result}"
155
			);
156
		}
157
	}
158
159
	/**
160
	 * Small helper to render templates from strings
161
	 */
162
	public function render($templateString, $data = null, $cacheTemplate = false) {
163
		$t = SSViewer::fromString($templateString, $cacheTemplate);
164
		if(!$data) $data = new SSViewerTestFixture();
165
		return trim(''.$t->process($data));
166
	}
167
168
	public function testRequirements() {
169
		$requirements = $this->getMock("SilverStripe\\View\\Requirements_Backend", array("javascript", "css"));
170
		$jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
171
		$cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
172
173
		$requirements->expects($this->once())->method('javascript')->with($jsFile);
174
		$requirements->expects($this->once())->method('css')->with($cssFile);
175
176
		$origReq = Requirements::backend();
177
		Requirements::set_backend($requirements);
178
		$template = $this->render("<% require javascript($jsFile) %>
179
		<% require css($cssFile) %>");
180
		Requirements::set_backend($origReq);
181
182
		$this->assertFalse((bool)trim($template), "Should be no content in this return.");
183
	}
184
185
	public function testRequirementsCombine(){
186
		$testBackend = Injector::inst()->create('SilverStripe\\View\\Requirements_Backend');
187
		$testBackend->setSuffixRequirements(false);
188
		//$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...
189
190
		$jsFile = FRAMEWORK_DIR . '/tests/view/themes/javascript/bad.js';
191
		$jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
192
		$testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
193
194
		// first make sure that our test js file causes an exception to be thrown
195
		try{
196
			require_once('thirdparty/jsmin/jsmin.php');
197
			JSMin::minify($jsFileContents);
198
			$this->fail('JSMin did not throw exception on minify bad file: ');
199
		} catch(Exception $e) {
200
			// exception thrown... good
201
		}
202
203
		// secondly, make sure that requirements is generated, even though minification failed
204
		$testBackend->processCombinedFiles();
205
		$js = array_keys($testBackend->getJavascript());
206
		$combinedTestFilePath = BASE_PATH . reset($js);
207
		$this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
208
209
		// and make sure the combined content matches the input content, i.e. no loss of functionality
210
		if(!file_exists($combinedTestFilePath)) {
211
			$this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
212
		}
213
		$combinedTestFileContents = file_get_contents($combinedTestFilePath);
214
		$this->assertContains($jsFileContents, $combinedTestFileContents);
215
	}
216
217
218
219
	public function testComments() {
220
		$output = $this->render(<<<SS
221
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
222
<%-- Alone multi
223
	line comment --%>
224
Some more content
225
Mixing content and <%-- multi
226
	line comment --%> Final final
227
content
228
SS
229
);
230
		$shouldbe = <<<SS
231
This is my templateThis is some contentFinal content
232
233
Some more content
234
Mixing content and  Final final
235
content
236
SS;
237
238
		$this->assertEquals($shouldbe, $output);
239
	}
240
241
	public function testBasicText() {
242
		$this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
243
		$this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
244
		$this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
245
		$this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
246
	}
247
248
	public function testBasicInjection() {
249
		$this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
250
		$this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
251
		$this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
252
		$this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
253
254
		$this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
255
		$this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
256
		$this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
257
258
		$this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
259
		$this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'),
260
			'Escapes before injections are correctly unescaped');
261
	}
262
263
264
	public function testGlobalVariableCalls() {
265
		$this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
266
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
267
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
268
	}
269
270
	public function testGlobalVariableCallsWithArguments() {
271
		$this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
272
		$this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
273
		$this->assertEquals('zFoo:Bar:Bazz',
274
			$this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")'));
275
		$this->assertEquals('zreferencez',
276
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)'));
277
	}
278
279
	public function testGlobalVariablesAreEscaped() {
280
		$this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
281
		$this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
282
283
		$this->assertEquals('z<div></div>z',
284
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)'));
285
		$this->assertEquals('z&lt;div&gt;&lt;/div&gt;z',
286
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)'));
287
	}
288
289
	public function testCoreGlobalVariableCalls() {
290
		$this->assertEquals(Director::absoluteBaseURL(),
291
			$this->render('{$absoluteBaseURL}'), 'Director::absoluteBaseURL can be called from within template');
292
		$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$AbsoluteBaseURL}'),
293
			'Upper-case %AbsoluteBaseURL can be called from within template');
294
295
		$this->assertEquals(Director::is_ajax(), $this->render('{$isAjax}'),
296
			'All variations of is_ajax result in the correct call');
297
		$this->assertEquals(Director::is_ajax(), $this->render('{$IsAjax}'),
298
			'All variations of is_ajax result in the correct call');
299
		$this->assertEquals(Director::is_ajax(), $this->render('{$is_ajax}'),
300
			'All variations of is_ajax result in the correct call');
301
		$this->assertEquals(Director::is_ajax(), $this->render('{$Is_ajax}'),
302
			'All variations of is_ajax result in the correct call');
303
304
		$this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'),
305
			'i18n template functions result correct result');
306
		$this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'),
307
			'i18n template functions result correct result');
308
309
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'),
310
			'Member template functions result correct result');
311
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'),
312
			'Member template functions result correct result');
313
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'),
314
			'Member template functions result correct result');
315
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentUser}'),
316
			'Member template functions result correct result');
317
318
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'),
319
			'SecurityToken template functions result correct result');
320
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'),
321
			'SecurityToken template functions result correct result');
322
323
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
324
			'Permissions template functions result correct result');
325
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
326
			'Permissions template functions result correct result');
327
	}
328
329
	public function testNonFieldCastingHelpersNotUsedInHasValue() {
330
		// check if Link without $ in front of variable
331
		$result = $this->render(
332
			'A<% if Link %>$Link<% end_if %>B', new SSViewerTest_Object());
333
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
334
335
		// check if Link with $ in front of variable
336
		$result = $this->render(
337
			'A<% if $Link %>$Link<% end_if %>B', new SSViewerTest_Object());
338
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
339
	}
340
341
	public function testLocalFunctionsTakePriorityOverGlobals() {
342
		$data = new ArrayData(array(
343
			'Page' => new SSViewerTest_Object()
344
		));
345
346
		//call method with lots of arguments
347
		$result = $this->render(
348
			'<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',$data);
349
		$this->assertEquals("abcdefghijk",$result, "public function can accept up to 11 arguments");
350
351
		//call method that does not exist
352
		$result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>',$data);
353
		$this->assertEquals("",$result, "Method does not exist - empty result");
354
355
		//call if that does not exist
356
		$result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>',$data);
357
		$this->assertEquals("",$result, "Method does not exist - empty result");
358
359
		//call method with same name as a global method (local call should take priority)
360
		$result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>',$data);
361
		$this->assertEquals("testLocalFunctionPriorityCalled",$result,
362
			"Local Object's public function called. Did not return the actual baseURL of the current site");
363
	}
364
365
	public function testCurrentScopeLoopWith() {
366
		// Data to run the loop tests on - one sequence of three items, each with a subitem
367
		$data = new ArrayData(array(
368
			'Foo' => new ArrayList(array(
369
				'Subocean' => new ArrayData(array(
370
						'Name' => 'Higher'
371
					)),
372
				new ArrayData(array(
373
					'Sub' => new ArrayData(array(
374
						'Name' => 'SubKid1'
375
					))
376
				)),
377
				new ArrayData(array(
378
					'Sub' => new ArrayData(array(
379
						'Name' => 'SubKid2'
380
					))
381
				)),
382
				new SSViewerTest_Object('Number6')
383
			))
384
		));
385
386
		$result = $this->render(
387
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
388
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
389
390
		$result = $this->render(
391
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
392
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
393
394
		$result = $this->render('<% with Foo %>$Count<% end_with %>',$data);
395
		$this->assertEquals("4",$result, "4 items in the DataObjectSet");
396
397
		$result = $this->render('<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
398
			. '<% end_if %><% end_loop %><% end_with %>',$data);
399
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in with Up.Foo scope works");
400
401
		$result = $this->render('<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
402
			. '<% end_if %><% end_loop %><% end_with %>',$data);
403
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in current scope works");
404
	}
405
406
	public function testObjectDotArguments() {
407
		$this->assertEquals(
408
			'[out:TestObject.methodWithOneArgument(one)]
409
				[out:TestObject.methodWithTwoArguments(one,two)]
410
				[out:TestMethod(Arg1,Arg2).Bar.Val]
411
				[out:TestMethod(Arg1,Arg2).Bar]
412
				[out:TestMethod(Arg1,Arg2)]
413
				[out:TestMethod(Arg1).Bar.Val]
414
				[out:TestMethod(Arg1).Bar]
415
				[out:TestMethod(Arg1)]',
416
			$this->render('$TestObject.methodWithOneArgument(one)
417
				$TestObject.methodWithTwoArguments(one,two)
418
				$TestMethod(Arg1, Arg2).Bar.Val
419
				$TestMethod(Arg1, Arg2).Bar
420
				$TestMethod(Arg1, Arg2)
421
				$TestMethod(Arg1).Bar.Val
422
				$TestMethod(Arg1).Bar
423
				$TestMethod(Arg1)')
424
		);
425
	}
426
427
	public function testEscapedArguments() {
428
		$this->assertEquals(
429
			'[out:Foo(Arg1,Arg2).Bar.Val].Suffix
430
				[out:Foo(Arg1,Arg2).Val]_Suffix
431
				[out:Foo(Arg1,Arg2)]/Suffix
432
				[out:Foo(Arg1).Bar.Val]textSuffix
433
				[out:Foo(Arg1).Bar].Suffix
434
				[out:Foo(Arg1)].Suffix
435
				[out:Foo.Bar.Val].Suffix
436
				[out:Foo.Bar].Suffix
437
				[out:Foo].Suffix',
438
			$this->render('{$Foo(Arg1, Arg2).Bar.Val}.Suffix
439
				{$Foo(Arg1, Arg2).Val}_Suffix
440
				{$Foo(Arg1, Arg2)}/Suffix
441
				{$Foo(Arg1).Bar.Val}textSuffix
442
				{$Foo(Arg1).Bar}.Suffix
443
				{$Foo(Arg1)}.Suffix
444
				{$Foo.Bar.Val}.Suffix
445
				{$Foo.Bar}.Suffix
446
				{$Foo}.Suffix')
447
		);
448
	}
449
450
	public function testLoopWhitespace() {
451
		$this->assertEquals(
452
			'before[out:SingleItem.Test]after
453
				beforeTestafter',
454
			$this->render('before<% loop SingleItem %>$Test<% end_loop %>after
455
				before<% loop SingleItem %>Test<% end_loop %>after')
456
		);
457
458
		// The control tags are removed from the output, but no whitespace
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:SingleItem.ItemOnItsOwnLine]
465
466
after',
467
			$this->render('before
468
<% loop SingleItem %>
469
$ItemOnItsOwnLine
470
<% end_loop %>
471
after')
472
		);
473
474
		// The whitespace within the control tags is preserve in a loop
475
		// This is a quirk that could be changed, but included in the test to make the current
476
		// behaviour explicit
477
		$this->assertEquals(
478
			'before
479
480
[out:Loop3.ItemOnItsOwnLine]
481
482
[out:Loop3.ItemOnItsOwnLine]
483
484
[out:Loop3.ItemOnItsOwnLine]
485
486
after',
487
			$this->render('before
488
<% loop Loop3 %>
489
$ItemOnItsOwnLine
490
<% end_loop %>
491
after')
492
		);
493
	}
494
495
	public function testControls() {
496
		// Single item controls
497
		$this->assertEquals(
498
			'a[out:Foo.Bar.Item]b
499
				[out:Foo.Bar(Arg1).Item]
500
				[out:Foo(Arg1).Item]
501
				[out:Foo(Arg1,Arg2).Item]
502
				[out:Foo(Arg1,Arg2,Arg3).Item]',
503
			$this->render('<% with Foo.Bar %>a{$Item}b<% end_with %>
504
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
505
				<% with Foo(Arg1) %>$Item<% end_with %>
506
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
507
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>')
508
		);
509
510
		// Loop controls
511
		$this->assertEquals('a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
512
			$this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>'));
513
514
		$this->assertEquals('[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
515
			$this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>'));
516
517
		$this->assertEquals('[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
518
			$this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>'));
519
520
		$this->assertEquals('[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
521
			$this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>'));
522
523
		$this->assertEquals('[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
524
			$this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>'));
525
526
	}
527
528
	public function testIfBlocks() {
529
		// Basic test
530
		$this->assertEquals('AC',
531
			$this->render('A<% if NotSet %>B$NotSet<% end_if %>C'));
532
533
		// Nested test
534
		$this->assertEquals('AB1C',
535
			$this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C'));
536
537
		// else_if
538
		$this->assertEquals('ACD',
539
			$this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
540
		$this->assertEquals('AD',
541
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D'));
542
		$this->assertEquals('ADE',
543
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
544
545
		$this->assertEquals('ADE',
546
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
547
548
		// Dot syntax
549
		$this->assertEquals('ACD',
550
			$this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D'));
551
		$this->assertEquals('ACD',
552
			$this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D'));
553
554
		// Params
555
		$this->assertEquals('ACD',
556
			$this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D'));
557
		$this->assertEquals('ABD',
558
			$this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D'));
559
560
		// Negation
561
		$this->assertEquals('AC',
562
			$this->render('A<% if not IsSet %>B<% end_if %>C'));
563
		$this->assertEquals('ABC',
564
			$this->render('A<% if not NotSet %>B<% end_if %>C'));
565
566
		// Or
567
		$this->assertEquals('ABD',
568
			$this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D'));
569
		$this->assertEquals('ACD',
570
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D'));
571
		$this->assertEquals('AD',
572
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D'));
573
		$this->assertEquals('ACD',
574
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D'));
575
		$this->assertEquals('AD',
576
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D'));
577
578
		// Negated Or
579
		$this->assertEquals('ACD',
580
			$this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
581
		$this->assertEquals('ABD',
582
			$this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
583
		$this->assertEquals('ABD',
584
			$this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
585
586
		// And
587
		$this->assertEquals('ABD',
588
			$this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D'));
589
		$this->assertEquals('ACD',
590
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
591
		$this->assertEquals('AD',
592
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D'));
593
		$this->assertEquals('ACD',
594
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D'));
595
		$this->assertEquals('AD',
596
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D'));
597
598
		// Equality
599
		$this->assertEquals('ABC',
600
			$this->render('A<% if RawVal == RawVal %>B<% end_if %>C'));
601
		$this->assertEquals('ACD',
602
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D'));
603
		$this->assertEquals('ABC',
604
			$this->render('A<% if Right != Wrong %>B<% end_if %>C'));
605
		$this->assertEquals('AD',
606
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D'));
607
608
		// test inequalities with simple numbers
609
		$this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
610
		$this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
611
		$this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
612
		$this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
613
614
		$this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
615
		$this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
616
		$this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
617
		$this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
618
619
		$this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
620
		$this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
621
		$this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
622
		$this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
623
624
		// empty else_if and else tags, if this would not be supported,
625
		// the output would stop after A, thereby failing the assert
626
		$this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
627
		$this->assertEquals('AD',
628
			$this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D'));
629
		$this->assertEquals('AD',
630
			$this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D'));
631
632
		// Bare words with ending space
633
		$this->assertEquals('ABC',
634
			$this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C'));
635
636
		// Else
637
		$this->assertEquals('ADE',
638
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E'));
639
640
		// 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...
641
		$this->assertEquals('ABC',
642
			$this->render('A<% if NotSet %><% else %>B<% end_if %>C'));
643
	}
644
645
	public function testBaseTagGeneration() {
646
		// XHTML wil have a closed base tag
647
		$tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
648
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
649
				. ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
650
			<html>
651
				<head><% base_tag %></head>
652
				<body><p>test</p><body>
653
			</html>';
654
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
655
656
		// HTML4 and 5 will only have it for IE
657
		$tmpl2 = '<!DOCTYPE html>
658
			<html>
659
				<head><% base_tag %></head>
660
				<body><p>test</p><body>
661
			</html>';
662
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
663
			$this->render($tmpl2));
664
665
666
		$tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
667
			<html>
668
				<head><% base_tag %></head>
669
				<body><p>test</p><body>
670
			</html>';
671
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
672
			$this->render($tmpl3));
673
674
		// Check that the content negotiator converts to the equally legal formats
675
		$negotiator = new ContentNegotiator();
676
677
		$response = new HTTPResponse($this->render($tmpl1));
678
		$negotiator->html($response);
679
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
680
			$response->getBody());
681
682
		$response = new HTTPResponse($this->render($tmpl1));
683
		$negotiator->xhtml($response);
684
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
685
	}
686
687
	public function testIncludeWithArguments() {
688
		$this->assertEquals(
689
			$this->render('<% include SSViewerTestIncludeWithArguments %>'),
690
			'<p>[out:Arg1]</p><p>[out:Arg2]</p>'
691
		);
692
693
		$this->assertEquals(
694
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
695
			'<p>A</p><p>[out:Arg2]</p>'
696
		);
697
698
		$this->assertEquals(
699
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
700
			'<p>A</p><p>B</p>'
701
		);
702
703
		$this->assertEquals(
704
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
705
			'<p>A Bare String</p><p>B Bare String</p>'
706
		);
707
708
		$this->assertEquals(
709
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
710
				new ArrayData(array('B' => 'Bar'))),
711
			'<p>A</p><p>Bar</p>'
712
		);
713
714
		$this->assertEquals(
715
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
716
				new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))),
717
			'<p>A</p><p>Bar</p>'
718
		);
719
720
		$this->assertEquals(
721
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
722
				new ArrayData(array('Items' => new ArrayList(array(
723
					new ArrayData(array('Title' => 'Foo')),
724
					new ArrayData(array('Title' => 'Bar'))
725
				))))),
726
			'SomeArg - Foo - Bar - SomeArg'
727
		);
728
729
		$this->assertEquals(
730
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
731
				new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))),
732
			'A - B - A'
733
		);
734
735
		$this->assertEquals(
736
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
737
				new ArrayData(array(
738
					'Item' => new ArrayData(array(
739
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
740
					)))
741
				)),
742
			'A - B - C - B - A'
743
		);
744
745
		$this->assertEquals(
746
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
747
				new ArrayData(array(
748
					'Item' => new ArrayData(array(
749
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
750
					)))
751
				)),
752
			'A - A - A'
753
		);
754
755
		$data = new ArrayData(array(
756
			'Nested' => new ArrayData(array(
757
				'Object' => new ArrayData(array('Key' => 'A'))
758
			)),
759
			'Object' => new ArrayData(array('Key' => 'B'))
760
		));
761
762
		$tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
763
		$res  = $tmpl->process($data);
764
		$this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
765
	}
766
767
	public function testNamespaceInclude() {
768
		$data = new ArrayData([]);
769
770
		$this->assertEquals(
771
			"tests:( NamespaceInclude\n )",
772
			$this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
773
			'Backslashes work for namespace references in includes'
774
		);
775
776
		$this->assertEquals(
777
			"tests:( NamespaceInclude\n )",
778
			$this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
779
			'Forward slashes work for namespace references in includes'
780
		);
781
	}
782
783
784
	public function testRecursiveInclude() {
785
		$view = new SSViewer(array('Includes/SSViewerTestRecursiveInclude'));
786
787
		$data = new ArrayData(array(
788
			'Title' => 'A',
789
			'Children' => new ArrayList(array(
790
				new ArrayData(array(
791
					'Title' => 'A1',
792
					'Children' => new ArrayList(array(
793
						new ArrayData(array( 'Title' => 'A1 i', )),
794
						new ArrayData(array( 'Title' => 'A1 ii', )),
795
					)),
796
				)),
797
				new ArrayData(array( 'Title' => 'A2', )),
798
				new ArrayData(array( 'Title' => 'A3', )),
799
			)),
800
		));
801
802
		$result = $view->process($data);
803
		// We don't care about whitespace
804
		$rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
805
806
		$this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
807
	}
808
809
	public function assertEqualIgnoringWhitespace($a, $b, $message = '') {
810
		$this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b), $message);
811
	}
812
813
	/**
814
	 * See {@link ViewableDataTest} for more extensive casting tests,
815
	 * this test just ensures that basic casting is correctly applied during template parsing.
816
	 */
817
	public function testCastingHelpers() {
818
		$vd = new SSViewerTest_ViewableData();
819
		$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...
820
		$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...
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
823
		// Value casted as "Text"
824
		$this->assertEquals(
825
			'&lt;b&gt;html&lt;/b&gt;',
826
			$t = SSViewer::fromString('$TextValue')->process($vd)
827
		);
828
		$this->assertEquals(
829
			'<b>html</b>',
830
			$t = SSViewer::fromString('$TextValue.RAW')->process($vd)
831
		);
832
		$this->assertEquals(
833
			'&lt;b&gt;html&lt;/b&gt;',
834
			$t = SSViewer::fromString('$TextValue.XML')->process($vd)
835
		);
836
837
		// Value casted as "HTMLText"
838
		$this->assertEquals(
839
			'<b>html</b>',
840
			$t = SSViewer::fromString('$HTMLValue')->process($vd)
841
		);
842
		$this->assertEquals(
843
			'<b>html</b>',
844
			$t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
845
		);
846
		$this->assertEquals(
847
			'&lt;b&gt;html&lt;/b&gt;',
848
			$t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
849
		);
850
851
		// Uncasted value (falls back to ViewableData::$default_cast="Text")
852
		$vd = new SSViewerTest_ViewableData();
853
		$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...
854
		$this->assertEquals(
855
			'&lt;b&gt;html&lt;/b&gt;',
856
			$t = SSViewer::fromString('$UncastedValue')->process($vd)
857
		);
858
		$this->assertEquals(
859
			'<b>html</b>',
860
			$t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
861
		);
862
		$this->assertEquals(
863
			'&lt;b&gt;html&lt;/b&gt;',
864
			$t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
865
		);
866
	}
867
868
	public function testSSViewerBasicIteratorSupport() {
869
		$data = new ArrayData(array(
870
			'Set' => new ArrayList(array(
871
				new SSViewerTest_Object("1"),
872
				new SSViewerTest_Object("2"),
873
				new SSViewerTest_Object("3"),
874
				new SSViewerTest_Object("4"),
875
				new SSViewerTest_Object("5"),
876
				new SSViewerTest_Object("6"),
877
				new SSViewerTest_Object("7"),
878
				new SSViewerTest_Object("8"),
879
				new SSViewerTest_Object("9"),
880
				new SSViewerTest_Object("10"),
881
			))
882
		));
883
884
		//base test
885
		$result = $this->render('<% loop Set %>$Number<% end_loop %>',$data);
886
		$this->assertEquals("12345678910",$result,"Numbers rendered in order");
887
888
		//test First
889
		$result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>',$data);
890
		$this->assertEquals("1",$result,"Only the first number is rendered");
891
892
		//test Last
893
		$result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>',$data);
894
		$this->assertEquals("10",$result,"Only the last number is rendered");
895
896
		//test Even
897
		$result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>',$data);
898
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
899
900
		//test Even with quotes
901
		$result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>',$data);
902
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
903
904
		//test Even without quotes
905
		$result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>',$data);
906
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
907
908
		//test Even with zero-based start index
909
		$result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>',$data);
910
		$this->assertEquals("13579",$result,"Even (with zero-based index) numbers rendered in order");
911
912
		//test Odd
913
		$result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>',$data);
914
		$this->assertEquals("13579",$result,"Odd numbers rendered in order");
915
916
		//test FirstLast
917
		$result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>',$data);
918
		$this->assertEquals("1first10last",$result,"First and last numbers rendered in order");
919
920
		//test Middle
921
		$result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>',$data);
922
		$this->assertEquals("23456789",$result,"Middle numbers rendered in order");
923
924
		//test MiddleString
925
		$result = $this->render('<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
926
			. '<% end_loop %>',$data);
927
		$this->assertEquals("2middle3middle4middle5middle6middle7middle8middle9middle",$result,
928
			"Middle numbers rendered in order");
929
930
		//test EvenOdd
931
		$result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>',$data);
932
		$this->assertEquals("oddevenoddevenoddevenoddevenoddeven",$result,
933
			"Even and Odd is returned in sequence numbers rendered in order");
934
935
		//test Pos
936
		$result = $this->render('<% loop Set %>$Pos<% end_loop %>',$data);
937
		$this->assertEquals("12345678910", $result, '$Pos is rendered in order');
938
939
		//test Pos
940
		$result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>',$data);
941
		$this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
942
943
		//test FromEnd
944
		$result = $this->render('<% loop Set %>$FromEnd<% end_loop %>',$data);
945
		$this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
946
947
		//test FromEnd
948
		$result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>',$data);
949
		$this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
950
951
		//test Total
952
		$result = $this->render('<% loop Set %>$TotalItems<% end_loop %>',$data);
953
		$this->assertEquals("10101010101010101010",$result,"10 total items X 10 are returned");
954
955
		//test Modulus
956
		$result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>',$data);
957
		$this->assertEquals("1010101010",$result,"1-indexed pos modular divided by 2 rendered in order");
958
959
		//test MultipleOf 3
960
		$result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>',$data);
961
		$this->assertEquals("369",$result,"Only numbers that are multiples of 3 are returned");
962
963
		//test MultipleOf 4
964
		$result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>',$data);
965
		$this->assertEquals("48",$result,"Only numbers that are multiples of 4 are returned");
966
967
		//test MultipleOf 5
968
		$result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>',$data);
969
		$this->assertEquals("510",$result,"Only numbers that are multiples of 5 are returned");
970
971
		//test MultipleOf 10
972
		$result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>',$data);
973
		$this->assertEquals("10",$result,"Only numbers that are multiples of 10 (with 1-based indexing) are returned");
974
975
		//test MultipleOf 9 zero-based
976
		$result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>',$data);
977
		$this->assertEquals("110",$result,
978
			"Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)");
979
980
		//test MultipleOf 11
981
		$result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>',$data);
982
		$this->assertEquals("",$result,"Only numbers that are multiples of 11 are returned. I.e. nothing returned");
983
	}
984
985
	/**
986
	 * Test $Up works when the scope $Up refers to was entered with a "with" block
987
	 */
988
	public function testUpInWith() {
989
990
		// Data to run the loop tests on - three levels deep
991
		$data = new ArrayData(array(
992
			'Name' => 'Top',
993
			'Foo' => new ArrayData(array(
994
				'Name' => 'Foo',
995
				'Bar' => new ArrayData(array(
996
					'Name' => 'Bar',
997
					'Baz' => new ArrayData(array(
998
						'Name' => 'Baz'
999
					)),
1000
					'Qux' => new ArrayData(array(
1001
						'Name' => 'Qux'
1002
					))
1003
				))
1004
			))
1005
		));
1006
1007
		// Basic functionality
1008
		$this->assertEquals('BarFoo',
1009
			$this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data));
1010
1011
		// Two level with block, up refers to internally referenced Bar
1012
		$this->assertEquals('BarFoo',
1013
			$this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data));
1014
1015
		// Stepping up & back down the scope tree
1016
		$this->assertEquals('BazBarQux',
1017
			$this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data));
1018
1019
		// Using $Up in a with block
1020
		$this->assertEquals('BazBarQux',
1021
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
1022
				.'<% end_with %>', $data));
1023
1024
		// Stepping up & back down the scope tree with with blocks
1025
		$this->assertEquals('BazBarQuxBarBaz',
1026
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
1027
				. '{$Name}<% end_with %>{$Name}<% end_with %>', $data));
1028
1029
		// Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1030
		$this->assertEquals('Foo',
1031
			$this->render('<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
1032
				. '<% end_with %>', $data));
1033
1034
		// Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
1035
		$this->assertEquals('Foo',
1036
			$this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data));
1037
	}
1038
1039
	/**
1040
	 * Test $Up works when the scope $Up refers to was entered with a "loop" block
1041
	 */
1042
	public function testUpInLoop(){
1043
1044
		// Data to run the loop tests on - one sequence of three items, each with a subitem
1045
		$data = new ArrayData(array(
1046
			'Name' => 'Top',
1047
			'Foo' => new ArrayList(array(
1048
				new ArrayData(array(
1049
					'Name' => '1',
1050
					'Sub' => new ArrayData(array(
1051
						'Name' => 'Bar'
1052
					))
1053
				)),
1054
				new ArrayData(array(
1055
					'Name' => '2',
1056
					'Sub' => new ArrayData(array(
1057
						'Name' => 'Baz'
1058
					))
1059
				)),
1060
				new ArrayData(array(
1061
					'Name' => '3',
1062
					'Sub' => new ArrayData(array(
1063
						'Name' => 'Qux'
1064
					))
1065
				))
1066
			))
1067
		));
1068
1069
		// Make sure inside a loop, $Up refers to the current item of the loop
1070
		$this->assertEqualIgnoringWhitespace(
1071
			'111 222 333',
1072
			$this->render(
1073
				'<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1074
				$data
1075
			)
1076
		);
1077
1078
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1079
		// and doesn't interfere with the original iterator
1080
		$this->assertEqualIgnoringWhitespace(
1081
			'1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1082
			$this->render(
1083
				'<% loop $Foo %>
1084
					$Name
1085
					<% with $Sub %>
1086
						$Name
1087
						<% loop $Up %>$Name<% end_loop %>
1088
						$Name
1089
					<% end_with %>
1090
					$Name
1091
				<% end_loop %>',
1092
				$data
1093
			)
1094
		);
1095
1096
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1097
		// and doesn't interfere with the original iterator or local lookups
1098
		$this->assertEqualIgnoringWhitespace(
1099
			'1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1100
			$this->render(
1101
				'<% loop $Foo %>
1102
					$Name
1103
					<% with $Sub %>
1104
						{$Name}{$Up.Name}
1105
						<% loop $Up %>$Name<% end_loop %>
1106
						{$Up.Name}{$Name}
1107
					<% end_with %>
1108
					$Name
1109
				<% end_loop %>',
1110
				$data
1111
			)
1112
		);
1113
	}
1114
1115
	/**
1116
	 * Test that nested loops restore the loop variables correctly when pushing and popping states
1117
	 */
1118
	public function testNestedLoops(){
1119
1120
		// Data to run the loop tests on - one sequence of three items, one with child elements
1121
		// (of a different size to the main sequence)
1122
		$data = new ArrayData(array(
1123
			'Foo' => new ArrayList(array(
1124
				new ArrayData(array(
1125
					'Name' => '1',
1126
					'Children' => new ArrayList(array(
1127
						new ArrayData(array(
1128
							'Name' => 'a'
1129
						)),
1130
						new ArrayData(array(
1131
							'Name' => 'b'
1132
						)),
1133
					)),
1134
				)),
1135
				new ArrayData(array(
1136
					'Name' => '2',
1137
					'Children' => new ArrayList(),
1138
				)),
1139
				new ArrayData(array(
1140
					'Name' => '3',
1141
					'Children' => new ArrayList(),
1142
				)),
1143
			)),
1144
		));
1145
1146
		// Make sure that including a loop inside a loop will not destroy the internal count of
1147
		// items, checked by using "Last"
1148
		$this->assertEqualIgnoringWhitespace(
1149
			'1ab23last',
1150
			$this->render('<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1151
				. '<% end_loop %>', $data
1152
			)
1153
		);
1154
	}
1155
1156
	public function testLayout() {
1157
		$self = $this;
1158
1159
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1160
			$template = new SSViewer(array('Page'));
1161
			$self->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1162
1163
			$template = new SSViewer(array('Shortcodes', 'Page'));
1164
			$self->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1165
		});
1166
	}
1167
1168
	/**
1169
	 * @covers SilverStripe\View\SSViewer::get_templates_by_class()
1170
	 */
1171
	public function testGetTemplatesByClass() {
1172
		$self = $this;
1173
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1174
			// Test passing a string
1175
			$templates = SSViewer::get_templates_by_class(
1176
				'TestNamespace\\SSViewerTestModel_Controller',
1177
				'',
1178
				'SilverStripe\\Control\\Controller'
1179
			);
1180
			$self->assertEquals([
1181
				'TestNamespace\\SSViewerTestModel_Controller',
1182
				[
1183
					'type' => 'Includes',
1184
					'TestNamespace\\SSViewerTestModel_Controller',
1185
				],
1186
				'TestNamespace\\SSViewerTestModel',
1187
    			'SilverStripe\\Control\\Controller',
1188
				[
1189
					'type' => 'Includes',
1190
    				'SilverStripe\\Control\\Controller',
1191
				],
1192
			], $templates);
1193
1194
			// Test to ensure we're stopping at the base class.
1195
			$templates = SSViewer::get_templates_by_class(
1196
				'TestNamespace\SSViewerTestModel_Controller',
1197
				'',
1198
				'TestNamespace\SSViewerTestModel_Controller'
1199
			);
1200
			$self->assertEquals([
1201
				'TestNamespace\\SSViewerTestModel_Controller',
1202
				[
1203
					'type' => 'Includes',
1204
					'TestNamespace\\SSViewerTestModel_Controller',
1205
				],
1206
				'TestNamespace\\SSViewerTestModel',
1207
			], $templates);
1208
1209
			// Make sure we can search templates by suffix.
1210
			$templates = SSViewer::get_templates_by_class(
1211
				'TestNamespace\\SSViewerTestModel',
1212
				'_Controller',
1213
				'SilverStripe\\ORM\\DataObject'
1214
			);
1215
			$self->assertEquals([
1216
				'TestNamespace\\SSViewerTestModel_Controller',
1217
				[
1218
					'type' => 'Includes',
1219
					'TestNamespace\\SSViewerTestModel_Controller',
1220
				],
1221
				'SilverStripe\\ORM\\DataObject_Controller',
1222
				[
1223
					'type' => 'Includes',
1224
					'SilverStripe\\ORM\\DataObject_Controller',
1225
				],
1226
			], $templates);
1227
1228
			// Let's throw something random in there.
1229
			$self->setExpectedException('InvalidArgumentException');
1230
			SSViewer::get_templates_by_class(array());
1231
		});
1232
	}
1233
1234
	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...
1235
		SSViewer::config()->update('rewrite_hash_links', true);
1236
1237
		$_SERVER['HTTP_HOST'] = 'www.mysite.com';
1238
		$_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1239
1240
		// 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...
1241
		// Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1242
		// as protocol-less absolute urls
1243
		$base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1244
1245
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1246
1247
		// Note: SSViewer_FromString doesn't rewrite hash links.
1248
		file_put_contents($tmplFile, '<!DOCTYPE html>
1249
			<html>
1250
				<head><% base_tag %></head>
1251
				<body>
1252
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1253
				$ExternalInsertedLink
1254
				<a class="inline" href="#anchor">InlineLink</a>
1255
				$InsertedLink
1256
				<svg><use xlink:href="#sprite"></use></svg>
1257
				<body>
1258
			</html>');
1259
		$tmpl = new SSViewer($tmplFile);
1260
		$obj = new ViewableData();
1261
		$obj->InsertedLink = DBField::create_field(
0 ignored issues
show
Documentation introduced by
The property InsertedLink does not exist on object<SilverStripe\View\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...
1262
			'HTMLFragment',
1263
			'<a class="inserted" href="#anchor">InsertedLink</a>'
1264
		);
1265
		$obj->ExternalInsertedLink = DBField::create_field(
0 ignored issues
show
Documentation introduced by
The property ExternalInsertedLink does not exist on object<SilverStripe\View\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...
1266
			'HTMLFragment',
1267
			'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>'
1268
		);
1269
		$result = $tmpl->process($obj);
1270
		$this->assertContains(
1271
			'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1272
			$result
1273
		);
1274
		$this->assertContains(
1275
			'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1276
			$result
1277
		);
1278
		$this->assertContains(
1279
			'<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1280
			$result
1281
		);
1282
		$this->assertContains(
1283
			'<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1284
			$result
1285
		);
1286
		$this->assertContains(
1287
			'<svg><use xlink:href="#sprite"></use></svg>',
1288
			$result,
1289
			'SSTemplateParser should only rewrite anchor hrefs'
1290
		);
1291
1292
		unlink($tmplFile);
1293
	}
1294
1295
	public function testRewriteHashlinksInPhpMode() {
1296
		SSViewer::config()->update('rewrite_hash_links', 'php');
1297
1298
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1299
1300
		// Note: SSViewer_FromString doesn't rewrite hash links.
1301
		file_put_contents($tmplFile, '<!DOCTYPE html>
1302
			<html>
1303
				<head><% base_tag %></head>
1304
				<body>
1305
				<a class="inline" href="#anchor">InlineLink</a>
1306
				$InsertedLink
1307
				<svg><use xlink:href="#sprite"></use></svg>
1308
				<body>
1309
			</html>');
1310
		$tmpl = new SSViewer($tmplFile);
1311
		$obj = new ViewableData();
1312
		$obj->InsertedLink = DBField::create_field(
0 ignored issues
show
Documentation introduced by
The property InsertedLink does not exist on object<SilverStripe\View\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...
1313
			'HTMLFragment',
1314
			'<a class="inserted" href="#anchor">InsertedLink</a>'
1315
		);
1316
		$result = $tmpl->process($obj);
1317
1318
		$code = <<<'EOC'
1319
<a class="inserted" href="<?php echo \SilverStripe\Core\Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1320
EOC;
1321
		$this->assertContains($code, $result);
1322
		// TODO Fix inline links in PHP mode
1323
		// $this->assertContains(
1324
		// 	'<a class="inline" href="<?php echo str_replace(',
1325
		// 	$result
1326
		// );
1327
		$this->assertContains(
1328
			'<svg><use xlink:href="#sprite"></use></svg>',
1329
			$result,
1330
			'SSTemplateParser should only rewrite anchor hrefs'
1331
		);
1332
1333
		unlink($tmplFile);
1334
	}
1335
1336
	public function testRenderWithSourceFileComments() {
1337
		Director::config()->update('environment_type', 'dev');
1338
		SSViewer::config()->update('source_file_comments', true);
1339
		$i = FRAMEWORK_PATH . '/tests/templates/Includes';
1340
		$f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments';
1341
		$templates = array(
1342
			array(
1343
				'name' => 'SSViewerTestCommentsFullSource',
1344
				'expected' => ""
1345
					. "<!doctype html>"
1346
					. "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1347
					. "<html>"
1348
					. "\t<head></head>"
1349
					. "\t<body></body>"
1350
					. "</html>"
1351
					. "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1352
			),
1353
			array(
1354
				'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1355
				'expected' => ""
1356
					. "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1357
					. "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1358
					. "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1359
					. "<html>"
1360
					. "\t<head></head>"
1361
					. "\t<body></body>"
1362
					. "</html>"
1363
					. "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1364
			),
1365
			array(
1366
				'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1367
				'expected' => ""
1368
					. "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1369
					. "\t<head></head>"
1370
					. "\t<body></body>"
1371
					. "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1372
			),
1373
			array(
1374
				'name' => 'SSViewerTestCommentsFullSourceIfIE',
1375
				'expected' => ""
1376
					. "<!doctype html>"
1377
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1378
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1379
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1380
					. "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1381
					. "\t<head></head>"
1382
					. "\t<body></body>"
1383
					. "</html>"
1384
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1385
			),
1386
			array(
1387
				'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1388
				'expected' => ""
1389
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1390
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1391
					. "<!--[if !IE]><!--> <html class='no-ie'>"
1392
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1393
					. " <!--<![endif]-->"
1394
					. "\t<head></head>"
1395
					. "\t<body></body>"
1396
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1397
			),
1398
			array(
1399
				'name' => 'SSViewerTestCommentsPartialSource',
1400
				'expected' =>
1401
				"<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1402
					. "<div class='typography'></div>"
1403
					. "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1404
			),
1405
			array(
1406
				'name' => 'SSViewerTestCommentsWithInclude',
1407
				'expected' =>
1408
				"<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1409
					. "<div class='typography'>"
1410
					. "<!-- include 'SSViewerTestCommentsInclude' -->"
1411
					. "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
1412
					. "Included"
1413
					. "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
1414
					. "<!-- end include 'SSViewerTestCommentsInclude' -->"
1415
					. "</div>"
1416
					. "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1417
			),
1418
		);
1419
		foreach ($templates as $template) {
1420
			$this->_renderWithSourceFileComments('SSViewerTestComments/'.$template['name'], $template['expected']);
1421
		}
1422
	}
1423
	private function _renderWithSourceFileComments($name, $expected) {
1424
		$viewer = new SSViewer(array($name));
1425
		$data = new ArrayData(array());
1426
		$result = $viewer->process($data);
1427
		$expected = str_replace(array("\r", "\n"), '', $expected);
1428
		$result = str_replace(array("\r", "\n"), '', $result);
1429
		$this->assertEquals($result, $expected);
1430
	}
1431
1432
	public function testLoopIteratorIterator() {
1433
		$list = new PaginatedList(new ArrayList());
1434
		$viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1435
		$result = $viewer->process(new ArrayData(array('List' => $list)));
1436
		$this->assertEquals($result, '');
1437
	}
1438
1439
	public function testProcessOnlyIncludesRequirementsOnce() {
1440
		$template = new SSViewer(array('SSViewerTestProcess'));
1441
		$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
1442
1443
		$backend = Injector::inst()->create('SilverStripe\\View\\Requirements_Backend');
1444
		$backend->setCombinedFilesEnabled(false);
1445
		$backend->combineFiles(
1446
			'RequirementsTest_ab.css',
1447
			array(
1448
				$basePath . '/RequirementsTest_a.css',
1449
				$basePath . '/RequirementsTest_b.css'
1450
			)
1451
		);
1452
1453
		Requirements::set_backend($backend);
1454
1455
		$this->assertEquals(1, substr_count($template->process(array()), "a.css"));
1456
		$this->assertEquals(1, substr_count($template->process(array()), "b.css"));
1457
1458
		// if we disable the requirements then we should get nothing
1459
		$template->includeRequirements(false);
1460
		$this->assertEquals(0, substr_count($template->process(array()), "a.css"));
1461
		$this->assertEquals(0, substr_count($template->process(array()), "b.css"));
1462
	}
1463
1464
	public function testRequireCallInTemplateInclude() {
1465
		//TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1466
		if(FRAMEWORK_DIR === 'framework') {
1467
			$template = new SSViewer(array('SSViewerTestProcess'));
1468
1469
			Requirements::set_suffix_requirements(false);
1470
1471
			$this->assertEquals(1, substr_count(
1472
				$template->process(array()),
1473
				"tests/javascript/forms/RequirementsTest_a.js"
1474
			));
1475
		}
1476
		else {
1477
			$this->markTestSkipped('Requirement will always fail if the framework dir is not '.
1478
				'named \'framework\', since templates require hard coded paths');
1479
		}
1480
	}
1481
1482
	public function testCallsWithArguments() {
1483
		$data = new ArrayData(array(
1484
			'Set' => new ArrayList(array(
1485
				new SSViewerTest_Object("1"),
1486
				new SSViewerTest_Object("2"),
1487
				new SSViewerTest_Object("3"),
1488
				new SSViewerTest_Object("4"),
1489
				new SSViewerTest_Object("5"),
1490
			)),
1491
			'Level' => new SSViewerTest_LevelTest(1),
1492
			'Nest' => array(
1493
				'Level' => new SSViewerTest_LevelTest(2),
1494
			),
1495
		));
1496
1497
		$tests = array(
1498
			'$Level.output(1)' => '1-1',
1499
			'$Nest.Level.output($Set.First.Number)' => '2-1',
1500
			'<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1501
			'<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1502
			'<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1503
			'<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1504
			'<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1505
			'<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1506
			'<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1507
			'<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1508
			'<% with $Nest %>
1509
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1510
			<% end_with %>' => '1-hi',
1511
			'<% with $Nest %>
1512
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1513
			<% end_with %>' => '!0!1!2!3!4',
1514
		);
1515
1516
		foreach($tests as $template => $expected) {
1517
			$this->assertEquals($expected, trim($this->render($template, $data)));
1518
		}
1519
	}
1520
1521
	public function testRepeatedCallsAreCached() {
1522
		$data = new SSViewerTest_CacheTestData();
1523
		$template = '
1524
			<% if $TestWithCall %>
1525
				<% with $TestWithCall %>
1526
					{$Message}
1527
				<% end_with %>
1528
1529
				{$TestWithCall.Message}
1530
			<% end_if %>';
1531
1532
		$this->assertEquals('HiHi', preg_replace('/\s+/', '', $this->render($template, $data)));
1533
		$this->assertEquals(1, $data->testWithCalls,
1534
			'SSViewerTest_CacheTestData::TestWithCall() should only be called once. Subsequent calls should be cached');
1535
1536
		$data = new SSViewerTest_CacheTestData();
1537
		$template = '
1538
			<% if $TestLoopCall %>
1539
				<% loop $TestLoopCall %>
1540
					{$Message}
1541
				<% end_loop %>
1542
			<% end_if %>';
1543
1544
		$this->assertEquals('OneTwo', preg_replace('/\s+/', '', $this->render($template, $data)));
1545
		$this->assertEquals(1, $data->testLoopCalls,
1546
			'SSViewerTest_CacheTestData::TestLoopCall() should only be called once. Subsequent calls should be cached');
1547
	}
1548
1549
	public function testClosedBlockExtension() {
1550
		$count = 0;
1551
		$parser = new SSTemplateParser();
1552
		$parser->addClosedBlock(
1553
			'test',
1554
			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...
1555
				$count++;
1556
			}
1557
		);
1558
1559
		$template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
1560
		$template->process(new SSViewerTestFixture());
1561
1562
		$this->assertEquals(1, $count);
1563
	}
1564
1565
	public function testOpenBlockExtension() {
1566
		$count = 0;
1567
		$parser = new SSTemplateParser();
1568
		$parser->addOpenBlock(
1569
			'test',
1570
			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...
1571
				$count++;
1572
			}
1573
		);
1574
1575
		$template = new SSViewer_FromString("<% test %>", $parser);
1576
		$template->process(new SSViewerTestFixture());
1577
1578
		$this->assertEquals(1, $count);
1579
	}
1580
1581
	/**
1582
	 * Tests if caching for SSViewer_FromString is working
1583
	 */
1584
	public function testFromStringCaching() {
1585
		$content = 'Test content';
1586
		$cacheFile = TEMP_FOLDER . '/.cache.' . sha1($content);
1587
		if (file_exists($cacheFile)) {
1588
			unlink($cacheFile);
1589
		}
1590
1591
		// Test global behaviors
1592
		$this->render($content, null, null);
1593
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1594
1595
		SSViewer_FromString::config()->update('cache_template', true);
1596
		$this->render($content, null, null);
1597
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1598
		unlink($cacheFile);
1599
1600
		// Test instance behaviors
1601
		$this->render($content, null, false);
1602
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1603
1604
		$this->render($content, null, true);
1605
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1606
		unlink($cacheFile);
1607
	}
1608
}
1609
1610
/**
1611
 * A test fixture that will echo back the template item
1612
 */
1613
class SSViewerTestFixture extends ViewableData {
1614
	protected $name;
1615
1616
	public function __construct($name = null) {
1617
		$this->name = $name;
1618
		parent::__construct();
1619
	}
1620
1621
1622
	private function argedName($fieldName, $arguments) {
1623
		$childName = $this->name ? "$this->name.$fieldName" : $fieldName;
1624
		if($arguments) return $childName . '(' . implode(',', $arguments) . ')';
1625
		else return $childName;
1626
	}
1627
	public function obj($fieldName, $arguments=null, $cache=false, $cacheName=null) {
1628
		$childName = $this->argedName($fieldName, $arguments);
1629
1630
		// Special field name Loop### to create a list
1631
		if(preg_match('/^Loop([0-9]+)$/', $fieldName, $matches)) {
1632
			$output = new ArrayList();
1633
			for($i=0;$i<$matches[1];$i++) $output->push(new SSViewerTestFixture($childName));
1634
			return $output;
1635
1636
		} else if(preg_match('/NotSet/i', $fieldName)) {
1637
			return new ViewableData();
1638
1639
		} else {
1640
			return new SSViewerTestFixture($childName);
1641
		}
1642
	}
1643
1644
1645
	public function XML_val($fieldName, $arguments = null, $cache = false) {
1646
		if(preg_match('/NotSet/i', $fieldName)) {
1647
			return '';
1648
		} else if(preg_match('/Raw/i', $fieldName)) {
1649
			return $fieldName;
1650
		} else {
1651
			return '[out:' . $this->argedName($fieldName, $arguments) . ']';
1652
		}
1653
	}
1654
1655
	public function hasValue($fieldName, $arguments = null, $cache = true) {
1656
		return (bool)$this->XML_val($fieldName, $arguments);
1657
	}
1658
}
1659
1660
class SSViewerTest_ViewableData extends ViewableData implements TestOnly {
1661
1662
	private static $default_cast = 'Text';
1663
1664
	private static $casting = array(
1665
		'TextValue' => 'Text',
1666
		'HTMLValue' => 'HTMLFragment'
1667
	);
1668
1669
	public function methodWithOneArgument($arg1) {
1670
		return "arg1:{$arg1}";
1671
	}
1672
1673
	public function methodWithTwoArguments($arg1, $arg2) {
1674
		return "arg1:{$arg1},arg2:{$arg2}";
1675
	}
1676
}
1677
1678
class SSViewerTest_CacheTestData extends ViewableData implements TestOnly {
1679
1680
	public $testWithCalls = 0;
1681
	public $testLoopCalls = 0;
1682
1683
	public function TestWithCall() {
1684
		$this->testWithCalls++;
1685
		return ArrayData::create(array('Message' => 'Hi'));
1686
	}
1687
1688
	public function TestLoopCall() {
1689
		$this->testLoopCalls++;
1690
		return ArrayList::create(array(
1691
			ArrayData::create(array('Message' => 'One')),
1692
			ArrayData::create(array('Message' => 'Two'))
1693
		));
1694
	}
1695
1696
}
1697
1698
class SSViewerTest_Object extends DataObject implements TestOnly {
1699
1700
	public $number = null;
1701
1702
	private static $casting = array(
1703
		'Link' => 'Text',
1704
	);
1705
1706
1707
	public function __construct($number = null) {
1708
		parent::__construct();
1709
		$this->number = $number;
1710
	}
1711
1712
	public function Number() {
1713
		return $this->number;
1714
	}
1715
1716
	public function absoluteBaseURL() {
1717
		return "testLocalFunctionPriorityCalled";
1718
	}
1719
1720
	public function lotsOfArguments11($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
1721
		return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
1722
	}
1723
1724
	public function Link() {
1725
		return 'some/url.html';
1726
	}
1727
}
1728
1729
class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly {
1730
1731
	public static function get_template_global_variables() {
1732
		return array(
1733
			'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html', 'casting' => 'HTMLFragment'),
1734
			'SSViewerTest_GlobalHTMLEscaped' => array('method' => 'get_html'),
1735
1736
			'SSViewerTest_GlobalAutomatic',
1737
			'SSViewerTest_GlobalReferencedByString' => 'get_reference',
1738
			'SSViewerTest_GlobalReferencedInArray' => array('method' => 'get_reference'),
1739
1740
			'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix', 'casting' => 'HTMLFragment')
1741
1742
		);
1743
	}
1744
1745
	public static function get_html() {
1746
		return '<div></div>';
1747
	}
1748
1749
	public static function SSViewerTest_GlobalAutomatic() {
1750
		return 'automatic';
1751
	}
1752
1753
	public static function get_reference() {
1754
		return 'reference';
1755
	}
1756
1757
	public static function get_argmix() {
1758
		$args = func_get_args();
1759
		return 'z' . implode(':', $args) . 'z';
1760
	}
1761
1762
}
1763
1764
class SSViewerTest_LevelTest extends ViewableData implements TestOnly {
1765
	protected $depth;
1766
1767
	public function __construct($depth = 1) {
1768
		$this->depth = $depth;
1769
	}
1770
1771
	public function output($val) {
1772
		return "$this->depth-$val";
1773
	}
1774
1775
	public function forLoop($number) {
1776
		$ret = array();
1777
		for($i = 0; $i < (int)$number; ++$i) {
1778
			$ret[] = new SSViewerTest_Object("!$i");
1779
		}
1780
		return new ArrayList($ret);
1781
	}
1782
1783
	public function forWith($number) {
1784
		return new self($number);
1785
	}
1786
}
1787