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