[LON-CAPA-cvs] cvs: doc /loncapafiles loncapafiles.lpml loncom/interface loncommon.pm lonextresedit.pm lonexturlcheck.pm lonsyllabus.pm rat lonwrapper.pm

raeburn raeburn at source.lon-capa.org
Wed May 1 22:12:43 EDT 2019


raeburn		Thu May  2 02:12:43 2019 EDT

  Added files:                 
    /loncom/interface	lonexturlcheck.pm 

  Modified files:              
    /loncom/interface	lonextresedit.pm loncommon.pm lonsyllabus.pm 
    /rat	lonwrapper.pm 
    /doc/loncapafiles	loncapafiles.lpml 
  Log:
  - Bug 6910
  Gracefully handle display (and preview) for External Resources for which  
  Content-Security-Policy or X-Frame-Options prevent display in iframe in LC.
  
  
-------------- next part --------------
Index: loncom/interface/lonextresedit.pm
diff -u loncom/interface/lonextresedit.pm:1.27 loncom/interface/lonextresedit.pm:1.28
--- loncom/interface/lonextresedit.pm:1.27	Wed Nov  7 18:56:48 2018
+++ loncom/interface/lonextresedit.pm	Thu May  2 02:12:18 2019
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Documents
 #
-# $Id: lonextresedit.pm,v 1.27 2018/11/07 18:56:48 raeburn Exp $
+# $Id: lonextresedit.pm,v 1.28 2019/05/02 02:12:18 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -767,6 +767,12 @@
         invurl  => 'Invalid URL',
         titbl   => 'Title is blank',
         invtool => 'Please select an external tool',
+        mixfra  => 'Show preview in pop-up? (http in https page + no framing)',
+        mixonly => 'Show preview in pop-up? (http in https page)',
+        fraonly => 'Show preview in pop-up? (framing disallowed)',
+        nopopup => 'Pop-up blocked',
+        nopriv  => 'Insufficient privileges to use preview',
+        badurl  => 'URL is not: http://hostname/path or https://hostname/path',
     );
     &js_escape(\%js_lt);
 
@@ -959,17 +965,74 @@
         var url = document.getElementById(caller).value;
         if (regexp.test(url)) {
             var http_regex = /^http\:\/\//gi;
+            var mixed = 0;
+            var noiframe = 0;
+            var nopriv = 0;
+            var badurl = 0;
+            var name = "externalpreview";
             if ((protocol == 'https') && (http_regex.test(url))) {
-                window.open(url,"externalpreview","height=400,width=500,scrollbars=1,resizable=1,menubar=0,location=1");
-            } else {
-                openMyModal(url,500,400,'yes');
+                mixed = 1;
+            }
+            var http = new XMLHttpRequest();
+            var lcurl = "/adm/exturlcheck";
+            var params = "exturl="+url;
+            http.open("POST",lcurl, true);
+            http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+            http.onreadystatechange = function() {
+                if (http.readyState == 4) {
+                    if (http.status == 200) {
+                        if (http.responseText == 1) {
+                            noiframe = 1;
+                        } else if (http.responseText == -1) {
+                            nopriv = 1;
+                        } else if (http.responseText == 0) {
+                            badurl = 1;
+                        }
+                        openPreviewWindow(url,name,noiframe,mixed,nopriv,badurl);
+                    }
+                }
             }
+            http.send(params);
         } else {
             alert("$js_lt{'invurl'}");
         }
     }
 }
 
+var previewLCWindow = null;
+function openPreviewWindow(url,name,noiframe,mixed,nopriv,badurl) {
+    if (previewLCWindow !=null) {
+        previewLCWindow.close();
+    }
+    if (badurl) {
+        alert("$js_lt{'badurl'}");
+    } else if (nopriv) {
+        alert("$js_lt{'nopriv'}");
+    } else if ((noiframe == 1) || (mixed == 1)) {
+        var encurl = encodeURI(url);
+        var msg;
+        if (mixed == 1) {
+            if (noiframe == 1) {
+                msg = "$js_lt{'mixfra'}";
+            } else {
+                msg = "$js_lt{'mixonly'}";
+            }
+        } else {
+            msg = "$js_lt{'fraonly'}";
+        }
+        if (confirm(msg)) {
+            previewLCWindow = window.open(url,name,"height=400,width=500,scrollbars=1,resizable=1,menubar=0,location=1");
+            if (previewLCWindow != null) {
+                previewLCWindow.focus();
+            } else {
+                alert("$js_lt{'nopopup'}");
+            }
+        }
+    } else {
+        openMyModal(url,500,400,'yes');
+    }
+}
+
 function updateExttool(caller,form,supplementalflag) {
     var prefix = '';
     if (supplementalflag == 1) {
Index: loncom/interface/loncommon.pm
diff -u loncom/interface/loncommon.pm:1.1327 loncom/interface/loncommon.pm:1.1328
--- loncom/interface/loncommon.pm:1.1327	Wed Apr 24 01:44:30 2019
+++ loncom/interface/loncommon.pm	Thu May  2 02:12:18 2019
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1327 2019/04/24 01:44:30 raeburn Exp $
+# $Id: loncommon.pm,v 1.1328 2019/05/02 02:12:18 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -72,6 +72,7 @@
 use Apache::courseclassifier();
 use LONCAPA qw(:DEFAULT :match);
 use LONCAPA::LWPReq;
+use HTTP::Request;
 use DateTime::TimeZone;
 use DateTime::Locale;
 use Encode();
@@ -18191,6 +18192,120 @@
     return $init;
 }
 
+sub is_nonframeable {
+    my ($url,$absolute,$hostname,$ip) = @_;
+    my $uselink;
+    my $request = new HTTP::Request('HEAD',$url);
+    my $response = &LONCAPA::LWPReq::makerequest('',$request,'','',5);
+    if ($response->is_success()) {
+        my $secpolicy = lc($response->header('content-security-policy'));
+        my $xframeop = lc($response->header('x-frame-options'));
+        $secpolicy =~ s/^\s+|\s+$//g;
+        $xframeop =~ s/^\s+|\s+$//g;
+        if (($secpolicy ne '') || ($xframeop ne '')) {
+            my ($remotehost) = ($url =~ m{^(https?\://[^/?#]+)});
+            $remotehost = lc($remotehost);
+            my ($origin,$protocol,$port);
+            if ($ENV{'SERVER_PORT'} =~/^\d+$/) {
+                $port = $ENV{'SERVER_PORT'};
+            } else {
+                $port = 80;
+            }
+            if ($absolute eq '') {
+                $protocol = 'http:';
+                if ($port == 443) {
+                    $protocol = 'https:';
+                }
+                $origin = $protocol.'//'.lc($hostname);
+            } else {
+                $origin = lc($absolute);
+                ($protocol,$hostname) = ($absolute =~ m{^(https?:)//([^/]+)$});
+            }
+            if (($secpolicy) && ($secpolicy =~ /\Qframe-ancestors\E([^;]*)(;|$)/)) {
+                my $framepolicy = $1;
+                $framepolicy =~ s/^\s+|\s+$//g;
+                my @policies = split(/\s+/,$framepolicy);
+                if (@policies) {
+                    if (grep(/^\Q'none'\E$/, at policies)) {
+                        $uselink = 1;
+                    } else {
+                        $uselink = 1;
+                        if ((grep(/^\Q*\E$/, at policies)) || (grep(/^\Q$protocol\E$/, at policies)) ||
+                                (($origin ne '') && (grep(/^\Q$origin\E$/, at policies))) ||
+                                (($ip ne '') && (grep(/^\Q$ip\E$/, at policies)))) {
+                            undef($uselink);
+                        }
+                        if ($uselink) {
+                            if (grep(/^\Q'self'\E$/, at policies)) {
+                                if (($origin ne '') && ($remotehost eq $origin)) {
+                                    undef($uselink);
+                                }
+                            }
+                        }
+                        if ($uselink) {
+                            my @possok;
+                            if ($ip ne '') {
+                                push(@possok,$ip);
+                            }
+                            my $hoststr = '';
+                            foreach my $part (reverse(split(/\./,$hostname))) {
+                                if ($hoststr eq '') {
+                                    $hoststr = $part;
+                                } else {
+                                    $hoststr = "$part.$hoststr";
+                                }
+                                if ($hoststr eq $hostname) {
+                                    push(@possok,$hostname);
+                                } else {
+                                    push(@possok,"*.$hoststr");
+                                }
+                            }
+                            if (@possok) {
+                                foreach my $poss (@possok) {
+                                    last if (!$uselink);
+                                    foreach my $policy (@policies) {
+                                        if ($policy =~ m{^(\Q$protocol\E//|)\Q$poss\E(\Q:$port\E|)$}) {
+                                            undef($uselink);
+                                            last;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            } elsif ($xframeop ne '') {
+                $uselink = 1;
+                my @policies = split(/\s*,\s*/,$xframeop);
+                if (@policies) {
+                    unless (grep(/^deny$/, at policies)) {
+                        if ($origin ne '') {
+                            if (grep(/^sameorigin$/, at policies)) {
+                                if ($remotehost eq $origin) {
+                                    undef($uselink);
+                                }
+                            }
+                            if ($uselink) {
+                                foreach my $policy (@policies) {
+                                    if ($policy =~ /^allow-from\s*(.+)$/) {
+                                        my $allowfrom = $1;
+                                        if (($allowfrom ne '') && ($allowfrom eq $origin)) {
+                                            undef($uselink);
+                                            last;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return $uselink;
+}
+
+
 1;
 __END__;
 
Index: loncom/interface/lonsyllabus.pm
diff -u loncom/interface/lonsyllabus.pm:1.145 loncom/interface/lonsyllabus.pm:1.146
--- loncom/interface/lonsyllabus.pm:1.145	Thu Dec 27 20:10:31 2018
+++ loncom/interface/lonsyllabus.pm	Thu May  2 02:12:19 2019
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # Syllabus
 #
-# $Id: lonsyllabus.pm,v 1.145 2018/12/27 20:10:31 raeburn Exp $
+# $Id: lonsyllabus.pm,v 1.146 2019/05/02 02:12:19 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -177,7 +177,7 @@
                         $brcrum =
                             &Apache::lonhtmlcommon::docs_breadcrumbs(undef,$crstype,undef,$title,1);
                     }
-                    $r->print(&Apache::lonwrapper::wrapper($item,$brcrum,$env{'request.use_absolute'},
+                    $r->print(&Apache::lonwrapper::wrapper($r,$item,$brcrum,$env{'request.use_absolute'},
                                                            undef,$is_pdf,undef,&mt('Syllabus')));
                 }
             }
@@ -204,7 +204,7 @@
                     $brcrum =
                         &Apache::lonhtmlcommon::docs_breadcrumbs(undef,$crstype,undef,$title,1);
                 }
-                $r->print(&Apache::lonwrapper::wrapper($external,$brcrum,$env{'request.use_absolute'},
+                $r->print(&Apache::lonwrapper::wrapper($r,$external,$brcrum,$env{'request.use_absolute'},
                                                        $is_ext,$is_pdf,undef,&mt('Syllabus')));
             }
             return OK;
Index: rat/lonwrapper.pm
diff -u rat/lonwrapper.pm:1.68 rat/lonwrapper.pm:1.69
--- rat/lonwrapper.pm:1.68	Sat Dec 30 00:16:36 2017
+++ rat/lonwrapper.pm	Thu May  2 02:12:31 2019
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Wrapper for external and binary files as standalone resources
 #
-# $Id: lonwrapper.pm,v 1.68 2017/12/30 00:16:36 raeburn Exp $
+# $Id: lonwrapper.pm,v 1.69 2019/05/02 02:12:31 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -44,7 +44,7 @@
 
 # ================================================================ Main Handler
 sub wrapper {
-    my ($url,$brcrum,$absolute,$is_ext,$is_pdf,$exttool,$linktext,$explanation,
+    my ($r,$url,$brcrum,$absolute,$is_ext,$is_pdf,$exttool,$linktext,$explanation,
         $title,$width,$height) = @_;
 
     my $forcereg;
@@ -56,7 +56,7 @@
                                           'show' => 'Show content in pop-up window',
                                         );
 
-    my $anchor;
+    my ($anchor,$uselink);
     if ($is_ext) {
         if ($env{'form.symb'}) {
             (undef,undef,my $res) = &Apache::lonnet::decode_symb($env{'form.symb'});
@@ -66,6 +66,12 @@
         } elsif ($env{'form.anchor'} ne '') {
             $anchor = '#'.$env{'form.anchor'};
         }
+        unless (($is_pdf) && ($env{'browser.mobile'})) {
+            my $hostname = $r->hostname();
+            my $lonhost = $r->dir_config('lonHostID');
+            my $ip = &Apache::lonnet::get_host_ip($lonhost);
+            $uselink = &Apache::loncommon::is_nonframeable($url,$absolute,$hostname,$ip);
+        }
     }
 
     my $noiframe = &Apache::loncommon::modal_link($url.$anchor,$lt{'show'},500,400);
@@ -132,22 +138,21 @@
     my $startpage = &Apache::loncommon::start_page('Menu',undef,$args).$countdown.$donemsg;
     my $endpage = &Apache::loncommon::end_page();
 
+    if (($uselink) && ($title eq '')) {
+        if ($env{'form.symb'}) {
+            $title=&Apache::lonnet::gettitle($env{'form.symb'});
+        } else {
+            my $symb=&Apache::lonnet::symbread($r->uri);
+            if ($symb) {
+                $title=&Apache::lonnet::gettitle($symb);
+            }
+        }
+    }
     if (($env{'browser.mobile'}) || ($exttool eq 'window') || ($exttool eq 'tab')) {
         my $output = $startpage;
         if ($is_pdf) {
-            if ($title eq '') {
-                $title = $env{'form.title'};
-                if ($title eq '') {
-                    unless ($env{'request.enc'}) {
-                        ($title) = ($url =~ m{/([^/]+)$});
-                        $title =~ s/(\?[^\?]+)$//;
-                    }
-                }
-            }
-            unless ($title eq '') {
-                $output .= $title.'<br />';
-            }
-            $output .= '<a href="'.$url.'">'.&mt('Link to PDF (for mobile devices)').'</a>';
+            $linktext = &mt('Link to PDF (for mobile devices)');
+            $output .= &create_link($url,$anchor,$title,$linktext);
         } elsif (($exttool eq 'window') || ($exttool eq 'tab')) {
             if ($linktext eq '') {
                 $linktext = &mt('Launch External Tool');
@@ -193,15 +198,23 @@
                 $output .= &Apache::lonfeedback::list_discussion('tool','OPEN');
             }
         } else {
-            my $dest = &HTML::Entities::encode($url.$anchor,'&<>"');
-            $output .= '<div style="overflow:scroll; -webkit-overflow-scrolling:touch;">'."\n".
-                       '<iframe src="'.$dest.'" height="100%" width="100%" frameborder="0">'."\n".
-                       "$lt{'noif'} $noiframe\n".
-                       "</iframe>\n".
-                       "</div>\n";
+            if ($uselink) {
+                $linktext = &mt('Link to resource');
+                $output .= &create_link($url,$anchor,$title,$linktext);
+            } else {
+                my $dest = &HTML::Entities::encode($url.$anchor,'&<>"');
+                $output .= '<div style="overflow:scroll; -webkit-overflow-scrolling:touch;">'."\n".
+                           '<iframe src="'.$dest.'" height="100%" width="100%" frameborder="0">'."\n".
+                           "$lt{'noif'} $noiframe\n".
+                           "</iframe>\n".
+                           "</div>\n";
+            }
         }
         $output .= $endpage;
         return $output;
+    } elsif ($uselink) {
+        $linktext = &mt('Link to resource');
+        return $startpage.&create_link($url,$anchor,$title,$linktext).$endpage;
     } else {
         my $offset = 5;
         &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['inhibitmenu']);
@@ -246,6 +259,26 @@
     }
 }
 
+sub create_link {
+    my ($url,$anchor,$title,$linktext) = @_;
+    my $shownlink;
+    if ($title eq '') {
+        $title = $env{'form.title'};
+        if ($title eq '') {
+            unless ($env{'request.enc'}) {
+                ($title) = ($url =~ m{/([^/]+)$});
+                $title =~ s/(\?[^\?]+)$//;
+            }
+        }
+    }
+    unless ($title eq '') {
+        $shownlink = '<span style="font-weight:bold;">'.$title.'</span><br />';
+    }
+    my $dest = &HTML::Entities::encode($url.$anchor,'&<>"');
+    $shownlink .= '<a href="'.$dest.'">'.$linktext.'</a>';
+    return $shownlink;
+}
+
 sub handler {
     my $r=shift;
     &Apache::loncommon::content_type($r,'text/html');
@@ -264,7 +297,6 @@
         s|:|:|g;              
     }
 
-
     if ($url =~ /\.pdf$/i) {
         $is_pdf = 1;
     } elsif ($url =~ m{^/adm/($match_domain)/($match_courseid)/(\d+)/ext\.tool$}) {
@@ -367,7 +399,7 @@
             &Apache::lonenc::check_encrypt(\$url);
         }
 
-        $r->print( wrapper($url,$brcrum,$absolute,$is_ext,$is_pdf,$exttool,
+        $r->print( wrapper($r,$url,$brcrum,$absolute,$is_ext,$is_pdf,$exttool,
                            $linktext,$explanation,undef,$width,$height) );
 
     } # not just the menu
@@ -395,10 +427,14 @@
 
 =over
 
-=item wrapper($url,$brcrum,$absolute,$is_ext,$is_pdf,$linktext,$explanation,$title,$width,$height)
+=item wrapper($r,$url,$brcrum,$absolute,$is_ext,$is_pdf,$linktext,$explanation,$title,$width,$height)
 
 =over
 
+=item $r
+
+request object
+
 =item $url
 
 url to display by including in an iframe within a
Index: doc/loncapafiles/loncapafiles.lpml
diff -u doc/loncapafiles/loncapafiles.lpml:1.984 doc/loncapafiles/loncapafiles.lpml:1.985
--- doc/loncapafiles/loncapafiles.lpml:1.984	Tue Apr  9 15:54:12 2019
+++ doc/loncapafiles/loncapafiles.lpml	Thu May  2 02:12:42 2019
@@ -2,7 +2,7 @@
  "http://lpml.sourceforge.net/DTD/lpml.dtd">
 <!-- loncapafiles.lpml -->
 
-<!-- $Id: loncapafiles.lpml,v 1.984 2019/04/09 15:54:12 raeburn Exp $ -->
+<!-- $Id: loncapafiles.lpml,v 1.985 2019/05/02 02:12:42 raeburn Exp $ -->
 
 <!--
 
@@ -2590,6 +2590,15 @@
 <status>works/unverified</status>
 </file>
 <file>
+<source>loncom/interface/lonexturlcheck.pm </source>
+<target dist='default'>home/httpd/lib/perl/Apache/lonexturlcheck.pm</target>
+<categoryname>handler</categoryname>
+<description>
+Handler to check if an external resource can be displayed in an iframe
+</description>
+<status>works/unverified</status>
+</file>
+<file>
 <source>loncom/interface/lonexttool.pm</source>
 <target dist='default'>home/httpd/lib/perl/Apache/lonexttool.pm</target>
 <categoryname>handler</categoryname>

Index: loncom/interface/lonexturlcheck.pm
+++ loncom/interface/lonexturlcheck.pm
# The LearningOnline Network with CAPA
# Handler to check if external resource can be shown in iframe
#
# $Id: lonexturlcheck.pm,v 1.1 2019/05/02 02:12:19 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#
#
###############################################################
###############################################################

=pod

=head1 NAME

Apache::lonexturlcheck - External Resource URL checker

=head1 SYNOPSIS

Called in course context by course personnel either with the course editing
privilege or with view-only access to course editing tools.

Query string contains one item: name=exturl, value=URL of external resource
(format: http://hostname/path or https://hostname/path). 
 
The resource URL is sent to &loncommon::is_nonframeable() to check whether
it can be displayed in an iframe in a page served by the current host. 

=head1 OVERVIEW

Input: external resource URL (from query string passed to /adm/exturlcheck).

Hostname, lonHostID, and IP address for this node are retrieved from Apache.

Dependencies: calls &loncommon::is_nonframeable() to check if server where
external resource is hosted is configured with a Content-Security-Policy or 
with X-Frame-options settings which prohibit display of the resource within
an iframe in a LON-CAPA page served from this node. 

Output to print buffer: (content-type: text/plain):  1, 0, -1 or empty string.
'' -- display in iframe is allowed
1  -- display in iframe not allowed 
0  -- invalid URL
-1 -- could not verify course editing privilege or view-only access to 
      course editing tools

HTTP Return codes: 
406 -- if user is not in a course
200 -- otherwise

=cut

package Apache::lonexturlcheck;

use strict;
use Apache::Constants qw(:common :http);
use Apache::lonnet;
use Apache::loncommon;
use LONCAPA::LWPReq;
use HTTP::Request;

sub handler {
    my $r=shift;
    if ($r->header_only) {
        &Apache::loncommon::content_type($r,'text/html');
        $r->send_http_header;
        return OK;
    }
    if (!$env{'request.course.fn'}) {
        # Not in a course.
        $env{'user.error.msg'}="/adm/lonexturlcheck:bre:0:0:Not in a course";
        return HTTP_NOT_ACCEPTABLE;
    }
    &Apache::loncommon::content_type($r,'text/plain');
    $r->send_http_header;
    my $uselink;
    if (($env{'request.course.id'}) &&
        ((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) ||
         (&Apache::lonnet::allowed('cev',$env{'request.course.id'})))) {
        &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['exturl']);
        if ($env{'form.exturl'} =~ m{^https?\://[^/]+}) {
            my $hostname = $r->hostname();
            my $lonhost = $r->dir_config('lonHostID');
            my $ip = &Apache::lonnet::get_host_ip($lonhost);
            $r->print(&Apache::loncommon::is_nonframeable($env{'form.exturl'},'',$hostname,$ip));
        } else {
            $r->print(0);
        }
    } else {
        $r->print(-1);
    }
    return OK;
}

1;


More information about the LON-CAPA-cvs mailing list