mourningdove/cgi-bin/DW/Controller/RPC/MiscLegacy.pm

793 lines
26 KiB
Perl
Raw Permalink Normal View History

2026-05-24 01:03:05 +00:00
# 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 DW::Controller::RPC::MiscLegacy;
use strict;
use DW::Routing;
use DW::RPC;
use LJ::CreatePage;
# do not put any endpoints that do not have the "forked from LJ" header in this file
DW::Routing->register_rpc( "changerelation", \&change_relation_handler, format => 'json' );
DW::Routing->register_rpc( "checkforusername", \&check_username_handler, format => 'json' );
DW::Routing->register_rpc( "controlstrip", \&control_strip_handler, format => 'json' );
DW::Routing->register_rpc( "ctxpopup", \&ctxpopup_handler, format => 'json' );
DW::Routing->register_rpc( "esn_inbox", \&esn_inbox_handler, format => 'json' );
DW::Routing->register_rpc( "esn_subs", \&esn_subs_handler, format => 'json' );
DW::Routing->register_rpc( "getsecurityoptions", \&get_security_options_handler, format => 'json' );
DW::Routing->register_rpc( "gettags", \&get_tags_handler, format => 'json' );
DW::Routing->register_rpc( "load_state_codes", \&load_state_codes_handler, format => 'json' );
DW::Routing->register_rpc(
"profileexpandcollapse",
\&profileexpandcollapse_handler,
format => 'json'
);
DW::Routing->register_rpc( "userpicselect", \&get_userpics_handler, format => 'json' );
DW::Routing->register_rpc( "widget", \&widget_handler, format => 'json' );
sub change_relation_handler {
my $r = DW::Request->get;
my $post = $r->post_args;
# get user
my $remote = LJ::get_remote();
return DW::RPC->err("Sorry, you must be logged in to use this feature.")
unless $remote;
return DW::RPC->err("Invalid auth token")
unless $remote->check_ajax_auth_token( '/__rpc_changerelation', %$post );
my ( $target, $action );
$target = $post->{target} or return DW::RPC->err("No target specified");
$action = $post->{action} or return DW::RPC->err("No action specified");
# Prevent XSS attacks
$target = LJ::ehtml($target);
$action = LJ::ehtml($action);
my $targetu = LJ::load_user($target);
return DW::RPC->err("Invalid user $target")
unless $targetu;
my $success = 0;
my %ret = ();
if ( $action eq 'addTrust' ) {
my $error;
return DW::RPC->err($error)
unless $remote->can_trust( $targetu, errref => \$error );
$success = $remote->add_edge( $targetu, trust => {} );
}
elsif ( $action eq 'addWatch' ) {
my $error;
return DW::RPC->err($error)
unless $remote->can_watch( $targetu, errref => \$error );
$success = $remote->add_edge( $targetu, watch => {} );
$success &&= $remote->add_to_default_filters($targetu);
}
elsif ( $action eq 'removeTrust' ) {
$success = $remote->remove_edge( $targetu, trust => {} );
}
elsif ( $action eq 'removeWatch' ) {
$success = $remote->remove_edge( $targetu, watch => {} );
}
elsif ( $action eq 'join' ) {
my $error;
if ( $remote->can_join( $targetu, errref => \$error ) ) {
$success = $remote->join_community($targetu);
}
else {
if ( $error eq LJ::Lang::ml('edges.join.error.targetnotopen')
&& $targetu->is_moderated_membership )
{
$targetu->comm_join_request($remote);
$ret{note} = LJ::Lang::ml('edges.join.response.reqsubmitted');
}
else {
return DW::RPC->err($error);
}
}
}
elsif ( $action eq 'leave' ) {
my $error;
return DW::RPC->err($error)
unless $remote->can_leave( $targetu, errref => \$error );
$success = $remote->leave_community($targetu);
}
elsif ( $action eq 'accept' ) {
$success = $remote->accept_comm_invite($targetu);
}
elsif ( $action eq 'setBan' ) {
my $list_of_banned = LJ::load_rel_user( $remote, 'B' ) || [];
return DW::RPC->err("Exceeded limit maximum of banned users")
if @$list_of_banned >= ( $LJ::MAX_BANS || 5000 );
my $ban_user = LJ::load_user($target);
$success = $remote->ban_user($ban_user);
LJ::Hooks::run_hooks( 'ban_set', $remote, $ban_user );
}
elsif ( $action eq 'setUnban' ) {
my $unban_user = LJ::load_user($target);
$success = $remote->unban_user_multi( $unban_user->{userid} );
}
else {
return DW::RPC->err("Invalid action $action");
}
return DW::RPC->out(
success => $success,
is_trusting => $remote->trusts($targetu),
is_watching => $remote->watches($targetu),
is_member => $remote->member_of($targetu),
is_banned => $remote->has_banned($targetu),
%ret,
);
}
sub check_username_handler {
my $r = DW::Request->get;
my $args = $r->get_args;
my $error = LJ::CreatePage->verify_username( $args->{user} );
return DW::RPC->err($error);
}
sub control_strip_handler {
my $r = DW::Request->get;
my $args = $r->get_args;
my $control_strip;
my $user = $args->{user};
if ( defined $user ) {
unless ( defined LJ::get_active_journal() ) {
LJ::set_active_journal( LJ::load_user($user) );
}
$control_strip = LJ::control_strip(
user => $user,
host => $args->{host},
uri => $args->{uri},
args => $args->{args},
view => $args->{view}
);
}
return DW::RPC->out( control_strip => $control_strip );
}
sub ctxpopup_handler {
my $r = DW::Request->get;
my $get = $r->get_args;
my $get_user = sub {
# three ways to load a user:
# username:
if ( defined $get->{user} && ( my $user = LJ::canonical_username( $get->{user} ) ) ) {
return LJ::load_user($user);
}
# identity:
if ( defined $get->{userid} && ( my $userid = $get->{userid} ) ) {
return undef unless $userid =~ /^\d+$/;
my $u = LJ::load_userid($userid);
return undef unless $u && $u->identity;
return $u;
}
# based on userpic url
if ( defined $get->{userpic_url} && ( my $upurl = $get->{userpic_url} ) ) {
return undef unless $upurl =~ m!(\d+)/(\d+)!;
my ( $picid, $userid ) = ( $1, $2 );
my $u = LJ::load_userid($userid);
my $up = LJ::Userpic->instance( $u, $picid );
return $up->valid ? $u : undef;
}
};
my $remote = LJ::get_remote();
my $u = $get_user->();
my %ret = $u ? $u->info_for_js : ();
my $reason = $u ? $u->prop('delete_reason') : '';
$reason = $reason ? "Reason given: " . $reason : "No reason given.";
return DW::RPC->err("Error: Invalid mode")
unless $get->{mode} eq 'getinfo';
return DW::RPC->out( error => "No such user", noshow => 1 )
unless $u;
return DW::RPC->err( "This user's account is deleted.<br />" . $reason )
if $u->is_deleted;
return DW::RPC->err("This user's account is deleted and purged.")
if $u->is_expunged;
return DW::RPC->err("This user's account is suspended.")
if $u->is_suspended;
# uri for changerelation auth token
my $uri = '/__rpc_changerelation';
# actions to generate auth tokens for
my @actions = ();
$ret{url_addtrust} = "$LJ::SITEROOT/circle/" . $u->{user} . "/edit?action=access";
$ret{url_addwatch} = "$LJ::SITEROOT/circle/" . $u->{user} . "/edit?action=subscribe";
my $up = $u->userpic;
if ($up) {
$ret{url_userpic} = $up->url;
$ret{userpic_w} = $up->width;
$ret{userpic_h} = $up->height;
}
else {
# if it's a feed, make their userpic the feed icon
if ( $u->is_syndicated ) {
$ret{url_userpic} = "$LJ::IMGPREFIX/feed100x100.png";
}
elsif ( $u->is_identity ) {
$ret{url_userpic} = "$LJ::IMGPREFIX/identity_100x100.png";
}
else {
$ret{url_userpic} = "$LJ::IMGPREFIX/nouserpic.png";
}
$ret{userpic_w} = 100;
$ret{userpic_h} = 100;
}
if ($remote) {
$ret{is_trusting} = $remote->trusts($u);
$ret{is_trusted_by} = $u->trusts($remote);
$ret{is_watching} = $remote->watches($u);
$ret{is_watched_by} = $u->watches($remote);
$ret{is_requester} = $remote->equals($u);
$ret{other_is_identity} = $u->is_identity;
$ret{self_is_identity} = $remote->is_identity;
$ret{can_message} = $u->can_receive_message($remote);
$ret{url_message} = $u->message_url;
$ret{can_receive_vgifts} = $u->can_receive_vgifts_from($remote);
$ret{url_vgift} = $u->virtual_gift_url;
}
$ret{is_logged_in} = $remote ? 1 : 0;
if ( $u->is_comm ) {
$ret{url_joincomm} = "$LJ::SITEROOT/circle/" . $u->{user} . "/edit";
$ret{url_leavecomm} = "$LJ::SITEROOT/circle/" . $u->{user} . "/edit";
$ret{url_acceptinvite} = "$LJ::SITEROOT/manage/invites";
$ret{is_member} = $remote->member_of($u) if $remote;
$ret{is_closed_membership} = $u->is_closed_membership;
my $pending = $remote ? ( $remote->get_pending_invites || [] ) : [];
$ret{is_invited} = ( grep { $_->[0] == $u->id } @$pending ) ? 1 : 0;
push @actions, 'join', 'leave', 'accept';
}
# generate auth tokens
if ($remote) {
push @actions, 'addTrust', 'addWatch', 'removeTrust', 'removeWatch', 'setBan', 'setUnban';
foreach my $action (@actions) {
$ret{"${action}_authtoken"} = $remote->ajax_auth_token(
$uri,
target => $u->user,
action => $action,
);
}
}
my %extrainfo = LJ::Hooks::run_hook( "ctxpopup_extra_info", $u ) || ();
%ret = ( %ret, %extrainfo ) if %extrainfo;
$ret{is_banned} = $remote->has_banned($u) ? 1 : 0 if $remote;
$ret{success} = 1;
return DW::RPC->out(%ret);
}
sub esn_inbox_handler {
my $r = DW::Request->get;
my $post = $r->post_args;
my $remote = LJ::get_remote();
return DW::RPC->err("Sorry, you must be logged in to use this feature.")
unless $remote;
my $authas = delete $post->{authas};
my $action = $post->{action};
return DW::RPC->err("No action specified") unless $action;
my $success = 0;
my %ret;
# do authas
my $u = LJ::get_authas_user($authas) || $remote;
return DW::RPC->err("You could not be authenticated as the specified user.")
unless $u;
# get qids
my @qids;
@qids = split( ',', $post->{qids} ) if $post->{qids};
my @items;
if ( scalar @qids ) {
foreach my $qid (@qids) {
my $item = eval { LJ::NotificationItem->new( $u, $qid ) };
push @items, $item if $item;
}
}
$ret{items} = [];
my $inbox = $u->notification_inbox;
my $cur_folder = $post->{cur_folder} || 'all';
my $itemid = $post->{itemid} && $post->{itemid} =~ /^\d+$/ ? $post->{itemid} + 0 : 0;
# do actions
if ( $action eq 'mark_read' ) {
$_->mark_read foreach @items;
$success = 1;
}
elsif ( $action eq 'mark_unread' ) {
$_->mark_unread foreach @items;
$success = 1;
}
elsif ( $action eq 'delete' ) {
foreach my $item (@items) {
push @{ $ret{items} }, { qid => $item->qid, deleted => 1 };
$item->delete;
}
$success = 1;
}
elsif ( $action eq 'delete_all' ) {
@items = $inbox->delete_all( $cur_folder, itemid => $itemid );
foreach my $item (@items) {
push @{ $ret{items} }, { qid => $item->{qid}, deleted => 1 };
}
$success = 1;
}
elsif ( $action eq 'mark_all_read' ) {
$inbox->mark_all_read( $cur_folder, itemid => $itemid );
$success = 1;
}
elsif ( $action eq 'set_default_expand_prop' ) {
$u->set_prop( 'esn_inbox_default_expand', $post->{default_expand} eq 'Y' ? 'Y' : 'N' );
}
elsif ( $action eq 'get_unread_items' ) {
$ret{unread_count} = $u->notification_inbox->unread_count;
}
elsif ( $action eq 'toggle_bookmark' ) {
my $up;
$up = LJ::Hooks::run_hook( 'upgrade_message', $u, 'bookmark' );
$up = "<br />$up" if ($up);
foreach my $item (@items) {
my $ret = $u->notification_inbox->toggle_bookmark( $item->qid );
return DW::RPC->err("Max number of bookmarks reached.$up") unless $ret;
}
$success = 1;
}
else {
return DW::RPC->err("Invalid action $action");
}
foreach my $item ( $u->notification_inbox->items ) {
my $class = $item->event->class;
$class =~ s/LJ::Event:://;
push @{ $ret{items} },
{
read => $item->read,
qid => $item->qid,
bookmarked => $u->notification_inbox->is_bookmark( $item->qid ),
category => $class,
};
}
return DW::RPC->out(
success => $success,
unread_all => $inbox->all_event_count,
unread_usermsg_recvd => $inbox->usermsg_recvd_event_count,
unread_friend => $inbox->circle_event_count,
unread_entrycomment => $inbox->entrycomment_event_count,
unread_pollvote => $inbox->pollvote_event_count,
unread_usermsg_sent => $inbox->usermsg_sent_event_count,
%ret,
);
}
sub esn_subs_handler {
my $r = DW::Request->get;
my $post = $r->post_args;
return DW::RPC->err("Sorry async ESN is not enabled") unless LJ::is_enabled('esn_ajax');
my $remote = LJ::get_remote();
return DW::RPC->err("Sorry, you must be logged in to use this feature.")
unless $remote;
# check auth token
return DW::RPC->err("Invalid auth token")
unless $remote->check_ajax_auth_token( '/__rpc_esn_subs', %$post );
my $action = $post->{action} or return DW::RPC->err("No action specified");
my $success = 0;
my %ret;
if ( $action eq 'delsub' ) {
my $subid = $post->{subid} or return DW::RPC->err("No subid");
my $subscr = LJ::Subscription->new_by_id( $remote, $subid );
return DW::RPC->out( success => 0 )
unless $subscr;
my %postauth;
foreach my $subkey (qw(journalid arg1 arg2 etypeid)) {
$ret{$subkey} = $subscr->$subkey || 0;
$postauth{$subkey} = $ret{$subkey} if $ret{$subkey};
}
$ret{event_class} = $subscr->event_class;
$subscr->delete;
$success = 1;
$ret{msg} = "Notification Tracking Removed";
$ret{subscribed} = 0;
my $auth_token = $remote->ajax_auth_token(
'/__rpc_esn_subs',
action => 'addsub',
%postauth,
);
if ( $subscr->event_class eq 'LJ::Event::JournalNewEntry' ) {
$ret{newentry_token} = $auth_token;
}
else {
$ret{auth_token} = $auth_token;
}
}
elsif ( $action eq 'addsub' ) {
return DW::RPC->err(
"Reached limit of " . $remote->count_max_subscriptions . " active notifications" )
unless $remote->can_add_inbox_subscription;
my %subparams = ();
return DW::RPC->err("Invalid notification tracking parameters")
unless ( defined $post->{journalid} ) && $post->{etypeid} + 0;
foreach my $param (qw(journalid etypeid arg1 arg2)) {
$subparams{$param} = $post->{$param} + 0;
}
$subparams{method} = 'Inbox';
my ($subscr) = $remote->has_subscription(%subparams);
$subparams{flags} = LJ::Subscription::TRACKING;
eval { $subscr ||= $remote->subscribe(%subparams) };
return DW::RPC->err($@) if $@;
if ($subscr) {
$subscr->activate;
$success = 1;
$ret{msg} = "Notification Tracking Added";
$ret{subscribed} = 1;
$ret{event_class} = $subscr->event_class;
my %sub_info = $subscr->sub_info;
$ret{sub_info} = \%sub_info;
# subscribe to email as well
my %email_sub_info = %sub_info;
$email_sub_info{method} = "Email";
$remote->subscribe(%email_sub_info);
# special case for JournalNewComment: need to return dtalkid for
# updating of tracking icons (on subscriptions with jtalkid)
if ( $subscr->event_class eq 'LJ::Event::JournalNewComment' && $subscr->arg2 ) {
my $cmt = LJ::Comment->new( $subscr->journal, jtalkid => $subscr->arg2 );
$ret{dtalkid} = $cmt->dtalkid if $cmt;
}
my $auth_token = $remote->ajax_auth_token(
'/__rpc_esn_subs',
subid => $subscr->id,
action => 'delsub'
);
if ( $subscr->event_class eq 'LJ::Event::JournalNewEntry' ) {
$ret{newentry_token} = $auth_token;
$ret{newentry_subid} = $subscr->id;
}
else {
$ret{auth_token} = $auth_token;
$ret{subid} = $subscr->id;
}
}
else {
$success = 0;
$ret{subscribed} = 0;
}
}
else {
return DW::RPC->err("Invalid action $action");
}
return DW::RPC->out(
success => $success,
%ret,
);
}
sub get_security_options_handler {
my $r = DW::Request->get;
my $args = $r->get_args;
my $remote = LJ::get_remote();
my $user = $args->{user};
my $u = LJ::load_user($user);
return DW::RPC->out
unless $u;
my %ret = (
is_comm => $u->is_comm ? 1 : 0,
can_manage => $remote && $remote->can_manage($u) ? 1 : 0,
);
return DW::RPC->out( ret => \%ret )
unless $remote && $remote->can_post_to($u);
unless ( $ret{is_comm} ) {
my $friend_groups = $u->trust_groups;
$ret{friend_groups_exist} = keys %$friend_groups ? 1 : 0;
}
$ret{minsecurity} = $u->newpost_minsecurity;
return DW::RPC->out( ret => \%ret );
}
sub get_tags_handler {
my $r = DW::Request->get;
my $args = $r->get_args;
my $remote = LJ::get_remote();
my $user = $args->{user};
my $u = LJ::load_user($user);
my $tags = $u ? $u->tags : {};
return DW::RPC->alert("You cannot view this journal's tags.")
unless $remote && $remote->can_post_to($u);
return DW::RPC->alert("You cannot use this journal's tags.")
unless $remote->can_add_tags_to($u);
my @tag_names;
if ( keys %$tags ) {
@tag_names = map { $_->{name} } values %$tags;
@tag_names = sort { lc $a cmp lc $b } @tag_names;
}
return DW::RPC->out( tags => \@tag_names );
}
sub load_state_codes_handler {
my $r = DW::Request->get;
my $post = $r->post_args;
my $country = $post->{country};
return DW::RPC->err("no country parameter") unless $country;
my %states;
my $states_type = $LJ::COUNTRIES_WITH_REGIONS{$country}->{type};
LJ::load_codes( { $states_type => \%states } ) if defined $states_type;
return DW::RPC->out(
states => [
map { $_, $states{$_} }
sort { $states{$a} cmp $states{$b} }
keys %states
],
head => LJ::Lang::ml('states.head.defined'),
);
}
sub profileexpandcollapse_handler {
my $r = DW::Request->get;
my $get = $r->get_args;
# if any opts aren't defined, they'll be passed in as empty strings
# (actually header and expand are sometimes undefined in my testing,
# hence the updates below. --kareila 2015/08/19)
my $mode = $get->{mode} eq "save" ? "save" : "load";
my $header = ( defined $get->{header} && $get->{header} eq "" ) ? undef : $get->{header};
my $expand = ( defined $get->{expand} && $get->{expand} eq "false" ) ? 0 : 1;
my $remote = LJ::get_remote();
return DW::RPC->out() unless $remote;
my $collapsed_headers = $remote->prop("profile_collapsed_headers") // '';
my @collapsed_headers = split( /,/, $collapsed_headers );
if ( $mode eq "save" ) {
return unless $header && $header =~ /_header$/;
$header =~ s/_header$//;
my %is_collapsed = map { $_ => 1 } @collapsed_headers;
# this header is already saved as expanded or collapsed, so we don't need to do anything
return if $is_collapsed{$header} && !$expand;
return if !$is_collapsed{$header} && $expand;
# remove header from list if expanding
# add header to list if collapsing
if ($expand) {
delete $is_collapsed{$header};
$remote->set_prop( profile_collapsed_headers => join( ",", keys %is_collapsed ) );
}
else { # collapse
$is_collapsed{$header} = 1;
$remote->set_prop( profile_collapsed_headers => join( ",", keys %is_collapsed ) );
}
return $r->OK;
}
else { # load
return DW::RPC->out( headers => \@collapsed_headers );
}
}
sub get_userpics_handler {
my $r = DW::Request->get;
my $get = $r->get_args;
my $remote = LJ::get_remote();
my $alt_u;
$alt_u = LJ::load_user( $get->{user} )
if $get->{user} && $remote->has_priv("supporthelp");
# get user
my $u = ( $alt_u || $remote );
return DW::RPC->alert("Sorry, you must be logged in to use this feature.")
unless $u;
# get userpics
my @userpics = LJ::Userpic->load_user_userpics($u);
my %upics = (); # info to return
$upics{pics} = {}; # upicid -> hashref of metadata
foreach my $upic (@userpics) {
next if $upic->inactive;
my $id = $upic->id;
$upics{pics}{$id} = {
url => $upic->url,
state => $upic->state,
width => $upic->width,
height => $upic->height,
# we don't want the full version of alttext here, because the keywords, etc
# will already likely be displayed by the icon
# We don't want to use ehtml, because we want the JSON converter
# handle escaping ", ', etc. We just escape the < and > ourselves
alt => LJ::etags( $upic->description ),
comment => LJ::strip_html( $upic->comment ),
id => $id,
keywords => [ map { LJ::strip_html($_) } $upic->keywords ],
};
}
$upics{ids} = [ sort { $a <=> $b } keys %{ $upics{pics} } ];
return DW::RPC->out(%upics);
}
sub widget_handler {
my $r = DW::Request->get;
my $get = $r->get_args;
my $post = $r->post_args;
return DW::RPC->err("Sorry widget AJAX is not enabled")
unless LJ::is_enabled('widget_ajax');
my $remote = LJ::get_remote();
my $widget_class = LJ::ehtml( $post->{_widget_class} || $get->{_widget_class} );
return DW::RPC->err("Invalid widget class $widget_class")
unless $widget_class =~ /^(IPPU::)?\w+$/gm;
$widget_class = "LJ::Widget::$widget_class";
return DW::RPC->err("Cannot do AJAX request to $widget_class")
unless $widget_class->ajax;
# hack to circumvent a bigip/perlbal interaction
# that sometimes closes keepalive POST requests under
# certain conditions. accepting GETs makes it work fine
if ( %$get && $widget_class->can_fake_ajax_post ) {
$post->clear;
$post->{$_} = $get->{$_} foreach keys %$get;
}
my $widget_id = $post->{_widget_id};
my $doing_post = delete $post->{_widget_post};
my %ret = (
_widget_id => $widget_id,
_widget_class => $widget_class,
);
# make sure that we're working with the right user
if ( $post->{authas} ) {
if ( $widget_class->authas ) {
my $u = LJ::get_authas_user( $post->{authas} );
return DW::RPC->err("Invalid user.") unless $u;
}
else {
return DW::RPC->err("Widget does not support authas authentication.");
}
}
if ($doing_post) {
# just a normal post request, handle it and then return status
local $LJ::WIDGET_NO_AUTH_CHECK = 1
if LJ::Auth->check_ajax_auth_token( $remote, "/_widget",
auth_token => delete $post->{auth_token} );
my %res;
# set because LJ::Widget->handle_post uses this global variable
@BMLCodeBlock::errors = ();
eval { %res = LJ::Widget->handle_post( $post, $widget_class ); };
$ret{res} = \%res;
$ret{errors} = $@ ? [$@] : \@BMLCodeBlock::errors;
$ret{_widget_post} = 1;
# generate new auth token for future requests if succesfully checked auth token
$ret{auth_token} = LJ::Auth->ajax_auth_token( $remote, "/_widget" )
if $LJ::WIDGET_NO_AUTH_CHECK;
}
if ( delete $post->{_widget_update} ) {
# render the widget and return it
# remove the widget prefix from the POST vars
foreach my $key ( keys %$post ) {
my $orig_key = $key;
if ( $key =~ s/^Widget\[\w+?\]_// ) {
$post->{$key} = $post->{$orig_key};
delete $post->{$orig_key};
}
}
$ret{_widget_body} = eval { $widget_class->render_body(%$post); };
$ret{_widget_body} = "Error: $@" if $@;
$ret{_widget_update} = 1;
}
return DW::RPC->out(%ret);
}
1;