*
*/
namespace ComboStrap;
use dokuwiki\Extension\SyntaxPlugin;
use syntax_plugin_combo_media;
require_once(__DIR__ . '/DokuPath.php');
/**
* Class InternalMedia
* Represent a media link
*
*
* @package ComboStrap
*
* Wrapper around {@link Doku_Handler_Parse_Media}
*
* Not that for dokuwiki the `type` key of the attributes is the `call`
* and therefore determine the function in an render
* (ie {@link \Doku_Renderer::internalmedialink()} or {@link \Doku_Renderer::externalmedialink()}
*
* It's a HTML tag and a URL (in the dokuwiki mode) build around its file system path
*/
abstract class MediaLink extends DokuPath
{
/**
* The dokuwiki type and mode name
* (ie call)
* * ie {@link MediaLink::EXTERNAL_MEDIA_CALL_NAME}
* or {@link MediaLink::INTERNAL_MEDIA_CALL_NAME}
*
* The dokuwiki type (internalmedia/externalmedia)
* is saved in a `type` key that clash with the
* combostrap type. To avoid the clash, we renamed it
*/
const MEDIA_DOKUWIKI_TYPE = 'dokuwiki_type';
const INTERNAL_MEDIA_CALL_NAME = "internalmedia";
const EXTERNAL_MEDIA_CALL_NAME = "externalmedia";
const CANONICAL = "image";
/**
* This attributes does not apply
* to a URL
* They are only for the tag (img, svg, ...)
* or internal
*/
const NON_URL_ATTRIBUTES = [
self::ALIGN_KEY,
self::LINKING_KEY,
TagAttributes::TITLE_KEY,
Hover::ON_HOVER_ATTRIBUTE,
Animation::ON_VIEW_ATTRIBUTE,
MediaLink::MEDIA_DOKUWIKI_TYPE,
MediaLink::DOKUWIKI_SRC
];
/**
* This attribute applies
* to a image url (img, svg, ...)
*/
const URL_ATTRIBUTES = [
Dimension::WIDTH_KEY,
Dimension::HEIGHT_KEY,
CacheMedia::CACHE_KEY,
];
/**
* Default image linking value
*/
const CONF_DEFAULT_LINKING = "defaultImageLinking";
const LINKING_LINKONLY_VALUE = "linkonly";
const LINKING_DETAILS_VALUE = 'details';
const LINKING_NOLINK_VALUE = 'nolink';
/**
* @deprecated 2021-06-12
*/
const LINK_PATTERN = "{{\s*([^|\s]*)\s*\|?.*}}";
const LINKING_DIRECT_VALUE = 'direct';
/**
* Only used by Dokuwiki
* Contains the path and eventually an anchor
* never query parameters
*/
const DOKUWIKI_SRC = "src";
/**
* Link value:
* * 'nolink'
* * 'direct': directly to the image
* * 'linkonly': show only a url
* * 'details': go to the details media viewer
*
* @var
*/
const LINKING_KEY = 'linking';
const ALIGN_KEY = 'align';
private $lazyLoad = null;
/**
* @var TagAttributes
*/
protected $tagAttributes;
/**
* Image constructor.
* @param $ref
* @param TagAttributes $tagAttributes
* @param string $rev - mtime
*
* Protected and not private
* to allow cascading init
* If private, the parent attributes are null
*
*/
protected function __construct($absolutePath, $tagAttributes = null, $rev = null)
{
parent::__construct($absolutePath, DokuPath::MEDIA_TYPE, $rev);
if ($tagAttributes == null) {
$this->tagAttributes = TagAttributes::createEmpty();
} else {
$this->tagAttributes = $tagAttributes;
}
}
/**
* Create an image from dokuwiki internal call media attributes
* @param array $callAttributes
* @return MediaLink
*/
public static function createFromIndexAttributes(array $callAttributes)
{
$id = $callAttributes[0]; // path
$title = $callAttributes[1];
$align = $callAttributes[2];
$width = $callAttributes[3];
$height = $callAttributes[4];
$cache = $callAttributes[5];
$linking = $callAttributes[6];
$tagAttributes = TagAttributes::createEmpty();
$tagAttributes->addComponentAttributeValue(TagAttributes::TITLE_KEY, $title);
$tagAttributes->addComponentAttributeValue(self::ALIGN_KEY, $align);
$tagAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, $width);
$tagAttributes->addComponentAttributeValue(Dimension::HEIGHT_KEY, $height);
$tagAttributes->addComponentAttributeValue(CacheMedia::CACHE_KEY, $cache);
$tagAttributes->addComponentAttributeValue(self::LINKING_KEY, $linking);
return self::createMediaLinkFromNonQualifiedPath($id, $tagAttributes);
}
/**
* A function to explicitly create an internal media from
* a call stack array (ie key string and value) that we get in the {@link SyntaxPlugin::render()}
* from the {@link MediaLink::toCallStackArray()}
*
* @param $attributes - the attributes created by the function {@link MediaLink::getParseAttributes()}
* @param $rev - the mtime
* @return MediaLink|RasterImageLink|SvgImageLink
*/
public static function createFromCallStackArray($attributes, $rev = null)
{
if (!is_array($attributes)) {
// Debug for the key_exist below because of the following message:
// `PHP Warning: key_exists() expects parameter 2 to be array, array given`
LogUtility::msg("The `attributes` parameter is not an array. Value ($attributes)", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
}
/**
* Media id are not cleaned
* They are always absolute ?
*/
if (!isset($attributes[DokuPath::PATH_ATTRIBUTE])) {
$path = "notfound";
LogUtility::msg("A path attribute is mandatory when creating a media link and was not found in the attributes " . print_r($attributes, true), LogUtility::LVL_MSG_ERROR, self::CANONICAL);
} else {
$path = $attributes[DokuPath::PATH_ATTRIBUTE];
unset($attributes[DokuPath::PATH_ATTRIBUTE]);
}
$tagAttributes = TagAttributes::createFromCallStackArray($attributes);
return self::createMediaLinkFromNonQualifiedPath($path, $rev, $tagAttributes);
}
/**
* @param $match - the match of the renderer (just a shortcut)
* @return MediaLink
*/
public static function createFromRenderMatch($match)
{
/**
* The parsing function {@link Doku_Handler_Parse_Media} has some flow / problem
* * It keeps the anchor only if there is no query string
* * It takes the first digit as the width (ie media.pdf?page=31 would have a width of 31)
* * `src` is not only the media path but may have a anchor
* We parse it then
*/
/**
* * Delete the opening and closing character
* * create the url and description
*/
$match = preg_replace(array('/^\{\{/', '/\}\}$/u'), '', $match);
$parts = explode('|', $match, 2);
$description = null;
$url = $parts[0];
if (isset($parts[1])) {
$description = $parts[1];
}
/**
* Media Alignment
*/
$rightAlign = (bool)preg_match('/^ /', $url);
$leftAlign = (bool)preg_match('/ $/', $url);
$url = trim($url);
// Logic = what's that ;)...
if ($leftAlign & $rightAlign) {
$align = 'center';
} else if ($rightAlign) {
$align = 'right';
} else if ($leftAlign) {
$align = 'left';
} else {
$align = null;
}
/**
* The combo attributes array
*/
$parsedAttributes = DokuwikiUrl::createFromUrl($url)->toArray();
$path = $parsedAttributes[DokuPath::PATH_ATTRIBUTE];
if (!isset($parsedAttributes[MediaLink::LINKING_KEY])) {
$parsedAttributes[MediaLink::LINKING_KEY] = PluginUtility::getConfValue(self::CONF_DEFAULT_LINKING, self::LINKING_DIRECT_VALUE);
}
/**
* Media Type
*/
if (media_isexternal($path) || link_isinterwiki($path)) {
$mediaType = MediaLink::EXTERNAL_MEDIA_CALL_NAME;
} else {
$mediaType = MediaLink::INTERNAL_MEDIA_CALL_NAME;
}
/**
* src in dokuwiki is the path and the anchor if any
*/
$src = $path;
if (isset($parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]) != null) {
$src = $src . "#" . $parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES];
}
/**
* To avoid clash with the combostrap component type
* ie this is also a ComboStrap attribute where we set the type of a SVG (icon, illustration, background)
* we store the media type (ie external/internal) in another key
*
* There is no need to repeat the attributes as the arrays are merged
* into on but this is also an informal code to show which attributes
* are only Dokuwiki Native
*
*/
$dokuwikiAttributes = array(
self::MEDIA_DOKUWIKI_TYPE => $mediaType,
self::DOKUWIKI_SRC => $src,
Dimension::WIDTH_KEY => $parsedAttributes[Dimension::WIDTH_KEY],
Dimension::HEIGHT_KEY => $parsedAttributes[Dimension::HEIGHT_KEY],
CacheMedia::CACHE_KEY => $parsedAttributes[CacheMedia::CACHE_KEY],
'title' => $description,
MediaLink::ALIGN_KEY => $align,
MediaLink::LINKING_KEY => $parsedAttributes[MediaLink::LINKING_KEY],
);
/**
* Merge standard dokuwiki attributes and
* parsed attributes
*/
$mergedAttributes = PluginUtility::mergeAttributes($dokuwikiAttributes, $parsedAttributes);
/**
* If this is an internal media,
* we are using our implementation
* and we have a change on attribute specification
*/
if ($mediaType == MediaLink::INTERNAL_MEDIA_CALL_NAME) {
/**
* The align attribute on an image parse
* is a float right
* ComboStrap does a difference between a block right and a float right
*/
if ($mergedAttributes[self::ALIGN_KEY] === "right") {
unset($mergedAttributes[self::ALIGN_KEY]);
$mergedAttributes[FloatAttribute::FLOAT_KEY] = "right";
}
}
return self::createFromCallStackArray($mergedAttributes);
}
public
function setLazyLoad($false)
{
$this->lazyLoad = $false;
}
public
function getLazyLoad()
{
return $this->lazyLoad;
}
/**
* Create a media link from a unknown type path (ie relative or absolute)
*
* This function transforms the path to absolute against the actual namespace of the requested page ID if the
* path is relative.
*
* @param $nonQualifiedPath
* @param TagAttributes $tagAttributes
* @param string $rev
* @return MediaLink
*/
public
static function createMediaLinkFromNonQualifiedPath($nonQualifiedPath, $rev = null, $tagAttributes = null)
{
if (is_object($rev)) {
LogUtility::msg("rev should not be an object", LogUtility::LVL_MSG_ERROR, "support");
}
if ($tagAttributes == null) {
$tagAttributes = TagAttributes::createEmpty();
} else {
if (!($tagAttributes instanceof TagAttributes)) {
LogUtility::msg("TagAttributes is not an instance of Tag Attributes", LogUtility::LVL_MSG_ERROR, "support");
}
}
/**
* Resolution
*/
$qualifiedPath = $nonQualifiedPath;
if(!media_isexternal($qualifiedPath)) {
global $ID;
$qualifiedId = $nonQualifiedPath;
resolve_mediaid(getNS($ID), $qualifiedId, $exists);
$qualifiedPath = DokuPath::PATH_SEPARATOR . $qualifiedId;
}
/**
* Processing
*/
$dokuPath = DokuPath::createMediaPathFromAbsolutePath($qualifiedPath, $rev);
if ($dokuPath->getExtension() == "svg") {
/**
* The mime type is set when uploading, not when
* viewing.
* Because they are internal image, the svg was already uploaded
* Therefore, no authorization scheme here
*/
$mime = "image/svg+xml";
} else {
$mime = $dokuPath->getKnownMime();
}
if (substr($mime, 0, 5) == 'image') {
if (substr($mime, 6) == "svg+xml") {
// The require is here because Svg Image Link is child of Internal Media Link (extends)
require_once(__DIR__ . '/SvgImageLink.php');
$internalMedia = new SvgImageLink($qualifiedPath, $tagAttributes, $rev);
} else {
// The require is here because Raster Image Link is child of Internal Media Link (extends)
require_once(__DIR__ . '/RasterImageLink.php');
$internalMedia = new RasterImageLink($qualifiedPath, $tagAttributes);
}
} else {
if ($mime == false) {
LogUtility::msg("The mime type of the media ($nonQualifiedPath) is unknown (not in the configuration file)", LogUtility::LVL_MSG_ERROR, "support");
$internalMedia = new RasterImageLink($qualifiedPath, $tagAttributes);
} else {
LogUtility::msg("The type ($mime) of media ($nonQualifiedPath) is not an image", LogUtility::LVL_MSG_DEBUG, "image");
$internalMedia = new ThirdMediaLink($qualifiedPath, $tagAttributes);
}
}
return $internalMedia;
}
/**
* A function to set explicitly which array format
* is used in the returned data of a {@link SyntaxPlugin::handle()}
* (which ultimately is stored in the {@link CallStack)
*
* This is to make the difference with the {@link MediaLink::createFromIndexAttributes()}
* that is indexed by number (ie without property name)
*
*
* Return the same array than with the {@link self::parse()} method
* that is used in the {@link CallStack}
*
* @return array of key string and value
*/
public
function toCallStackArray()
{
/**
* Trying to stay inline with the dokuwiki key
* We use the 'src' attributes as id
*
* src is a path (not an id)
*/
$array = array(
DokuPath::PATH_ATTRIBUTE => $this->getPath()
);
// Add the extra attribute
return array_merge($this->tagAttributes->toCallStackArray(), $array);
}
/**
* @return string the wiki syntax
*/
public
function getMarkupSyntax()
{
$descriptionPart = "";
if ($this->tagAttributes->hasComponentAttribute(TagAttributes::TITLE_KEY)) {
$descriptionPart = "|" . $this->tagAttributes->getValue(TagAttributes::TITLE_KEY);
}
return '{{:' . $this->getId() . $descriptionPart . '}}';
}
public
static function isInternalMediaSyntax($text)
{
return preg_match(' / ' . syntax_plugin_combo_media::MEDIA_PATTERN . ' / msSi', $text);
}
public
function getRequestedHeight()
{
return $this->tagAttributes->getValue(Dimension::HEIGHT_KEY);
}
/**
* The requested width
*/
public
function getRequestedWidth()
{
return $this->tagAttributes->getValue(Dimension::WIDTH_KEY);
}
public
function getCache()
{
return $this->tagAttributes->getValue(CacheMedia::CACHE_KEY);
}
protected
function getTitle()
{
return $this->tagAttributes->getValue(TagAttributes::TITLE_KEY);
}
public
function __toString()
{
return $this->getId();
}
private
function getAlign()
{
return $this->getTagAttributes()->getComponentAttributeValue(self::ALIGN_KEY, null);
}
private
function getLinking()
{
return $this->getTagAttributes()->getComponentAttributeValue(self::LINKING_KEY, null);
}
public
function &getTagAttributes()
{
return $this->tagAttributes;
}
/**
* @return string - the HTML of the image inside a link if asked
*/
public
function renderMediaTagWithLink()
{
/**
* Link to the media
*
*/
$imageLink = TagAttributes::createEmpty();
// https://www.dokuwiki.org/config:target
global $conf;
$target = $conf['target']['media'];
$imageLink->addHtmlAttributeValueIfNotEmpty("target", $target);
if (!empty($target)) {
$imageLink->addHtmlAttributeValue("rel", 'noopener');
}
/**
* Do we add a link to the image ?
*/
$linking = $this->tagAttributes->getValue(self::LINKING_KEY);
switch ($linking) {
case self::LINKING_LINKONLY_VALUE: // show only a url
$src = ml(
$this->getId(),
array(
'id' => $this->getId(),
'cache' => $this->getCache(),
'rev' => $this->getRevision()
)
);
$imageLink->addHtmlAttributeValue("href", $src);
$title = $this->getTitle();
if (empty($title)) {
$title = $this->getBaseName();
}
return $imageLink->toHtmlEnterTag("a") . $title . "";
case self::LINKING_NOLINK_VALUE:
return $this->renderMediaTag();
default:
case self::LINKING_DIRECT_VALUE:
//directly to the image
$src = ml(
$this->getId(),
array(
'id' => $this->getId(),
'cache' => $this->getCache(),
'rev' => $this->getRevision()
),
true
);
$imageLink->addHtmlAttributeValue("href", $src);
return $imageLink->toHtmlEnterTag("a") .
$this->renderMediaTag() .
"";
case self::LINKING_DETAILS_VALUE:
//go to the details media viewer
$src = ml(
$this->getId(),
array(
'id' => $this->getId(),
'cache' => $this->getCache(),
'rev' => $this->getRevision()
),
false
);
$imageLink->addHtmlAttributeValue("href", $src);
return $imageLink->toHtmlEnterTag("a") .
$this->renderMediaTag() .
"";
}
}
/**
* @param $imgTagHeight
* @param $imgTagWidth
* @return float|mixed
*/
public
function checkWidthAndHeightRatioAndReturnTheGoodValue($imgTagWidth, $imgTagHeight)
{
/**
* Check of height and width dimension
* as specified here
* https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
*/
$targetRatio = $this->getTargetRatio();
if (!(
$imgTagHeight * $targetRatio >= $imgTagWidth - 0.5
&&
$imgTagHeight * $targetRatio <= $imgTagWidth + 0.5
)) {
// check the second statement
if (!(
$imgTagWidth / $targetRatio >= $imgTagHeight - 0.5
&&
$imgTagWidth / $targetRatio <= $imgTagHeight + 0.5
)) {
$requestedHeight = $this->getRequestedHeight();
$requestedWidth = $this->getRequestedWidth();
if (
!empty($requestedHeight)
&& !empty($requestedWidth)
) {
/**
* The user has asked for a width and height
*/
$imgTagWidth = round($imgTagHeight * $targetRatio);
LogUtility::msg("The width ($requestedWidth) and height ($requestedHeight) specified on the image ($this) does not follow the natural ratio as required by HTML. The width was then set to ($imgTagWidth).", LogUtility::LVL_MSG_INFO, self::CANONICAL);
} else {
/**
* Programmatic error from the developer
*/
$imgTagRatio = $imgTagWidth / $imgTagHeight;
LogUtility::msg("Internal Error: The width ($imgTagWidth) and height ($imgTagHeight) calculated for the image ($this) does not pass the ratio test. They have a ratio of ($imgTagRatio) while the natural dimension ratio is ($targetRatio)");
}
}
}
return $imgTagWidth;
}
/**
* Target ratio as explained here
* https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
* @return float|int|false
* false if the image is not supported
*
* It's needed for an img tag to set the img `width` and `height` that pass the
* {@link MediaLink::checkWidthAndHeightRatioAndReturnTheGoodValue() check}
* to avoid layout shift
*
*/
protected function getTargetRatio()
{
if ($this->getMediaHeight() == null || $this->getMediaWidth() == null) {
return false;
} else {
return $this->getMediaWidth() / $this->getMediaHeight();
}
}
/**
* Return the height that the image should take on the screen
* for the specified size
*
* @param null $localRequestedWidth - the width to derive the height from (in case the image is created for responsive lazy loading)
* if not specified, the requested width and if not specified the intrinsic width
* @return int the height value attribute in a img
*/
public
function getImgTagHeightValue($localRequestedWidth = null)
{
/**
* Cropping is not yet supported.
*/
$requestedHeight = $this->getRequestedHeight();
$requestedWidth = $this->getRequestedWidth();
if (
$requestedHeight != null
&& $requestedHeight != 0
&& $requestedWidth != null
&& $requestedWidth != 0
) {
global $ID;
if ($ID != "wiki:syntax") {
/**
* Cropping
*/
LogUtility::msg("The width and height has been set on the image ($this) but we don't support yet cropping. Set only the width or the height (0x250)", LogUtility::LVL_MSG_WARNING, self::CANONICAL);
}
}
/**
* If resize by height, the img tag height is the requested height
*/
if ($localRequestedWidth == null) {
if ($requestedHeight != null) {
return $requestedHeight;
} else {
$localRequestedWidth = $this->getRequestedWidth();
if (empty($localRequestedWidth)) {
$localRequestedWidth = $this->getMediaWidth();
}
}
}
/**
* Computation
*/
$computedHeight = $this->getRequestedHeight();
$targetRatio = $this->getTargetRatio();
if ($targetRatio !== false) {
/**
* Scale the height by target ratio
*/
$computedHeight = $localRequestedWidth / $this->getTargetRatio();
/**
* Check
*/
if ($requestedHeight != null) {
if ($requestedHeight < $computedHeight) {
LogUtility::msg("The computed height cannot be greater than the requested height");
}
}
}
/**
* Rounding to integer
* The fetch.php file takes int as value for width and height
* making a rounding if we pass a double (such as 37.5)
* This is important because the security token is based on width and height
* and therefore the fetch will failed
*
* And not directly {@link intval} because it will make from 3.6, 3 and not 4
*/
return intval(round($computedHeight));
}
/**
* @return int - the width value attribute in a img (in CSS pixel that the image should takes)
*/
public
function getImgTagWidthValue()
{
$linkWidth = $this->getRequestedWidth();
if (empty($linkWidth)) {
if (empty($this->getRequestedHeight())) {
$linkWidth = $this->getMediaWidth();
} else {
// Height is not empty
// We derive the width from it
if ($this->getMediaHeight() != 0
&& !empty($this->getMediaHeight())
&& !empty($this->getMediaWidth())
) {
$linkWidth = $this->getMediaWidth() * ($this->getRequestedHeight() / $this->getMediaHeight());
}
}
}
/**
* Rounding to integer
* The fetch.php file takes int as value for width and height
* making a rounding if we pass a double (such as 37.5)
* This is important because the security token is based on width and height
* and therefore the fetch will failed
*
* And this is also ask by the specification
* a non-null positive integer
* https://html.spec.whatwg.org/multipage/embedded-content-other.html#attr-dim-height
*
* And not {@link intval} because it will make from 3.6, 3 and not 4
*/
return intval(round($linkWidth));
}
/**
* @return string - the HTML of the image
*/
public abstract function renderMediaTag();
/**
* The Url
* @return mixed
*/
public abstract function getAbsoluteUrl();
/**
* For a raster image, the internal width
* for a svg, the defined viewBox
*
* This is needed to calculate the {@link MediaLink::getTargetRatio() target ratio}
* and pass them to the img tag to avoid layout shift
*
* @return mixed
*/
public abstract function getMediaWidth();
/**
* For a raster image, the internal height
* for a svg, the defined `viewBox` value
*
* This is needed to calculate the {@link MediaLink::getTargetRatio() target ratio}
* and pass them to the img tag to avoid layout shift
*
* @return mixed
*/
public abstract function getMediaHeight();
}