Completed
Push — master ( b39c91...747c07 )
by Daniel
109:45 queued 99:13
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 SilverStripe\Control\Controller;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\ContentNegotiator;
8
use SilverStripe\Control\HTTPResponse;
9
use SilverStripe\Core\Convert;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Dev\Debug;
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\SSViewer;
24
use SilverStripe\View\Requirements;
25
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModel;
26
use SilverStripe\View\Tests\SSViewerTest\SSViewerTestModelController;
27
use SilverStripe\View\ViewableData;
28
use SilverStripe\View\SSViewer_FromString;
29
use SilverStripe\View\SSTemplateParser;
30
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
31
use JSMin;
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 $extraDataObjects = array(
45
        SSViewerTest\TestObject::class,
46
    );
47
48
    public function setUp()
0 ignored issues
show
Coding Style introduced by
setUp uses the super-global variable $_SERVER which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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
    public function tearDown()
0 ignored issues
show
Coding Style introduced by
tearDown uses the super-global variable $_SERVER which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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
        //TODO: SiteConfig moved to CMS
72
        SSViewer::config()->update('theme', 'mytheme');
73
        $this->assertEquals(
74
            'mytheme',
75
            SSViewer::config()->get('theme'),
76
            'Current theme is the default - user has not defined one'
77
        );
78
    }
79
80
    /**
81
     * Test that a template without a <head> tag still renders.
82
     */
83
    public function testTemplateWithoutHeadRenders()
84
    {
85
        $data = new ArrayData(
86
            array(
87
            'Var' => 'var value'
88
            )
89
        );
90
91
        $result = $data->renderWith("SSViewerTestPartialTemplate");
92
        $this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U", '', $result)));
93
    }
94
95
    public function testIncludeScopeInheritance()
96
    {
97
        $data = $this->getScopeInheritanceTestData();
98
        $expected = array(
99
            'Item 1 - First-ODD top:Item 1',
100
            'Item 2 - EVEN top:Item 2',
101
            'Item 3 - ODD top:Item 3',
102
            'Item 4 - EVEN top:Item 4',
103
            'Item 5 - ODD top:Item 5',
104
            'Item 6 - Last-EVEN top:Item 6',
105
        );
106
107
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
108
        $this->assertExpectedStrings($result, $expected);
109
110
        // reset results for the tests that include arguments (the title is passed as an arg)
111
        $expected = array(
112
            'Item 1 _ Item 1 - First-ODD top:Item 1',
113
            'Item 2 _ Item 2 - EVEN top:Item 2',
114
            'Item 3 _ Item 3 - ODD top:Item 3',
115
            'Item 4 _ Item 4 - EVEN top:Item 4',
116
            'Item 5 _ Item 5 - ODD top:Item 5',
117
            'Item 6 _ Item 6 - Last-EVEN top:Item 6',
118
        );
119
120
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
121
        $this->assertExpectedStrings($result, $expected);
122
    }
123
124
    public function testIncludeTruthyness()
125
    {
126
        $data = new ArrayData(
127
            array(
128
            'Title' => 'TruthyTest',
129
            'Items' => new ArrayList(
130
                array(
131
                new ArrayData(array('Title' => 'Item 1')),
132
                new ArrayData(array('Title' => '')),
133
                new ArrayData(array('Title' => true)),
134
                new ArrayData(array('Title' => false)),
135
                new ArrayData(array('Title' => null)),
136
                new ArrayData(array('Title' => 0)),
137
                new ArrayData(array('Title' => 7))
138
                )
139
            )
140
            )
141
        );
142
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
143
144
        // We should not end up with empty values appearing as empty
145
        $expected = array(
146
            'Item 1 _ Item 1 - First-ODD top:Item 1',
147
            'Untitled - EVEN top:',
148
            '1 _ 1 - ODD top:1',
149
            'Untitled - EVEN top:',
150
            'Untitled - ODD top:',
151
            'Untitled - EVEN top:0',
152
            '7 _ 7 - Last-ODD top:7'
153
        );
154
        $this->assertExpectedStrings($result, $expected);
155
    }
156
157
    private function getScopeInheritanceTestData()
158
    {
159
        return new ArrayData(
160
            array(
161
            'Title' => 'TopTitleValue',
162
            'Items' => new ArrayList(
163
                array(
164
                new ArrayData(array('Title' => 'Item 1')),
165
                new ArrayData(array('Title' => 'Item 2')),
166
                new ArrayData(array('Title' => 'Item 3')),
167
                new ArrayData(array('Title' => 'Item 4')),
168
                new ArrayData(array('Title' => 'Item 5')),
169
                new ArrayData(array('Title' => 'Item 6'))
170
                )
171
            )
172
            )
173
        );
174
    }
175
176
    private function assertExpectedStrings($result, $expected)
177
    {
178
        foreach ($expected as $expectedStr) {
179
            $this->assertTrue(
180
                (boolean) preg_match("/{$expectedStr}/", $result),
181
                "Didn't find '{$expectedStr}' in:\n{$result}"
182
            );
183
        }
184
    }
185
186
    /**
187
     * Small helper to render templates from strings
188
     *
189
     * @param  string $templateString
190
     * @param  null   $data
191
     * @param  bool   $cacheTemplate
192
     * @return string
193
     */
194
    public function render($templateString, $data = null, $cacheTemplate = false)
195
    {
196
        $t = SSViewer::fromString($templateString, $cacheTemplate);
197
        if (!$data) {
198
            $data = new SSViewerTest\TestFixture();
199
        }
200
        return trim(''.$t->process($data));
201
    }
202
203
    public function testRequirements()
204
    {
205
        $requirements = $this->getMock("SilverStripe\\View\\Requirements_Backend", array("javascript", "css"));
206
        $jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
207
        $cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
208
209
        $requirements->expects($this->once())->method('javascript')->with($jsFile);
210
        $requirements->expects($this->once())->method('css')->with($cssFile);
211
212
        $origReq = Requirements::backend();
213
        Requirements::set_backend($requirements);
214
        $template = $this->render(
215
            "<% require javascript($jsFile) %>
216
		<% require css($cssFile) %>"
217
        );
218
        Requirements::set_backend($origReq);
219
220
        $this->assertFalse((bool)trim($template), "Should be no content in this return.");
221
    }
222
223
    public function testRequirementsCombine()
224
    {
225
        $testBackend = Injector::inst()->create(Requirements_Backend::class);
226
        $testBackend->setSuffixRequirements(false);
227
        //$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
228
229
        $jsFile = $this->getCurrentRelativePath() . '/SSViewerTest/javascript/bad.js';
230
        $jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
231
        $testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
232
233
        // first make sure that our test js file causes an exception to be thrown
234
        try {
235
            include_once 'jsmin/jsmin.php';
236
            JSMin::minify($jsFileContents);
237
            $this->fail('JSMin did not throw exception on minify bad file: ');
238
        } catch (Exception $e) {
239
            // exception thrown... good
240
        }
241
242
        // secondly, make sure that requirements is generated, even though minification failed
243
        $testBackend->processCombinedFiles();
244
        $js = array_keys($testBackend->getJavascript());
245
        $combinedTestFilePath = BASE_PATH . reset($js);
246
        $this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
247
248
        // and make sure the combined content matches the input content, i.e. no loss of functionality
249
        if (!file_exists($combinedTestFilePath)) {
250
            $this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
251
        }
252
        $combinedTestFileContents = file_get_contents($combinedTestFilePath);
253
        $this->assertContains($jsFileContents, $combinedTestFileContents);
254
    }
255
256
257
258
    public function testComments()
259
    {
260
        $output = $this->render(
261
            <<<SS
262
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
263
<%-- Alone multi
264
	line comment --%>
265
Some more content
266
Mixing content and <%-- multi
267
	line comment --%> Final final
268
content
269
SS
270
        );
271
        $shouldbe = <<<SS
272
This is my templateThis is some contentFinal content
273
274
Some more content
275
Mixing content and  Final final
276
content
277
SS;
278
279
        $this->assertEquals($shouldbe, $output);
280
    }
281
282
    public function testBasicText()
283
    {
284
        $this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
285
        $this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
286
        $this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
287
        $this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
288
    }
289
290
    public function testBasicInjection()
291
    {
292
        $this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
293
        $this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
294
        $this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
295
        $this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
296
297
        $this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
298
        $this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
299
        $this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
300
301
        $this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
302
        $this->assertEquals(
303
            '{\\[out:Test]}',
304
            $this->render('{\\\\$Test}'),
305
            'Escapes before injections are correctly unescaped'
306
        );
307
    }
308
309
310
    public function testGlobalVariableCalls()
311
    {
312
        $this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
313
        $this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
314
        $this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
315
    }
316
317
    public function testGlobalVariableCallsWithArguments()
318
    {
319
        $this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
320
        $this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
321
        $this->assertEquals(
322
            'zFoo:Bar:Bazz',
323
            $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")')
324
        );
325
        $this->assertEquals(
326
            'zreferencez',
327
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)')
328
        );
329
    }
330
331
    public function testGlobalVariablesAreEscaped()
332
    {
333
        $this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
334
        $this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
335
336
        $this->assertEquals(
337
            'z<div></div>z',
338
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)')
339
        );
340
        $this->assertEquals(
341
            'z&lt;div&gt;&lt;/div&gt;z',
342
            $this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)')
343
        );
344
    }
345
346
    public function testCoreGlobalVariableCalls()
347
    {
348
        $this->assertEquals(
349
            Director::absoluteBaseURL(),
350
            $this->render('{$absoluteBaseURL}'),
351
            'Director::absoluteBaseURL can be called from within template'
352
        );
353
        $this->assertEquals(
354
            Director::absoluteBaseURL(),
355
            $this->render('{$AbsoluteBaseURL}'),
356
            'Upper-case %AbsoluteBaseURL can be called from within template'
357
        );
358
359
        $this->assertEquals(
360
            Director::is_ajax(),
361
            $this->render('{$isAjax}'),
362
            'All variations of is_ajax result in the correct call'
363
        );
364
        $this->assertEquals(
365
            Director::is_ajax(),
366
            $this->render('{$IsAjax}'),
367
            'All variations of is_ajax result in the correct call'
368
        );
369
        $this->assertEquals(
370
            Director::is_ajax(),
371
            $this->render('{$is_ajax}'),
372
            'All variations of is_ajax result in the correct call'
373
        );
374
        $this->assertEquals(
375
            Director::is_ajax(),
376
            $this->render('{$Is_ajax}'),
377
            'All variations of is_ajax result in the correct call'
378
        );
379
380
        $this->assertEquals(
381
            i18n::get_locale(),
382
            $this->render('{$i18nLocale}'),
383
            'i18n template functions result correct result'
384
        );
385
        $this->assertEquals(
386
            i18n::get_locale(),
387
            $this->render('{$get_locale}'),
388
            'i18n template functions result correct result'
389
        );
390
391
        $this->assertEquals(
392
            (string)Member::currentUser(),
393
            $this->render('{$CurrentMember}'),
394
            'Member template functions result correct result'
395
        );
396
        $this->assertEquals(
397
            (string)Member::currentUser(),
398
            $this->render('{$CurrentUser}'),
399
            'Member template functions result correct result'
400
        );
401
        $this->assertEquals(
402
            (string)Member::currentUser(),
403
            $this->render('{$currentMember}'),
404
            'Member template functions result correct result'
405
        );
406
        $this->assertEquals(
407
            (string)Member::currentUser(),
408
            $this->render('{$currentUser}'),
409
            'Member template functions result correct result'
410
        );
411
412
        $this->assertEquals(
413
            SecurityToken::getSecurityID(),
414
            $this->render('{$getSecurityID}'),
415
            'SecurityToken template functions result correct result'
416
        );
417
        $this->assertEquals(
418
            SecurityToken::getSecurityID(),
419
            $this->render('{$SecurityID}'),
420
            'SecurityToken template functions result correct result'
421
        );
422
423
        $this->assertEquals(
424
            Permission::check("ADMIN"),
425
            (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
426
            'Permissions template functions result correct result'
427
        );
428
        $this->assertEquals(
429
            Permission::check("ADMIN"),
430
            (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
431
            'Permissions template functions result correct result'
432
        );
433
    }
434
435
    public function testNonFieldCastingHelpersNotUsedInHasValue()
436
    {
437
        // check if Link without $ in front of variable
438
        $result = $this->render(
439
            'A<% if Link %>$Link<% end_if %>B',
440
            new SSViewerTest\TestObject()
441
        );
442
        $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
443
444
        // check if Link with $ in front of variable
445
        $result = $this->render(
446
            'A<% if $Link %>$Link<% end_if %>B',
447
            new SSViewerTest\TestObject()
448
        );
449
        $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
450
    }
451
452
    public function testLocalFunctionsTakePriorityOverGlobals()
453
    {
454
        $data = new ArrayData(
455
            array(
456
            'Page' => new SSViewerTest\TestObject()
457
            )
458
        );
459
460
        //call method with lots of arguments
461
        $result = $this->render(
462
            '<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',
463
            $data
464
        );
465
        $this->assertEquals("abcdefghijk", $result, "public function can accept up to 11 arguments");
466
467
        //call method that does not exist
468
        $result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>', $data);
469
        $this->assertEquals("", $result, "Method does not exist - empty result");
470
471
        //call if that does not exist
472
        $result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>', $data);
473
        $this->assertEquals("", $result, "Method does not exist - empty result");
474
475
        //call method with same name as a global method (local call should take priority)
476
        $result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>', $data);
477
        $this->assertEquals(
478
            "testLocalFunctionPriorityCalled",
479
            $result,
480
            "Local Object's public function called. Did not return the actual baseURL of the current site"
481
        );
482
    }
483
484
    public function testCurrentScopeLoopWith()
485
    {
486
        // Data to run the loop tests on - one sequence of three items, each with a subitem
487
        $data = new ArrayData(
488
            array(
489
            'Foo' => new ArrayList(
490
                array(
491
                'Subocean' => new ArrayData(
492
                    array(
493
                        'Name' => 'Higher'
494
                    )
495
                ),
496
                new ArrayData(
497
                    array(
498
                    'Sub' => new ArrayData(
499
                        array(
500
                        'Name' => 'SubKid1'
501
                        )
502
                    )
503
                    )
504
                ),
505
                new ArrayData(
506
                    array(
507
                    'Sub' => new ArrayData(
508
                        array(
509
                        'Name' => 'SubKid2'
510
                        )
511
                    )
512
                    )
513
                ),
514
                new SSViewerTest\TestObject('Number6')
515
                )
516
            )
517
            )
518
        );
519
520
        $result = $this->render(
521
            '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',
522
            $data
523
        );
524
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works");
525
526
        $result = $this->render(
527
            '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',
528
            $data
529
        );
530
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works");
531
532
        $result = $this->render('<% with Foo %>$Count<% end_with %>', $data);
533
        $this->assertEquals("4", $result, "4 items in the DataObjectSet");
534
535
        $result = $this->render(
536
            '<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
537
            . '<% end_if %><% end_loop %><% end_with %>',
538
            $data
539
        );
540
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in with Up.Foo scope works");
541
542
        $result = $this->render(
543
            '<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
544
            . '<% end_if %><% end_loop %><% end_with %>',
545
            $data
546
        );
547
        $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in current scope works");
548
    }
549
550
    public function testObjectDotArguments()
551
    {
552
        $this->assertEquals(
553
            '[out:TestObject.methodWithOneArgument(one)]
554
				[out:TestObject.methodWithTwoArguments(one,two)]
555
				[out:TestMethod(Arg1,Arg2).Bar.Val]
556
				[out:TestMethod(Arg1,Arg2).Bar]
557
				[out:TestMethod(Arg1,Arg2)]
558
				[out:TestMethod(Arg1).Bar.Val]
559
				[out:TestMethod(Arg1).Bar]
560
				[out:TestMethod(Arg1)]',
561
            $this->render(
562
                '$TestObject.methodWithOneArgument(one)
563
				$TestObject.methodWithTwoArguments(one,two)
564
				$TestMethod(Arg1, Arg2).Bar.Val
565
				$TestMethod(Arg1, Arg2).Bar
566
				$TestMethod(Arg1, Arg2)
567
				$TestMethod(Arg1).Bar.Val
568
				$TestMethod(Arg1).Bar
569
				$TestMethod(Arg1)'
570
            )
571
        );
572
    }
573
574
    public function testEscapedArguments()
575
    {
576
        $this->assertEquals(
577
            '[out:Foo(Arg1,Arg2).Bar.Val].Suffix
578
				[out:Foo(Arg1,Arg2).Val]_Suffix
579
				[out:Foo(Arg1,Arg2)]/Suffix
580
				[out:Foo(Arg1).Bar.Val]textSuffix
581
				[out:Foo(Arg1).Bar].Suffix
582
				[out:Foo(Arg1)].Suffix
583
				[out:Foo.Bar.Val].Suffix
584
				[out:Foo.Bar].Suffix
585
				[out:Foo].Suffix',
586
            $this->render(
587
                '{$Foo(Arg1, Arg2).Bar.Val}.Suffix
588
				{$Foo(Arg1, Arg2).Val}_Suffix
589
				{$Foo(Arg1, Arg2)}/Suffix
590
				{$Foo(Arg1).Bar.Val}textSuffix
591
				{$Foo(Arg1).Bar}.Suffix
592
				{$Foo(Arg1)}.Suffix
593
				{$Foo.Bar.Val}.Suffix
594
				{$Foo.Bar}.Suffix
595
				{$Foo}.Suffix'
596
            )
597
        );
598
    }
599
600
    public function testLoopWhitespace()
601
    {
602
        $this->assertEquals(
603
            'before[out:SingleItem.Test]after
604
				beforeTestafter',
605
            $this->render(
606
                'before<% loop SingleItem %>$Test<% end_loop %>after
607
				before<% loop SingleItem %>Test<% end_loop %>after'
608
            )
609
        );
610
611
        // The control tags are removed from the output, but no whitespace
612
        // This is a quirk that could be changed, but included in the test to make the current
613
        // behaviour explicit
614
        $this->assertEquals(
615
            'before
616
617
[out:SingleItem.ItemOnItsOwnLine]
618
619
after',
620
            $this->render(
621
                'before
622
<% loop SingleItem %>
623
$ItemOnItsOwnLine
624
<% end_loop %>
625
after'
626
            )
627
        );
628
629
        // The whitespace within the control tags is preserve in a loop
630
        // This is a quirk that could be changed, but included in the test to make the current
631
        // behaviour explicit
632
        $this->assertEquals(
633
            'before
634
635
[out:Loop3.ItemOnItsOwnLine]
636
637
[out:Loop3.ItemOnItsOwnLine]
638
639
[out:Loop3.ItemOnItsOwnLine]
640
641
after',
642
            $this->render(
643
                'before
644
<% loop Loop3 %>
645
$ItemOnItsOwnLine
646
<% end_loop %>
647
after'
648
            )
649
        );
650
    }
651
652
    public function testControls()
653
    {
654
        // Single item controls
655
        $this->assertEquals(
656
            'a[out:Foo.Bar.Item]b
657
				[out:Foo.Bar(Arg1).Item]
658
				[out:Foo(Arg1).Item]
659
				[out:Foo(Arg1,Arg2).Item]
660
				[out:Foo(Arg1,Arg2,Arg3).Item]',
661
            $this->render(
662
                '<% with Foo.Bar %>a{$Item}b<% end_with %>
663
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
664
				<% with Foo(Arg1) %>$Item<% end_with %>
665
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
666
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>'
667
            )
668
        );
669
670
        // Loop controls
671
        $this->assertEquals(
672
            'a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
673
            $this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>')
674
        );
675
676
        $this->assertEquals(
677
            '[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
678
            $this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>')
679
        );
680
681
        $this->assertEquals(
682
            '[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
683
            $this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>')
684
        );
685
686
        $this->assertEquals(
687
            '[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
688
            $this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>')
689
        );
690
691
        $this->assertEquals(
692
            '[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
693
            $this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>')
694
        );
695
    }
696
697
    public function testIfBlocks()
698
    {
699
        // Basic test
700
        $this->assertEquals(
701
            'AC',
702
            $this->render('A<% if NotSet %>B$NotSet<% end_if %>C')
703
        );
704
705
        // Nested test
706
        $this->assertEquals(
707
            'AB1C',
708
            $this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C')
709
        );
710
711
        // else_if
712
        $this->assertEquals(
713
            'ACD',
714
            $this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D')
715
        );
716
        $this->assertEquals(
717
            'AD',
718
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D')
719
        );
720
        $this->assertEquals(
721
            'ADE',
722
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E')
723
        );
724
725
        $this->assertEquals(
726
            'ADE',
727
            $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E')
728
        );
729
730
        // Dot syntax
731
        $this->assertEquals(
732
            'ACD',
733
            $this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D')
734
        );
735
        $this->assertEquals(
736
            'ACD',
737
            $this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D')
738
        );
739
740
        // Params
741
        $this->assertEquals(
742
            'ACD',
743
            $this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D')
744
        );
745
        $this->assertEquals(
746
            'ABD',
747
            $this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D')
748
        );
749
750
        // Negation
751
        $this->assertEquals(
752
            'AC',
753
            $this->render('A<% if not IsSet %>B<% end_if %>C')
754
        );
755
        $this->assertEquals(
756
            'ABC',
757
            $this->render('A<% if not NotSet %>B<% end_if %>C')
758
        );
759
760
        // Or
761
        $this->assertEquals(
762
            'ABD',
763
            $this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D')
764
        );
765
        $this->assertEquals(
766
            'ACD',
767
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D')
768
        );
769
        $this->assertEquals(
770
            'AD',
771
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D')
772
        );
773
        $this->assertEquals(
774
            'ACD',
775
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D')
776
        );
777
        $this->assertEquals(
778
            'AD',
779
            $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D')
780
        );
781
782
        // Negated Or
783
        $this->assertEquals(
784
            'ACD',
785
            $this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
786
        );
787
        $this->assertEquals(
788
            'ABD',
789
            $this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
790
        );
791
        $this->assertEquals(
792
            'ABD',
793
            $this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D')
794
        );
795
796
        // And
797
        $this->assertEquals(
798
            'ABD',
799
            $this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D')
800
        );
801
        $this->assertEquals(
802
            'ACD',
803
            $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D')
804
        );
805
        $this->assertEquals(
806
            'AD',
807
            $this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D')
808
        );
809
        $this->assertEquals(
810
            'ACD',
811
            $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D')
812
        );
813
        $this->assertEquals(
814
            'AD',
815
            $this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D')
816
        );
817
818
        // Equality
819
        $this->assertEquals(
820
            'ABC',
821
            $this->render('A<% if RawVal == RawVal %>B<% end_if %>C')
822
        );
823
        $this->assertEquals(
824
            'ACD',
825
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D')
826
        );
827
        $this->assertEquals(
828
            'ABC',
829
            $this->render('A<% if Right != Wrong %>B<% end_if %>C')
830
        );
831
        $this->assertEquals(
832
            'AD',
833
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D')
834
        );
835
836
        // test inequalities with simple numbers
837
        $this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
838
        $this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
839
        $this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
840
        $this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
841
842
        $this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
843
        $this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
844
        $this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
845
        $this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
846
847
        $this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
848
        $this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
849
        $this->assertEquals('ACD', $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
852
        // empty else_if and else tags, if this would not be supported,
853
        // the output would stop after A, thereby failing the assert
854
        $this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
855
        $this->assertEquals(
856
            'AD',
857
            $this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D')
858
        );
859
        $this->assertEquals(
860
            'AD',
861
            $this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D')
862
        );
863
864
        // Bare words with ending space
865
        $this->assertEquals(
866
            'ABC',
867
            $this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C')
868
        );
869
870
        // Else
871
        $this->assertEquals(
872
            'ADE',
873
            $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E')
874
        );
875
876
        // Empty if with else
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
877
        $this->assertEquals(
878
            'ABC',
879
            $this->render('A<% if NotSet %><% else %>B<% end_if %>C')
880
        );
881
    }
882
883
    public function testBaseTagGeneration()
884
    {
885
        // XHTML wil have a closed base tag
886
        $tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
887
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
888
                . ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
889
			<html>
890
				<head><% base_tag %></head>
891
				<body><p>test</p><body>
892
			</html>';
893
        $this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
894
895
        // HTML4 and 5 will only have it for IE
896
        $tmpl2 = '<!DOCTYPE html>
897
			<html>
898
				<head><% base_tag %></head>
899
				<body><p>test</p><body>
900
			</html>';
901
        $this->assertRegExp(
902
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
903
            $this->render($tmpl2)
904
        );
905
906
907
        $tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
908
			<html>
909
				<head><% base_tag %></head>
910
				<body><p>test</p><body>
911
			</html>';
912
        $this->assertRegExp(
913
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
914
            $this->render($tmpl3)
915
        );
916
917
        // Check that the content negotiator converts to the equally legal formats
918
        $negotiator = new ContentNegotiator();
919
920
        $response = new HTTPResponse($this->render($tmpl1));
921
        $negotiator->html($response);
922
        $this->assertRegExp(
923
            '/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
924
            $response->getBody()
925
        );
926
927
        $response = new HTTPResponse($this->render($tmpl1));
928
        $negotiator->xhtml($response);
929
        $this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
930
    }
931
932
    public function testIncludeWithArguments()
933
    {
934
        $this->assertEquals(
935
            $this->render('<% include SSViewerTestIncludeWithArguments %>'),
936
            '<p>[out:Arg1]</p><p>[out:Arg2]</p>'
937
        );
938
939
        $this->assertEquals(
940
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
941
            '<p>A</p><p>[out:Arg2]</p>'
942
        );
943
944
        $this->assertEquals(
945
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
946
            '<p>A</p><p>B</p>'
947
        );
948
949
        $this->assertEquals(
950
            $this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
951
            '<p>A Bare String</p><p>B Bare String</p>'
952
        );
953
954
        $this->assertEquals(
955
            $this->render(
956
                '<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
957
                new ArrayData(array('B' => 'Bar'))
958
            ),
959
            '<p>A</p><p>Bar</p>'
960
        );
961
962
        $this->assertEquals(
963
            $this->render(
964
                '<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
965
                new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))
966
            ),
967
            '<p>A</p><p>Bar</p>'
968
        );
969
970
        $this->assertEquals(
971
            $this->render(
972
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
973
                new ArrayData(
974
                    array('Items' => new ArrayList(
975
                        array(
976
                        new ArrayData(array('Title' => 'Foo')),
977
                        new ArrayData(array('Title' => 'Bar'))
978
                        )
979
                    ))
980
                )
981
            ),
982
            'SomeArg - Foo - Bar - SomeArg'
983
        );
984
985
        $this->assertEquals(
986
            $this->render(
987
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
988
                new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))
989
            ),
990
            'A - B - A'
991
        );
992
993
        $this->assertEquals(
994
            $this->render(
995
                '<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
996
                new ArrayData(
997
                    array(
998
                    'Item' => new ArrayData(
999
                        array(
1000
                        'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
1001
                        )
1002
                    ))
1003
                )
1004
            ),
1005
            'A - B - C - B - A'
1006
        );
1007
1008
        $this->assertEquals(
1009
            $this->render(
1010
                '<% include SSViewerTestIncludeScopeInheritanceWithUpAndTop Title="A" %>',
1011
                new ArrayData(
1012
                    array(
1013
                    'Item' => new ArrayData(
1014
                        array(
1015
                        'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
1016
                        )
1017
                    ))
1018
                )
1019
            ),
1020
            'A - A - A'
1021
        );
1022
1023
        $data = new ArrayData(
1024
            array(
1025
            'Nested' => new ArrayData(
1026
                array(
1027
                'Object' => new ArrayData(array('Key' => 'A'))
1028
                )
1029
            ),
1030
            'Object' => new ArrayData(array('Key' => 'B'))
1031
            )
1032
        );
1033
1034
        $tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
1035
        $res  = $tmpl->process($data);
1036
        $this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
1037
    }
1038
1039
    public function testNamespaceInclude()
1040
    {
1041
        $data = new ArrayData([]);
1042
1043
        $this->assertEquals(
1044
            "tests:( NamespaceInclude\n )",
1045
            $this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data),
1046
            'Backslashes work for namespace references in includes'
1047
        );
1048
1049
        $this->assertEquals(
1050
            "tests:( NamespaceInclude\n )",
1051
            $this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data),
1052
            'Forward slashes work for namespace references in includes'
1053
        );
1054
    }
1055
1056
1057
    public function testRecursiveInclude()
1058
    {
1059
        $view = new SSViewer(array('Includes/SSViewerTestRecursiveInclude'));
1060
1061
        $data = new ArrayData(
1062
            array(
1063
            'Title' => 'A',
1064
            'Children' => new ArrayList(
1065
                array(
1066
                new ArrayData(
1067
                    array(
1068
                    'Title' => 'A1',
1069
                    'Children' => new ArrayList(
1070
                        array(
1071
                        new ArrayData(array( 'Title' => 'A1 i', )),
1072
                        new ArrayData(array( 'Title' => 'A1 ii', )),
1073
                        )
1074
                    ),
1075
                    )
1076
                ),
1077
                new ArrayData(array( 'Title' => 'A2', )),
1078
                new ArrayData(array( 'Title' => 'A3', )),
1079
                )
1080
            ),
1081
            )
1082
        );
1083
1084
        $result = $view->process($data);
1085
        // We don't care about whitespace
1086
        $rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
1087
1088
        $this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
1089
    }
1090
1091
    public function assertEqualIgnoringWhitespace($a, $b, $message = '')
1092
    {
1093
        $this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b), $message);
1094
    }
1095
1096
    /**
1097
     * See {@link ViewableDataTest} for more extensive casting tests,
1098
     * this test just ensures that basic casting is correctly applied during template parsing.
1099
     */
1100
    public function testCastingHelpers()
1101
    {
1102
        $vd = new SSViewerTest\TestViewableData();
1103
        $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...
1104
        $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...
1105
        $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...
1106
1107
        // Value casted as "Text"
1108
        $this->assertEquals(
1109
            '&lt;b&gt;html&lt;/b&gt;',
1110
            $t = SSViewer::fromString('$TextValue')->process($vd)
1111
        );
1112
        $this->assertEquals(
1113
            '<b>html</b>',
1114
            $t = SSViewer::fromString('$TextValue.RAW')->process($vd)
1115
        );
1116
        $this->assertEquals(
1117
            '&lt;b&gt;html&lt;/b&gt;',
1118
            $t = SSViewer::fromString('$TextValue.XML')->process($vd)
1119
        );
1120
1121
        // Value casted as "HTMLText"
1122
        $this->assertEquals(
1123
            '<b>html</b>',
1124
            $t = SSViewer::fromString('$HTMLValue')->process($vd)
1125
        );
1126
        $this->assertEquals(
1127
            '<b>html</b>',
1128
            $t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
1129
        );
1130
        $this->assertEquals(
1131
            '&lt;b&gt;html&lt;/b&gt;',
1132
            $t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
1133
        );
1134
1135
        // Uncasted value (falls back to ViewableData::$default_cast="Text")
1136
        $vd = new SSViewerTest\TestViewableData();
1137
        $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...
1138
        $this->assertEquals(
1139
            '&lt;b&gt;html&lt;/b&gt;',
1140
            $t = SSViewer::fromString('$UncastedValue')->process($vd)
1141
        );
1142
        $this->assertEquals(
1143
            '<b>html</b>',
1144
            $t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
1145
        );
1146
        $this->assertEquals(
1147
            '&lt;b&gt;html&lt;/b&gt;',
1148
            $t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
1149
        );
1150
    }
1151
1152
    public function testSSViewerBasicIteratorSupport()
1153
    {
1154
        $data = new ArrayData(
1155
            array(
1156
            'Set' => new ArrayList(
1157
                array(
1158
                new SSViewerTest\TestObject("1"),
1159
                new SSViewerTest\TestObject("2"),
1160
                new SSViewerTest\TestObject("3"),
1161
                new SSViewerTest\TestObject("4"),
1162
                new SSViewerTest\TestObject("5"),
1163
                new SSViewerTest\TestObject("6"),
1164
                new SSViewerTest\TestObject("7"),
1165
                new SSViewerTest\TestObject("8"),
1166
                new SSViewerTest\TestObject("9"),
1167
                new SSViewerTest\TestObject("10"),
1168
                )
1169
            )
1170
            )
1171
        );
1172
1173
        //base test
1174
        $result = $this->render('<% loop Set %>$Number<% end_loop %>', $data);
1175
        $this->assertEquals("12345678910", $result, "Numbers rendered in order");
1176
1177
        //test First
1178
        $result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>', $data);
1179
        $this->assertEquals("1", $result, "Only the first number is rendered");
1180
1181
        //test Last
1182
        $result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>', $data);
1183
        $this->assertEquals("10", $result, "Only the last number is rendered");
1184
1185
        //test Even
1186
        $result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>', $data);
1187
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1188
1189
        //test Even with quotes
1190
        $result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>', $data);
1191
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1192
1193
        //test Even without quotes
1194
        $result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>', $data);
1195
        $this->assertEquals("246810", $result, "Even numbers rendered in order");
1196
1197
        //test Even with zero-based start index
1198
        $result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>', $data);
1199
        $this->assertEquals("13579", $result, "Even (with zero-based index) numbers rendered in order");
1200
1201
        //test Odd
1202
        $result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>', $data);
1203
        $this->assertEquals("13579", $result, "Odd numbers rendered in order");
1204
1205
        //test FirstLast
1206
        $result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>', $data);
1207
        $this->assertEquals("1first10last", $result, "First and last numbers rendered in order");
1208
1209
        //test Middle
1210
        $result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>', $data);
1211
        $this->assertEquals("23456789", $result, "Middle numbers rendered in order");
1212
1213
        //test MiddleString
1214
        $result = $this->render(
1215
            '<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
1216
            . '<% end_loop %>',
1217
            $data
1218
        );
1219
        $this->assertEquals(
1220
            "2middle3middle4middle5middle6middle7middle8middle9middle",
1221
            $result,
1222
            "Middle numbers rendered in order"
1223
        );
1224
1225
        //test EvenOdd
1226
        $result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>', $data);
1227
        $this->assertEquals(
1228
            "oddevenoddevenoddevenoddevenoddeven",
1229
            $result,
1230
            "Even and Odd is returned in sequence numbers rendered in order"
1231
        );
1232
1233
        //test Pos
1234
        $result = $this->render('<% loop Set %>$Pos<% end_loop %>', $data);
1235
        $this->assertEquals("12345678910", $result, '$Pos is rendered in order');
1236
1237
        //test Pos
1238
        $result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>', $data);
1239
        $this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
1240
1241
        //test FromEnd
1242
        $result = $this->render('<% loop Set %>$FromEnd<% end_loop %>', $data);
1243
        $this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
1244
1245
        //test FromEnd
1246
        $result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>', $data);
1247
        $this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
1248
1249
        //test Total
1250
        $result = $this->render('<% loop Set %>$TotalItems<% end_loop %>', $data);
1251
        $this->assertEquals("10101010101010101010", $result, "10 total items X 10 are returned");
1252
1253
        //test Modulus
1254
        $result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>', $data);
1255
        $this->assertEquals("1010101010", $result, "1-indexed pos modular divided by 2 rendered in order");
1256
1257
        //test MultipleOf 3
1258
        $result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>', $data);
1259
        $this->assertEquals("369", $result, "Only numbers that are multiples of 3 are returned");
1260
1261
        //test MultipleOf 4
1262
        $result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>', $data);
1263
        $this->assertEquals("48", $result, "Only numbers that are multiples of 4 are returned");
1264
1265
        //test MultipleOf 5
1266
        $result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>', $data);
1267
        $this->assertEquals("510", $result, "Only numbers that are multiples of 5 are returned");
1268
1269
        //test MultipleOf 10
1270
        $result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>', $data);
1271
        $this->assertEquals("10", $result, "Only numbers that are multiples of 10 (with 1-based indexing) are returned");
1272
1273
        //test MultipleOf 9 zero-based
1274
        $result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>', $data);
1275
        $this->assertEquals(
1276
            "110",
1277
            $result,
1278
            "Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)"
1279
        );
1280
1281
        //test MultipleOf 11
1282
        $result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>', $data);
1283
        $this->assertEquals("", $result, "Only numbers that are multiples of 11 are returned. I.e. nothing returned");
1284
    }
1285
1286
    /**
1287
     * Test $Up works when the scope $Up refers to was entered with a "with" block
1288
     */
1289
    public function testUpInWith()
1290
    {
1291
1292
        // Data to run the loop tests on - three levels deep
1293
        $data = new ArrayData(
1294
            array(
1295
            'Name' => 'Top',
1296
            'Foo' => new ArrayData(
1297
                array(
1298
                'Name' => 'Foo',
1299
                'Bar' => new ArrayData(
1300
                    array(
1301
                    'Name' => 'Bar',
1302
                    'Baz' => new ArrayData(
1303
                        array(
1304
                        'Name' => 'Baz'
1305
                        )
1306
                    ),
1307
                    'Qux' => new ArrayData(
1308
                        array(
1309
                        'Name' => 'Qux'
1310
                        )
1311
                    )
1312
                    )
1313
                )
1314
                )
1315
            )
1316
            )
1317
        );
1318
1319
        // Basic functionality
1320
        $this->assertEquals(
1321
            'BarFoo',
1322
            $this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data)
1323
        );
1324
1325
        // Two level with block, up refers to internally referenced Bar
1326
        $this->assertEquals(
1327
            'BarFoo',
1328
            $this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data)
1329
        );
1330
1331
        // Stepping up & back down the scope tree
1332
        $this->assertEquals(
1333
            'BazBarQux',
1334
            $this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data)
1335
        );
1336
1337
        // Using $Up in a with block
1338
        $this->assertEquals(
1339
            'BazBarQux',
1340
            $this->render(
1341
                '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
1342
                .'<% end_with %>',
1343
                $data
1344
            )
1345
        );
1346
1347
        // Stepping up & back down the scope tree with with blocks
1348
        $this->assertEquals(
1349
            'BazBarQuxBarBaz',
1350
            $this->render(
1351
                '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
1352
                . '{$Name}<% end_with %>{$Name}<% end_with %>',
1353
                $data
1354
            )
1355
        );
1356
1357
        // Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
1358
        $this->assertEquals(
1359
            'Foo',
1360
            $this->render(
1361
                '<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
1362
                . '<% end_with %>',
1363
                $data
1364
            )
1365
        );
1366
1367
        // Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
1368
        $this->assertEquals(
1369
            'Foo',
1370
            $this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data)
1371
        );
1372
    }
1373
1374
    /**
1375
     * Test $Up works when the scope $Up refers to was entered with a "loop" block
1376
     */
1377
    public function testUpInLoop()
1378
    {
1379
1380
        // Data to run the loop tests on - one sequence of three items, each with a subitem
1381
        $data = new ArrayData(
1382
            array(
1383
            'Name' => 'Top',
1384
            'Foo' => new ArrayList(
1385
                array(
1386
                new ArrayData(
1387
                    array(
1388
                    'Name' => '1',
1389
                    'Sub' => new ArrayData(
1390
                        array(
1391
                        'Name' => 'Bar'
1392
                        )
1393
                    )
1394
                    )
1395
                ),
1396
                new ArrayData(
1397
                    array(
1398
                    'Name' => '2',
1399
                    'Sub' => new ArrayData(
1400
                        array(
1401
                        'Name' => 'Baz'
1402
                        )
1403
                    )
1404
                    )
1405
                ),
1406
                new ArrayData(
1407
                    array(
1408
                    'Name' => '3',
1409
                    'Sub' => new ArrayData(
1410
                        array(
1411
                        'Name' => 'Qux'
1412
                        )
1413
                    )
1414
                    )
1415
                )
1416
                )
1417
            )
1418
            )
1419
        );
1420
1421
        // Make sure inside a loop, $Up refers to the current item of the loop
1422
        $this->assertEqualIgnoringWhitespace(
1423
            '111 222 333',
1424
            $this->render(
1425
                '<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1426
                $data
1427
            )
1428
        );
1429
1430
        // Make sure inside a loop, looping over $Up uses a separate iterator,
1431
        // and doesn't interfere with the original iterator
1432
        $this->assertEqualIgnoringWhitespace(
1433
            '1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1434
            $this->render(
1435
                '<% loop $Foo %>
1436
					$Name
1437
					<% with $Sub %>
1438
						$Name
1439
						<% loop $Up %>$Name<% end_loop %>
1440
						$Name
1441
					<% end_with %>
1442
					$Name
1443
				<% end_loop %>',
1444
                $data
1445
            )
1446
        );
1447
1448
        // Make sure inside a loop, looping over $Up uses a separate iterator,
1449
        // and doesn't interfere with the original iterator or local lookups
1450
        $this->assertEqualIgnoringWhitespace(
1451
            '1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1452
            $this->render(
1453
                '<% loop $Foo %>
1454
					$Name
1455
					<% with $Sub %>
1456
						{$Name}{$Up.Name}
1457
						<% loop $Up %>$Name<% end_loop %>
1458
						{$Up.Name}{$Name}
1459
					<% end_with %>
1460
					$Name
1461
				<% end_loop %>',
1462
                $data
1463
            )
1464
        );
1465
    }
1466
1467
    /**
1468
     * Test that nested loops restore the loop variables correctly when pushing and popping states
1469
     */
1470
    public function testNestedLoops()
1471
    {
1472
1473
        // Data to run the loop tests on - one sequence of three items, one with child elements
1474
        // (of a different size to the main sequence)
1475
        $data = new ArrayData(
1476
            array(
1477
            'Foo' => new ArrayList(
1478
                array(
1479
                new ArrayData(
1480
                    array(
1481
                    'Name' => '1',
1482
                    'Children' => new ArrayList(
1483
                        array(
1484
                        new ArrayData(
1485
                            array(
1486
                            'Name' => 'a'
1487
                            )
1488
                        ),
1489
                        new ArrayData(
1490
                            array(
1491
                            'Name' => 'b'
1492
                            )
1493
                        ),
1494
                        )
1495
                    ),
1496
                    )
1497
                ),
1498
                new ArrayData(
1499
                    array(
1500
                    'Name' => '2',
1501
                    'Children' => new ArrayList(),
1502
                    )
1503
                ),
1504
                new ArrayData(
1505
                    array(
1506
                    'Name' => '3',
1507
                    'Children' => new ArrayList(),
1508
                    )
1509
                ),
1510
                )
1511
            ),
1512
            )
1513
        );
1514
1515
        // Make sure that including a loop inside a loop will not destroy the internal count of
1516
        // items, checked by using "Last"
1517
        $this->assertEqualIgnoringWhitespace(
1518
            '1ab23last',
1519
            $this->render(
1520
                '<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1521
                . '<% end_loop %>',
1522
                $data
1523
            )
1524
        );
1525
    }
1526
1527
    public function testLayout()
1528
    {
1529
        $this->useTestTheme(
1530
            __DIR__.'/SSViewerTest',
1531
            'layouttest',
1532
            function () {
1533
                $template = new SSViewer(array('Page'));
1534
                $this->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1535
1536
                $template = new SSViewer(array('Shortcodes', 'Page'));
1537
                $this->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1538
            }
1539
        );
1540
    }
1541
1542
    /**
1543
     * @covers \SilverStripe\View\SSViewer::get_templates_by_class()
1544
     */
1545
    public function testGetTemplatesByClass()
1546
    {
1547
        $this->useTestTheme(
1548
            __DIR__ . '/SSViewerTest',
1549
            'layouttest',
1550
            function () {
1551
                // Test passing a string
1552
                $templates = SSViewer::get_templates_by_class(
1553
                    SSViewerTestModelController::class,
1554
                    '',
1555
                    Controller::class
1556
                );
1557
                $this->assertEquals(
1558
                    [
1559
                        SSViewerTestModelController::class,
1560
                        [
1561
                            'type' => 'Includes',
1562
                            SSViewerTestModelController::class,
1563
                        ],
1564
                        SSViewerTestModel::class,
1565
                        Controller::class,
1566
                        [
1567
                            'type' => 'Includes',
1568
                            Controller::class,
1569
                        ],
1570
                    ],
1571
                    $templates
1572
                );
1573
1574
                // Test to ensure we're stopping at the base class.
1575
                $templates = SSViewer::get_templates_by_class(
1576
                    SSViewerTestModelController::class,
1577
                    '',
1578
                    SSViewerTestModelController::class
1579
                );
1580
                $this->assertEquals(
1581
                    [
1582
                        SSViewerTestModelController::class,
1583
                        [
1584
                            'type' => 'Includes',
1585
                            SSViewerTestModelController::class,
1586
                        ],
1587
                        SSViewerTestModel::class,
1588
                    ],
1589
                    $templates
1590
                );
1591
1592
                // Make sure we can search templates by suffix.
1593
                $templates = SSViewer::get_templates_by_class(
1594
                    SSViewerTestModel::class,
1595
                    'Controller',
1596
                    DataObject::class
1597
                );
1598
                $this->assertEquals(
1599
                    [
1600
                        SSViewerTestModelController::class,
1601
                        [
1602
                            'type' => 'Includes',
1603
                            SSViewerTestModelController::class,
1604
                        ],
1605
                        DataObject::class . 'Controller',
1606
                        [
1607
                            'type' => 'Includes',
1608
                            DataObject::class . 'Controller',
1609
                        ],
1610
                    ],
1611
                    $templates
1612
                );
1613
1614
                // Let's throw something random in there.
1615
                $this->setExpectedException('InvalidArgumentException');
1616
                SSViewer::get_templates_by_class(array());
1617
            }
1618
        );
1619
    }
1620
1621
    public function testRewriteHashlinks()
0 ignored issues
show
Coding Style introduced by
testRewriteHashlinks uses the super-global variable $_SERVER which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1622
    {
1623
        SSViewer::config()->update('rewrite_hash_links', true);
1624
1625
        $_SERVER['HTTP_HOST'] = 'www.mysite.com';
1626
        $_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1627
1628
        // Emulate SSViewer::process()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
1629
        // Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1630
        // as protocol-less absolute urls
1631
        $base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1632
1633
        $tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1634
1635
        // Note: SSViewer_FromString doesn't rewrite hash links.
1636
        file_put_contents(
1637
            $tmplFile,
1638
            '<!DOCTYPE html>
1639
			<html>
1640
				<head><% base_tag %></head>
1641
				<body>
1642
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1643
				$ExternalInsertedLink
1644
				<a class="inline" href="#anchor">InlineLink</a>
1645
				$InsertedLink
1646
				<svg><use xlink:href="#sprite"></use></svg>
1647
				<body>
1648
			</html>'
1649
        );
1650
        $tmpl = new SSViewer($tmplFile);
1651
        $obj = new ViewableData();
1652
        $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...
1653
            'HTMLFragment',
1654
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1655
        );
1656
        $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...
1657
            'HTMLFragment',
1658
            '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>'
1659
        );
1660
        $result = $tmpl->process($obj);
1661
        $this->assertContains(
1662
            '<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1663
            $result
1664
        );
1665
        $this->assertContains(
1666
            '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1667
            $result
1668
        );
1669
        $this->assertContains(
1670
            '<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1671
            $result
1672
        );
1673
        $this->assertContains(
1674
            '<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1675
            $result
1676
        );
1677
        $this->assertContains(
1678
            '<svg><use xlink:href="#sprite"></use></svg>',
1679
            $result,
1680
            'SSTemplateParser should only rewrite anchor hrefs'
1681
        );
1682
1683
        unlink($tmplFile);
1684
    }
1685
1686
    public function testRewriteHashlinksInPhpMode()
1687
    {
1688
        SSViewer::config()->update('rewrite_hash_links', 'php');
1689
1690
        $tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1691
1692
        // Note: SSViewer_FromString doesn't rewrite hash links.
1693
        file_put_contents(
1694
            $tmplFile,
1695
            '<!DOCTYPE html>
1696
			<html>
1697
				<head><% base_tag %></head>
1698
				<body>
1699
				<a class="inline" href="#anchor">InlineLink</a>
1700
				$InsertedLink
1701
				<svg><use xlink:href="#sprite"></use></svg>
1702
				<body>
1703
			</html>'
1704
        );
1705
        $tmpl = new SSViewer($tmplFile);
1706
        $obj = new ViewableData();
1707
        $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...
1708
            'HTMLFragment',
1709
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1710
        );
1711
        $result = $tmpl->process($obj);
1712
1713
        $code = <<<'EOC'
1714
<a class="inserted" href="<?php echo \SilverStripe\Core\Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1715
EOC;
1716
        $this->assertContains($code, $result);
1717
        // TODO Fix inline links in PHP mode
1718
        // $this->assertContains(
1719
        // 	'<a class="inline" href="<?php echo str_replace(',
1720
        // 	$result
1721
        // );
1722
        $this->assertContains(
1723
            '<svg><use xlink:href="#sprite"></use></svg>',
1724
            $result,
1725
            'SSTemplateParser should only rewrite anchor hrefs'
1726
        );
1727
1728
        unlink($tmplFile);
1729
    }
1730
1731
    public function testRenderWithSourceFileComments()
1732
    {
1733
        Director::config()->update('environment_type', 'dev');
1734
        SSViewer::config()->update('source_file_comments', true);
1735
        $i = __DIR__ . '/SSViewerTest/templates/Includes';
1736
        $f = __DIR__ . '/SSViewerTest/templates/SSViewerTestComments';
1737
        $templates = array(
1738
            array(
1739
                'name' => 'SSViewerTestCommentsFullSource',
1740
                'expected' => ""
1741
                    . "<!doctype html>"
1742
                    . "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1743
                    . "<html>"
1744
                    . "\t<head></head>"
1745
                    . "\t<body></body>"
1746
                    . "</html>"
1747
                    . "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1748
            ),
1749
            array(
1750
                'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1751
                'expected' => ""
1752
                    . "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1753
                    . "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1754
                    . "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1755
                    . "<html>"
1756
                    . "\t<head></head>"
1757
                    . "\t<body></body>"
1758
                    . "</html>"
1759
                    . "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1760
            ),
1761
            array(
1762
                'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1763
                'expected' => ""
1764
                    . "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1765
                    . "\t<head></head>"
1766
                    . "\t<body></body>"
1767
                    . "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1768
            ),
1769
            array(
1770
                'name' => 'SSViewerTestCommentsFullSourceIfIE',
1771
                'expected' => ""
1772
                    . "<!doctype html>"
1773
                    . "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1774
                    . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1775
                    . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1776
                    . "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1777
                    . "\t<head></head>"
1778
                    . "\t<body></body>"
1779
                    . "</html>"
1780
                    . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1781
            ),
1782
            array(
1783
                'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1784
                'expected' => ""
1785
                    . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1786
                    . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1787
                    . "<!--[if !IE]><!--> <html class='no-ie'>"
1788
                    . "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1789
                    . " <!--<![endif]-->"
1790
                    . "\t<head></head>"
1791
                    . "\t<body></body>"
1792
                    . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1793
            ),
1794
            array(
1795
                'name' => 'SSViewerTestCommentsPartialSource',
1796
                'expected' =>
1797
                "<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1798
                    . "<div class='typography'></div>"
1799
                    . "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1800
            ),
1801
            array(
1802
                'name' => 'SSViewerTestCommentsWithInclude',
1803
                'expected' =>
1804
                "<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1805
                    . "<div class='typography'>"
1806
                    . "<!-- include 'SSViewerTestCommentsInclude' -->"
1807
                    . "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
1808
                    . "Included"
1809
                    . "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
1810
                    . "<!-- end include 'SSViewerTestCommentsInclude' -->"
1811
                    . "</div>"
1812
                    . "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1813
            ),
1814
        );
1815
        foreach ($templates as $template) {
1816
            $this->_renderWithSourceFileComments('SSViewerTestComments/'.$template['name'], $template['expected']);
1817
        }
1818
    }
1819
    private function _renderWithSourceFileComments($name, $expected)
1820
    {
1821
        $viewer = new SSViewer(array($name));
1822
        $data = new ArrayData(array());
1823
        $result = $viewer->process($data);
1824
        $expected = str_replace(array("\r", "\n"), '', $expected);
1825
        $result = str_replace(array("\r", "\n"), '', $result);
1826
        $this->assertEquals($result, $expected);
1827
    }
1828
1829
    public function testLoopIteratorIterator()
1830
    {
1831
        $list = new PaginatedList(new ArrayList());
1832
        $viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1833
        $result = $viewer->process(new ArrayData(array('List' => $list)));
1834
        $this->assertEquals($result, '');
1835
    }
1836
1837
    public function testProcessOnlyIncludesRequirementsOnce()
1838
    {
1839
        $template = new SSViewer(array('SSViewerTestProcess'));
1840
        $basePath = $this->getCurrentRelativePath() . '/SSViewerTest';
1841
1842
        $backend = Injector::inst()->create(Requirements_Backend::class);
1843
        $backend->setCombinedFilesEnabled(false);
1844
        $backend->combineFiles(
1845
            'RequirementsTest_ab.css',
1846
            array(
1847
                $basePath . '/css/RequirementsTest_a.css',
1848
                $basePath . '/css/RequirementsTest_b.css'
1849
            )
1850
        );
1851
1852
        Requirements::set_backend($backend);
1853
1854
        $this->assertEquals(1, substr_count($template->process(array()), "a.css"));
1855
        $this->assertEquals(1, substr_count($template->process(array()), "b.css"));
1856
1857
        // if we disable the requirements then we should get nothing
1858
        $template->includeRequirements(false);
1859
        $this->assertEquals(0, substr_count($template->process(array()), "a.css"));
1860
        $this->assertEquals(0, substr_count($template->process(array()), "b.css"));
1861
    }
1862
1863
    public function testRequireCallInTemplateInclude()
1864
    {
1865
        //TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1866
        if (FRAMEWORK_DIR === 'framework') {
1867
            $template = new SSViewer(array('SSViewerTestProcess'));
1868
1869
            Requirements::set_suffix_requirements(false);
1870
1871
            $this->assertEquals(
1872
                1,
1873
                substr_count(
1874
                    $template->process(array()),
1875
                    "tests/php/View/SSViewerTest/javascript/RequirementsTest_a.js"
1876
                )
1877
            );
1878
        } else {
1879
            $this->markTestSkipped(
1880
                'Requirement will always fail if the framework dir is not '.
1881
                'named \'framework\', since templates require hard coded paths'
1882
            );
1883
        }
1884
    }
1885
1886
    public function testCallsWithArguments()
1887
    {
1888
        $data = new ArrayData(
1889
            array(
1890
            'Set' => new ArrayList(
1891
                array(
1892
                new SSViewerTest\TestObject("1"),
1893
                new SSViewerTest\TestObject("2"),
1894
                new SSViewerTest\TestObject("3"),
1895
                new SSViewerTest\TestObject("4"),
1896
                new SSViewerTest\TestObject("5"),
1897
                )
1898
            ),
1899
            'Level' => new SSViewerTest\LevelTestData(1),
1900
            'Nest' => array(
1901
                'Level' => new SSViewerTest\LevelTestData(2),
1902
            ),
1903
            )
1904
        );
1905
1906
        $tests = array(
1907
            '$Level.output(1)' => '1-1',
1908
            '$Nest.Level.output($Set.First.Number)' => '2-1',
1909
            '<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1910
            '<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1911
            '<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1912
            '<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1913
            '<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1914
            '<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1915
            '<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1916
            '<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1917
            '<% with $Nest %>
1918
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1919
			<% end_with %>' => '1-hi',
1920
            '<% with $Nest %>
1921
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1922
			<% end_with %>' => '!0!1!2!3!4',
1923
        );
1924
1925
        foreach ($tests as $template => $expected) {
1926
            $this->assertEquals($expected, trim($this->render($template, $data)));
1927
        }
1928
    }
1929
1930
    public function testRepeatedCallsAreCached()
1931
    {
1932
        $data = new SSViewerTest\CacheTestData();
1933
        $template = '
1934
			<% if $TestWithCall %>
1935
				<% with $TestWithCall %>
1936
					{$Message}
1937
				<% end_with %>
1938
1939
				{$TestWithCall.Message}
1940
			<% end_if %>';
1941
1942
        $this->assertEquals('HiHi', preg_replace('/\s+/', '', $this->render($template, $data)));
1943
        $this->assertEquals(
1944
            1,
1945
            $data->testWithCalls,
1946
            'SSViewerTest_CacheTestData::TestWithCall() should only be called once. Subsequent calls should be cached'
1947
        );
1948
1949
        $data = new SSViewerTest\CacheTestData();
1950
        $template = '
1951
			<% if $TestLoopCall %>
1952
				<% loop $TestLoopCall %>
1953
					{$Message}
1954
				<% end_loop %>
1955
			<% end_if %>';
1956
1957
        $this->assertEquals('OneTwo', preg_replace('/\s+/', '', $this->render($template, $data)));
1958
        $this->assertEquals(
1959
            1,
1960
            $data->testLoopCalls,
1961
            'SSViewerTest_CacheTestData::TestLoopCall() should only be called once. Subsequent calls should be cached'
1962
        );
1963
    }
1964
1965
    public function testClosedBlockExtension()
1966
    {
1967
        $count = 0;
1968
        $parser = new SSTemplateParser();
1969
        $parser->addClosedBlock(
1970
            'test',
1971
            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...
1972
                $count++;
1973
            }
1974
        );
1975
1976
        $template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
1977
        $template->process(new SSViewerTest\TestFixture());
1978
1979
        $this->assertEquals(1, $count);
1980
    }
1981
1982
    public function testOpenBlockExtension()
1983
    {
1984
        $count = 0;
1985
        $parser = new SSTemplateParser();
1986
        $parser->addOpenBlock(
1987
            'test',
1988
            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...
1989
                $count++;
1990
            }
1991
        );
1992
1993
        $template = new SSViewer_FromString("<% test %>", $parser);
1994
        $template->process(new SSViewerTest\TestFixture());
1995
1996
        $this->assertEquals(1, $count);
1997
    }
1998
1999
    /**
2000
     * Tests if caching for SSViewer_FromString is working
2001
     */
2002
    public function testFromStringCaching()
2003
    {
2004
        $content = 'Test content';
2005
        $cacheFile = TEMP_FOLDER . '/.cache.' . sha1($content);
2006
        if (file_exists($cacheFile)) {
2007
            unlink($cacheFile);
2008
        }
2009
2010
        // Test global behaviors
2011
        $this->render($content, null, null);
2012
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2013
2014
        SSViewer_FromString::config()->update('cache_template', true);
2015
        $this->render($content, null, null);
2016
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2017
        unlink($cacheFile);
2018
2019
        // Test instance behaviors
2020
        $this->render($content, null, false);
2021
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2022
2023
        $this->render($content, null, true);
2024
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2025
        unlink($cacheFile);
2026
    }
2027
}
2028