|
1
|
|
|
<?php |
|
2
|
|
|
/* |
|
3
|
|
|
* Copyright (c) 2013-2016 Mark C. Prins <[email protected]> |
|
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 <[email protected]> |
|
23
|
|
|
*/ |
|
24
|
|
|
|
|
25
|
|
|
class 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, '&', 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
|
|
|
|