xref: /plugin/combo/ComboStrap/MediaLink.php (revision 1fa8c418ed5809db58049141be41b7738471dd32)
1<?php
2/**
3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15use dokuwiki\Extension\SyntaxPlugin;
16use syntax_plugin_combo_media;
17
18require_once(__DIR__ . '/PluginUtility.php');
19
20/**
21 * Class InternalMedia
22 * Represent a media link
23 *
24 *
25 * @package ComboStrap
26 *
27 * Wrapper around {@link Doku_Handler_Parse_Media}
28 *
29 * Not that for dokuwiki the `type` key of the attributes is the `call`
30 * and therefore determine the function in an render
31 * (ie {@link \Doku_Renderer::internalmedialink()} or {@link \Doku_Renderer::externalmedialink()}
32 *
33 * This is a link to a media (pdf, image, ...).
34 * It's used to check the media type and to
35 * take over if the media type is an image
36 */
37abstract class MediaLink
38{
39
40
41    /**
42     * The dokuwiki type and mode name
43     * (ie call)
44     *  * ie {@link MediaLink::EXTERNAL_MEDIA_CALL_NAME}
45     *  or {@link MediaLink::INTERNAL_MEDIA_CALL_NAME}
46     *
47     * The dokuwiki type (internalmedia/externalmedia)
48     * is saved in a `type` key that clash with the
49     * combostrap type. To avoid the clash, we renamed it
50     */
51    const MEDIA_DOKUWIKI_TYPE = 'dokuwiki_type';
52    const INTERNAL_MEDIA_CALL_NAME = "internalmedia";
53    const EXTERNAL_MEDIA_CALL_NAME = "externalmedia";
54
55    const CANONICAL = "image";
56
57    /**
58     * This attributes does not apply
59     * to a URL
60     * They are only for the tag (img, svg, ...)
61     * or internal
62     */
63    const NON_URL_ATTRIBUTES = [
64        MediaLink::ALIGN_KEY,
65        MediaLink::LINKING_KEY,
66        TagAttributes::TITLE_KEY,
67        Hover::ON_HOVER_ATTRIBUTE,
68        Animation::ON_VIEW_ATTRIBUTE,
69        MediaLink::MEDIA_DOKUWIKI_TYPE,
70        MediaLink::DOKUWIKI_SRC
71    ];
72
73    /**
74     * This attribute applies
75     * to a image url (img, svg, ...)
76     */
77    const URL_ATTRIBUTES = [
78        Dimension::WIDTH_KEY,
79        Dimension::HEIGHT_KEY,
80        CacheMedia::CACHE_KEY,
81    ];
82
83    /**
84     * Default image linking value
85     */
86    const CONF_DEFAULT_LINKING = "defaultImageLinking";
87    const LINKING_LINKONLY_VALUE = "linkonly";
88    const LINKING_DETAILS_VALUE = 'details';
89    const LINKING_NOLINK_VALUE = 'nolink';
90
91    /**
92     * @deprecated 2021-06-12
93     */
94    const LINK_PATTERN = "{{\s*([^|\s]*)\s*\|?.*}}";
95
96    const LINKING_DIRECT_VALUE = 'direct';
97
98    /**
99     * Only used by Dokuwiki
100     * Contains the path and eventually an anchor
101     * never query parameters
102     */
103    const DOKUWIKI_SRC = "src";
104    /**
105     * Link value:
106     *   * 'nolink'
107     *   * 'direct': directly to the image
108     *   * 'linkonly': show only a url
109     *   * 'details': go to the details media viewer
110     *
111     * @var
112     */
113    const LINKING_KEY = 'linking';
114    const ALIGN_KEY = 'align';
115
116
117    private $lazyLoad = null;
118
119
120    /**
121     * The path of the media
122     * @var Media[]
123     */
124    private $media;
125
126
127    /**
128     * Image constructor.
129     * @param Image $media
130     *
131     * Protected and not private
132     * to allow cascading init
133     * If private, the parent attributes are null
134     */
135    protected function __construct(Media $media)
136    {
137        $this->media = $media;
138    }
139
140
141
142    /**
143     * Create an image from dokuwiki internal call media attributes
144     * @param array $callAttributes
145     * @return MediaLink
146     */
147    public static function createFromIndexAttributes(array $callAttributes)
148    {
149        $id = $callAttributes[0]; // path
150        $title = $callAttributes[1];
151        $align = $callAttributes[2];
152        $width = $callAttributes[3];
153        $height = $callAttributes[4];
154        $cache = $callAttributes[5];
155        $linking = $callAttributes[6];
156
157        $tagAttributes = TagAttributes::createEmpty();
158        $tagAttributes->addComponentAttributeValue(TagAttributes::TITLE_KEY, $title);
159        $tagAttributes->addComponentAttributeValue(self::ALIGN_KEY, $align);
160        $tagAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, $width);
161        $tagAttributes->addComponentAttributeValue(Dimension::HEIGHT_KEY, $height);
162        $tagAttributes->addComponentAttributeValue(CacheMedia::CACHE_KEY, $cache);
163        $tagAttributes->addComponentAttributeValue(self::LINKING_KEY, $linking);
164
165        return self::createMediaLinkFromNonQualifiedPath($id, $tagAttributes);
166
167    }
168
169    /**
170     * A function to explicitly create an internal media from
171     * a call stack array (ie key string and value) that we get in the {@link SyntaxPlugin::render()}
172     * from the {@link MediaLink::toCallStackArray()}
173     *
174     * @param $attributes - the attributes created by the function {@link MediaLink::getParseAttributes()}
175     * @param $rev - the mtime
176     * @return MediaLink|RasterImageLink|SvgImageLink
177     */
178    public static function createFromCallStackArray($attributes, $rev = null)
179    {
180
181        if (!is_array($attributes)) {
182            // Debug for the key_exist below because of the following message:
183            // `PHP Warning:  key_exists() expects parameter 2 to be array, array given`
184            LogUtility::msg("The `attributes` parameter is not an array. Value ($attributes)", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
185        }
186
187        /**
188         * Media id are not cleaned
189         * They are always absolute ?
190         */
191        if (!isset($attributes[DokuPath::PATH_ATTRIBUTE])) {
192            $path = "notfound";
193            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);
194        } else {
195            $path = $attributes[DokuPath::PATH_ATTRIBUTE];
196            unset($attributes[DokuPath::PATH_ATTRIBUTE]);
197        }
198
199
200        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
201
202
203        return self::createMediaLinkFromNonQualifiedPath($path, $rev, $tagAttributes);
204
205    }
206
207    /**
208     * @param $match - the match of the renderer (just a shortcut)
209     * @return MediaLink
210     */
211    public static function createFromRenderMatch($match)
212    {
213
214        /**
215         * The parsing function {@link Doku_Handler_Parse_Media} has some flow / problem
216         *    * It keeps the anchor only if there is no query string
217         *    * It takes the first digit as the width (ie media.pdf?page=31 would have a width of 31)
218         *    * `src` is not only the media path but may have a anchor
219         * We parse it then
220         */
221
222
223        /**
224         *   * Delete the opening and closing character
225         *   * create the url and description
226         */
227        $match = preg_replace(array('/^\{\{/', '/\}\}$/u'), '', $match);
228        $parts = explode('|', $match, 2);
229        $description = null;
230        $url = $parts[0];
231        if (isset($parts[1])) {
232            $description = $parts[1];
233        }
234
235        /**
236         * Media Alignment
237         */
238        $rightAlign = (bool)preg_match('/^ /', $url);
239        $leftAlign = (bool)preg_match('/ $/', $url);
240        $url = trim($url);
241
242        // Logic = what's that ;)...
243        if ($leftAlign & $rightAlign) {
244            $align = 'center';
245        } else if ($rightAlign) {
246            $align = 'right';
247        } else if ($leftAlign) {
248            $align = 'left';
249        } else {
250            $align = null;
251        }
252
253        /**
254         * The combo attributes array
255         */
256        $parsedAttributes = DokuwikiUrl::createFromUrl($url)->toArray();
257        $path = $parsedAttributes[DokuPath::PATH_ATTRIBUTE];
258        if (!isset($parsedAttributes[MediaLink::LINKING_KEY])) {
259            $parsedAttributes[MediaLink::LINKING_KEY] = PluginUtility::getConfValue(self::CONF_DEFAULT_LINKING, self::LINKING_DIRECT_VALUE);
260        }
261
262        /**
263         * Media Type
264         */
265        if (media_isexternal($path) || link_isinterwiki($path)) {
266            $mediaType = MediaLink::EXTERNAL_MEDIA_CALL_NAME;
267        } else {
268            $mediaType = MediaLink::INTERNAL_MEDIA_CALL_NAME;
269        }
270
271
272        /**
273         * src in dokuwiki is the path and the anchor if any
274         */
275        $src = $path;
276        if (isset($parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]) != null) {
277            $src = $src . "#" . $parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES];
278        }
279
280        /**
281         * To avoid clash with the combostrap component type
282         * ie this is also a ComboStrap attribute where we set the type of a SVG (icon, illustration, background)
283         * we store the media type (ie external/internal) in another key
284         *
285         * There is no need to repeat the attributes as the arrays are merged
286         * into on but this is also an informal code to show which attributes
287         * are only Dokuwiki Native
288         *
289         */
290        $dokuwikiAttributes = array(
291            self::MEDIA_DOKUWIKI_TYPE => $mediaType,
292            self::DOKUWIKI_SRC => $src,
293            Dimension::WIDTH_KEY => $parsedAttributes[Dimension::WIDTH_KEY],
294            Dimension::HEIGHT_KEY => $parsedAttributes[Dimension::HEIGHT_KEY],
295            CacheMedia::CACHE_KEY => $parsedAttributes[CacheMedia::CACHE_KEY],
296            TagAttributes::TITLE_KEY => $description,
297            MediaLink::ALIGN_KEY => $align,
298            MediaLink::LINKING_KEY => $parsedAttributes[MediaLink::LINKING_KEY],
299        );
300
301        /**
302         * Merge standard dokuwiki attributes and
303         * parsed attributes
304         */
305        $mergedAttributes = PluginUtility::mergeAttributes($dokuwikiAttributes, $parsedAttributes);
306
307        /**
308         * If this is an internal media,
309         * we are using our implementation
310         * and we have a change on attribute specification
311         */
312        if ($mediaType == MediaLink::INTERNAL_MEDIA_CALL_NAME) {
313
314            /**
315             * The align attribute on an image parse
316             * is a float right
317             * ComboStrap does a difference between a block right and a float right
318             */
319            if ($mergedAttributes[self::ALIGN_KEY] === "right") {
320                unset($mergedAttributes[self::ALIGN_KEY]);
321                $mergedAttributes[FloatAttribute::FLOAT_KEY] = "right";
322            }
323
324
325        }
326
327        return self::createFromCallStackArray($mergedAttributes);
328
329    }
330
331
332    public
333    function setLazyLoad($false)
334    {
335        $this->lazyLoad = $false;
336    }
337
338    public
339    function getLazyLoad()
340    {
341        return $this->lazyLoad;
342    }
343
344
345    /**
346     * Create a media link from a unknown type path (ie relative or absolute)
347     *
348     * This function transforms the path to absolute against the actual namespace of the requested page ID if the
349     * path is relative.
350     *
351     * @param $nonQualifiedPath
352     * @param TagAttributes $tagAttributes
353     * @param string $rev
354     * @return MediaLink
355     */
356    public
357    static function createMediaLinkFromNonQualifiedPath($nonQualifiedPath, $rev = null, $tagAttributes = null)
358    {
359        if (is_object($rev)) {
360            LogUtility::msg("rev should not be an object", LogUtility::LVL_MSG_ERROR, "support");
361        }
362        if ($tagAttributes == null) {
363            $tagAttributes = TagAttributes::createEmpty();
364        } else {
365            if (!($tagAttributes instanceof TagAttributes)) {
366                LogUtility::msg("TagAttributes is not an instance of Tag Attributes", LogUtility::LVL_MSG_ERROR, "support");
367            }
368        }
369
370        /**
371         * Resolution
372         */
373        $qualifiedPath = $nonQualifiedPath;
374        if (!media_isexternal($qualifiedPath)) {
375            global $ID;
376            $qualifiedId = $nonQualifiedPath;
377            resolve_mediaid(getNS($ID), $qualifiedId, $exists);
378            $qualifiedPath = DokuPath::PATH_SEPARATOR . $qualifiedId;
379        }
380
381
382        return self::createMediaLinkFromAbsolutePath($qualifiedPath,$rev,$tagAttributes);
383    }
384
385    public static function createMediaLinkFromAbsolutePath($qualifiedPath, $rev = null, $tagAttributes = null)
386    {
387        /**
388         * Processing
389         */
390        $dokuPath = DokuPath::createMediaPathFromAbsolutePath($qualifiedPath, $rev);
391        if ($dokuPath->getExtension() == "svg") {
392            /**
393             * The mime type is set when uploading, not when
394             * viewing.
395             * Because they are internal image, the svg was already uploaded
396             * Therefore, no authorization scheme here
397             */
398            $mime = "image/svg+xml";
399        } else {
400            $mime = $dokuPath->getKnownMime();
401        }
402
403        if (substr($mime, 0, 5) == 'image') {
404            if (substr($mime, 6) == "svg+xml") {
405
406                $svgImage = new ImageSvg($qualifiedPath, $rev, $tagAttributes);
407                $internalMedia = new SvgImageLink($svgImage);
408            } else {
409                $rasterImage = new ImageRaster($qualifiedPath, $rev, $tagAttributes);
410                $internalMedia = new RasterImageLink($rasterImage );
411            }
412        } else {
413            if ($mime == false) {
414                LogUtility::msg("The mime type of the media ($qualifiedPath) is <a href=\"https://www.dokuwiki.org/mime\">unknown (not in the configuration file)</a>", LogUtility::LVL_MSG_ERROR);
415                $media = new ImageRaster($qualifiedPath, $rev, $tagAttributes);
416                $internalMedia = new RasterImageLink($media);
417            } else {
418                LogUtility::msg("The type ($mime) of media ($qualifiedPath) is not an image", LogUtility::LVL_MSG_DEBUG, "image");
419                $media = Media::create($qualifiedPath, $rev, $tagAttributes);
420                $internalMedia = new ThirdMediaLink($media);
421            }
422        }
423
424        return $internalMedia;
425    }
426
427
428    /**
429     * A function to set explicitly which array format
430     * is used in the returned data of a {@link SyntaxPlugin::handle()}
431     * (which ultimately is stored in the {@link CallStack)
432     *
433     * This is to make the difference with the {@link MediaLink::createFromIndexAttributes()}
434     * that is indexed by number (ie without property name)
435     *
436     *
437     * Return the same array than with the {@link self::parse()} method
438     * that is used in the {@link CallStack}
439     *
440     * @return array of key string and value
441     */
442    public function toCallStackArray(): array
443    {
444        /**
445         * Trying to stay inline with the dokuwiki key
446         * We use the 'src' attributes as id
447         *
448         * src is a path (not an id)
449         */
450        $array = array(
451            DokuPath::PATH_ATTRIBUTE => $this->getMedia()->getPath()
452        );
453
454
455        // Add the extra attribute
456        return array_merge($this->getMedia()->getAttributes()->toCallStackArray(), $array);
457
458
459    }
460
461
462
463
464
465    public
466    static function isInternalMediaSyntax($text)
467    {
468        return preg_match(' / ' . syntax_plugin_combo_media::MEDIA_PATTERN . ' / msSi', $text);
469    }
470
471
472
473    public
474    function __toString()
475    {
476        return $this->getMedia()->getId();
477    }
478
479    private
480    function getAlign()
481    {
482        return $this->getMedia()->getAttributes()->getComponentAttributeValue(self::ALIGN_KEY);
483    }
484
485    private
486    function getLinking()
487    {
488        return $this->getMedia()->getAttributes()->getComponentAttributeValue(self::LINKING_KEY);
489    }
490
491
492
493    /**
494     * @return string - the HTML of the image inside a link if asked
495     */
496    public
497    function renderMediaTagWithLink(): string
498    {
499
500        /**
501         * Link to the media
502         *
503         */
504        $mediaLink = TagAttributes::createEmpty();
505        // https://www.dokuwiki.org/config:target
506        global $conf;
507        $target = $conf['target']['media'];
508        $mediaLink->addHtmlAttributeValueIfNotEmpty("target", $target);
509        if (!empty($target)) {
510            $mediaLink->addHtmlAttributeValue("rel", 'noopener');
511        }
512
513        /**
514         * Do we add a link to the image ?
515         */
516        $media = $this->getMedia();
517        $linking = $this->getLinking();
518        switch ($linking) {
519            case self::LINKING_LINKONLY_VALUE: // show only a url
520                $src = ml(
521                    $media->getId(),
522                    array(
523                        'id' => $media->getId(),
524                        'cache' => $media->getCache(),
525                        'rev' => $media->getRevision()
526                    )
527                );
528                $mediaLink->addHtmlAttributeValue("href", $src);
529                $title = $media->getTitle();
530                if (empty($title)) {
531                    $title = $media->getBaseName();
532                }
533                return $mediaLink->toHtmlEnterTag("a") . $title . "</a>";
534            case self::LINKING_NOLINK_VALUE:
535                return $this->renderMediaTag();
536            default:
537            case self::LINKING_DIRECT_VALUE:
538                //directly to the image
539                $src = ml(
540                    $media->getId(),
541                    array(
542                        'id' => $media->getId(),
543                        'cache' => $media->getCache(),
544                        'rev' => $media->getRevision()
545                    ),
546                    true
547                );
548                $mediaLink->addHtmlAttributeValue("href", $src);
549                return $mediaLink->toHtmlEnterTag("a") .
550                    $this->renderMediaTag() .
551                    "</a>";
552
553            case self::LINKING_DETAILS_VALUE:
554                //go to the details media viewer
555                $src = ml(
556                    $media->getId(),
557                    array(
558                        'id' => $media->getId(),
559                        'cache' => $media->getCache(),
560                        'rev' => $media->getRevision()
561                    ),
562                    false
563                );
564                $mediaLink->addHtmlAttributeValue("href", $src);
565                return $mediaLink->toHtmlEnterTag("a") .
566                    $this->renderMediaTag() .
567                    "</a>";
568
569        }
570
571
572    }
573
574
575
576
577
578    /**
579     * @return string - the HTML of the image
580     */
581    public abstract function renderMediaTag(): string;
582
583
584    /**
585     * The file
586     * @return Media
587     */
588    public function getMedia(): Media
589    {
590        return $this->media;
591    }
592
593
594}
595