API Update link tracking for image shortcodes

This commit is contained in:
Damian Mooyman 2016-03-10 11:57:39 +13:00
parent 3017028755
commit 067d44ac6c
7 changed files with 78 additions and 137 deletions

View File

@ -1612,46 +1612,6 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
return ($liveRecord) ? $liveRecord->URLSegment : null; return ($liveRecord) ? $liveRecord->URLSegment : null;
} }
/**
* Rewrites any linked images on this page without creating a new version record.
* Non-image files should be linked via shortcodes
* Triggers the onRenameLinkedAsset action on extensions.
*
* @todo Implement image shortcodes and remove this feature
*/
public function rewriteFileLinks() {
// Skip live stage
if(\Versioned::get_stage() === \Versioned::LIVE) {
return;
}
// Update the content without actually creating a new version
foreach($this->db() as $fieldName => $fieldType) {
// Skip if non HTML or if empty
if ($fieldType !== 'HTMLText') {
continue;
}
$fieldValue = $this->{$fieldName};
if(empty($fieldValue)) {
continue;
}
// Regenerate content
$content = Image::regenerate_html_links($fieldValue);
if($content === $fieldValue) {
continue;
}
// Write content directly without updating linked assets
$table = ClassInfo::table_for_object_field($this, $fieldName);
$query = sprintf('UPDATE "%s" SET "%s" = ? WHERE "ID" = ?', $table, $fieldName);
DB::prepared_query($query, array($content, $this->ID));
// Update linked assets
$this->invokeWithExtensions('onRenameLinkedAsset');
}
}
/** /**
* Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc. * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
* *

View File

@ -124,43 +124,4 @@ class SiteTreeFileExtension extends DataExtension {
} }
} }
} }
public function onAfterWrite() {
// Update any database references in the current stage
$this->updateLinks();
}
public function onAfterVersionedPublish() {
// Ensure that ->updateLinks is invoked on the draft record
// after ->doPublish() is invoked.
$this->updateLinks();
}
/**
* Rewrite links to the $old file to now point to the $new file.
*
* @uses SiteTree->rewriteFileID()
*/
public function updateLinks() {
// Skip live stage
if(\Versioned::get_stage() === \Versioned::LIVE) {
return;
}
if(class_exists('Subsite')) {
Subsite::disable_subsite_filter(true);
}
$pages = $this->owner->BackLinkTracking();
if($pages) {
foreach($pages as $page) {
$page->rewriteFileLinks();
}
}
if(class_exists('Subsite')) {
Subsite::disable_subsite_filter(false);
}
}
} }

View File

@ -93,8 +93,6 @@ class SiteTreeLinkTracking extends DataExtension {
/** /**
* Scrape the content of a field to detect anly links to local SiteTree pages or files * Scrape the content of a field to detect anly links to local SiteTree pages or files
* *
* @todo - Replace image tracking with shortcodes
*
* @param string $fieldName The name of the field on {@link @owner} to scrape * @param string $fieldName The name of the field on {@link @owner} to scrape
*/ */
public function trackLinksInField($fieldName) { public function trackLinksInField($fieldName) {
@ -108,6 +106,11 @@ class SiteTreeLinkTracking extends DataExtension {
// Highlight broken links in the content. // Highlight broken links in the content.
foreach ($links as $link) { foreach ($links as $link) {
// Skip links without domelements
if(!isset($link['DOMReference'])) {
continue;
}
$classStr = trim($link['DOMReference']->getAttribute('class')); $classStr = trim($link['DOMReference']->getAttribute('class'));
if (!$classStr) { if (!$classStr) {
$classes = array(); $classes = array();
@ -142,6 +145,7 @@ class SiteTreeLinkTracking extends DataExtension {
break; break;
case 'file': case 'file':
case 'image':
if ($link['Broken']) { if ($link['Broken']) {
$record->HasBrokenFile = true; $record->HasBrokenFile = true;
} else { } else {
@ -157,22 +161,6 @@ class SiteTreeLinkTracking extends DataExtension {
} }
} }
// Add file tracking for image references
if($images = $htmlValue->getElementsByTagName('img')) foreach($images as $img) {
// {@see HtmlEditorField} for data-fileid source
$fileID = $img->getAttribute('data-fileid');
if(!$fileID) {
continue;
}
// Assuming a local file is linked, check if it's valid
if($image = File::get()->byID($fileID)) {
$linkedFiles[] = $image->ID;
} else {
$record->HasBrokenFile = true;
}
}
// Update the "LinkTracking" many_many // Update the "LinkTracking" many_many
if($record->ID && $record->manyManyComponent('LinkTracking') && ($tracker = $record->LinkTracking())) { if($record->ID && $record->manyManyComponent('LinkTracking') && ($tracker = $record->LinkTracking())) {
$tracker->removeByFilter(array( $tracker->removeByFilter(array(
@ -274,15 +262,15 @@ class SiteTreeLinkTracking_Parser {
// Link to a page on this site. // Link to a page on this site.
$matches = array(); $matches = array();
if(preg_match('/\[sitetree_link(?:\s*|%20|,)?id=([0-9]+)\](#(.*))?/i', $href, $matches)) { if(preg_match('/\[sitetree_link(?:\s*|%20|,)?id=(?<id>[0-9]+)\](#(?<anchor>.*))?/i', $href, $matches)) {
$page = DataObject::get_by_id('SiteTree', $matches[1]); $page = DataObject::get_by_id('SiteTree', $matches['id']);
$broken = false; $broken = false;
if (!$page) { if (!$page) {
// Page doesn't exist. // Page doesn't exist.
$broken = true; $broken = true;
} else if (!empty($matches[3])) { } else if (!empty($matches['anchor'])) {
$anchor = preg_quote($matches[3], '/'); $anchor = preg_quote($matches['anchor'], '/');
if (!preg_match("/(name|id)=\"{$anchor}\"/", $page->Content)) { if (!preg_match("/(name|id)=\"{$anchor}\"/", $page->Content)) {
// Broken anchor on the target page. // Broken anchor on the target page.
@ -292,8 +280,8 @@ class SiteTreeLinkTracking_Parser {
$results[] = array( $results[] = array(
'Type' => 'sitetree', 'Type' => 'sitetree',
'Target' => $matches[1], 'Target' => $matches['id'],
'Anchor' => empty($matches[3]) ? null : $matches[3], 'Anchor' => empty($matches['anchor']) ? null : $matches['anchor'],
'DOMReference' => $link, 'DOMReference' => $link,
'Broken' => $broken 'Broken' => $broken
); );
@ -303,13 +291,13 @@ class SiteTreeLinkTracking_Parser {
// Link to a file on this site. // Link to a file on this site.
$matches = array(); $matches = array();
if(preg_match('/\[file_link(?:\s*|%20|,)?id=([0-9]+)\]/i', $href, $matches)) { if(preg_match('/\[file_link(?:\s*|%20|,)?id=(?<id>[0-9]+)/i', $href, $matches)) {
$results[] = array( $results[] = array(
'Type' => 'file', 'Type' => 'file',
'Target' => $matches[1], 'Target' => $matches['id'],
'Anchor' => null, 'Anchor' => null,
'DOMReference' => $link, 'DOMReference' => $link,
'Broken' => !DataObject::get_by_id('File', $matches[1]) 'Broken' => !DataObject::get_by_id('File', $matches['id'])
); );
continue; continue;
@ -331,6 +319,19 @@ class SiteTreeLinkTracking_Parser {
} }
// Find all [image ] shortcodes (will be inline, not inside attributes)
$content = $htmlValue->getContent();
if(preg_match_all('/\[image([^\]]+)\bid=(["])?(?<id>\d+)\D/i', $content, $matches)) {
foreach($matches['id'] as $id) {
$results[] = array(
'Type' => 'image',
'Target' => (int)$id,
'Anchor' => null,
'DOMReference' => null,
'Broken' => !DataObject::get_by_id('Image', (int)$id)
);
}
}
return $results; return $results;
} }

View File

@ -28,7 +28,7 @@ class FileLinkTrackingTest extends SapphireTest {
$imageID = $this->idFromFixture('Image', 'file1'); $imageID = $this->idFromFixture('Image', 'file1');
$page = $this->objFromFixture('Page', 'page1'); $page = $this->objFromFixture('Page', 'page1');
$page->Content = sprintf( $page->Content = sprintf(
'<p><img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg" data-fileid="%d" /></p>', '<p>[image src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg" id="%d"]</p>',
$imageID $imageID
); );
$page->write(); $page->write();
@ -39,20 +39,28 @@ class FileLinkTrackingTest extends SapphireTest {
parent::tearDown(); parent::tearDown();
} }
/**
* Test uses global state through Versioned::set_reading_mode() since
* the shortcode parser doesn't pass along the underlying DataObject
* context, hence we can't call getSourceQueryParams().
*/
public function testFileRenameUpdatesDraftAndPublishedPages() { public function testFileRenameUpdatesDraftAndPublishedPages() {
$page = $this->objFromFixture('Page', 'page1'); $page = $this->objFromFixture('Page', 'page1');
$page->doPublish(); $page->doPublish();
// Live and stage pages both have link to public file // Live and stage pages both have link to public file
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
Versioned::set_stage(Versioned::LIVE);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
Versioned::set_stage(Versioned::DRAFT);
$file = $this->objFromFixture('Image', 'file1'); $file = $this->objFromFixture('Image', 'file1');
$file->Name = 'renamed-test-file.jpg'; $file->Name = 'renamed-test-file.jpg';
$file->write(); $file->write();
@ -60,38 +68,43 @@ class FileLinkTrackingTest extends SapphireTest {
// Staged record now points to secure URL of renamed file, live record remains unchanged // Staged record now points to secure URL of renamed file, live record remains unchanged
// Note that the "secure" url doesn't have the "FileLinkTrackingTest" component because // Note that the "secure" url doesn't have the "FileLinkTrackingTest" component because
// the mocked test location disappears for secure files. // the mocked test location disappears for secure files.
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains( $this->assertContains(
'<img src="/assets/55b443b601/renamed-test-file.jpg"', '<img src="/assets/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
Versioned::set_stage(Versioned::LIVE);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
// Publishing the file should result in a direct public link (indicated by "FileLinkTrackingTest") // Publishing the file should result in a direct public link (indicated by "FileLinkTrackingTest")
// Although the old live page will still point to the old record. // Although the old live page will still point to the old record.
// @todo - Ensure shortcodes are used with all images to prevent live records having broken links // @todo - Ensure shortcodes are used with all images to prevent live records having broken links
$file->doPublish(); $file->doPublish();
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
Versioned::set_stage(Versioned::LIVE);
$this->assertContains( $this->assertContains(
// Note: Broken link until shortcode-enabled '<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"', Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()
); );
// Publishing the page after publishing the asset will resolve any link issues // Publishing the page after publishing the asset should retain linking
$page->doPublish(); $page->doPublish();
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
Versioned::set_stage(Versioned::LIVE);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
} }
@ -112,20 +125,20 @@ class FileLinkTrackingTest extends SapphireTest {
$file->write(); $file->write();
// Verify that the draft virtual pages have the correct content // Verify that the draft virtual pages have the correct content
$svp = Versioned::get_by_stage('VirtualPage', Versioned::DRAFT)->byID($svp->ID); Versioned::set_stage(Versioned::DRAFT);
$this->assertContains( $this->assertContains(
'<img src="/assets/55b443b601/renamed-test-file.jpg"', '<img src="/assets/55b443b601/renamed-test-file.jpg"',
$svp->Content Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
// Publishing both file and page will update the live record // Publishing both file and page will update the live record
$file->doPublish(); $file->doPublish();
$page->doPublish(); $page->doPublish();
$svp = Versioned::get_by_stage('VirtualPage', Versioned::LIVE)->byID($svp->ID); Versioned::set_stage(Versioned::LIVE);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file.jpg"',
$svp->Content Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
} }
@ -151,11 +164,15 @@ class FileLinkTrackingTest extends SapphireTest {
public function testTwoFileRenamesInARowWork() { public function testTwoFileRenamesInARowWork() {
$page = $this->objFromFixture('Page', 'page1'); $page = $this->objFromFixture('Page', 'page1');
$this->assertTrue($page->doPublish()); $this->assertTrue($page->doPublish());
Versioned::set_stage(Versioned::LIVE);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value()); Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
);
// Rename the file twice // Rename the file twice
Versioned::set_stage(Versioned::DRAFT);
$file = $this->objFromFixture('Image', 'file1'); $file = $this->objFromFixture('Image', 'file1');
$file->Name = 'renamed-test-file.jpg'; $file->Name = 'renamed-test-file.jpg';
$file->write(); $file->write();
@ -168,16 +185,18 @@ class FileLinkTrackingTest extends SapphireTest {
$file->doPublish(); $file->doPublish();
// Confirm that the correct image is shown in both the draft and live site // Confirm that the correct image is shown in both the draft and live site
Versioned::set_stage(Versioned::DRAFT);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
// Publishing this record also updates live record // Publishing this record also updates live record
$page->doPublish(); $page->doPublish();
Versioned::set_stage(Versioned::LIVE);
$this->assertContains( $this->assertContains(
'<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"', '<img src="/assets/FileLinkTrackingTest/55b443b601/renamed-test-file-second-time.jpg"',
DB::prepared_query("SELECT \"Content\" FROM \"SiteTree_Live\" WHERE \"ID\" = ?", array($page->ID))->value() Page::get()->byID($page->ID)->dbObject('Content')->forTemplate()
); );
} }
} }

View File

@ -9,4 +9,4 @@ Page:
page1: page1:
Title: page1 Title: page1
URLSegment: page1 URLSegment: page1
Content: '<p><img src="/assets/FileLinkTrackingTest/55b443b601/testscript-test-file.jpg" /></p>' # Content is set via test setup

View File

@ -124,7 +124,7 @@ class SiteTreeHtmlEditorFieldTest extends FunctionalTest {
$editor = new HtmlEditorField('Content'); $editor = new HtmlEditorField('Content');
$file = $this->objFromFixture('Image', 'example_image'); $file = $this->objFromFixture('Image', 'example_image');
$editor->setValue(sprintf('<img src="%s" data-fileid="%d" />', $file->getURL(), $file->ID)); $editor->setValue(sprintf('[image src="%s" id="%d"]', $file->getURL(), $file->ID));
$editor->saveInto($sitetree); $editor->saveInto($sitetree);
$sitetree->write(); $sitetree->write();
$this->assertEquals( $this->assertEquals(

View File

@ -62,8 +62,8 @@ class SiteTreeLinkTrackingTest extends SapphireTest {
} }
public function testHasBrokenFile() { public function testHasBrokenFile() {
$this->assertTrue($this->pageIsBrokenFile('<img src="someurl.jpg" data-fileid="99999999" />')); $this->assertTrue($this->pageIsBrokenFile('[image src="someurl.jpg" id="99999999"]'));
$this->assertFalse($this->pageIsBrokenFile('<img src="someurl.jpg" />')); $this->assertFalse($this->pageIsBrokenFile('[image src="someurl.jpg"]'));
} }
protected function pageIsBrokenFile($content) { protected function pageIsBrokenFile($content) {