# 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::Widget; use strict; use Carp; use LJ::ModuleLoader; use LJ::Auth; # FIXME: don't really need all widgets now LJ::ModuleLoader->require_subclasses("LJ::Widget"); LJ::ModuleLoader->require_subclasses("DW::Widget"); our $currentId = 1; # can pass in "id" opt to use instead of incrementing $currentId. # useful for when a widget will be created more than once but we want to keep its ID the same. sub new { my $class = shift; my %opts = @_; my $id = $opts{id} ? $opts{id} : $currentId++; return bless { id => $id }, $class; } sub need_res { return (); } sub need_res_opts { return (); } sub render_body { return ""; } sub start_form { my $class = shift; my %opts = @_; croak "Cannot call start_form on parent widget class" if $class eq "LJ::Widget"; my $eopts = ""; my $ehtml = $opts{noescape} ? 0 : 1; foreach my $attr ( grep { !/^(noescape)$/ && !/^(authas)$/ } keys %opts ) { $eopts .= " $attr=\"" . ( $ehtml ? LJ::ehtml( $opts{$attr} ) : $opts{$_} ) . "\""; } my $ret = "
"; $ret .= LJ::form_auth(); if ( $class->authas ) { my $u = $opts{authas} || $BMLCodeBlock::GET{authas} || $BMLCodeBlock::POST{authas}; $u = LJ::load_user($u) unless LJ::isu($u); my $authas = LJ::isu($u) ? $u->user : undef; if ( $authas && !$LJ::REQ_GLOBAL{widget_authas_form} ) { $ret .= $class->html_hidden( { name => "authas", value => $authas, id => "_widget_authas" } ); $LJ::REQ_GLOBAL{widget_authas_form} = 1; } } return $ret; } sub end_form { my $class = shift; croak "Cannot call end_form on parent widget class" if $class eq "LJ::Widget"; my $ret = "
"; return $ret; } # should this widget be rendered? # -- not a page logic decision sub should_render { my $class = shift; return $class->is_disabled ? 0 : 1; } # returns the dom id of this widget element sub widget_ele_id { my $class = shift; my $widget_id = ref $class ? $class->{id} : $currentId++; return "LJWidget_$widget_id"; } # render a widget, including its content wrapper sub render { my ( $class, @opts ) = @_; my $subclass = $class->subclass; my $css_subclass = lc($subclass); # figure out where "Odd number of elements in hash assignment" warning is coming from if ( scalar(@opts) % 2 == 1 ) { carp "Odd number of \@opts passed from $subclass"; } my %opt_hash = @opts; my $widget_ele_id = $class->widget_ele_id; return "" unless $class->should_render; my $ret = "
\n"; my $rv = eval { my $widget = $class; my $opts = { $widget->need_res_opts }; # include any resources that this widget declares foreach my $file ( $widget->need_res ) { if ( $file =~ m!^[^/]+\.(js|css)$!i ) { my $prefix = $1 eq 'js' ? "js" : "stc"; LJ::need_res( $opts, "$prefix/widgets/$subclass/$file" ); next; } LJ::need_res( $opts, $file ); } LJ::need_res( $opt_hash{stylesheet} ) if $opt_hash{stylesheet}; return $widget->render_body(@opts); }; if ( defined $rv && $rv =~ /\w/ ) { $ret .= $rv; } elsif ($@) { $ret .= "[Error: $@]handle_error; } else { return ""; } $ret .= "
\n"; return $ret; } sub post_fields_by_widget { my $class = shift; my %opts = @_; my $post = $opts{post}; my $widgets = $opts{widgets}; my $errors = $opts{errors}; my %per_widget = map { /^(?:LJ::Widget::)?(.+)$/; $1 => {} } @$widgets; my $eff_submit = undef; # per_widget is populated above for widgets which # are declared to be able to post to this page... if # it's not in the hashref then it's not whitelisted my $allowed = sub { my $wclass = shift; return 1 if $per_widget{$wclass}; push @$errors, "Submit from disallowed class: $wclass"; return 0; }; foreach my $key ( keys %$post ) { next unless $key; # FIXME: this is currently unused, but might be useful if ( $key =~ /^Widget_Submit_(.+)$/ ) { die "Multiple effective submits? class=$1" if $eff_submit; # is this class whitelisted? next unless $allowed->($1); $eff_submit = $1; next; } my ( $subclass, $field ) = $key =~ /^Widget(?:\[([\w]+)\])?_(.+)$/; next unless $subclass && $field; $subclass =~ s/_/::/g; # whitelisted widget class? next unless $allowed->($subclass); $per_widget{$subclass}->{$field} = $post->{$key}; } # now let's remove empty hashref placeholders from %per_widget while ( my ( $k, $v ) = each %per_widget ) { delete $per_widget{$k} unless %$v; } return \%per_widget; } sub post_fields_of_widget { my $class = shift; my $widget = shift; my $post = shift() || \%BMLCodeBlock::POST; my $errors = []; my $per_widget = LJ::Widget->post_fields_by_widget( post => $post, widgets => [$widget], errors => $errors ); return $per_widget->{$widget} || {}; } sub post_fields { my $class = shift; my $post = shift() || \%BMLCodeBlock::POST; my @widgets = ( $class->subclass ); my $errors = []; my $per_widget = LJ::Widget->post_fields_by_widget( post => $post, widgets => \@widgets, errors => $errors ); return $per_widget->{ $class->subclass } || {}; } sub get_args { my $class = shift; return \%BMLCodeBlock::GET; } sub get_effective_remote { my $class = shift; if ( $class->authas ) { return LJ::get_effective_remote(); } return LJ::get_remote(); } # call to have a widget process a form submission. this checks for formauth unless # an ajax auth token was already verified # returns hash returned from the last processed widget # pushes any errors onto @BMLCodeBlock::errors sub handle_post { my $class = shift; my $post = shift; my @widgets; # support for per-widget handle_post() options my %widget_opts = (); while (@_) { my $w = shift; if ( @_ && ref $_[0] ) { $widget_opts{$w} = shift(@_); } push @widgets, $w; } # no errors, return empty list return () unless LJ::did_post() && @widgets; # is this widget disabled? return () if $class->is_disabled; # require form auth for widget submissions my $errorsref = \@BMLCodeBlock::errors; unless ( LJ::check_form_auth( $post->{lj_form_auth} ) || $LJ::WIDGET_NO_AUTH_CHECK ) { push @$errorsref, LJ::Lang::ml('error.invalidform'); } my $per_widget = $class->post_fields_by_widget( post => $post, widgets => \@widgets, errors => $errorsref ); my %res; while ( my ( $class, $fields ) = each %$per_widget ) { eval { %res = "LJ::Widget::$class"->handle_post( $fields, %{ $widget_opts{$class} or {} } ); } or "LJ::Widget::$class"->handle_error( $@ => $errorsref ); } return %res; } # handles post vars for a widget, passes result of handle_post to render sub handle_post_and_render { my ( $class, $post, $widgetclass, %opts ) = @_; my %post_result = LJ::Widget->handle_post( $post, $widgetclass ); my $subclass = LJ::Widget::subclass($widgetclass); $opts{$_} = $post_result{$_} foreach keys %post_result; return "LJ::Widget::$subclass"->render(%opts); } *error = \&handle_error; sub handle_error { my ( $class, $errstr, $errref ) = @_; $errstr ||= $@; $errref ||= \@BMLCodeBlock::errors; return 0 unless $errstr; $errstr =~ s/\s+at\s+.+line \d+.*$//ig unless $LJ::IS_DEV_SERVER; push @$errref, $errstr; return 1; } sub error_list { my ( $class, @errors ) = @_; if (@errors) { $class->error($_) foreach @errors; } return @BMLCodeBlock::errors; } sub is_disabled { my $class = shift; my $subclass = $class->subclass; return 0 unless $subclass; return $LJ::WIDGET_DISABLED{$subclass} ? 1 : 0; } # returns the widget subclass name sub subclass { my $class = shift; $class = ref $class if ref $class; return $class unless $class =~ /::/; return ( $class =~ /(?:LJ|DW)::Widget::([\w:]+)$/ )[0]; } # wrapper around BML... for now sub decl_params { my $class = shift; return BML::decl_params(@_); } sub form_auth { my $class = shift; return LJ::form_auth(@_); } # override in subclasses with a string of JS to extend the widget subclass with sub js { '' } # override to return a true value if this widget accept AJAX posts sub ajax { 0 } # override if this widget can perform an AJAX request via GET instead of post sub can_fake_ajax_post { 0 } # override in subclasses that support authas authentication sub authas { 0 } # instance method to return javascript for this widget # "page_js_obj" opt: # The JS object that is defined by the page the widget is in. # Used to create a variable "." which holds # this widget's JS object. Then the page can call functions that are # on specific widgets. sub wrapped_js { my $self = shift; my %opts = @_; croak "wrapped_js is an instance method" unless ref $self; my $widgetid = $self->widget_ele_id or return ''; my $widgetclass = $self->subclass; my $js = $self->js or return ''; my $authtoken = LJ::Auth->ajax_auth_token( LJ::get_remote(), "/_widget" ); $authtoken = LJ::ejs($authtoken); LJ::need_res(qw(js/ljwidget.js)); my $widgetvar = "LJWidget.widgets[\"$widgetid\"]"; my $widget_js_obj = $opts{page_js_obj} ? "$opts{page_js_obj}.$widgetclass = $widgetvar;" : ""; return qq { }; } # allows given form fields to be passed into the widget's handle_post, even if they don't have the widget prefix on them # this is needed for recaptcha modules in widgets sub use_specific_form_fields { my $class = shift; my %opts = @_; my $post = $opts{post}; my $widget = $opts{widget}; my %given_fields = map { $_ => 1 } @{ $opts{fields} }; foreach my $field (%$post) { $post->{"Widget[$widget]_$field"} = $post->{$field} if $given_fields{$field}; } return; } package LJ::Error::WidgetError; use strict; use base qw(LJ::Error); sub fields { qw(errstr) } sub new { my $class = shift; my ( $errstr, %opts ) = @_; my $self = { errstr => $errstr }; return bless $self, $class; } sub as_html { my $self = shift; return $self->{errstr}; } ################################################## # htmlcontrols-like utility methods package LJ::Widget; use strict; # most of these are flat wrappers, but swapping in a valid 'name' sub _html_star { my $class = shift; my $func = shift; my %opts = @_; croak "Cannot call htmlcontrols-like utility method on parent widget class" if $class eq "LJ::Widget"; my $prefix = $class->input_prefix; $opts{name} = "${prefix}_$opts{name}"; return $func->( \%opts ); } sub _html_star_list { my $class = shift; my $func = shift; my @params = @_; croak "Cannot call htmlcontrols-like utility method on parent widget class" if $class eq "LJ::Widget"; # If there's only one (non-ref) element in @params, then there # is no name for the field and nothing should be changed. unless ( @params == 1 && !ref $params[0] ) { my $prefix = $class->input_prefix; my $is_name = 1; # if true, the next element we'll check is a name (not a value) foreach my $el (@params) { if ( ref $el ) { $el->{name} = "${prefix}_$el->{name}" if $el->{name}; $is_name = 1; next; } if ($is_name) { $el = "${prefix}_$el"; $is_name = 0; } else { $is_name = 1; } } } return $func->(@params); } sub html_text { my $class = shift; return $class->_html_star( \&LJ::html_text, @_ ); } sub html_check { my $class = shift; return $class->_html_star( \&LJ::html_check, @_ ); } sub html_textarea { my $class = shift; return $class->_html_star( \&LJ::html_textarea, @_ ); } sub html_color { my $class = shift; return $class->_html_star( \&LJ::html_color, @_ ); } sub input_prefix { my $class = shift; my $subclass = $class->subclass; $subclass =~ s/::/_/g; return 'Widget[' . $subclass . ']'; } sub html_select { my $class = shift; croak "Cannot call htmlcontrols-like utility method on parent widget class" if $class eq "LJ::Widget"; my $prefix = $class->input_prefix; # old calling method, exact wrapper around html_select if ( ref $_[0] ) { my $opts = shift; $opts->{name} = "${prefix}_$opts->{name}"; return LJ::html_select( $opts, @_ ); } # newer calling method, no hashref w/ list as list => [ ... ] my %opts = @_; my $list = delete $opts{list}; $opts{name} = "${prefix}_$opts{name}"; return LJ::html_select( \%opts, @$list ); } sub html_datetime { my $class = shift; return $class->_html_star( \&LJ::html_datetime, @_ ); } sub html_hidden { my $class = shift; return $class->_html_star_list( \&LJ::html_hidden, @_ ); } sub html_submit { my $class = shift; return $class->_html_star_list( \&LJ::html_submit, @_ ); } ################################################## # Utility methods for getting/setting ML strings # in the 'widget' ML domain # -- these are usually living in a db table somewhere # and input by an admin who wants translateable text sub ml_key { my $class = shift; my $key = shift; croak "invalid key: $key" unless $key; my $ml_class = lc $class->subclass; return "widget.$ml_class.$key"; } sub ml_remove_text { my $class = shift; my $ml_key = shift; my $ml_dmid = $class->ml_dmid; my $root_lncode = $class->ml_root_lncode; return LJ::Lang::remove_text( $ml_dmid, $ml_key, $root_lncode ); } sub ml_set_text { my $class = shift; my ( $ml_key, $text ) = @_; # create new translation system entry my $ml_dmid = $class->ml_dmid; my $root_lncode = $class->ml_root_lncode; # call web_set_text, though there shouldn't be any # commits going on since this is the 'widget' dmid return LJ::Lang::web_set_text( $ml_dmid, $root_lncode, $ml_key, $text, { changeseverity => 1, childrenlatest => 1 } ); } sub ml_dmid { my $class = shift; my $dom = LJ::Lang::get_dom("widget"); return $dom->{dmid}; } sub ml_root_lncode { my $class = shift; my $ml_dom = LJ::Lang::get_dom("widget"); my $root_lang = LJ::Lang::get_root_lang($ml_dom); return $root_lang->{lncode}; } # override LJ::Lang::is_missing_string to return true # if the string equals the class name (the fallthrough # for LJ::Widget->ml) sub ml_is_missing_string { my $class = shift; my $string = shift; $class =~ /.+::(\w+)$/; return $string eq $1 || LJ::Lang::is_missing_string($string); } # this function should be used when getting any widget ML string # -- it's really just a wrapper around LJ::Lang::ml or BML::ml, # but it does nice things like falling back to global definition # -- also allows getting of strings from the 'widget' ML domain # for text which was dynamically defined by an admin sub ml { my ( $class, $code, $vars ) = @_; # can pass in a string and check 3 places in order: # 1) widget.foo.text => general .widget.foo.text (overridden by current page) # 2) widget.foo.text => general widget.foo.text (defined in en(_LJ).dat) # 3) widget.foo.text => widget widget.foo.text (user-defined by a tool) # whether passed with or without a ".", eat that immediately $code =~ s/^\.//; # 1) try with a ., for current page override in 'general' domain # 2) try without a ., for global version in 'general' domain foreach my $curr_code ( ".$code", $code ) { my $string = LJ::Lang::ml( $curr_code, $vars ); return "" if $string eq "_none"; return $string unless LJ::Lang::is_missing_string($string); } # 3) now try with "widget" domain for user-entered translation string my $dmid = $class->ml_dmid; my $lncode = LJ::Lang::get_effective_lang(); my $string = LJ::Lang::get_text( $lncode, $code, $dmid, $vars ); return "" if $string eq "_none"; return $string unless LJ::Lang::is_missing_string($string); # return the class name if we didn't find anything $class =~ /.+::(\w+)$/; return $1; } 1; __END__ =head1 NAME LJ::Widget - parent class for areas of contained information and code (widgets) to be used on one or more BML pages =head1 SYNOPSIS LJ::Widget::WidgetName->render; LJ::Widget::AnotherWidget->render( options ); LJ::Widget->handle_post(\%POST, qw( WidgetName AnotherWidget )); my $widget = LJ::Widget::AjaxWidget->new; $headextra .= $widget->wrapped_js( options ); $widget->render; =head1 DESCRIPTION This is the parent class for widgets. A widget is a part of a BML page that can be relatively self-contained and is sometimes used on multiple pages. Using a widget instead of putting the code directly in a BML page allows more flexibility in terms of re-using code and readability. It is much easier to read and understand a BML page with calls to a couple of widgets than a BML page with large blocks of unrelated code. Widgets can do POST actions to themselves or to other widgets, but the goal is to keep the function of each widget relatively simple. POST form elements in a widget are given widget-specific prefixes in their names. These are then removed when the different POST values are being checked in C. AJAX POSTs go to the endpoint "widget.bml", and they perform form auths differently than non-AJAX POSTs do. Strings within widgets can and should be English-stripped. Usually, these strings are defined within en.dat or en_LJ.dat with the string name of "widget.$widgetname.$stringname". However, these strings can also be defined in BML pages, which will override what's defined in en(_LJ).dat. Strings in the "widget" ML domain get there when a user inputs text that should be translatable in a widget web form on the site. In almost all cases, methods are called on subclasses of this parent class, and not on the parent class itself. It is explicitly noted when a method should be called on the parent class instead of on a subclass. Also, methods that can be and are often subclassed are noted as such. Most other methods can be subclassed, but it's probably not particularly useful to do so. =head1 CONSTRUCTOR =over 4 =item C This is only needed when you want to create a widget without actually rendering it yet. It is usually called with no options, but you can pass an C to give the widget a defined ID (number) instead of an auto-generated one (useful if you're using AJAX and widgets get re-rendered and you don't want IDs to change). =back =head1 METHODS =over 4 =item C Renders a widget's display. It wraps the output of C with a div and includes the files defined in C. It will return an empty string if C returns false. Options passed to it will be passed on to C. =item C This is called when C is called. It returns the HTML/BML that should be printed when a widget is rendered. Can be subclassed. =item C Returns if this widget should render or not. It cannot be passed any parameters. It is called automatically when C is called, and by default it will return false if the widget is disabled via C. Can be subclassed. =item C Code that's run when a widget that POSTs is submitted. This should be called on the parent class instead of on the specific widget, and the widget(s) you want to be handled should be passed as parameters. The parent class method calls the subclass methods appropriately. Returns the hash returned from the last processed widget. Can be subclassed. =item C This is called on the parent class, and it handles the POST for a single given widget and returns the results of that POST to C. =item C Returns a list of paths to static files that should be included on the page that the widget is called on (i.e. CSS and JS). Can be subclassed. =item C Returns a hash of opts that can be passed to need_res -- for example, ( group => 'jquery' ). Can be subclassed. =item C Returns a hashref of the POST fields for each widget that was handled via C when a POST action occurs. Generally only used as a helper method. Should be called on the parent class. =item C Returns the POST fields for a specific given widget handled via C. Should be called on the parent class. =item C Same as C, but returns the POST fields for the widget it's called on. =item C Given the POST values and a list of specific fields, this will allow those fields to be passed into the widget's C even if they don't have the necessary widget prefix on them. This is currently used for widgets that have a reCAPTCHA module, since you can't modify the name of the fields for it. =item C Returns the GET args of the page the widget is on. =item C If the widget is an C widget, it returns the currently authenticated user (remote or a journal remote manages). Otherwise, it returns remote. =item C Pushes an error onto a given arrayref of errors (or @BMLCodeBlock::errors) for display. =item C Returns a list of errors for a widget, using C to build up the list in @BMLCodeBlock::errors. =item C Returns if a widget is disabled or not based on a config hash value. =item C Given a widget package name, returns the name of the widget subclass. Example: giving "LJ::Widget::WidgetName" would return "WidgetName". =item C Returns the HTML id attribute for this widget. =item C Wrapper around BML::decl_params(). =item C Wrapper around LJ::form_auth(). =back =head2 AJAX-Related Methods =over 4 =item C Returns a string of JavaScript for a widget so it does not have to be included as a separate file. Can be subclassed. =item C Returns the JavaScript that's in C. Also sets up JavaScript so that AJAX widgets can be used. If a C parameter is passed in, its value is used to create a JavaScript variable that holds the widget JavaScript object in it. =back =head2 Flags for Widgets =over 4 =item C Returns if the widget can accept AJAX POSTs or not. Can be subclassed. =item C Returns if a widget can perform AJAX requests via GET instead of POST or not. Can be subclassed. =item C Returns if a widget supports authas authentication or not (in GET or POST). Can be subclassed. =back =head2 Form Utility Methods =over 4 =item C Returns HTML for the start of a form (including form auth) that POSTs to a widget. Can be passed options similar to that of htmlcontrols methods. =item C Returns HTML for the end of a form that POSTs to a widget. =item C Widget-specific HTML text field. Must be used in place of LJ::html_text() if C is being used. =item C Widget-specific HTML text checkbox/radio button. Must be used in place of LJ::html_check() if C is being used. =item C Widget-specific HTML text area. Must be used in place of LJ::html_textarea() if C is being used. =item C Widget-specific HTML color field. Must be used in place of LJ::html_color() if C is being used. =item C Widget-specific HTML selection box. Must be used in place of LJ::html_select() if C is being used. =item C Widget-specific HTML datetime field. Must be used in place of LJ::html_datetime() if C is being used. =item C Widget-specific HTML hidden field. Must be used in place of LJ::html_hidden() if C is being used. =item C Widget-specific HTML submit button. Must be used in place of LJ::html_submit() if C is being used. =item C The prefix that's added on to form element names to make them widget-specific. =back =head2 Translation String Methods =over 4 =item C The full ML key for a widget string, given the part of the key that's specific to the string. =item C Removes a translation string from the widget ML domain. =item C Adds a translation string to the widget ML domain. =item C Domain ID for the widget ML domain. =item C Root language for the widget ML domain. =item C Returns if a widget string is missing or not. =item C Returns the translation string for a given ML key. Can be a general string defined by the page or in en(_LJ).dat, or a string in the widget domain that was defined by a user via a tool. =back =head1 EXAMPLES See these widgets for some basic examples of different types of widgets: cgi-bin/LJ/Widget/ExampleRenderWidget.pm cgi-bin/LJ/Widget/ExamplePostWidget.pm cgi-bin/LJ/Widget/ExampleAjaxWidget.pm