xref: /plugin/socialcards/action.php (revision 03499faa54552d0a8e07c428e654db5f00cc7412)
1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Extension\EventHandler;
5use dokuwiki\Extension\Event;
6
7/*
8 * Copyright (c) 2013-2026 Mark C. Prins <mprins@users.sf.net>
9 *
10 * Permission to use, copy, modify, and distribute this software for any
11 * purpose with or without fee is hereby granted, provided that the above
12 * copyright notice and this permission notice appear in all copies.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
15 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
16 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
17 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
18 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
19 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
20 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 */
22/**
23 * DokuWiki Plugin socialcards (Action Component).
24 *
25 * @license BSD license
26 * @author  Mark C. Prins <mprins@users.sf.net>
27 *
28 * @phpcs:disable Squiz.Classes.ValidClassName.NotPascalCase
29 */
30class action_plugin_socialcards extends ActionPlugin
31{
32    /**
33     * Register our callback for the TPL_METAHEADER_OUTPUT event.
34     *
35     * @param $controller Doku_Event_Handler
36     * @see DokuWiki_Action_Plugin::register()
37     */
38    public function register(EventHandler $controller): void
39    {
40        $controller->register_hook(
41            'TPL_METAHEADER_OUTPUT',
42            'BEFORE',
43            $this,
44            'handleTplMetaheaderOutput'
45        );
46    }
47
48    /**
49     * Retrieve metadata and add to the head of the page using appropriate meta
50     * tags unless the page does not exist.
51     *
52     * @param Event $event the DokuWiki event. $event->data is a two-dimensional
53     *                          array of all meta headers. The keys are meta, link and script.
54     *                          handler was registered (not used)
55     *
56     * @global array     $INFO
57     * @global string    $ID    page id
58     * @global array     $conf  global wiki configuration
59     * @see http://www.dokuwiki.org/devel:event:tpl_metaheader_output
60     */
61    public function handleTplMetaheaderOutput(Event $event): void
62    {
63        global $ID, $conf, $INFO;
64
65        if (!page_exists($ID)) {
66            return;
67        }
68        if (auth_quickaclcheck($ID) < AUTH_READ) {
69            return;
70        }
71
72        // twitter card, see https://dev.twitter.com/cards/markup
73        // creat a summary card, see https://dev.twitter.com/cards/types/summary
74        $event->data['meta'][] = ['name'    => 'twitter:card', 'content' => "summary"];
75        $event->data['meta'][] = ['name'    => 'twitter:site', 'content' => $this->getConf('twitterName')];
76        $event->data['meta'][] = ['name'    => 'twitter:title',
77            'content' => p_get_metadata($ID, 'title', METADATA_RENDER_USING_SIMPLE_CACHE)];
78
79        $desc = p_get_metadata($ID, 'description', METADATA_RENDER_USING_SIMPLE_CACHE);
80        if (!empty($desc)) {
81            $desc                  = str_replace("\n", " ", $desc['abstract']);
82            $event->data['meta'][] = ['name'    => 'twitter:description', 'content' => $desc];
83        }
84
85        if ($this->getConf('twitterUserName') !== '') {
86            $event->data['meta'][] = [
87                'name' => 'twitter:creator',
88                'content' => $this->getConf('twitterUserName')
89            ];
90        }
91
92        $event->data['meta'][] = ['name'    => 'twitter:image', 'content' => $this->getImage()];
93        $event->data['meta'][] = ['name'    => 'twitter:image:alt', 'content' => $this->getImageAlt()];
94
95        // opengraph, see http://ogp.me/
96        //
97        // to make this work properly the template should be modified adding the
98        // namespaces for a (x)html 4 template make html tag:
99        //
100        // <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="nl" lang="nl"
101        //       xmlns:og="http://ogp.me/ns#" xmlns:fb="http://ogp.me/ns/fb#"
102        //       xmlns:article="http://ogp.me/ns/article#" xmlns:place="http://ogp.me/ns/place#">
103        //
104        // and for a (x)html 5 template make head tag:
105        //
106        // <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#
107        //    article: http://ogp.me/ns/article# place: http://ogp.me/ns/place#">
108
109        // og namespace http://ogp.me/ns#
110        $event->data['meta'][] = ['property' => 'og:locale', 'content'  => $this->getConf('languageTerritory')];
111        $event->data['meta'][] = ['property' => 'og:site_name', 'content'  => $conf['title']];
112        $event->data['meta'][] = ['property' => 'og:url', 'content'  => wl($ID, '', true)];
113        $event->data['meta'][] = ['property' => 'og:title',
114            'content'  => p_get_metadata($ID, 'title', METADATA_RENDER_USING_SIMPLE_CACHE)];
115        if (!empty($desc)) {
116            $event->data['meta'][] = ['property' => 'og:description', 'content'  => $desc];
117        }
118        $event->data['meta'][] = ['property' => 'og:type', 'content'  => "article"];
119        $ogImage               = $this->getImage();
120        $secure                = str_starts_with($ogImage, 'https') ? ':secure_url' : '';
121        $event->data['meta'][] = ['property' => 'og:image' . $secure, 'content'  => $ogImage];
122
123        // article namespace http://ogp.me/ns/article#
124        $_dates                = p_get_metadata($ID, 'date', METADATA_RENDER_USING_SIMPLE_CACHE);
125        $event->data['meta'][] = ['property' => 'article:published_time', 'content'  => dformat($_dates['created'])];
126        $event->data['meta'][] = ['property' => 'article:modified_time', 'content'  => dformat($_dates['modified'])];
127        $event->data['meta'][] = ['property' => 'article:author', 'content'  => $INFO['editor']];
128//        $event->data['meta'][] = array(
129//            'property' => 'article:author',
130//            'content'  => p_get_metadata($ID, 'creator', METADATA_RENDER_USING_SIMPLE_CACHE),
131//        );
132//        $event->data['meta'][] = array(
133//            'property' => 'article:author',
134//            'content'  => p_get_metadata($ID, 'user', METADATA_RENDER_USING_SIMPLE_CACHE),
135//        );
136        $_subject = p_get_metadata($ID, 'subject', METADATA_RENDER_USING_SIMPLE_CACHE);
137        if (!empty($_subject)) {
138            if (!is_array($_subject)) {
139                $_subject = [$_subject];
140            }
141            foreach ($_subject as $tag) {
142                $event->data['meta'][] = ['property' => 'article:tag', 'content'  => $tag];
143            }
144        }
145
146        // place namespace http://ogp.me/ns/place#
147        $geotags = p_get_metadata($ID, 'geo', METADATA_RENDER_USING_SIMPLE_CACHE);
148        if (is_array($geotags)) {
149            $lat = $geotags['lat'] ?? 0;
150            $lon = $geotags['lon'] ?? 0;
151            if (!(empty($lat) && empty($lon))) {
152                $event->data['meta'][] = ['property' => 'place:location:latitude', 'content'  => $lat];
153                $event->data['meta'][] = ['property' => 'place:location:longitude', 'content'  => $lon];
154            }
155            // see https://developers.facebook.com/docs/opengraph/property-types/#geopoint
156            $alt = $geotags['alt'] ?? 0;
157            if (!empty($alt)) {
158                // facebook expects feet...
159                $alt                   *= 3.2808;
160                $event->data['meta'][] = ['property' => 'place:location:altitude', 'content'  => $alt];
161            }
162            /* these are not valid for the GeoPoint type..
163            $region    = $geotags['region'];
164            $country   = $geotags['country'];
165            $placename = $geotags['placename'];
166            if(!empty($region)) {
167                $event->data['meta'][] = array('property' => 'place:location:region', 'content' => $region,);
168            }
169            if(!empty($placename)) {
170                $event->data['meta'][] = array('property' => 'place:location:locality', 'content' => $placename,);
171            }
172            if(!empty($country)) {
173                $event->data['meta'][] = array('property' => 'place:location:country-name', 'content' => $country,);
174            }
175            */
176        }
177
178        // optional facebook app ID
179        $appId = $this->getConf('fbAppId');
180        if (!empty($appId)) {
181            $event->data['meta'][] = ['property' => 'fb:app_id', 'content'  => $appId];
182        }
183    }
184
185    /**
186     * Gets the canonical image path for this page.
187     *
188     * @return string the url to the image to use for this page
189     * @global string $ID page id
190     */
191    private function getImage(): string
192    {
193        global $ID;
194        $rel = p_get_metadata($ID, 'relation', METADATA_RENDER_USING_SIMPLE_CACHE);
195        $img = $rel['firstimage'];
196
197        if (empty($img)) {
198            $img = $this->getConf('fallbackImage');
199            if (str_starts_with($img, "http")) {
200                // don't use ml() as this results in a HTTP redirect after
201                //   hitting the wiki making the card image fail.
202                return $img;
203            }
204        }
205
206        return ml($img, [], true, '&amp;', true);
207    }
208
209    /**
210     * Gets the alt text for this page image.
211     *
212     * @return string alt text
213     * @global string $ID page id
214     */
215    private function getImageAlt(): string
216    {
217        global $ID;
218        $rel   = p_get_metadata($ID, 'relation', METADATA_RENDER_USING_SIMPLE_CACHE);
219        $imgID = $rel['firstimage'];
220        $alt   = "";
221
222        if (!empty($imgID)) {
223            require_once(DOKU_INC . 'inc/JpegMeta.php');
224            $jpegmeta = new JpegMeta(mediaFN($imgID));
225            $tags     = ['IPTC.Caption', 'EXIF.UserComment', 'EXIF.TIFFImageDescription',
226                'EXIF.TIFFUserComment', 'IPTC.Headline', 'Xmp.dc:title'];
227            $alt      = media_getTag($tags, $jpegmeta);
228        }
229        return htmlspecialchars($alt);
230    }
231}
232