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