array('SiteTreeSubsites', 'Translatable') ); protected $requiredExtensions = array( 'SiteTree' => array('VirtualPageTest_PageExtension') ); public function setUp() { parent::setUp(); $this->origInitiallyCopiedFields = VirtualPage::config()->initially_copied_fields; Config::inst()->remove('VirtualPage', 'initially_copied_fields'); VirtualPage::config()->initially_copied_fields = array_merge( $this->origInitiallyCopiedFields, array('MyInitiallyCopiedField') ); $this->origNonVirtualField = VirtualPage::config()->non_virtual_fields; Config::inst()->remove('VirtualPage', 'non_virtual_fields'); VirtualPage::config()->non_virtual_fields = array_merge( $this->origNonVirtualField, array('MyNonVirtualField', 'MySharedNonVirtualField') ); } public function tearDown() { parent::tearDown(); Config::inst()->remove('VirtualPage', 'initially_copied_fields'); Config::inst()->remove('VirtualPage', 'non_virtual_fields'); VirtualPage::config()->initially_copied_fields = $this->origInitiallyCopiedFields; VirtualPage::config()->non_virtual_fields = $this->origNonVirtualField; } /** * Test that, after you update the source page of a virtual page, all the virtual pages * are updated */ public function testEditingSourcePageUpdatesVirtualPages() { $master = $this->objFromFixture('Page', 'master'); $master->Title = "New title"; $master->MenuTitle = "New menutitle"; $master->Content = "
New content
"; $master->write(); $vp1 = $this->objFromFixture('VirtualPage', 'vp1'); $vp2 = $this->objFromFixture('VirtualPage', 'vp2'); $this->assertEquals("New title", $vp1->Title); $this->assertEquals("New title", $vp2->Title); $this->assertEquals("New menutitle", $vp1->MenuTitle); $this->assertEquals("New menutitle", $vp2->MenuTitle); $this->assertEquals("New content
", $vp1->Content); $this->assertEquals("New content
", $vp2->Content); } /** * Test that, after you publish the source page of a virtual page, all the already published * virtual pages are published */ public function testPublishingSourcePagePublishesAlreadyPublishedVirtualPages() { $this->logInWithPermission('ADMIN'); $master = $this->objFromFixture('Page', 'master'); $master->doPublish(); $master->Title = "New title"; $master->MenuTitle = "New menutitle"; $master->Content = "New content
"; $master->write(); $vp1 = DataObject::get_by_id("VirtualPage", $this->idFromFixture('VirtualPage', 'vp1')); $vp2 = DataObject::get_by_id("VirtualPage", $this->idFromFixture('VirtualPage', 'vp2')); $this->assertTrue($vp1->doPublish()); $this->assertTrue($vp2->doPublish()); $master->doPublish(); Versioned::reading_stage("Live"); $vp1 = DataObject::get_by_id("VirtualPage", $this->idFromFixture('VirtualPage', 'vp1')); $vp2 = DataObject::get_by_id("VirtualPage", $this->idFromFixture('VirtualPage', 'vp2')); $this->assertNotNull($vp1); $this->assertNotNull($vp2); $this->assertEquals("New title", $vp1->Title); $this->assertEquals("New title", $vp2->Title); $this->assertEquals("New menutitle", $vp1->MenuTitle); $this->assertEquals("New menutitle", $vp2->MenuTitle); $this->assertEquals("New content
", $vp1->Content); $this->assertEquals("New content
", $vp2->Content); Versioned::reading_stage("Stage"); } /** * Test that virtual pages get the content from the master page when they are created. */ public function testNewVirtualPagesGrabTheContentFromTheirMaster() { $vp = new VirtualPage(); $vp->write(); $vp->CopyContentFromID = $this->idFromFixture('Page', 'master'); $vp->write(); $this->assertEquals("My Page", $vp->Title); $this->assertEquals("My Page Nav", $vp->MenuTitle); $vp->CopyContentFromID = $this->idFromFixture('Page', 'master2'); $vp->write(); $this->assertEquals("My Other Page", $vp->Title); $this->assertEquals("My Other Page Nav", $vp->MenuTitle); } /** * Virtual pages are always supposed to chose the same content as the published source page. * This means that when you publish them, they should show the published content of the source * page, not the draft content at the time when you clicked 'publish' in the CMS. */ public function testPublishingAVirtualPageCopiedPublishedContentNotDraftContent() { $p = new Page(); $p->Content = "published content"; $p->write(); $p->doPublish(); // Don't publish this change - published page will still say 'published content' $p->Content = "draft content"; $p->write(); $vp = new VirtualPage(); $vp->CopyContentFromID = $p->ID; $vp->write(); $vp->doPublish(); // The draft content of the virtual page should say 'draft content' $this->assertEquals('draft content', DB::query('SELECT "Content" from "SiteTree" WHERE "ID" = ' . $vp->ID)->value()); // The published content of the virtual page should say 'published content' $this->assertEquals('published content', DB::query('SELECT "Content" from "SiteTree_Live" WHERE "ID" = ' . $vp->ID)->value()); } public function testCantPublishVirtualPagesBeforeTheirSource() { // An unpublished source page $p = new Page(); $p->Content = "test content"; $p->write(); // With no source page, we can't publish $vp = new VirtualPage(); $vp->write(); $this->assertFalse($vp->canPublish()); // When the source page isn't published, we can't publish $vp->CopyContentFromID = $p->ID; $vp->write(); $this->assertFalse($vp->canPublish()); // Once the source page gets published, then we can publish $p->doPublish(); $this->assertTrue($vp->canPublish()); } public function testCanDeleteOrphanedVirtualPagesFromLive() { // An unpublished source page $p = new Page(); $p->Content = "test content"; $p->write(); $p->doPublish(); $vp = new VirtualPage(); $vp->CopyContentFromID = $p->ID; $vp->write(); // Delete the source page $this->assertTrue($vp->canPublish()); $this->assertTrue($p->doDeleteFromLive()); // Confirm that we can unpublish, but not publish $this->assertTrue($vp->canDeleteFromLive()); $this->assertFalse($vp->canPublish()); // Confirm that the action really works $this->assertTrue($vp->doDeleteFromLive()); $this->assertNull(DB::query("SELECT \"ID\" FROM \"SiteTree_Live\" WHERE \"ID\" = $vp->ID")->value()); } public function testVirtualPagesArentInappropriatelyPublished() { // Fixture $p = new Page(); $p->Content = "test content"; $p->write(); $vp = new VirtualPage(); $vp->CopyContentFromID = $p->ID; $vp->write(); // VP is oragne $this->assertTrue($vp->IsAddedToStage); // VP is still orange after we publish $p->doPublish(); $this->fixVersionNumberCache($vp); $this->assertTrue($vp->IsAddedToStage); // A new VP created after P's initial construction $vp2 = new VirtualPage(); $vp2->CopyContentFromID = $p->ID; $vp2->write(); $this->assertTrue($vp2->IsAddedToStage); // Also remains orange after a republish $p->Content = "new content"; $p->write(); $p->doPublish(); $this->fixVersionNumberCache($vp2); $this->assertTrue($vp2->IsAddedToStage); // VP is now published $vp->doPublish(); $this->fixVersionNumberCache($vp); $this->assertTrue($vp->ExistsOnLive); $this->assertFalse($vp->IsModifiedOnStage); // P edited, VP and P both go green $p->Content = "third content"; $p->write(); $this->fixVersionNumberCache($vp, $p); $this->assertTrue($p->IsModifiedOnStage); $this->assertTrue($vp->IsModifiedOnStage); // Publish, VP goes black $p->doPublish(); $this->fixVersionNumberCache($vp); $this->assertTrue($vp->ExistsOnLive); $this->assertFalse($vp->IsModifiedOnStage); } public function testVirtualPagesCreateVersionRecords() { $source = $this->objFromFixture('Page', 'master'); $source->Title = "T0"; $source->write(); $source->doPublish(); // Creating a new VP to ensure that Version #s are out of alignment $vp = new VirtualPage(); $vp->CopyContentFromID = $source->ID; $vp->write(); $source->Title = "T1"; $source->write(); $source->Title = "T2"; $source->write(); $this->assertEquals($vp->ID, DB::query("SELECT \"RecordID\" FROM \"SiteTree_versions\" WHERE \"RecordID\" = $vp->ID AND \"Title\" = 'T1'")->value()); $this->assertEquals($vp->ID, DB::query("SELECT \"RecordID\" FROM \"SiteTree_versions\" WHERE \"RecordID\" = $vp->ID AND \"Title\" = 'T2'")->value()); $this->assertEquals($vp->ID, DB::query("SELECT \"RecordID\" FROM \"SiteTree_versions\" WHERE \"RecordID\" = $vp->ID AND \"Version\" = $vp->Version")->value()); $vp->doPublish(); // Check that the published content is copied from the published page, with a legal // version $liveVersion = DB::query("SELECT \"Version\" FROM \"SiteTree_Live\" WHERE \"ID\" = $vp->ID")->value(); $this->assertEquals("T0", DB::query("SELECT \"Title\" FROM \"SiteTree_Live\" WHERE \"ID\" = $vp->ID")->value()); // SiteTree_Live.Version should reference a legal entry in SiteTree_versions for the // virtual page $this->assertEquals("T0", DB::query("SELECT \"Title\" FROM \"SiteTree_versions\" WHERE \"RecordID\" = $vp->ID AND \"Version\" = $liveVersion")->value()); } public function fixVersionNumberCache($page) { $pages = func_get_args(); foreach($pages as $p) { Versioned::prepopulate_versionnumber_cache('SiteTree', 'Stage', array($p->ID)); Versioned::prepopulate_versionnumber_cache('SiteTree', 'Live', array($p->ID)); } } public function testUnpublishingSourcePageOfAVirtualPageAlsoUnpublishesVirtualPage() { // Create page and virutal page $p = new Page(); $p->Title = "source"; $p->write(); $this->assertTrue($p->doPublish()); $vp = new VirtualPage(); $vp->CopyContentFromID = $p->ID; $vp->write(); $this->assertTrue($vp->doPublish()); // All is fine, the virtual page doesn't have a broken link $this->assertFalse($vp->HasBrokenLink); // Unpublish the source page, confirm that the virtual page has also been unpublished $p->doUnpublish(); // The draft VP still has the CopyContentFromID link $vp->flushCache(); $vp = DataObject::get_by_id('SiteTree', $vp->ID); $this->assertEquals($p->ID, $vp->CopyContentFromID); $vpLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree"."ID" = ' . $vp->ID); $this->assertNull($vpLive); // Delete from draft, confirm that the virtual page has a broken link on the draft site $p->delete(); $vp->flushCache(); $vp = DataObject::get_by_id('SiteTree', $vp->ID); $this->assertEquals(1, $vp->HasBrokenLink); } public function testDeletingFromLiveSourcePageOfAVirtualPageAlsoUnpublishesVirtualPage() { // Create page and virutal page $p = new Page(); $p->Title = "source"; $p->write(); $this->assertTrue($p->doPublish()); $vp = new VirtualPage(); $vp->CopyContentFromID = $p->ID; $vp->write(); $this->assertTrue($vp->doPublish()); // All is fine, the virtual page doesn't have a broken link $this->assertFalse($vp->HasBrokenLink); // Delete the source page from draft, confirm that this creates a broken link $pID = $p->ID; $p->delete(); $vp->flushCache(); $vp = DataObject::get_by_id('SiteTree', $vp->ID); $this->assertEquals(1, $vp->HasBrokenLink); // Delete the source page form live, confirm that the virtual page has also been unpublished $pLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree"."ID" = ' . $pID); $this->assertTrue($pLive->doDeleteFromLive()); $vpLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree"."ID" = ' . $vp->ID); $this->assertNull($vpLive); // Delete from draft, confirm that the virtual page has a broken link on the draft site $pLive->delete(); $vp->flushCache(); $vp = DataObject::get_by_id('SiteTree', $vp->ID); $this->assertEquals(1, $vp->HasBrokenLink); } /** * Base functionality tested in {@link SiteTreeTest->testAllowedChildrenValidation()}. */ public function testAllowedChildrenLimitedOnVirtualPages() { $classA = new SiteTreeTest_ClassA(); $classA->write(); $classB = new SiteTreeTest_ClassB(); $classB->write(); $classBVirtual = new VirtualPage(); $classBVirtual->CopyContentFromID = $classB->ID; $classBVirtual->write(); $classC = new SiteTreeTest_ClassC(); $classC->write(); $classCVirtual = new VirtualPage(); $classCVirtual->CopyContentFromID = $classC->ID; $classCVirtual->write(); $classBVirtual->ParentID = $classA->ID; $valid = $classBVirtual->validate(); $this->assertTrue($valid->valid(), "Does allow child linked to virtual page type allowed by parent"); $classCVirtual->ParentID = $classA->ID; $valid = $classCVirtual->validate(); $this->assertFalse($valid->valid(), "Doesn't allow child linked to virtual page type disallowed by parent"); } public function testGetVirtualFields() { // Needs association with an original, otherwise will just return the "base" virtual fields $page = new VirtualPageTest_ClassA(); $page->write(); $virtual = new VirtualPage(); $virtual->CopyContentFromID = $page->ID; $virtual->write(); $this->assertContains('MyVirtualField', $virtual->getVirtualFields()); $this->assertNotContains('MyNonVirtualField', $virtual->getVirtualFields()); $this->assertNotContains('MyInitiallyCopiedField', $virtual->getVirtualFields()); } public function testCopyFrom() { $original = new VirtualPageTest_ClassA(); $original->MyInitiallyCopiedField = 'original'; $original->MyVirtualField = 'original'; $original->MyNonVirtualField = 'original'; $original->write(); $virtual = new VirtualPage(); $virtual->CopyContentFromID = $original->ID; $virtual->write(); $virtual->copyFrom($original); // Using getField() to avoid side effects from an overloaded __get() $this->assertEquals( 'original', $virtual->getField('MyInitiallyCopiedField'), 'Fields listed in $initially_copied_fields are copied on first copyFrom() invocation' ); $this->assertEquals( 'original', $virtual->getField('MyVirtualField'), 'Fields not listed in $initially_copied_fields are copied in copyFrom()' ); $this->assertNull( $virtual->getField('MyNonVirtualField'), 'Fields listed in $non_virtual_fields are not copied in copyFrom()' ); $original->MyInitiallyCopiedField = 'changed'; $original->write(); $virtual->copyFrom($original); $this->assertEquals( 'original', $virtual->MyInitiallyCopiedField, 'Fields listed in $initially_copied_fields are not copied on subsequent copyFrom() invocations' ); } public function testWriteWithoutVersion() { $original = new SiteTree(); $original->write(); // Create a second version (different behaviour), // as SiteTree->onAfterWrite() checks for Version == 1 $original->Title = 'prepare'; $original->write(); $originalVersion = $original->Version; $virtual = new VirtualPage(); $virtual->CopyContentFromID = $original->ID; $virtual->write(); // Create a second version, see above. $virtual->Title = 'prepare'; $virtual->write(); $virtualVersion = $virtual->Version; $virtual->Title = 'changed 1'; $virtual->writeWithoutVersion(); $this->assertEquals( $virtual->Version, $virtualVersion, 'writeWithoutVersion() on VirtualPage doesnt increment version' ); $original->Title = 'changed 2'; $original->writeWithoutVersion(); DataObject::flush_and_destroy_cache(); $virtual = DataObject::get_by_id('VirtualPage', $virtual->ID, false); $this->assertEquals( $virtual->Version, $virtualVersion, 'writeWithoutVersion() on original page doesnt increment version on related VirtualPage' ); $original->Title = 'changed 3'; $original->write(); DataObject::flush_and_destroy_cache(); $virtual = DataObject::get_by_id('VirtualPage', $virtual->ID, false); $this->assertGreaterThan( $virtualVersion, $virtual->Version, 'write() on original page does increment version on related VirtualPage' ); } public function testCanBeRoot() { $page = new SiteTree(); $page->ParentID = 0; $page->write(); $notRootPage = new VirtualPageTest_NotRoot(); // we don't want the original on root, but rather the VirtualPage pointing to it $notRootPage->ParentID = $page->ID; $notRootPage->write(); $virtual = new VirtualPage(); $virtual->CopyContentFromID = $page->ID; $virtual->write(); $virtual = DataObject::get_by_id('VirtualPage', $virtual->ID, false); $virtual->CopyContentFromID = $notRootPage->ID; $virtual->flushCache(); $isDetected = false; try { $virtual->write(); } catch(ValidationException $e) { $this->assertContains('is not allowed on the root level', $e->getMessage()); $isDetected = true; } if(!$isDetected) $this->fail('Fails validation with $can_be_root=false'); } public function testPageTypeChangeDoesntKeepOrphanedVirtualPageRecord() { $page = new SiteTree(); $page->write(); $page->publish('Stage', 'Live'); $virtual = new VirtualPageTest_VirtualPageSub(); $virtual->CopyContentFromID = $page->ID; $virtual->write(); $virtual->publish('Stage', 'Live'); $nonVirtual = $virtual; $nonVirtual->ClassName = 'VirtualPageTest_ClassA'; $nonVirtual->write(); // not publishing $this->assertNotNull( DB::query(sprintf('SELECT "ID" FROM "SiteTree" WHERE "ID" = %d', $nonVirtual->ID))->value(), "Shared base database table entry exists after type change" ); $this->assertNull( DB::query(sprintf('SELECT "ID" FROM "VirtualPage" WHERE "ID" = %d', $nonVirtual->ID))->value(), "Base database table entry no longer exists after type change" ); $this->assertNull( DB::query(sprintf('SELECT "ID" FROM "VirtualPageTest_VirtualPageSub" WHERE "ID" = %d', $nonVirtual->ID))->value(), "Sub database table entry no longer exists after type change" ); $this->assertNull( DB::query(sprintf('SELECT "ID" FROM "VirtualPage_Live" WHERE "ID" = %d', $nonVirtual->ID))->value(), "Base live database table entry no longer exists after type change" ); $this->assertNull( DB::query(sprintf('SELECT "ID" FROM "VirtualPageTest_VirtualPageSub_Live" WHERE "ID" = %d', $nonVirtual->ID))->value(), "Sub live database table entry no longer exists after type change" ); } public function testPageTypeChangePropagatesToLive() { $page = new SiteTree(); $page->MySharedNonVirtualField = 'original'; $page->write(); $page->publish('Stage', 'Live'); $virtual = new VirtualPageTest_VirtualPageSub(); $virtual->CopyContentFromID = $page->ID; $virtual->write(); $virtual->publish('Stage', 'Live'); $page->Title = 'original'; // 'Title' is a virtual field // Publication would causes the virtual field to copy through onBeforeWrite(), // but we want to test that it gets copied on class name change instead $page->write(); $nonVirtual = $virtual; $nonVirtual->ClassName = 'VirtualPageTest_ClassA'; $nonVirtual->MySharedNonVirtualField = 'changed on new type'; $nonVirtual->write(); // not publishing the page type change here $this->assertEquals('original', $nonVirtual->Title, 'Copies virtual fields from original draft into new instance on type change ' ); $nonVirtualLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree_Live"."ID" = ' . $nonVirtual->ID); $this->assertNotNull($nonVirtualLive); $this->assertEquals('VirtualPageTest_ClassA', $nonVirtualLive->ClassName); $this->assertEquals('changed on new type', $nonVirtualLive->MySharedNonVirtualField); $page->MySharedNonVirtualField = 'changed only on original'; $page->write(); $page->publish('Stage', 'Live'); $nonVirtualLive = Versioned::get_one_by_stage('SiteTree', 'Live', '"SiteTree_Live"."ID" = ' . $nonVirtual->ID, false); $this->assertEquals('changed on new type', $nonVirtualLive->MySharedNonVirtualField, 'No field copying from previous original after page type changed' ); } public function testVirtualPageFindsCorrectCasting() { $page = new VirtualPageTest_ClassA(); $page->CastingTest = "Some content"; $page->write(); $virtual = new VirtualPage(); $virtual->CopyContentFromID = $page->ID; $virtual->write(); $this->assertEquals('VirtualPageTest_TestDBField', $virtual->castingHelper('CastingTest')); $this->assertEquals('SOME CONTENT', $virtual->obj('CastingTest')->forTemplate()); } } class VirtualPageTest_ClassA extends Page implements TestOnly { private static $db = array( 'MyInitiallyCopiedField' => 'Text', 'MyVirtualField' => 'Text', 'MyNonVirtualField' => 'Text', 'CastingTest' => 'VirtualPageTest_TestDBField' ); private static $allowed_children = array('VirtualPageTest_ClassB'); } class VirtualPageTest_ClassB extends Page implements TestOnly { private static $allowed_children = array('VirtualPageTest_ClassC'); } class VirtualPageTest_ClassC extends Page implements TestOnly { private static $allowed_children = array(); } class VirtualPageTest_NotRoot extends Page implements TestOnly { private static $can_be_root = false; } class VirtualPageTest_TestDBField extends Varchar implements TestOnly { public function forTemplate() { return strtoupper($this->XML()); } } class VirtualPageTest_VirtualPageSub extends VirtualPage implements TestOnly { private static $db = array( 'MyProperty' => 'Varchar', ); } class VirtualPageTest_PageExtension extends DataExtension implements TestOnly { private static $db = array( // These fields are just on an extension to simulate shared properties between Page and VirtualPage. // Not possible through direct $db definitions due to VirtualPage inheriting from Page, and Page being defined elsewhere. 'MySharedVirtualField' => 'Text', 'MySharedNonVirtualField' => 'Text', ); }