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, '&', 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