silverstripe-framework/src/Forms/HTMLEditor/TinyMCECombinedGenerator.php

293 lines
8.4 KiB
PHP

<?php
namespace SilverStripe\Forms\HTMLEditor;
use SilverStripe\Assets\Storage\GeneratedAssetHandler;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ModuleResource;
/**
* Generates tinymce config using a combined file generated via a standard
* SilverStripe {@link GeneratedAssetHandler}
*/
class TinyMCECombinedGenerator implements TinyMCEScriptGenerator, Flushable
{
use Configurable;
use Injectable;
/**
* Named config
*
* @var string
*/
private static $filename_base = '_tinymce/tinymce-{name}-{hash}.js';
/**
* @var GeneratedAssetHandler
*/
protected $assetHandler = null;
/**
* Assign backend store for generated assets
*
* @param GeneratedAssetHandler $assetHandler
* @return $this
*/
public function setAssetHandler(GeneratedAssetHandler $assetHandler)
{
$this->assetHandler = $assetHandler;
return $this;
}
/**
* Get backend for assets
* @return GeneratedAssetHandler
*/
public function getAssetHandler()
{
return $this->assetHandler;
}
/**
* Generate a script URL for the given config
*
* @param TinyMCEConfig $config
* @return string
*/
public function getScriptURL(TinyMCEConfig $config)
{
// Build URL for this config based on named config and hash of settings
$url = $this->generateFilename($config);
// Pass content generator
return $this->getAssetHandler()->getContentURL($url, function () use ($config) {
return $this->generateContent($config);
});
}
/**
* Build raw config for tinymce
*
* @param TinyMCEConfig $config
* @return string
*/
public function generateContent(TinyMCEConfig $config)
{
$tinymceDir = $config->getTinyMCEResource();
// List of string / ModuleResource references to embed
$files = [];
// Add core languages
$language = $config->getOption('language');
if ($language) {
$files[] = $this->resolveRelativeResource($tinymceDir, "langs/{$language}");
}
// Add plugins, along with any plugin specific lang files
foreach ($config->getPlugins() as $plugin => $path) {
// Add external plugin
if ($path) {
// Convert URLS to relative paths
if (is_string($path)
&& (Director::is_absolute_url($path) || strpos($path, '/') === 0)
) {
$path = Director::makeRelative($path);
}
// Ensure file exists
if ($this->resourceExists($path)) {
$files[] = $path;
}
continue;
}
// Core tinymce plugin
$files[] = $this->resolveRelativeResource($tinymceDir, "plugins/{$plugin}/plugin");
if ($language) {
$files[] = $this->resolveRelativeResource($tinymceDir, "plugins/{$plugin}/langs/{$language}");
}
}
// Add themes
$theme = $config->getTheme();
if ($theme) {
$files[] = $this->resolveRelativeResource($tinymceDir, "themes/{$theme}/theme");
if ($language) {
$files[] = $this->resolveRelativeResource($tinymceDir, "themes/{$theme}/langs/{$language}");
}
}
// Process source files
$files = array_filter($files);
$libResource = $this->resolveRelativeResource($tinymceDir, 'tinymce');
$libContent = $this->getFileContents($libResource);
// Register vars for config
$baseDirJS = Convert::raw2js(Director::absoluteBaseURL());
$buffer = [];
$buffer[] = <<<SCRIPT
(function() {
var baseTag = window.document.getElementsByTagName('base');
var baseURL = baseTag.length ? baseTag[0].baseURI : '$baseDirJS';
SCRIPT;
$buffer[] = <<<SCRIPT
(function() {
// Avoid double-registration
if (window.tinymce) {
return;
}
var tinyMCEPreInit = {
base: baseURL,
suffix: '.min',
};
$libContent
})();
SCRIPT;
// Load all tinymce script files into buffer
foreach ($files as $path) {
$buffer[] = $this->getFileContents($path);
}
// Generate urls for all files
// Note all urls must be relative to base_dir
$fileURLS = array_map(function ($path) {
if ($path instanceof ModuleResource) {
return Director::makeRelative($path->getURL());
}
return $path;
}, $files);
// Join list of paths
$filesList = Convert::raw2js(implode(',', $fileURLS));
// Mark all themes, plugins and languages as done
$buffer[] = "window.tinymce.each('$filesList'.split(',')," .
"function(f){tinymce.ScriptLoader.markDone(baseURL+f);});";
$buffer[] = '})();';
return implode("\n", $buffer) . "\n";
}
/**
* Returns the contents of the script file if it exists and removes the UTF-8 BOM header if it exists.
*
* @param string|ModuleResource $file File to load.
* @return string File contents or empty string if it doesn't exist.
*/
protected function getFileContents($file)
{
if ($file instanceof ModuleResource) {
$path = $file->getPath();
} else {
$path = Director::baseFolder() . '/' . $file;
}
if (!file_exists($path)) {
return null;
}
$content = file_get_contents($path);
// Remove UTF-8 BOM
if (substr($content, 0, 3) === pack("CCC", 0xef, 0xbb, 0xbf)) {
$content = substr($content, 3);
}
return $content;
}
/**
* Check if this config is registered under a given key
*
* @param TinyMCEConfig $config
* @return string
*/
protected function checkName(TinyMCEConfig $config)
{
$configs = HTMLEditorConfig::get_available_configs_map();
foreach ($configs as $id => $name) {
if (HTMLEditorConfig::get($id) === $config) {
return $id;
}
}
return 'custom';
}
/**
* Get filename to use for this config
*
* @param TinyMCEConfig $config
* @return mixed
*/
public function generateFilename(TinyMCEConfig $config)
{
$hash = substr(sha1(json_encode($config->getAttributes())), 0, 10);
$name = $this->checkName($config);
$url = str_replace(
['{name}', '{hash}'],
[$name, $hash],
$this->config()->get('filename_base')
);
return $url;
}
/**
* This function is triggered early in the request if the "flush" query
* parameter has been set. Each class that implements Flushable implements
* this function which looks after it's own specific flushing functionality.
*
* @see FlushMiddleware
*/
public static function flush()
{
$dir = dirname(static::config()->get('filename_base'));
static::singleton()->getAssetHandler()->removeContent($dir);
}
/**
* Get relative resource for a given base and string
*
* @param ModuleResource|string $base
* @param string $resource
* @return ModuleResource|string
*/
protected function resolveRelativeResource($base, $resource)
{
// Return resource path based on relative resource path
foreach (['', '.min.js', '.js'] as $ext) {
// Map resource
if ($base instanceof ModuleResource) {
$next = $base->getRelativeResource($resource . $ext);
} else {
$next = rtrim($base, '/') . '/' . $resource . $ext;
}
// Ensure resource exists
if ($this->resourceExists($next)) {
return $next;
}
}
return null;
}
/**
* Check if the given resource exists
*
* @param string|ModuleResource $resource
* @return bool
*/
protected function resourceExists($resource)
{
if (!$resource) {
return false;
}
if ($resource instanceof ModuleResource) {
return $resource->exists();
}
$base = rtrim(Director::baseFolder(), '/');
return file_exists($base . '/' . $resource);
}
}