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\Action\Plugin;
16use dokuwiki\Extension\SyntaxPlugin;
17use dokuwiki\Parsing\ParserMode\Internallink;
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     * The method to lazy load resources (Ie media)
120     */
121    const LAZY_LOAD_METHOD = "lazy-method";
122    const LAZY_LOAD_METHOD_HTML_VALUE = "html-attribute";
123    const LAZY_LOAD_METHOD_LOZAD_VALUE = "lozad";
124    const UNKNOWN_MIME = "unknwon";
125    /**
126     * @var string
127     */
128    private $lazyLoadMethod;
129
130    private $lazyLoad = null;
131
132
133    /**
134     * The path of the media
135     * @var Media[]
136     */
137    private $media;
138    private $linking;
139    private $linkingClass;
140
141
142    /**
143     * Image constructor.
144     * @param Image $media
145     *
146     * Protected and not private
147     * to allow cascading init
148     * If private, the parent attributes are null
149     */
150    protected function __construct(Media $media)
151    {
152        $this->media = $media;
153    }
154
155
156    /**
157     * Create an image from dokuwiki {@link Internallink internal call media attributes}
158     *
159     * Dokuwiki extracts already the width, height and align property
160     * @param array $callAttributes
161     * @return MediaLink
162     */
163    public static function createFromIndexAttributes(array $callAttributes)
164    {
165        $src = $callAttributes[0];
166        $title = $callAttributes[1];
167        $align = $callAttributes[2];
168        $width = $callAttributes[3];
169        $height = $callAttributes[4];
170        $cache = $callAttributes[5];
171        $linking = $callAttributes[6];
172
173        $tagAttributes = TagAttributes::createEmpty();
174        $tagAttributes->addComponentAttributeValue(TagAttributes::TITLE_KEY, $title);
175        $tagAttributes->addComponentAttributeValue(self::ALIGN_KEY, $align);
176        $tagAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, $width);
177        $tagAttributes->addComponentAttributeValue(Dimension::HEIGHT_KEY, $height);
178        $tagAttributes->addComponentAttributeValue(CacheMedia::CACHE_KEY, $cache);
179        $tagAttributes->addComponentAttributeValue(self::LINKING_KEY, $linking);
180
181        return self::createMediaLinkFromId($src, $tagAttributes);
182
183    }
184
185    /**
186     * A function to explicitly create an internal media from
187     * a call stack array (ie key string and value) that we get in the {@link SyntaxPlugin::render()}
188     * from the {@link MediaLink::toCallStackArray()}
189     *
190     * @param $attributes - the attributes created by the function {@link MediaLink::getParseAttributes()}
191     * @param $rev - the mtime
192     * @return null|MediaLink
193     */
194    public static function createFromCallStackArray($attributes, $rev = null): ?MediaLink
195    {
196
197        if (!is_array($attributes)) {
198            // Debug for the key_exist below because of the following message:
199            // `PHP Warning:  key_exists() expects parameter 2 to be array, array given`
200            LogUtility::msg("The `attributes` parameter is not an array. Value ($attributes)", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
201        }
202
203        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
204
205        $src = $attributes[self::DOKUWIKI_SRC];
206        if ($src === null) {
207            /**
208             * Dokuwiki parse already the src and create the path and the attributes
209             * The new model will not, we check if we are in the old mode
210             */
211            $src = $attributes[PagePath::PROPERTY_NAME];
212            if ($src === null) {
213                LogUtility::msg("src is mandatory for an image link and was not passed");
214                return null;
215            }
216        }
217        $dokuUrl = DokuwikiUrl::createFromUrl($src);
218        $scheme = $dokuUrl->getScheme();
219        switch ($scheme) {
220            case DokuFs::SCHEME:
221                $id = $dokuUrl->getPath();
222                // the id is always absolute, except in a link
223                // It may be relative, transform it as absolute
224                global $ID;
225                resolve_mediaid(getNS($ID), $id, $exists);
226                $path = DokuPath::createMediaPathFromId($id, $rev);
227                return self::createMediaLinkFromPath($path, $tagAttributes);
228            case InterWikiPath::scheme:
229                $path = InterWikiPath::create($dokuUrl->getPath());
230                return self::createMediaLinkFromPath($path, $tagAttributes);
231            case InternetPath::scheme:
232                $path = InternetPath::create($dokuUrl->getPath());
233                return self::createMediaLinkFromPath($path, $tagAttributes);
234            default:
235                LogUtility::msg("The media with the scheme ($scheme) are not yet supported. Media Source: $src");
236                return null;
237
238        }
239
240
241    }
242
243    /**
244     * @param $match - the match of the renderer (just a shortcut)
245     * @return MediaLink
246     */
247    public static function createFromRenderMatch($match)
248    {
249
250        /**
251         * The parsing function {@link Doku_Handler_Parse_Media} has some flow / problem
252         *    * It keeps the anchor only if there is no query string
253         *    * It takes the first digit as the width (ie media.pdf?page=31 would have a width of 31)
254         *    * `src` is not only the media path but may have a anchor
255         * We parse it then
256         */
257
258
259        /**
260         *   * Delete the opening and closing character
261         *   * create the url and description
262         */
263        $match = preg_replace(array('/^\{\{/', '/\}\}$/u'), '', $match);
264        $parts = explode('|', $match, 2);
265        $description = null;
266        $url = $parts[0];
267        if (isset($parts[1])) {
268            $description = $parts[1];
269        }
270
271        /**
272         * Media Alignment
273         */
274        $rightAlign = (bool)preg_match('/^ /', $url);
275        $leftAlign = (bool)preg_match('/ $/', $url);
276        $url = trim($url);
277
278        // Logic = what's that ;)...
279        if ($leftAlign & $rightAlign) {
280            $align = 'center';
281        } else if ($rightAlign) {
282            $align = 'right';
283        } else if ($leftAlign) {
284            $align = 'left';
285        } else {
286            $align = null;
287        }
288
289        /**
290         * The combo attributes array
291         */
292        $dokuwikiUrl = DokuwikiUrl::createFromUrl($url);
293        $parsedAttributes = $dokuwikiUrl->toArray();
294        $path = $dokuwikiUrl->getPath();
295        $linkingKey = $dokuwikiUrl->getQueryParameter(MediaLink::LINKING_KEY);
296        $parsedAttributes[MediaLink::LINKING_KEY] = $linkingKey;
297
298        /**
299         * Media Type
300         */
301        $scheme = $dokuwikiUrl->getScheme();
302        if ($scheme === DokuFs::SCHEME) {
303            $mediaType = MediaLink::INTERNAL_MEDIA_CALL_NAME;
304        } else {
305            $mediaType = MediaLink::EXTERNAL_MEDIA_CALL_NAME;
306        }
307
308
309        /**
310         * src in dokuwiki is the path and the anchor if any
311         */
312        $src = $path;
313        if (isset($parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES]) != null) {
314            $src = $src . "#" . $parsedAttributes[DokuwikiUrl::ANCHOR_ATTRIBUTES];
315        }
316
317        /**
318         * To avoid clash with the combostrap component type
319         * ie this is also a ComboStrap attribute where we set the type of a SVG (icon, illustration, background)
320         * we store the media type (ie external/internal) in another key
321         *
322         * There is no need to repeat the attributes as the arrays are merged
323         * into on but this is also an informal code to show which attributes
324         * are only Dokuwiki Native
325         *
326         */
327        $dokuwikiAttributes = array(
328            self::MEDIA_DOKUWIKI_TYPE => $mediaType,
329            self::DOKUWIKI_SRC => $src,
330            Dimension::WIDTH_KEY => $parsedAttributes[Dimension::WIDTH_KEY],
331            Dimension::HEIGHT_KEY => $parsedAttributes[Dimension::HEIGHT_KEY],
332            CacheMedia::CACHE_KEY => $parsedAttributes[CacheMedia::CACHE_KEY],
333            TagAttributes::TITLE_KEY => $description,
334            MediaLink::ALIGN_KEY => $align,
335            MediaLink::LINKING_KEY => $parsedAttributes[MediaLink::LINKING_KEY],
336        );
337
338        /**
339         * Merge standard dokuwiki attributes and
340         * parsed attributes
341         */
342        $mergedAttributes = PluginUtility::mergeAttributes($dokuwikiAttributes, $parsedAttributes);
343
344        /**
345         * If this is an internal media,
346         * we are using our implementation
347         * and we have a change on attribute specification
348         */
349        if ($mediaType == MediaLink::INTERNAL_MEDIA_CALL_NAME) {
350
351            /**
352             * The align attribute on an image parse
353             * is a float right
354             * ComboStrap does a difference between a block right and a float right
355             */
356            if ($mergedAttributes[self::ALIGN_KEY] === "right") {
357                unset($mergedAttributes[self::ALIGN_KEY]);
358                $mergedAttributes[FloatAttribute::FLOAT_KEY] = "right";
359            }
360
361
362        }
363
364        return self::createFromCallStackArray($mergedAttributes);
365
366    }
367
368
369    public
370    function setLazyLoad($false): MediaLink
371    {
372        $this->lazyLoad = $false;
373        return $this;
374    }
375
376    public
377    function getLazyLoad()
378    {
379        return $this->lazyLoad;
380    }
381
382
383    /**
384     * Create a media link from a wiki id
385     *
386     *
387     * @param $wikiId - dokuwiki id
388     * @param TagAttributes|null $tagAttributes
389     * @param string|null $rev
390     * @return MediaLink
391     */
392    public
393    static function createMediaLinkFromId($wikiId, ?string $rev = '', TagAttributes $tagAttributes = null)
394    {
395        if (is_object($rev)) {
396            LogUtility::msg("rev should not be an object", LogUtility::LVL_MSG_ERROR, "support");
397        }
398        if ($tagAttributes == null) {
399            $tagAttributes = TagAttributes::createEmpty();
400        } else {
401            if (!($tagAttributes instanceof TagAttributes)) {
402                LogUtility::msg("TagAttributes is not an instance of Tag Attributes", LogUtility::LVL_MSG_ERROR, "support");
403            }
404        }
405
406        $dokuPath = DokuPath::createMediaPathFromId($wikiId, $rev);
407        return self::createMediaLinkFromPath($dokuPath, $tagAttributes);
408
409    }
410
411    /**
412     * @param Path $path
413     * @param TagAttributes|null $tagAttributes
414     * @return RasterImageLink|SvgImageLink|ThirdMediaLink
415     */
416    public static function createMediaLinkFromPath(Path $path, TagAttributes $tagAttributes = null)
417    {
418
419        if ($tagAttributes === null) {
420            $tagAttributes = TagAttributes::createEmpty();
421        }
422
423        /**
424         * Get and delete the attribute for the link
425         * (The rest is for the image)
426         */
427        $lazyLoadMethod = $tagAttributes->getValueAndRemoveIfPresent(self::LAZY_LOAD_METHOD, self::LAZY_LOAD_METHOD_LOZAD_VALUE);
428        $linking = $tagAttributes->getValueAndRemoveIfPresent(self::LINKING_KEY);
429        $linkingClass = $tagAttributes->getValueAndRemoveIfPresent(syntax_plugin_combo_media::LINK_CLASS_ATTRIBUTE);
430
431        /**
432         * Processing
433         */
434        $mime = $path->getMime();
435        if ($path->getExtension() === "svg") {
436            /**
437             * The mime type is set when uploading, not when
438             * viewing.
439             * Because they are internal image, the svg was already uploaded
440             * Therefore, no authorization scheme here
441             */
442            $mime = Mime::create(Mime::SVG);
443        }
444
445        if ($mime === null) {
446            $stringMime = self::UNKNOWN_MIME;
447        } else {
448            $stringMime = $mime->toString();
449        }
450
451        switch ($stringMime) {
452            case self::UNKNOWN_MIME:
453                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);
454                $media = new ImageRaster($path, $tagAttributes);
455                $mediaLink = new RasterImageLink($media);
456                break;
457            case Mime::SVG:
458                $media = new ImageSvg($path, $tagAttributes);
459                $mediaLink = new SvgImageLink($media);
460                break;
461            default:
462                if (!$mime->isImage()) {
463                    LogUtility::msg("The type ($mime) of media ($path) is not an image", LogUtility::LVL_MSG_DEBUG, "image");
464                    $media = new ThirdMedia($path, $tagAttributes);
465                    $mediaLink = new ThirdMediaLink($media);
466                } else {
467                    $media = new ImageRaster($path, $tagAttributes);
468                    $mediaLink = new RasterImageLink($media);
469                }
470                break;
471        }
472
473        $mediaLink
474            ->setLazyLoadMethod($lazyLoadMethod)
475            ->setLinking($linking)
476            ->setLinkingClass($linkingClass);
477        return $mediaLink;
478
479    }
480
481    public function setLazyLoadMethod(string $lazyLoadMethod): MediaLink
482    {
483        $this->lazyLoadMethod = $lazyLoadMethod;
484        return $this;
485    }
486
487
488    /**
489     * A function to set explicitly which array format
490     * is used in the returned data of a {@link SyntaxPlugin::handle()}
491     * (which ultimately is stored in the {@link CallStack)
492     *
493     * This is to make the difference with the {@link MediaLink::createFromIndexAttributes()}
494     * that is indexed by number (ie without property name)
495     *
496     *
497     * Return the same array than with the {@link self::parse()} method
498     * that is used in the {@link CallStack}
499     *
500     * @return array of key string and value
501     */
502    public
503    function toCallStackArray(): array
504    {
505        /**
506         * Trying to stay inline with the dokuwiki key
507         * We use the 'src' attributes as id
508         *
509         * src is a path (not an id)
510         */
511        $array = array(
512            PagePath::PROPERTY_NAME => $this->getMedia()->getPath()->toString(),
513            self::LINKING_KEY => $this->getLinking()
514        );
515
516
517        // Add the extra attribute
518        return array_merge($this->getMedia()->getAttributes()->toCallStackArray(), $array);
519
520
521    }
522
523
524    public
525    static function isInternalMediaSyntax($text)
526    {
527        return preg_match(' / ' . syntax_plugin_combo_media::MEDIA_PATTERN . ' / msSi', $text);
528    }
529
530
531    public
532    function __toString()
533    {
534        $media = $this->getMedia();
535        $dokuPath = $media->getPath();
536        if ($dokuPath !== null) {
537            return $dokuPath->getDokuwikiId();
538        } else {
539            return $media->__toString();
540        }
541    }
542
543
544    private
545    function getLinking()
546    {
547        return $this->linking;
548    }
549
550    private
551    function setLinking($value): MediaLink
552    {
553        $this->linking = $value;
554        return $this;
555    }
556
557    private
558    function getLinkingClass()
559    {
560        return $this->linkingClass;
561    }
562
563    private
564    function setLinkingClass($value): MediaLink
565    {
566        $this->linkingClass = $value;
567        return $this;
568    }
569
570    /**
571     * @return string - the HTML of the image inside a link if asked
572     */
573    public
574    function renderMediaTagWithLink(): string
575    {
576
577        /**
578         * Link to the media
579         *
580         */
581        $mediaLink = TagAttributes::createEmpty();
582        // https://www.dokuwiki.org/config:target
583        global $conf;
584        $target = $conf['target']['media'];
585        $mediaLink->addOutputAttributeValueIfNotEmpty("target", $target);
586        if (!empty($target)) {
587            $mediaLink->addOutputAttributeValue("rel", 'noopener');
588        }
589
590        /**
591         * Do we add a link to the image ?
592         */
593        $media = $this->getMedia();
594        $dokuPath = $media->getPath();
595        if (!($dokuPath instanceof DokuPath)) {
596            LogUtility::msg("Media Link are only supported on media from the internal library ($media)", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
597            return "";
598        }
599        $linking = $this->getLinking();
600        if ($linking === null && $dokuPath->getMime()->isImage()) {
601            $linking = PluginUtility::getConfValue(self::CONF_DEFAULT_LINKING, self::LINKING_DIRECT_VALUE);
602        }
603        switch ($linking) {
604            case self::LINKING_LINKONLY_VALUE: // show only a url
605                $src = ml(
606                    $dokuPath->getDokuwikiId(),
607                    array(
608                        'id' => $dokuPath->getDokuwikiId(),
609                        'cache' => $media->getCache(),
610                        'rev' => $dokuPath->getRevision()
611                    )
612                );
613                $mediaLink->addOutputAttributeValue("href", $src);
614                $title = $media->getTitle();
615                if (empty($title)) {
616                    $title = $media->getType();
617                }
618                return $mediaLink->toHtmlEnterTag("a") . $title . "</a>";
619            case self::LINKING_NOLINK_VALUE:
620                return $this->renderMediaTag();
621            default:
622            case self::LINKING_DIRECT_VALUE:
623                //directly to the image
624                $src = ml(
625                    $dokuPath->getDokuwikiId(),
626                    array(
627                        'id' => $dokuPath->getDokuwikiId(),
628                        'cache' => $media->getCache(),
629                        'rev' => $dokuPath->getRevision()
630                    ),
631                    true
632                );
633                $mediaLink->addOutputAttributeValue("href", $src);
634                $snippetId = "lightbox";
635                $mediaLink->addClassName("{$snippetId}-combo");
636                $linkingClass = $this->getLinkingClass();
637                if ($linkingClass !== null) {
638                    $mediaLink->addClassName($linkingClass);
639                }
640                $snippetManager = PluginUtility::getSnippetManager();
641                $snippetManager->attachJavascriptComboLibrary();
642                $snippetManager->attachInternalJavascriptForSlot("lightbox");
643                $snippetManager->attachCssInternalStyleSheetForSlot("lightbox");
644                return $mediaLink->toHtmlEnterTag("a") . $this->renderMediaTag() . "</a>";
645
646            case self::LINKING_DETAILS_VALUE:
647                //go to the details media viewer
648                $src = ml(
649                    $dokuPath->getDokuwikiId(),
650                    array(
651                        'id' => $dokuPath->getDokuwikiId(),
652                        'cache' => $media->getCache(),
653                        'rev' => $dokuPath->getRevision()
654                    ),
655                    false
656                );
657                $mediaLink->addOutputAttributeValue("href", $src);
658                return $mediaLink->toHtmlEnterTag("a") .
659                    $this->renderMediaTag() .
660                    "</a>";
661
662        }
663
664
665    }
666
667
668    /**
669     * @return string - the HTML of the image
670     */
671    public
672
673    abstract function renderMediaTag(): string;
674
675
676    /**
677     * The file
678     * @return Media
679     */
680    public function getMedia(): Media
681    {
682        return $this->media;
683    }
684
685    protected function getLazyLoadMethod(): string
686    {
687        return $this->lazyLoadMethod;
688    }
689
690
691}
692