1fdb4710bSMark Prins<?php 27e4aad61Sgithub-actions[bot] 37e4aad61Sgithub-actions[bot]use dokuwiki\Extension\ActionPlugin; 47e4aad61Sgithub-actions[bot]use dokuwiki\Extension\EventHandler; 57e4aad61Sgithub-actions[bot]use dokuwiki\Extension\Event; 67e4aad61Sgithub-actions[bot] 7635a5e2dSMark Prins/* 8*03499faaSMark Prins * Copyright (c) 2013-2026 Mark C. Prins <mprins@users.sf.net> 9fdb4710bSMark Prins * 10fdb4710bSMark Prins * Permission to use, copy, modify, and distribute this software for any 11fdb4710bSMark Prins * purpose with or without fee is hereby granted, provided that the above 12fdb4710bSMark Prins * copyright notice and this permission notice appear in all copies. 13fdb4710bSMark Prins * 14fdb4710bSMark Prins * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 15fdb4710bSMark Prins * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 16fdb4710bSMark Prins * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 17fdb4710bSMark Prins * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 18fdb4710bSMark Prins * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 19fdb4710bSMark Prins * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 20fdb4710bSMark Prins * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21635a5e2dSMark Prins */ 22ae2f5c78SMark Prins/** 23ae2f5c78SMark Prins * DokuWiki Plugin socialcards (Action Component). 24ae2f5c78SMark Prins * 25ae2f5c78SMark Prins * @license BSD license 26ae2f5c78SMark Prins * @author Mark C. Prins <mprins@users.sf.net> 27*03499faaSMark Prins * 28*03499faaSMark Prins * @phpcs:disable Squiz.Classes.ValidClassName.NotPascalCase 29ae2f5c78SMark Prins */ 307e4aad61Sgithub-actions[bot]class action_plugin_socialcards extends ActionPlugin 317e4aad61Sgithub-actions[bot]{ 32ae2f5c78SMark Prins /** 33ae2f5c78SMark Prins * Register our callback for the TPL_METAHEADER_OUTPUT event. 34ae2f5c78SMark Prins * 35ae2f5c78SMark Prins * @param $controller Doku_Event_Handler 36ae2f5c78SMark Prins * @see DokuWiki_Action_Plugin::register() 37ae2f5c78SMark Prins */ 387e4aad61Sgithub-actions[bot] public function register(EventHandler $controller): void 397e4aad61Sgithub-actions[bot] { 403cc863c5SMark Prins $controller->register_hook( 413cc863c5SMark Prins 'TPL_METAHEADER_OUTPUT', 423cc863c5SMark Prins 'BEFORE', 433cc863c5SMark Prins $this, 443cc863c5SMark Prins 'handleTplMetaheaderOutput' 453cc863c5SMark Prins ); 46fdb4710bSMark Prins } 47fdb4710bSMark Prins 48fdb4710bSMark Prins /** 4989906951SMark Prins * Retrieve metadata and add to the head of the page using appropriate meta 50156465eeSMark Prins * tags unless the page does not exist. 5189906951SMark Prins * 52c3673c20Sgithub-actions[bot] * @param Event $event the DokuWiki event. $event->data is a two-dimensional 53fdb4710bSMark Prins * array of all meta headers. The keys are meta, link and script. 542765f2acSMark Prins * handler was registered (not used) 5589906951SMark Prins * 563cc863c5SMark Prins * @global array $INFO 573cc863c5SMark Prins * @global string $ID page id 583cc863c5SMark Prins * @global array $conf global wiki configuration 5989906951SMark Prins * @see http://www.dokuwiki.org/devel:event:tpl_metaheader_output 60fdb4710bSMark Prins */ 61*03499faaSMark Prins public function handleTplMetaheaderOutput(Event $event): void 627e4aad61Sgithub-actions[bot] { 6389906951SMark Prins global $ID, $conf, $INFO; 64fdb4710bSMark Prins 653cc863c5SMark Prins if (!page_exists($ID)) { 663cc863c5SMark Prins return; 673cc863c5SMark Prins } 68a67fa79eSMark Prins if (auth_quickaclcheck($ID) < AUTH_READ) { 69a67fa79eSMark Prins return; 70a67fa79eSMark Prins } 71156465eeSMark Prins 722765f2acSMark Prins // twitter card, see https://dev.twitter.com/cards/markup 732765f2acSMark Prins // creat a summary card, see https://dev.twitter.com/cards/types/summary 747e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['name' => 'twitter:card', 'content' => "summary"]; 757e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['name' => 'twitter:site', 'content' => $this->getConf('twitterName')]; 76*03499faaSMark Prins $event->data['meta'][] = ['name' => 'twitter:title', 77*03499faaSMark Prins 'content' => p_get_metadata($ID, 'title', METADATA_RENDER_USING_SIMPLE_CACHE)]; 782765f2acSMark Prins 79eecf9074SMark Prins $desc = p_get_metadata($ID, 'description', METADATA_RENDER_USING_SIMPLE_CACHE); 80aeb704feSMark Prins if (!empty($desc)) { 8189906951SMark Prins $desc = str_replace("\n", " ", $desc['abstract']); 827e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['name' => 'twitter:description', 'content' => $desc]; 83fc03d0b3SMark Prins } 842765f2acSMark Prins 85fa5b18beSMark Prins if ($this->getConf('twitterUserName') !== '') { 86*03499faaSMark Prins $event->data['meta'][] = [ 87*03499faaSMark Prins 'name' => 'twitter:creator', 88*03499faaSMark Prins 'content' => $this->getConf('twitterUserName') 89*03499faaSMark Prins ]; 90e0737b20SMark Prins } 912765f2acSMark Prins 927e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['name' => 'twitter:image', 'content' => $this->getImage()]; 937e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['name' => 'twitter:image:alt', 'content' => $this->getImageAlt()]; 9489906951SMark Prins 9589906951SMark Prins // opengraph, see http://ogp.me/ 9689906951SMark Prins // 9789906951SMark Prins // to make this work properly the template should be modified adding the 9889906951SMark Prins // namespaces for a (x)html 4 template make html tag: 9989906951SMark Prins // 10089906951SMark Prins // <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="nl" lang="nl" 10189906951SMark Prins // xmlns:og="http://ogp.me/ns#" xmlns:fb="http://ogp.me/ns/fb#" 10289906951SMark Prins // xmlns:article="http://ogp.me/ns/article#" xmlns:place="http://ogp.me/ns/place#"> 10389906951SMark Prins // 10489906951SMark Prins // and for a (x)html 5 template make head tag: 10589906951SMark Prins // 1065cd64445SMark Prins // <head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# 1075cd64445SMark Prins // article: http://ogp.me/ns/article# place: http://ogp.me/ns/place#"> 10889906951SMark Prins 10989906951SMark Prins // og namespace http://ogp.me/ns# 1107e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'og:locale', 'content' => $this->getConf('languageTerritory')]; 1117e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'og:site_name', 'content' => $conf['title']]; 1127e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'og:url', 'content' => wl($ID, '', true)]; 113*03499faaSMark Prins $event->data['meta'][] = ['property' => 'og:title', 114*03499faaSMark Prins 'content' => p_get_metadata($ID, 'title', METADATA_RENDER_USING_SIMPLE_CACHE)]; 11589906951SMark Prins if (!empty($desc)) { 1167e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'og:description', 'content' => $desc]; 117fdb4710bSMark Prins } 1187e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'og:type', 'content' => "article"]; 119f3501156SLaurent Marquet $ogImage = $this->getImage(); 120*03499faaSMark Prins $secure = str_starts_with($ogImage, 'https') ? ':secure_url' : ''; 1217e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'og:image' . $secure, 'content' => $ogImage]; 12289906951SMark Prins 12389906951SMark Prins // article namespace http://ogp.me/ns/article# 124eecf9074SMark Prins $_dates = p_get_metadata($ID, 'date', METADATA_RENDER_USING_SIMPLE_CACHE); 1257e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'article:published_time', 'content' => dformat($_dates['created'])]; 1267e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'article:modified_time', 'content' => dformat($_dates['modified'])]; 1277e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'article:author', 'content' => $INFO['editor']]; 1283ab78d94SMark Prins// $event->data['meta'][] = array( 1293ab78d94SMark Prins// 'property' => 'article:author', 130eecf9074SMark Prins// 'content' => p_get_metadata($ID, 'creator', METADATA_RENDER_USING_SIMPLE_CACHE), 1313ab78d94SMark Prins// ); 1323ab78d94SMark Prins// $event->data['meta'][] = array( 1333ab78d94SMark Prins// 'property' => 'article:author', 134eecf9074SMark Prins// 'content' => p_get_metadata($ID, 'user', METADATA_RENDER_USING_SIMPLE_CACHE), 1353ab78d94SMark Prins// ); 136eecf9074SMark Prins $_subject = p_get_metadata($ID, 'subject', METADATA_RENDER_USING_SIMPLE_CACHE); 13789906951SMark Prins if (!empty($_subject)) { 13889906951SMark Prins if (!is_array($_subject)) { 1397e4aad61Sgithub-actions[bot] $_subject = [$_subject]; 14089906951SMark Prins } 14189906951SMark Prins foreach ($_subject as $tag) { 1427e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'article:tag', 'content' => $tag]; 14389906951SMark Prins } 14489906951SMark Prins } 14589906951SMark Prins 14689906951SMark Prins // place namespace http://ogp.me/ns/place# 147eecf9074SMark Prins $geotags = p_get_metadata($ID, 'geo', METADATA_RENDER_USING_SIMPLE_CACHE); 1482ebecb01SMark Prins if (is_array($geotags)) { 1492ebecb01SMark Prins $lat = $geotags['lat'] ?? 0; 1502ebecb01SMark Prins $lon = $geotags['lon'] ?? 0; 15189906951SMark Prins if (!(empty($lat) && empty($lon))) { 1527e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'place:location:latitude', 'content' => $lat]; 1537e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'place:location:longitude', 'content' => $lon]; 1543322a5daSMark Prins } 1553322a5daSMark Prins // see https://developers.facebook.com/docs/opengraph/property-types/#geopoint 1562ebecb01SMark Prins $alt = $geotags['alt'] ?? 0; 1573322a5daSMark Prins if (!empty($alt)) { 1583322a5daSMark Prins // facebook expects feet... 159fa5b18beSMark Prins $alt *= 3.2808; 1607e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'place:location:altitude', 'content' => $alt]; 16189906951SMark Prins } 162ae2f5c78SMark Prins /* these are not valid for the GeoPoint type.. 16389906951SMark Prins $region = $geotags['region']; 16489906951SMark Prins $country = $geotags['country']; 16589906951SMark Prins $placename = $geotags['placename']; 1663ab78d94SMark Prins if(!empty($region)) { 1673ab78d94SMark Prins $event->data['meta'][] = array('property' => 'place:location:region', 'content' => $region,); 1683ab78d94SMark Prins } 1693ab78d94SMark Prins if(!empty($placename)) { 1703ab78d94SMark Prins $event->data['meta'][] = array('property' => 'place:location:locality', 'content' => $placename,); 1713ab78d94SMark Prins } 1723ab78d94SMark Prins if(!empty($country)) { 1733ab78d94SMark Prins $event->data['meta'][] = array('property' => 'place:location:country-name', 'content' => $country,); 1743ab78d94SMark Prins } 17589906951SMark Prins */ 1762ebecb01SMark Prins } 17789906951SMark Prins 17889906951SMark Prins // optional facebook app ID 17989906951SMark Prins $appId = $this->getConf('fbAppId'); 18089906951SMark Prins if (!empty($appId)) { 1817e4aad61Sgithub-actions[bot] $event->data['meta'][] = ['property' => 'fb:app_id', 'content' => $appId]; 18289906951SMark Prins } 18389906951SMark Prins } 18489906951SMark Prins 18589906951SMark Prins /** 18689906951SMark Prins * Gets the canonical image path for this page. 18789906951SMark Prins * 18889906951SMark Prins * @return string the url to the image to use for this page 1893cc863c5SMark Prins * @global string $ID page id 19089906951SMark Prins */ 1917e4aad61Sgithub-actions[bot] private function getImage(): string 1927e4aad61Sgithub-actions[bot] { 19389906951SMark Prins global $ID; 194eecf9074SMark Prins $rel = p_get_metadata($ID, 'relation', METADATA_RENDER_USING_SIMPLE_CACHE); 19589906951SMark Prins $img = $rel['firstimage']; 196ae2f5c78SMark Prins 197ae2f5c78SMark Prins if (empty($img)) { 19893bf3c56SMark Prins $img = $this->getConf('fallbackImage'); 199*03499faaSMark Prins if (str_starts_with($img, "http")) { 2003efb311dSMark Prins // don't use ml() as this results in a HTTP redirect after 2013efb311dSMark Prins // hitting the wiki making the card image fail. 20293bf3c56SMark Prins return $img; 20393bf3c56SMark Prins } 20493bf3c56SMark Prins } 205ae2f5c78SMark Prins 2067e4aad61Sgithub-actions[bot] return ml($img, [], true, '&', true); 20789906951SMark Prins } 20889906951SMark Prins 209a543e5b7SMark Prins /** 210a543e5b7SMark Prins * Gets the alt text for this page image. 211a543e5b7SMark Prins * 212a543e5b7SMark Prins * @return string alt text 2133cc863c5SMark Prins * @global string $ID page id 214a543e5b7SMark Prins */ 2157e4aad61Sgithub-actions[bot] private function getImageAlt(): string 2167e4aad61Sgithub-actions[bot] { 217a543e5b7SMark Prins global $ID; 218eecf9074SMark Prins $rel = p_get_metadata($ID, 'relation', METADATA_RENDER_USING_SIMPLE_CACHE); 219a543e5b7SMark Prins $imgID = $rel['firstimage']; 220a543e5b7SMark Prins $alt = ""; 221a543e5b7SMark Prins 222a543e5b7SMark Prins if (!empty($imgID)) { 223a543e5b7SMark Prins require_once(DOKU_INC . 'inc/JpegMeta.php'); 224a543e5b7SMark Prins $jpegmeta = new JpegMeta(mediaFN($imgID)); 225*03499faaSMark Prins $tags = ['IPTC.Caption', 'EXIF.UserComment', 'EXIF.TIFFImageDescription', 226*03499faaSMark Prins 'EXIF.TIFFUserComment', 'IPTC.Headline', 'Xmp.dc:title']; 227*03499faaSMark Prins $alt = media_getTag($tags, $jpegmeta); 228a543e5b7SMark Prins } 229a543e5b7SMark Prins return htmlspecialchars($alt); 230a543e5b7SMark Prins } 231fdb4710bSMark Prins} 232