summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/meta.pm
blob: 34e902bec4d19d98730612e6ee78cc157ce7299d (plain)
  1. #!/usr/bin/perl
  2. # Ikiwiki metadata plugin.
  3. package IkiWiki::Plugin::meta;
  4. use warnings;
  5. use strict;
  6. use IkiWiki 3.00;
  7. my %metaheaders;
  8. sub import {
  9. hook(type => "getsetup", id => "meta", call => \&getsetup);
  10. hook(type => "needsbuild", id => "meta", call => \&needsbuild);
  11. hook(type => "preprocess", id => "meta", call => \&preprocess, scan => 1);
  12. hook(type => "pagetemplate", id => "meta", call => \&pagetemplate);
  13. }
  14. sub getsetup () {
  15. return
  16. plugin => {
  17. safe => 1,
  18. rebuild => undef,
  19. section => "core",
  20. },
  21. }
  22. sub needsbuild (@) {
  23. my $needsbuild=shift;
  24. foreach my $page (keys %pagestate) {
  25. if (exists $pagestate{$page}{meta}) {
  26. if (exists $pagesources{$page} &&
  27. grep { $_ eq $pagesources{$page} } @$needsbuild) {
  28. # remove state, it will be re-added
  29. # if the preprocessor directive is still
  30. # there during the rebuild
  31. delete $pagestate{$page}{meta};
  32. }
  33. }
  34. }
  35. }
  36. sub scrub ($$) {
  37. if (IkiWiki::Plugin::htmlscrubber->can("sanitize")) {
  38. return IkiWiki::Plugin::htmlscrubber::sanitize(
  39. content => shift, destpage => shift);
  40. }
  41. else {
  42. return shift;
  43. }
  44. }
  45. sub safeurl ($) {
  46. my $url=shift;
  47. if (exists $IkiWiki::Plugin::htmlscrubber::{safe_url_regexp} &&
  48. defined $IkiWiki::Plugin::htmlscrubber::safe_url_regexp) {
  49. return $url=~/$IkiWiki::Plugin::htmlscrubber::safe_url_regexp/;
  50. }
  51. else {
  52. return 1;
  53. }
  54. }
  55. sub htmlize ($$$) {
  56. my $page = shift;
  57. my $destpage = shift;
  58. return IkiWiki::htmlize($page, $destpage, pagetype($pagesources{$page}),
  59. IkiWiki::linkify($page, $destpage,
  60. IkiWiki::preprocess($page, $destpage, shift)));
  61. }
  62. sub preprocess (@) {
  63. return "" unless @_;
  64. my %params=@_;
  65. my $key=shift;
  66. my $value=$params{$key};
  67. delete $params{$key};
  68. my $page=$params{page};
  69. delete $params{page};
  70. my $destpage=$params{destpage};
  71. delete $params{destpage};
  72. delete $params{preview};
  73. eval q{use HTML::Entities};
  74. # Always decode, even if encoding later, since it might not be
  75. # fully encoded.
  76. $value=decode_entities($value);
  77. # Metadata collection that needs to happen during the scan pass.
  78. if ($key eq 'title') {
  79. my $encoded = HTML::Entities::encode_numeric($value);
  80. $pagestate{$page}{meta}{title} = $encoded;
  81. if (exists $params{sortas}) {
  82. $pagestate{$page}{meta}{titlesort}=$params{sortas};
  83. }
  84. elsif ($encoded ne $value) {
  85. $pagestate{$page}{meta}{titlesort}=$value;
  86. }
  87. else {
  88. delete $pagestate{$page}{meta}{titlesort};
  89. }
  90. return "";
  91. }
  92. elsif ($key eq 'description') {
  93. $pagestate{$page}{meta}{description}=HTML::Entities::encode_numeric($value);
  94. # fallthrough
  95. }
  96. elsif ($key eq 'guid') {
  97. $pagestate{$page}{meta}{guid}=HTML::Entities::encode_numeric($value);
  98. # fallthrough
  99. }
  100. elsif ($key eq 'license') {
  101. push @{$metaheaders{$page}}, '<link rel="license" href="#page_license" />';
  102. $pagestate{$page}{meta}{license}=$value;
  103. return "";
  104. }
  105. elsif ($key eq 'copyright') {
  106. push @{$metaheaders{$page}}, '<link rel="copyright" href="#page_copyright" />';
  107. $pagestate{$page}{meta}{copyright}=$value;
  108. return "";
  109. }
  110. elsif ($key eq 'link' && ! %params) {
  111. # hidden WikiLink
  112. add_link($page, $value);
  113. return "";
  114. }
  115. elsif ($key eq 'author') {
  116. $pagestate{$page}{meta}{author}=$value;
  117. if (exists $params{sortas}) {
  118. $pagestate{$page}{meta}{authorsort}=$params{sortas};
  119. }
  120. else {
  121. delete $pagestate{$page}{meta}{authorsort};
  122. }
  123. # fallthorough
  124. }
  125. elsif ($key eq 'authorurl') {
  126. $pagestate{$page}{meta}{authorurl}=$value if safeurl($value);
  127. # fallthrough
  128. }
  129. elsif ($key eq 'permalink') {
  130. $pagestate{$page}{meta}{permalink}=$value if safeurl($value);
  131. # fallthrough
  132. }
  133. elsif ($key eq 'date') {
  134. eval q{use Date::Parse};
  135. if (! $@) {
  136. my $time = str2time($value);
  137. $IkiWiki::pagectime{$page}=$time if defined $time;
  138. }
  139. }
  140. elsif ($key eq 'updated') {
  141. eval q{use Date::Parse};
  142. if (! $@) {
  143. my $time = str2time($value);
  144. $pagestate{$page}{meta}{updated}=$time if defined $time;
  145. }
  146. }
  147. if (! defined wantarray) {
  148. # avoid collecting duplicate data during scan pass
  149. return;
  150. }
  151. # Metadata handling that happens only during preprocessing pass.
  152. if ($key eq 'permalink') {
  153. if (safeurl($value)) {
  154. push @{$metaheaders{$page}}, scrub('<link rel="bookmark" href="'.encode_entities($value).'" />', $destpage);
  155. }
  156. }
  157. elsif ($key eq 'stylesheet') {
  158. my $rel=exists $params{rel} ? $params{rel} : "alternate stylesheet";
  159. my $title=exists $params{title} ? $params{title} : $value;
  160. # adding .css to the value prevents using any old web
  161. # editable page as a stylesheet
  162. my $stylesheet=bestlink($page, $value.".css");
  163. if (! length $stylesheet) {
  164. error gettext("stylesheet not found")
  165. }
  166. push @{$metaheaders{$page}}, '<link href="'.urlto($stylesheet, $page).
  167. '" rel="'.encode_entities($rel).
  168. '" title="'.encode_entities($title).
  169. "\" type=\"text/css\" />";
  170. }
  171. elsif ($key eq 'openid') {
  172. my $delegate=0; # both by default
  173. if (exists $params{delegate}) {
  174. $delegate = 1 if lc $params{delegate} eq 'openid';
  175. $delegate = 2 if lc $params{delegate} eq 'openid2';
  176. }
  177. if (exists $params{server} && safeurl($params{server})) {
  178. push @{$metaheaders{$page}}, '<link href="'.encode_entities($params{server}).
  179. '" rel="openid.server" />' if $delegate ne 2;
  180. push @{$metaheaders{$page}}, '<link href="'.encode_entities($params{server}).
  181. '" rel="openid2.provider" />' if $delegate ne 1;
  182. }
  183. if (safeurl($value)) {
  184. push @{$metaheaders{$page}}, '<link href="'.encode_entities($value).
  185. '" rel="openid.delegate" />' if $delegate ne 2;
  186. push @{$metaheaders{$page}}, '<link href="'.encode_entities($value).
  187. '" rel="openid2.local_id" />' if $delegate ne 1;
  188. }
  189. if (exists $params{"xrds-location"} && safeurl($params{"xrds-location"})) {
  190. push @{$metaheaders{$page}}, '<meta http-equiv="X-XRDS-Location"'.
  191. 'content="'.encode_entities($params{"xrds-location"}).'" />';
  192. }
  193. }
  194. elsif ($key eq 'redir') {
  195. return "" if $page ne $destpage;
  196. my $safe=0;
  197. if ($value !~ /^\w+:\/\//) {
  198. my ($redir_page, $redir_anchor) = split /\#/, $value;
  199. my $link=bestlink($page, $redir_page);
  200. if (! length $link) {
  201. error gettext("redir page not found")
  202. }
  203. add_depends($page, $link, deptype("presence"));
  204. $value=urlto($link, $page);
  205. $value.='#'.$redir_anchor if defined $redir_anchor;
  206. $safe=1;
  207. # redir cycle detection
  208. $pagestate{$page}{meta}{redir}=$link;
  209. my $at=$page;
  210. my %seen;
  211. while (exists $pagestate{$at}{meta}{redir}) {
  212. if ($seen{$at}) {
  213. error gettext("redir cycle is not allowed")
  214. }
  215. $seen{$at}=1;
  216. $at=$pagestate{$at}{meta}{redir};
  217. }
  218. }
  219. else {
  220. $value=encode_entities($value);
  221. }
  222. my $delay=int(exists $params{delay} ? $params{delay} : 0);
  223. my $redir="<meta http-equiv=\"refresh\" content=\"$delay; URL=$value\" />";
  224. if (! $safe) {
  225. $redir=scrub($redir, $destpage);
  226. }
  227. push @{$metaheaders{$page}}, $redir;
  228. }
  229. elsif ($key eq 'link') {
  230. if (%params) {
  231. push @{$metaheaders{$page}}, scrub("<link href=\"".encode_entities($value)."\" ".
  232. join(" ", map {
  233. encode_entities($_)."=\"".encode_entities(decode_entities($params{$_}))."\""
  234. } keys %params).
  235. " />\n", $destpage);
  236. }
  237. }
  238. elsif ($key eq 'robots') {
  239. push @{$metaheaders{$page}}, '<meta name="robots"'.
  240. ' content="'.encode_entities($value).'" />';
  241. }
  242. elsif ($key eq 'description') {
  243. push @{$metaheaders{$page}}, '<meta name="'.encode_entities($key).
  244. '" content="'.encode_entities($value).'" />';
  245. }
  246. else {
  247. push @{$metaheaders{$page}}, scrub('<meta name="'.encode_entities($key).
  248. '" content="'.encode_entities($value).'" />', $destpage);
  249. }
  250. return "";
  251. }
  252. sub pagetemplate (@) {
  253. my %params=@_;
  254. my $page=$params{page};
  255. my $destpage=$params{destpage};
  256. my $template=$params{template};
  257. if (exists $metaheaders{$page} && $template->query(name => "meta")) {
  258. # avoid duplicate meta lines
  259. my %seen;
  260. $template->param(meta => join("\n", grep { (! $seen{$_}) && ($seen{$_}=1) } @{$metaheaders{$page}}));
  261. }
  262. if (exists $pagestate{$page}{meta}{title} && $template->query(name => "title")) {
  263. $template->param(title => $pagestate{$page}{meta}{title});
  264. $template->param(title_overridden => 1);
  265. }
  266. foreach my $field (qw{author authorurl description permalink}) {
  267. $template->param($field => $pagestate{$page}{meta}{$field})
  268. if exists $pagestate{$page}{meta}{$field} && $template->query(name => $field);
  269. }
  270. foreach my $field (qw{license copyright}) {
  271. if (exists $pagestate{$page}{meta}{$field} && $template->query(name => $field) &&
  272. ($page eq $destpage || ! exists $pagestate{$destpage}{meta}{$field} ||
  273. $pagestate{$page}{meta}{$field} ne $pagestate{$destpage}{meta}{$field})) {
  274. $template->param($field => htmlize($page, $destpage, $pagestate{$page}{meta}{$field}));
  275. }
  276. }
  277. }
  278. sub get_sort_key {
  279. my $page = $_[0];
  280. my $meta = $_[1];
  281. # e.g. titlesort (also makes sense for author)
  282. my $key = $pagestate{$page}{meta}{$meta . "sort"};
  283. return $key if defined $key;
  284. # e.g. title
  285. $key = $pagestate{$page}{meta}{$meta};
  286. return $key if defined $key;
  287. # fall back to closer-to-core things
  288. if ($meta eq 'title') {
  289. return pagetitle(IkiWiki::basename($page));
  290. }
  291. elsif ($meta eq 'date') {
  292. return $IkiWiki::pagectime{$page};
  293. }
  294. elsif ($meta eq 'updated') {
  295. return $IkiWiki::pagemtime{$page};
  296. }
  297. else {
  298. return '';
  299. }
  300. }
  301. sub match {
  302. my $field=shift;
  303. my $page=shift;
  304. # turn glob into a safe regexp
  305. my $re=IkiWiki::glob2re(shift);
  306. my $val;
  307. if (exists $pagestate{$page}{meta}{$field}) {
  308. $val=$pagestate{$page}{meta}{$field};
  309. }
  310. elsif ($field eq 'title') {
  311. $val = pagetitle($page);
  312. }
  313. if (defined $val) {
  314. if ($val=~/^$re$/i) {
  315. return IkiWiki::SuccessReason->new("$re matches $field of $page", $page => $IkiWiki::DEPEND_CONTENT, "" => 1);
  316. }
  317. else {
  318. return IkiWiki::FailReason->new("$re does not match $field of $page", "" => 1);
  319. }
  320. }
  321. else {
  322. return IkiWiki::FailReason->new("$page does not have a $field", "" => 1);
  323. }
  324. }
  325. package IkiWiki::PageSpec;
  326. sub match_title ($$;@) {
  327. IkiWiki::Plugin::meta::match("title", @_);
  328. }
  329. sub match_author ($$;@) {
  330. IkiWiki::Plugin::meta::match("author", @_);
  331. }
  332. sub match_authorurl ($$;@) {
  333. IkiWiki::Plugin::meta::match("authorurl", @_);
  334. }
  335. sub match_license ($$;@) {
  336. IkiWiki::Plugin::meta::match("license", @_);
  337. }
  338. sub match_copyright ($$;@) {
  339. IkiWiki::Plugin::meta::match("copyright", @_);
  340. }
  341. package IkiWiki::SortSpec;
  342. sub cmp_meta {
  343. my $meta = $_[0];
  344. error(gettext("sort=meta requires a parameter")) unless defined $meta;
  345. if ($meta eq 'updated' || $meta eq 'date') {
  346. return IkiWiki::Plugin::meta::get_sort_key($a, $meta)
  347. <=>
  348. IkiWiki::Plugin::meta::get_sort_key($b, $meta);
  349. }
  350. return IkiWiki::Plugin::meta::get_sort_key($a, $meta)
  351. cmp
  352. IkiWiki::Plugin::meta::get_sort_key($b, $meta);
  353. }
  354. # A prototype of how sort=title could behave in 4.0 or something
  355. sub cmp_meta_title {
  356. $_[0] = 'title';
  357. return cmp_meta(@_);
  358. }
  359. 1