Passed
Push — 4.0.4 ( fe4f6f )
by Robbie
09:31
created

SSViewerTest::testObjectDotArguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 18
nc 1
nop 0
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View\Tests;
4
5
use Exception;
6
use InvalidArgumentException;
7
use PHPUnit_Framework_MockObject_MockObject;
8
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Assets\Test...toreTest\TestAssetStore was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1757
        SSViewer::setRewriteHashLinksDefault(/** @scrutinizer ignore-type */ 'php');
Loading history...
1758
1759
        $tmplFile = TEMP_PATH . DIRECTORY_SEPARATOR . 'SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1760
1761
        // Note: SSViewer_FromString doesn't rewrite hash links.
1762
        file_put_contents(
1763
            $tmplFile,
1764
            '<!DOCTYPE html>
1765
			<html>
1766
				<head><% base_tag %></head>
1767
				<body>
1768
				<a class="inline" href="#anchor">InlineLink</a>
1769
				$InsertedLink
1770
				<svg><use xlink:href="#sprite"></use></svg>
1771
				<body>
1772
			</html>'
1773
        );
1774
        $tmpl = new SSViewer($tmplFile);
1775
        $obj = new ViewableData();
1776
        $obj->InsertedLink = DBField::create_field(
0 ignored issues
show
Bug Best Practice introduced by
The property InsertedLink does not exist on SilverStripe\View\ViewableData. Since you implemented __set, consider adding a @property annotation.
Loading history...
1777
            'HTMLFragment',
1778
            '<a class="inserted" href="#anchor">InsertedLink</a>'
1779
        );
1780
        $result = $tmpl->process($obj);
1781
1782
        $code = <<<'EOC'
1783
<a class="inserted" href="<?php echo \SilverStripe\Core\Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1784
EOC;
1785
        $this->assertContains($code, $result);
1786
        // TODO Fix inline links in PHP mode
1787
        // $this->assertContains(
1788
        //  '<a class="inline" href="<?php echo str_replace(',
1789
        //  $result
1790
        // );
1791
        $this->assertContains(
1792
            '<svg><use xlink:href="#sprite"></use></svg>',
1793
            $result,
1794
            'SSTemplateParser should only rewrite anchor hrefs'
1795
        );
1796
1797
        unlink($tmplFile);
1798
    }
1799
1800
    public function testRenderWithSourceFileComments()
1801
    {
1802
        SSViewer::config()->update('source_file_comments', true);
1803
        $i = __DIR__ . '/SSViewerTest/templates/Includes';
1804
        $f = __DIR__ . '/SSViewerTest/templates/SSViewerTestComments';
1805
        $templates = array(
1806
        array(
1807
            'name' => 'SSViewerTestCommentsFullSource',
1808
            'expected' => ""
1809
                . "<!doctype html>"
1810
                . "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1811
                . "<html>"
1812
                . "\t<head></head>"
1813
                . "\t<body></body>"
1814
                . "</html>"
1815
                . "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1816
        ),
1817
        array(
1818
            'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1819
            'expected' => ""
1820
                . "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1821
                . "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1822
                . "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1823
                . "<html>"
1824
                . "\t<head></head>"
1825
                . "\t<body></body>"
1826
                . "</html>"
1827
                . "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1828
        ),
1829
        array(
1830
            'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1831
            'expected' => ""
1832
                . "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1833
                . "\t<head></head>"
1834
                . "\t<body></body>"
1835
                . "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1836
        ),
1837
        array(
1838
            'name' => 'SSViewerTestCommentsFullSourceIfIE',
1839
            'expected' => ""
1840
                . "<!doctype html>"
1841
                . "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1842
                . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1843
                . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1844
                . "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1845
                . "\t<head></head>"
1846
                . "\t<body></body>"
1847
                . "</html>"
1848
                . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1849
        ),
1850
        array(
1851
            'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1852
            'expected' => ""
1853
                . "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1854
                . "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1855
                . "<!--[if !IE]><!--> <html class='no-ie'>"
1856
                . "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1857
                . " <!--<![endif]-->"
1858
                . "\t<head></head>"
1859
                . "\t<body></body>"
1860
                . "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1861
        ),
1862
        array(
1863
            'name' => 'SSViewerTestCommentsPartialSource',
1864
            'expected' =>
1865
            "<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1866
                . "<div class='typography'></div>"
1867
                . "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1868
        ),
1869
        array(
1870
            'name' => 'SSViewerTestCommentsWithInclude',
1871
            'expected' =>
1872
            "<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1873
                . "<div class='typography'>"
1874
                . "<!-- include 'SSViewerTestCommentsInclude' -->"
1875
                . "<!-- template $i/SSViewerTestCommentsInclude.ss -->"
1876
                . "Included"
1877
                . "<!-- end template $i/SSViewerTestCommentsInclude.ss -->"
1878
                . "<!-- end include 'SSViewerTestCommentsInclude' -->"
1879
                . "</div>"
1880
                . "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1881
        ),
1882
        );
1883
        foreach ($templates as $template) {
1884
            $this->_renderWithSourceFileComments('SSViewerTestComments/' . $template['name'], $template['expected']);
1885
        }
1886
    }
1887
    private function _renderWithSourceFileComments($name, $expected)
1888
    {
1889
        $viewer = new SSViewer(array($name));
1890
        $data = new ArrayData(array());
1891
        $result = $viewer->process($data);
1892
        $expected = str_replace(array("\r", "\n"), '', $expected);
1893
        $result = str_replace(array("\r", "\n"), '', $result);
1894
        $this->assertEquals($result, $expected);
1895
    }
1896
1897
    public function testLoopIteratorIterator()
1898
    {
1899
        $list = new PaginatedList(new ArrayList());
1900
        $viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1901
        $result = $viewer->process(new ArrayData(array('List' => $list)));
1902
        $this->assertEquals($result, '');
1903
    }
1904
1905
    public function testProcessOnlyIncludesRequirementsOnce()
1906
    {
1907
        $template = new SSViewer(array('SSViewerTestProcess'));
1908
        $basePath = $this->getCurrentRelativePath() . '/SSViewerTest';
1909
1910
        $backend = Injector::inst()->create(Requirements_Backend::class);
1911
        $backend->setCombinedFilesEnabled(false);
1912
        $backend->combineFiles(
1913
            'RequirementsTest_ab.css',
1914
            array(
1915
            $basePath . '/css/RequirementsTest_a.css',
1916
            $basePath . '/css/RequirementsTest_b.css'
1917
            )
1918
        );
1919
1920
        Requirements::set_backend($backend);
1921
1922
        $this->assertEquals(1, substr_count($template->process(new ViewableData()), "a.css"));
1923
        $this->assertEquals(1, substr_count($template->process(new ViewableData()), "b.css"));
1924
1925
        // if we disable the requirements then we should get nothing
1926
        $template->includeRequirements(false);
1927
        $this->assertEquals(0, substr_count($template->process(new ViewableData()), "a.css"));
1928
        $this->assertEquals(0, substr_count($template->process(new ViewableData()), "b.css"));
1929
    }
1930
1931
    public function testRequireCallInTemplateInclude()
1932
    {
1933
        //TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1934
        if (FRAMEWORK_DIR === 'framework') {
1935
            $template = new SSViewer(array('SSViewerTestProcess'));
1936
1937
            Requirements::set_suffix_requirements(false);
1938
1939
            $this->assertEquals(
1940
                1,
1941
                substr_count(
1942
                    $template->process(new ViewableData()),
1943
                    "tests/php/View/SSViewerTest/javascript/RequirementsTest_a.js"
1944
                )
1945
            );
1946
        } else {
1947
            $this->markTestSkipped(
1948
                'Requirement will always fail if the framework dir is not ' .
1949
                'named \'framework\', since templates require hard coded paths'
1950
            );
1951
        }
1952
    }
1953
1954
    public function testCallsWithArguments()
1955
    {
1956
        $data = new ArrayData(
1957
            array(
1958
            'Set' => new ArrayList(
1959
                array(
1960
                new SSViewerTest\TestObject("1"),
1961
                new SSViewerTest\TestObject("2"),
1962
                new SSViewerTest\TestObject("3"),
1963
                new SSViewerTest\TestObject("4"),
1964
                new SSViewerTest\TestObject("5"),
1965
                )
1966
            ),
1967
            'Level' => new SSViewerTest\LevelTestData(1),
1968
            'Nest' => array(
1969
            'Level' => new SSViewerTest\LevelTestData(2),
1970
            ),
1971
            )
1972
        );
1973
1974
        $tests = array(
1975
        '$Level.output(1)' => '1-1',
1976
        '$Nest.Level.output($Set.First.Number)' => '2-1',
1977
        '<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1978
        '<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1979
        '<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1980
        '<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1981
        '<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1982
        '<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1983
        '<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1984
        '<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1985
        '<% with $Nest %>
1986
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1987
			<% end_with %>' => '1-hi',
1988
        '<% with $Nest %>
1989
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1990
			<% end_with %>' => '!0!1!2!3!4',
1991
        );
1992
1993
        foreach ($tests as $template => $expected) {
1994
            $this->assertEquals($expected, trim($this->render($template, $data)));
1995
        }
1996
    }
1997
1998
    public function testRepeatedCallsAreCached()
1999
    {
2000
        $data = new SSViewerTest\CacheTestData();
2001
        $template = '
2002
			<% if $TestWithCall %>
2003
				<% with $TestWithCall %>
2004
					{$Message}
2005
				<% end_with %>
2006
2007
				{$TestWithCall.Message}
2008
			<% end_if %>';
2009
2010
        $this->assertEquals('HiHi', preg_replace('/\s+/', '', $this->render($template, $data)));
2011
        $this->assertEquals(
2012
            1,
2013
            $data->testWithCalls,
2014
            'SSViewerTest_CacheTestData::TestWithCall() should only be called once. Subsequent calls should be cached'
2015
        );
2016
2017
        $data = new SSViewerTest\CacheTestData();
2018
        $template = '
2019
			<% if $TestLoopCall %>
2020
				<% loop $TestLoopCall %>
2021
					{$Message}
2022
				<% end_loop %>
2023
			<% end_if %>';
2024
2025
        $this->assertEquals('OneTwo', preg_replace('/\s+/', '', $this->render($template, $data)));
2026
        $this->assertEquals(
2027
            1,
2028
            $data->testLoopCalls,
2029
            'SSViewerTest_CacheTestData::TestLoopCall() should only be called once. Subsequent calls should be cached'
2030
        );
2031
    }
2032
2033
    public function testClosedBlockExtension()
2034
    {
2035
        $count = 0;
2036
        $parser = new SSTemplateParser();
2037
        $parser->addClosedBlock(
2038
            'test',
2039
            function ($res) use (&$count) {
0 ignored issues
show
Unused Code introduced by
The parameter $res is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

2039
            function (/** @scrutinizer ignore-unused */ $res) use (&$count) {

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

Loading history...
2040
                $count++;
2041
            }
2042
        );
2043
2044
        $template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
2045
        $template->process(new SSViewerTest\TestFixture());
2046
2047
        $this->assertEquals(1, $count);
2048
    }
2049
2050
    public function testOpenBlockExtension()
2051
    {
2052
        $count = 0;
2053
        $parser = new SSTemplateParser();
2054
        $parser->addOpenBlock(
2055
            'test',
2056
            function ($res) use (&$count) {
0 ignored issues
show
Unused Code introduced by
The parameter $res is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

2056
            function (/** @scrutinizer ignore-unused */ $res) use (&$count) {

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

Loading history...
2057
                $count++;
2058
            }
2059
        );
2060
2061
        $template = new SSViewer_FromString("<% test %>", $parser);
2062
        $template->process(new SSViewerTest\TestFixture());
2063
2064
        $this->assertEquals(1, $count);
2065
    }
2066
2067
    /**
2068
     * Tests if caching for SSViewer_FromString is working
2069
     */
2070
    public function testFromStringCaching()
2071
    {
2072
        $content = 'Test content';
2073
        $cacheFile = TEMP_PATH . DIRECTORY_SEPARATOR . '.cache.' . sha1($content);
2074
        if (file_exists($cacheFile)) {
2075
            unlink($cacheFile);
2076
        }
2077
2078
        // Test global behaviors
2079
        $this->render($content, null, null);
2080
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2081
2082
        SSViewer_FromString::config()->update('cache_template', true);
2083
        $this->render($content, null, null);
2084
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2085
        unlink($cacheFile);
2086
2087
        // Test instance behaviors
2088
        $this->render($content, null, false);
2089
        $this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
2090
2091
        $this->render($content, null, true);
2092
        $this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
2093
        unlink($cacheFile);
2094
    }
2095
}
2096