Completed
Push — master ( f4c8b4...9b7bf9 )
by Daniel
16s queued 11s
created

SiteTreeTest::testRelativeLink()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 19
nc 1
nop 0
dl 0
loc 26
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\CMS\Tests\Model;
4
5
use ReflectionMethod;
6
use SilverStripe\CMS\Model\RedirectorPage;
7
use SilverStripe\CMS\Model\SiteTree;
8
use SilverStripe\CMS\Model\VirtualPage;
9
use SilverStripe\Control\ContentNegotiator;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Dev\SapphireTest;
14
use SilverStripe\i18n\i18n;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\ORM\DB;
17
use SilverStripe\ORM\ValidationException;
18
use SilverStripe\Security\Group;
19
use SilverStripe\Security\InheritedPermissions;
20
use SilverStripe\Security\Member;
21
use SilverStripe\Security\Permission;
22
use SilverStripe\Security\Security;
23
use SilverStripe\SiteConfig\SiteConfig;
24
use SilverStripe\Versioned\Versioned;
25
use SilverStripe\View\Parsers\Diff;
26
use SilverStripe\View\Parsers\ShortcodeParser;
27
use SilverStripe\View\Parsers\URLSegmentFilter;
28
use SilverStripe\Core\Injector\Injector;
29
use LogicException;
30
use Page;
0 ignored issues
show
Bug introduced by
The type Page 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...
31
32
class SiteTreeTest extends SapphireTest
33
{
34
    protected static $fixture_file = 'SiteTreeTest.yml';
35
36
    protected static $illegal_extensions = [
37
        SiteTree::class => ['SiteTreeSubsites', 'Translatable'],
38
    ];
39
40
    protected static $extra_dataobjects = [
41
        SiteTreeTest_ClassA::class,
42
        SiteTreeTest_ClassB::class,
43
        SiteTreeTest_ClassC::class,
44
        SiteTreeTest_ClassD::class,
45
        SiteTreeTest_ClassCext::class,
46
        SiteTreeTest_NotRoot::class,
47
        SiteTreeTest_StageStatusInherit::class,
48
        SiteTreeTest_DataObject::class,
49
    ];
50
51
    public function reservedSegmentsProvider()
52
    {
53
        return [
54
            // segments reserved by rules
55
            ['Admin', 'admin-2'],
56
            ['Dev', 'dev-2'],
57
            ['Robots in disguise', 'robots-in-disguise'],
58
            // segments reserved by folder name
59
            ['resources', 'resources-2'],
60
            ['assets', 'assets-2'],
61
            ['notafoldername', 'notafoldername'],
62
        ];
63
    }
64
65
    public function testCreateDefaultpages()
66
    {
67
        $remove = SiteTree::get();
68
        if ($remove) {
0 ignored issues
show
introduced by
$remove is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
69
            foreach ($remove as $page) {
70
                $page->delete();
71
            }
72
        }
73
        // Make sure the table is empty
74
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
75
76
        // Disable the creation
77
        SiteTree::config()->create_default_pages = false;
78
        singleton(SiteTree::class)->requireDefaultRecords();
79
80
        // The table should still be empty
81
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 0);
82
83
        // Enable the creation
84
        SiteTree::config()->create_default_pages = true;
85
        singleton(SiteTree::class)->requireDefaultRecords();
86
87
        // The table should now have three rows (home, about-us, contact-us)
88
        $this->assertEquals(DB::query('SELECT COUNT("ID") FROM "SiteTree"')->value(), 3);
89
    }
90
91
    /**
92
     * Test generation of the URLSegment values.
93
     *  - Turns things into lowercase-hyphen-format
94
     *  - Generates from Title by default, unless URLSegment is explicitly set
95
     *  - Resolves duplicates by appending a number
96
     *  - renames classes with a class name conflict
97
     */
98
    public function testURLGeneration()
99
    {
100
        $expectedURLs = [
101
            'home' => 'home',
102
            'staff' => 'my-staff',
103
            'about' => 'about-us',
104
            'staffduplicate' => 'my-staff-2',
105
            'product1' => '1-1-test-product',
106
            'product2' => 'another-product',
107
            'product3' => 'another-product-2',
108
            'product4' => 'another-product-3',
109
            'object'   => 'object',
110
            'controller' => 'controller',
111
            'numericonly' => '1930',
112
        ];
113
114
        foreach ($expectedURLs as $fixture => $urlSegment) {
115
            $obj = $this->objFromFixture(SiteTree::class, $fixture);
116
            $this->assertEquals($urlSegment, $obj->URLSegment);
117
        }
118
    }
119
120
    /**
121
     * Check if reserved URL's are properly appended with a number at top level
122
     * @dataProvider reservedSegmentsProvider
123
     */
124
    public function testDisallowedURLGeneration($title, $urlSegment)
125
    {
126
        $page = SiteTree::create(['Title' => $title]);
127
        $id = $page->write();
128
        $page = SiteTree::get()->byID($id);
129
        $this->assertEquals($urlSegment, $page->URLSegment);
130
    }
131
132
    /**
133
     * Check if reserved URL's are not appended with a number on a child page
134
     * It's okay to have a URL like domain.com/my-page/admin as it won't interfere with domain.com/admin
135
     * @dataProvider reservedSegmentsProvider
136
     */
137
    public function testDisallowedChildURLGeneration($title, $urlSegment)
138
    {
139
        // Using the same dataprovider, strip out the -2 from the admin and dev segment
140
        $urlSegment = str_replace('-2', '', $urlSegment);
141
        $page = SiteTree::create(['Title' => $title, 'ParentID' => 1]);
142
        $id = $page->write();
143
        $page = SiteTree::get()->byID($id);
144
        $this->assertEquals($urlSegment, $page->URLSegment);
145
    }
146
147
    /**
148
     * Test that publication copies data to SiteTree_Live
149
     */
150
    public function testPublishCopiesToLiveTable()
151
    {
152
        $obj = $this->objFromFixture(SiteTree::class, 'about');
153
        $obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
154
155
        $createdID = DB::query(
156
            "SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"URLSegment\" = '$obj->URLSegment'"
157
        )->value();
158
        $this->assertEquals($obj->ID, $createdID);
159
    }
160
161
    /**
162
     * Test that field which are set and then cleared are also transferred to the published site.
163
     */
164
    public function testPublishDeletedFields()
165
    {
166
        $this->logInWithPermission('ADMIN');
167
168
        $obj = $this->objFromFixture(SiteTree::class, 'about');
169
        $obj->Title = "asdfasdf";
170
        $obj->write();
171
        $this->assertTrue($obj->publishRecursive());
172
173
        $this->assertEquals(
174
            'asdfasdf',
175
            DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value()
176
        );
177
178
        $obj->Title = null;
179
        $obj->write();
180
        $this->assertTrue($obj->publishRecursive());
181
182
        $this->assertNull(DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = '$obj->ID'")->value());
183
    }
184
185
    public function testParentNodeCachedInMemory()
186
    {
187
        $parent = SiteTree::create();
188
        $parent->Title = 'Section Title';
189
        $child = SiteTree::create();
190
        $child->Title = 'Page Title';
191
        $child->setParent($parent);
192
193
        $this->assertInstanceOf(SiteTree::class, $child->Parent);
194
        $this->assertEquals("Section Title", $child->Parent->Title);
195
    }
196
197
    public function testParentModelReturnType()
198
    {
199
        $parent = new SiteTreeTest_PageNode();
200
        $child = new SiteTreeTest_PageNode();
201
202
        $child->setParent($parent);
203
        $this->assertInstanceOf(SiteTreeTest_PageNode::class, $child->Parent);
0 ignored issues
show
Bug Best Practice introduced by
The property Parent does not exist on SilverStripe\CMS\Tests\Model\SiteTreeTest_PageNode. Since you implemented __get, consider adding a @property annotation.
Loading history...
204
    }
205
206
    /**
207
     * Confirm that DataObject::get_one() gets records from SiteTree_Live
208
     */
209
    public function testGetOneFromLive()
210
    {
211
        $s = SiteTree::create();
212
        $s->Title = "V1";
213
        $s->URLSegment = "get-one-test-page";
214
        $s->write();
215
        $s->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
216
        $s->Title = "V2";
217
        $s->write();
218
219
        $oldMode = Versioned::get_reading_mode();
220
        Versioned::set_stage(Versioned::LIVE);
221
222
        $checkSiteTree = DataObject::get_one(SiteTree::class, [
223
            '"SiteTree"."URLSegment"' => 'get-one-test-page',
224
        ]);
225
        $this->assertEquals("V1", $checkSiteTree->Title);
226
227
        Versioned::set_reading_mode($oldMode);
228
    }
229
230
    public function testChidrenOfRootAreTopLevelPages()
231
    {
232
        $pages = SiteTree::get();
233
        foreach ($pages as $page) {
234
            $page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
235
        }
236
        unset($pages);
237
238
        /* If we create a new SiteTree object with ID = 0 */
239
        $obj = SiteTree::create();
240
        /* Then its children should be the top-level pages */
241
        $stageChildren = $obj->stageChildren()->map('ID', 'Title');
242
        $liveChildren = $obj->liveChildren()->map('ID', 'Title');
243
        $allChildren = $obj->AllChildrenIncludingDeleted()->map('ID', 'Title');
244
245
        $this->assertContains('Home', $stageChildren);
246
        $this->assertContains('Products', $stageChildren);
247
        $this->assertNotContains('Staff', $stageChildren);
248
249
        $this->assertContains('Home', $liveChildren);
250
        $this->assertContains('Products', $liveChildren);
251
        $this->assertNotContains('Staff', $liveChildren);
252
253
        $this->assertContains('Home', $allChildren);
254
        $this->assertContains('Products', $allChildren);
255
        $this->assertNotContains('Staff', $allChildren);
256
    }
257
258
    public function testCanSaveBlankToHasOneRelations()
259
    {
260
        /* DataObject::write() should save to a has_one relationship if you set a field called (relname)ID */
261
        $page = SiteTree::create();
262
        $parentID = $this->idFromFixture(SiteTree::class, 'home');
263
        $page->ParentID = $parentID;
264
        $page->write();
265
        $this->assertEquals(
266
            $parentID,
267
            DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value()
268
        );
269
270
        /* You should then be able to save a null/0/'' value to the relation */
271
        $page->ParentID = null;
272
        $page->write();
273
        $this->assertEquals(0, DB::query("SELECT \"ParentID\" FROM \"SiteTree\" WHERE \"ID\" = $page->ID")->value());
274
    }
275
276
    public function testStageStates()
277
    {
278
        // newly created page
279
        $createdPage = SiteTree::create();
280
        $createdPage->write();
281
        $this->assertTrue($createdPage->isOnDraft());
282
        $this->assertFalse($createdPage->isPublished());
283
        $this->assertTrue($createdPage->isOnDraftOnly());
284
        $this->assertTrue($createdPage->isModifiedOnDraft());
285
286
        // published page
287
        $publishedPage = SiteTree::create();
288
        $publishedPage->write();
289
        $publishedPage->copyVersionToStage('Stage', 'Live');
290
        $this->assertTrue($publishedPage->isOnDraft());
291
        $this->assertTrue($publishedPage->isPublished());
292
        $this->assertFalse($publishedPage->isOnDraftOnly());
293
        $this->assertFalse($publishedPage->isOnLiveOnly());
294
        $this->assertFalse($publishedPage->isModifiedOnDraft());
295
296
        // published page, deleted from stage
297
        $deletedFromDraftPage = SiteTree::create();
298
        $deletedFromDraftPage->write();
299
        $deletedFromDraftPage->copyVersionToStage('Stage', 'Live');
300
        $deletedFromDraftPage->deleteFromStage('Stage');
301
        $this->assertFalse($deletedFromDraftPage->isArchived());
302
        $this->assertFalse($deletedFromDraftPage->isOnDraft());
303
        $this->assertTrue($deletedFromDraftPage->isPublished());
304
        $this->assertFalse($deletedFromDraftPage->isOnDraftOnly());
305
        $this->assertTrue($deletedFromDraftPage->isOnLiveOnly());
306
        $this->assertFalse($deletedFromDraftPage->isModifiedOnDraft());
307
308
        // published page, deleted from live
309
        $deletedFromLivePage = SiteTree::create();
310
        $deletedFromLivePage->write();
311
        $deletedFromLivePage->copyVersionToStage('Stage', 'Live');
312
        $deletedFromLivePage->deleteFromStage('Live');
313
        $this->assertFalse($deletedFromLivePage->isArchived());
314
        $this->assertTrue($deletedFromLivePage->isOnDraft());
315
        $this->assertFalse($deletedFromLivePage->isPublished());
316
        $this->assertTrue($deletedFromLivePage->isOnDraftOnly());
317
        $this->assertFalse($deletedFromLivePage->isOnLiveOnly());
318
        $this->assertTrue($deletedFromLivePage->isModifiedOnDraft());
319
320
        // published page, deleted from both stages
321
        $deletedFromAllStagesPage = SiteTree::create();
322
        $deletedFromAllStagesPage->write();
323
        $deletedFromAllStagesPage->copyVersionToStage('Stage', 'Live');
324
        $deletedFromAllStagesPage->deleteFromStage('Stage');
325
        $deletedFromAllStagesPage->deleteFromStage('Live');
326
        $this->assertTrue($deletedFromAllStagesPage->isArchived());
327
        $this->assertFalse($deletedFromAllStagesPage->isOnDraft());
328
        $this->assertFalse($deletedFromAllStagesPage->isPublished());
329
        $this->assertFalse($deletedFromAllStagesPage->isOnDraftOnly());
330
        $this->assertFalse($deletedFromAllStagesPage->isOnLiveOnly());
331
        $this->assertFalse($deletedFromAllStagesPage->isModifiedOnDraft());
332
333
        // published page, modified
334
        $modifiedOnDraftPage = SiteTree::create();
335
        $modifiedOnDraftPage->write();
336
        $modifiedOnDraftPage->copyVersionToStage('Stage', 'Live');
337
        $modifiedOnDraftPage->Content = 'modified';
338
        $modifiedOnDraftPage->write();
339
        $this->assertFalse($modifiedOnDraftPage->isArchived());
340
        $this->assertTrue($modifiedOnDraftPage->isOnDraft());
341
        $this->assertTrue($modifiedOnDraftPage->isPublished());
342
        $this->assertFalse($modifiedOnDraftPage->isOnDraftOnly());
343
        $this->assertFalse($modifiedOnDraftPage->isOnLiveOnly());
344
        $this->assertTrue($modifiedOnDraftPage->isModifiedOnDraft());
345
    }
346
347
    /**
348
     * Test that a page can be completely deleted and restored to the stage site
349
     */
350
    public function testRestoreToStage()
351
    {
352
        $page = $this->objFromFixture(SiteTree::class, 'about');
353
        $pageID = $page->ID;
354
        $page->delete();
355
        $this->assertTrue(!DataObject::get_by_id(SiteTree::class, $pageID));
356
357
        $deletedPage = Versioned::get_latest_version(SiteTree::class, $pageID);
358
        $resultPage = $deletedPage->doRestoreToStage();
359
360
        $requeriedPage = DataObject::get_by_id(SiteTree::class, $pageID);
361
362
        $this->assertEquals($pageID, $resultPage->ID);
363
        $this->assertEquals($pageID, $requeriedPage->ID);
364
        $this->assertEquals('About Us', $requeriedPage->Title);
365
        $this->assertInstanceOf(SiteTree::class, $requeriedPage);
366
367
368
        $page2 = $this->objFromFixture(SiteTree::class, 'products');
369
        $page2ID = $page2->ID;
370
        $page2->doUnpublish();
371
        $page2->delete();
372
373
        // Check that if we restore while on the live site that the content still gets pushed to
374
        // stage
375
        Versioned::set_stage(Versioned::LIVE);
376
        $deletedPage = Versioned::get_latest_version(SiteTree::class, $page2ID);
377
        $deletedPage->doRestoreToStage();
378
        $this->assertFalse(
379
            (bool)Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, "\"SiteTree\".\"ID\" = " . $page2ID)
380
        );
381
382
        Versioned::set_stage(Versioned::DRAFT);
383
        $requeriedPage = DataObject::get_by_id(SiteTree::class, $page2ID);
384
        $this->assertEquals('Products', $requeriedPage->Title);
385
        $this->assertInstanceOf(SiteTree::class, $requeriedPage);
386
    }
387
388
    public function testNoCascadingDeleteWithoutID()
389
    {
390
        Config::inst()->update('SiteTree', 'enforce_strict_hierarchy', true);
0 ignored issues
show
Bug introduced by
The method update() does not exist on SilverStripe\Config\Coll...nfigCollectionInterface. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\Config\Coll...nfigCollectionInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

390
        Config::inst()->/** @scrutinizer ignore-call */ update('SiteTree', 'enforce_strict_hierarchy', true);
Loading history...
391
        $count = SiteTree::get()->count();
392
        $this->assertNotEmpty($count);
393
        $obj = SiteTree::create();
394
        $this->assertFalse($obj->exists());
395
        $fail = true;
396
        try {
397
            $obj->delete();
398
        } catch (LogicException $e) {
399
            $fail = false;
400
        }
401
        if ($fail) {
402
            $this->fail('Failed to throw delete exception');
403
        }
404
        $this->assertCount($count, SiteTree::get());
405
    }
406
407
    public function testGetByLink()
408
    {
409
        $home     = $this->objFromFixture(SiteTree::class, 'home');
410
        $about    = $this->objFromFixture(SiteTree::class, 'about');
411
        $staff    = $this->objFromFixture(SiteTree::class, 'staff');
412
        $product  = $this->objFromFixture(SiteTree::class, 'product1');
413
414
        SiteTree::config()->nested_urls = false;
415
416
        $this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID);
417
        $this->assertEquals($home->ID, SiteTree::get_by_link('/home/', false)->ID);
418
        $this->assertEquals($about->ID, SiteTree::get_by_link($about->Link(), false)->ID);
419
        $this->assertEquals($staff->ID, SiteTree::get_by_link($staff->Link(), false)->ID);
420
        $this->assertEquals($product->ID, SiteTree::get_by_link($product->Link(), false)->ID);
421
422
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
423
424
        $this->assertEquals($home->ID, SiteTree::get_by_link('/', false)->ID);
425
        $this->assertEquals($home->ID, SiteTree::get_by_link('/home/', false)->ID);
426
        $this->assertEquals($about->ID, SiteTree::get_by_link($about->Link(), false)->ID);
427
        $this->assertEquals($staff->ID, SiteTree::get_by_link($staff->Link(), false)->ID);
428
        $this->assertEquals($product->ID, SiteTree::get_by_link($product->Link(), false)->ID);
429
430
        $this->assertEquals(
431
            $staff->ID,
432
            SiteTree::get_by_link('/my-staff/', false)->ID,
433
            'Assert a unique URLSegment can be used for b/c.'
434
        );
435
    }
436
437
    public function testRelativeLink()
438
    {
439
        $about    = $this->objFromFixture(SiteTree::class, 'about');
440
        $staff    = $this->objFromFixture(SiteTree::class, 'staff');
441
442
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
443
444
        $this->assertEquals(
445
            'about-us/',
446
            $about->RelativeLink(),
447
            'Matches URLSegment on top level without parameters'
448
        );
449
        $this->assertEquals(
450
            'about-us/my-staff/',
451
            $staff->RelativeLink(),
452
            'Matches URLSegment plus parent on second level without parameters'
453
        );
454
        $this->assertEquals(
455
            'about-us/edit',
456
            $about->RelativeLink('edit'),
457
            'Matches URLSegment plus parameter on top level'
458
        );
459
        $this->assertEquals(
460
            'about-us/tom&jerry',
461
            $about->RelativeLink('tom&jerry'),
462
            'Doesnt url encode parameter'
463
        );
464
    }
465
466
    public function testPageLevel()
467
    {
468
        $about = $this->objFromFixture(SiteTree::class, 'about');
469
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
470
        $this->assertEquals(1, $about->getPageLevel());
471
        $this->assertEquals(2, $staff->getPageLevel());
472
    }
473
474
    public function testAbsoluteLiveLink()
475
    {
476
        $parent = $this->objFromFixture(SiteTree::class, 'about');
477
        $child = $this->objFromFixture(SiteTree::class, 'staff');
478
479
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
480
481
        $child->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
482
        $parent->URLSegment = 'changed-on-live';
483
        $parent->write();
484
        $parent->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
485
        $parent->URLSegment = 'changed-on-draft';
486
        $parent->write();
487
488
        $this->assertStringEndsWith('changed-on-live/my-staff/', $child->getAbsoluteLiveLink(false));
489
        $this->assertStringEndsWith('changed-on-live/my-staff/?stage=Live', $child->getAbsoluteLiveLink());
490
    }
491
492
    public function testDuplicateChildrenRetainSort()
493
    {
494
        $parent = SiteTree::create();
495
        $parent->Title = 'Parent';
496
        $parent->write();
497
498
        $child1 = SiteTree::create();
499
        $child1->ParentID = $parent->ID;
500
        $child1->Title = 'Child 1';
501
        $child1->Sort = 2;
502
        $child1->write();
503
504
        $child2 = SiteTree::create();
505
        $child2->ParentID = $parent->ID;
506
        $child2->Title = 'Child 2';
507
        $child2->Sort = 1;
508
        $child2->write();
509
510
        $duplicateParent = $parent->duplicateWithChildren();
511
        $duplicateChildren = $duplicateParent->AllChildren()->toArray();
512
        $this->assertCount(2, $duplicateChildren);
513
514
        $duplicateChild2 = array_shift($duplicateChildren);
515
        $duplicateChild1 = array_shift($duplicateChildren);
516
517
518
        $this->assertEquals('Child 1', $duplicateChild1->Title);
519
        $this->assertEquals('Child 2', $duplicateChild2->Title);
520
521
        // assertGreaterThan works by having the LOWER value first
522
        $this->assertGreaterThan($duplicateChild2->Sort, $duplicateChild1->Sort);
523
    }
524
525
    public function testDeleteFromStageOperatesRecursively()
526
    {
527
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
528
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
529
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
530
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
531
532
        $pageAbout->delete();
533
534
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
535
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
536
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
537
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
538
    }
539
540
    public function testDeleteFromStageOperatesRecursivelyStrict()
541
    {
542
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
543
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
544
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
545
546
        $pageAbout->delete();
547
548
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
549
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaff->ID));
550
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID));
551
    }
552
553
    public function testDuplicate()
554
    {
555
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
556
        $dupe = $pageAbout->duplicate();
557
        $this->assertEquals($pageAbout->Title, $dupe->Title);
558
        $this->assertNotEquals($pageAbout->URLSegment, $dupe->URLSegment);
559
        $this->assertNotEquals($pageAbout->Sort, $dupe->Sort);
560
    }
561
562
    public function testDeleteFromLiveOperatesRecursively()
563
    {
564
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
565
        $this->logInWithPermission('ADMIN');
566
567
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
568
        $pageAbout->publishRecursive();
569
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
570
        $pageStaff->publishRecursive();
571
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
572
        $pageStaffDuplicate->publishRecursive();
573
574
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
575
576
        $parentPage->doUnpublish();
577
578
        Versioned::set_stage(Versioned::LIVE);
579
580
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
581
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
582
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
583
        Versioned::set_stage(Versioned::DRAFT);
584
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
585
    }
586
587
    public function testUnpublishDoesNotDeleteChildrenWithLooseHierachyOn()
588
    {
589
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', false);
590
        $this->logInWithPermission('ADMIN');
591
592
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
593
        $pageAbout->publishRecursive();
594
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
595
        $pageStaff->publishRecursive();
596
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
597
        $pageStaffDuplicate->publishRecursive();
598
599
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
600
        $parentPage->doUnpublish();
601
602
        Versioned::set_stage(Versioned::LIVE);
603
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
604
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaff->ID) instanceof SiteTree);
605
        $this->assertTrue(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID) instanceof SiteTree);
606
        Versioned::set_stage(Versioned::DRAFT);
607
        Config::modify()->set(SiteTree::class, 'enforce_strict_hierarchy', true);
608
    }
609
610
    public function testDeleteFromLiveOperatesRecursivelyStrict()
611
    {
612
        $this->logInWithPermission('ADMIN');
613
614
        $pageAbout = $this->objFromFixture(SiteTree::class, 'about');
615
        $pageAbout->publishRecursive();
616
        $pageStaff = $this->objFromFixture(SiteTree::class, 'staff');
617
        $pageStaff->publishRecursive();
618
        $pageStaffDuplicate = $this->objFromFixture(SiteTree::class, 'staffduplicate');
619
        $pageStaffDuplicate->publishRecursive();
620
621
        $parentPage = $this->objFromFixture(SiteTree::class, 'about');
622
        $parentPage->doUnpublish();
623
624
        Versioned::set_stage(Versioned::LIVE);
625
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageAbout->ID));
626
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaff->ID));
627
        $this->assertNull(DataObject::get_by_id(SiteTree::class, $pageStaffDuplicate->ID));
628
        Versioned::set_stage(Versioned::DRAFT);
629
    }
630
631
    /**
632
     * Simple test to confirm that querying from a particular archive date doesn't throw
633
     * an error
634
     */
635
    public function testReadArchiveDate()
636
    {
637
        $date = '2009-07-02 14:05:07';
638
        Versioned::reading_archived_date($date);
639
        SiteTree::get()->where([
640
            '"SiteTree"."ParentID"' => 0,
641
        ])->sql($args);
642
        $this->assertContains($date, $args);
643
    }
644
645
    public function testEditPermissions()
646
    {
647
        $editor = $this->objFromFixture(Member::class, "editor");
648
649
        $home = $this->objFromFixture(SiteTree::class, "home");
650
        $staff = $this->objFromFixture(SiteTree::class, "staff");
651
        $products = $this->objFromFixture(SiteTree::class, "products");
652
        $product1 = $this->objFromFixture(SiteTree::class, "product1");
653
        $product4 = $this->objFromFixture(SiteTree::class, "product4");
654
655
        // Test logged out users cannot edit
656
        $this->logOut();
657
        $this->assertFalse($staff->canEdit());
658
659
        // Can't edit a page that is locked to admins
660
        $this->assertFalse($home->canEdit($editor));
661
662
        // Can edit a page that is locked to editors
663
        $this->assertTrue($products->canEdit($editor));
664
665
        // Can edit a child of that page that inherits
666
        $this->assertTrue($product1->canEdit($editor));
667
668
        // Can't edit a child of that page that has its permissions overridden
669
        $this->assertFalse($product4->canEdit($editor));
670
    }
671
672
    public function testCanEditWithAccessToAllSections()
673
    {
674
        $page = SiteTree::create();
675
        $page->write();
676
        $allSectionMember = $this->objFromFixture(Member::class, 'allsections');
677
        $securityAdminMember = $this->objFromFixture(Member::class, 'securityadmin');
678
679
        $this->assertTrue($page->canEdit($allSectionMember));
680
        $this->assertFalse($page->canEdit($securityAdminMember));
681
    }
682
683
    public function testCreatePermissions()
684
    {
685
        // Test logged out users cannot create
686
        $this->logOut();
687
        $this->assertFalse(singleton(SiteTree::class)->canCreate());
688
689
        // Login with another permission
690
        $this->logInWithPermission('DUMMY');
691
        $this->assertFalse(singleton(SiteTree::class)->canCreate());
692
693
        // Login with basic CMS permission
694
        $perms = SiteConfig::config()->required_permission;
695
        $this->logInWithPermission(reset($perms));
696
        $this->assertTrue(singleton(SiteTree::class)->canCreate());
697
698
        // Test creation underneath a parent which this user doesn't have access to
699
        $parent = $this->objFromFixture(SiteTree::class, 'about');
700
        $this->assertFalse(singleton(SiteTree::class)->canCreate(null, ['Parent' => $parent]));
701
702
        // Test creation underneath a parent which doesn't allow a certain child
703
        $parentB = new SiteTreeTest_ClassB();
704
        $parentB->Title = 'Only Allows SiteTreeTest_ClassC';
705
        $parentB->write();
706
        $this->assertTrue(singleton(SiteTreeTest_ClassA::class)->canCreate(null));
707
        $this->assertFalse(singleton(SiteTreeTest_ClassA::class)->canCreate(null, ['Parent' => $parentB]));
708
        $this->assertTrue(singleton(SiteTreeTest_ClassC::class)->canCreate(null, ['Parent' => $parentB]));
709
710
        // Test creation underneath a parent which doesn't exist in the database. This should
711
        // fall back to checking whether the user can create pages at the root of the site
712
        $this->assertTrue(singleton(SiteTree::class)->canCreate(null, ['Parent' => singleton(SiteTree::class)]));
713
714
        //Test we don't check for allowedChildren on parent context if it's not SiteTree instance
715
        $this->assertTrue(
716
            singleton(SiteTree::class)->canCreate(
717
                null,
718
                ['Parent' => $this->objFromFixture(SiteTreeTest_DataObject::class, 'relations')]
719
            )
720
        );
721
    }
722
723
    public function testEditPermissionsOnDraftVsLive()
724
    {
725
        // Create an inherit-permission page
726
        $page = SiteTree::create();
727
        $page->write();
728
        $page->CanEditType = "Inherit";
729
        $page->publishRecursive();
730
        $pageID = $page->ID;
0 ignored issues
show
Unused Code introduced by
The assignment to $pageID is dead and can be removed.
Loading history...
731
732
        // Lock down the site config
733
        $sc = $page->SiteConfig;
734
        $sc->CanEditType = 'OnlyTheseUsers';
735
        $sc->EditorGroups()->add($this->idFromFixture(Group::class, 'admins'));
736
        $sc->write();
737
738
        // Confirm that Member.editor can't edit the page
739
        $member = $this->objFromFixture(Member::class, 'editor');
740
        Security::setCurrentUser($member);
741
        $this->assertFalse($page->canEdit());
742
743
        // Change the page to be editable by Group.editors, but do not publish
744
        $admin = $this->objFromFixture(Member::class, 'admin');
745
        Security::setCurrentUser($admin);
746
        $page->CanEditType = 'OnlyTheseUsers';
747
        $page->EditorGroups()->add($this->idFromFixture(Group::class, 'editors'));
748
        $page->write();
749
750
        // Clear permission cache
751
        /** @var InheritedPermissions $checker */
752
        $checker = SiteTree::getPermissionChecker();
753
        $checker->clearCache();
754
755
        // Confirm that Member.editor can now edit the page
756
        $member = $this->objFromFixture(Member::class, 'editor');
757
        Security::setCurrentUser($member);
758
        $this->assertTrue($page->canEdit());
759
760
        // Publish the changes to the page
761
        $admin = $this->objFromFixture(Member::class, 'admin');
762
        Security::setCurrentUser($admin);
763
        $page->publishRecursive();
764
765
        // Confirm that Member.editor can still edit the page
766
        $member = $this->objFromFixture(Member::class, 'editor');
767
        Security::setCurrentUser($member);
768
        $this->assertTrue($page->canEdit());
769
    }
770
771
    public function testCompareVersions()
772
    {
773
        // Necessary to avoid
774
        $oldCleanerClass = Diff::$html_cleaner_class;
775
        Diff::$html_cleaner_class = SiteTreeTest_NullHtmlCleaner::class;
776
777
        $page = SiteTree::create();
778
        $page->write();
779
        $this->assertEquals(1, $page->Version);
780
781
        // Use inline element to avoid double wrapping applied to
782
        // blocklevel elements depending on HTMLCleaner implementation:
783
        // <ins><p> gets converted to <ins><p><inst>
784
        $page->Content = "<span>This is a test</span>";
785
        $page->write();
786
        $this->assertEquals(2, $page->Version);
787
788
        $diff = $page->compareVersions(1, 2);
789
790
        $processedContent = trim($diff->Content);
791
        $processedContent = preg_replace('/\s*</', '<', $processedContent);
792
        $processedContent = preg_replace('/>\s*/', '>', $processedContent);
793
        $this->assertEquals("<ins><span>This is a test</span></ins>", $processedContent);
794
795
        Diff::$html_cleaner_class = $oldCleanerClass;
796
    }
797
798
    public function testAuthorIDAndPublisherIDFilledOutOnPublish()
799
    {
800
        // Ensure that we have a member ID who is doing all this work
801
        $member = $this->objFromFixture(Member::class, "admin");
802
        $this->logInAs($member);
803
804
        // Write the page
805
        $about = $this->objFromFixture(SiteTree::class, 'about');
806
        $about->Title = "Another title";
807
        $about->write();
808
809
        // Check the version created
810
        $savedVersion = DB::prepared_query(
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\ORM\Connect\Query::first() has been deprecated: Use record() instead ( Ignorable by Annotation )

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

810
        $savedVersion = /** @scrutinizer ignore-deprecated */ DB::prepared_query(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
811
            "SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
812
            WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
813
            [$about->ID]
814
        )->first();
815
        $this->assertEquals($member->ID, $savedVersion['AuthorID']);
816
        $this->assertEquals(0, $savedVersion['PublisherID']);
817
818
        // Publish the page
819
        $about->publishRecursive();
820
        $publishedVersion = DB::prepared_query(
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\ORM\Connect\Query::first() has been deprecated: Use record() instead ( Ignorable by Annotation )

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

820
        $publishedVersion = /** @scrutinizer ignore-deprecated */ DB::prepared_query(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
821
            "SELECT \"AuthorID\", \"PublisherID\" FROM \"SiteTree_Versions\"
822
            WHERE \"RecordID\" = ? ORDER BY \"Version\" DESC",
823
            [$about->ID]
824
        )->first();
825
826
        // Check the version created
827
        $this->assertEquals($member->ID, $publishedVersion['AuthorID']);
828
        $this->assertEquals($member->ID, $publishedVersion['PublisherID']);
829
    }
830
831
    public function testLinkShortcodeHandler()
832
    {
833
        $aboutPage = $this->objFromFixture(SiteTree::class, 'about');
834
        $redirectPage = $this->objFromFixture(RedirectorPage::class, 'external');
835
836
        $parser = new ShortcodeParser();
837
        $parser->register('sitetree_link', [SiteTree::class, 'link_shortcode_handler']);
838
839
        $aboutShortcode = sprintf('[sitetree_link,id=%d]', $aboutPage->ID);
840
        $aboutEnclosed  = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $aboutPage->ID);
841
842
        $aboutShortcodeExpected = $aboutPage->Link();
843
        $aboutEnclosedExpected  = sprintf('<a href="%s">Example Content</a>', $aboutPage->Link());
844
845
        $this->assertEquals(
846
            $aboutShortcodeExpected,
847
            $parser->parse($aboutShortcode),
848
            'Test that simple linking works.'
849
        );
850
        $this->assertEquals(
851
            $aboutEnclosedExpected,
852
            $parser->parse($aboutEnclosed),
853
            'Test enclosed content is linked.'
854
        );
855
856
        $aboutPage->delete();
857
858
        $this->assertEquals(
859
            $aboutShortcodeExpected,
860
            $parser->parse($aboutShortcode),
861
            'Test that deleted pages still link.'
862
        );
863
        $this->assertEquals($aboutEnclosedExpected, $parser->parse($aboutEnclosed));
864
865
        $aboutShortcode = '[sitetree_link,id="-1"]';
866
        $aboutEnclosed  = '[sitetree_link,id="-1"]Example Content[/sitetree_link]';
867
868
        $this->assertEquals('', $parser->parse($aboutShortcode), 'Test empty result if no suitable matches.');
869
        $this->assertEquals('', $parser->parse($aboutEnclosed));
870
871
        $redirectShortcode = sprintf('[sitetree_link,id=%d]', $redirectPage->ID);
872
        $redirectEnclosed  = sprintf('[sitetree_link,id=%d]Example Content[/sitetree_link]', $redirectPage->ID);
873
        $redirectExpected = 'http://www.google.com?a&amp;b';
874
875
        $this->assertEquals($redirectExpected, $parser->parse($redirectShortcode));
876
        $this->assertEquals(
877
            sprintf('<a href="%s">Example Content</a>', $redirectExpected),
878
            $parser->parse($redirectEnclosed)
879
        );
880
881
        $this->assertEquals('', $parser->parse('[sitetree_link]'), 'Test that invalid ID attributes are not parsed.');
882
        $this->assertEquals('', $parser->parse('[sitetree_link,id="text"]'));
883
        $this->assertEquals('', $parser->parse('[sitetree_link]Example Content[/sitetree_link]'));
884
    }
885
886
    public function testIsCurrent()
887
    {
888
        $aboutPage = $this->objFromFixture(SiteTree::class, 'about');
889
        $productPage = $this->objFromFixture(SiteTree::class, 'products');
890
891
        Director::set_current_page($aboutPage);
892
        $this->assertTrue($aboutPage->isCurrent(), 'Assert that basic isCurrent checks works.');
893
        $this->assertFalse($productPage->isCurrent());
894
895
        $this->assertTrue(
896
            DataObject::get_one(SiteTree::class, [
897
                '"SiteTree"."Title"' => 'About Us',
898
            ])->isCurrent(),
899
            'Assert that isCurrent works on another instance with the same ID.'
900
        );
901
902
        Director::set_current_page($newPage = SiteTree::create());
903
        $this->assertTrue($newPage->isCurrent(), 'Assert that isCurrent works on unsaved pages.');
904
    }
905
906
    public function testIsSection()
907
    {
908
        $about = $this->objFromFixture(SiteTree::class, 'about');
909
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
910
        $ceo   = $this->objFromFixture(SiteTree::class, 'ceo');
911
912
        Director::set_current_page($about);
913
        $this->assertTrue($about->isSection());
914
        $this->assertFalse($staff->isSection());
915
        $this->assertFalse($ceo->isSection());
916
917
        Director::set_current_page($staff);
918
        $this->assertTrue($about->isSection());
919
        $this->assertTrue($staff->isSection());
920
        $this->assertFalse($ceo->isSection());
921
922
        Director::set_current_page($ceo);
923
        $this->assertTrue($about->isSection());
924
        $this->assertTrue($staff->isSection());
925
        $this->assertTrue($ceo->isSection());
926
    }
927
928
    public function testURLSegmentReserved()
929
    {
930
        $siteTree = SiteTree::create(['URLSegment' => 'admin']);
931
        $segment = $siteTree->validURLSegment();
932
933
        $this->assertFalse($segment);
934
    }
935
936
    public function testURLSegmentAutoUpdate()
937
    {
938
        $sitetree = SiteTree::create();
939
        $sitetree->Title = _t(
940
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
941
            'New {pagetype}',
942
            ['pagetype' => $sitetree->i18n_singular_name()]
943
        );
944
        $sitetree->write();
945
        $this->assertEquals(
946
            'new-page',
947
            $sitetree->URLSegment,
948
            'Sets based on default title on first save'
949
        );
950
951
        $sitetree->Title = 'Changed';
952
        $sitetree->write();
953
        $this->assertEquals(
954
            'changed',
955
            $sitetree->URLSegment,
956
            'Auto-updates when set to default title'
957
        );
958
959
        $sitetree->Title = 'Changed again';
960
        $sitetree->write();
961
        $this->assertEquals(
962
            'changed',
963
            $sitetree->URLSegment,
964
            'Does not auto-update once title has been changed'
965
        );
966
    }
967
968
    public function testURLSegmentAutoUpdateLocalized()
969
    {
970
        $oldLocale = i18n::get_locale();
971
        i18n::set_locale('de_DE');
972
973
        $sitetree = SiteTree::create();
974
        $sitetree->Title = _t(
975
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
976
            'New {pagetype}',
977
            ['pagetype' => $sitetree->i18n_singular_name()]
978
        );
979
        $sitetree->write();
980
        $this->assertEquals(
981
            'neue-seite',
982
            $sitetree->URLSegment,
983
            'Sets based on default title on first save'
984
        );
985
986
        $sitetree->Title = 'Changed';
987
        $sitetree->write();
988
        $this->assertEquals(
989
            'changed',
990
            $sitetree->URLSegment,
991
            'Auto-updates when set to default title'
992
        );
993
994
        $sitetree->Title = 'Changed again';
995
        $sitetree->write();
996
        $this->assertEquals(
997
            'changed',
998
            $sitetree->URLSegment,
999
            'Does not auto-update once title has been changed'
1000
        );
1001
1002
        i18n::set_locale($oldLocale);
1003
    }
1004
1005
    /**
1006
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1007
     */
1008
    public function testValidURLSegmentURLSegmentConflicts()
1009
    {
1010
        $sitetree = SiteTree::create();
1011
        SiteTree::config()->nested_urls = false;
1012
1013
        $sitetree->URLSegment = 'home';
1014
        $this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
1015
        $sitetree->URLSegment = 'home-noconflict';
1016
        $this->assertTrue($sitetree->validURLSegment());
1017
1018
        $sitetree->ParentID   = $this->idFromFixture(SiteTree::class, 'about');
1019
        $sitetree->URLSegment = 'home';
1020
        $this->assertFalse($sitetree->validURLSegment(), 'Conflicts are still recognised with a ParentID value');
1021
1022
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
1023
1024
        $sitetree->ParentID   = 0;
1025
        $sitetree->URLSegment = 'home';
1026
        $this->assertFalse($sitetree->validURLSegment(), 'URLSegment conflicts are recognised');
1027
1028
        $sitetree->ParentID = $this->idFromFixture(SiteTree::class, 'about');
1029
        $this->assertTrue($sitetree->validURLSegment(), 'URLSegments can be the same across levels');
1030
1031
        $sitetree->URLSegment = 'my-staff';
1032
        $this->assertFalse($sitetree->validURLSegment(), 'Nested URLSegment conflicts are recognised');
1033
        $sitetree->URLSegment = 'my-staff-noconflict';
1034
        $this->assertTrue($sitetree->validURLSegment());
1035
    }
1036
1037
    /**
1038
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1039
     */
1040
    public function testValidURLSegmentClassNameConflicts()
1041
    {
1042
        $sitetree = SiteTree::create();
1043
        $sitetree->URLSegment = Controller::class;
1044
1045
        $this->assertTrue($sitetree->validURLSegment(), 'Class names are no longer conflicts');
1046
    }
1047
1048
    /**
1049
     * @covers \SilverStripe\CMS\Model\SiteTree::validURLSegment
1050
     */
1051
    public function testValidURLSegmentControllerConflicts()
1052
    {
1053
        Config::modify()->set(SiteTree::class, 'nested_urls', true);
1054
1055
        $sitetree = SiteTree::create();
1056
        $sitetree->ParentID = $this->idFromFixture(SiteTreeTest_Conflicted::class, 'parent');
1057
1058
        $sitetree->URLSegment = 'index';
1059
        $this->assertFalse($sitetree->validURLSegment(), 'index is not a valid URLSegment');
1060
1061
        $sitetree->URLSegment = 'conflicted-action';
1062
        $this->assertFalse($sitetree->validURLSegment(), 'allowed_actions conflicts are recognised');
1063
1064
        $sitetree->URLSegment = 'conflicted-template';
1065
        $this->assertFalse($sitetree->validURLSegment(), 'Action-specific template conflicts are recognised');
1066
1067
        $sitetree->URLSegment = 'valid';
1068
        $this->assertTrue($sitetree->validURLSegment(), 'Valid URLSegment values are allowed');
1069
    }
1070
1071
    public function testURLSegmentPrioritizesExtensionVotes()
1072
    {
1073
        $sitetree = SiteTree::create();
1074
        $sitetree->URLSegment = 'unique-segment';
1075
        $this->assertTrue($sitetree->validURLSegment());
1076
1077
        SiteTree::add_extension(SiteTreeTest_Extension::class);
1078
        $sitetree = SiteTree::create();
1079
        $sitetree->URLSegment = 'unique-segment';
1080
        $this->assertFalse($sitetree->validURLSegment());
1081
        SiteTree::remove_extension(SiteTreeTest_Extension::class);
1082
    }
1083
1084
    public function testURLSegmentMultiByte()
1085
    {
1086
        URLSegmentFilter::config()->set('default_allow_multibyte', true);
1087
        $sitetree = SiteTree::create();
1088
        $sitetree->write();
1089
1090
        $sitetree->URLSegment = 'brötchen';
1091
        $sitetree->write();
1092
        $sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
1093
        $this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
1094
1095
        $sitetree->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1096
        $sitetree = DataObject::get_by_id(SiteTree::class, $sitetree->ID, false);
1097
        $this->assertEquals($sitetree->URLSegment, rawurlencode('brötchen'));
1098
        $sitetreeLive = Versioned::get_one_by_stage(
1099
            SiteTree::class,
1100
            Versioned::LIVE,
1101
            '"SiteTree"."ID" = ' . $sitetree->ID,
1102
            false
1103
        );
1104
        $this->assertEquals($sitetreeLive->URLSegment, rawurlencode('brötchen'));
1105
    }
1106
1107
    public function testVersionsAreCreated()
1108
    {
1109
        $p = SiteTree::create();
1110
        $p->Content = "one";
1111
        $p->write();
1112
        $this->assertEquals(1, $p->Version);
1113
1114
        // No changes don't bump version
1115
        $p->write();
1116
        $this->assertEquals(1, $p->Version);
1117
1118
        $p->Content = "two";
1119
        $p->write();
1120
        $this->assertEquals(2, $p->Version);
1121
1122
        // Only change meta-data don't bump version
1123
        $p->HasBrokenLink = true;
1124
        $p->write();
1125
        $p->HasBrokenLink = false;
1126
        $p->write();
1127
        $this->assertEquals(2, $p->Version);
1128
1129
        $p->Content = "three";
1130
        $p->write();
1131
        $this->assertEquals(3, $p->Version);
1132
    }
1133
1134
    public function testPageTypeClasses()
1135
    {
1136
        $classes = SiteTree::page_type_classes();
1137
        $this->assertNotContains(SiteTree::class, $classes, 'Page types do not include base class');
1138
        $this->assertContains('Page', $classes, 'Page types do contain subclasses');
1139
1140
        // Testing what happens in an incorrect config value is set - hide_ancestor should be a string
1141
        Config::modify()->set(SiteTreeTest_ClassA::class, 'hide_ancestor', true);
1142
        $newClasses = SiteTree::page_type_classes();
1143
        $this->assertEquals(
1144
            $classes,
1145
            $newClasses,
1146
            'Setting hide_ancestor to a boolean (incorrect) value caused a page class to be hidden'
1147
        );
1148
    }
1149
1150
    /**
1151
     * Tests that core subclasses of SiteTree are included in allowedChildren() by default, but not instances of
1152
     * HiddenClass
1153
     */
1154
    public function testAllowedChildrenContainsCoreSubclassesButNotHiddenClass()
1155
    {
1156
        $page = SiteTree::create();
1157
        $allowedChildren = $page->allowedChildren();
1158
1159
        $this->assertContains(
1160
            VirtualPage::class,
1161
            $allowedChildren,
1162
            'Includes core subclasses by default'
1163
        );
1164
1165
        $this->assertNotContains(
1166
            SiteTreeTest_ClassE::class,
1167
            $allowedChildren,
1168
            'HiddenClass instances should not be returned'
1169
        );
1170
    }
1171
1172
    /**
1173
     * Tests that various types of SiteTree classes will or will not be returned from the allowedChildren method
1174
     * @dataProvider allowedChildrenProvider
1175
     * @param string $className
1176
     * @param array  $expected
1177
     * @param string $assertionMessage
1178
     */
1179
    public function testAllowedChildren($className, $expected, $assertionMessage)
1180
    {
1181
        $class = new $className;
1182
        $this->assertEquals($expected, $class->allowedChildren(), $assertionMessage);
1183
    }
1184
1185
    /**
1186
     * @return array
1187
     */
1188
    public function allowedChildrenProvider()
1189
    {
1190
        return [
1191
            [
1192
                // Class name
1193
                SiteTreeTest_ClassA::class,
1194
                // Expected
1195
                [ SiteTreeTest_ClassB::class ],
1196
                // Assertion message
1197
                'Direct setting of allowed children',
1198
            ],
1199
            [
1200
                SiteTreeTest_ClassB::class,
1201
                [ SiteTreeTest_ClassC::class, SiteTreeTest_ClassCext::class ],
1202
                'Includes subclasses',
1203
            ],
1204
            [
1205
                SiteTreeTest_ClassC::class,
1206
                [],
1207
                'Null setting',
1208
            ],
1209
            [
1210
                SiteTreeTest_ClassD::class,
1211
                [SiteTreeTest_ClassC::class],
1212
                'Excludes subclasses if class is prefixed by an asterisk',
1213
            ],
1214
        ];
1215
    }
1216
1217
    public function testAllowedChildrenValidation()
1218
    {
1219
        $page = SiteTree::create();
1220
        $page->write();
1221
        $classA = new SiteTreeTest_ClassA();
1222
        $classA->write();
1223
        $classB = new SiteTreeTest_ClassB();
1224
        $classB->write();
1225
        $classC = new SiteTreeTest_ClassC();
1226
        $classC->write();
1227
        $classD = new SiteTreeTest_ClassD();
1228
        $classD->write();
1229
        $classCext = new SiteTreeTest_ClassCext();
1230
        $classCext->write();
1231
1232
        $classB->ParentID = $page->ID;
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1233
        $valid = $classB->doValidate();
1234
        $this->assertTrue($valid->isValid(), "Does allow children on unrestricted parent");
1235
1236
        $classB->ParentID = $classA->ID;
1237
        $valid = $classB->doValidate();
1238
        $this->assertTrue($valid->isValid(), "Does allow child specifically allowed by parent");
1239
1240
        $classC->ParentID = $classA->ID;
1241
        $valid = $classC->doValidate();
1242
        $this->assertFalse($valid->isValid(), "Doesnt allow child on parents specifically restricting children");
1243
1244
        $classB->ParentID = $classC->ID;
1245
        $valid = $classB->doValidate();
1246
        $this->assertFalse($valid->isValid(), "Doesnt allow child on parents disallowing all children");
1247
1248
        $classB->ParentID = $classCext->ID;
1249
        $valid = $classB->doValidate();
1250
        $this->assertTrue($valid->isValid(), "Extensions of allowed classes are incorrectly reported as invalid");
1251
1252
        $classCext->ParentID = $classD->ID;
1253
        $valid = $classCext->doValidate();
1254
        $this->assertFalse(
1255
            $valid->isValid(),
1256
            "Doesnt allow child where only parent class is allowed on parent node, and asterisk prefixing is used"
1257
        );
1258
    }
1259
1260
    public function testClassDropdown()
1261
    {
1262
        $sitetree = SiteTree::create();
1263
        $method = new ReflectionMethod($sitetree, 'getClassDropdown');
1264
        $method->setAccessible(true);
1265
1266
        Security::setCurrentUser(null);
1267
        $this->assertArrayNotHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1268
1269
        $this->loginWithPermission('ADMIN');
1270
        $this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1271
1272
        $this->loginWithPermission('CMS_ACCESS_CMSMain');
1273
        $this->assertArrayHasKey(SiteTreeTest_ClassA::class, $method->invoke($sitetree));
1274
1275
        Security::setCurrentUser(null);
1276
    }
1277
1278
    public function testCanBeRoot()
1279
    {
1280
        $page = SiteTree::create();
1281
        $page->ParentID = 0;
1282
        $page->write();
1283
1284
        $notRootPage = new SiteTreeTest_NotRoot();
1285
        $notRootPage->ParentID = 0;
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1286
        $isDetected = false;
1287
        try {
1288
            $notRootPage->write();
1289
        } catch (ValidationException $e) {
1290
            $this->assertContains('is not allowed on the root level', $e->getMessage());
1291
            $isDetected = true;
1292
        }
1293
1294
        if (!$isDetected) {
1295
            $this->fail('Fails validation with $can_be_root=false');
1296
        }
1297
    }
1298
1299
    public function testModifyStatusFlagByInheritance()
1300
    {
1301
        $node = new SiteTreeTest_StageStatusInherit();
1302
        $treeTitle = $node->getTreeTitle();
1303
        $this->assertContains('InheritedTitle', $treeTitle);
1304
        $this->assertContains('inherited-class', $treeTitle);
1305
    }
1306
1307
    public function testMenuTitleIsUnsetWhenEqualsTitle()
1308
    {
1309
        $page = SiteTree::create();
1310
        $page->Title = 'orig';
1311
        $page->MenuTitle = 'orig';
1312
        $page->write();
1313
1314
        // change menu title
1315
        $page->MenuTitle = 'changed';
1316
        $page->write();
1317
        $page = SiteTree::get()->byID($page->ID);
1318
        $this->assertEquals('changed', $page->getField('MenuTitle'));
1319
1320
        // change menu title back
1321
        $page->MenuTitle = 'orig';
1322
        $page->write();
1323
        $page = SiteTree::get()->byID($page->ID);
1324
        $this->assertEquals(null, $page->getField('MenuTitle'));
1325
    }
1326
1327
    public function testMetaTagGeneratorDisabling()
1328
    {
1329
        $generator = Config::inst()->get(SiteTree::class, 'meta_generator');
1330
1331
        $page = new SiteTreeTest_PageNode();
1332
1333
        $meta = $page->MetaTags();
1334
        $this->assertEquals(
1335
            1,
1336
            preg_match('/.*meta name="generator" content="SilverStripe - http:\/\/silverstripe.org".*/', $meta),
1337
            'test default functionality - uses value from Config'
1338
        );
1339
1340
        // test proper escaping of quotes in attribute value
1341
        Config::modify()->set(SiteTree::class, 'meta_generator', 'Generator with "quotes" in it');
1342
        $meta = $page->MetaTags();
1343
        $this->assertEquals(
1344
            1,
1345
            preg_match('/.*meta name="generator" content="Generator with &quot;quotes&quot; in it".*/', $meta),
1346
            'test proper escaping of values from Config'
1347
        );
1348
1349
        // test empty generator - no tag should appear at all
1350
        Config::modify()->set(SiteTree::class, 'meta_generator', '');
1351
        $meta = $page->MetaTags();
1352
        $this->assertEquals(
1353
            0,
1354
            preg_match('/.*meta name=.generator..*/', $meta),
1355
            'test blank value means no tag generated'
1356
        );
1357
1358
        // reset original value
1359
        Config::modify()->set(SiteTree::class, 'meta_generator', $generator);
1360
    }
1361
1362
1363
    public function testGetBreadcrumbItems()
1364
    {
1365
        $page = $this->objFromFixture(SiteTree::class, "breadcrumbs");
1366
        $this->assertEquals(1, $page->getBreadcrumbItems()->count(), "Only display current page.");
1367
1368
        // Test breadcrumb order
1369
        $page = $this->objFromFixture(SiteTree::class, "breadcrumbs5");
1370
        $breadcrumbs = $page->getBreadcrumbItems();
1371
        $this->assertEquals($breadcrumbs->count(), 5, "Display all breadcrumbs");
1372
        $this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs", "Breadcrumbs should be the first item.");
1373
        $this->assertEquals($breadcrumbs->last()->Title, "Breadcrumbs 5", "Breadcrumbs 5 should be last item.");
1374
1375
        // Test breadcrumb max depth
1376
        $breadcrumbs = $page->getBreadcrumbItems(2);
1377
        $this->assertEquals($breadcrumbs->count(), 2, "Max depth should limit the breadcrumbs to 2 items.");
1378
        $this->assertEquals($breadcrumbs->first()->Title, "Breadcrumbs 4", "First item should be Breadrcumbs 4.");
1379
        $this->assertEquals($breadcrumbs->last()->Title, "Breadcrumbs 5", "Breadcrumbs 5 should be last.");
1380
    }
1381
1382
    /**
1383
     * Tests SiteTree::MetaTags
1384
     * Note that this test makes no assumption on the closing of tags (other than <title></title>)
1385
     */
1386
    public function testMetaTags()
1387
    {
1388
        $this->logInWithPermission('ADMIN');
1389
        $page = $this->objFromFixture(SiteTree::class, 'metapage');
1390
1391
        // Test with title
1392
        $meta = $page->MetaTags();
1393
        $charset = Config::inst()->get(ContentNegotiator::class, 'encoding');
1394
        $this->assertContains('<meta http-equiv="Content-Type" content="text/html; charset=' . $charset . '"', $meta);
1395
        $this->assertContains('<meta name="description" content="The &lt;br /&gt; and &lt;br&gt; tags"', $meta);
1396
        $this->assertContains('<link rel="canonical" href="http://www.mysite.com/html-and-xml"', $meta);
1397
        $this->assertContains('<meta name="x-page-id" content="' . $page->ID . '"', $meta);
1398
        $this->assertContains('<meta name="x-cms-edit-link" content="' . $page->CMSEditLink() . '"', $meta);
1399
        $this->assertContains('<title>HTML &amp; XML</title>', $meta);
1400
1401
        // Test without title
1402
        $meta = $page->MetaTags(false);
1403
        $this->assertNotContains('<title>', $meta);
1404
    }
1405
1406
    /**
1407
     * Test that orphaned pages are handled correctly
1408
     */
1409
    public function testOrphanedPages()
1410
    {
1411
        $origStage = Versioned::get_reading_mode();
1412
1413
        // Setup user who can view draft content, but lacks cms permission.
1414
        // To users such as this, orphaned pages should be inaccessible. canView for these pages is only
1415
        // necessary for admin / cms users, who require this permission to edit / rearrange these pages.
1416
        $permission = new Permission();
1417
        $permission->Code = 'VIEW_DRAFT_CONTENT';
1418
        $group = new Group(['Title' => 'Staging Users']);
1419
        $group->write();
1420
        $group->Permissions()->add($permission);
1421
        $member = new Member();
1422
        $member->Email = '[email protected]';
1423
        $member->write();
1424
        $member->Groups()->add($group);
1425
1426
        // both pages are viewable in stage
1427
        Versioned::set_stage(Versioned::DRAFT);
1428
        $about = $this->objFromFixture(SiteTree::class, 'about');
1429
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1430
        $this->assertFalse($about->isOrphaned());
1431
        $this->assertFalse($staff->isOrphaned());
1432
        $this->assertTrue($about->canView($member));
1433
        $this->assertTrue($staff->canView($member));
1434
1435
        // Publishing only the child page to live should orphan the live record, but not the staging one
1436
        $staff->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1437
        $this->assertFalse($staff->isOrphaned());
1438
        $this->assertTrue($staff->canView($member));
1439
        Versioned::set_stage(Versioned::LIVE);
1440
        $staff = $this->objFromFixture(SiteTree::class, 'staff'); // Live copy of page
1441
        $this->assertTrue($staff->isOrphaned()); // because parent isn't published
1442
        $this->assertFalse($staff->canView($member));
1443
1444
        // Publishing the parent page should restore visibility
1445
        Versioned::set_stage(Versioned::DRAFT);
1446
        $about = $this->objFromFixture(SiteTree::class, 'about');
1447
        $about->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1448
        Versioned::set_stage(Versioned::LIVE);
1449
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1450
        $this->assertFalse($staff->isOrphaned());
1451
        $this->assertTrue($staff->canView($member));
1452
1453
        // Removing staging page should not prevent live page being visible
1454
        $about->deleteFromStage('Stage');
1455
        $staff->deleteFromStage('Stage');
1456
        $staff = $this->objFromFixture(SiteTree::class, 'staff');
1457
        $this->assertFalse($staff->isOrphaned());
1458
        $this->assertTrue($staff->canView($member));
1459
1460
        // Cleanup
1461
        Versioned::set_reading_mode($origStage);
1462
    }
1463
1464
    /**
1465
     * Test archived page behaviour
1466
     */
1467
    public function testArchivedPages()
1468
    {
1469
        $this->logInWithPermission('ADMIN');
1470
1471
        /** @var SiteTree $page */
1472
        $page = $this->objFromFixture(SiteTree::class, 'home');
1473
        $this->assertTrue($page->canAddChildren());
1474
        $this->assertTrue($page->isOnDraft());
1475
        $this->assertFalse($page->isPublished());
1476
1477
        // Publish
1478
        $page->publishRecursive();
1479
        $this->assertTrue($page->canAddChildren());
1480
        $this->assertTrue($page->isOnDraft());
1481
        $this->assertTrue($page->isPublished());
1482
1483
        // Archive
1484
        $page->doArchive();
1485
        $this->assertFalse($page->canAddChildren());
1486
        $this->assertFalse($page->isOnDraft());
1487
        $this->assertTrue($page->isArchived());
1488
        $this->assertFalse($page->isPublished());
1489
    }
1490
1491
    public function testCanNot()
1492
    {
1493
        // Test that
1494
        $this->logInWithPermission('ADMIN');
1495
        $page = new SiteTreeTest_AdminDenied();
1496
        $this->assertFalse($page->canCreate());
1497
        $this->assertFalse($page->canEdit());
1498
        $this->assertFalse($page->canDelete());
1499
        $this->assertFalse($page->canAddChildren());
1500
        $this->assertFalse($page->canView());
1501
    }
1502
1503
    public function testCanPublish()
1504
    {
1505
        $page = new SiteTreeTest_ClassD();
1506
        $this->logOut();
1507
1508
        // Test that false overrides any can_publish = true
1509
        SiteTreeTest_ExtensionA::$can_publish = true;
1510
        SiteTreeTest_ExtensionB::$can_publish = false;
1511
        $this->assertFalse($page->canPublish());
1512
        SiteTreeTest_ExtensionA::$can_publish = false;
1513
        SiteTreeTest_ExtensionB::$can_publish = true;
1514
        $this->assertFalse($page->canPublish());
1515
1516
        // Test null extensions fall back to canEdit()
1517
        SiteTreeTest_ExtensionA::$can_publish = null;
1518
        SiteTreeTest_ExtensionB::$can_publish = null;
1519
        $page->canEditValue = true;
1520
        $this->assertTrue($page->canPublish());
1521
        $page->canEditValue = false;
1522
        $this->assertFalse($page->canPublish());
1523
    }
1524
1525
    /**
1526
     * Test url rewriting extensions
1527
     */
1528
    public function testLinkExtension()
1529
    {
1530
        Director::config()->set('alternate_base_url', 'http://www.baseurl.com');
1531
        $page = new SiteTreeTest_ClassD();
1532
        $page->URLSegment = 'classd';
1533
        $page->write();
1534
        $this->assertEquals(
1535
            'http://www.updatedhost.com/classd/myaction?extra=1',
1536
            $page->Link('myaction')
1537
        );
1538
        $this->assertEquals(
1539
            'http://www.updatedhost.com/classd/myaction?extra=1',
1540
            $page->AbsoluteLink('myaction')
1541
        );
1542
        $this->assertEquals(
1543
            'classd/myaction',
1544
            $page->RelativeLink('myaction')
1545
        );
1546
    }
1547
1548
    /**
1549
     * Test that the controller name for a SiteTree instance can be gathered by appending "Controller" to the SiteTree
1550
     * class name in a PSR-2 compliant manner.
1551
     */
1552
    public function testGetControllerName()
1553
    {
1554
        $class = Page::create();
1555
        $this->assertSame('PageController', $class->getControllerName());
1556
    }
1557
1558
    /**
1559
     * Test that underscored class names (legacy) are still supported (deprecation notice is issued though).
1560
     */
1561
    public function testGetControllerNameWithUnderscoresIsSupported()
1562
    {
1563
        $class = new SiteTreeTest_LegacyControllerName;
1564
        $this->assertEquals(SiteTreeTest_LegacyControllerName_Controller::class, $class->getControllerName());
1565
    }
1566
1567
    public function testTreeTitleCache()
1568
    {
1569
        $siteTree = SiteTree::create();
1570
        $user = $this->objFromFixture(Member::class, 'allsections');
1571
        Security::setCurrentUser($user);
1572
        $pageClass = array_values(SiteTree::page_type_classes())[0];
1573
1574
        $mockPageMissesCache = $this->getMockBuilder($pageClass)
1575
            ->setMethods(['canCreate'])
1576
            ->getMock();
1577
        $mockPageMissesCache
1578
            ->expects($this->exactly(3))
1579
            ->method('canCreate');
1580
1581
        $mockPageHitsCache = $this->getMockBuilder($pageClass)
1582
            ->setMethods(['canCreate'])
1583
            ->getMock();
1584
        $mockPageHitsCache
1585
            ->expects($this->never())
1586
            ->method('canCreate');
1587
1588
        // Initially, cache misses (1)
1589
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1590
        $title = $siteTree->getTreeTitle();
1591
        $this->assertNotNull($title);
1592
1593
        // Now it hits
1594
        Injector::inst()->registerService($mockPageHitsCache, $pageClass);
1595
        $title = $siteTree->getTreeTitle();
1596
        $this->assertNotNull($title);
1597
1598
1599
        // Mutating member record invalidates cache. Misses (2)
1600
        $user->FirstName = 'changed';
1601
        $user->write();
1602
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1603
        $title = $siteTree->getTreeTitle();
1604
        $this->assertNotNull($title);
1605
1606
        // Now it hits again
1607
        Injector::inst()->registerService($mockPageHitsCache, $pageClass);
1608
        $title = $siteTree->getTreeTitle();
1609
        $this->assertNotNull($title);
1610
1611
        // Different user. Misses. (3)
1612
        $user = $this->objFromFixture(Member::class, 'editor');
1613
        Security::setCurrentUser($user);
1614
        Injector::inst()->registerService($mockPageMissesCache, $pageClass);
1615
        $title = $siteTree->getTreeTitle();
1616
        $this->assertNotNull($title);
1617
    }
1618
}
1619