Completed
Push — fix-2494 ( 3153ee )
by Sam
07:19
created

SSViewerTest::testFromStringCaching()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 16
nc 2
nop 0
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View\Tests;
4
5
use PHPUnit_Framework_MockObject_MockObject;
6
use SilverStripe\Control\Controller;
7
use SilverStripe\Control\Director;
8
use SilverStripe\Control\ContentNegotiator;
9
use SilverStripe\Control\HTTPResponse;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Dev\SapphireTest;
13
use SilverStripe\i18n\i18n;
14
use SilverStripe\ORM\DataObject;
15
use SilverStripe\ORM\FieldType\DBField;
16
use SilverStripe\ORM\ArrayList;
17
use SilverStripe\ORM\PaginatedList;
18
use SilverStripe\Security\Member;
19
use SilverStripe\Security\SecurityToken;
20
use SilverStripe\Security\Permission;
21
use SilverStripe\View\ArrayData;
22
use SilverStripe\View\Requirements_Backend;
23
use SilverStripe\View\Requirements_Minifier;
24
use SilverStripe\View\SSViewer;
25
use SilverStripe\View\Requirements;
26
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModel;
27
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModelController;
28
use SilverStripe\View\ViewableData;
29
use SilverStripe\View\SSViewer_FromString;
30
use SilverStripe\View\SSTemplateParser;
31
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
32
use Exception;
33
34
class SSViewerTest extends SapphireTest
35
{
36
37
    /**
38
     * Backup of $_SERVER global
39
     *
40
     * @var array
41
     */
42
    protected $oldServer = array();
43
44
    protected static $extra_dataobjects = array(
45
        SSViewerTest\TestObject::class,
46
    );
47
48
    protected function setUp()
49
    {
50
        parent::setUp();
51
        SSViewer::config()->update('source_file_comments', false);
52
        SSViewer_FromString::config()->update('cache_template', false);
53
        TestAssetStore::activate('SSViewerTest');
54
        $this->oldServer = $_SERVER;
55
    }
56
57
    protected function tearDown()
58
    {
59
        $_SERVER = $this->oldServer;
60
        TestAssetStore::reset();
61
        parent::tearDown();
62
    }
63
64
    /**
65
     * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
66
     * of user defined themes via {@link SiteConfig} and default theme
67
     * when no user themes are defined.
68
     */
69
    public function testCurrentTheme()
70
    {
71
        SSViewer::config()->update('theme', 'mytheme');
72
        $this->assertEquals(
73
            'mytheme',
74
            SSViewer::config()->uninherited('theme'),
75
            'Current theme is the default - user has not defined one'
76
        );
77
    }
78
79
    /**
80
     * Test that a template without a <head> tag still renders.
81
     */
82
    public function testTemplateWithoutHeadRenders()
83
    {
84
        $data = new ArrayData([ 'Var' => 'var value' ]);
85
        $result = $data->renderWith("SSViewerTestPartialTemplate");
86
        $this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U", '', $result)));
87
    }
88
89
    public function testIncludeScopeInheritance()
90
    {
91
        $data = $this->getScopeInheritanceTestData();
92
        $expected = array(
93
        'Item 1 - First-ODD top:Item 1',
94
        'Item 2 - EVEN top:Item 2',
95
        'Item 3 - ODD top:Item 3',
96
        'Item 4 - EVEN top:Item 4',
97
        'Item 5 - ODD top:Item 5',
98
        'Item 6 - Last-EVEN top:Item 6',
99
        );
100
101
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
102
        $this->assertExpectedStrings($result, $expected);
103
104
        // reset results for the tests that include arguments (the title is passed as an arg)
105
        $expected = array(
106
        'Item 1 _ Item 1 - First-ODD top:Item 1',
107
        'Item 2 _ Item 2 - EVEN top:Item 2',
108
        'Item 3 _ Item 3 - ODD top:Item 3',
109
        'Item 4 _ Item 4 - EVEN top:Item 4',
110
        'Item 5 _ Item 5 - ODD top:Item 5',
111
        'Item 6 _ Item 6 - Last-EVEN top:Item 6',
112
        );
113
114
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
115
        $this->assertExpectedStrings($result, $expected);
116
    }
117
118
    public function testIncludeTruthyness()
119
    {
120
        $data = new ArrayData([
121
            'Title' => 'TruthyTest',
122
            'Items' => new ArrayList([
123
                new ArrayData(['Title' => 'Item 1']),
124
                new ArrayData(['Title' => '']),
125
                new ArrayData(['Title' => true]),
126
                new ArrayData(['Title' => false]),
127
                new ArrayData(['Title' => null]),
128
                new ArrayData(['Title' => 0]),
129
                new ArrayData(['Title' => 7])
130
            ])
131
        ]);
132
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
133
134
        // We should not end up with empty values appearing as empty
135
        $expected = [
136
            'Item 1 _ Item 1 - First-ODD top:Item 1',
137
            'Untitled - EVEN top:',
138
            '1 _ 1 - ODD top:1',
139
            'Untitled - EVEN top:',
140
            'Untitled - ODD top:',
141
            'Untitled - EVEN top:0',
142
            '7 _ 7 - Last-ODD top:7',
143
        ];
144
        $this->assertExpectedStrings($result, $expected);
145
    }
146
147
    private function getScopeInheritanceTestData()
148
    {
149
        return new ArrayData([
150
            'Title' => 'TopTitleValue',
151
            'Items' => new ArrayList([
152
                new ArrayData(['Title' => 'Item 1']),
153
                new ArrayData(['Title' => 'Item 2']),
154
                new ArrayData(['Title' => 'Item 3']),
155
                new ArrayData(['Title' => 'Item 4']),
156
                new ArrayData(['Title' => 'Item 5']),
157
                new ArrayData(['Title' => 'Item 6'])
158
            ])
159
        ]);
160
    }
161
162
    private function assertExpectedStrings($result, $expected)
163
    {
164
        foreach ($expected as $expectedStr) {
165
            $this->assertTrue(
166
                (boolean) preg_match("/{$expectedStr}/", $result),
167
                "Didn't find '{$expectedStr}' in:\n{$result}"
168
            );
169
        }
170
    }
171
172
    /**
173
     * Small helper to render templates from strings
174
     *
175
     * @param  string $templateString
176
     * @param  null   $data
177
     * @param  bool   $cacheTemplate
178
     * @return string
179
     */
180 View Code Duplication
    public function render($templateString, $data = null, $cacheTemplate = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
181
    {
182
        $t = SSViewer::fromString($templateString, $cacheTemplate);
183
        if (!$data) {
184
            $data = new SSViewerTest\TestFixture();
185
        }
186
        return trim(''.$t->process($data));
187
    }
188
189
    public function testRequirements()
190
    {
191
        /** @var Requirements_Backend|PHPUnit_Framework_MockObject_MockObject $requirements */
192
        $requirements = $this
193
            ->getMockBuilder(Requirements_Backend::class)
194
            ->setMethods(array("javascript", "css"))
195
            ->getMock();
196
        $jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
197
        $cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
198
199
        $requirements->expects($this->once())->method('javascript')->with($jsFile);
0 ignored issues
show
Bug introduced by
The method expects does only exist in PHPUnit_Framework_MockObject_MockObject, but not in SilverStripe\View\Requirements_Backend.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
200
        $requirements->expects($this->once())->method('css')->with($cssFile);
201
202
        $origReq = Requirements::backend();
203
        Requirements::set_backend($requirements);
204
        $template = $this->render(
205
            "<% require javascript($jsFile) %>
206
		<% require css($cssFile) %>"
207
        );
208
        Requirements::set_backend($origReq);
209
210
        $this->assertFalse((bool)trim($template), "Should be no content in this return.");
211
    }
212
213
    public function testRequirementsCombine()
214
    {
215
        $testBackend = Injector::inst()->create(Requirements_Backend::class);
216
        $testBackend->setSuffixRequirements(false);
217
        //$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
218
219
        $jsFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/bad.js';
220
        $jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
221
        $testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
222
223
        // secondly, make sure that requirements is generated, even though minification failed
224
        $testBackend->processCombinedFiles();
225
        $js = array_keys($testBackend->getJavascript());
226
        $combinedTestFilePath = BASE_PATH . reset($js);
227
        $this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
228
229
        // and make sure the combined content matches the input content, i.e. no loss of functionality
230
        if (!file_exists($combinedTestFilePath)) {
231
            $this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
232
        }
233
        $combinedTestFileContents = file_get_contents($combinedTestFilePath);
234
        $this->assertContains($jsFileContents, $combinedTestFileContents);
235
    }
236
237
    public function testRequirementsMinification()
238
    {
239
        $testBackend = Injector::inst()->create(Requirements_Backend::class);
240
        $testBackend->setSuffixRequirements(false);
241
        $testBackend->setMinifyCombinedFiles(true);
242
        $testFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/RequirementsTest_a.js';
243
        $testFileContent = file_get_contents($testFile);
244
245
        $mockMinifier = $this->getMockBuilder(Requirements_Minifier::class)
246
        ->setMethods(['minify'])
247
        ->getMock();
248
249
        $mockMinifier->expects($this->once())
250
        ->method('minify')
251
        ->with(
252
            $testFileContent,
253
            'js',
254
            $testFile
255
        );
256
        $testBackend->setMinifier($mockMinifier);
257
        $testBackend->combineFiles('testRequirementsMinified.js', array($testFile));
258
        $testBackend->processCombinedFiles();
259
260
        $testBackend->setMinifyCombinedFiles(false);
261
        $mockMinifier->expects($this->never())
262
        ->method('minify');
263
        $testBackend->processCombinedFiles();
264
265
        $this->setExpectedExceptionRegExp(
266
            Exception::class,
267
            '/minification service/'
268
        );
269
270
        $testBackend->setMinifyCombinedFiles(true);
271
        $testBackend->setMinifier(null);
272
        $testBackend->processCombinedFiles();
273
    }
274
275
276
277
    public function testComments()
278
    {
279
        $input = <<<SS
280
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
281
<%-- Alone multi
282
	line comment --%>
283
Some more content
284
Mixing content and <%-- multi
285
	line comment --%> Final final
286
content
287
SS;
288
        $output = $this->render($input);
289
        $shouldbe = <<<SS
290
This is my templateThis is some contentFinal content
291
292
Some more content
293
Mixing content and  Final final
294
content
295
SS;
296
        $this->assertEquals($shouldbe, $output);
297
    }
298
299
    public function testBasicText()
300
    {
301
        $this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
302
        $this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
303
        $this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
304
        $this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
305
    }
306
307
    public function testBasicInjection()
308
    {
309
        $this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
310
        $this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
311
        $this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
312
        $this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
313
314
        $this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
315
        $this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
316
        $this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
317
318
        $this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
319
        $this->assertEquals(
320
            '{\\[out:Test]}',
321
            $this->render('{\\\\$Test}'),
322
            'Escapes before injections are correctly unescaped'
323
        );
324
    }
325
326
327
    public function testGlobalVariableCalls()
328
    {
329
        $this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
330
        $this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
331
        $this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
332
    }
333
334
    public function testGlobalVariableCallsWithArguments()
335
    {
336
        $this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
337
        $this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
338
        $this->assertEquals(
339
            'zFoo:Bar:Bazz',
340
            $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")')
341
        );
342
        $this->assertEquals(
343
            'zreferencez',
344
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)')
345
        );
346
    }
347
348
    public function testGlobalVariablesAreEscaped()
349
    {
350
        $this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
351
        $this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
352
353
        $this->assertEquals(
354
            'z<div></div>z',
355
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)')
356
        );
357
        $this->assertEquals(
358
            'z&lt;div&gt;&lt;/div&gt;z',
359
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)')
360
        );
361
    }
362
363
    public function testCoreGlobalVariableCalls()
364
    {
365
        $this->assertEquals(
366
            Director::absoluteBaseURL(),
367
            $this->render('{$absoluteBaseURL}'),
368
            'Director::absoluteBaseURL can be called from within template'
369
        );
370
        $this->assertEquals(
371
            Director::absoluteBaseURL(),
372
            $this->render('{$AbsoluteBaseURL}'),
373
            'Upper-case %AbsoluteBaseURL can be called from within template'
374
        );
375
376
        $this->assertEquals(
377
            Director::is_ajax(),
378
            $this->render('{$isAjax}'),
379
            'All variations of is_ajax result in the correct call'
380
        );
381
        $this->assertEquals(
382
            Director::is_ajax(),
383
            $this->render('{$IsAjax}'),
384
            'All variations of is_ajax result in the correct call'
385
        );
386
        $this->assertEquals(
387
            Director::is_ajax(),
388
            $this->render('{$is_ajax}'),
389
            'All variations of is_ajax result in the correct call'
390
        );
391
        $this->assertEquals(
392
            Director::is_ajax(),
393
            $this->render('{$Is_ajax}'),
394
            'All variations of is_ajax result in the correct call'
395
        );
396
397
        $this->assertEquals(
398
            i18n::get_locale(),
399
            $this->render('{$i18nLocale}'),
400
            'i18n template functions result correct result'
401
        );
402
        $this->assertEquals(
403
            i18n::get_locale(),
404
            $this->render('{$get_locale}'),
405
            'i18n template functions result correct result'
406
        );
407
408
        $this->assertEquals(
409
            (string)Member::currentUser(),
410
            $this->render('{$CurrentMember}'),
411
            'Member template functions result correct result'
412
        );
413
        $this->assertEquals(
414
            (string)Member::currentUser(),
415
            $this->render('{$CurrentUser}'),
416
            'Member template functions result correct result'
417
        );
418
        $this->assertEquals(
419
            (string)Member::currentUser(),
420
            $this->render('{$currentMember}'),
421
            'Member template functions result correct result'
422
        );
423
        $this->assertEquals(
424
            (string)Member::currentUser(),
425
            $this->render('{$currentUser}'),
426
            'Member template functions result correct result'
427
        );
428
429
        $this->assertEquals(
430
            SecurityToken::getSecurityID(),
431
            $this->render('{$getSecurityID}'),
432
            'SecurityToken template functions result correct result'
433
        );
434
        $this->assertEquals(
435
            SecurityToken::getSecurityID(),
436
            $this->render('{$SecurityID}'),
437
            'SecurityToken template functions result correct result'
438
        );
439
440
        $this->assertEquals(
441
            Permission::check("ADMIN"),
442
            (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
443
            'Permissions template functions result correct result'
444
        );
445
        $this->assertEquals(
446
            Permission::check("ADMIN"),
447
            (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
448
            'Permissions template functions result correct result'
449
        );
450
    }
451
452
    public function testNonFieldCastingHelpersNotUsedInHasValue()
453
    {
454
        // check if Link without $ in front of variable
455
        $result = $this->render(
456
            'A<% if Link %>$Link<% end_if %>B',
457
            new SSViewerTest\TestObject()
458
        );
459
        $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
460
461
        // check if Link with $ in front of variable
462
        $result = $this->render(
463
            'A<% if $Link %>$Link<% end_if %>B',
464
            new SSViewerTest\TestObject()
465
        );
466
        $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
467
    }
468
469
    public function testLocalFunctionsTakePriorityOverGlobals()
470
    {
471
        $data = new ArrayData([
472
            'Page' => new SSViewerTest\TestObject()
473
        ]);
474
475
        //call method with lots of arguments
476
        $result = $this->render(
477
            '<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',
478
            $data
479
        );
480
        $this->assertEquals("abcdefghijk", $result, "public function can accept up to 11 arguments");
481
482
        //call method that does not exist
483
        $result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>', $data);
484
        $this->assertEquals("", $result, "Method does not exist - empty result");
485
486
        //call if that does not exist
487
        $result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>', $data);
488
        $this->assertEquals("", $result, "Method does not exist - empty result");
489
490
        //call method with same name as a global method (local call should take priority)
491
        $result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>', $data);
492
        $this->assertEquals(
493
            "testLocalFunctionPriorityCalled",
494
            $result,
495
            "Local Object's public function called. Did not return the actual baseURL of the current site"
496
        );
497
    }
498
499
    public function testCurrentScopeLoopWith()
500
    {
501
        // Data to run the loop tests on - one sequence of three items, each with a subitem
502
        $data = new ArrayData([
503
            'Foo' => new ArrayList([
504
                'Subocean' => new ArrayData([
505
                    'Name' => 'Higher'
506
                ]),
507
                new ArrayData([
508
                    'Sub' => new ArrayData([
509
                        'Name' => 'SubKid1'
510
                    ])
511
                ]),
512
                new ArrayData([
513
                    'Sub' => new ArrayData([
514
                        'Name' => 'SubKid2'
515
                    ])
516
                ]),
517
                new SSViewerTest\TestObject('Number6')
518
            ])
519
        ]);
520
521
        $result = $this->render(
522
            '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',
523
            $data
524
        );
525
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works");
526
527
        $result = $this->render(
528
            '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',
529
            $data
530
        );
531
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works");
532
533
        $result = $this->render('<% with Foo %>$Count<% end_with %>', $data);
534
        $this->assertEquals("4", $result, "4 items in the DataObjectSet");
535
536
        $result = $this->render(
537
            '<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
538
            . '<% end_if %><% end_loop %><% end_with %>',
539
            $data
540
        );
541
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in with Up.Foo scope works");
542
543
        $result = $this->render(
544
            '<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
545
            . '<% end_if %><% end_loop %><% end_with %>',
546
            $data
547
        );
548
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in current scope works");
549
    }
550
551
    public function testObjectDotArguments()
552
    {
553
        $this->assertEquals(
554
            '[out:TestObject.methodWithOneArgument(one)]
555
				[out:TestObject.methodWithTwoArguments(one,two)]
556
				[out:TestMethod(Arg1,Arg2).Bar.Val]
557
				[out:TestMethod(Arg1,Arg2).Bar]
558
				[out:TestMethod(Arg1,Arg2)]
559
				[out:TestMethod(Arg1).Bar.Val]
560
				[out:TestMethod(Arg1).Bar]
561
				[out:TestMethod(Arg1)]',
562
            $this->render(
563
                '$TestObject.methodWithOneArgument(one)
564
				$TestObject.methodWithTwoArguments(one,two)
565
				$TestMethod(Arg1, Arg2).Bar.Val
566
				$TestMethod(Arg1, Arg2).Bar
567
				$TestMethod(Arg1, Arg2)
568
				$TestMethod(Arg1).Bar.Val
569
				$TestMethod(Arg1).Bar
570
				$TestMethod(Arg1)'
571
            )
572
        );
573
    }
574
575
    public function testEscapedArguments()
576
    {
577
        $this->assertEquals(
578
            '[out:Foo(Arg1,Arg2).Bar.Val].Suffix
579
				[out:Foo(Arg1,Arg2).Val]_Suffix
580
				[out:Foo(Arg1,Arg2)]/Suffix
581
				[out:Foo(Arg1).Bar.Val]textSuffix
582
				[out:Foo(Arg1).Bar].Suffix
583
				[out:Foo(Arg1)].Suffix
584
				[out:Foo.Bar.Val].Suffix
585
				[out:Foo.Bar].Suffix
586
				[out:Foo].Suffix',
587
            $this->render(
588
                '{$Foo(Arg1, Arg2).Bar.Val}.Suffix
589
				{$Foo(Arg1, Arg2).Val}_Suffix
590
				{$Foo(Arg1, Arg2)}/Suffix
591
				{$Foo(Arg1).Bar.Val}textSuffix
592
				{$Foo(Arg1).Bar}.Suffix
593
				{$Foo(Arg1)}.Suffix
594
				{$Foo.Bar.Val}.Suffix
595
				{$Foo.Bar}.Suffix
596
				{$Foo}.Suffix'
597
            )
598
        );
599
    }
600
601
    public function testLoopWhitespace()
602
    {
603
        $this->assertEquals(
604
            'before[out:SingleItem.Test]after
605
				beforeTestafter',
606
            $this->render(
607
                'before<% loop SingleItem %>$Test<% end_loop %>after
608
				before<% loop SingleItem %>Test<% end_loop %>after'
609
            )
610
        );
611
612
        // The control tags are removed from the output, but no whitespace
613
        // This is a quirk that could be changed, but included in the test to make the current
614
        // behaviour explicit
615
        $this->assertEquals(
616
            'before
617
618
[out:SingleItem.ItemOnItsOwnLine]
619
620
after',
621
            $this->render(
622
                'before
623
<% loop SingleItem %>
624
$ItemOnItsOwnLine
625
<% end_loop %>
626
after'
627
            )
628
        );
629
630
        // The whitespace within the control tags is preserve in a loop
631
        // This is a quirk that could be changed, but included in the test to make the current
632
        // behaviour explicit
633
        $this->assertEquals(
634
            'before
635
636
[out:Loop3.ItemOnItsOwnLine]
637
638
[out:Loop3.ItemOnItsOwnLine]
639
640
[out:Loop3.ItemOnItsOwnLine]
641
642
after',
643
            $this->render(
644
                'before
645
<% loop Loop3 %>
646
$ItemOnItsOwnLine
647
<% end_loop %>
648
after'
649
            )
650
        );
651
    }
652
653
    public function testControls()
654
    {
655
        // Single item controls
656
        $this->assertEquals(
657
            'a[out:Foo.Bar.Item]b
658
				[out:Foo.Bar(Arg1).Item]
659
				[out:Foo(Arg1).Item]
660
				[out:Foo(Arg1,Arg2).Item]
661
				[out:Foo(Arg1,Arg2,Arg3).Item]',
662
            $this->render(
663
                '<% with Foo.Bar %>a{$Item}b<% end_with %>
664
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
665
				<% with Foo(Arg1) %>$Item<% end_with %>
666
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
667
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>'
668
            )
669
        );
670
671
        // Loop controls
672
        $this->assertEquals(
673
            'a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
674
            $this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>')
675
        );
676
677
        $this->assertEquals(
678
            '[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
679
            $this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>')
680
        );
681
682
        $this->assertEquals(
683
            '[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
684
            $this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>')
685
        );
686
687
        $this->assertEquals(
688
            '[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
689
            $this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>')
690
        );
691
692
        $this->assertEquals(
693
            '[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
694
            $this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>')
695
        );
696
    }
697
698
    public function testIfBlocks()
699
    {
700
        // Basic test
701
        $this->assertEquals(
702
            'AC',
703
            $this->render('A<% if NotSet %>B$NotSet<% end_if %>C')
704
        );
705
706
        // Nested test
707
        $this->assertEquals(
708
            'AB1C',
709
            $this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C')
710
        );
711
712
        // else_if
713
        $this->assertEquals(
714
            'ACD',
715
            $this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D')
716
        );
717
        $this->assertEquals(
718
            'AD',
719
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D')
720
        );
721
        $this->assertEquals(
722
            'ADE',
723
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E')
724
        );
725
726
        $this->assertEquals(
727
            'ADE',
728
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E')
729
        );
730
731
        // Dot syntax
732
        $this->assertEquals(
733
            'ACD',
734
            $this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D')
735
        );
736
        $this->assertEquals(
737
            'ACD',
738
            $this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D')
739
        );
740
741
        // Params
742
        $this->assertEquals(
743
            'ACD',
744
            $this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D')
745
        );
746
        $this->assertEquals(
747
            'ABD',
748
            $this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D')
749
        );
750
751
        // Negation
752
        $this->assertEquals(
753
            'AC',
754
            $this->render('A<% if not IsSet %>B<% end_if %>C')
755
        );
756
        $this->assertEquals(
757
            'ABC',
758
            $this->render('A<% if not NotSet %>B<% end_if %>C')
759
        );
760
761
        // Or
762
        $this->assertEquals(
763
            'ABD',
764
            $this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D')
765
        );
766
        $this->assertEquals(
767
            'ACD',
768
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D')
769
        );
770
        $this->assertEquals(
771
            'AD',
772
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D')
773
        );
774
        $this->assertEquals(
775
            'ACD',
776
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D')
777
        );
778
        $this->assertEquals(
779
            'AD',
780
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D')
781
        );
782
783
        // Negated Or
784
        $this->assertEquals(
785
            'ACD',
786
            $this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
787
        );
788
        $this->assertEquals(
789
            'ABD',
790
            $this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
791
        );
792
        $this->assertEquals(
793
            'ABD',
794
            $this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
795
        );
796
797
        // And
798
        $this->assertEquals(
799
            'ABD',
800
            $this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D')
801
        );
802
        $this->assertEquals(
803
            'ACD',
804
            $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D')
805
        );
806
        $this->assertEquals(
807
            'AD',
808
            $this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D')
809
        );
810
        $this->assertEquals(
811
            'ACD',
812
            $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D')
813
        );
814
        $this->assertEquals(
815
            'AD',
816
            $this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D')
817
        );
818
819
        // Equality
820
        $this->assertEquals(
821
            'ABC',
822
            $this->render('A<% if RawVal == RawVal %>B<% end_if %>C')
823
        );
824
        $this->assertEquals(
825
            'ACD',
826
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D')
827
        );
828
        $this->assertEquals(
829
            'ABC',
830
            $this->render('A<% if Right != Wrong %>B<% end_if %>C')
831
        );
832
        $this->assertEquals(
833
            'AD',
834
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D')
835
        );
836
837
        // test inequalities with simple numbers
838
        $this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
839
        $this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
840
        $this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
841
        $this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
842
843
        $this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
844
        $this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
845
        $this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
846
        $this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
847
848
        $this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
849
        $this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
850
        $this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
851
        $this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
852
853
        // empty else_if and else tags, if this would not be supported,
854
        // the output would stop after A, thereby failing the assert
855
        $this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
856
        $this->assertEquals(
857
            'AD',
858
            $this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D')
859
        );
860
        $this->assertEquals(
861
            'AD',
862
            $this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D')
863
        );
864
865
        // Bare words with ending space
866
        $this->assertEquals(
867
            'ABC',
868
            $this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C')
869
        );
870
871
        // Else
872
        $this->assertEquals(
873
            'ADE',
874
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E')
875
        );
876
877
        // Empty if with else
878
        $this->assertEquals(
879
            'ABC',
880
            $this->render('A<% if NotSet %><% else %>B<% end_if %>C')
881
        );
882
    }
883
884
    public function testBaseTagGeneration()
885
    {
886
        // XHTML wil have a closed base tag
887
        $tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
888
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
889
            . ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
890
			<html>
891
				<head><% base_tag %></head>
892
				<body><p>test</p><body>
893
			</html>';
894
        $this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
895
896
        // HTML4 and 5 will only have it for IE
897
        $tmpl2 = '<!DOCTYPE html>
898
			<html>
899
				<head><% base_tag %></head>
900
				<body><p>test</p><body>
901
			</html>';
902
        $this->assertRegExp(
903
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
904
            $this->render($tmpl2)
905
        );
906
907
908
        $tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
909
			<html>
910
				<head><% base_tag %></head>
911
				<body><p>test</p><body>
912
			</html>';
913
        $this->assertRegExp(
914
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
915
            $this->render($tmpl3)
916
        );
917
918
        // Check that the content negotiator converts to the equally legal formats
919
        $negotiator = new ContentNegotiator();
920
921
        $response = new HTTPResponse($this->render($tmpl1));
922
        $negotiator->html($response);
923
        $this->assertRegExp(
924
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
925
            $response->getBody()
926
        );
927
928
        $response = new HTTPResponse($this->render($tmpl1));
929
        $negotiator->xhtml($response);
930
        $this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
931
    }
932
933
    public function testIncludeWithArguments()
934
    {
935
        $this->assertEquals(
936
            $this->render('<% include SSViewerTestIncludeWithArguments %>'),
937
            '<p>[out:Arg1]</p><p>[out:Arg2]</p>'
938
        );
939
940
        $this->assertEquals(
941
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
942
            '<p>A</p><p>[out:Arg2]</p>'
943
        );
944
945
        $this->assertEquals(
946
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
947
            '<p>A</p><p>B</p>'
948
        );
949
950
        $this->assertEquals(
951
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
952
            '<p>A Bare String</p><p>B Bare String</p>'
953
        );
954
955
        $this->assertEquals(
956
            $this->render(
957
                '<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
958
                new ArrayData(array('B' => 'Bar'))
959
            ),
960
            '<p>A</p><p>Bar</p>'
961
        );
962
963
        $this->assertEquals(
964
            $this->render(
965
                '<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
966
                new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))
967
            ),
968
            '<p>A</p><p>Bar</p>'
969
        );
970
971
        $this->assertEquals(
972
            $this->render(
973
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
974
                new ArrayData(
975
                    array('Items' => new ArrayList(
976
                        array(
977
                        new ArrayData(array('Title' => 'Foo')),
978
                        new ArrayData(array('Title' => 'Bar'))
979
                        )
980
                    ))
981
                )
982
            ),
983
            'SomeArg - Foo - Bar - SomeArg'
984
        );
985
986
        $this->assertEquals(
987
            $this->render(
988
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
989
                new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))
990
            ),
991
            'A - B - A'
992
        );
993
994
        $this->assertEquals(
995
            $this->render(
996
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
997
                new ArrayData(
998
                    array(
999
                    'Item' => new ArrayData(
1000
                        array(
1001
                        'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
1002
                        )
1003
                    ))
1004
                )
1005
            ),
1006
            'A - B - C - B - A'
1007
        );
1008
1009
        $this->assertEquals(
1010
            $this->render(
1011
                '<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
1012
                new ArrayData(
1013
                    array(
1014
                    'Item' => new ArrayData(
1015
                        array(
1016
                        'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
1017
                        )
1018
                    ))
1019
                )
1020
            ),
1021
            'A - A - A'
1022
        );
1023
1024
        $data = new ArrayData(
1025
            array(
1026
            'Nested' => new ArrayData(
1027
                array(
1028
                'Object' => new ArrayData(array('Key' => 'A'))
1029
                )
1030
            ),
1031
            'Object' => new ArrayData(array('Key' => 'B'))
1032
            )
1033
        );
1034
1035
        $tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
1036
        $res  = $tmpl->process($data);
1037
        $this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
1038
    }
1039
1040
    public function testNamespaceInclude()
1041
    {
1042
        $data = new ArrayData([]);
1043
1044
        $this->assertEquals(
1045
            "tests:( NamespaceInclude\n )",
1046
            $this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
1047
            'Backslashes work for namespace references in includes'
1048
        );
1049
1050
        $this->assertEquals(
1051
            "tests:( NamespaceInclude\n )",
1052
            $this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
1053
            'Forward slashes work for namespace references in includes'
1054
        );
1055
    }
1056
1057
1058
    public function testRecursiveInclude()
1059
    {
1060
        $view = new SSViewer(array('Includes/SSViewerTestRecursiveInclude'));
1061
1062
        $data = new ArrayData(
1063
            array(
1064
            'Title' => 'A',
1065
            'Children' => new ArrayList(
1066
                array(
1067
                new ArrayData(
1068
                    array(
1069
                    'Title' => 'A1',
1070
                    'Children' => new ArrayList(
1071
                        array(
1072
                        new ArrayData(array( 'Title' => 'A1 i', )),
1073
                        new ArrayData(array( 'Title' => 'A1 ii', )),
1074
                        )
1075
                    ),
1076
                    )
1077
                ),
1078
                new ArrayData(array( 'Title' => 'A2', )),
1079
                new ArrayData(array( 'Title' => 'A3', )),
1080
                )
1081
            ),
1082
            )
1083
        );
1084
1085
        $result = $view->process($data);
1086
        // We don't care about whitespace
1087
        $rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
1088
1089
        $this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
1090
    }
1091
1092
    public function assertEqualIgnoringWhitespace($a, $b, $message = '')
1093
    {
1094
        $this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b), $message);
1095
    }
1096
1097
    /**
1098
     * See {@link ViewableDataTest} for more extensive casting tests,
1099
     * this test just ensures that basic casting is correctly applied during template parsing.
1100
     */
1101
    public function testCastingHelpers()
1102
    {
1103
        $vd = new SSViewerTest\TestViewableData();
1104
        $vd->TextValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property TextValue does not exist on object<SilverStripe\View...rTest\TestViewableData>. 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...
1105
        $vd->HTMLValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property HTMLValue does not exist on object<SilverStripe\View...rTest\TestViewableData>. 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...
1106
        $vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SilverStripe\View...rTest\TestViewableData>. 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...
1107
1108
        // Value casted as "Text"
1109
        $this->assertEquals(
1110
            '&lt;b&gt;html&lt;/b&gt;',
1111
            $t = SSViewer::fromString('$TextValue')->process($vd)
1112
        );
1113
        $this->assertEquals(
1114
            '<b>html</b>',
1115
            $t = SSViewer::fromString('$TextValue.RAW')->process($vd)
1116
        );
1117
        $this->assertEquals(
1118
            '&lt;b&gt;html&lt;/b&gt;',
1119
            $t = SSViewer::fromString('$TextValue.XML')->process($vd)
1120
        );
1121
1122
        // Value casted as "HTMLText"
1123
        $this->assertEquals(
1124
            '<b>html</b>',
1125
            $t = SSViewer::fromString('$HTMLValue')->process($vd)
1126
        );
1127
        $this->assertEquals(
1128
            '<b>html</b>',
1129
            $t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
1130
        );
1131
        $this->assertEquals(
1132
            '&lt;b&gt;html&lt;/b&gt;',
1133
            $t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
1134
        );
1135
1136
        // Uncasted value (falls back to ViewableData::$default_cast="Text")
1137
        $vd = new SSViewerTest\TestViewableData();
1138
        $vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SilverStripe\View...rTest\TestViewableData>. 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...
1139
        $this->assertEquals(
1140
            '&lt;b&gt;html&lt;/b&gt;',
1141
            $t = SSViewer::fromString('$UncastedValue')->process($vd)
1142
        );
1143
        $this->assertEquals(
1144
            '<b>html</b>',
1145
            $t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
1146
        );
1147
        $this->assertEquals(
1148
            '&lt;b&gt;html&lt;/b&gt;',
1149
            $t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
1150
        );
1151
    }
1152
1153
    public function testSSViewerBasicIteratorSupport()
1154
    {
1155
        $data = new ArrayData(
1156
            array(
1157
            'Set' => new ArrayList(
1158
                array(
1159
                new SSViewerTest\TestObject("1"),
1160
                new SSViewerTest\TestObject("2"),
1161
                new SSViewerTest\TestObject("3"),
1162
                new SSViewerTest\TestObject("4"),
1163
                new SSViewerTest\TestObject("5"),
1164
                new SSViewerTest\TestObject("6"),
1165
                new SSViewerTest\TestObject("7"),
1166
                new SSViewerTest\TestObject("8"),
1167
                new SSViewerTest\TestObject("9"),
1168
                new SSViewerTest\TestObject("10"),
1169
                )
1170
            )
1171
            )
1172
        );
1173
1174
        //base test
1175
        $result = $this->render('<% loop Set %>$Number<% end_loop %>', $data);
1176
        $this->assertEquals("12345678910", $result, "Numbers rendered in order");
1177
1178
        //test First
1179
        $result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>', $data);
1180
        $this->assertEquals("1", $result, "Only the first number is rendered");
1181
1182
        //test Last
1183
        $result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>', $data);
1184
        $this->assertEquals("10", $result, "Only the last number is rendered");
1185
1186
        //test Even
1187
        $result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>', $data);
1188
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1189
1190
        //test Even with quotes
1191
        $result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>', $data);
1192
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1193
1194
        //test Even without quotes
1195
        $result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>', $data);
1196
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1197
1198
        //test Even with zero-based start index
1199
        $result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>', $data);
1200
        $this->assertEquals("13579", $result, "Even (with zero-based index) numbers rendered in order");
1201
1202
        //test Odd
1203
        $result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>', $data);
1204
        $this->assertEquals("13579", $result, "Odd numbers rendered in order");
1205
1206
        //test FirstLast
1207
        $result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>', $data);
1208
        $this->assertEquals("1first10last", $result, "First and last numbers rendered in order");
1209
1210
        //test Middle
1211
        $result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>', $data);
1212
        $this->assertEquals("23456789", $result, "Middle numbers rendered in order");
1213
1214
        //test MiddleString
1215
        $result = $this->render(
1216
            '<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
1217
            . '<% end_loop %>',
1218
            $data
1219
        );
1220
        $this->assertEquals(
1221
            "2middle3middle4middle5middle6middle7middle8middle9middle",
1222
            $result,
1223
            "Middle numbers rendered in order"
1224
        );
1225
1226
        //test EvenOdd
1227
        $result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>', $data);
1228
        $this->assertEquals(
1229
            "oddevenoddevenoddevenoddevenoddeven",
1230
            $result,
1231
            "Even and Odd is returned in sequence numbers rendered in order"
1232
        );
1233
1234
        //test Pos
1235
        $result = $this->render('<% loop Set %>$Pos<% end_loop %>', $data);
1236
        $this->assertEquals("12345678910", $result, '$Pos is rendered in order');
1237
1238
        //test Pos
1239
        $result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>', $data);
1240
        $this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
1241
1242
        //test FromEnd
1243
        $result = $this->render('<% loop Set %>$FromEnd<% end_loop %>', $data);
1244
        $this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
1245
1246
        //test FromEnd
1247
        $result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>', $data);
1248
        $this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
1249
1250
        //test Total
1251
        $result = $this->render('<% loop Set %>$TotalItems<% end_loop %>', $data);
1252
        $this->assertEquals("10101010101010101010", $result, "10 total items X 10 are returned");
1253
1254
        //test Modulus
1255
        $result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>', $data);
1256
        $this->assertEquals("1010101010", $result, "1-indexed pos modular divided by 2 rendered in order");
1257
1258
        //test MultipleOf 3
1259
        $result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>', $data);
1260
        $this->assertEquals("369", $result, "Only numbers that are multiples of 3 are returned");
1261
1262
        //test MultipleOf 4
1263
        $result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>', $data);
1264
        $this->assertEquals("48", $result, "Only numbers that are multiples of 4 are returned");
1265
1266
        //test MultipleOf 5
1267
        $result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>', $data);
1268
        $this->assertEquals("510", $result, "Only numbers that are multiples of 5 are returned");
1269
1270
        //test MultipleOf 10
1271
        $result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>', $data);
1272
        $this->assertEquals("10", $result, "Only numbers that are multiples of 10 (with 1-based indexing) are returned");
1273
1274
        //test MultipleOf 9 zero-based
1275
        $result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>', $data);
1276
        $this->assertEquals(
1277
            "110",
1278
            $result,
1279
            "Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)"
1280
        );
1281
1282
        //test MultipleOf 11
1283
        $result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>', $data);
1284
        $this->assertEquals("", $result, "Only numbers that are multiples of 11 are returned. I.e. nothing returned");
1285
    }
1286
1287
    /**
1288
     * Test $Up works when the scope $Up refers to was entered with a "with" block
1289
     */
1290
    public function testUpInWith()
1291
    {
1292
1293
        // Data to run the loop tests on - three levels deep
1294
        $data = new ArrayData(
1295
            array(
1296
            'Name' => 'Top',
1297
            'Foo' => new ArrayData(
1298
                array(
1299
                'Name' => 'Foo',
1300
                'Bar' => new ArrayData(
1301
                    array(
1302
                    'Name' => 'Bar',
1303
                    'Baz' => new ArrayData(
1304
                        array(
1305
                        'Name' => 'Baz'
1306
                        )
1307
                    ),
1308
                    'Qux' => new ArrayData(
1309
                        array(
1310
                        'Name' => 'Qux'
1311
                        )
1312
                    )
1313
                    )
1314
                )
1315
                )
1316
            )
1317
            )
1318
        );
1319
1320
        // Basic functionality
1321
        $this->assertEquals(
1322
            'BarFoo',
1323
            $this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data)
1324
        );
1325
1326
        // Two level with block, up refers to internally referenced Bar
1327
        $this->assertEquals(
1328
            'BarFoo',
1329
            $this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data)
1330
        );
1331
1332
        // Stepping up & back down the scope tree
1333
        $this->assertEquals(
1334
            'BazBarQux',
1335
            $this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data)
1336
        );
1337
1338
        // Using $Up in a with block
1339
        $this->assertEquals(
1340
            'BazBarQux',
1341
            $this->render(
1342
                '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
1343
                .'<% end_with %>',
1344
                $data
1345
            )
1346
        );
1347
1348
        // Stepping up & back down the scope tree with with blocks
1349
        $this->assertEquals(
1350
            'BazBarQuxBarBaz',
1351
            $this->render(
1352
                '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
1353
                . '{$Name}<% end_with %>{$Name}<% end_with %>',
1354
                $data
1355
            )
1356
        );
1357
1358
        // Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1359
        $this->assertEquals(
1360
            'Foo',
1361
            $this->render(
1362
                '<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
1363
                . '<% end_with %>',
1364
                $data
1365
            )
1366
        );
1367
1368
        // Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
1369
        $this->assertEquals(
1370
            'Foo',
1371
            $this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data)
1372
        );
1373
    }
1374
1375
    /**
1376
     * Test $Up works when the scope $Up refers to was entered with a "loop" block
1377
     */
1378
    public function testUpInLoop()
1379
    {
1380
1381
        // Data to run the loop tests on - one sequence of three items, each with a subitem
1382
        $data = new ArrayData(
1383
            array(
1384
            'Name' => 'Top',
1385
            'Foo' => new ArrayList(
1386
                array(
1387
                new ArrayData(
1388
                    array(
1389
                    'Name' => '1',
1390
                    'Sub' => new ArrayData(
1391
                        array(
1392
                        'Name' => 'Bar'
1393
                        )
1394
                    )
1395
                    )
1396
                ),
1397
                new ArrayData(
1398
                    array(
1399
                    'Name' => '2',
1400
                    'Sub' => new ArrayData(
1401
                        array(
1402
                        'Name' => 'Baz'
1403
                        )
1404
                    )
1405
                    )
1406
                ),
1407
                new ArrayData(
1408
                    array(
1409
                    'Name' => '3',
1410
                    'Sub' => new ArrayData(
1411
                        array(
1412
                        'Name' => 'Qux'
1413
                        )
1414
                    )
1415
                    )
1416
                )
1417
                )
1418
            )
1419
            )
1420
        );
1421
1422
        // Make sure inside a loop, $Up refers to the current item of the loop
1423
        $this->assertEqualIgnoringWhitespace(
1424
            '111 222 333',
1425
            $this->render(
1426
                '<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1427
                $data
1428
            )
1429
        );
1430
1431
        // Make sure inside a loop, looping over $Up uses a separate iterator,
1432
        // and doesn't interfere with the original iterator
1433
        $this->assertEqualIgnoringWhitespace(
1434
            '1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1435
            $this->render(
1436
                '<% loop $Foo %>
1437
					$Name
1438
					<% with $Sub %>
1439
						$Name
1440
						<% loop $Up %>$Name<% end_loop %>
1441
						$Name
1442
					<% end_with %>
1443
					$Name
1444
				<% end_loop %>',
1445
                $data
1446
            )
1447
        );
1448
1449
        // Make sure inside a loop, looping over $Up uses a separate iterator,
1450
        // and doesn't interfere with the original iterator or local lookups
1451
        $this->assertEqualIgnoringWhitespace(
1452
            '1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1453
            $this->render(
1454
                '<% loop $Foo %>
1455
					$Name
1456
					<% with $Sub %>
1457
						{$Name}{$Up.Name}
1458
						<% loop $Up %>$Name<% end_loop %>
1459
						{$Up.Name}{$Name}
1460
					<% end_with %>
1461
					$Name
1462
				<% end_loop %>',
1463
                $data
1464
            )
1465
        );
1466
    }
1467
1468
    /**
1469
     * Test that nested loops restore the loop variables correctly when pushing and popping states
1470
     */
1471
    public function testNestedLoops()
1472
    {
1473
1474
        // Data to run the loop tests on - one sequence of three items, one with child elements
1475
        // (of a different size to the main sequence)
1476
        $data = new ArrayData(
1477
            array(
1478
            'Foo' => new ArrayList(
1479
                array(
1480
                new ArrayData(
1481
                    array(
1482
                    'Name' => '1',
1483
                    'Children' => new ArrayList(
1484
                        array(
1485
                        new ArrayData(
1486
                            array(
1487
                            'Name' => 'a'
1488
                            )
1489
                        ),
1490
                        new ArrayData(
1491
                            array(
1492
                            'Name' => 'b'
1493
                            )
1494
                        ),
1495
                        )
1496
                    ),
1497
                    )
1498
                ),
1499
                new ArrayData(
1500
                    array(
1501
                    'Name' => '2',
1502
                    'Children' => new ArrayList(),
1503
                    )
1504
                ),
1505
                new ArrayData(
1506
                    array(
1507
                    'Name' => '3',
1508
                    'Children' => new ArrayList(),
1509
                    )
1510
                ),
1511
                )
1512
            ),
1513
            )
1514
        );
1515
1516
        // Make sure that including a loop inside a loop will not destroy the internal count of
1517
        // items, checked by using "Last"
1518
        $this->assertEqualIgnoringWhitespace(
1519
            '1ab23last',
1520
            $this->render(
1521
                '<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1522
                . '<% end_loop %>',
1523
                $data
1524
            )
1525
        );
1526
    }
1527
1528
    public function testLayout()
1529
    {
1530
        $this->useTestTheme(
1531
            __DIR__.'/SSViewerTest',
1532
            'layouttest',
1533
            function () {
1534
                $template = new SSViewer(array('Page'));
1535
                $this->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1536
1537
                $template = new SSViewer(array('Shortcodes', 'Page'));
1538
                $this->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1539
            }
1540
        );
1541
    }
1542
1543
    /**
1544
     * @covers \SilverStripe\View\SSViewer::get_templates_by_class()
1545
     */
1546
    public function testGetTemplatesByClass()
1547
    {
1548
        $this->useTestTheme(
1549
            __DIR__ . '/SSViewerTest',
1550
            'layouttest',
1551
            function () {
1552
            // Test passing a string
1553
                $templates = SSViewer::get_templates_by_class(
1554
                    SSViewerTestModelController::class,
1555
                    '',
1556
                    Controller::class
1557
                );
1558
                $this->assertEquals(
1559
                    [
1560
                    SSViewerTestModelController::class,
1561
                    [
1562
                        'type' => 'Includes',
1563
                        SSViewerTestModelController::class,
1564
                    ],
1565
                    SSViewerTestModel::class,
1566
                    Controller::class,
1567
                    [
1568
                        'type' => 'Includes',
1569
                        Controller::class,
1570
                    ],
1571
                    ],
1572
                    $templates
1573
                );
1574
1575
            // Test to ensure we're stopping at the base class.
1576
                $templates = SSViewer::get_templates_by_class(
1577
                    SSViewerTestModelController::class,
1578
                    '',
1579
                    SSViewerTestModelController::class
1580
                );
1581
                $this->assertEquals(
1582
                    [
1583
                    SSViewerTestModelController::class,
1584
                    [
1585
                        'type' => 'Includes',
1586
                        SSViewerTestModelController::class,
1587
                    ],
1588
                    SSViewerTestModel::class,
1589
                    ],
1590
                    $templates
1591
                );
1592
1593
            // Make sure we can search templates by suffix.
1594
                $templates = SSViewer::get_templates_by_class(
1595
                    SSViewerTestModel::class,
1596
                    'Controller',
1597
                    DataObject::class
1598
                );
1599
                $this->assertEquals(
1600
                    [
1601
                    SSViewerTestModelController::class,
1602
                    [
1603
                        'type' => 'Includes',
1604
                        SSViewerTestModelController::class,
1605
                    ],
1606
                    DataObject::class . 'Controller',
1607
                    [
1608
                        'type' => 'Includes',
1609
                        DataObject::class . 'Controller',
1610
                    ],
1611
                    ],
1612
                    $templates
1613
                );
1614
1615
                // Let's throw something random in there.
1616
                $this->setExpectedException('InvalidArgumentException');
1617
                SSViewer::get_templates_by_class(array());
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a string|object.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1618
            }
1619
        );
1620
    }
1621
1622
    public function testRewriteHashlinks()
1623
    {
1624
        SSViewer::config()->update('rewrite_hash_links', true);
1625
1626
        $_SERVER['HTTP_HOST'] = 'www.mysite.com';
1627
        $_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1628
1629
        // Emulate SSViewer::process()
1630
        // Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1631
        // as protocol-less absolute urls
1632
        $base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1633
1634
        $tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1635
1636
        // Note: SSViewer_FromString doesn't rewrite hash links.
1637
        file_put_contents(
1638
            $tmplFile,
1639
            '<!DOCTYPE html>
1640
			<html>
1641
				<head><% base_tag %></head>
1642
				<body>
1643
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1644
				$ExternalInsertedLink
1645
				<a class="inline" href="#anchor">InlineLink</a>
1646
				$InsertedLink
1647
				<svg><use xlink:href="#sprite"></use></svg>
1648
				<body>
1649
			</html>'
1650
        );
1651
        $tmpl = new SSViewer($tmplFile);
1652
        $obj = new ViewableData();
1653
        $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...
1654
            'HTMLFragment',
1655
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1656
        );
1657
        $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...
1658
            'HTMLFragment',
1659
            '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>'
1660
        );
1661
        $result = $tmpl->process($obj);
1662
        $this->assertContains(
1663
            '<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1664
            $result
1665
        );
1666
        $this->assertContains(
1667
            '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1668
            $result
1669
        );
1670
        $this->assertContains(
1671
            '<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1672
            $result
1673
        );
1674
        $this->assertContains(
1675
            '<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1676
            $result
1677
        );
1678
        $this->assertContains(
1679
            '<svg><use xlink:href="#sprite"></use></svg>',
1680
            $result,
1681
            'SSTemplateParser should only rewrite anchor hrefs'
1682
        );
1683
1684
        unlink($tmplFile);
1685
    }
1686
1687
    public function testRewriteHashlinksInPhpMode()
1688
    {
1689
        SSViewer::config()->update('rewrite_hash_links', 'php');
1690
1691
        $tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1692
1693
        // Note: SSViewer_FromString doesn't rewrite hash links.
1694
        file_put_contents(
1695
            $tmplFile,
1696
            '<!DOCTYPE html>
1697
			<html>
1698
				<head><% base_tag %></head>
1699
				<body>
1700
				<a class="inline" href="#anchor">InlineLink</a>
1701
				$InsertedLink
1702
				<svg><use xlink:href="#sprite"></use></svg>
1703
				<body>
1704
			</html>'
1705
        );
1706
        $tmpl = new SSViewer($tmplFile);
1707
        $obj = new ViewableData();
1708
        $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...
1709
            'HTMLFragment',
1710
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1711
        );
1712
        $result = $tmpl->process($obj);
1713
1714
        $code = <<<'EOC'
1715
<a class="inserted" href="<?php echo \SilverStripe\Core\Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1716
EOC;
1717
        $this->assertContains($code, $result);
1718
        // TODO Fix inline links in PHP mode
1719
        // $this->assertContains(
1720
        // 	'<a class="inline" href="<?php echo str_replace(',
1721
        // 	$result
1722
        // );
1723
        $this->assertContains(
1724
            '<svg><use xlink:href="#sprite"></use></svg>',
1725
            $result,
1726
            'SSTemplateParser should only rewrite anchor hrefs'
1727
        );
1728
1729
        unlink($tmplFile);
1730
    }
1731
1732
    public function testRenderWithSourceFileComments()
1733
    {
1734
        Director::set_environment_type('dev');
1735
        SSViewer::config()->update('source_file_comments', true);
1736
        $i = __DIR__ . '/SSViewerTest/templates/Includes';
1737
        $f = __DIR__ . '/SSViewerTest/templates/SSViewerTestComments';
1738
        $templates = array(
1739
        array(
1740
            'name' => 'SSViewerTestCommentsFullSource',
1741
            'expected' => ""
1742
                . "<!doctype html>"
1743
                . "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1744
                . "<html>"
1745
                . "\t<head></head>"
1746
                . "\t<body></body>"
1747
                . "</html>"
1748
                . "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1749
        ),
1750
        array(
1751
            'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1752
            'expected' => ""
1753
                . "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1754
                . "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1755
                . "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1756
                . "<html>"
1757
                . "\t<head></head>"
1758
                . "\t<body></body>"
1759
                . "</html>"
1760
                . "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1761
        ),
1762
        array(
1763
            'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1764
            'expected' => ""
1765
                . "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1766
                . "\t<head></head>"
1767
                . "\t<body></body>"
1768
                . "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1769
        ),
1770
        array(
1771
            'name' => 'SSViewerTestCommentsFullSourceIfIE',
1772
            'expected' => ""
1773
                . "<!doctype html>"
1774
                . "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1775
                . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1776
                . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1777
                . "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1778
                . "\t<head></head>"
1779
                . "\t<body></body>"
1780
                . "</html>"
1781
                . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1782
        ),
1783
        array(
1784
            'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1785
            'expected' => ""
1786
                . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1787
                . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1788
                . "<!--[if !IE]><!--> <html class='no-ie'>"
1789
                . "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1790
                . " <!--<![endif]-->"
1791
                . "\t<head></head>"
1792
                . "\t<body></body>"
1793
                . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1794
        ),
1795
        array(
1796
            'name' => 'SSViewerTestCommentsPartialSource',
1797
            'expected' =>
1798
            "<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1799
                . "<div class='typography'></div>"
1800
                . "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1801
        ),
1802
        array(
1803
            'name' => 'SSViewerTestCommentsWithInclude',
1804
            'expected' =>
1805
            "<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1806
                . "<div class='typography'>"
1807
                . "<!-- include 'SSViewerTestCommentsInclude' -->"
1808
                . "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
1809
                . "Included"
1810
                . "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
1811
                . "<!-- end include 'SSViewerTestCommentsInclude' -->"
1812
                . "</div>"
1813
                . "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1814
        ),
1815
        );
1816
        foreach ($templates as $template) {
1817
            $this->_renderWithSourceFileComments('SSViewerTestComments/'.$template['name'], $template['expected']);
1818
        }
1819
    }
1820
    private function _renderWithSourceFileComments($name, $expected)
1821
    {
1822
        $viewer = new SSViewer(array($name));
1823
        $data = new ArrayData(array());
1824
        $result = $viewer->process($data);
1825
        $expected = str_replace(array("\r", "\n"), '', $expected);
1826
        $result = str_replace(array("\r", "\n"), '', $result);
1827
        $this->assertEquals($result, $expected);
1828
    }
1829
1830
    public function testLoopIteratorIterator()
1831
    {
1832
        $list = new PaginatedList(new ArrayList());
1833
        $viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1834
        $result = $viewer->process(new ArrayData(array('List' => $list)));
1835
        $this->assertEquals($result, '');
1836
    }
1837
1838
    public function testProcessOnlyIncludesRequirementsOnce()
1839
    {
1840
        $template = new SSViewer(array('SSViewerTestProcess'));
1841
        $basePath = $this->getCurrentRelativePath() . '/SSViewerTest';
1842
1843
        $backend = Injector::inst()->create(Requirements_Backend::class);
1844
        $backend->setCombinedFilesEnabled(false);
1845
        $backend->combineFiles(
1846
            'RequirementsTest_ab.css',
1847
            array(
1848
            $basePath . '/css/RequirementsTest_a.css',
1849
            $basePath . '/css/RequirementsTest_b.css'
1850
            )
1851
        );
1852
1853
        Requirements::set_backend($backend);
1854
1855
        $this->assertEquals(1, substr_count($template->process(array()), "a.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<SilverStripe\View\ViewableData>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1862
    }
1863
1864
    public function testRequireCallInTemplateInclude()
1865
    {
1866
        //TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1867
        if (FRAMEWORK_DIR === 'framework') {
1868
            $template = new SSViewer(array('SSViewerTestProcess'));
1869
1870
            Requirements::set_suffix_requirements(false);
1871
1872
            $this->assertEquals(
1873
                1,
1874
                substr_count(
1875
                    $template->process(array()),
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<SilverStripe\View\ViewableData>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1876
                    "tests/php/View/SSViewerTest/javascript/RequirementsTest_a.js"
1877
                )
1878
            );
1879
        } else {
1880
            $this->markTestSkipped(
1881
                'Requirement will always fail if the framework dir is not '.
1882
                'named \'framework\', since templates require hard coded paths'
1883
            );
1884
        }
1885
    }
1886
1887
    public function testCallsWithArguments()
1888
    {
1889
        $data = new ArrayData(
1890
            array(
1891
            'Set' => new ArrayList(
1892
                array(
1893
                new SSViewerTest\TestObject("1"),
1894
                new SSViewerTest\TestObject("2"),
1895
                new SSViewerTest\TestObject("3"),
1896
                new SSViewerTest\TestObject("4"),
1897
                new SSViewerTest\TestObject("5"),
1898
                )
1899
            ),
1900
            'Level' => new SSViewerTest\LevelTestData(1),
1901
            'Nest' => array(
1902
            'Level' => new SSViewerTest\LevelTestData(2),
1903
            ),
1904
            )
1905
        );
1906
1907
        $tests = array(
1908
        '$Level.output(1)' => '1-1',
1909
        '$Nest.Level.output($Set.First.Number)' => '2-1',
1910
        '<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1911
        '<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1912
        '<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1913
        '<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1914
        '<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1915
        '<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1916
        '<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1917
        '<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1918
        '<% with $Nest %>
1919
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1920
			<% end_with %>' => '1-hi',
1921
        '<% with $Nest %>
1922
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1923
			<% end_with %>' => '!0!1!2!3!4',
1924
        );
1925
1926
        foreach ($tests as $template => $expected) {
1927
            $this->assertEquals($expected, trim($this->render($template, $data)));
1928
        }
1929
    }
1930
1931
    public function testRepeatedCallsAreCached()
1932
    {
1933
        $data = new SSViewerTest\CacheTestData();
1934
        $template = '
1935
			<% if $TestWithCall %>
1936
				<% with $TestWithCall %>
1937
					{$Message}
1938
				<% end_with %>
1939
1940
				{$TestWithCall.Message}
1941
			<% end_if %>';
1942
1943
        $this->assertEquals('HiHi', preg_replace('/\s+/', '', $this->render($template, $data)));
1944
        $this->assertEquals(
1945
            1,
1946
            $data->testWithCalls,
1947
            'SSViewerTest_CacheTestData::TestWithCall() should only be called once. Subsequent calls should be cached'
1948
        );
1949
1950
        $data = new SSViewerTest\CacheTestData();
1951
        $template = '
1952
			<% if $TestLoopCall %>
1953
				<% loop $TestLoopCall %>
1954
					{$Message}
1955
				<% end_loop %>
1956
			<% end_if %>';
1957
1958
        $this->assertEquals('OneTwo', preg_replace('/\s+/', '', $this->render($template, $data)));
1959
        $this->assertEquals(
1960
            1,
1961
            $data->testLoopCalls,
1962
            'SSViewerTest_CacheTestData::TestLoopCall() should only be called once. Subsequent calls should be cached'
1963
        );
1964
    }
1965
1966 View Code Duplication
    public function testClosedBlockExtension()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1967
    {
1968
        $count = 0;
1969
        $parser = new SSTemplateParser();
1970
        $parser->addClosedBlock(
1971
            'test',
1972
            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...
1973
                $count++;
1974
            }
1975
        );
1976
1977
        $template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
1978
        $template->process(new SSViewerTest\TestFixture());
1979
1980
        $this->assertEquals(1, $count);
1981
    }
1982
1983 View Code Duplication
    public function testOpenBlockExtension()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1984
    {
1985
        $count = 0;
1986
        $parser = new SSTemplateParser();
1987
        $parser->addOpenBlock(
1988
            'test',
1989
            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...
1990
                $count++;
1991
            }
1992
        );
1993
1994
        $template = new SSViewer_FromString("<% test %>", $parser);
1995
        $template->process(new SSViewerTest\TestFixture());
1996
1997
        $this->assertEquals(1, $count);
1998
    }
1999
2000
    /**
2001
     * Tests if caching for SSViewer_FromString is working
2002
     */
2003
    public function testFromStringCaching()
2004
    {
2005
        $content = 'Test content';
2006
        $cacheFile = TEMP_FOLDER . '/.cache.' . sha1($content);
2007
        if (file_exists($cacheFile)) {
2008
            unlink($cacheFile);
2009
        }
2010
2011
        // Test global behaviors
2012
        $this->render($content, null, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2017
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2018
        unlink($cacheFile);
2019
2020
        // Test instance behaviors
2021
        $this->render($content, null, false);
2022
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2023
2024
        $this->render($content, null, true);
2025
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2026
        unlink($cacheFile);
2027
    }
2028
}
2029