Completed
Push — master ( 4ad6bd...3873e4 )
by Ingo
11:53
created

SSViewerTest::testRequirementsCombine()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 0
dl 0
loc 26
rs 8.8571
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;
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
class SSViewerTest extends SapphireTest
36
{
37
38
    /**
39
     * Backup of $_SERVER global
40
     *
41
     * @var array
42
     */
43
    protected $oldServer = array();
44
45
    protected static $extra_dataobjects = array(
46
        SSViewerTest\TestObject::class,
47
    );
48
49
    protected 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...
50
    {
51
        parent::setUp();
52
        SSViewer::config()->update('source_file_comments', false);
53
        SSViewer_FromString::config()->update('cache_template', false);
54
        TestAssetStore::activate('SSViewerTest');
55
        $this->oldServer = $_SERVER;
56
    }
57
58
    protected 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...
59
    {
60
        $_SERVER = $this->oldServer;
61
        TestAssetStore::reset();
62
        parent::tearDown();
63
    }
64
65
    /**
66
     * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
67
     * of user defined themes via {@link SiteConfig} and default theme
68
     * when no user themes are defined.
69
     */
70
    public function testCurrentTheme()
71
    {
72
        SSViewer::config()->update('theme', 'mytheme');
73
        $this->assertEquals(
74
            'mytheme',
75
            SSViewer::config()->uninherited('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([ 'Var' => 'var value' ]);
86
        $result = $data->renderWith("SSViewerTestPartialTemplate");
87
        $this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U", '', $result)));
88
    }
89
90
    public function testIncludeScopeInheritance()
91
    {
92
        $data = $this->getScopeInheritanceTestData();
93
        $expected = array(
94
        'Item 1 - First-ODD top:Item 1',
95
        'Item 2 - EVEN top:Item 2',
96
        'Item 3 - ODD top:Item 3',
97
        'Item 4 - EVEN top:Item 4',
98
        'Item 5 - ODD top:Item 5',
99
        'Item 6 - Last-EVEN top:Item 6',
100
        );
101
102
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
103
        $this->assertExpectedStrings($result, $expected);
104
105
        // reset results for the tests that include arguments (the title is passed as an arg)
106
        $expected = array(
107
        'Item 1 _ Item 1 - First-ODD top:Item 1',
108
        'Item 2 _ Item 2 - EVEN top:Item 2',
109
        'Item 3 _ Item 3 - ODD top:Item 3',
110
        'Item 4 _ Item 4 - EVEN top:Item 4',
111
        'Item 5 _ Item 5 - ODD top:Item 5',
112
        'Item 6 _ Item 6 - Last-EVEN top:Item 6',
113
        );
114
115
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
116
        $this->assertExpectedStrings($result, $expected);
117
    }
118
119
    public function testIncludeTruthyness()
120
    {
121
        $data = new ArrayData([
122
            'Title' => 'TruthyTest',
123
            'Items' => new ArrayList([
124
                new ArrayData(['Title' => 'Item 1']),
125
                new ArrayData(['Title' => '']),
126
                new ArrayData(['Title' => true]),
127
                new ArrayData(['Title' => false]),
128
                new ArrayData(['Title' => null]),
129
                new ArrayData(['Title' => 0]),
130
                new ArrayData(['Title' => 7])
131
            ])
132
        ]);
133
        $result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
134
135
        // We should not end up with empty values appearing as empty
136
        $expected = [
137
            'Item 1 _ Item 1 - First-ODD top:Item 1',
138
            'Untitled - EVEN top:',
139
            '1 _ 1 - ODD top:1',
140
            'Untitled - EVEN top:',
141
            'Untitled - ODD top:',
142
            'Untitled - EVEN top:0',
143
            '7 _ 7 - Last-ODD top:7',
144
        ];
145
        $this->assertExpectedStrings($result, $expected);
146
    }
147
148
    private function getScopeInheritanceTestData()
149
    {
150
        return new ArrayData([
151
            'Title' => 'TopTitleValue',
152
            'Items' => new ArrayList([
153
                new ArrayData(['Title' => 'Item 1']),
154
                new ArrayData(['Title' => 'Item 2']),
155
                new ArrayData(['Title' => 'Item 3']),
156
                new ArrayData(['Title' => 'Item 4']),
157
                new ArrayData(['Title' => 'Item 5']),
158
                new ArrayData(['Title' => 'Item 6'])
159
            ])
160
        ]);
161
    }
162
163
    private function assertExpectedStrings($result, $expected)
164
    {
165
        foreach ($expected as $expectedStr) {
166
            $this->assertTrue(
167
                (boolean) preg_match("/{$expectedStr}/", $result),
168
                "Didn't find '{$expectedStr}' in:\n{$result}"
169
            );
170
        }
171
    }
172
173
    /**
174
     * Small helper to render templates from strings
175
     *
176
     * @param  string $templateString
177
     * @param  null   $data
178
     * @param  bool   $cacheTemplate
179
     * @return string
180
     */
181
    public function render($templateString, $data = null, $cacheTemplate = false)
182
    {
183
        $t = SSViewer::fromString($templateString, $cacheTemplate);
184
        if (!$data) {
185
            $data = new SSViewerTest\TestFixture();
186
        }
187
        return trim(''.$t->process($data));
188
    }
189
190
    public function testRequirements()
191
    {
192
        /** @var Requirements_Backend|PHPUnit_Framework_MockObject_MockObject $requirements */
193
        $requirements = $this
194
            ->getMockBuilder(Requirements_Backend::class)
195
            ->setMethods(array("javascript", "css"))
196
            ->getMock();
197
        $jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
198
        $cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
199
200
        $requirements->expects($this->once())->method('javascript')->with($jsFile);
0 ignored issues
show
Bug introduced by
The method expects does only exist in PHPUnit_Framework_MockObject_MockObject, but not in SilverStripe\View\Requirements_Backend.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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