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