Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add image tag to shortcode task #227

Merged
merged 42 commits into from
May 6, 2019
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
23d3188
Add image tag to shortcode task
bergice Mar 27, 2019
b63a0f2
- Add `TagsToShortcode` test
bergice Apr 2, 2019
ae50a21
Use `FilenameParsing` helper classes to parse new file paths. This is…
bergice Apr 9, 2019
e07504d
Add options to `TagsToShortcodeTask`:
bergice Apr 9, 2019
279e5d6
Linting
bergice Apr 10, 2019
a963d42
Lintylint
bergice Apr 10, 2019
69cee26
Review `TagsToShortcodeHelper`
bergice Apr 10, 2019
e1234a5
BUG Fix typo to fetch a dynamic field rather than always assume it's …
Apr 17, 2019
75bb5d9
Refactor
bergice Apr 18, 2019
0baf678
PhpDoc
bergice Apr 18, 2019
93ed167
Move logger to $dependencies
bergice Apr 18, 2019
534e930
Works with existing shortcodes, not just HTML tags
bergice Apr 18, 2019
3604e27
Add more robust testing for `TagsToShortcodeHelper`
bergice Apr 29, 2019
87db65f
BUG Set correct COMPOSER_ROOT_VERSION value
Apr 29, 2019
efaaa86
BUG Add more complicated tests for the TagsToShortcodeHelper
Apr 30, 2019
b335c68
BUG Split the new content unit test.
May 1, 2019
5676e2a
Add a bunch of tests
May 1, 2019
e836fd6
Simplify testRewrite to test a simpler string and test the presence o…
May 1, 2019
904fa73
Add a versioned test
May 2, 2019
0ff0df7
Move test to matching Dev\Tasks namespace
May 2, 2019
a267a8b
Add tests for non-sitetree object
May 2, 2019
0c3a7de
Fix tests
bergice May 2, 2019
6745acf
Merge branch 'pulls/1/img-to-shortcode' of https://github.com/open-sa…
bergice May 2, 2019
d8159dc
Use full hashes in the tests
May 2, 2019
4336e1b
Merge branch 'pulls/1/img-to-shortcode' of https://github.com/open-sa…
bergice May 2, 2019
9c1faf1
Strip variant from url
bergice May 2, 2019
5fa823a
Fix more tests
bergice May 2, 2019
100a298
Fix invalic file variant
bergice May 2, 2019
a23346d
Add logic to skip fields when short codes have been disabled on them
May 2, 2019
dba0f05
Merge branch 'pulls/1/img-to-shortcode' of github.com:open-sausages/s…
May 2, 2019
7ec9937
Fix broken test
May 2, 2019
574e4eb
Tweak installed test dependencies
May 2, 2019
f198e08
Require CMS
May 2, 2019
8be3ca4
Tweak tests
May 2, 2019
41816bf
Tweak the logger message to not assume we are always updating tables
May 3, 2019
de7bf81
Bring back recipe-cms in test
May 3, 2019
1b55166
Strip empty images from image tags
May 3, 2019
b43833f
Move getFieldMap to TagsToShortcodeHelper
May 5, 2019
d6cb934
Remove `string` definition
May 6, 2019
a5897db
Minor tweak to fix unit tests.
May 6, 2019
5385760
Remove broken test.
May 6, 2019
96d5f26
Disable transaction on TagsToShortcodeHelperTest
May 6, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sudo: false

env:
global:
- COMPOSER_ROOT_VERSION=1.5.x-dev
- COMPOSER_ROOT_VERSION=1.4.x-dev
- CORE_RELEASE=master

matrix:
Expand Down Expand Up @@ -37,7 +37,7 @@ before_script:
# Install composer dependencies
- composer validate
- composer install --prefer-dist
- composer require --prefer-dist --no-update silverstripe/recipe-core:4.x-dev silverstripe/versioned:1.x-dev
- composer require --prefer-dist --no-update silverstripe/recipe-core:4.4.x-dev silverstripe/recipe-cms:4.4.x-dev
- if [[ $DB == PGSQL ]]; then composer require --no-update silverstripe/postgresql:2.x-dev --prefer-dist; fi
- composer update --prefer-dist
- if [[ $PHPCS_TEST ]]; then composer global require squizlabs/php_codesniffer:^3 --prefer-dist --no-interaction --no-progress --no-suggest -o; fi
Expand Down
328 changes: 328 additions & 0 deletions src/Dev/Tasks/TagsToShortcodeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
<?php

namespace SilverStripe\Assets\Dev\Tasks;

use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use SilverStripe\Assets\FilenameParsing\FileIDHelperResolutionStrategy;
use SilverStripe\Assets\FilenameParsing\HashFileIDHelper;
use SilverStripe\Assets\FilenameParsing\LegacyFileIDHelper;
use SilverStripe\Assets\FilenameParsing\NaturalFileIDHelper;
use SilverStripe\Assets\FilenameParsing\ParsedFileID;
use SilverStripe\Assets\Flysystem\FlysystemAssetStore;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Environment;
use SilverStripe\Assets\File;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\Connect\DatabaseException;
use SilverStripe\ORM\Connect\Query;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectSchema;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\Queries\SQLUpdate;
use SilverStripe\Versioned\Versioned;

/**
* SS4 and its File Migration Task changes the way in which files are stored in the assets folder, with files placed
* in subfolders named with partial hashmap values of the file version. This helper class goes through the HTML content
* fields looking for instances of image links, and corrects the link path to what it should be, with an image shortcode.
*/
class TagsToShortcodeHelper
{
use Injectable;
use Configurable;

const VALID_TAGS = [
'img' => 'image',
'a' => 'file_link'
];

const VALID_ATTRIBUTES = [
'src',
'href'
];

/** @var string */
private $validTagsPattern;

/** @var string */
private $validAttributesPattern;

/** @var FlysystemAssetStore */
private $flysystemAssetStore;

/** @var string */
private $baseClass;

/** @var bool */
private $includeBaseClass;

private static $dependencies = [
'logger' => '%$' . LoggerInterface::class,
];

/** @var LoggerInterface|null */
private $logger;

/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}

/**
* TagsToShortcodeHelper constructor.
* @param string $baseClass The base class that will be used to look up HTMLText fields
* @param bool $includeBaseClass Whether to include the base class' HTMLText fields or not
*/
public function __construct($baseClass = null, $includeBaseClass = false)
{
$flysystemAssetStore = singleton(AssetStore::class);
bergice marked this conversation as resolved.
Show resolved Hide resolved
if (!($flysystemAssetStore instanceof FlysystemAssetStore)) {
throw new InvalidArgumentException("FlysystemAssetStore missing");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making the error message a little clearer for developers.

Suggested change
throw new InvalidArgumentException("FlysystemAssetStore missing");
throw new LogicException("TagsToShortcodeHelper requires your default asset store to extends FlysystemAssetStore.");

}
$this->flysystemAssetStore = $flysystemAssetStore;

$this->baseClass = $baseClass ?: DataObject::class;
$this->includeBaseClass = $includeBaseClass;

$this->validTagsPattern = implode('|', array_keys(static::VALID_TAGS));
$this->validAttributesPattern = implode('|', static::VALID_ATTRIBUTES);
}

/**
* @throws \ReflectionException
*/
public function run()
{
Environment::increaseTimeLimitTo();

$classes = DataObjectSchema::getFieldMap($this->baseClass, $this->includeBaseClass, [
'HTMLText',
'HTMLVarchar'
]);
foreach ($classes as $class => $tables) {
/** @var DataObject $singleton */
$singleton = singleton($class);
$hasVersions = $singleton->hasExtension(Versioned::class) && $singleton->hasStages();

foreach ($tables as $table => $fields) {
foreach ($fields as $field) {

/** @var DBField $dbField */
$dbField = DataObject::singleton($class)->dbObject($field);
if ($dbField &&
$dbField->hasMethod('getProcessShortcodes') &&
!$dbField->getProcessShortcodes()) {
continue;
}

try {
// Update table
$this->updateTable($table, $field);

// Update live
if ($hasVersions) {
$this->updateTable($table.'_Live', $field);
}
} catch (DatabaseException $exception) {
bergice marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}

private function updateTable(string $table, string $field)
{
$sqlSelect = SQLSelect::create(
['ID', $field],
$table
);
$whereAnys = [];
foreach (array_keys(static::VALID_TAGS) as $tag) {
$whereAnys[]= "\"$table\".\"$field\" LIKE '%<$tag%'";
$whereAnys[]= "\"$table\".\"$field\" LIKE '%[$tag%'";
}
$sqlSelect->addWhereAny($whereAnys);
$records = $sqlSelect->execute();
$this->rewriteFieldForRecords($records, $table, $field);
}

/**
* Takes a set of query results and updates image urls within a page's content.
* @param Query $records
* @param string $updateTable
* @param string $field
*/
private function rewriteFieldForRecords(Query $records, $updateTable, $field)
{
foreach ($records as $row) {
$content = $row[$field];
$newContent = $this->getNewContent($content);
if ($content == $newContent) {
continue;
}

$updateSQL = SQLUpdate::create($updateTable)->addWhere(['"ID"' => $row['ID']]);
$updateSQL->addAssignments(["\"$field\"" => $newContent]);
$updateSQL->execute();
if ($this->logger) {
$this->logger->info("Updated record ID {$row['ID']} on table $updateTable");
}
}
}

/**
* @param string $content
* @return string
*/
public function getNewContent($content)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe give this method a slighlty different name, because getNewContent could be confuse with a getter.

e.g.: normaliseContent, convertFileShortCode, migrateContent, or whatever else catches your fancy.

{
$tags = $this->getTagsInContent($content);
foreach ($tags as $tag) {
if ($newTag = $this->getNewTag($tag)) {
$content = str_replace($tag, $newTag, $content);
}
}

return $content;
}

/**
* Get all tags within some page content and return them as an array.
* @param string $content The page content
* @return array An array of tags found
*/
private function getTagsInContent($content)
{
$resultTags = [];

preg_match_all('/<('.$this->validTagsPattern.').*?('.$this->validAttributesPattern.')\s*=.*?>/i', $content, $matches, PREG_SET_ORDER);
if ($matches) {
foreach ($matches as $match) {
$resultTags []= $match[0];
}
}

return $resultTags;
}

/**
* @param string $tag
* @return array
*/
private function getTagTuple($tag)
{
$pattern = sprintf(
'/.*(?:<|\[)(?<tagType>%s).*(?<attribute>%s)=(?:"|\')(?<src>[^"]*)(?:"|\')/i',
$this->validTagsPattern,
$this->validAttributesPattern
);
preg_match($pattern, $tag, $matches);
return $matches;
}

/**
* @param string $src
* @return null|ParsedFileID
* @throws \League\Flysystem\Exception
*/
private function getParsedFileIDFromSrc($src)
{
$fileIDHelperResolutionStrategy = new FileIDHelperResolutionStrategy();
$fileIDHelperResolutionStrategy->setResolutionFileIDHelpers([
$hashFileIdHelper = new HashFileIDHelper(),
new LegacyFileIDHelper(),
$defaultFileIDHelper = new NaturalFileIDHelper(),
]);
$fileIDHelperResolutionStrategy->setDefaultFileIDHelper($defaultFileIDHelper);

// set fileID to the filepath relative to assets dir
$pattern = sprintf('#^/?(%s/?)?#', ASSETS_DIR);
$fileID = preg_replace($pattern, '', $src);

// Try resolving with public filesystem first
$filesystem = $this->flysystemAssetStore->getPublicFilesystem();
$parsedFileId = $fileIDHelperResolutionStrategy->softResolveFileID($fileID, $filesystem);
if (!$parsedFileId) {
// Try resolving with protected filesystem
$filesystem = $this->flysystemAssetStore->getProtectedFilesystem();
$parsedFileId = $fileIDHelperResolutionStrategy->softResolveFileID($fileID, $filesystem);
}

if (!$parsedFileId) {
return null;
}

$parsedFileId = $parsedFileId->setVariant("");
$newFileId = $hashFileIdHelper->buildFileID($parsedFileId->getFilename(), $parsedFileId->getHash());
return $parsedFileId
->setVariant("")
->setFileID($newFileId);
}

/**
* @param string $tag
* @return string|null Returns the new tag or null if the tag does not need to be rewritten
*/
private function getNewTag($tag)
{
$tuple = $this->getTagTuple($tag);
if (!isset($tuple['tagType']) || !isset($tuple['src'])) {
return null;
}
$tagType = strtolower($tuple['tagType']);
$src = $tuple['src'] ?: $tuple['href'];

// Search for a File object containing this filename
$parsedFileID = $this->getParsedFileIDFromSrc($src);
if (!$parsedFileID) {
return null;
}

/** @var File $file */
$file = File::get()->filter('FileFilename', $parsedFileID->getFilename())->first();
if (!$file) {
$file = Versioned::withVersionedMode(function () use ($parsedFileID) {
Versioned::set_stage(Versioned::LIVE);
return File::get()->filter('FileFilename', $parsedFileID->getFilename())->first();
});
}

if ($parsedFileID && $file) {
if ($tagType == 'img') {
$find = [
'/(<|\[)img/i',
'/src\s*=\s*(?:"|\').*?(?:"|\')/i',
'/href\s*=\s*(?:"|\').*?(?:"|\')/i',
'/id\s*=\s*(?:"|\').*?(?:"|\')/i',
'/\s*(\/?>|\])/',
'/\s?\S+=("")|(\'\')/'
];
$replace = [
'[image',
maxime-rainville marked this conversation as resolved.
Show resolved Hide resolved
"src=\"/".ASSETS_DIR."/{$parsedFileID->getFileID()}\"",
"href=\"/".ASSETS_DIR."/{$parsedFileID->getFileID()}\"",
"",
" id=\"{$file->ID}\"]",
"",
];
$shortcode = preg_replace($find, $replace, $tag);
} elseif ($tagType == 'a') {
$attribute = 'href';
$find= "/$attribute\s*=\s*(?:\"|').*?(?:\"|')/i";
$replace = "$attribute=\"[file_link,id={$file->ID}]\"";
$shortcode = preg_replace($find, $replace, $tag);
} else {
return null;
}
return $shortcode;
}
return null;
}
}
40 changes: 40 additions & 0 deletions src/Dev/Tasks/TagsToShortcodeTask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace SilverStripe\Assets\Dev\Tasks;

use SilverStripe\Dev\BuildTask;

/**
* SS4 and its File Migration Task changes the way in which files are stored in the assets folder, with files placed
* in subfolders named with partial hashmap values of the file version. This build task goes through the HTML content
* fields looking for instances of image links, and corrects the link path to what it should be, with an image shortcode.
*/
class TagsToShortcodeTask extends BuildTask
{
private static $segment = 'TagsToShortcode';

protected $title = 'Rewrite tags to shortcodes';

protected $description = "
bergice marked this conversation as resolved.
Show resolved Hide resolved
Rewrites tags to shortcodes in any HTMLText field

Parameters:
- baseClass: The base class that will be used to look up HTMLText fields. Defaults to SilverStripe\ORM\DataObject
- includeBaseClass: Whether to include the base class' HTMLText fields or not
";

/**
* @param \SilverStripe\Control\HTTPRequest $request
* @throws \ReflectionException
*/
public function run($request)
{
$tagsToShortcodeHelper = new TagsToShortcodeHelper(
$request->getVar('baseClass'),
isset($request->getVars()['includeBaseClass'])
);
$tagsToShortcodeHelper->run();

echo 'DONE';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the logger instead.

}
}
Loading