summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/meta.pm
blob: cd7d0d1277a47d4fcdd889961e81204d2a470010 (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. $pagestate{$page}{meta}{title}=HTML::Entities::encode_numeric($value);
  80. if (exists $params{sort}) {
  81. $pagestate{$page}{meta}{titlesort}=$params{sort};
  82. }
  83. else {
  84. $pagestate{$page}{meta}{titlesort}=$value;
  85. }
  86. return "";
  87. }
  88. elsif ($key eq 'description') {
  89. $pagestate{$page}{meta}{description}=HTML::Entities::encode_numeric($value);
  90. # fallthrough
  91. }
  92. elsif ($key eq 'guid') {
  93. $pagestate{$page}{meta}{guid}=HTML::Entities::encode_numeric($value);
  94. # fallthrough
  95. }
  96. elsif ($key eq 'license') {
  97. push @{$metaheaders{$page}}, '<link rel="license" href="#page_license" />';
  98. $pagestate{$page}{meta}{license}=$value;
  99. return "";
  100. }
  101. elsif ($key eq 'copyright') {
  102. push @{$metaheaders{$page}}, '<link rel="copyright" href="#page_copyright" />';
  103. $pagestate{$page}{meta}{copyright}=$value;
  104. return "";
  105. }
  106. elsif ($key eq 'link' && ! %params) {
  107. # hidden WikiLink
  108. add_link($page, $value);
  109. return "";
  110. }
  111. elsif ($key eq 'author') {
  112. $pagestate{$page}{meta}{author}=$value;
  113. # fallthorough
  114. }
  115. elsif ($key eq 'authorurl') {
  116. $pagestate{$page}{meta}{authorurl}=$value if safeurl($value);
  117. # fallthrough
  118. }
  119. elsif ($key eq 'permalink') {
  120. $pagestate{$page}{meta}{permalink}=$value if safeurl($value);
  121. # fallthrough
  122. }
  123. elsif ($key eq 'date') {
  124. eval q{use Date::Parse};
  125. if (! $@) {
  126. my $time = str2time($value);
  127. $IkiWiki::pagectime{$page}=$time if defined $time;
  128. }
  129. }
  130. elsif ($key eq 'updated') {
  131. eval q{use Date::Parse};
  132. if (! $@) {
  133. my $time = str2time($value);
  134. $pagestate{$page}{meta}{updated}=$time if defined $time;
  135. }
  136. }
  137. if (! defined wantarray) {
  138. # avoid collecting duplicate data during scan pass
  139. return;
  140. }
  141. # Metadata handling that happens only during preprocessing pass.
  142. if ($key eq 'permalink') {
  143. if (safeurl($value)) {
  144. push @{$metaheaders{$page}}, scrub('<link rel="bookmark" href="'.encode_entities($value).'" />', $destpage);
  145. }
  146. }
  147. elsif ($key eq 'stylesheet') {
  148. my $rel=exists $params{rel} ? $params{rel} : "alternate stylesheet";
  149. my $title=exists $params{title} ? $params{title} : $value;
  150. # adding .css to the value prevents using any old web
  151. # editable page as a stylesheet
  152. my $stylesheet=bestlink($page, $value.".css");
  153. if (! length $stylesheet) {
  154. error gettext("stylesheet not found")
  155. }
  156. push @{$metaheaders{$page}}, '<link href="'.urlto($stylesheet, $page).
  157. '" rel="'.encode_entities($rel).
  158. '" title="'.encode_entities($title).
  159. "\" type=\"text/css\" />";
  160. }
  161. elsif ($key eq 'openid') {
  162. my $delegate=0; # both by default
  163. if (exists $params{delegate}) {
  164. $delegate = 1 if lc $params{delegate} eq 'openid';
  165. $delegate = 2 if lc $params{delegate} eq 'openid2';
  166. }
  167. if (exists $params{server} && safeurl($params{server})) {
  168. push @{$metaheaders{$page}}, '<link href="'.encode_entities($params{server}).
  169. '" rel="openid.server" />' if $delegate ne 2;
  170. push @{$metaheaders{$page}}, '<link href="'.encode_entities($params{server}).
  171. '" rel="openid2.provider" />' if $delegate ne 1;
  172. }
  173. if (safeurl($value)) {
  174. push @{$metaheaders{$page}}, '<link href="'.encode_entities($value).
  175. '" rel="openid.delegate" />' if $delegate ne 2;
  176. push @{$metaheaders{$page}}, '<link href="'.encode_entities($value).
  177. '" rel="openid2.local_id" />' if $delegate ne 1;
  178. }
  179. if (exists $params{"xrds-location"} && safeurl($params{"xrds-location"})) {
  180. push @{$metaheaders{$page}}, '<meta http-equiv="X-XRDS-Location"'.
  181. 'content="'.encode_entities($params{"xrds-location"}).'" />';
  182. }
  183. }
  184. elsif ($key eq 'redir') {
  185. return "" if $page ne $destpage;
  186. my $safe=0;
  187. if ($value !~ /^\w+:\/\//) {
  188. my ($redir_page, $redir_anchor) = split /\#/, $value;
  189. my $link=bestlink($page, $redir_page);
  190. if (! length $link) {
  191. error gettext("redir page not found")
  192. }
  193. add_depends($page, $link, deptype("presence"));
  194. $value=urlto($link, $page);
  195. $value.='#'.$redir_anchor if defined $redir_anchor;
  196. $safe=1;
  197. # redir cycle detection
  198. $pagestate{$page}{meta}{redir}=$link;
  199. my $at=$page;
  200. my %seen;
  201. while (exists $pagestate{$at}{meta}{redir}) {
  202. if ($seen{$at}) {
  203. error gettext("redir cycle is not allowed")
  204. }
  205. $seen{$at}=1;
  206. $at=$pagestate{$at}{meta}{redir};
  207. }
  208. }
  209. else {
  210. $value=encode_entities($value);
  211. }
  212. my $delay=int(exists $params{delay} ? $params{delay} : 0);
  213. my $redir="<meta http-equiv=\"refresh\" content=\"$delay; URL=$value\" />";
  214. if (! $safe) {
  215. $redir=scrub($redir, $destpage);
  216. }
  217. push @{$metaheaders{$page}}, $redir;
  218. }
  219. elsif ($key eq 'link') {
  220. if (%params) {
  221. push @{$metaheaders{$page}}, scrub("<link href=\"".encode_entities($value)."\" ".
  222. join(" ", map {
  223. encode_entities($_)."=\"".encode_entities(decode_entities($params{$_}))."\""
  224. } keys %params).
  225. " />\n", $destpage);
  226. }
  227. }
  228. elsif ($key eq 'robots') {
  229. push @{$metaheaders{$page}}, '<meta name="robots"'.
  230. ' content="'.encode_entities($value).'" />';
  231. }
  232. elsif ($key eq 'description') {
  233. push @{$metaheaders{$page}}, '<meta name="'.encode_entities($key).
  234. '" content="'.encode_entities($value).'" />';
  235. }
  236. else {
  237. push @{$metaheaders{$page}}, scrub('<meta name="'.encode_entities($key).
  238. '" content="'.encode_entities($value).'" />', $destpage);
  239. }
  240. return "";
  241. }
  242. sub pagetemplate (@) {
  243. my %params=@_;
  244. my $page=$params{page};
  245. my $destpage=$params{destpage};
  246. my $template=$params{template};
  247. if (exists $metaheaders{$page} && $template->query(name => "meta")) {
  248. # avoid duplicate meta lines
  249. my %seen;
  250. $template->param(meta => join("\n", grep { (! $seen{$_}) && ($seen{$_}=1) } @{$metaheaders{$page}}));
  251. }
  252. if (exists $pagestate{$page}{meta}{title} && $template->query(name => "title")) {
  253. $template->param(title => $pagestate{$page}{meta}{title});
  254. $template->param(title_overridden => 1);
  255. }
  256. foreach my $field (qw{author authorurl description permalink}) {
  257. $template->param($field => $pagestate{$page}{meta}{$field})
  258. if exists $pagestate{$page}{meta}{$field} && $template->query(name => $field);
  259. }
  260. foreach my $field (qw{license copyright}) {
  261. if (exists $pagestate{$page}{meta}{$field} && $template->query(name => $field) &&
  262. ($page eq $destpage || ! exists $pagestate{$destpage}{meta}{$field} ||
  263. $pagestate{$page}{meta}{$field} ne $pagestate{$destpage}{meta}{$field})) {
  264. $template->param($field => htmlize($page, $destpage, $pagestate{$page}{meta}{$field}));
  265. }
  266. }
  267. }
  268. sub titlesort {
  269. my $key = $pagestate{$_[0]}{meta}{titlesort};
  270. if (defined $key) {
  271. return $key;
  272. }
  273. return pagetitle(IkiWiki::basename($_[0]));
  274. }
  275. sub match {
  276. my $field=shift;
  277. my $page=shift;
  278. # turn glob into a safe regexp
  279. my $re=IkiWiki::glob2re(shift);
  280. my $val;
  281. if (exists $pagestate{$page}{meta}{$field}) {
  282. $val=$pagestate{$page}{meta}{$field};
  283. }
  284. elsif ($field eq 'title') {
  285. $val = pagetitle($page);
  286. }
  287. if (defined $val) {
  288. if ($val=~/^$re$/i) {
  289. return IkiWiki::SuccessReason->new("$re matches $field of $page", $page => $IkiWiki::DEPEND_CONTENT, "" => 1);
  290. }
  291. else {
  292. return IkiWiki::FailReason->new("$re does not match $field of $page", "" => 1);
  293. }
  294. }
  295. else {
  296. return IkiWiki::FailReason->new("$page does not have a $field", "" => 1);
  297. }
  298. }
  299. package IkiWiki::PageSpec;
  300. sub match_title ($$;@) {
  301. IkiWiki::Plugin::meta::match("title", @_);
  302. }
  303. sub match_author ($$;@) {
  304. IkiWiki::Plugin::meta::match("author", @_);
  305. }
  306. sub match_authorurl ($$;@) {
  307. IkiWiki::Plugin::meta::match("authorurl", @_);
  308. }
  309. sub match_license ($$;@) {
  310. IkiWiki::Plugin::meta::match("license", @_);
  311. }
  312. sub match_copyright ($$;@) {
  313. IkiWiki::Plugin::meta::match("copyright", @_);
  314. }
  315. package IkiWiki::SortSpec;
  316. sub cmp_meta_title {
  317. IkiWiki::Plugin::meta::titlesort($_[0])
  318. cmp
  319. IkiWiki::Plugin::meta::titlesort($_[1])
  320. }
  321. 1