Passed
Push — consistent-limit ( 2d3f1d...474034 )
by Sam
05:57
created

ControllerTest::testUndefinedActions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Control\Tests;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\Tests\ControllerTest\AccessBaseController;
8
use SilverStripe\Control\Tests\ControllerTest\AccessSecuredController;
9
use SilverStripe\Control\Tests\ControllerTest\AccessWildcardSecuredController;
10
use SilverStripe\Control\Tests\ControllerTest\ContainerController;
11
use SilverStripe\Control\Tests\ControllerTest\HasAction;
12
use SilverStripe\Control\Tests\ControllerTest\HasAction_Unsecured;
13
use SilverStripe\Control\Tests\ControllerTest\IndexSecuredController;
14
use SilverStripe\Control\Tests\ControllerTest\SubController;
15
use SilverStripe\Control\Tests\ControllerTest\TestController;
16
use SilverStripe\Control\Tests\ControllerTest\UnsecuredController;
17
use SilverStripe\Dev\FunctionalTest;
18
use SilverStripe\Security\Member;
19
use SilverStripe\View\SSViewer;
20
21
class ControllerTest extends FunctionalTest
22
{
23
24
    protected static $fixture_file = 'ControllerTest.yml';
25
26
    protected $autoFollowRedirection = false;
27
28
    protected $depSettings = null;
29
30
    protected static $extra_controllers = [
31
        AccessBaseController::class,
32
        AccessSecuredController::class,
33
        AccessWildcardSecuredController::class,
34
        ContainerController::class,
35
        HasAction::class,
36
        HasAction_Unsecured::class,
37
        IndexSecuredController::class,
38
        SubController::class,
39
        TestController::class,
40
        UnsecuredController::class,
41
    ];
42
43
    protected function setUp()
44
    {
45
        parent::setUp();
46
        Director::config()->update('alternate_base_url', '/');
47
48
        // Add test theme
49
        $themeDir = substr(__DIR__, strlen(FRAMEWORK_DIR)) . '/ControllerTest/';
50
        $themes = [
51
            "silverstripe/framework:{$themeDir}",
52
            SSViewer::DEFAULT_THEME,
53
        ];
54
        SSViewer::set_themes($themes);
55
    }
56
57
    public function testDefaultAction()
58
    {
59
        /* For a controller with a template, the default action will simple run that template. */
60
        $response = $this->get("TestController/");
61
        $this->assertContains("This is the main template. Content is 'default content'", $response->getBody());
62
    }
63
64
    public function testMethodActions()
65
    {
66
        /* The Action can refer to a method that is called on the object.  If a method returns an array, then it
67
        * will be used to customise the template data */
68
        $response = $this->get("TestController/methodaction");
69
        $this->assertContains("This is the main template. Content is 'methodaction content'.", $response->getBody());
70
71
        /* If the method just returns a string, then that will be used as the response */
72
        $response = $this->get("TestController/stringaction");
73
        $this->assertContains("stringaction was called.", $response->getBody());
74
    }
75
76
    public function testTemplateActions()
77
    {
78
        /* If there is no method, it can be used to point to an alternative template. */
79
        $response = $this->get("TestController/templateaction");
80
        $this->assertContains(
81
            "This is the template for templateaction. Content is 'default content'.",
82
            $response->getBody()
83
        );
84
    }
85
86
    public function testUndefinedActions()
87
    {
88
        $response = $this->get('IndexSecuredController/undefinedaction');
89
        $this->assertInstanceOf('SilverStripe\\Control\\HTTPResponse', $response);
90
        $this->assertEquals(404, $response->getStatusCode(), 'Undefined actions return a not found response.');
91
    }
92
93
    public function testAllowedActions()
94
    {
95
        $adminUser = $this->objFromFixture(Member::class, 'admin');
96
97
        $response = $this->get("UnsecuredController/");
98
        $this->assertEquals(
99
            200,
100
            $response->getStatusCode(),
101
            'Access granted on index action without $allowed_actions on defining controller, ' . 'when called without an action in the URL'
102
        );
103
104
        $response = $this->get("UnsecuredController/index");
105
        $this->assertEquals(
106
            200,
107
            $response->getStatusCode(),
108
            'Access denied on index action without $allowed_actions on defining controller, ' . 'when called with an action in the URL'
109
        );
110
111
        $response = $this->get("UnsecuredController/method1");
112
        $this->assertEquals(
113
            403,
114
            $response->getStatusCode(),
115
            'Access denied on action without $allowed_actions on defining controller, ' . 'when called without an action in the URL'
116
        );
117
118
        $response = $this->get("AccessBaseController/");
119
        $this->assertEquals(
120
            200,
121
            $response->getStatusCode(),
122
            'Access granted on index with empty $allowed_actions on defining controller, ' . 'when called without an action in the URL'
123
        );
124
125
        $response = $this->get("AccessBaseController/index");
126
        $this->assertEquals(
127
            200,
128
            $response->getStatusCode(),
129
            'Access granted on index with empty $allowed_actions on defining controller, ' . 'when called with an action in the URL'
130
        );
131
132
        $response = $this->get("AccessBaseController/method1");
133
        $this->assertEquals(
134
            403,
135
            $response->getStatusCode(),
136
            'Access denied on action with empty $allowed_actions on defining controller'
137
        );
138
139
        $response = $this->get("AccessBaseController/method2");
140
        $this->assertEquals(
141
            403,
142
            $response->getStatusCode(),
143
            'Access denied on action with empty $allowed_actions on defining controller, ' . 'even when action is allowed in subclasses (allowed_actions don\'t inherit)'
144
        );
145
146
        $response = $this->get("AccessSecuredController/");
147
        $this->assertEquals(
148
            200,
149
            $response->getStatusCode(),
150
            'Access granted on index with non-empty $allowed_actions on defining controller, ' . 'even when index isn\'t specifically mentioned in there'
151
        );
152
153
        $response = $this->get("AccessSecuredController/method1");
154
        $this->assertEquals(
155
            403,
156
            $response->getStatusCode(),
157
            'Access denied on action which is only defined in parent controller, ' . 'even when action is allowed in currently called class (allowed_actions don\'t inherit)'
158
        );
159
160
        $response = $this->get("AccessSecuredController/method2");
161
        $this->assertEquals(
162
            200,
163
            $response->getStatusCode(),
164
            'Access granted on action originally defined with empty $allowed_actions on parent controller, ' . 'because it has been redefined in the subclass'
165
        );
166
167
        $response = $this->get("AccessSecuredController/templateaction");
168
        $this->assertEquals(
169
            403,
170
            $response->getStatusCode(),
171
            'Access denied on action with $allowed_actions on defining controller, ' . 'if action is not a method but rather a template discovered by naming convention'
172
        );
173
174
        $response = $this->get("AccessSecuredController/templateaction");
175
        $this->assertEquals(
176
            403,
177
            $response->getStatusCode(),
178
            'Access denied on action with $allowed_actions on defining controller, ' . 'if action is not a method but rather a template discovered by naming convention'
179
        );
180
181
        Member::actAs($adminUser, function () {
182
            $response = $this->get("AccessSecuredController/templateaction");
183
            $this->assertEquals(
184
                200,
185
                $response->getStatusCode(),
186
                'Access granted for logged in admin on action with $allowed_actions on defining controller, ' . 'if action is not a method but rather a template discovered by naming convention'
187
            );
188
        });
189
190
        $response = $this->get("AccessSecuredController/adminonly");
191
        $this->assertEquals(
192
            403,
193
            $response->getStatusCode(),
194
            'Access denied on action with $allowed_actions on defining controller, ' . 'when restricted by unmatched permission code'
195
        );
196
197
        $response = $this->get("AccessSecuredController/aDmiNOnlY");
198
        $this->assertEquals(
199
            403,
200
            $response->getStatusCode(),
201
            'Access denied on action with $allowed_actions on defining controller, ' . 'regardless of capitalization'
202
        );
203
204
        $response = $this->get('AccessSecuredController/protectedmethod');
205
        $this->assertEquals(
206
            404,
207
            $response->getStatusCode(),
208
            "Access denied to protected method even if its listed in allowed_actions"
209
        );
210
211
        Member::actAs($adminUser, function () {
212
            $response = $this->get("AccessSecuredController/adminonly");
213
            $this->assertEquals(
214
                200,
215
                $response->getStatusCode(),
216
                "Permission codes are respected when set in \$allowed_actions"
217
            );
218
        });
219
220
        $response = $this->get('AccessBaseController/extensionmethod1');
221
        $this->assertEquals(
222
            200,
223
            $response->getStatusCode(),
224
            "Access granted to method defined in allowed_actions on extension, " . "where method is also defined on extension"
225
        );
226
227
        $response = $this->get('AccessSecuredController/extensionmethod1');
228
        $this->assertEquals(
229
            200,
230
            $response->getStatusCode(),
231
            "Access granted to method defined in allowed_actions on extension, " . "where method is also defined on extension, even when called in a subclass"
232
        );
233
234
        $response = $this->get('AccessBaseController/extensionmethod2');
235
        $this->assertEquals(
236
            404,
237
            $response->getStatusCode(),
238
            "Access denied to method not defined in allowed_actions on extension, " . "where method is also defined on extension"
239
        );
240
241
        $response = $this->get('IndexSecuredController/');
242
        $this->assertEquals(
243
            403,
244
            $response->getStatusCode(),
245
            "Access denied when index action is limited through allowed_actions, " . "and doesn't satisfy checks, and action is empty"
246
        );
247
248
        $response = $this->get('IndexSecuredController/index');
249
        $this->assertEquals(
250
            403,
251
            $response->getStatusCode(),
252
            "Access denied when index action is limited through allowed_actions, " . "and doesn't satisfy checks"
253
        );
254
255
        Member::actAs($adminUser, function () {
256
            $response = $this->get('IndexSecuredController/');
257
            $this->assertEquals(
258
                200,
259
                $response->getStatusCode(),
260
                "Access granted when index action is limited through allowed_actions, " . "and does satisfy checks"
261
            );
262
        });
263
    }
264
265
    /**
266
     * @expectedException \InvalidArgumentException
267
     * @expectedExceptionMessage Invalid allowed_action '*'
268
     */
269
    public function testWildcardAllowedActions()
270
    {
271
        $this->get('AccessWildcardSecuredController');
272
    }
273
274
    /**
275
     * Test Controller::join_links()
276
     */
277
    public function testJoinLinks()
278
    {
279
        /* Controller::join_links() will reliably join two URL-segments together so that they will be
280
        * appropriately parsed by the URL parser */
281
        $this->assertEquals("admin/crm/MyForm", Controller::join_links("admin/crm", "MyForm"));
282
        $this->assertEquals("admin/crm/MyForm", Controller::join_links("admin/crm/", "MyForm"));
283
284
        /* It will also handle appropriate combination of querystring variables */
285
        $this->assertEquals("admin/crm/MyForm?flush=1", Controller::join_links("admin/crm/?flush=1", "MyForm"));
286
        $this->assertEquals("admin/crm/MyForm?flush=1", Controller::join_links("admin/crm/", "MyForm?flush=1"));
287
        $this->assertEquals(
288
            "admin/crm/MyForm?field=1&other=1",
289
            Controller::join_links("admin/crm/?field=1", "MyForm?other=1")
290
        );
291
292
        /* It can handle arbitrary numbers of components, and will ignore empty ones */
293
        $this->assertEquals("admin/crm/MyForm/", Controller::join_links("admin/", "crm", "", "MyForm/"));
294
        $this->assertEquals(
295
            "admin/crm/MyForm/?a=1&b=2",
296
            Controller::join_links("admin/?a=1", "crm", "", "MyForm/?b=2")
297
        );
298
299
        /* It can also be used to attach additional get variables to a link */
300
        $this->assertEquals("admin/crm?flush=1", Controller::join_links("admin/crm", "?flush=1"));
301
        $this->assertEquals("admin/crm?existing=1&flush=1", Controller::join_links("admin/crm?existing=1", "?flush=1"));
302
        $this->assertEquals(
303
            "admin/crm/MyForm?a=1&b=2&c=3",
304
            Controller::join_links("?a=1", "admin/crm", "?b=2", "MyForm?c=3")
305
        );
306
307
        // And duplicates are handled nicely
308
        $this->assertEquals(
309
            "admin/crm?foo=2&bar=3&baz=1",
310
            Controller::join_links("admin/crm?foo=1&bar=1&baz=1", "?foo=2&bar=3")
311
        );
312
313
        $this->assertEquals(
314
            'admin/action',
315
            Controller::join_links('admin/', '/', '/action'),
316
            'Test that multiple slashes are trimmed.'
317
        );
318
319
        $this->assertEquals('/admin/action', Controller::join_links('/admin', 'action'));
320
321
        /* One fragment identifier is handled as you would expect */
322
        $this->assertEquals("my-page?arg=var#subsection", Controller::join_links("my-page#subsection", "?arg=var"));
323
324
        /* If there are multiple, it takes the last one */
325
        $this->assertEquals(
326
            "my-page?arg=var#second-section",
327
            Controller::join_links("my-page#subsection", "?arg=var", "#second-section")
328
        );
329
330
        /* Does type-safe checks for zero value */
331
        $this->assertEquals("my-page/0", Controller::join_links("my-page", 0));
332
333
        // Test array args
334
        $this->assertEquals(
335
            "admin/crm/MyForm?a=1&b=2&c=3",
336
            Controller::join_links(["?a=1", "admin/crm", "?b=2", "MyForm?c=3"])
337
        );
338
    }
339
340
    public function testLink()
341
    {
342
        $controller = new HasAction();
343
        $this->assertEquals('HasAction/', $controller->Link());
344
        $this->assertEquals('HasAction/', $controller->Link(null));
345
        $this->assertEquals('HasAction/', $controller->Link(false));
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $action of SilverStripe\Control\RequestHandler::Link(). ( Ignorable by Annotation )

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

345
        $this->assertEquals('HasAction/', $controller->Link(/** @scrutinizer ignore-type */ false));
Loading history...
346
        $this->assertEquals('HasAction/allowed-action/', $controller->Link('allowed-action'));
347
    }
348
349
    /**
350
     * @covers \SilverStripe\Control\Controller::hasAction
351
     */
352
    public function testHasAction()
353
    {
354
        $controller = new HasAction();
355
        $unsecuredController = new HasAction_Unsecured();
356
        $securedController = new AccessSecuredController();
357
358
        $this->assertFalse(
359
            $controller->hasAction('1'),
360
            'Numeric actions do not slip through.'
361
        );
362
        //$this->assertFalse(
363
        //  $controller->hasAction('lowercase_permission'),
364
        //  'Lowercase permission does not slip through.'
365
        //);
366
        $this->assertFalse(
367
            $controller->hasAction('undefined'),
368
            'undefined actions do not exist'
369
        );
370
        $this->assertTrue(
371
            $controller->hasAction('allowed_action'),
372
            'allowed actions are recognised'
373
        );
374
        $this->assertTrue(
375
            $controller->hasAction('template_action'),
376
            'action-specific templates are recognised'
377
        );
378
379
        $this->assertTrue(
380
            $unsecuredController->hasAction('defined_action'),
381
            'Without an allowed_actions, any defined methods are recognised as actions'
382
        );
383
384
        $this->assertTrue(
385
            $securedController->hasAction('adminonly'),
386
            'Method is generally visible even if its denied via allowed_actions'
387
        );
388
389
        $this->assertFalse(
390
            $securedController->hasAction('protectedmethod'),
391
            'Method is not visible when protected, even if its defined in allowed_actions'
392
        );
393
394
        $this->assertTrue(
395
            $securedController->hasAction('extensionmethod1'),
396
            'Method is visible when defined on an extension and part of allowed_actions'
397
        );
398
399
        $this->assertFalse(
400
            $securedController->hasAction('internalextensionmethod'),
401
            'Method is not visible when defined on an extension, but not part of allowed_actions'
402
        );
403
404
        $this->assertFalse(
405
            $securedController->hasAction('protectedextensionmethod'),
406
            'Method is not visible when defined on an extension, part of allowed_actions, ' . 'but with protected visibility'
407
        );
408
    }
409
410
    public function testRedirectBackByReferer()
411
    {
412
        $internalRelativeUrl = Controller::join_links(Director::baseURL(), '/some-url');
413
        $internalAbsoluteUrl = Controller::join_links(Director::absoluteBaseURL(), '/some-url');
414
415
        $response = $this->get(
416
            'TestController/redirectbacktest',
417
            null,
418
            array('Referer' => $internalRelativeUrl)
419
        );
420
        $this->assertEquals(302, $response->getStatusCode());
421
        $this->assertEquals(
422
            $internalAbsoluteUrl,
423
            $response->getHeader('Location'),
424
            "Redirects on internal relative URLs"
425
        );
426
427
        $response = $this->get(
428
            'TestController/redirectbacktest',
429
            null,
430
            array('Referer' => $internalAbsoluteUrl)
431
        );
432
        $this->assertEquals(302, $response->getStatusCode());
433
        $this->assertEquals(
434
            $internalAbsoluteUrl,
435
            $response->getHeader('Location'),
436
            "Redirects on internal absolute URLs"
437
        );
438
439
        $externalAbsoluteUrl = 'http://myhost.com/some-url';
440
        $response = $this->get(
441
            'TestController/redirectbacktest',
442
            null,
443
            array('Referer' => $externalAbsoluteUrl)
444
        );
445
        $this->assertEquals(
446
            Director::absoluteBaseURL(),
447
            $response->getHeader('Location'),
448
            "Redirects back to home page on external url"
449
        );
450
    }
451
452
    public function testRedirectBackByBackUrl()
453
    {
454
        $internalRelativeUrl = Controller::join_links(Director::baseURL(), '/some-url');
455
        $internalAbsoluteUrl = Controller::join_links(Director::absoluteBaseURL(), '/some-url');
456
457
        $response = $this->get('TestController/redirectbacktest?BackURL=' . urlencode($internalRelativeUrl));
458
        $this->assertEquals(302, $response->getStatusCode());
459
        $this->assertEquals(
460
            $internalAbsoluteUrl,
461
            $response->getHeader('Location'),
462
            "Redirects on internal relative URLs"
463
        );
464
465
        // BackURL is internal link
466
        $internalAbsoluteUrl = Director::absoluteBaseURL() . '/some-url';
467
        $link = 'TestController/redirectbacktest?BackURL=' . urlencode($internalAbsoluteUrl);
468
        $response = $this->get($link);
469
        $this->assertEquals($internalAbsoluteUrl, $response->getHeader('Location'));
470
        $this->assertEquals(
471
            302,
472
            $response->getStatusCode(),
473
            "Redirects on internal absolute URLs"
474
        );
475
476
        // Note that this test is affected by the prior ->get()
477
        $externalAbsoluteUrl = 'http://myhost.com/some-url';
478
        $response = $this->get('TestController/redirectbacktest?BackURL=' . urlencode($externalAbsoluteUrl));
479
        $this->assertEquals(
480
            Director::absoluteURL($link),
481
            $response->getHeader('Location'),
482
            "If BackURL Is external link, fall back to last url (Referer)"
483
        );
484
    }
485
486
    public function testSubActions()
487
    {
488
        // If a controller action returns another controller, ensure that the $action variable is correctly forwarded
489
        $response = $this->get("ContainerController/subcontroller/subaction");
490
        $this->assertEquals('subaction', $response->getBody());
491
492
        // Handle nested action
493
        $response = $this->get('ContainerController/subcontroller/substring/subvieweraction');
494
        $this->assertEquals('Hope this works', $response->getBody());
495
    }
496
}
497