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