mourningdove/cgi-bin/LJ/JSON.pm
2026-05-24 01:03:05 +00:00

229 lines
4.9 KiB
Perl

# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.
package LJ::JSON;
use strict;
use base qw(Exporter);
@LJ::JSON::EXPORT = qw( from_json to_json );
=head1 NAME
LJ::JSON - Wrapper for JSON which handles text encoding
=head1 SYNOPSIS
use LJ::JSON; # never use JSON! That could introduce subtle encoding issues
my $is_yummy = 1;
my $json_string = to_json( { a => "apple",
yummy => LJ::JSON->to_boolean( $is_yummy ) # true/false
yummy2 => $is_yummy # 1/0
# both can work in JS, but prefer true/false
} );
my $object = from_json( q!{ "a": "apple" }! );
=cut
my $wrap;
sub to_json ($@) {
my (@args) = @_;
return $wrap->encode(@args);
}
sub from_json ($@) {
my ($dump) = @_;
return unless $dump;
return $wrap->decode($dump);
}
sub class {
return ref $wrap;
}
sub true { $wrap->true }
sub false { $wrap->false }
sub to_boolean {
my ( $class, $what ) = @_;
return $what ? $wrap->true : $wrap->false;
}
sub to_number {
my ( $class, $what ) = @_;
# not using int deliberately because we may be handling floats here
return $what + 0;
}
foreach my $class (qw(LJ::JSON::XS LJ::JSON::JSONv2 LJ::JSON::JSONv1)) {
if ( $class->can_load ) {
$wrap = $class->new;
last;
}
}
die unless $wrap;
1;
package LJ::JSON::Wrapper;
use Encode qw();
sub traverse {
my ( $class, $what, $sub ) = @_;
my $type = ref $what;
# simple scalar
if ( $type eq '' ) {
return $sub->($what);
}
# hashref
if ( $type eq 'HASH' ) {
my %ret;
foreach my $k ( keys %$what ) {
$ret{ $sub->($k) } = $class->traverse( $what->{$k}, $sub );
}
return \%ret;
}
# arrayref
if ( $type eq 'ARRAY' ) {
my @ret;
foreach my $v (@$what) {
push @ret, $class->traverse( $v, $sub );
}
return \@ret;
}
# unknown type; let the subclass decode it to a scalar
# (base class function defaults to plain stringification)
return $sub->( $class->decode_unknown_type($what) );
}
sub traverse_fix_encoding {
my ( $class, $what ) = @_;
return $class->traverse(
$what,
sub {
my ($scalar) = @_;
return $scalar unless Encode::is_utf8($scalar);
# if the string does indeed contain wide characters (which happens
# in case the source string literals contained chars specified as
# '\u041c'), encode stuff as utf8
if ( $scalar =~ /[^\x01-\xff]/ ) {
return Encode::encode( "utf8", $scalar );
}
return Encode::encode( "iso-8859-1", $scalar );
}
);
}
sub decode_unknown_type {
my ( $class, $what ) = @_;
return "$what";
}
package LJ::JSON::XS;
our @ISA;
BEGIN { @ISA = qw(LJ::JSON::Wrapper JSON::XS); }
sub can_load {
eval { require JSON::XS; JSON::XS->import; };
return !$@;
}
sub new {
my ($class) = @_;
return $class->SUPER::new->latin1;
}
sub decode {
my ( $class, $dump ) = @_;
my $decoded = $class->SUPER::decode($dump);
$decoded = $class->traverse_fix_encoding($decoded);
return $decoded;
}
sub decode_unknown_type {
my ( $class, $what ) = @_;
# booleans get converted to undef for false and 1 for true
return $what ? 1 : 0 if JSON::XS::is_bool($what);
# otherwise, stringify
return "$what";
}
1;
package LJ::JSON::JSONv2;
our @ISA;
BEGIN { @ISA = qw(LJ::JSON::Wrapper JSON); }
sub can_load {
eval { require JSON };
return !$@ && $JSON::VERSION ge 2;
}
sub new {
my ($class) = @_;
return $class->SUPER::new->latin1;
}
sub decode {
my ( $class, $dump ) = @_;
my $decoded = $class->SUPER::decode($dump);
$decoded = $class->traverse_fix_encoding($decoded);
return $decoded;
}
sub decode_unknown_type {
my ( $class, $what ) = @_;
# booleans get converted to 0 for false and 1 for true
return $what ? 1 : 0 if JSON::is_bool($what);
# otherwise, stringify
return "$what";
}
1;
package LJ::JSON::JSONv1;
our @ISA;
BEGIN { @ISA = qw(LJ::JSON::Wrapper JSON); }
sub can_load {
eval { require JSON };
return !$@ && $JSON::VERSION ge 1;
}
*encode = \&JSON::objToJson;
*decode = \&JSON::jsonToObj;
1;