Completed
Push — master ( deca00...5388ff )
by Sam
24s
created

RequestHandlingTest::testBaseUrlPrefixed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 0
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Control\Tests;
4
5
use SilverStripe\Admin\LeftAndMain;
6
use SilverStripe\CMS\Controllers\ErrorPageControllerExtension;
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Control\Tests\RequestHandlingTest\AllowedController;
9
use SilverStripe\Control\Tests\RequestHandlingTest\ControllerFormWithAllowedActions;
10
use SilverStripe\Control\Tests\RequestHandlingTest\FieldController;
11
use SilverStripe\Control\Tests\RequestHandlingTest\FormActionController;
12
use SilverStripe\Control\Tests\RequestHandlingTest\TestController;
13
use SilverStripe\Dev\FunctionalTest;
14
use SilverStripe\Control\RequestHandler;
15
use SilverStripe\Control\Director;
16
use SilverStripe\Forms\Form;
17
use SilverStripe\Security\SecurityToken;
18
19
/**
20
 * Tests for RequestHandler and HTTPRequest.
21
 * We've set up a simple URL handling model based on
22
 */
23
class RequestHandlingTest extends FunctionalTest
24
{
25
    protected static $fixture_file = null;
26
27
    protected $illegalExtensions = array(
28
        // Suppress CMS error page handling
29
        Controller::class => array(
30
            ErrorPageControllerExtension::class,
31
        ),
32
        Form::class => array(
33
            ErrorPageControllerExtension::class,
34
        ),
35
        LeftAndMain::class => array(
36
            ErrorPageControllerExtension::class,
37
        ),
38
    );
39
40
    protected $extraControllers = [
41
        TestController::class,
42
        AllowedController::class,
43
        ControllerFormWithAllowedActions::class,
44
        FieldController::class,
45
        FormActionController::class
46
    ];
47
48
    public function getExtraRoutes()
49
    {
50
        $routes = parent::getExtraRoutes();
51
        return array_merge(
52
            $routes,
53
            [
54
                // If we don't request any variables, then the whole URL will get shifted off.
55
                // This is fine, but it means that the controller will have to parse the Action from the URL itself.
56
                'testGoodBase1' => TestController::class,
57
58
                // The double-slash indicates how much of the URL should be shifted off the stack.
59
                // This is important for dealing with nested request handlers appropriately.
60
                'testGoodBase2//$Action/$ID/$OtherID' => TestController::class,
61
62
                // By default, the entire URL will be shifted off. This creates a bit of
63
                // backward-incompatability, but makes the URL rules much more explicit.
64
                'testBadBase/$Action/$ID/$OtherID' => TestController::class,
65
66
                // Rules with an extension always default to the index() action
67
                'testBaseWithExtension/virtualfile.xml' => TestController::class,
68
69
                // Without the extension, the methodname should be matched
70
                'testBaseWithExtension//$Action/$ID/$OtherID' => TestController::class,
71
72
                // Test nested base
73
                'testParentBase/testChildBase//$Action/$ID/$OtherID' => TestController::class,
74
            ]
75
        );
76
    }
77
78
    public function testConstructedWithNullRequest()
79
    {
80
        $r = new RequestHandler();
81
        $this->assertInstanceOf('SilverStripe\\Control\\NullHTTPRequest', $r->getRequest());
82
    }
83
84
    public function testRequestHandlerChainingAllParams()
85
    {
86
        $this->markTestIncomplete();
87
    }
88
89
    public function testMethodCallingOnController()
90
    {
91
        /* Calling a controller works just like it always has */
92
        $response = Director::test("testGoodBase1");
93
        $this->assertEquals("This is the controller", $response->getBody());
94
95
        /* ID and OtherID are extracted from the URL and passed in $request->params. */
96
        $response = Director::test("testGoodBase1/method/1/2");
97
        $this->assertEquals("This is a method on the controller: 1, 2", $response->getBody());
98
99
        /* In addition, these values are availalbe in $controller->urlParams.  This is mainly for backward
100
        * compatability. */
101
        $response = Director::test("testGoodBase1/legacymethod/3/4");
102
        $this->assertEquals("\$this->urlParams can be used, for backward compatibility: 3, 4", $response->getBody());
103
    }
104
105
    public function testPostRequests()
106
    {
107
        /* The HTTP Request handler can trigger special behaviour for GET and POST. */
108
        $response = Director::test("testGoodBase1/TestForm", array("MyField" => 3), null, "POST");
109
        $this->assertEquals("Form posted", $response->getBody());
110
111
        $response = Director::test("testGoodBase1/TestForm");
112
        $this->assertEquals("Get request on form", $response->getBody());
113
    }
114
115
    public function testRequestHandlerChaining()
116
    {
117
        /* Request handlers can be chained, from Director to Controller to Form to FormField.  Here, we can make a get
118
        request on a FormField. */
119
        $response = Director::test("testGoodBase1/TestForm/fields/MyField");
120
        $this->assertEquals("MyField requested", $response->getBody());
121
122
        /* We can also make a POST request on a form field, which could be used for in-place editing, for example. */
123
        $response = Director::test("testGoodBase1/TestForm/fields/MyField", array("MyField" => 5));
124
        $this->assertEquals("MyField posted, update to 5", $response->getBody());
125
    }
126
127
    public function testBaseUrlPrefixed()
128
    {
129
        $this->withBaseFolder(
130
            '/silverstripe',
131
            function ($test) {
0 ignored issues
show
Unused Code introduced by
The parameter $test 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...
132
                $this->assertEquals(
133
                    'MyField requested',
134
                    Director::test('/silverstripe/testGoodBase1/TestForm/fields/MyField')->getBody()
135
                );
136
137
                $this->assertEquals(
138
                    'MyField posted, update to 5',
139
                    Director::test('/silverstripe/testGoodBase1/TestForm/fields/MyField', array('MyField' => 5))->getBody()
140
                );
141
            }
142
        );
143
    }
144
145
    public function testBadBase()
146
    {
147
        /* We no longer support using hacky attempting to handle URL parsing with broken rules */
148
        $response = Director::test("testBadBase/method/1/2");
149
        $this->assertNotEquals("This is a method on the controller: 1, 2", $response->getBody());
150
151
        $response = Director::test("testBadBase/TestForm", array("MyField" => 3), null, "POST");
152
        $this->assertNotEquals("Form posted", $response->getBody());
153
154
        $response = Director::test("testBadBase/TestForm/fields/MyField");
155
        $this->assertNotEquals("MyField requested", $response->getBody());
156
    }
157
158
    public function testBaseWithExtension()
159
    {
160
        /* Rules with an extension always default to the index() action */
161
        $response = Director::test("testBaseWithExtension/virtualfile.xml");
162
        $this->assertEquals("This is the controller", $response->getBody());
163
164
        /* Without the extension, the methodname should be matched */
165
        $response = Director::test("testBaseWithExtension/virtualfile");
166
        $this->assertEquals("This is the virtualfile method", $response->getBody());
167
    }
168
169
    public function testNestedBase()
170
    {
171
        /* Nested base should leave out the two parts and correctly map arguments */
172
        $response = Director::test("testParentBase/testChildBase/method/1/2");
173
        $this->assertEquals("This is a method on the controller: 1, 2", $response->getBody());
174
    }
175
176
    public function testInheritedUrlHandlers()
177
    {
178
        /* $url_handlers can be defined on any class, and */
179
        $response = Director::test("testGoodBase1/TestForm/fields/SubclassedField/something");
180
        $this->assertEquals("customSomething", $response->getBody());
181
182
        /* However, if the subclass' url_handlers don't match, then the parent class' url_handlers will be used */
183
        $response = Director::test("testGoodBase1/TestForm/fields/SubclassedField");
184
        $this->assertEquals("SubclassedField requested", $response->getBody());
185
    }
186
187
    public function testDisallowedExtendedActions()
188
    {
189
        /* Actions on an extension are allowed because they specifically provided appropriate allowed_actions items */
190
        $response = Director::test("testGoodBase1/otherExtendedMethod");
191
        $this->assertEquals("otherExtendedMethod", $response->getBody());
192
193
        /* The failoverMethod action wasn't explicitly listed and so isnt' allowed */
194
        $response = Director::test("testGoodBase1/failoverMethod");
195
        $this->assertEquals(404, $response->getStatusCode());
196
197
        /* However, on RequestHandlingTest_AllowedController it has been explicitly allowed */
198
        $response = Director::test("AllowedController/failoverMethod");
199
        $this->assertEquals("failoverMethod", $response->getBody());
200
201
        /* The action on the extension is allowed when explicitly allowed on extension,
202
        even if its not mentioned in controller */
203
        $response = Director::test("AllowedController/extendedMethod");
204
        $this->assertEquals(200, $response->getStatusCode());
205
206
        /* This action has been blocked by an argument to a method */
207
        $response = Director::test('AllowedController/blockMethod');
208
        $this->assertEquals(403, $response->getStatusCode());
209
210
        /* Whereas this one has been allowed by a method without an argument */
211
        $response = Director::test('AllowedController/allowMethod');
212
        $this->assertEquals('allowMethod', $response->getBody());
213
    }
214
215
    public function testHTTPException()
216
    {
217
        $exception = Director::test('TestController/throwexception');
218
        $this->assertEquals(400, $exception->getStatusCode());
219
        $this->assertEquals('This request was invalid.', $exception->getBody());
220
221
        $responseException = (Director::test('TestController/throwresponseexception'));
222
        $this->assertEquals(500, $responseException->getStatusCode());
223
        $this->assertEquals('There was an internal server error.', $responseException->getBody());
224
    }
225
226
    public function testHTTPError()
227
    {
228
        RequestHandlingTest\ControllerExtension::$called_error = false;
229
        RequestHandlingTest\ControllerExtension::$called_404_error = false;
230
231
        $response = Director::test('TestController/throwhttperror');
232
233
        $this->assertEquals(404, $response->getStatusCode());
234
        $this->assertEquals('This page does not exist.', $response->getBody());
235
236
        // Confirm that RequestHandlingTest\ControllerExtension::onBeforeHTTPError() called
237
        $this->assertTrue(RequestHandlingTest\ControllerExtension::$called_error);
238
        // Confirm that RequestHandlingTest\ControllerExtension::onBeforeHTTPError404() called
239
        $this->assertTrue(RequestHandlingTest\ControllerExtension::$called_404_error);
240
    }
241
242
    public function testMethodsOnParentClassesOfRequestHandlerDeclined()
243
    {
244
        $response = Director::test('testGoodBase1/getIterator');
245
        $this->assertEquals(404, $response->getStatusCode());
246
    }
247
248
    public function testFormActionsCanBypassAllowedActions()
249
    {
250
        SecurityToken::enable();
251
252
        $response = $this->get('FormActionController');
253
        $this->assertEquals(200, $response->getStatusCode());
254
        $tokenEls = $this->cssParser()->getBySelector('#Form_Form_SecurityID');
255
        $securityId = (string)$tokenEls[0]['value'];
256
257
        $data = array('action_formaction' => 1);
258
        $response = $this->post('FormActionController/Form', $data);
259
        $this->assertEquals(
260
            400,
261
            $response->getStatusCode(),
262
            'Should fail: Invocation through POST form handler, not contained in $allowed_actions, without CSRF token'
263
        );
264
265
        $data = array('action_disallowedcontrollermethod' => 1, 'SecurityID' => $securityId);
266
        $response = $this->post('FormActionController/Form', $data);
267
        $this->assertEquals(
268
            403,
269
            $response->getStatusCode(),
270
            'Should fail: Invocation through POST form handler, controller action instead of form action,'
271
            .' not contained in $allowed_actions, with CSRF token'
272
        );
273
274
        $data = array('action_formaction' => 1, 'SecurityID' => $securityId);
275
        $response = $this->post('FormActionController/Form', $data);
276
        $this->assertEquals(200, $response->getStatusCode());
277
        $this->assertEquals(
278
            'formaction',
279
            $response->getBody(),
280
            'Should pass: Invocation through POST form handler, not contained in $allowed_actions, with CSRF token'
281
        );
282
283
        $data = array('action_controlleraction' => 1, 'SecurityID' => $securityId);
284
        $response = $this->post('FormActionController/Form', $data);
285
        $this->assertEquals(
286
            200,
287
            $response->getStatusCode(),
288
            'Should pass: Invocation through POST form handler, controller action instead of form action, contained in'
289
                . ' $allowed_actions, with CSRF token'
290
        );
291
292
        $data = array('action_formactionInAllowedActions' => 1);
293
        $response = $this->post('FormActionController/Form', $data);
294
        $this->assertEquals(
295
            400,
296
            $response->getStatusCode(),
297
            'Should fail: Invocation through POST form handler, contained in $allowed_actions, without CSRF token'
298
        );
299
300
        $data = array('action_formactionInAllowedActions' => 1, 'SecurityID' => $securityId);
301
        $response = $this->post('FormActionController/Form', $data);
302
        $this->assertEquals(
303
            200,
304
            $response->getStatusCode(),
305
            'Should pass: Invocation through POST form handler, contained in $allowed_actions, with CSRF token'
306
        );
307
308
        $data = array();
309
        $response = $this->post('FormActionController/formaction', $data);
310
        $this->assertEquals(
311
            404,
312
            $response->getStatusCode(),
313
            'Should fail: Invocation through POST URL, not contained in $allowed_actions, without CSRF token'
314
        );
315
316
        $data = array();
317
        $response = $this->post('FormActionController/formactionInAllowedActions', $data);
318
        $this->assertEquals(
319
            200,
320
            $response->getStatusCode(),
321
            'Should pass: Invocation of form action through POST URL, contained in $allowed_actions, without CSRF token'
322
        );
323
324
        $data = array('SecurityID' => $securityId);
325
        $response = $this->post('FormActionController/formactionInAllowedActions', $data);
326
        $this->assertEquals(
327
            200,
328
            $response->getStatusCode(),
329
            'Should pass: Invocation of form action through POST URL, contained in $allowed_actions, with CSRF token'
330
        );
331
332
        $data = array(); // CSRF protection doesnt kick in for direct requests
333
        $response = $this->post('FormActionController/formactionInAllowedActions', $data);
334
        $this->assertEquals(
335
            200,
336
            $response->getStatusCode(),
337
            'Should pass: Invocation of form action through POST URL, contained in $allowed_actions, without CSRF token'
338
        );
339
340
        SecurityToken::disable();
341
    }
342
343
    public function testAllowedActionsEnforcedOnForm()
344
    {
345
        $data = array('action_allowedformaction' => 1);
346
        $response = $this->post('ControllerFormWithAllowedActions/Form', $data);
347
        $this->assertEquals(200, $response->getStatusCode());
348
        $this->assertEquals('allowedformaction', $response->getBody());
349
350
        $data = array('action_disallowedformaction' => 1);
351
        $response = $this->post('ControllerFormWithAllowedActions/Form', $data);
352
        $this->assertEquals(403, $response->getStatusCode());
353
        // Note: Looks for a specific 403 thrown by Form->httpSubmission(), not RequestHandler->handleRequest()
354
        $this->assertContains('not allowed on form', $response->getBody());
355
    }
356
357
    public function testActionHandlingOnField()
358
    {
359
        $data = array('action_actionOnField' => 1);
360
        $response = $this->post('FieldController/TestForm', $data);
361
        $this->assertEquals(200, $response->getStatusCode());
362
        $this->assertEquals('Test method on MyField', $response->getBody());
363
364
        $data = array('action_actionNotAllowedOnField' => 1);
365
        $response = $this->post('FieldController/TestForm', $data);
366
        $this->assertEquals(404, $response->getStatusCode());
367
    }
368
}
369