Completed
Push — master ( 3387be...b922df )
by Loz
18s queued 14s
created

tests/php/Model/SiteTreeTest.php (1 issue)

Severity
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\FieldType\DBDatetime;
22
use SilverStripe\ORM\ValidationException;
23
use SilverStripe\Security\Group;
24
use SilverStripe\Security\InheritedPermissions;
25
use SilverStripe\Security\Member;
26
use SilverStripe\Security\Permission;
27
use SilverStripe\Security\Security;
28
use SilverStripe\SiteConfig\SiteConfig;
29
use SilverStripe\Subsites\Extensions\SiteTreeSubsites;
30
use SilverStripe\Versioned\Versioned;
31
use SilverStripe\View\Parsers\Diff;
32
use SilverStripe\View\Parsers\ShortcodeParser;
33
use SilverStripe\View\Parsers\URLSegmentFilter;
34
use TractorCow\Fluent\Extension\FluentSiteTreeExtension;
35
use const ASSETS_DIR;
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' => ManifestFileFinder::RESOURCES_DIR]);
161
        $id = $page->write();
162
        $page = SiteTree::get()->byID($id);
163
        $this->assertSame(ManifestFileFinder::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 (ManifestFileFinder::RESOURCES_DIR !== 'resources') {
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 (ManifestFileFinder::RESOURCES_DIR === 'resources') {
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->assertNotNull(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
        DBDatetime::set_mock_now('2009-07-02 14:05:07');
690
        $oldPage = SiteTree::create();
691
        $oldPage->Title = 'A really old page';
692
        $oldPage->write();
693
        DBDatetime::clear_mock_now();
694
695
        $date = '2009-07-02 14:05:07';
696
        Versioned::reading_archived_date($date);
697
        $result = SiteTree::get()->where([
698
            '"SiteTree"."ParentID"' => 0
699
        ]);
700
        $this->assertCount(1, $result, '"A really old page" should be returned');
701
    }
702
703
    public function testEditPermissions()
704
    {
705
        $editor = $this->objFromFixture(Member::class, "editor");
706
707
        $home = $this->objFromFixture(SiteTree::class, "home");
708
        $staff = $this->objFromFixture(SiteTree::class, "staff");
709
        $products = $this->objFromFixture(SiteTree::class, "products");
710
        $product1 = $this->objFromFixture(SiteTree::class, "product1");
711
        $product4 = $this->objFromFixture(SiteTree::class, "product4");
712
713
        // Test logged out users cannot edit
714
        $this->logOut();
715
        $this->assertFalse($staff->canEdit());
716
717
        // Can't edit a page that is locked to admins
718
        $this->assertFalse($home->canEdit($editor));
719
720
        // Can edit a page that is locked to editors
721
        $this->assertTrue($products->canEdit($editor));
722
723
        // Can edit a child of that page that inherits
724
        $this->assertTrue($product1->canEdit($editor));
725
726
        // Can't edit a child of that page that has its permissions overridden
727
        $this->assertFalse($product4->canEdit($editor));
728
    }
729
730
    public function testCanEditWithAccessToAllSections()
731
    {
732
        $page = SiteTree::create();
733
        $page->write();
734
        $allSectionMember = $this->objFromFixture(Member::class, 'allsections');
735
        $securityAdminMember = $this->objFromFixture(Member::class, 'securityadmin');
736
737
        $this->assertTrue($page->canEdit($allSectionMember));
738
        $this->assertFalse($page->canEdit($securityAdminMember));
739
    }
740
741
    public function testCreatePermissions()
742
    {
743
        // Test logged out users cannot create
744
        $this->logOut();
745
        $this->assertFalse(singleton(SiteTree::class)->canCreate());
746
747
        // Login with another permission
748
        $this->logInWithPermission('DUMMY');
749
        $this->assertFalse(singleton(SiteTree::class)->canCreate());
750
751
        // Login with basic CMS permission
752
        $perms = SiteConfig::config()->required_permission;
753
        $this->logInWithPermission(reset($perms));
754
        $this->assertTrue(singleton(SiteTree::class)->canCreate());
755
756
        // Test creation underneath a parent which this user doesn't have access to
757
        $parent = $this->objFromFixture(SiteTree::class, 'about');
758
        $this->assertFalse(singleton(SiteTree::class)->canCreate(null, ['Parent' => $parent]));
759
760
        // Test creation underneath a parent which doesn't allow a certain child
761
        $parentB = new SiteTreeTest_ClassB();
762
        $parentB->Title = 'Only Allows SiteTreeTest_ClassC';
763
        $parentB->write();
764
        $this->assertTrue(singleton(SiteTreeTest_ClassA::class)->canCreate(null));
765
        $this->assertFalse(singleton(SiteTreeTest_ClassA::class)->canCreate(null, ['Parent' => $parentB]));
766
        $this->assertTrue(singleton(SiteTreeTest_ClassC::class)->canCreate(null, ['Parent' => $parentB]));
767
768
        // Test creation underneath a parent which doesn't exist in the database. This should
769
        // fall back to checking whether the user can create pages at the root of the site
770
        $this->assertTrue(singleton(SiteTree::class)->canCreate(null, ['Parent' => singleton(SiteTree::class)]));
771
772
        //Test we don't check for allowedChildren on parent context if it's not SiteTree instance
773
        $this->assertTrue(
774
            singleton(SiteTree::class)->canCreate(
775
                null,
776
                ['Parent' => $this->objFromFixture(SiteTreeTest_DataObject::class, 'relations')]
777
            )
778
        );
779
    }
780
781
    public function testEditPermissionsOnDraftVsLive()
782
    {
783
        // Create an inherit-permission page
784
        $page = SiteTree::create();
785
        $page->write();
786
        $page->CanEditType = "Inherit";
787
        $page->publishRecursive();
788
        $pageID = $page->ID;
789
790
        // Lock down the site config
791
        $sc = $page->SiteConfig;
792
        $sc->CanEditType = 'OnlyTheseUsers';
793
        $sc->EditorGroups()->add($this->idFromFixture(Group::class, 'admins'));
794
        $sc->write();
795
796
        // Confirm that Member.editor can't edit the page
797
        $member = $this->objFromFixture(Member::class, 'editor');
798
        Security::setCurrentUser($member);
799
        $this->assertFalse($page->canEdit());
800
801
        // Change the page to be editable by Group.editors, but do not publish
802
        $admin = $this->objFromFixture(Member::class, 'admin');
803
        Security::setCurrentUser($admin);
804
        $page->CanEditType = 'OnlyTheseUsers';
805
        $page->EditorGroups()->add($this->idFromFixture(Group::class, 'editors'));
806
        $page->write();
807
808
        // Clear permission cache
809
        /** @var InheritedPermissions $checker */
810
        $checker = SiteTree::getPermissionChecker();
811
        $checker->clearCache();
812
813
        // Confirm that Member.editor can now edit the page
814
        $member = $this->objFromFixture(Member::class, 'editor');
815
        Security::setCurrentUser($member);
816
        $this->assertTrue($page->canEdit());
817
818
        // Publish the changes to the page
819
        $admin = $this->objFromFixture(Member::class, 'admin');
820
        Security::setCurrentUser($admin);
821
        $page->publishRecursive();
822
823
        // Confirm that Member.editor can still edit the page
824
        $member = $this->objFromFixture(Member::class, 'editor');
825
        Security::setCurrentUser($member);
826
        $this->assertTrue($page->canEdit());
827
    }
828
829
    public function testCompareVersions()
830
    {
831
        // Necessary to avoid
832
        $oldCleanerClass = Diff::$html_cleaner_class;
833
        Diff::$html_cleaner_class = SiteTreeTest_NullHtmlCleaner::class;
834
835
        $page = SiteTree::create();
836
        $page->write();
837
        $this->assertEquals(1, $page->Version);
838
839
        // Use inline element to avoid double wrapping applied to
840
        // blocklevel elements depending on HTMLCleaner implementation:
841
        // <ins><p> gets converted to <ins><p><inst>
842
        $page->Content = "<span>This is a test</span>";
843
        $page->write();
844
        $this->assertEquals(2, $page->Version);
845
846
        $diff = $page->compareVersions(1, 2);
847
848
        $processedContent = trim($diff->Content);
849
        $processedContent = preg_replace('/\s*</', '<', $processedContent);
850
        $processedContent = preg_replace('/>\s*/', '>', $processedContent);
851
        $this->assertEquals("<ins><span>This is a test</span></ins>", $processedContent);
852
853
        Diff::$html_cleaner_class = $oldCleanerClass;
854
    }
855
856
    public function testAuthorIDAndPublisherIDFilledOutOnPublish()
857
    {
858
        // Ensure that we have a member ID who is doing all this work
859
        $member = $this->objFromFixture(Member::class, "admin");
860
        $this->logInAs($member);
861
862
        // Write the page
863
        $about = $this->objFromFixture(SiteTree::class, 'about');
864
        $about->Title = "Another title";
865
        $about->write();
866
867
        // Check the version created
868
        $savedVersion = DB::prepared_query(
869
            "SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
870
            WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
871
            [$about->ID]
872
        )->first();
873
        $this->assertEquals($member->ID, $savedVersion['AuthorID']);
874
        $this->assertEquals(0, $savedVersion['PublisherID']);
875
876
        // Publish the page
877
        $about->publishRecursive();
878
        $publishedVersion = DB::prepared_query(
879
            "SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
880
            WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
881
            [$about->ID]
882
        )->first();
883
884
        // Check the version created
885
        $this->assertEquals($member->ID, $publishedVersion['AuthorID']);
886
        $this->assertEquals($member->ID, $publishedVersion['PublisherID']);
887
    }
888
889
    public function testLinkShortcodeHandler()
890
    {
891
        $aboutPage = $this->objFromFixture(SiteTree::class, 'about');
892
        $redirectPage = $this->objFromFixture(RedirectorPage::class, 'external');
893
894
        $parser = new ShortcodeParser();
895
        $parser->register('sitetree_link', [SiteTree::class, 'link_shortcode_handler']);
896
897
        $aboutShortcode = sprintf('[sitetree_link,id=%d]', $aboutPage->ID);
898
        $aboutEnclosed  = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $aboutPage->ID);
899
900
        $aboutShortcodeExpected = $aboutPage->Link();
901
        $aboutEnclosedExpected  = sprintf('<a href="%s">Example Content</a>', $aboutPage->Link());
902
903
        $this->assertEquals(
904
            $aboutShortcodeExpected,
905
            $parser->parse($aboutShortcode),
906
            'Test that simple linking works.'
907
        );
908
        $this->assertEquals(
909
            $aboutEnclosedExpected,
910
            $parser->parse($aboutEnclosed),
911
            'Test enclosed content is linked.'
912
        );
913
914
        $aboutPage->delete();
915
916
        $this->assertEquals(
917
            $aboutShortcodeExpected,
918
            $parser->parse($aboutShortcode),
919
            'Test that deleted pages still link.'
920
        );
921
        $this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed));
922
923
        $aboutShortcode = '[sitetree_link,id="-1"]';
924
        $aboutEnclosed  = '[sitetree_link,id="-1"]Example Content[/sitetree_link]';
925
926
        $this->assertEquals('', $parser->parse($aboutShortcode), 'Test empty result if no suitable matches.');
927
        $this->assertEquals('', $parser->parse($aboutEnclosed));
928
929
        $redirectShortcode = sprintf('[sitetree_link,id=%d]', $redirectPage->ID);
930
        $redirectEnclosed  = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $redirectPage->ID);
931
        $redirectExpected = 'http://www.google.com?a&amp;b';
932
933
        $this->assertEquals($redirectExpected, $parser->parse($redirectShortcode));
934
        $this->assertEquals(
935
            sprintf('<a href="%s">Example Content</a>', $redirectExpected),
936
            $parser->parse($redirectEnclosed)
937
        );
938
939
        $this->assertEquals('', $parser->parse('[sitetree_link]'), 'Test that invalid ID attributes are not parsed.');
940
        $this->assertEquals('', $parser->parse('[sitetree_link,id="text"]'));
941
        $this->assertEquals('', $parser->parse('[sitetree_link]Example Content[/sitetree_link]'));
942
    }
943
944
    public function testIsCurrent()
945
    {
946
        $aboutPage = $this->objFromFixture(SiteTree::class, 'about');
947
        $productPage = $this->objFromFixture(SiteTree::class, 'products');
948
949
        Director::set_current_page($aboutPage);
950
        $this->assertTrue($aboutPage->isCurrent(), 'Assert that basic isCurrent checks works.');
951
        $this->assertFalse($productPage->isCurrent());
952
953
        $this->assertTrue(
954
            DataObject::get_one(SiteTree::class, [
955
                '"SiteTree"."Title"' => 'About Us',
956
            ])->isCurrent(),
957
            'Assert that isCurrent works on another instance with the same ID.'
958
        );
959
960
        Director::set_current_page($newPage = SiteTree::create());
961
        $this->assertTrue($newPage->isCurrent(), 'Assert that isCurrent works on unsaved pages.');
962
    }
963
964
    public function testIsSection()
965
    {
966
        $about = $this->objFromFixture(SiteTree::class, 'about');
967
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
968
        $ceo   = $this->objFromFixture(SiteTree::class, 'ceo');
969
970
        Director::set_current_page($about);
971
        $this->assertTrue($about->isSection());
972
        $this->assertFalse($staff->isSection());
973
        $this->assertFalse($ceo->isSection());
974
975
        Director::set_current_page($staff);
976
        $this->assertTrue($about->isSection());
977
        $this->assertTrue($staff->isSection());
978
        $this->assertFalse($ceo->isSection());
979
980
        Director::set_current_page($ceo);
981
        $this->assertTrue($about->isSection());
982
        $this->assertTrue($staff->isSection());
983
        $this->assertTrue($ceo->isSection());
984
    }
985
986
    public function testURLSegmentReserved()
987
    {
988
        $siteTree = SiteTree::create(['URLSegment' => 'admin']);
989
        $segment = $siteTree->validURLSegment();
990
991
        $this->assertFalse($segment);
992
    }
993
994
    public function testURLSegmentAutoUpdate()
995
    {
996
        $sitetree = SiteTree::create();
997
        $sitetree->Title = _t(
998
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
999
            'New {pagetype}',
1000
            ['pagetype' => $sitetree->i18n_singular_name()]
1001
        );
1002
        $sitetree->write();
1003
        $this->assertEquals(
1004
            'new-page',
1005
            $sitetree->URLSegment,
1006
            'Sets based on default title on first save'
1007
        );
1008
1009
        $sitetree->Title = 'Changed';
1010
        $sitetree->write();
1011
        $this->assertEquals(
1012
            'changed',
1013
            $sitetree->URLSegment,
1014
            'Auto-updates when set to default title'
1015
        );
1016
1017
        $sitetree->Title = 'Changed again';
1018
        $sitetree->write();
1019
        $this->assertEquals(
1020
            'changed',
1021
            $sitetree->URLSegment,
1022
            'Does not auto-update once title has been changed'
1023
        );
1024
    }
1025
1026
    public function testURLSegmentAutoUpdateLocalized()
1027
    {
1028
        $oldLocale = i18n::get_locale();
1029
        i18n::set_locale('de_DE');
1030
1031
        $sitetree = SiteTree::create();
1032
        $sitetree->Title = _t(
1033
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1034
            'New {pagetype}',
1035
            ['pagetype' => $sitetree->i18n_singular_name()]
1036
        );
1037
        $sitetree->write();
1038
        $this->assertEquals(
1039
            'neue-seite',
1040
            $sitetree->URLSegment,
1041
            'Sets based on default title on first save'
1042
        );
1043
1044
        $sitetree->Title = 'Changed';
1045
        $sitetree->write();
1046
        $this->assertEquals(
1047
            'changed',
1048
            $sitetree->URLSegment,
1049
            'Auto-updates when set to default title'
1050
        );
1051
1052
        $sitetree->Title = 'Changed again';
1053
        $sitetree->write();
1054
        $this->assertEquals(
1055
            'changed',
1056
            $sitetree->URLSegment,
1057
            'Does not auto-update once title has been changed'
1058
        );
1059
1060
        i18n::set_locale($oldLocale);
1061
    }
1062
1063
    /**
1064
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1065
     */
1066
    public function testValidURLSegmentURLSegmentConflicts()
1067
    {
1068
        $sitetree = SiteTree::create();
1069
        SiteTree::config()->nested_urls = false;
1070
1071
        $sitetree->URLSegment = 'home';
1072
        $this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
1073
        $sitetree->URLSegment = 'home-noconflict';
1074
        $this->assertTrue($sitetree->validURLSegment());
1075
1076
        $sitetree->ParentID   = $this->idFromFixture(SiteTree::class, 'about');
1077
        $sitetree->URLSegment = 'home';
1078
        $this->assertFalse($sitetree->validURLSegment(), 'Conflicts are still recognised with a ParentID value');
1079
1080
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
1081
1082
        $sitetree->ParentID   = 0;
1083
        $sitetree->URLSegment = 'home';
1084
        $this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
1085
1086
        $sitetree->ParentID = $this->idFromFixture(SiteTree::class, 'about');
1087
        $this->assertTrue($sitetree->validURLSegment(), 'URLSegments can be the same across levels');
1088
1089
        $sitetree->URLSegment = 'my-staff';
1090
        $this->assertFalse($sitetree->validURLSegment(), 'Nested URLSegment conflicts are recognised');
1091
        $sitetree->URLSegment = 'my-staff-noconflict';
1092
        $this->assertTrue($sitetree->validURLSegment());
1093
    }
1094
1095
    /**
1096
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1097
     */
1098
    public function testValidURLSegmentClassNameConflicts()
1099
    {
1100
        $sitetree = SiteTree::create();
1101
        $sitetree->URLSegment = Controller::class;
1102
1103
        $this->assertTrue($sitetree->validURLSegment(), 'Class names are no longer conflicts');
1104
    }
1105
1106
    /**
1107
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1108
     */
1109
    public function testValidURLSegmentControllerConflicts()
1110
    {
1111
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
1112
1113
        $sitetree = SiteTree::create();
1114
        $sitetree->ParentID = $this->idFromFixture(SiteTreeTest_Conflicted::class, 'parent');
1115
1116
        $sitetree->URLSegment = 'index';
1117
        $this->assertFalse($sitetree->validURLSegment(), 'index is not a valid URLSegment');
1118
1119
        $sitetree->URLSegment = 'conflicted-action';
1120
        $this->assertFalse($sitetree->validURLSegment(), 'allowed_actions conflicts are recognised');
1121
1122
        $sitetree->URLSegment = 'conflicted-template';
1123
        $this->assertFalse($sitetree->validURLSegment(), 'Action-specific template conflicts are recognised');
1124
1125
        $sitetree->URLSegment = 'valid';
1126
        $this->assertTrue($sitetree->validURLSegment(), 'Valid URLSegment values are allowed');
1127
    }
1128
1129
    public function testURLSegmentPrioritizesExtensionVotes()
1130
    {
1131
        $sitetree = SiteTree::create();
1132
        $sitetree->URLSegment = 'unique-segment';
1133
        $this->assertTrue($sitetree->validURLSegment());
1134
1135
        SiteTree::add_extension(SiteTreeTest_Extension::class);
1136
        $sitetree = SiteTree::create();
1137
        $sitetree->URLSegment = 'unique-segment';
1138
        $this->assertFalse($sitetree->validURLSegment());
1139
        SiteTree::remove_extension(SiteTreeTest_Extension::class);
1140
    }
1141
1142
    public function testURLSegmentMultiByte()
1143
    {
1144
        URLSegmentFilter::config()->set('default_allow_multibyte', true);
1145
        $sitetree = SiteTree::create();
1146
        $sitetree->write();
1147
1148
        $sitetree->URLSegment = 'brötchen';
1149
        $sitetree->write();
1150
        $sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
1151
        $this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
1152
1153
        $sitetree->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1154
        $sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
1155
        $this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
1156
        $sitetreeLive = Versioned::get_one_by_stage(
1157
            SiteTree::class,
1158
            Versioned::LIVE,
1159
            '"SiteTree"."ID" = ' . $sitetree->ID,
1160
            false
1161
        );
1162
        $this->assertEquals($sitetreeLive->URLSegment, rawurlencode('brötchen'));
1163
    }
1164
1165
    public function testVersionsAreCreated()
1166
    {
1167
        $p = SiteTree::create();
1168
        $p->Content = "one";
1169
        $p->write();
1170
        $this->assertEquals(1, $p->Version);
1171
1172
        // No changes don't bump version
1173
        $p->write();
1174
        $this->assertEquals(1, $p->Version);
1175
1176
        $p->Content = "two";
1177
        $p->write();
1178
        $this->assertEquals(2, $p->Version);
1179
1180
        // Only change meta-data don't bump version
1181
        $p->HasBrokenLink = true;
1182
        $p->write();
1183
        $p->HasBrokenLink = false;
1184
        $p->write();
1185
        $this->assertEquals(2, $p->Version);
1186
1187
        $p->Content = "three";
1188
        $p->write();
1189
        $this->assertEquals(3, $p->Version);
1190
    }
1191
1192
    public function testPageTypeClasses()
1193
    {
1194
        $classes = SiteTree::page_type_classes();
1195
        $this->assertNotContains(SiteTree::class, $classes, 'Page types do not include base class');
1196
        $this->assertContains('Page', $classes, 'Page types do contain subclasses');
1197
1198
        // Testing what happens in an incorrect config value is set - hide_ancestor should be a string
1199
        Config::modify()->set(SiteTreeTest_ClassA::class, 'hide_ancestor', true);
1200
        $newClasses = SiteTree::page_type_classes();
1201
        $this->assertEquals(
1202
            $classes,
1203
            $newClasses,
1204
            'Setting hide_ancestor to a boolean (incorrect) value caused a page class to be hidden'
1205
        );
1206
    }
1207
1208
    /**
1209
     * Tests that core subclasses of SiteTree are included in allowedChildren() by default, but not instances of
1210
     * HiddenClass
1211
     */
1212
    public function testAllowedChildrenContainsCoreSubclassesButNotHiddenClass()
1213
    {
1214
        $page = SiteTree::create();
1215
        $allowedChildren = $page->allowedChildren();
1216
1217
        $this->assertContains(
1218
            VirtualPage::class,
1219
            $allowedChildren,
1220
            'Includes core subclasses by default'
1221
        );
1222
1223
        $this->assertNotContains(
1224
            SiteTreeTest_ClassE::class,
1225
            $allowedChildren,
1226
            'HiddenClass instances should not be returned'
1227
        );
1228
    }
1229
1230
    /**
1231
     * Tests that various types of SiteTree classes will or will not be returned from the allowedChildren method
1232
     * @dataProvider allowedChildrenProvider
1233
     * @param string $className
1234
     * @param array  $expected
1235
     * @param string $assertionMessage
1236
     */
1237
    public function testAllowedChildren($className, $expected, $assertionMessage)
1238
    {
1239
        $class = new $className();
1240
        $this->assertEquals($expected, $class->allowedChildren(), $assertionMessage);
1241
    }
1242
1243
    /**
1244
     * @return array
1245
     */
1246
    public function allowedChildrenProvider()
1247
    {
1248
        return [
1249
            [
1250
                // Class name
1251
                SiteTreeTest_ClassA::class,
1252
                // Expected
1253
                [ SiteTreeTest_ClassB::class ],
1254
                // Assertion message
1255
                'Direct setting of allowed children',
1256
            ],
1257
            [
1258
                SiteTreeTest_ClassB::class,
1259
                [ SiteTreeTest_ClassC::class, SiteTreeTest_ClassCext::class ],
1260
                'Includes subclasses',
1261
            ],
1262
            [
1263
                SiteTreeTest_ClassC::class,
1264
                [],
1265
                'Null setting',
1266
            ],
1267
            [
1268
                SiteTreeTest_ClassD::class,
1269
                [SiteTreeTest_ClassC::class],
1270
                'Excludes subclasses if class is prefixed by an asterisk',
1271
            ],
1272
        ];
1273
    }
1274
1275
    public function testAllowedChildrenValidation()
1276
    {
1277
        $page = SiteTree::create();
1278
        $page->write();
1279
        $classA = new SiteTreeTest_ClassA();
1280
        $classA->write();
1281
        $classB = new SiteTreeTest_ClassB();
1282
        $classB->write();
1283
        $classC = new SiteTreeTest_ClassC();
1284
        $classC->write();
1285
        $classD = new SiteTreeTest_ClassD();
1286
        $classD->write();
1287
        $classCext = new SiteTreeTest_ClassCext();
1288
        $classCext->write();
1289
1290
        $classB->ParentID = $page->ID;
1291
        $valid = $classB->doValidate();
1292
        $this->assertTrue($valid->isValid(), "Does allow children on unrestricted parent");
1293
1294
        $classB->ParentID = $classA->ID;
1295
        $valid = $classB->doValidate();
1296
        $this->assertTrue($valid->isValid(), "Does allow child specifically allowed by parent");
1297
1298
        $classC->ParentID = $classA->ID;
1299
        $valid = $classC->doValidate();
1300
        $this->assertFalse($valid->isValid(), "Doesnt allow child on parents specifically restricting children");
1301
1302
        $classB->ParentID = $classC->ID;
1303
        $valid = $classB->doValidate();
1304
        $this->assertFalse($valid->isValid(), "Doesnt allow child on parents disallowing all children");
1305
1306
        $classB->ParentID = $classCext->ID;
1307
        $valid = $classB->doValidate();
1308
        $this->assertTrue($valid->isValid(), "Extensions of allowed classes are incorrectly reported as invalid");
1309
1310
        $classCext->ParentID = $classD->ID;
1311
        $valid = $classCext->doValidate();
1312
        $this->assertFalse(
1313
            $valid->isValid(),
1314
            "Doesnt allow child where only parent class is allowed on parent node, and asterisk prefixing is used"
1315
        );
1316
    }
1317
1318
    public function testClassDropdown()
1319
    {
1320
        $sitetree = SiteTree::create();
1321
        $method = new ReflectionMethod($sitetree, 'getClassDropdown');
1322
        $method->setAccessible(true);
1323
1324
        Security::setCurrentUser(null);
1325
        $this->assertArrayNotHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1326
1327
        $this->loginWithPermission('ADMIN');
1328
        $this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1329
1330
        $this->loginWithPermission('CMS_ACCESS_CMSMain');
1331
        $this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1332
1333
        Security::setCurrentUser(null);
1334
    }
1335
1336
    public function testCanBeRoot()
1337
    {
1338
        $this->expectException(ValidationException::class);
1339
        $this->expectExceptionMessageMatches('/is not allowed on the root level/');
1340
        $page = SiteTree::create();
1341
        $page->ParentID = 0;
1342
        $page->write();
1343
1344
        $notRootPage = new SiteTreeTest_NotRoot();
1345
        $notRootPage->ParentID = 0;
1346
        $isDetected = false;
0 ignored issues
show
The assignment to $isDetected is dead and can be removed.
Loading history...
1347
        $notRootPage->write();
1348
    }
1349
1350
    public function testModifyStatusFlagByInheritance()
1351
    {
1352
        $node = new SiteTreeTest_StageStatusInherit();
1353
        $treeTitle = $node->getTreeTitle();
1354
        $this->assertStringContainsString('InheritedTitle', $treeTitle);
1355
        $this->assertStringContainsString('inherited-class', $treeTitle);
1356
    }
1357
1358
    public function testMenuTitleIsUnsetWhenEqualsTitle()
1359
    {
1360
        $page = SiteTree::create();
1361
        $page->Title = 'orig';
1362
        $page->MenuTitle = 'orig';
1363
        $page->write();
1364
1365
        // change menu title
1366
        $page->MenuTitle = 'changed';
1367
        $page->write();
1368
        $page = SiteTree::get()->byID($page->ID);
1369
        $this->assertEquals('changed', $page->getField('MenuTitle'));
1370
1371
        // change menu title back
1372
        $page->MenuTitle = 'orig';
1373
        $page->write();
1374
        $page = SiteTree::get()->byID($page->ID);
1375
        $this->assertEquals(null, $page->getField('MenuTitle'));
1376
    }
1377
1378
    public function testMetaTagGeneratorDisabling()
1379
    {
1380
        $generator = Config::inst()->get(SiteTree::class, 'meta_generator');
1381
1382
        // Stub to ensure customisations don't affect the test
1383
        Config::modify()->set(SiteTree::class, 'meta_generator', 'SilverStripe - https://www.silverstripe.org');
1384
1385
        $page = new SiteTreeTest_PageNode();
1386
1387
        $meta = $page->MetaTags();
1388
        $this->assertStringContainsString('meta name="generator"', $meta, 'Should include generator tag');
1389
        $this->assertStringContainsString(
1390
            'content="SilverStripe - https://www.silverstripe.org',
1391
            $meta,
1392
            'Should contain default meta generator info'
1393
        );
1394
1395
        // test proper escaping of quotes in attribute value
1396
        Config::modify()->set(SiteTree::class, 'meta_generator', 'Generator with "quotes" in it');
1397
        $meta = $page->MetaTags();
1398
        $this->assertStringContainsString(
1399
            'content="Generator with &quot;quotes&quot; in it',
1400
            $meta,
1401
            'test proper escaping of values from Config'
1402
        );
1403
1404
        // test empty generator - no tag should appear at all
1405
        Config::modify()->set(SiteTree::class, 'meta_generator', '');
1406
        $meta = $page->MetaTags();
1407
        $this->assertStringNotContainsString(
1408
            'meta name="generator"',
1409
            $meta,
1410
            'test blank value means no tag generated'
1411
        );
1412
    }
1413
1414
1415
    public function testGetBreadcrumbItems()
1416
    {
1417
        $page = $this->objFromFixture(SiteTree::class, "breadcrumbs");
1418
        $this->assertEquals(1, $page->getBreadcrumbItems()->count(), "Only display current page.");
1419
1420
        // Test breadcrumb order
1421
        $page = $this->objFromFixture(SiteTree::class, "breadcrumbs5");
1422
        $breadcrumbs = $page->getBreadcrumbItems();
1423
        $this->assertEquals($breadcrumbs->count(), 5, "Display all breadcrumbs");
1424
        $this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs", "Breadcrumbs should be the first item.");
1425
        $this->assertEquals($breadcrumbs->last()->Title, "Breadcrumbs 5", "Breadcrumbs 5 should be last item.");
1426
1427
        // Test breadcrumb max depth
1428
        $breadcrumbs = $page->getBreadcrumbItems(2);
1429
        $this->assertEquals($breadcrumbs->count(), 2, "Max depth should limit the breadcrumbs to 2 items.");
1430
        $this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs 4", "First item should be Breadrcumbs 4.");
1431
        $this->assertEquals($breadcrumbs->last()->Title, "Breadcrumbs 5", "Breadcrumbs 5 should be last.");
1432
    }
1433
1434
    /**
1435
     * Tests SiteTree::MetaTags
1436
     * Note that this test makes no assumption on the closing of tags (other than <title></title>)
1437
     */
1438
    public function testMetaTags()
1439
    {
1440
        $this->logInWithPermission('ADMIN');
1441
        $page = $this->objFromFixture(SiteTree::class, 'metapage');
1442
1443
        // Test with title
1444
        $meta = $page->MetaTags();
1445
        $charset = Config::inst()->get(ContentNegotiator::class, 'encoding');
1446
        $this->assertStringContainsString('<meta http-equiv="Content-Type" content="text/html; charset=' . $charset . '"', $meta);
1447
        $this->assertStringContainsString('<meta name="description" content="The &lt;br /&gt; and &lt;br&gt; tags"', $meta);
1448
        $this->assertStringContainsString('<link rel="canonical" href="http://www.mysite.com/html-and-xml"', $meta);
1449
        $this->assertStringContainsString('<meta name="x-page-id" content="' . $page->ID . '"', $meta);
1450
        $this->assertStringContainsString('<meta name="x-cms-edit-link" content="' . $page->CMSEditLink() . '"', $meta);
1451
        $this->assertStringContainsString('<title>HTML &amp; XML</title>', $meta);
1452
1453
        // Test without title
1454
        $meta = $page->MetaTags(false);
1455
        $this->assertStringNotContainsString('<title>', $meta);
1456
1457
        $meta = $page->MetaTags('false');
1458
        $this->assertStringNotContainsString('<title>', $meta);
1459
    }
1460
1461
    public function testMetaComponents()
1462
    {
1463
        $this->logInWithPermission('ADMIN');
1464
        /** @var SiteTree $page */
1465
        $page = $this->objFromFixture(SiteTree::class, 'metapage');
1466
1467
        $charset = Config::inst()->get(ContentNegotiator::class, 'encoding');
1468
1469
        $expected = [
1470
            'title' => [
1471
                'tag' => 'title',
1472
                'content' => "HTML &amp; XML",
1473
            ],
1474
            'generator' => [
1475
                'attributes' => [
1476
                    'name' => 'generator',
1477
                    'content' => Config::inst()->get(SiteTree::class, 'meta_generator')
1478
                ],
1479
            ],
1480
            'contentType' => [
1481
                'attributes' => [
1482
                    'http-equiv' => 'Content-Type',
1483
                    'content' => "text/html; charset=$charset",
1484
                ],
1485
            ],
1486
            'description' => [
1487
                'attributes' => [
1488
                    'name' => 'description',
1489
                    'content' => 'The <br /> and <br> tags'
1490
                ]
1491
            ],
1492
            'pageId' => [
1493
                'attributes' => [
1494
                    'name' => 'x-page-id',
1495
                    'content' => $page->ID
1496
                ],
1497
            ],
1498
            'cmsEditLink' => [
1499
                'attributes' => [
1500
                    'name' => 'x-cms-edit-link',
1501
                    'content' => $page->CMSEditLink()
1502
                ]
1503
            ]
1504
        ];
1505
1506
        $this->assertEquals($expected, $page->MetaComponents());
1507
    }
1508
1509
    /**
1510
     * Test that orphaned pages are handled correctly
1511
     */
1512
    public function testOrphanedPages()
1513
    {
1514
        $origStage = Versioned::get_reading_mode();
1515
1516
        // Setup user who can view draft content, but lacks cms permission.
1517
        // To users such as this, orphaned pages should be inaccessible. canView for these pages is only
1518
        // necessary for admin / cms users, who require this permission to edit / rearrange these pages.
1519
        $permission = new Permission();
1520
        $permission->Code = 'VIEW_DRAFT_CONTENT';
1521
        $group = new Group(['Title' => 'Staging Users']);
1522
        $group->write();
1523
        $group->Permissions()->add($permission);
1524
        $member = new Member();
1525
        $member->Email = '[email protected]';
1526
        $member->write();
1527
        $member->Groups()->add($group);
1528
1529
        // both pages are viewable in stage
1530
        Versioned::set_stage(Versioned::DRAFT);
1531
        $about = $this->objFromFixture(SiteTree::class, 'about');
1532
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1533
        $this->assertFalse($about->isOrphaned());
1534
        $this->assertFalse($staff->isOrphaned());
1535
        $this->assertTrue($about->canView($member));
1536
        $this->assertTrue($staff->canView($member));
1537
1538
        // Publishing only the child page to live should orphan the live record, but not the staging one
1539
        $staff->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1540
        $this->assertFalse($staff->isOrphaned());
1541
        $this->assertTrue($staff->canView($member));
1542
        Versioned::set_stage(Versioned::LIVE);
1543
        $staff = $this->objFromFixture(SiteTree::class, 'staff'); // Live copy of page
1544
        $this->assertTrue($staff->isOrphaned()); // because parent isn't published
1545
        $this->assertFalse($staff->canView($member));
1546
1547
        // Publishing the parent page should restore visibility
1548
        Versioned::set_stage(Versioned::DRAFT);
1549
        $about = $this->objFromFixture(SiteTree::class, 'about');
1550
        $about->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1551
        Versioned::set_stage(Versioned::LIVE);
1552
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1553
        $this->assertFalse($staff->isOrphaned());
1554
        $this->assertTrue($staff->canView($member));
1555
1556
        // Removing staging page should not prevent live page being visible
1557
        $about->deleteFromStage('Stage');
1558
        $staff->deleteFromStage('Stage');
1559
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1560
        $this->assertFalse($staff->isOrphaned());
1561
        $this->assertTrue($staff->canView($member));
1562
1563
        // Cleanup
1564
        Versioned::set_reading_mode($origStage);
1565
    }
1566
1567
    /**
1568
     * Test archived page behaviour
1569
     */
1570
    public function testArchivedPages()
1571
    {
1572
        $this->logInWithPermission('ADMIN');
1573
1574
        /** @var SiteTree $page */
1575
        $page = $this->objFromFixture(SiteTree::class, 'home');
1576
        $this->assertTrue($page->canAddChildren());
1577
        $this->assertTrue($page->isOnDraft());
1578
        $this->assertFalse($page->isPublished());
1579
1580
        // Publish
1581
        $page->publishRecursive();
1582
        $this->assertTrue($page->canAddChildren());
1583
        $this->assertTrue($page->isOnDraft());
1584
        $this->assertTrue($page->isPublished());
1585
1586
        // Archive
1587
        $page->doArchive();
1588
        $this->assertFalse($page->canAddChildren());
1589
        $this->assertFalse($page->isOnDraft());
1590
        $this->assertTrue($page->isArchived());
1591
        $this->assertFalse($page->isPublished());
1592
    }
1593
1594
    public function testCanNot()
1595
    {
1596
        // Test that
1597
        $this->logInWithPermission('ADMIN');
1598
        $page = new SiteTreeTest_AdminDenied();
1599
        $this->assertFalse($page->canCreate());
1600
        $this->assertFalse($page->canEdit());
1601
        $this->assertFalse($page->canDelete());
1602
        $this->assertFalse($page->canAddChildren());
1603
        $this->assertFalse($page->canView());
1604
    }
1605
1606
    public function testCanPublish()
1607
    {
1608
        $page = new SiteTreeTest_ClassD();
1609
        $this->logOut();
1610
1611
        // Test that false overrides any can_publish = true
1612
        SiteTreeTest_ExtensionA::$can_publish = true;
1613
        SiteTreeTest_ExtensionB::$can_publish = false;
1614
        $this->assertFalse($page->canPublish());
1615
        SiteTreeTest_ExtensionA::$can_publish = false;
1616
        SiteTreeTest_ExtensionB::$can_publish = true;
1617
        $this->assertFalse($page->canPublish());
1618
1619
        // Test null extensions fall back to canEdit()
1620
        SiteTreeTest_ExtensionA::$can_publish = null;
1621
        SiteTreeTest_ExtensionB::$can_publish = null;
1622
        $page->canEditValue = true;
1623
        $this->assertTrue($page->canPublish());
1624
        $page->canEditValue = false;
1625
        $this->assertFalse($page->canPublish());
1626
    }
1627
1628
    /**
1629
     * Test url rewriting extensions
1630
     */
1631
    public function testLinkExtension()
1632
    {
1633
        Director::config()->set('alternate_base_url', 'http://www.baseurl.com');
1634
        $page = new SiteTreeTest_ClassD();
1635
        $page->URLSegment = 'classd';
1636
        $page->write();
1637
        $this->assertEquals(
1638
            'http://www.updatedhost.com/classd/myaction?extra=1',
1639
            $page->Link('myaction')
1640
        );
1641
        $this->assertEquals(
1642
            'http://www.updatedhost.com/classd/myaction?extra=1',
1643
            $page->AbsoluteLink('myaction')
1644
        );
1645
        $this->assertEquals(
1646
            'classd/myaction',
1647
            $page->RelativeLink('myaction')
1648
        );
1649
    }
1650
1651
    /**
1652
     * Test that the controller name for a SiteTree instance can be gathered by appending "Controller" to the SiteTree
1653
     * class name in a PSR-2 compliant manner.
1654
     */
1655
    public function testGetControllerName()
1656
    {
1657
        $class = Page::create();
1658
        $this->assertSame('PageController', $class->getControllerName());
1659
    }
1660
1661
    /**
1662
     * Test that the controller name for a SiteTree instance can be gathered when set directly via config var
1663
     */
1664
    public function testGetControllerNameFromConfig()
1665
    {
1666
        Config::inst()->update(Page::class, 'controller_name', 'This\\Is\\A\\New\\Controller');
1667
        $class = new Page();
1668
        $this->assertSame('This\\Is\\A\\New\\Controller', $class->getControllerName());
1669
    }
1670
1671
    /**
1672
     * Test that underscored class names (legacy) are still supported (deprecation notice is issued though).
1673
     */
1674
    public function testGetControllerNameWithUnderscoresIsSupported()
1675
    {
1676
        $class = new SiteTreeTest_LegacyControllerName();
1677
        $this->assertEquals(SiteTreeTest_LegacyControllerName_Controller::class, $class->getControllerName());
1678
    }
1679
1680
    public function testTreeTitleCache()
1681
    {
1682
        $siteTree = SiteTree::create();
1683
        $user = $this->objFromFixture(Member::class, 'allsections');
1684
        Security::setCurrentUser($user);
1685
        $pageClass = array_values(SiteTree::page_type_classes())[0];
1686
1687
        $mockPageMissesCache = $this->getMockBuilder($pageClass)
1688
            ->setMethods(['canCreate'])
1689
            ->getMock();
1690
        $mockPageMissesCache
1691
            ->expects($this->exactly(3))
1692
            ->method('canCreate');
1693
1694
        $mockPageHitsCache = $this->getMockBuilder($pageClass)
1695
            ->setMethods(['canCreate'])
1696
            ->getMock();
1697
        $mockPageHitsCache
1698
            ->expects($this->never())
1699
            ->method('canCreate');
1700
1701
        // Initially, cache misses (1)
1702
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1703
        $title = $siteTree->getTreeTitle();
1704
        $this->assertNotNull($title);
1705
1706
        // Now it hits
1707
        Injector::inst()->registerService($mockPageHitsCache, $pageClass);
1708
        $title = $siteTree->getTreeTitle();
1709
        $this->assertNotNull($title);
1710
1711
1712
        // Mutating member record invalidates cache. Misses (2)
1713
        $user->FirstName = 'changed';
1714
        $user->write();
1715
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1716
        $title = $siteTree->getTreeTitle();
1717
        $this->assertNotNull($title);
1718
1719
        // Now it hits again
1720
        Injector::inst()->registerService($mockPageHitsCache, $pageClass);
1721
        $title = $siteTree->getTreeTitle();
1722
        $this->assertNotNull($title);
1723
1724
        // Different user. Misses. (3)
1725
        $user = $this->objFromFixture(Member::class, 'editor');
1726
        Security::setCurrentUser($user);
1727
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1728
        $title = $siteTree->getTreeTitle();
1729
        $this->assertNotNull($title);
1730
    }
1731
1732
    public function testDependentPagesOnUnsavedRecord()
1733
    {
1734
        $record = new SiteTree();
1735
        $pages = $record->DependentPages();
1736
        $this->assertCount(0, $pages, 'Unsaved pages should have no dependent pages');
1737
    }
1738
}
1739