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