summaryrefslogtreecommitdiff
path: root/perl/IkiWiki/Plugin/meta.pm
blob: 5cfa7283350885181b9e8e194e87a4448c235c10 (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 'redir') {
  196.         return "" if $page ne $destpage;
  197.         my $safe=0;
  198.         if ($value !~ /^\w+:\/\//) {
  199.             my ($redir_page$redir_anchor) = split /\#/$value;
  200.             my $link=bestlink($page$redir_page);
  201.             if (! length $link) {
  202.                 error gettext("redir page not found")
  203.             }
  204.             add_depends($page$linkdeptype("presence"));
  205.             $value=urlto($link$page);
  206.             $value.='#'.$redir_anchor if defined $redir_anchor;
  207.             $safe=1;
  208.             # redir cycle detection
  209.             $pagestate{$page}{meta}{redir}=$link;
  210.             my $at=$page;
  211.             my %seen;
  212.             while (exists $pagestate{$at}{meta}{redir}) {
  213.                 if ($seen{$at}) {
  214.                     error gettext("redir cycle is not allowed")
  215.                 }
  216.                 $seen{$at}=1;
  217.                 $at=$pagestate{$at}{meta}{redir};
  218.             }
  219.         }
  220.         else {
  221.             $value=encode_entities($value);
  222.         }
  223.         my $delay=int(exists $params{delay$params{delay} : 0);
  224.         my $redir="<meta http-equiv=\"refresh\" content=\"$delay; URL=$value\" />";
  225.         if (! $safe) {
  226.             $redir=scrub($redir$destpage);
  227.         }
  228.         push @{$metaheaders{$page}}, $redir;
  229.     }
  230.     elsif ($key eq 'link') {
  231.         if (%params) {
  232.             push @{$metaheaders{$page}}, scrub("<link href=\"".encode_entities($value)."\" ".
  233.                 join(" "map {
  234.                     encode_entities($_)."=\"".encode_entities(decode_entities($params{$_}))."\""
  235.                 keys %params).
  236.                 " />\n"$destpage);
  237.         }
  238.     }
  239.     elsif ($key eq 'robots') {
  240.         push @{$metaheaders{$page}}, '<meta name="robots"'.
  241.             ' content="'.encode_entities($value).'" />';
  242.     }
  243.     elsif ($key eq 'description') {
  244.         push @{$metaheaders{$page}}, '<meta name="'.
  245.             encode_entities($key).
  246.             '" content="'.encode_entities($value).'" />';
  247.     }
  248.     elsif ($key eq 'name') {
  249.         push @{$metaheaders{$page}}, scrub('<meta '.$key.'="'.
  250.             encode_entities($value).
  251.             join(' 'map "$_=\"$params{$_}\"" keys %params).
  252.             ' />'$destpage);
  253.     }
  254.     else {
  255.         push @{$metaheaders{$page}}, scrub('<meta name="'.
  256.             encode_entities($key).'" content="'.
  257.             encode_entities($value).'" />'$destpage);
  258.     }
  259.     return "";
  260. }
  261. sub pagetemplate (@) {
  262.     my %params=@_;
  263.         my $page=$params{page};
  264.         my $destpage=$params{destpage};
  265.         my $template=$params{template};
  266.     if (exists $metaheaders{$page} && $template->query(name => "meta")) {
  267.         # avoid duplicate meta lines
  268.         my %seen;
  269.         $template->param(meta => join("\n"grep { (! $seen{$_}) && ($seen{$_}=1) } @{$metaheaders{$page}}));
  270.     }
  271.     if (exists $pagestate{$page}{meta}{title} && $template->query(name => "title")) {
  272.         $template->param(title => HTML::Entities::encode_numeric($pagestate{$page}{meta}{title}));
  273.         $template->param(title_overridden => 1);
  274.     }
  275.     foreach my $field (qw{author authorurl permalink}) {
  276.         $template->param($field => $pagestate{$page}{meta}{$field})
  277.             if exists $pagestate{$page}{meta}{$field} && $template->query(name => $field);
  278.     }
  279.     foreach my $field (qw{description}) {
  280.         $template->param($field => HTML::Entities::encode_numeric($pagestate{$page}{meta}{$field}))
  281.             if exists $pagestate{$page}{meta}{$field} && $template->query(name => $field);
  282.     }
  283.     foreach my $field (qw{license copyright}) {
  284.         if (exists $pagestate{$page}{meta}{$field} && $template->query(name => $field) &&
  285.             ($page eq $destpage || ! exists $pagestate{$destpage}{meta}{$field} ||
  286.              $pagestate{$page}{meta}{$fieldne $pagestate{$destpage}{meta}{$field})) {
  287.             $template->param($field => htmlize($page$destpage$pagestate{$page}{meta}{$field}));
  288.         }
  289.     }
  290. }
  291. sub get_sort_key {
  292.     my $page shift;
  293.     my $meta shift;
  294.     # e.g. titlesort (also makes sense for author)
  295.     my $key $pagestate{$page}{meta}{$meta "sort"};
  296.     return $key if defined $key;
  297.     # e.g. title
  298.     $key $pagestate{$page}{meta}{$meta};
  299.     return $key if defined $key;
  300.     # fall back to closer-to-core things
  301.     if ($meta eq 'title') {
  302.         return pagetitle(IkiWiki::basename($page));
  303.     }
  304.     elsif ($meta eq 'date') {
  305.         return $IkiWiki::pagectime{$page};
  306.     }
  307.     elsif ($meta eq 'updated') {
  308.         return $IkiWiki::pagemtime{$page};
  309.     }
  310.     else {
  311.         return '';
  312.     }
  313. }
  314. sub match {
  315.     my $field=shift;
  316.     my $page=shift;
  317.     
  318.     # turn glob into a safe regexp
  319.     my $re=IkiWiki::glob2re(shift);
  320.     my $val;
  321.     if (exists $pagestate{$page}{meta}{$field}) {
  322.         $val=$pagestate{$page}{meta}{$field};
  323.     }
  324.     elsif ($field eq 'title') {
  325.         $val pagetitle($page);
  326.     }
  327.     if (defined $val) {
  328.         if ($val=~/^$re$/i) {
  329.             return IkiWiki::SuccessReason->new("$re matches $field of $page"$page => $IkiWiki::DEPEND_CONTENT"" => 1);
  330.         }
  331.         else {
  332.             return IkiWiki::FailReason->new("$re does not match $field of $page"$page => $IkiWiki::DEPEND_CONTENT"" => 1);
  333.         }
  334.     }
  335.     else {
  336.         return IkiWiki::FailReason->new("$page does not have a $field"$page => $IkiWiki::DEPEND_CONTENT);
  337.     }
  338. }
  339. package IkiWiki::PageSpec;
  340. sub match_title ($$;@) {
  341.     IkiWiki::Plugin::meta::match("title"@_);
  342. }
  343. sub match_author ($$;@) {
  344.     IkiWiki::Plugin::meta::match("author"@_);
  345. }
  346. sub match_authorurl ($$;@) {
  347.     IkiWiki::Plugin::meta::match("authorurl"@_);
  348. }
  349. sub match_license ($$;@) {
  350.     IkiWiki::Plugin::meta::match("license"@_);
  351. }
  352. sub match_copyright ($$;@) {
  353.     IkiWiki::Plugin::meta::match("copyright"@_);
  354. }
  355. sub match_guid ($$;@) {
  356.     IkiWiki::Plugin::meta::match("guid"@_);
  357. }
  358. package IkiWiki::SortSpec;
  359. sub cmp_meta {
  360.     my $meta shift;
  361.     error(gettext("sort=meta requires a parameter")) unless defined $meta;
  362.     if ($meta eq 'updated' || $meta eq 'date') {
  363.         return IkiWiki::Plugin::meta::get_sort_key($a$meta)
  364.             <=>
  365.             IkiWiki::Plugin::meta::get_sort_key($b$meta);
  366.     }
  367.     return IkiWiki::Plugin::meta::get_sort_key($a$meta)
  368.         cmp
  369.         IkiWiki::Plugin::meta::get_sort_key($b$meta);
  370. }
  371. # A prototype of how sort=title could behave in 4.0 or something
  372. sub cmp_meta_title {
  373.     $_[0] = 'title';
  374.     return cmp_meta(@_);
  375. }
  376. 1