diff options
-rw-r--r-- | IkiWiki/Plugin/img.pm | 230 | ||||
-rw-r--r-- | IkiWiki/Plugin/meta.pm | 41 | ||||
-rw-r--r-- | IkiWiki/Plugin/osm.pm | 111 |
3 files changed, 289 insertions, 93 deletions
diff --git a/IkiWiki/Plugin/img.pm b/IkiWiki/Plugin/img.pm index b92e24c..b858243 100644 --- a/IkiWiki/Plugin/img.pm +++ b/IkiWiki/Plugin/img.pm @@ -21,6 +21,28 @@ sub getsetup () { rebuild => undef, section => "widget", }, + img_allowed_formats => { + type => "string", + default => [qw(jpeg png gif svg)], + description => "Image formats to process (jpeg, png, gif, svg, pdf or 'everything' to accept all)", + # ImageMagick has had arbitrary code execution flaws, + # and the whole delegates mechanism is scary from + # that perspective + safe => 0, + rebuild => 0, + }, +} + +sub allowed { + my $format = shift; + my $allowed = $config{img_allowed_formats}; + $allowed = ['jpeg', 'png', 'gif', 'svg'] unless defined $allowed && @$allowed; + + foreach my $a (@$allowed) { + return 1 if lc($a) eq $format || lc($a) eq 'everything'; + } + + return 0; } sub preprocess (@) { @@ -64,52 +86,142 @@ sub preprocess (@) { my $dir = $params{page}; my $base = IkiWiki::basename($file); - my $issvg = $base=~s/\.svg$/.png/i; + my $extension; + my $format; + + if ($base =~ m/\.([a-z0-9]+)$/is) { + $extension = $1; + } + else { + error gettext("Unable to detect image type from extension"); + } + + # Never interpret well-known file extensions as any other format, + # in case the wiki configuration unwisely allows attaching + # arbitrary files named *.jpg, etc. + my $magic; + my $offset = 0; + open(my $in, '<', $srcfile) or error sprintf(gettext("failed to read %s: %s"), $file, $!); + binmode($in); + + if ($extension =~ m/^(jpeg|jpg)$/is) { + $format = 'jpeg'; + $magic = "\377\330\377"; + } + elsif ($extension =~ m/^(png)$/is) { + $format = 'png'; + $magic = "\211PNG\r\n\032\n"; + } + elsif ($extension =~ m/^(gif)$/is) { + $format = 'gif'; + $magic = "GIF8"; + } + elsif ($extension =~ m/^(svg)$/is) { + $format = 'svg'; + } + elsif ($extension =~ m/^(pdf)$/is) { + $format = 'pdf'; + $magic = "%PDF-"; + } + else { + # allow ImageMagick to auto-detect (potentially dangerous) + my $im = Image::Magick->new(); + my $r = $im->Ping(file => $in); + if ($r) { + $format = lc $r; + } + else { + error sprintf(gettext("failed to determine format of %s"), $file); + } + } + + error sprintf(gettext("%s image processing disabled in img_allowed_formats configuration"), $format ? $format : "\"$extension\"") unless allowed($format ? $format : "everything"); + + # Try harder to protect ImageMagick from itself + if (defined $magic) { + my $content; + read($in, $content, length $magic) or error sprintf(gettext("failed to read %s: %s"), $file, $!); + if ($magic ne $content) { + error sprintf(gettext("\"%s\" does not seem to be a valid %s file"), $file, $format); + } + } + + my $ispdf = $base=~s/\.pdf$/.png/i; + my $pagenumber = exists($params{pagenumber}) ? int($params{pagenumber}) : 0; + if ($pagenumber != 0) { + $base = "p$pagenumber-$base"; + } - eval q{use Image::Magick}; - error gettext("Image::Magick is not installed") if $@; - my $im = Image::Magick->new($issvg ? (magick => "png") : ()); my $imglink; - my $r = $im->Read($srcfile); - error sprintf(gettext("failed to read %s: %s"), $file, $r) if $r; - + my $imgdatalink; my ($dwidth, $dheight); + my ($w, $h); if ($params{size} ne 'full') { - my ($w, $h) = ($params{size} =~ /^(\d*)x(\d*)$/); - error sprintf(gettext('wrong size format "%s" (should be WxH)'), $params{size}) - unless (defined $w && defined $h && - (length $w || length $h)); - - if ((length $w && $w > $im->Get("width")) || - (length $h && $h > $im->Get("height"))) { - # resizing larger - $imglink = $file; + ($w, $h) = ($params{size} =~ /^(\d*)x(\d*)$/); + } - # don't generate larger image, just set display size - if (length $w && length $h) { - ($dwidth, $dheight)=($w, $h); - } - # avoid division by zero on 0x0 image - elsif ($im->Get("width") == 0 || $im->Get("height") == 0) { + if ($format eq 'svg') { + # svg images are not scaled using ImageMagick because the + # pipeline is complex. Instead, the image size is simply + # set to the provided values. + # + # Aspect ratio will be preserved automatically when + # only a width or only a height is specified. + # When both are specified, aspect ratio will not be + # preserved. + $imglink = $file; + $dwidth = $w if length $w; + $dheight = $h if length $h; + } + else { + eval q{use Image::Magick}; + error gettext("Image::Magick is not installed") if $@; + my $im = Image::Magick->new(); + my $r = $im->Read("$format:$srcfile\[$pagenumber]"); + error sprintf(gettext("failed to read %s: %s"), $file, $r) if $r; + + if ($config{deterministic}) { + $im->Set('date:create' => 0); + $im->Set('date:modify' => 0); + $im->Set('option' => 'png:exclude-chunk=time'); + } + + if (! defined $im->Get("width") || ! defined $im->Get("height")) { + error sprintf(gettext("failed to get dimensions of %s"), $file); + } + + if (! length $w && ! length $h) { + $dwidth = $im->Get("width"); + $dheight = $im->Get("height"); + } else { + error sprintf(gettext('wrong size format "%s" (should be WxH)'), $params{size}) + unless (defined $w && defined $h && + (length $w || length $h)); + + if ($im->Get("width") == 0 || $im->Get("height") == 0) { ($dwidth, $dheight)=(0, 0); - } - # calculate unspecified size from the other one, preserving - # aspect ratio - elsif (length $w) { - $dwidth=$w; - $dheight=$w / $im->Get("width") * $im->Get("height"); - } - elsif (length $h) { + } elsif (! length $w || (length $h && $im->Get("height")*$w > $h * $im->Get("width"))) { + # using height because only height is given or ... + # because original image is more portrait than $w/$h + # ... slimness of $im > $h/w + # ... $im->Get("height")/$im->Get("width") > $h/$w + # ... $im->Get("height")*$w > $h * $im->Get("width") + $dheight=$h; $dwidth=$h / $im->Get("height") * $im->Get("width"); + } else { # (! length $h) or $w is what determines the resized size + $dwidth=$w; + $dheight=$w / $im->Get("width") * $im->Get("height"); } } - else { - # resizing smaller - my $outfile = "$config{destdir}/$dir/${w}x${h}-$base"; - $imglink = "$dir/${w}x${h}-$base"; - + + if ($dwidth < $im->Get("width") || $ispdf) { + # resize down, or resize to pixels at all + + my $outfile = "$config{destdir}/$dir/$params{size}-$base"; + $imglink = "$dir/$params{size}-$base"; + will_render($params{page}, $imglink); if (-e $outfile && (-M $srcfile >= -M $outfile)) { @@ -118,44 +230,40 @@ sub preprocess (@) { error sprintf(gettext("failed to read %s: %s"), $outfile, $r) if $r; } else { - $r = $im->Resize(geometry => "${w}x${h}"); + $r = $im->Resize(geometry => "${dwidth}x${dheight}"); error sprintf(gettext("failed to resize: %s"), $r) if $r; + $im->set($ispdf ? (magick => 'png') : ()); + my @blob = $im->ImageToBlob(); # don't actually write resized file in preview mode; # rely on width and height settings if (! $params{preview}) { - my @blob = $im->ImageToBlob(); writefile($imglink, $config{destdir}, $blob[0], 1); } else { - $imglink = $file; + eval q{use MIME::Base64}; + error($@) if $@; + $imgdatalink = "data:image/".$im->Get("magick").";base64,".encode_base64($blob[0]); } } - # always get the true size of the resized image - $dwidth = $im->Get("width"); + # always get the true size of the resized image (it could be + # that imagemagick did its calculations differently) + $dwidth = $im->Get("width"); $dheight = $im->Get("height"); + } else { + $imglink = $file; + } + + if (! defined($dwidth) || ! defined($dheight)) { + error sprintf(gettext("failed to determine size of image %s"), $file) } - } - else { - $imglink = $file; - $dwidth = $im->Get("width"); - $dheight = $im->Get("height"); - } - - if (! defined($dwidth) || ! defined($dheight)) { - error sprintf(gettext("failed to determine size of image %s"), $file) } my ($fileurl, $imgurl); - if (! $params{preview}) { - $fileurl=urlto($file, $params{destpage}); - $imgurl=urlto($imglink, $params{destpage}); - } - else { - $fileurl=urlto($file); - $imgurl=urlto($imglink); - } + my $urltobase = $params{preview} ? undef : $params{destpage}; + $fileurl=urlto($file, $urltobase); + $imgurl=$imgdatalink ? $imgdatalink : urlto($imglink, $urltobase); if (! exists $params{class}) { $params{class}="img"; @@ -168,10 +276,10 @@ sub preprocess (@) { } } - my $imgtag='<img src="'.$imgurl. - '" width="'.$dwidth. - '" height="'.$dheight.'"'. - $attrs. + my $imgtag='<img src="'.$imgurl.'"'; + $imgtag.=' width="'.$dwidth.'"' if defined $dwidth; + $imgtag.=' height="'.$dheight.'"' if defined $dheight; + $imgtag.= $attrs. (exists $params{align} && ! exists $params{caption} ? ' align="'.$params{align}.'"' : ''). ' />'; diff --git a/IkiWiki/Plugin/meta.pm b/IkiWiki/Plugin/meta.pm index 421f1dc..cd367da 100644 --- a/IkiWiki/Plugin/meta.pm +++ b/IkiWiki/Plugin/meta.pm @@ -107,12 +107,12 @@ sub preprocess (@) { # fallthrough } elsif ($key eq 'license') { - push @{$metaheaders{$page}}, '<link rel="license" href="#page_license" />'; + push @{$metaheaders{$page}}, '<link rel="license" href="#pagelicense" />'; $pagestate{$page}{meta}{license}=$value; return ""; } elsif ($key eq 'copyright') { - push @{$metaheaders{$page}}, '<link rel="copyright" href="#page_copyright" />'; + push @{$metaheaders{$page}}, '<link rel="copyright" href="#pagecopyright" />'; $pagestate{$page}{meta}{copyright}=$value; return ""; } @@ -121,6 +121,18 @@ sub preprocess (@) { add_link($page, $value); return ""; } + elsif ($key eq 'enclosure') { + my $link=bestlink($page, $value); + if (! length $link) { + error gettext("enclosure not found") + } + add_depends($page, $link, deptype("presence")); + + $value=urlto($link, $page, 1); + $pagestate{$page}{meta}{enclosure}=$value; + $pagestate{$page}{meta}{enclosurefile}=$link; + # fallthrough + } elsif ($key eq 'author') { $pagestate{$page}{meta}{author}=$value; if (exists $params{sortas}) { @@ -143,14 +155,30 @@ sub preprocess (@) { eval q{use Date::Parse}; if (! $@) { my $time = str2time($value); - $IkiWiki::pagectime{$page}=$time if defined $time; + if (defined $time) { + $IkiWiki::pagectime{$page}=$time; + } + else { + error(sprintf(gettext('cannot parse date/time: %s'), $value)); + } + } + else { + error $@; } } elsif ($key eq 'updated') { eval q{use Date::Parse}; if (! $@) { my $time = str2time($value); - $pagestate{$page}{meta}{updated}=$time if defined $time; + if (defined $time) { + $pagestate{$page}{meta}{updated}=$time; + } + else { + error(sprintf(gettext('cannot parse date/time: %s'), $value)); + } + } + else { + error $@; } } @@ -282,6 +310,7 @@ sub preprocess (@) { elsif ($key eq 'name') { push @{$metaheaders{$page}}, scrub('<meta name="'. encode_entities($value). + '" '. join(' ', map { "$_=\"$params{$_}\"" } keys %params). ' />', $page, $destpage); } @@ -318,6 +347,10 @@ sub pagetemplate (@) { $template->param(title_overridden => 1); } + if (exists $pagestate{$page}{meta}{enclosure}) { + $template->param(enclosure => HTML::Entities::encode_entities(IkiWiki::urlabs($pagestate{$page}{meta}{enclosure}, $config{url}))); + } + foreach my $field (qw{authorurl}) { eval q{use HTML::Entities}; $template->param($field => HTML::Entities::encode_entities($pagestate{$page}{meta}{$field})) diff --git a/IkiWiki/Plugin/osm.pm b/IkiWiki/Plugin/osm.pm index c9c5646..27db31a 100644 --- a/IkiWiki/Plugin/osm.pm +++ b/IkiWiki/Plugin/osm.pm @@ -14,7 +14,7 @@ sub import { hook(type => "format", id => "osm", call => \&format); hook(type => "preprocess", id => "osm", call => \&preprocess); hook(type => "preprocess", id => "waypoint", call => \&process_waypoint); - hook(type => "savestate", id => "waypoint", call => \&savestate); + hook(type => "changes", id => "waypoint", call => \&changes); hook(type => "cgi", id => "osm", call => \&cgi); } @@ -60,6 +60,46 @@ sub getsetup () { safe => 0, rebuild => 1, }, + osm_openlayers_url => { + type => "string", + example => "http://www.openlayers.org/api/OpenLayers.js", + description => "Url for the OpenLayers.js file", + safe => 0, + rebuild => 1, + }, + osm_layers => { + type => "string", + example => { 'OSM', 'GoogleSatellite' }, + description => "Layers to use in the map. Can be either the 'OSM' string or a type option for Google maps (GoogleNormal, GoogleSatellite, GoogleHybrid or GooglePhysical). It can also be an arbitrary URL in a syntax acceptable for OpenLayers.Layer.OSM.url parameter.", + safe => 0, + rebuild => 1, + }, + osm_google_apikey => { + type => "string", + example => "", + description => "Google maps API key, Google layer not used if missing, see https://code.google.com/apis/console/ to get an API key", + safe => 1, + rebuild => 1, + }, +} + +sub register_rendered_files { + my $map = shift; + my $page = shift; + my $dest = shift; + + if ($page eq $dest) { + my %formats = get_formats(); + if ($formats{'GeoJSON'}) { + will_render($page, "$map/pois.json"); + } + if ($formats{'CSV'}) { + will_render($page, "$map/pois.txt"); + } + if ($formats{'KML'}) { + will_render($page, "$map/pois.kml"); + } + } } sub preprocess { @@ -98,6 +138,8 @@ sub preprocess { ); } + register_rendered_files($map, $page, $dest); + $pagestate{$page}{'osm'}{$map}{'displays'}{$name} = { height => $height, width => $width, @@ -108,6 +150,7 @@ sub preprocess { lat => $lat, lon => $lon, href => $href, + google_apikey => $config{'osm_google_apikey'}, }; return "<div id=\"mapdiv-$name\"></div>"; } @@ -149,19 +192,9 @@ sub process_waypoint { } } $icon = urlto($icon, $dest, 1); + $icon =~ s!/*$!!; # hack - urlto shouldn't be appending a slash in the first place $tag = '' unless $tag; - if ($page eq $dest) { - my %formats = get_formats(); - if ($formats{'GeoJSON'}) { - will_render($page, "$map/pois.json"); - } - if ($formats{'CSV'}) { - will_render($page, "$map/pois.txt"); - } - if ($formats{'KML'}) { - will_render($page, "$map/pois.kml"); - } - } + register_rendered_files($map, $page, $dest); $pagestate{$page}{'osm'}{$map}{'waypoints'}{$name} = { page => $page, desc => $desc, @@ -200,7 +233,7 @@ sub get_tag_icon($) { my $tag = shift; # look for an icon attached to the tag my $attached = $tag . '/' . $config{'osm_tag_default_icon'}; - if (srcfile($attached)) { + if (srcfile($attached, 1)) { return $attached; } else { @@ -247,7 +280,7 @@ sub scrub_lonlat($$$) { return ($lon, $lat); } -sub savestate { +sub changes { my %waypoints = (); my %linestrings = (); @@ -327,22 +360,29 @@ sub writekml($;$) { foreach my $map (keys %waypoints) { my $output; my $writer = XML::Writer->new( OUTPUT => \$output, - DATA_MODE => 1, ENCODING => 'UTF-8'); + DATA_MODE => 1, DATA_INDENT => ' ', ENCODING => 'UTF-8'); $writer->xmlDecl(); $writer->startTag("kml", "xmlns" => "http://www.opengis.net/kml/2.2"); + $writer->startTag("Document"); # first pass: get the icons + my %tags_map = (); # keep track of tags seen foreach my $name (keys %{$waypoints{$map}}) { my %options = %{$waypoints{$map}{$name}}; - $writer->startTag("Style", id => $options{tag}); - $writer->startTag("IconStyle"); - $writer->startTag("Icon"); - $writer->startTag("href"); - $writer->characters($options{icon}); - $writer->endTag(); - $writer->endTag(); - $writer->endTag(); - $writer->endTag(); + if (!$tags_map{$options{tag}}) { + debug("found new style " . $options{tag}); + $tags_map{$options{tag}} = (); + $writer->startTag("Style", id => $options{tag}); + $writer->startTag("IconStyle"); + $writer->startTag("Icon"); + $writer->startTag("href"); + $writer->characters($options{icon}); + $writer->endTag(); + $writer->endTag(); + $writer->endTag(); + $writer->endTag(); + } + $tags_map{$options{tag}}{$name} = \%options; } foreach my $name (keys %{$waypoints{$map}}) { @@ -389,6 +429,7 @@ sub writekml($;$) { $writer->endTag(); } $writer->endTag(); + $writer->endTag(); $writer->end(); writefile("pois.kml", $config{destdir} . "/$map", $output); @@ -466,7 +507,7 @@ sub include_javascript ($) { } } if ($loader) { - return embed_map_code($page) . "<script type=\"text/javascript\" charset=\"utf-8\">$loader</script>"; + return embed_map_code($page) . "<script type=\"text/javascript\">$loader</script>"; } else { return ''; @@ -493,13 +534,14 @@ sub cgi($) { print "<html><body>"; print "<div id=\"mapdiv-$map\"></div>"; print embed_map_code(); - print "<script type=\"text/javascript\" charset=\"utf-8\">"; + print "<script type=\"text/javascript\">"; print map_setup_code($map, $map, lat => "urlParams['lat']", lon => "urlParams['lon']", zoom => "urlParams['zoom']", fullscreen => 1, editable => 1, + google_apikey => $config{'osm_google_apikey'}, ); print "</script>"; print "</body></html>"; @@ -509,9 +551,14 @@ sub cgi($) { sub embed_map_code(;$) { my $page=shift; - return '<script src="http://www.openlayers.org/api/OpenLayers.js" type="text/javascript" charset="utf-8"></script>'. + my $olurl = $config{osm_openlayers_url} || "http://www.openlayers.org/api/OpenLayers.js"; + my $code = '<script src="'.$olurl.'" type="text/javascript" charset="utf-8"></script>'."\n". '<script src="'.urlto("ikiwiki/osm.js", $page). '" type="text/javascript" charset="utf-8"></script>'."\n"; + if ($config{'osm_google_apikey'}) { + $code .= '<script src="http://maps.google.com/maps?file=api&v=2&key='.$config{'osm_google_apikey'}.'&sensor=false" type="text/javascript" charset="utf-8"></script>'; + } + return $code; } sub map_setup_code($;@) { @@ -519,6 +566,8 @@ sub map_setup_code($;@) { my $name=shift; my %options=@_; + my $mapurl = $config{osm_map_url}; + eval q{use JSON}; error $@ if $@; @@ -535,6 +584,12 @@ sub map_setup_code($;@) { $options{'kmlurl'} = urlto($map."/pois.kml"); } + if ($mapurl) { + $options{'mapurl'} = $mapurl; + } + $options{'layers'} = $config{osm_layers}; + + $name=~s/'//g; # $name comes from user input return "mapsetup('mapdiv-$name', " . to_json(\%options) . ");"; } |