'#url',
'cite' => '#url',
'data' => '#url',
'formaction' => '#url',
'href' => '#url',
'icon' => '#url',
'itemtype' => '#url',
'longdesc' => '#url',
'manifest' => '#url',
'ping' => '#url',
'poster' => '#url',
'src' => '#url'
];
/**
* @var array Hash of allowed HTML elements. Element names are lowercased and used as keys for
* this array
*/
protected $elements = [];
/**
* @var string Namespace prefix of the tags produced by this plugin's parser
*/
protected $prefix = 'html';
/**
* {@inheritdoc}
*/
protected $quickMatch = '<';
/**
* @var array Blacklist of elements that are considered unsafe
*/
protected $unsafeElements = [
'base',
'embed',
'frame',
'iframe',
'meta',
'object',
'script'
];
/**
* @var array Blacklist of attributes that are considered unsafe, in addition of any attribute
* whose name starts with "on" such as "onmouseover"
*/
protected $unsafeAttributes = [
'style',
'target'
];
/**
* Alias the HTML attribute of given HTML element to a given attribute name
*
* NOTE: will *not* create the target attribute
*
* @param string $elName Name of the HTML element
* @param string $attrName Name of the HTML attribute
* @param string $alias Alias
* @return void
*/
public function aliasAttribute($elName, $attrName, $alias)
{
$elName = $this->normalizeElementName($elName);
$attrName = $this->normalizeAttributeName($attrName);
$this->aliases[$elName][$attrName] = AttributeName::normalize($alias);
}
/**
* Alias an HTML element to a given tag name
*
* NOTE: will *not* create the target tag
*
* @param string $elName Name of the HTML element
* @param string $tagName Name of the tag
* @return void
*/
public function aliasElement($elName, $tagName)
{
$elName = $this->normalizeElementName($elName);
$this->aliases[$elName][''] = TagName::normalize($tagName);
}
/**
* Allow an HTML element to be used
*
* @param string $elName Name of the element
* @return Tag Tag that represents this element
*/
public function allowElement($elName)
{
return $this->allowElementWithSafety($elName, false);
}
/**
* Allow an unsafe HTML element to be used
*
* @param string $elName Name of the element
* @return Tag Tag that represents this element
*/
public function allowUnsafeElement($elName)
{
return $this->allowElementWithSafety($elName, true);
}
/**
* Allow a (potentially unsafe) HTML element to be used
*
* @param string $elName Name of the element
* @param bool $allowUnsafe Whether to allow unsafe elements
* @return Tag Tag that represents this element
*/
protected function allowElementWithSafety($elName, $allowUnsafe)
{
$elName = $this->normalizeElementName($elName);
$tagName = $this->prefix . ':' . $elName;
if (!$allowUnsafe && in_array($elName, $this->unsafeElements))
{
throw new RuntimeException("'" . $elName . "' elements are unsafe and are disabled by default. Please use " . __CLASS__ . '::allowUnsafeElement() to bypass this security measure');
}
// Retrieve or create the tag
$tag = ($this->configurator->tags->exists($tagName))
? $this->configurator->tags->get($tagName)
: $this->configurator->tags->add($tagName);
// Rebuild this tag's template
$this->rebuildTemplate($tag, $elName, $allowUnsafe);
// Record the element name
$this->elements[$elName] = 1;
return $tag;
}
/**
* Allow an attribute to be used in an HTML element
*
* @param string $elName Name of the element
* @param string $attrName Name of the attribute
* @return \s9e\Configurator\Items\Attribute
*/
public function allowAttribute($elName, $attrName)
{
return $this->allowAttributeWithSafety($elName, $attrName, false);
}
/**
* Allow an unsafe attribute to be used in an HTML element
*
* @param string $elName Name of the element
* @param string $attrName Name of the attribute
* @return \s9e\Configurator\Items\Attribute
*/
public function allowUnsafeAttribute($elName, $attrName)
{
return $this->allowAttributeWithSafety($elName, $attrName, true);
}
/**
* Allow a (potentially unsafe) attribute to be used in an HTML element
*
* @param string $elName Name of the element
* @param string $attrName Name of the attribute
* @param bool $allowUnsafe
* @return \s9e\Configurator\Items\Attribute
*/
protected function allowAttributeWithSafety($elName, $attrName, $allowUnsafe)
{
$elName = $this->normalizeElementName($elName);
$attrName = $this->normalizeAttributeName($attrName);
$tagName = $this->prefix . ':' . $elName;
if (!isset($this->elements[$elName]))
{
throw new RuntimeException("Element '" . $elName . "' has not been allowed");
}
if (!$allowUnsafe)
{
if (substr($attrName, 0, 2) === 'on'
|| in_array($attrName, $this->unsafeAttributes))
{
throw new RuntimeException("'" . $attrName . "' attributes are unsafe and are disabled by default. Please use " . __CLASS__ . '::allowUnsafeAttribute() to bypass this security measure');
}
}
$tag = $this->configurator->tags->get($tagName);
if (!isset($tag->attributes[$attrName]))
{
$attribute = $tag->attributes->add($attrName);
$attribute->required = false;
if (isset($this->attributeFilters[$attrName]))
{
$filterName = $this->attributeFilters[$attrName];
$filter = $this->configurator->attributeFilters->get($filterName);
$attribute->filterChain->append($filter);
}
}
// Rebuild this tag's template
$this->rebuildTemplate($tag, $elName, $allowUnsafe);
return $tag->attributes[$attrName];
}
/**
* Validate and normalize an element name
*
* Accepts any name that would be valid, regardless of whether this element exists in HTML5.
* Might be slightly off as the HTML5 specs don't seem to require it to start with a letter but
* our implementation does.
*
* @link http://dev.w3.org/html5/spec/syntax.html#syntax-tag-name
*
* @param string $elName Original element name
* @return string Normalized element name, in lowercase
*/
protected function normalizeElementName($elName)
{
if (!preg_match('#^[a-z][a-z0-9]*$#Di', $elName))
{
throw new InvalidArgumentException("Invalid element name '" . $elName . "'");
}
return strtolower($elName);
}
/**
* Validate and normalize an attribute name
*
* More restrictive than the specs but allows all HTML5 attributes and more.
*
* @param string $attrName Original attribute name
* @return string Normalized attribute name, in lowercase
*/
protected function normalizeAttributeName($attrName)
{
if (!preg_match('#^[a-z][-\\w]*$#Di', $attrName))
{
throw new InvalidArgumentException("Invalid attribute name '" . $attrName . "'");
}
return strtolower($attrName);
}
/**
* Rebuild a tag's template
*
* @param Tag $tag Source tag
* @param string $elName Name of the HTML element created by the template
* @param bool $allowUnsafe Whether to allow unsafe markup
* @return void
*/
protected function rebuildTemplate(Tag $tag, $elName, $allowUnsafe)
{
$template = '<' . $elName . '>';
foreach ($tag->attributes as $attrName => $attribute)
{
$template .= '';
}
$template .= '' . $elName . '>';
if ($allowUnsafe)
{
$template = new UnsafeTemplate($template);
}
$tag->setTemplate($template);
}
/**
* Generate this plugin's config
*
* @return array|null
*/
public function asConfig()
{
if (empty($this->elements) && empty($this->aliases))
{
return;
}
/**
* Regexp used to match an attributes definition (name + value if applicable)
*
* @link http://dev.w3.org/html5/spec/syntax.html#attributes-0
*/
$attrRegexp = '[a-z][-a-z0-9]*(?>\\s*=\\s*(?>"[^"]*"|\'[^\']*\'|[^\\s"\'=<>`]+))?';
$tagRegexp = RegexpBuilder::fromList(array_merge(
array_keys($this->aliases),
array_keys($this->elements)
));
$endTagRegexp = '/(' . $tagRegexp . ')';
$startTagRegexp = '(' . $tagRegexp . ')((?>\\s+' . $attrRegexp . ')*+)\\s*/?';
$regexp = '#<(?>' . $endTagRegexp . '|' . $startTagRegexp . ')\\s*>#i';
$config = [
'quickMatch' => $this->quickMatch,
'prefix' => $this->prefix,
'regexp' => $regexp
];
if (!empty($this->aliases))
{
// Preserve the aliases array's keys in JavaScript
$config['aliases'] = new Dictionary;
foreach ($this->aliases as $elName => $aliases)
{
$config['aliases'][$elName] = new Dictionary($aliases);
}
}
return $config;
}
/**
* {@inheritdoc}
*/
public function getJSHints()
{
return ['HTMLELEMENTS_HAS_ALIASES' => (int) !empty($this->aliases)];
}
}