Passed
Push — master ( 301f96...5c3d68 )
by Robbie
05:48
created

tests/php/Model/SiteTreeTest.php (5 issues)

1
<?php
2
3
namespace SilverStripe\CMS\Tests\Model;
4
5
use LogicException;
6
use Page;
7
use ReflectionMethod;
8
use SilverStripe\CMS\Model\RedirectorPage;
9
use SilverStripe\CMS\Model\SiteTree;
10
use SilverStripe\CMS\Model\VirtualPage;
11
use SilverStripe\Control\ContentNegotiator;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Core\Config\Config;
15
use SilverStripe\Core\Injector\Injector;
16
use SilverStripe\Core\Manifest\ManifestFileFinder;
17
use SilverStripe\Dev\SapphireTest;
18
use SilverStripe\i18n\i18n;
19
use SilverStripe\ORM\DataObject;
20
use SilverStripe\ORM\DB;
21
use SilverStripe\ORM\ValidationException;
22
use SilverStripe\Security\Group;
23
use SilverStripe\Security\InheritedPermissions;
24
use SilverStripe\Security\Member;
25
use SilverStripe\Security\Permission;
26
use SilverStripe\Security\Security;
27
use SilverStripe\SiteConfig\SiteConfig;
28
use SilverStripe\Subsites\Extensions\SiteTreeSubsites;
0 ignored issues
show
The type SilverStripe\Subsites\Extensions\SiteTreeSubsites was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
29
use SilverStripe\Versioned\Versioned;
30
use SilverStripe\View\Parsers\Diff;
31
use SilverStripe\View\Parsers\ShortcodeParser;
32
use SilverStripe\View\Parsers\URLSegmentFilter;
33
use TractorCow\Fluent\Extension\FluentSiteTreeExtension;
34
use const ASSETS_DIR;
35
use const RESOURCES_DIR;
0 ignored issues
show
The constant RESOURCES_DIR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
36
37
class SiteTreeTest extends SapphireTest
38
{
39
    protected static $fixture_file = 'SiteTreeTest.yml';
40
41
    protected static $illegal_extensions = [
42
        SiteTree::class => [
43
            SiteTreeSubsites::class,
44
            FluentSiteTreeExtension::class,
45
        ],
46
    ];
47
48
    protected static $extra_dataobjects = [
49
        SiteTreeTest_ClassA::class,
50
        SiteTreeTest_ClassB::class,
51
        SiteTreeTest_ClassC::class,
52
        SiteTreeTest_ClassD::class,
53
        SiteTreeTest_ClassCext::class,
54
        SiteTreeTest_NotRoot::class,
55
        SiteTreeTest_StageStatusInherit::class,
56
        SiteTreeTest_DataObject::class,
57
    ];
58
59
    public function reservedSegmentsProvider()
60
    {
61
        return [
62
            // segments reserved by rules
63
            ['Admin', 'admin-2'],
64
            ['Dev', 'dev-2'],
65
            ['Robots in disguise', 'robots-in-disguise'],
66
            // segments reserved by folder name
67
            [ASSETS_DIR, ASSETS_DIR . '-2'],
68
            ['notafoldername', 'notafoldername'],
69
        ];
70
    }
71
72
    public function testCreateDefaultpages()
73
    {
74
        $remove = SiteTree::get();
75
        if ($remove) {
76
            foreach ($remove as $page) {
77
                $page->delete();
78
            }
79
        }
80
        // Make sure the table is empty
81
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
82
83
        // Disable the creation
84
        SiteTree::config()->create_default_pages = false;
85
        singleton(SiteTree::class)->requireDefaultRecords();
86
87
        // The table should still be empty
88
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
89
90
        // Enable the creation
91
        SiteTree::config()->create_default_pages = true;
92
        singleton(SiteTree::class)->requireDefaultRecords();
93
94
        // The table should now have three rows (home, about-us, contact-us)
95
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 3);
96
    }
97
98
    /**
99
     * Test generation of the URLSegment values.
100
     *  - Turns things into lowercase-hyphen-format
101
     *  - Generates from Title by default, unless URLSegment is explicitly set
102
     *  - Resolves duplicates by appending a number
103
     *  - renames classes with a class name conflict
104
     */
105
    public function testURLGeneration()
106
    {
107
        $expectedURLs = [
108
            'home' => 'home',
109
            'staff' => 'my-staff',
110
            'about' => 'about-us',
111
            'staffduplicate' => 'my-staff-2',
112
            'product1' => '1-1-test-product',
113
            'product2' => 'another-product',
114
            'product3' => 'another-product-2',
115
            'product4' => 'another-product-3',
116
            'object'   => 'object',
117
            'controller' => 'controller',
118
            'numericonly' => '1930',
119
        ];
120
121
        foreach ($expectedURLs as $fixture => $urlSegment) {
122
            $obj = $this->objFromFixture(SiteTree::class, $fixture);
123
            $this->assertEquals($urlSegment, $obj->URLSegment);
124
        }
125
    }
126
127
    /**
128
     * Check if reserved URL's are properly appended with a number at top level
129
     * @dataProvider reservedSegmentsProvider
130
     */
131
    public function testDisallowedURLGeneration($title, $urlSegment)
132
    {
133
        $page = SiteTree::create(['Title' => $title]);
134
        $id = $page->write();
135
        $page = SiteTree::get()->byID($id);
136
        $this->assertEquals($urlSegment, $page->URLSegment);
137
    }
138
139
    /**
140
     * Check if reserved URL's are not appended with a number on a child page
141
     * It's okay to have a URL like domain.com/my-page/admin as it won't interfere with domain.com/admin
142
     * @dataProvider reservedSegmentsProvider
143
     */
144
    public function testDisallowedChildURLGeneration($title, $urlSegment)
145
    {
146
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
147
        // Using the same dataprovider, strip out the -2 from the admin and dev segment
148
        $urlSegment = str_replace('-2', '', $urlSegment);
149
        $page = SiteTree::create(['Title' => $title, 'ParentID' => 1]);
150
        $id = $page->write();
151
        $page = SiteTree::get()->byID($id);
152
        $this->assertEquals($urlSegment, $page->URLSegment);
153
    }
154
155
    /**
156
     * Check that explicitly setting a URL segment to the resources dir will rename it to have a -2 suffix
157
     */
158
    public function testExplicitlyUsingResourcesDirForURLSegment()
159
    {
160
        $page = SiteTree::create(['URLSegment' => RESOURCES_DIR]);
0 ignored issues
show
The constant RESOURCES_DIR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
161
        $id = $page->write();
162
        $page = SiteTree::get()->byID($id);
163
        $this->assertSame(RESOURCES_DIR . '-2', $page->URLSegment);
164
    }
165
166
    /**
167
     * For legacy resources dir values ("resources"), check that URLSegments get a -2 appended
168
     */
169
    public function testLegacyResourcesDirValuesHaveIncrementedValueAppended()
170
    {
171
        if (RESOURCES_DIR !== 'resources') {
0 ignored issues
show
The constant RESOURCES_DIR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
172
            $this->markTestSkipped('This legacy test requires RESOURCES_DIR to be "resources"');
173
        }
174
175
        $page = SiteTree::create(['Title' => 'Resources']);
176
        $id = $page->write();
177
        $page = SiteTree::get()->byID($id);
178
        $this->assertSame('resources-2', $page->URLSegment);
179
    }
180
181
    /**
182
     * For new/configured resources dir values ("_resources"), check that URLSegments have the leading underscore
183
     * removed
184
     */
185
    public function testDefaultResourcesDirHasLeadingUnderscoreRemovedAndResourcesIsUsed()
186
    {
187
        if (RESOURCES_DIR === 'resources') {
0 ignored issues
show
The constant RESOURCES_DIR was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
188
            $this->markTestSkipped('This test requires RESOURCES_DIR to be something other than "resources"');
189
        }
190
191
        $page = SiteTree::create(['Title' => '_Resources']);
192
        $id = $page->write();
193
        $page = SiteTree::get()->byID($id);
194
        $this->assertSame('resources', $page->URLSegment);
195
    }
196
197
    /**
198
     * Test that publication copies data to SiteTree_Live
199
     */
200
    public function testPublishCopiesToLiveTable()
201
    {
202
        $obj = $this->objFromFixture(SiteTree::class, 'about');
203
        $obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
204
205
        $createdID = DB::query(
206
            "SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"URLSegment\" = '$obj->URLSegment'"
207
        )->value();
208
        $this->assertEquals($obj->ID, $createdID);
209
    }
210
211
    /**
212
     * Test that field which are set and then cleared are also transferred to the published site.
213
     */
214
    public function testPublishDeletedFields()
215
    {
216
        $this->logInWithPermission('ADMIN');
217
218
        $obj = $this->objFromFixture(SiteTree::class, 'about');
219
        $obj->Title = "asdfasdf";
220
        $obj->write();
221
        $this->assertTrue($obj->publishRecursive());
222
223
        $this->assertEquals(
224
            'asdfasdf',
225
            DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value()
226
        );
227
228
        $obj->Title = null;
229
        $obj->write();
230
        $this->assertTrue($obj->publishRecursive());
231
232
        $this->assertNull(DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value());
233
    }
234
235
    public function testParentNodeCachedInMemory()
236
    {
237
        $parent = SiteTree::create();
238
        $parent->Title = 'Section Title';
239
        $child = SiteTree::create();
240
        $child->Title = 'Page Title';
241
        $child->setParent($parent);
242
243
        $this->assertInstanceOf(SiteTree::class, $child->Parent);
244
        $this->assertEquals("Section Title", $child->Parent->Title);
245
    }
246
247
    public function testParentModelReturnType()
248
    {
249
        $parent = new SiteTreeTest_PageNode();
250
        $child = new SiteTreeTest_PageNode();
251
252
        $child->setParent($parent);
253
        $this->assertInstanceOf(SiteTreeTest_PageNode::class, $child->Parent);
254
    }
255
256
    /**
257
     * Confirm that DataObject::get_one() gets records from SiteTree_Live
258
     */
259
    public function testGetOneFromLive()
260
    {
261
        $s = SiteTree::create();
262
        $s->Title = "V1";
263
        $s->URLSegment = "get-one-test-page";
264
        $s->write();
265
        $s->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
266
        $s->Title = "V2";
267
        $s->write();
268
269
        $oldMode = Versioned::get_reading_mode();
270
        Versioned::set_stage(Versioned::LIVE);
271
272
        $checkSiteTree = DataObject::get_one(SiteTree::class, [
273
            '"SiteTree"."URLSegment"' => 'get-one-test-page',
274
        ]);
275
        $this->assertEquals("V1", $checkSiteTree->Title);
276
277
        Versioned::set_reading_mode($oldMode);
278
    }
279
280
    public function testChidrenOfRootAreTopLevelPages()
281
    {
282
        $pages = SiteTree::get();
283
        foreach ($pages as $page) {
284
            $page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
285
        }
286
        unset($pages);
287
288
        /* If we create a new SiteTree object with ID = 0 */
289
        $obj = SiteTree::create();
290
        /* Then its children should be the top-level pages */
291
        $stageChildren = $obj->stageChildren()->map('ID', 'Title');
292
        $liveChildren = $obj->liveChildren()->map('ID', 'Title');
293
        $allChildren = $obj->AllChildrenIncludingDeleted()->map('ID', 'Title');
294
295
        $this->assertContains('Home', $stageChildren);
296
        $this->assertContains('Products', $stageChildren);
297
        $this->assertNotContains('Staff', $stageChildren);
298
299
        $this->assertContains('Home', $liveChildren);
300
        $this->assertContains('Products', $liveChildren);
301
        $this->assertNotContains('Staff', $liveChildren);
302
303
        $this->assertContains('Home', $allChildren);
304
        $this->assertContains('Products', $allChildren);
305
        $this->assertNotContains('Staff', $allChildren);
306
    }
307
308
    public function testCanSaveBlankToHasOneRelations()
309
    {
310
        /* DataObject::write() should save to a has_one relationship if you set a field called (relname)ID */
311
        $page = SiteTree::create();
312
        $parentID = $this->idFromFixture(SiteTree::class, 'home');
313
        $page->ParentID = $parentID;
314
        $page->write();
315
        $this->assertEquals(
316
            $parentID,
317
            DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value()
318
        );
319
320
        /* You should then be able to save a null/0/'' value to the relation */
321
        $page->ParentID = null;
322
        $page->write();
323
        $this->assertEquals(0, DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value());
324
    }
325
326
    public function testStageStates()
327
    {
328
        // newly created page
329
        $createdPage = SiteTree::create();
330
        $createdPage->write();
331
        $this->assertTrue($createdPage->isOnDraft());
332
        $this->assertFalse($createdPage->isPublished());
333
        $this->assertTrue($createdPage->isOnDraftOnly());
334
        $this->assertTrue($createdPage->isModifiedOnDraft());
335
336
        // published page
337
        $publishedPage = SiteTree::create();
338
        $publishedPage->write();
339
        $publishedPage->copyVersionToStage('Stage', 'Live');
340
        $this->assertTrue($publishedPage->isOnDraft());
341
        $this->assertTrue($publishedPage->isPublished());
342
        $this->assertFalse($publishedPage->isOnDraftOnly());
343
        $this->assertFalse($publishedPage->isOnLiveOnly());
344
        $this->assertFalse($publishedPage->isModifiedOnDraft());
345
346
        // published page, deleted from stage
347
        $deletedFromDraftPage = SiteTree::create();
348
        $deletedFromDraftPage->write();
349
        $deletedFromDraftPage->copyVersionToStage('Stage', 'Live');
350
        $deletedFromDraftPage->deleteFromStage('Stage');
351
        $this->assertFalse($deletedFromDraftPage->isArchived());
352
        $this->assertFalse($deletedFromDraftPage->isOnDraft());
353
        $this->assertTrue($deletedFromDraftPage->isPublished());
354
        $this->assertFalse($deletedFromDraftPage->isOnDraftOnly());
355
        $this->assertTrue($deletedFromDraftPage->isOnLiveOnly());
356
        $this->assertFalse($deletedFromDraftPage->isModifiedOnDraft());
357
358
        // published page, deleted from live
359
        $deletedFromLivePage = SiteTree::create();
360
        $deletedFromLivePage->write();
361
        $deletedFromLivePage->copyVersionToStage('Stage', 'Live');
362
        $deletedFromLivePage->deleteFromStage('Live');
363
        $this->assertFalse($deletedFromLivePage->isArchived());
364
        $this->assertTrue($deletedFromLivePage->isOnDraft());
365
        $this->assertFalse($deletedFromLivePage->isPublished());
366
        $this->assertTrue($deletedFromLivePage->isOnDraftOnly());
367
        $this->assertFalse($deletedFromLivePage->isOnLiveOnly());
368
        $this->assertTrue($deletedFromLivePage->isModifiedOnDraft());
369
370
        // published page, deleted from both stages
371
        $deletedFromAllStagesPage = SiteTree::create();
372
        $deletedFromAllStagesPage->write();
373
        $deletedFromAllStagesPage->copyVersionToStage('Stage', 'Live');
374
        $deletedFromAllStagesPage->deleteFromStage('Stage');
375
        $deletedFromAllStagesPage->deleteFromStage('Live');
376
        $this->assertTrue($deletedFromAllStagesPage->isArchived());
377
        $this->assertFalse($deletedFromAllStagesPage->isOnDraft());
378
        $this->assertFalse($deletedFromAllStagesPage->isPublished());
379
        $this->assertFalse($deletedFromAllStagesPage->isOnDraftOnly());
380
        $this->assertFalse($deletedFromAllStagesPage->isOnLiveOnly());
381
        $this->assertFalse($deletedFromAllStagesPage->isModifiedOnDraft());
382
383
        // published page, modified
384
        $modifiedOnDraftPage = SiteTree::create();
385
        $modifiedOnDraftPage->write();
386
        $modifiedOnDraftPage->copyVersionToStage('Stage', 'Live');
387
        $modifiedOnDraftPage->Content = 'modified';
388
        $modifiedOnDraftPage->write();
389
        $this->assertFalse($modifiedOnDraftPage->isArchived());
390
        $this->assertTrue($modifiedOnDraftPage->isOnDraft());
391
        $this->assertTrue($modifiedOnDraftPage->isPublished());
392
        $this->assertFalse($modifiedOnDraftPage->isOnDraftOnly());
393
        $this->assertFalse($modifiedOnDraftPage->isOnLiveOnly());
394
        $this->assertTrue($modifiedOnDraftPage->isModifiedOnDraft());
395
    }
396
397
    /**
398
     * Test that a page can be completely deleted and restored to the stage site
399
     */
400
    public function testRestoreToStage()
401
    {
402
        $page = $this->objFromFixture(SiteTree::class, 'about');
403
        $pageID = $page->ID;
404
        $page->delete();
405
        $this->assertTrue(!DataObject::get_by_id(SiteTree::class, $pageID));
406
407
        $deletedPage = Versioned::get_latest_version(SiteTree::class, $pageID);
408
        $resultPage = $deletedPage->doRestoreToStage();
409
410
        $requeriedPage = DataObject::get_by_id(SiteTree::class, $pageID);
411
412
        $this->assertEquals($pageID, $resultPage->ID);
413
        $this->assertEquals($pageID, $requeriedPage->ID);
414
        $this->assertEquals('About Us', $requeriedPage->Title);
415
        $this->assertInstanceOf(SiteTree::class, $requeriedPage);
416
417
418
        $page2 = $this->objFromFixture(SiteTree::class, 'products');
419
        $page2ID = $page2->ID;
420
        $page2->doUnpublish();
421
        $page2->delete();
422
423
        // Check that if we restore while on the live site that the content still gets pushed to
424
        // stage
425
        Versioned::set_stage(Versioned::LIVE);
426
        $deletedPage = Versioned::get_latest_version(SiteTree::class, $page2ID);
427
        $deletedPage->doRestoreToStage();
428
        $this->assertFalse(
429
            (bool)Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, "\"SiteTree\".\"ID\" = " . $page2ID)
430
        );
431
432
        Versioned::set_stage(Versioned::DRAFT);
433
        $requeriedPage = DataObject::get_by_id(SiteTree::class, $page2ID);
434
        $this->assertEquals('Products', $requeriedPage->Title);
435
        $this->assertInstanceOf(SiteTree::class, $requeriedPage);
436
    }
437
438
    public function testNoCascadingDeleteWithoutID()
439
    {
440
        Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', true);
441
        $count = SiteTree::get()->count();
442
        $this->assertNotEmpty($count);
443
        $obj = SiteTree::create();
444
        $this->assertFalse($obj->exists());
445
        $fail = true;
446
        try {
447
            $obj->delete();
448
        } catch (LogicException $e) {
449
            $fail = false;
450
        }
451
        if ($fail) {
452
            $this->fail('Failed to throw delete exception');
453
        }
454
        $this->assertCount($count, SiteTree::get());
455
    }
456
457
    public function testGetByLink()
458
    {
459
        $home     = $this->objFromFixture(SiteTree::class, 'home');
460
        $about    = $this->objFromFixture(SiteTree::class, 'about');
461
        $staff    = $this->objFromFixture(SiteTree::class, 'staff');
462
        $product  = $this->objFromFixture(SiteTree::class, 'product1');
463
464
        SiteTree::config()->nested_urls = false;
465
466
        $this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID);
467
        $this->assertEquals($home->ID, SiteTree::get_by_link('/home/', false)->ID);
468
        $this->assertEquals($about->ID, SiteTree::get_by_link($about->Link(), false)->ID);
469
        $this->assertEquals($staff->ID, SiteTree::get_by_link($staff->Link(), false)->ID);
470
        $this->assertEquals($product->ID, SiteTree::get_by_link($product->Link(), false)->ID);
471
472
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
473
474
        $this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID);
475
        $this->assertEquals($home->ID, SiteTree::get_by_link('/home/', false)->ID);
476
        $this->assertEquals($about->ID, SiteTree::get_by_link($about->Link(), false)->ID);
477
        $this->assertEquals($staff->ID, SiteTree::get_by_link($staff->Link(), false)->ID);
478
        $this->assertEquals($product->ID, SiteTree::get_by_link($product->Link(), false)->ID);
479
480
        $this->assertEquals(
481
            $staff->ID,
482
            SiteTree::get_by_link('/my-staff/', false)->ID,
483
            'Assert a unique URLSegment can be used for b/c.'
484
        );
485
    }
486
487
    public function testRelativeLink()
488
    {
489
        $about    = $this->objFromFixture(SiteTree::class, 'about');
490
        $staff    = $this->objFromFixture(SiteTree::class, 'staff');
491
492
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
493
494
        $this->assertEquals(
495
            'about-us/',
496
            $about->RelativeLink(),
497
            'Matches URLSegment on top level without parameters'
498
        );
499
        $this->assertEquals(
500
            'about-us/my-staff/',
501
            $staff->RelativeLink(),
502
            'Matches URLSegment plus parent on second level without parameters'
503
        );
504
        $this->assertEquals(
505
            'about-us/edit',
506
            $about->RelativeLink('edit'),
507
            'Matches URLSegment plus parameter on top level'
508
        );
509
        $this->assertEquals(
510
            'about-us/tom&jerry',
511
            $about->RelativeLink('tom&jerry'),
512
            'Doesnt url encode parameter'
513
        );
514
    }
515
516
    public function testPageLevel()
517
    {
518
        $about = $this->objFromFixture(SiteTree::class, 'about');
519
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
520
        $this->assertEquals(1, $about->getPageLevel());
521
        $this->assertEquals(2, $staff->getPageLevel());
522
    }
523
524
    public function testAbsoluteLiveLink()
525
    {
526
        $parent = $this->objFromFixture(SiteTree::class, 'about');
527
        $child = $this->objFromFixture(SiteTree::class, 'staff');
528
529
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
530
531
        $child->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
532
        $parent->URLSegment = 'changed-on-live';
533
        $parent->write();
534
        $parent->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
535
        $parent->URLSegment = 'changed-on-draft';
536
        $parent->write();
537
538
        $this->assertStringEndsWith('changed-on-live/my-staff/', $child->getAbsoluteLiveLink(false));
539
        $this->assertStringEndsWith('changed-on-live/my-staff/?stage=Live', $child->getAbsoluteLiveLink());
540
    }
541
542
    public function testDuplicateChildrenRetainSort()
543
    {
544
        $parent = SiteTree::create();
545
        $parent->Title = 'Parent';
546
        $parent->write();
547
548
        $child1 = SiteTree::create();
549
        $child1->ParentID = $parent->ID;
550
        $child1->Title = 'Child 1';
551
        $child1->Sort = 2;
552
        $child1->write();
553
554
        $child2 = SiteTree::create();
555
        $child2->ParentID = $parent->ID;
556
        $child2->Title = 'Child 2';
557
        $child2->Sort = 1;
558
        $child2->write();
559
560
        $duplicateParent = $parent->duplicateWithChildren();
561
        $duplicateChildren = $duplicateParent->AllChildren()->toArray();
562
        $this->assertCount(2, $duplicateChildren);
563
564
        $duplicateChild2 = array_shift($duplicateChildren);
565
        $duplicateChild1 = array_shift($duplicateChildren);
566
567
568
        $this->assertEquals('Child 1', $duplicateChild1->Title);
569
        $this->assertEquals('Child 2', $duplicateChild2->Title);
570
571
        // assertGreaterThan works by having the LOWER value first
572
        $this->assertGreaterThan($duplicateChild2->Sort, $duplicateChild1->Sort);
573
    }
574
575
    public function testDeleteFromStageOperatesRecursively()
576
    {
577
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
578
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
579
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
580
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
581
582
        $pageAbout->delete();
583
584
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
585
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
586
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
587
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
588
    }
589
590
    public function testDeleteFromStageOperatesRecursivelyStrict()
591
    {
592
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
593
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
594
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
595
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
596
597
        $pageAbout->delete();
598
599
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
600
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaff->ID));
601
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID));
602
    }
603
604
    public function testDuplicate()
605
    {
606
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
607
        $dupe = $pageAbout->duplicate();
608
        $this->assertEquals($pageAbout->Title, $dupe->Title);
609
        $this->assertNotEquals($pageAbout->URLSegment, $dupe->URLSegment);
610
        $this->assertNotEquals($pageAbout->Sort, $dupe->Sort);
611
    }
612
613
    public function testDeleteFromLiveOperatesRecursively()
614
    {
615
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
616
        $this->logInWithPermission('ADMIN');
617
618
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
619
        $pageAbout->publishRecursive();
620
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
621
        $pageStaff->publishRecursive();
622
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
623
        $pageStaffDuplicate->publishRecursive();
624
625
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
626
627
        $parentPage->doUnpublish();
628
629
        Versioned::set_stage(Versioned::LIVE);
630
631
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
632
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
633
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
634
        Versioned::set_stage(Versioned::DRAFT);
635
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
636
    }
637
638
    public function testUnpublishDoesNotDeleteChildrenWithLooseHierachyOn()
639
    {
640
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
641
        $this->logInWithPermission('ADMIN');
642
643
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
644
        $pageAbout->publishRecursive();
645
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
646
        $pageStaff->publishRecursive();
647
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
648
        $pageStaffDuplicate->publishRecursive();
649
650
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
651
        $parentPage->doUnpublish();
652
653
        Versioned::set_stage(Versioned::LIVE);
654
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
655
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
656
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
657
        Versioned::set_stage(Versioned::DRAFT);
658
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
659
    }
660
661
    public function testDeleteFromLiveOperatesRecursivelyStrict()
662
    {
663
        Config::inst()->update(SiteTree::class, 'enforce_strict_hierarchy', true);
664
        $this->logInWithPermission('ADMIN');
665
666
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
667
        $pageAbout->publishRecursive();
668
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
669
        $pageStaff->publishRecursive();
670
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
671
        $pageStaffDuplicate->publishRecursive();
672
673
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
674
        $parentPage->doUnpublish();
675
676
        Versioned::set_stage(Versioned::LIVE);
677
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
678
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaff->ID));
679
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID));
680
        Versioned::set_stage(Versioned::DRAFT);
681
    }
682
683
    /**
684
     * Simple test to confirm that querying from a particular archive date doesn't throw
685
     * an error
686
     */
687
    public function testReadArchiveDate()
688
    {
689
        $date = '2009-07-02 14:05:07';
690
        Versioned::reading_archived_date($date);
691
        SiteTree::get()->where([
692
            '"SiteTree"."ParentID"' => 0,
693
        ])->sql($args);
694
        $this->assertContains($date, $args);
695
    }
696
697
    public function testEditPermissions()
698
    {
699
        $editor = $this->objFromFixture(Member::class, "editor");
700
701
        $home = $this->objFromFixture(SiteTree::class, "home");
702
        $staff = $this->objFromFixture(SiteTree::class, "staff");
703
        $products = $this->objFromFixture(SiteTree::class, "products");
704
        $product1 = $this->objFromFixture(SiteTree::class, "product1");
705
        $product4 = $this->objFromFixture(SiteTree::class, "product4");
706
707
        // Test logged out users cannot edit
708
        $this->logOut();
709
        $this->assertFalse($staff->canEdit());
710
711
        // Can't edit a page that is locked to admins
712
        $this->assertFalse($home->canEdit($editor));
713
714
        // Can edit a page that is locked to editors
715
        $this->assertTrue($products->canEdit($editor));
716
717
        // Can edit a child of that page that inherits
718
        $this->assertTrue($product1->canEdit($editor));
719
720
        // Can't edit a child of that page that has its permissions overridden
721
        $this->assertFalse($product4->canEdit($editor));
722
    }
723
724
    public function testCanEditWithAccessToAllSections()
725
    {
726
        $page = SiteTree::create();
727
        $page->write();
728
        $allSectionMember = $this->objFromFixture(Member::class, 'allsections');
729
        $securityAdminMember = $this->objFromFixture(Member::class, 'securityadmin');
730
731
        $this->assertTrue($page->canEdit($allSectionMember));
732
        $this->assertFalse($page->canEdit($securityAdminMember));
733
    }
734
735
    public function testCreatePermissions()
736
    {
737
        // Test logged out users cannot create
738
        $this->logOut();
739
        $this->assertFalse(singleton(SiteTree::class)->canCreate());
740
741
        // Login with another permission
742
        $this->logInWithPermission('DUMMY');
743
        $this->assertFalse(singleton(SiteTree::class)->canCreate());
744
745
        // Login with basic CMS permission
746
        $perms = SiteConfig::config()->required_permission;
747
        $this->logInWithPermission(reset($perms));
748
        $this->assertTrue(singleton(SiteTree::class)->canCreate());
749
750
        // Test creation underneath a parent which this user doesn't have access to
751
        $parent = $this->objFromFixture(SiteTree::class, 'about');
752
        $this->assertFalse(singleton(SiteTree::class)->canCreate(null, ['Parent' => $parent]));
753
754
        // Test creation underneath a parent which doesn't allow a certain child
755
        $parentB = new SiteTreeTest_ClassB();
756
        $parentB->Title = 'Only Allows SiteTreeTest_ClassC';
757
        $parentB->write();
758
        $this->assertTrue(singleton(SiteTreeTest_ClassA::class)->canCreate(null));
759
        $this->assertFalse(singleton(SiteTreeTest_ClassA::class)->canCreate(null, ['Parent' => $parentB]));
760
        $this->assertTrue(singleton(SiteTreeTest_ClassC::class)->canCreate(null, ['Parent' => $parentB]));
761
762
        // Test creation underneath a parent which doesn't exist in the database. This should
763
        // fall back to checking whether the user can create pages at the root of the site
764
        $this->assertTrue(singleton(SiteTree::class)->canCreate(null, ['Parent' => singleton(SiteTree::class)]));
765
766
        //Test we don't check for allowedChildren on parent context if it's not SiteTree instance
767
        $this->assertTrue(
768
            singleton(SiteTree::class)->canCreate(
769
                null,
770
                ['Parent' => $this->objFromFixture(SiteTreeTest_DataObject::class, 'relations')]
771
            )
772
        );
773
    }
774
775
    public function testEditPermissionsOnDraftVsLive()
776
    {
777
        // Create an inherit-permission page
778
        $page = SiteTree::create();
779
        $page->write();
780
        $page->CanEditType = "Inherit";
781
        $page->publishRecursive();
782
        $pageID = $page->ID;
783
784
        // Lock down the site config
785
        $sc = $page->SiteConfig;
786
        $sc->CanEditType = 'OnlyTheseUsers';
787
        $sc->EditorGroups()->add($this->idFromFixture(Group::class, 'admins'));
788
        $sc->write();
789
790
        // Confirm that Member.editor can't edit the page
791
        $member = $this->objFromFixture(Member::class, 'editor');
792
        Security::setCurrentUser($member);
793
        $this->assertFalse($page->canEdit());
794
795
        // Change the page to be editable by Group.editors, but do not publish
796
        $admin = $this->objFromFixture(Member::class, 'admin');
797
        Security::setCurrentUser($admin);
798
        $page->CanEditType = 'OnlyTheseUsers';
799
        $page->EditorGroups()->add($this->idFromFixture(Group::class, 'editors'));
800
        $page->write();
801
802
        // Clear permission cache
803
        /** @var InheritedPermissions $checker */
804
        $checker = SiteTree::getPermissionChecker();
805
        $checker->clearCache();
806
807
        // Confirm that Member.editor can now edit the page
808
        $member = $this->objFromFixture(Member::class, 'editor');
809
        Security::setCurrentUser($member);
810
        $this->assertTrue($page->canEdit());
811
812
        // Publish the changes to the page
813
        $admin = $this->objFromFixture(Member::class, 'admin');
814
        Security::setCurrentUser($admin);
815
        $page->publishRecursive();
816
817
        // Confirm that Member.editor can still edit the page
818
        $member = $this->objFromFixture(Member::class, 'editor');
819
        Security::setCurrentUser($member);
820
        $this->assertTrue($page->canEdit());
821
    }
822
823
    public function testCompareVersions()
824
    {
825
        // Necessary to avoid
826
        $oldCleanerClass = Diff::$html_cleaner_class;
827
        Diff::$html_cleaner_class = SiteTreeTest_NullHtmlCleaner::class;
828
829
        $page = SiteTree::create();
830
        $page->write();
831
        $this->assertEquals(1, $page->Version);
832
833
        // Use inline element to avoid double wrapping applied to
834
        // blocklevel elements depending on HTMLCleaner implementation:
835
        // <ins><p> gets converted to <ins><p><inst>
836
        $page->Content = "<span>This is a test</span>";
837
        $page->write();
838
        $this->assertEquals(2, $page->Version);
839
840
        $diff = $page->compareVersions(1, 2);
841
842
        $processedContent = trim($diff->Content);
843
        $processedContent = preg_replace('/\s*</', '<', $processedContent);
844
        $processedContent = preg_replace('/>\s*/', '>', $processedContent);
845
        $this->assertEquals("<ins><span>This is a test</span></ins>", $processedContent);
846
847
        Diff::$html_cleaner_class = $oldCleanerClass;
848
    }
849
850
    public function testAuthorIDAndPublisherIDFilledOutOnPublish()
851
    {
852
        // Ensure that we have a member ID who is doing all this work
853
        $member = $this->objFromFixture(Member::class, "admin");
854
        $this->logInAs($member);
855
856
        // Write the page
857
        $about = $this->objFromFixture(SiteTree::class, 'about');
858
        $about->Title = "Another title";
859
        $about->write();
860
861
        // Check the version created
862
        $savedVersion = DB::prepared_query(
863
            "SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
864
            WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
865
            [$about->ID]
866
        )->first();
867
        $this->assertEquals($member->ID, $savedVersion['AuthorID']);
868
        $this->assertEquals(0, $savedVersion['PublisherID']);
869
870
        // Publish the page
871
        $about->publishRecursive();
872
        $publishedVersion = DB::prepared_query(
873
            "SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
874
            WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
875
            [$about->ID]
876
        )->first();
877
878
        // Check the version created
879
        $this->assertEquals($member->ID, $publishedVersion['AuthorID']);
880
        $this->assertEquals($member->ID, $publishedVersion['PublisherID']);
881
    }
882
883
    public function testLinkShortcodeHandler()
884
    {
885
        $aboutPage = $this->objFromFixture(SiteTree::class, 'about');
886
        $redirectPage = $this->objFromFixture(RedirectorPage::class, 'external');
887
888
        $parser = new ShortcodeParser();
889
        $parser->register('sitetree_link', [SiteTree::class, 'link_shortcode_handler']);
890
891
        $aboutShortcode = sprintf('[sitetree_link,id=%d]', $aboutPage->ID);
892
        $aboutEnclosed  = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $aboutPage->ID);
893
894
        $aboutShortcodeExpected = $aboutPage->Link();
895
        $aboutEnclosedExpected  = sprintf('<a href="%s">Example Content</a>', $aboutPage->Link());
896
897
        $this->assertEquals(
898
            $aboutShortcodeExpected,
899
            $parser->parse($aboutShortcode),
900
            'Test that simple linking works.'
901
        );
902
        $this->assertEquals(
903
            $aboutEnclosedExpected,
904
            $parser->parse($aboutEnclosed),
905
            'Test enclosed content is linked.'
906
        );
907
908
        $aboutPage->delete();
909
910
        $this->assertEquals(
911
            $aboutShortcodeExpected,
912
            $parser->parse($aboutShortcode),
913
            'Test that deleted pages still link.'
914
        );
915
        $this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed));
916
917
        $aboutShortcode = '[sitetree_link,id="-1"]';
918
        $aboutEnclosed  = '[sitetree_link,id="-1"]Example Content[/sitetree_link]';
919
920
        $this->assertEquals('', $parser->parse($aboutShortcode), 'Test empty result if no suitable matches.');
921
        $this->assertEquals('', $parser->parse($aboutEnclosed));
922
923
        $redirectShortcode = sprintf('[sitetree_link,id=%d]', $redirectPage->ID);
924
        $redirectEnclosed  = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $redirectPage->ID);
925
        $redirectExpected = 'http://www.google.com?a&amp;b';
926
927
        $this->assertEquals($redirectExpected, $parser->parse($redirectShortcode));
928
        $this->assertEquals(
929
            sprintf('<a href="%s">Example Content</a>', $redirectExpected),
930
            $parser->parse($redirectEnclosed)
931
        );
932
933
        $this->assertEquals('', $parser->parse('[sitetree_link]'), 'Test that invalid ID attributes are not parsed.');
934
        $this->assertEquals('', $parser->parse('[sitetree_link,id="text"]'));
935
        $this->assertEquals('', $parser->parse('[sitetree_link]Example Content[/sitetree_link]'));
936
    }
937
938
    public function testIsCurrent()
939
    {
940
        $aboutPage = $this->objFromFixture(SiteTree::class, 'about');
941
        $productPage = $this->objFromFixture(SiteTree::class, 'products');
942
943
        Director::set_current_page($aboutPage);
944
        $this->assertTrue($aboutPage->isCurrent(), 'Assert that basic isCurrent checks works.');
945
        $this->assertFalse($productPage->isCurrent());
946
947
        $this->assertTrue(
948
            DataObject::get_one(SiteTree::class, [
949
                '"SiteTree"."Title"' => 'About Us',
950
            ])->isCurrent(),
951
            'Assert that isCurrent works on another instance with the same ID.'
952
        );
953
954
        Director::set_current_page($newPage = SiteTree::create());
955
        $this->assertTrue($newPage->isCurrent(), 'Assert that isCurrent works on unsaved pages.');
956
    }
957
958
    public function testIsSection()
959
    {
960
        $about = $this->objFromFixture(SiteTree::class, 'about');
961
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
962
        $ceo   = $this->objFromFixture(SiteTree::class, 'ceo');
963
964
        Director::set_current_page($about);
965
        $this->assertTrue($about->isSection());
966
        $this->assertFalse($staff->isSection());
967
        $this->assertFalse($ceo->isSection());
968
969
        Director::set_current_page($staff);
970
        $this->assertTrue($about->isSection());
971
        $this->assertTrue($staff->isSection());
972
        $this->assertFalse($ceo->isSection());
973
974
        Director::set_current_page($ceo);
975
        $this->assertTrue($about->isSection());
976
        $this->assertTrue($staff->isSection());
977
        $this->assertTrue($ceo->isSection());
978
    }
979
980
    public function testURLSegmentReserved()
981
    {
982
        $siteTree = SiteTree::create(['URLSegment' => 'admin']);
983
        $segment = $siteTree->validURLSegment();
984
985
        $this->assertFalse($segment);
986
    }
987
988
    public function testURLSegmentAutoUpdate()
989
    {
990
        $sitetree = SiteTree::create();
991
        $sitetree->Title = _t(
992
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
993
            'New {pagetype}',
994
            ['pagetype' => $sitetree->i18n_singular_name()]
995
        );
996
        $sitetree->write();
997
        $this->assertEquals(
998
            'new-page',
999
            $sitetree->URLSegment,
1000
            'Sets based on default title on first save'
1001
        );
1002
1003
        $sitetree->Title = 'Changed';
1004
        $sitetree->write();
1005
        $this->assertEquals(
1006
            'changed',
1007
            $sitetree->URLSegment,
1008
            'Auto-updates when set to default title'
1009
        );
1010
1011
        $sitetree->Title = 'Changed again';
1012
        $sitetree->write();
1013
        $this->assertEquals(
1014
            'changed',
1015
            $sitetree->URLSegment,
1016
            'Does not auto-update once title has been changed'
1017
        );
1018
    }
1019
1020
    public function testURLSegmentAutoUpdateLocalized()
1021
    {
1022
        $oldLocale = i18n::get_locale();
1023
        i18n::set_locale('de_DE');
1024
1025
        $sitetree = SiteTree::create();
1026
        $sitetree->Title = _t(
1027
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1028
            'New {pagetype}',
1029
            ['pagetype' => $sitetree->i18n_singular_name()]
1030
        );
1031
        $sitetree->write();
1032
        $this->assertEquals(
1033
            'neue-seite',
1034
            $sitetree->URLSegment,
1035
            'Sets based on default title on first save'
1036
        );
1037
1038
        $sitetree->Title = 'Changed';
1039
        $sitetree->write();
1040
        $this->assertEquals(
1041
            'changed',
1042
            $sitetree->URLSegment,
1043
            'Auto-updates when set to default title'
1044
        );
1045
1046
        $sitetree->Title = 'Changed again';
1047
        $sitetree->write();
1048
        $this->assertEquals(
1049
            'changed',
1050
            $sitetree->URLSegment,
1051
            'Does not auto-update once title has been changed'
1052
        );
1053
1054
        i18n::set_locale($oldLocale);
1055
    }
1056
1057
    /**
1058
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1059
     */
1060
    public function testValidURLSegmentURLSegmentConflicts()
1061
    {
1062
        $sitetree = SiteTree::create();
1063
        SiteTree::config()->nested_urls = false;
1064
1065
        $sitetree->URLSegment = 'home';
1066
        $this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
1067
        $sitetree->URLSegment = 'home-noconflict';
1068
        $this->assertTrue($sitetree->validURLSegment());
1069
1070
        $sitetree->ParentID   = $this->idFromFixture(SiteTree::class, 'about');
1071
        $sitetree->URLSegment = 'home';
1072
        $this->assertFalse($sitetree->validURLSegment(), 'Conflicts are still recognised with a ParentID value');
1073
1074
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
1075
1076
        $sitetree->ParentID   = 0;
1077
        $sitetree->URLSegment = 'home';
1078
        $this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
1079
1080
        $sitetree->ParentID = $this->idFromFixture(SiteTree::class, 'about');
1081
        $this->assertTrue($sitetree->validURLSegment(), 'URLSegments can be the same across levels');
1082
1083
        $sitetree->URLSegment = 'my-staff';
1084
        $this->assertFalse($sitetree->validURLSegment(), 'Nested URLSegment conflicts are recognised');
1085
        $sitetree->URLSegment = 'my-staff-noconflict';
1086
        $this->assertTrue($sitetree->validURLSegment());
1087
    }
1088
1089
    /**
1090
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1091
     */
1092
    public function testValidURLSegmentClassNameConflicts()
1093
    {
1094
        $sitetree = SiteTree::create();
1095
        $sitetree->URLSegment = Controller::class;
1096
1097
        $this->assertTrue($sitetree->validURLSegment(), 'Class names are no longer conflicts');
1098
    }
1099
1100
    /**
1101
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1102
     */
1103
    public function testValidURLSegmentControllerConflicts()
1104
    {
1105
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
1106
1107
        $sitetree = SiteTree::create();
1108
        $sitetree->ParentID = $this->idFromFixture(SiteTreeTest_Conflicted::class, 'parent');
1109
1110
        $sitetree->URLSegment = 'index';
1111
        $this->assertFalse($sitetree->validURLSegment(), 'index is not a valid URLSegment');
1112
1113
        $sitetree->URLSegment = 'conflicted-action';
1114
        $this->assertFalse($sitetree->validURLSegment(), 'allowed_actions conflicts are recognised');
1115
1116
        $sitetree->URLSegment = 'conflicted-template';
1117
        $this->assertFalse($sitetree->validURLSegment(), 'Action-specific template conflicts are recognised');
1118
1119
        $sitetree->URLSegment = 'valid';
1120
        $this->assertTrue($sitetree->validURLSegment(), 'Valid URLSegment values are allowed');
1121
    }
1122
1123
    public function testURLSegmentPrioritizesExtensionVotes()
1124
    {
1125
        $sitetree = SiteTree::create();
1126
        $sitetree->URLSegment = 'unique-segment';
1127
        $this->assertTrue($sitetree->validURLSegment());
1128
1129
        SiteTree::add_extension(SiteTreeTest_Extension::class);
1130
        $sitetree = SiteTree::create();
1131
        $sitetree->URLSegment = 'unique-segment';
1132
        $this->assertFalse($sitetree->validURLSegment());
1133
        SiteTree::remove_extension(SiteTreeTest_Extension::class);
1134
    }
1135
1136
    public function testURLSegmentMultiByte()
1137
    {
1138
        URLSegmentFilter::config()->set('default_allow_multibyte', true);
1139
        $sitetree = SiteTree::create();
1140
        $sitetree->write();
1141
1142
        $sitetree->URLSegment = 'brötchen';
1143
        $sitetree->write();
1144
        $sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
1145
        $this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
1146
1147
        $sitetree->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1148
        $sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
1149
        $this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
1150
        $sitetreeLive = Versioned::get_one_by_stage(
1151
            SiteTree::class,
1152
            Versioned::LIVE,
1153
            '"SiteTree"."ID" = ' . $sitetree->ID,
1154
            false
1155
        );
1156
        $this->assertEquals($sitetreeLive->URLSegment, rawurlencode('brötchen'));
1157
    }
1158
1159
    public function testVersionsAreCreated()
1160
    {
1161
        $p = SiteTree::create();
1162
        $p->Content = "one";
1163
        $p->write();
1164
        $this->assertEquals(1, $p->Version);
1165
1166
        // No changes don't bump version
1167
        $p->write();
1168
        $this->assertEquals(1, $p->Version);
1169
1170
        $p->Content = "two";
1171
        $p->write();
1172
        $this->assertEquals(2, $p->Version);
1173
1174
        // Only change meta-data don't bump version
1175
        $p->HasBrokenLink = true;
1176
        $p->write();
1177
        $p->HasBrokenLink = false;
1178
        $p->write();
1179
        $this->assertEquals(2, $p->Version);
1180
1181
        $p->Content = "three";
1182
        $p->write();
1183
        $this->assertEquals(3, $p->Version);
1184
    }
1185
1186
    public function testPageTypeClasses()
1187
    {
1188
        $classes = SiteTree::page_type_classes();
1189
        $this->assertNotContains(SiteTree::class, $classes, 'Page types do not include base class');
1190
        $this->assertContains('Page', $classes, 'Page types do contain subclasses');
1191
1192
        // Testing what happens in an incorrect config value is set - hide_ancestor should be a string
1193
        Config::modify()->set(SiteTreeTest_ClassA::class, 'hide_ancestor', true);
1194
        $newClasses = SiteTree::page_type_classes();
1195
        $this->assertEquals(
1196
            $classes,
1197
            $newClasses,
1198
            'Setting hide_ancestor to a boolean (incorrect) value caused a page class to be hidden'
1199
        );
1200
    }
1201
1202
    /**
1203
     * Tests that core subclasses of SiteTree are included in allowedChildren() by default, but not instances of
1204
     * HiddenClass
1205
     */
1206
    public function testAllowedChildrenContainsCoreSubclassesButNotHiddenClass()
1207
    {
1208
        $page = SiteTree::create();
1209
        $allowedChildren = $page->allowedChildren();
1210
1211
        $this->assertContains(
1212
            VirtualPage::class,
1213
            $allowedChildren,
1214
            'Includes core subclasses by default'
1215
        );
1216
1217
        $this->assertNotContains(
1218
            SiteTreeTest_ClassE::class,
1219
            $allowedChildren,
1220
            'HiddenClass instances should not be returned'
1221
        );
1222
    }
1223
1224
    /**
1225
     * Tests that various types of SiteTree classes will or will not be returned from the allowedChildren method
1226
     * @dataProvider allowedChildrenProvider
1227
     * @param string $className
1228
     * @param array  $expected
1229
     * @param string $assertionMessage
1230
     */
1231
    public function testAllowedChildren($className, $expected, $assertionMessage)
1232
    {
1233
        $class = new $className();
1234
        $this->assertEquals($expected, $class->allowedChildren(), $assertionMessage);
1235
    }
1236
1237
    /**
1238
     * @return array
1239
     */
1240
    public function allowedChildrenProvider()
1241
    {
1242
        return [
1243
            [
1244
                // Class name
1245
                SiteTreeTest_ClassA::class,
1246
                // Expected
1247
                [ SiteTreeTest_ClassB::class ],
1248
                // Assertion message
1249
                'Direct setting of allowed children',
1250
            ],
1251
            [
1252
                SiteTreeTest_ClassB::class,
1253
                [ SiteTreeTest_ClassC::class, SiteTreeTest_ClassCext::class ],
1254
                'Includes subclasses',
1255
            ],
1256
            [
1257
                SiteTreeTest_ClassC::class,
1258
                [],
1259
                'Null setting',
1260
            ],
1261
            [
1262
                SiteTreeTest_ClassD::class,
1263
                [SiteTreeTest_ClassC::class],
1264
                'Excludes subclasses if class is prefixed by an asterisk',
1265
            ],
1266
        ];
1267
    }
1268
1269
    public function testAllowedChildrenValidation()
1270
    {
1271
        $page = SiteTree::create();
1272
        $page->write();
1273
        $classA = new SiteTreeTest_ClassA();
1274
        $classA->write();
1275
        $classB = new SiteTreeTest_ClassB();
1276
        $classB->write();
1277
        $classC = new SiteTreeTest_ClassC();
1278
        $classC->write();
1279
        $classD = new SiteTreeTest_ClassD();
1280
        $classD->write();
1281
        $classCext = new SiteTreeTest_ClassCext();
1282
        $classCext->write();
1283
1284
        $classB->ParentID = $page->ID;
1285
        $valid = $classB->doValidate();
1286
        $this->assertTrue($valid->isValid(), "Does allow children on unrestricted parent");
1287
1288
        $classB->ParentID = $classA->ID;
1289
        $valid = $classB->doValidate();
1290
        $this->assertTrue($valid->isValid(), "Does allow child specifically allowed by parent");
1291
1292
        $classC->ParentID = $classA->ID;
1293
        $valid = $classC->doValidate();
1294
        $this->assertFalse($valid->isValid(), "Doesnt allow child on parents specifically restricting children");
1295
1296
        $classB->ParentID = $classC->ID;
1297
        $valid = $classB->doValidate();
1298
        $this->assertFalse($valid->isValid(), "Doesnt allow child on parents disallowing all children");
1299
1300
        $classB->ParentID = $classCext->ID;
1301
        $valid = $classB->doValidate();
1302
        $this->assertTrue($valid->isValid(), "Extensions of allowed classes are incorrectly reported as invalid");
1303
1304
        $classCext->ParentID = $classD->ID;
1305
        $valid = $classCext->doValidate();
1306
        $this->assertFalse(
1307
            $valid->isValid(),
1308
            "Doesnt allow child where only parent class is allowed on parent node, and asterisk prefixing is used"
1309
        );
1310
    }
1311
1312
    public function testClassDropdown()
1313
    {
1314
        $sitetree = SiteTree::create();
1315
        $method = new ReflectionMethod($sitetree, 'getClassDropdown');
1316
        $method->setAccessible(true);
1317
1318
        Security::setCurrentUser(null);
1319
        $this->assertArrayNotHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1320
1321
        $this->loginWithPermission('ADMIN');
1322
        $this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1323
1324
        $this->loginWithPermission('CMS_ACCESS_CMSMain');
1325
        $this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1326
1327
        Security::setCurrentUser(null);
1328
    }
1329
1330
    public function testCanBeRoot()
1331
    {
1332
        $page = SiteTree::create();
1333
        $page->ParentID = 0;
1334
        $page->write();
1335
1336
        $notRootPage = new SiteTreeTest_NotRoot();
1337
        $notRootPage->ParentID = 0;
1338
        $isDetected = false;
1339
        try {
1340
            $notRootPage->write();
1341
        } catch (ValidationException $e) {
1342
            $this->assertContains('is not allowed on the root level', $e->getMessage());
1343
            $isDetected = true;
1344
        }
1345
1346
        if (!$isDetected) {
1347
            $this->fail('Fails validation with $can_be_root=false');
1348
        }
1349
    }
1350
1351
    public function testModifyStatusFlagByInheritance()
1352
    {
1353
        $node = new SiteTreeTest_StageStatusInherit();
1354
        $treeTitle = $node->getTreeTitle();
1355
        $this->assertContains('InheritedTitle', $treeTitle);
1356
        $this->assertContains('inherited-class', $treeTitle);
1357
    }
1358
1359
    public function testMenuTitleIsUnsetWhenEqualsTitle()
1360
    {
1361
        $page = SiteTree::create();
1362
        $page->Title = 'orig';
1363
        $page->MenuTitle = 'orig';
1364
        $page->write();
1365
1366
        // change menu title
1367
        $page->MenuTitle = 'changed';
1368
        $page->write();
1369
        $page = SiteTree::get()->byID($page->ID);
1370
        $this->assertEquals('changed', $page->getField('MenuTitle'));
1371
1372
        // change menu title back
1373
        $page->MenuTitle = 'orig';
1374
        $page->write();
1375
        $page = SiteTree::get()->byID($page->ID);
1376
        $this->assertEquals(null, $page->getField('MenuTitle'));
1377
    }
1378
1379
    public function testMetaTagGeneratorDisabling()
1380
    {
1381
        $generator = Config::inst()->get(SiteTree::class, 'meta_generator');
1382
1383
        // Stub to ensure customisations don't affect the test
1384
        Config::modify()->set(SiteTree::class, 'meta_generator', 'SilverStripe - https://www.silverstripe.org');
1385
1386
        $page = new SiteTreeTest_PageNode();
1387
1388
        $meta = $page->MetaTags();
1389
        $this->assertContains('meta name="generator"', $meta, 'Should include generator tag');
1390
        $this->assertContains(
1391
            'content="SilverStripe - https://www.silverstripe.org',
1392
            $meta,
1393
            'Should contain default meta generator info'
1394
        );
1395
1396
        // test proper escaping of quotes in attribute value
1397
        Config::modify()->set(SiteTree::class, 'meta_generator', 'Generator with "quotes" in it');
1398
        $meta = $page->MetaTags();
1399
        $this->assertContains(
1400
            'content="Generator with &quot;quotes&quot; in it',
1401
            $meta,
1402
            'test proper escaping of values from Config'
1403
        );
1404
1405
        // test empty generator - no tag should appear at all
1406
        Config::modify()->set(SiteTree::class, 'meta_generator', '');
1407
        $meta = $page->MetaTags();
1408
        $this->assertNotContains(
1409
            'meta name="generator"',
1410
            $meta,
1411
            'test blank value means no tag generated'
1412
        );
1413
    }
1414
1415
1416
    public function testGetBreadcrumbItems()
1417
    {
1418
        $page = $this->objFromFixture(SiteTree::class, "breadcrumbs");
1419
        $this->assertEquals(1, $page->getBreadcrumbItems()->count(), "Only display current page.");
1420
1421
        // Test breadcrumb order
1422
        $page = $this->objFromFixture(SiteTree::class, "breadcrumbs5");
1423
        $breadcrumbs = $page->getBreadcrumbItems();
1424
        $this->assertEquals($breadcrumbs->count(), 5, "Display all breadcrumbs");
1425
        $this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs", "Breadcrumbs should be the first item.");
1426
        $this->assertEquals($breadcrumbs->last()->Title, "Breadcrumbs 5", "Breadcrumbs 5 should be last item.");
1427
1428
        // Test breadcrumb max depth
1429
        $breadcrumbs = $page->getBreadcrumbItems(2);
1430
        $this->assertEquals($breadcrumbs->count(), 2, "Max depth should limit the breadcrumbs to 2 items.");
1431
        $this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs 4", "First item should be Breadrcumbs 4.");
1432
        $this->assertEquals($breadcrumbs->last()->Title, "Breadcrumbs 5", "Breadcrumbs 5 should be last.");
1433
    }
1434
1435
    /**
1436
     * Tests SiteTree::MetaTags
1437
     * Note that this test makes no assumption on the closing of tags (other than <title></title>)
1438
     */
1439
    public function testMetaTags()
1440
    {
1441
        $this->logInWithPermission('ADMIN');
1442
        $page = $this->objFromFixture(SiteTree::class, 'metapage');
1443
1444
        // Test with title
1445
        $meta = $page->MetaTags();
1446
        $charset = Config::inst()->get(ContentNegotiator::class, 'encoding');
1447
        $this->assertContains('<meta http-equiv="Content-Type" content="text/html; charset=' . $charset . '"', $meta);
1448
        $this->assertContains('<meta name="description" content="The &lt;br /&gt; and &lt;br&gt; tags"', $meta);
1449
        $this->assertContains('<link rel="canonical" href="http://www.mysite.com/html-and-xml"', $meta);
1450
        $this->assertContains('<meta name="x-page-id" content="' . $page->ID . '"', $meta);
1451
        $this->assertContains('<meta name="x-cms-edit-link" content="' . $page->CMSEditLink() . '"', $meta);
1452
        $this->assertContains('<title>HTML &amp; XML</title>', $meta);
1453
1454
        // Test without title
1455
        $meta = $page->MetaTags(false);
1456
        $this->assertNotContains('<title>', $meta);
1457
1458
        $meta = $page->MetaTags('false');
1459
        $this->assertNotContains('<title>', $meta);
1460
    }
1461
1462
    public function testMetaComponents()
1463
    {
1464
        $this->logInWithPermission('ADMIN');
1465
        /** @var SiteTree $page */
1466
        $page = $this->objFromFixture('Page', 'metapage');
1467
1468
        $charset = Config::inst()->get(ContentNegotiator::class, 'encoding');
1469
1470
        $expected = [
1471
            'title' => [
1472
                'tag' => 'title',
1473
                'content' => "HTML &amp; XML",
1474
            ],
1475
            'generator' => [
1476
                'attributes' => [
1477
                    'name' => 'generator',
1478
                    'content' => Config::inst()->get(SiteTree::class, 'meta_generator')
1479
                ],
1480
            ],
1481
            'contentType' => [
1482
                'attributes' => [
1483
                    'http-equiv' => 'Content-Type',
1484
                    'content' => "text/html; charset=$charset",
1485
                ],
1486
            ],
1487
            'description' => [
1488
                'attributes' => [
1489
                    'name' => 'description',
1490
                    'content' => 'The <br /> and <br> tags'
1491
                ]
1492
            ],
1493
            'pageId' => [
1494
                'attributes' => [
1495
                    'name' => 'x-page-id',
1496
                    'content' => $page->ID
1497
                ],
1498
            ],
1499
            'cmsEditLink' => [
1500
                'attributes' => [
1501
                    'name' => 'x-cms-edit-link',
1502
                    'content' => $page->CMSEditLink()
1503
                ]
1504
            ]
1505
        ];
1506
1507
        $this->assertEquals($expected, $page->MetaComponents());
1508
    }
1509
1510
    /**
1511
     * Test that orphaned pages are handled correctly
1512
     */
1513
    public function testOrphanedPages()
1514
    {
1515
        $origStage = Versioned::get_reading_mode();
1516
1517
        // Setup user who can view draft content, but lacks cms permission.
1518
        // To users such as this, orphaned pages should be inaccessible. canView for these pages is only
1519
        // necessary for admin / cms users, who require this permission to edit / rearrange these pages.
1520
        $permission = new Permission();
1521
        $permission->Code = 'VIEW_DRAFT_CONTENT';
1522
        $group = new Group(['Title' => 'Staging Users']);
1523
        $group->write();
1524
        $group->Permissions()->add($permission);
1525
        $member = new Member();
1526
        $member->Email = '[email protected]';
1527
        $member->write();
1528
        $member->Groups()->add($group);
1529
1530
        // both pages are viewable in stage
1531
        Versioned::set_stage(Versioned::DRAFT);
1532
        $about = $this->objFromFixture(SiteTree::class, 'about');
1533
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1534
        $this->assertFalse($about->isOrphaned());
1535
        $this->assertFalse($staff->isOrphaned());
1536
        $this->assertTrue($about->canView($member));
1537
        $this->assertTrue($staff->canView($member));
1538
1539
        // Publishing only the child page to live should orphan the live record, but not the staging one
1540
        $staff->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1541
        $this->assertFalse($staff->isOrphaned());
1542
        $this->assertTrue($staff->canView($member));
1543
        Versioned::set_stage(Versioned::LIVE);
1544
        $staff = $this->objFromFixture(SiteTree::class, 'staff'); // Live copy of page
1545
        $this->assertTrue($staff->isOrphaned()); // because parent isn't published
1546
        $this->assertFalse($staff->canView($member));
1547
1548
        // Publishing the parent page should restore visibility
1549
        Versioned::set_stage(Versioned::DRAFT);
1550
        $about = $this->objFromFixture(SiteTree::class, 'about');
1551
        $about->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1552
        Versioned::set_stage(Versioned::LIVE);
1553
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1554
        $this->assertFalse($staff->isOrphaned());
1555
        $this->assertTrue($staff->canView($member));
1556
1557
        // Removing staging page should not prevent live page being visible
1558
        $about->deleteFromStage('Stage');
1559
        $staff->deleteFromStage('Stage');
1560
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1561
        $this->assertFalse($staff->isOrphaned());
1562
        $this->assertTrue($staff->canView($member));
1563
1564
        // Cleanup
1565
        Versioned::set_reading_mode($origStage);
1566
    }
1567
1568
    /**
1569
     * Test archived page behaviour
1570
     */
1571
    public function testArchivedPages()
1572
    {
1573
        $this->logInWithPermission('ADMIN');
1574
1575
        /** @var SiteTree $page */
1576
        $page = $this->objFromFixture(SiteTree::class, 'home');
1577
        $this->assertTrue($page->canAddChildren());
1578
        $this->assertTrue($page->isOnDraft());
1579
        $this->assertFalse($page->isPublished());
1580
1581
        // Publish
1582
        $page->publishRecursive();
1583
        $this->assertTrue($page->canAddChildren());
1584
        $this->assertTrue($page->isOnDraft());
1585
        $this->assertTrue($page->isPublished());
1586
1587
        // Archive
1588
        $page->doArchive();
1589
        $this->assertFalse($page->canAddChildren());
1590
        $this->assertFalse($page->isOnDraft());
1591
        $this->assertTrue($page->isArchived());
1592
        $this->assertFalse($page->isPublished());
1593
    }
1594
1595
    public function testCanNot()
1596
    {
1597
        // Test that
1598
        $this->logInWithPermission('ADMIN');
1599
        $page = new SiteTreeTest_AdminDenied();
1600
        $this->assertFalse($page->canCreate());
1601
        $this->assertFalse($page->canEdit());
1602
        $this->assertFalse($page->canDelete());
1603
        $this->assertFalse($page->canAddChildren());
1604
        $this->assertFalse($page->canView());
1605
    }
1606
1607
    public function testCanPublish()
1608
    {
1609
        $page = new SiteTreeTest_ClassD();
1610
        $this->logOut();
1611
1612
        // Test that false overrides any can_publish = true
1613
        SiteTreeTest_ExtensionA::$can_publish = true;
1614
        SiteTreeTest_ExtensionB::$can_publish = false;
1615
        $this->assertFalse($page->canPublish());
1616
        SiteTreeTest_ExtensionA::$can_publish = false;
1617
        SiteTreeTest_ExtensionB::$can_publish = true;
1618
        $this->assertFalse($page->canPublish());
1619
1620
        // Test null extensions fall back to canEdit()
1621
        SiteTreeTest_ExtensionA::$can_publish = null;
1622
        SiteTreeTest_ExtensionB::$can_publish = null;
1623
        $page->canEditValue = true;
1624
        $this->assertTrue($page->canPublish());
1625
        $page->canEditValue = false;
1626
        $this->assertFalse($page->canPublish());
1627
    }
1628
1629
    /**
1630
     * Test url rewriting extensions
1631
     */
1632
    public function testLinkExtension()
1633
    {
1634
        Director::config()->set('alternate_base_url', 'http://www.baseurl.com');
1635
        $page = new SiteTreeTest_ClassD();
1636
        $page->URLSegment = 'classd';
1637
        $page->write();
1638
        $this->assertEquals(
1639
            'http://www.updatedhost.com/classd/myaction?extra=1',
1640
            $page->Link('myaction')
1641
        );
1642
        $this->assertEquals(
1643
            'http://www.updatedhost.com/classd/myaction?extra=1',
1644
            $page->AbsoluteLink('myaction')
1645
        );
1646
        $this->assertEquals(
1647
            'classd/myaction',
1648
            $page->RelativeLink('myaction')
1649
        );
1650
    }
1651
1652
    /**
1653
     * Test that the controller name for a SiteTree instance can be gathered by appending "Controller" to the SiteTree
1654
     * class name in a PSR-2 compliant manner.
1655
     */
1656
    public function testGetControllerName()
1657
    {
1658
        $class = Page::create();
1659
        $this->assertSame('PageController', $class->getControllerName());
1660
    }
1661
1662
    /**
1663
     * Test that the controller name for a SiteTree instance can be gathered when set directly via config var
1664
     */
1665
    public function testGetControllerNameFromConfig()
1666
    {
1667
        Config::inst()->update(Page::class, 'controller_name', 'This\\Is\\A\\New\\Controller');
1668
        $class = new Page();
1669
        $this->assertSame('This\\Is\\A\\New\\Controller', $class->getControllerName());
1670
    }
1671
1672
    /**
1673
     * Test that underscored class names (legacy) are still supported (deprecation notice is issued though).
1674
     */
1675
    public function testGetControllerNameWithUnderscoresIsSupported()
1676
    {
1677
        $class = new SiteTreeTest_LegacyControllerName();
1678
        $this->assertEquals(SiteTreeTest_LegacyControllerName_Controller::class, $class->getControllerName());
1679
    }
1680
1681
    public function testTreeTitleCache()
1682
    {
1683
        $siteTree = SiteTree::create();
1684
        $user = $this->objFromFixture(Member::class, 'allsections');
1685
        Security::setCurrentUser($user);
1686
        $pageClass = array_values(SiteTree::page_type_classes())[0];
1687
1688
        $mockPageMissesCache = $this->getMockBuilder($pageClass)
1689
            ->setMethods(['canCreate'])
1690
            ->getMock();
1691
        $mockPageMissesCache
1692
            ->expects($this->exactly(3))
1693
            ->method('canCreate');
1694
1695
        $mockPageHitsCache = $this->getMockBuilder($pageClass)
1696
            ->setMethods(['canCreate'])
1697
            ->getMock();
1698
        $mockPageHitsCache
1699
            ->expects($this->never())
1700
            ->method('canCreate');
1701
1702
        // Initially, cache misses (1)
1703
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1704
        $title = $siteTree->getTreeTitle();
1705
        $this->assertNotNull($title);
1706
1707
        // Now it hits
1708
        Injector::inst()->registerService($mockPageHitsCache, $pageClass);
1709
        $title = $siteTree->getTreeTitle();
1710
        $this->assertNotNull($title);
1711
1712
1713
        // Mutating member record invalidates cache. Misses (2)
1714
        $user->FirstName = 'changed';
1715
        $user->write();
1716
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1717
        $title = $siteTree->getTreeTitle();
1718
        $this->assertNotNull($title);
1719
1720
        // Now it hits again
1721
        Injector::inst()->registerService($mockPageHitsCache, $pageClass);
1722
        $title = $siteTree->getTreeTitle();
1723
        $this->assertNotNull($title);
1724
1725
        // Different user. Misses. (3)
1726
        $user = $this->objFromFixture(Member::class, 'editor');
1727
        Security::setCurrentUser($user);
1728
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1729
        $title = $siteTree->getTreeTitle();
1730
        $this->assertNotNull($title);
1731
    }
1732
1733
    public function testDependentPagesOnUnsavedRecord()
1734
    {
1735
        $record = new SiteTree();
1736
        $pages = $record->DependentPages();
1737
        $this->assertCount(0, $pages, 'Unsaved pages should have no dependent pages');
1738
    }
1739
}
1740