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