--- loncom/interface/lonnavmaps.pm 2018/04/29 15:52:08 1.509.2.6 +++ loncom/interface/lonnavmaps.pm 2022/01/01 04:14:05 1.509.2.14.2.1 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.509.2.6 2018/04/29 15:52:08 raeburn Exp $ +# $Id: lonnavmaps.pm,v 1.509.2.14.2.1 2022/01/01 04:14:05 raeburn Exp $ # # Copyright Michigan State University Board of Trustees @@ -486,7 +486,7 @@ use Apache::lonlocal; use Apache::lonnet; use Apache::lonmap; -use POSIX qw (floor strftime); +use POSIX qw (ceil floor strftime); use Time::HiRes qw( gettimeofday tv_interval ); use LONCAPA; use DateTime(); @@ -577,7 +577,11 @@ sub getLinkForResource { my $anchor; if ($res->is_page()) { foreach my $item (@$stack) { if (defined($item)) { $anchor = $item; } } - $anchor=&escape($anchor->shown_symb()); + if ($anchor->encrypted() && !&advancedUser()) { + $anchor='LC_'.$anchor->id(); + } else { + $anchor=&escape($anchor->shown_symb()); + } return ($res->link(),$res->shown_symb(),$anchor); } # in case folder was skipped over as "only sequence" @@ -941,8 +945,31 @@ sub render_resource { # links to open and close the folder my $whitespace = $location.'/whitespace_21.gif'; - my $linkopen = "".""; - my $linkclose = ""; + my ($nomodal,$linkopen,$linkclose); + unless ($resource->is_map() || $params->{'resource_nolink'}) { + $linkopen = ""; + $linkclose = ""; + if (($params->{'modalLink'}) && (!$resource->is_sequence())) { + if ($link =~m{^(?:|/adm/wrapper)/ext/([^#]+)}) { + my $exturl = $1; + if (($ENV{'SERVER_PORT'} == 443) && ($exturl !~ /^https:/)) { + $nomodal = 1; + } + } elsif (($link eq "/public/$LONCAPA::match_domain/$LONCAPA::match_courseid/syllabus") && + ($env{'request.course.id'}) && ($ENV{'SERVER_PORT'} == 443) && + ($env{'course.'.$env{'request.course.id'}.'.externalsyllabus'} =~ m{^http://})) { + $nomodal = 1; + } + my $esclink = &js_escape($link); + if ($nomodal) { + $linkopen .= ""; + } else { + $linkopen .= ""; + } + } else { + $linkopen .= ""; + } + } # Default icon: unknown page my $icon = ""; @@ -990,14 +1017,18 @@ sub render_resource { '&jump=' . &escape($resource->symb()) . "&folderManip=1\">"; - + $linkclose = ''; } else { # Don't allow users to manipulate folder $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; $icon = ""."\"".($nowOpen"; - - $linkopen = ""; - $linkclose = ""; + if ($params->{'caller'} eq 'sequence') { + $linkopen = ""; + $linkclose = ''; + } else { + $linkopen = ""; + $linkclose = ""; + } } if (((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) || (&Apache::lonnet::allowed('cev',$env{'request.course.id'}))) && @@ -1012,10 +1043,30 @@ sub render_resource { } if ($params->{'mapHidden'} || $resource->randomout()) { $nonLinkedText .= ' ('.&mt('hidden').') '; + } elsif ($params->{'mapUnlisted'}) { + $nonLinkedText .= ' ('.&mt('unlisted').') '; + } elsif ($params->{'mapHiddenDeepLink'} || $resource->deeplinkout()) { + $nonLinkedText .= ' ('.&mt('not shown').') '; } } else { if ($resource->randomout()) { $nonLinkedText .= ' ('.&mt('hidden').') '; + } elsif ($resource->deeplinkout()) { + $nonLinkedText .= ' ('.&mt('not shown').') '; + } else { + my $deeplink = $resource->deeplink($params->{caller}); + if ((($deeplink eq 'absent') || ($deeplink eq 'grades')) && + &advancedUser()) { + $nonLinkedText .= ' ('.&mt('unlisted').') '; + } elsif (($deeplink) && ($deeplink) ne 'full') { + if (&advancedUser()) { + $nonLinkedText .= ' ('.&mt('deep-link access'). + ') '; + } else { + $nonLinkedText .= ' ('.&mt('access via external site'). + ') '; + } + } } } if (!$resource->condval()) { @@ -1066,10 +1117,19 @@ sub render_resource { } if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) { - $result .= "$curMarkerBegin$title$partLabel$curMarkerEnd$editmapLink$nonLinkedText"; - } else { - $result .= "$curMarkerBegin$linkopen$title$partLabel$curMarkerEnd$editmapLink$nonLinkedText"; + $linkclose = ''; + if ($params->{'modalLink'}) { + my $esclink = &js_escape($link); + if ($nomodal) { + $linkopen = ""; + } else { + $linkopen = ""; + } + } else { + $linkopen = ""; + } } + $result .= "$curMarkerBegin$linkopen$title$partLabel$linkclose$curMarkerEnd$editmapLink$nonLinkedText"; return $result; } @@ -1331,6 +1391,9 @@ sub render { # an infinite loop my $oldFilterFunc = $filterFunc; $filterFunc = sub { my $res = shift; return !$res->randomout() && + ($res->deeplink($args->{'caller'}) ne 'absent') && + ($res->deeplink($args->{'caller'}) ne 'grades') && + !$res->deeplinkout() && &$oldFilterFunc($res);}; } @@ -1360,10 +1423,11 @@ sub render { my $currenturl = $env{'form.postdata'}; #$currenturl=~s/^http\:\/\///; #$currenturl=~s/^[^\/]+//; - - $here = $jump = &Apache::lonnet::symbread($currenturl); + unless ($args->{'caller'} eq 'sequence') { + $here = $jump = &Apache::lonnet::symbread($currenturl); + } } - if ($here eq '') { + if (($here eq '') && ($args->{'caller'} ne 'sequence')) { my $last; if (tie(my %hash,'GDBM_File',$env{'request.course.fn'}.'_symb.db', &GDBM_READER(),0640)) { @@ -1423,10 +1487,13 @@ sub render { if ($args->{'iterator_map'}) { my $map = $args->{'iterator_map'}; $map = $navmap->getResourceByUrl($map); - my $firstResource = $map->map_start(); - my $finishResource = $map->map_finish(); - - $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition); + if (ref($map)) { + my $firstResource = $map->map_start(); + my $finishResource = $map->map_finish(); + $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition); + } else { + return; + } } else { $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition,undef,$args->{'include_top_level_map'}); } @@ -1689,6 +1756,28 @@ END undef($args->{'sort'}); } + # Determine if page will be served with https in case + # it contains a syllabus which uses an external URL + # which points at an http site. + + my ($is_ssl,$cdom,$cnum,$hostname); + if ($ENV{'SERVER_PORT'} == 443) { + $is_ssl = 1; + if ($r) { + $hostname = $r->hostname(); + } else { + $hostname = $ENV{'SERVER_NAME'}; + } + } + if ($env{'request.course.id'}) { + $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + } + + my $inhibitmenu; + if ($args->{'modalLink'}) { + $inhibitmenu = '&inhibitmenu=yes'; + } while (1) { if ($args->{'sort'}) { @@ -1725,6 +1814,8 @@ END # If this is an empty sequence and we're filtering them, continue on $args->{'mapHidden'} = 0; + $args->{'mapUnlisted'} = 0; + $args->{'mapHiddenDeepLink'} = 0; if (($curRes->is_map()) && (!$curRes->{DATA}->{HAS_VISIBLE_CHILDREN})) { if ($args->{'suppressEmptySequences'}) { next; @@ -1737,6 +1828,22 @@ END } else { next; } + } elsif ($curRes->deeplinkout) { + if ($userCanSeeHidden) { + $args->{'mapHiddenDeepLink'} = 1; + } else { + next; + } + } else { + my $deeplink = $navmap->get_mapparam(undef,$mapname,"0.deeplink"); + my ($state,$others,$listed) = split(/,/,$deeplink); + if (($listed eq 'absent') || ($listed eq 'grades')) { + if ($userCanSeeHidden) { + $args->{'mapUnlisted'} = 1; + } else { + next; + } + } } } } @@ -1799,7 +1906,16 @@ END $args->{'condensed'} = 1; } } - } + } + # If deep-link parameter is set (and is not set to full) suppress link + # unless privileged user, tinyurl used for login resolved to a map, and + # the resource is within the map. + if ((!$curRes->deeplink($args->{'caller'})) || + ($curRes->deeplink($args->{'caller'}) eq 'full') || &advancedUser()) { + $args->{'resource_nolink'} = 0; + } else { + $args->{'resource_nolink'} = 1; + } # If the multipart problem was condensed, "forget" it was multipart if (scalar(@parts) == 1) { @@ -1822,11 +1938,35 @@ END $stack=$it->getStack(); } ($src,$symb,$anchor)=getLinkForResource($stack); + my $srcHasQuestion = $src =~ /\?/; + if ($env{'request.course.id'}) { + if (($is_ssl) && ($src =~ m{^\Q/public/$cdom/$cnum/syllabus\E($|\?)}) && + ($env{'course.'.$env{'request.course.id'}.'.externalsyllabus'} =~ m{^http://})) { + unless ((&Apache::lonnet::uses_sts()) || (&Apache::lonnet::waf_allssl($hostname))) { + if ($hostname ne '') { + $src = 'http://'.$hostname.$src; + } + $src .= ($srcHasQuestion? '&' : '?') . 'usehttp=1'; + $srcHasQuestion = 1; + } + } elsif (($is_ssl) && ($src =~ m{^\Q/adm/wrapper/ext/\E(?!https:)})) { + unless ((&Apache::lonnet::uses_sts()) || (&Apache::lonnet::waf_allssl($hostname))) { + if ($hostname ne '') { + $src = 'http://'.$hostname.$src; + } + $src .= ($srcHasQuestion? '&' : '?') . 'usehttp=1'; + $srcHasQuestion = 1; + } + } + } if (defined($anchor)) { $anchor='#'.$anchor; } - my $srcHasQuestion = $src =~ /\?/; - $args->{"resourceLink"} = $src. - ($srcHasQuestion?'&':'?') . - 'symb=' . &escape($symb).$anchor; + if (($args->{'caller'} eq 'sequence') && ($curRes->is_map())) { + $args->{"resourceLink"} = $src.($srcHasQuestion?'&':'?') .'navmap=1'; + } else { + $args->{"resourceLink"} = $src. + ($srcHasQuestion?'&':'?') . + 'symb=' . &escape($symb).$inhibitmenu.$anchor; + } } # Now, we've decided what parts to show. Loop through them and # show them. @@ -1854,7 +1994,7 @@ END $currentJumpDelta) { # Jam the anchor after the tag; # necessary for valid HTML (which Mozilla requires) - $colHTML =~ s/\>/\>\/; + $colHTML =~ s/\>/\>\\<\/a\>/; $displayedJumpMarker = 1; } $result .= $colHTML . "\n"; @@ -2240,7 +2380,7 @@ sub generate_email_discuss_status { foreach my $msgid (@keys) { if ((!$emailstatus{$msgid}) || ($emailstatus{$msgid} eq 'new')) { my ($sendtime,$shortsubj,$fromname,$fromdomain,$status,$fromcid, - $symb,$error) = &Apache::lonmsg::unpackmsgid($msgid); + $symb,$error) = &Apache::lonmsg::unpackmsgid(&LONCAPA::escape($msgid)); &Apache::lonenc::check_decrypt(\$symb); if (($fromcid ne '') && ($fromcid ne $cid)) { next; @@ -2341,7 +2481,7 @@ sub getIterator { my $self = shift; my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift, shift, undef, shift, - shift, shift); + shift, shift, shift); return $iterator; } @@ -2754,6 +2894,91 @@ sub recurseup_maps { return @recurseup; } +sub recursed_crumbs { + my ($self,$mapurl,$restitle) = @_; + my (@revmapinfo,@revmapres); + my $mapres = $self->getResourceByUrl($mapurl); + if (ref($mapres)) { + @revmapres = map { $self->getByMapPc($_); } split(/,/,$mapres->map_breadcrumbs()); + shift(@revmapres); + } + my $allowedlength = 60; + my $minlength = 5; + my $allowedtitle = 30; + if (($env{'environment.icons'} eq 'iconsonly') && (!$env{'browser.mobile'})) { + $allowedlength = 100; + $allowedtitle = 70; + } + if (length($restitle) > $allowedtitle) { + $restitle = &truncate_crumb_text($restitle,$allowedtitle); + } + my $totallength = length($restitle); + my @links; + + foreach my $map (@revmapres) { + my $pc = $map->map_pc(); + next if ((!$pc) || ($pc == 1)); + push(@links,$map); + push(@revmapinfo,{'href' => $env{'request.use_absolute'}.$map->link().'?navmap=1','text' => $map->title(),'no_mt' => 1,}); + $totallength += length($map->title()); + } + my $numlinks = scalar(@links); + if ($numlinks) { + if ($totallength - $allowedlength > 0) { + my $available = $allowedlength - length($restitle); + my $avg = POSIX::ceil($available/$numlinks); + if ($avg < $minlength) { + $avg = $minlength; + } + @revmapinfo = (); + foreach my $map (@links) { + my $showntitle = &truncate_crumb_text($map->title(),$avg); + if ($showntitle ne '') { + push(@revmapinfo,{'href' => $env{'request.use_absolute'}.$map->link().'?navmap=1','text' => $showntitle,'no_mt' => 1,}); + } + } + } + } + if ($restitle ne '') { + push(@revmapinfo,{'text' => $restitle, 'no_mt' => 1}); + } + return @revmapinfo; +} + +sub truncate_crumb_text { + my ($title,$limit) = @_; + my $showntitle = ''; + if (length($title) > $limit) { + my @words = split(/\b\s*/,$title); + if (@words == 1) { + $showntitle = substr($title,0,$limit).' ...'; + } else { + my $linklength = 0; + my $num = 0; + foreach my $word (@words) { + $linklength += 1+length($word); + if ($word eq '-') { + $showntitle =~ s/ $//; + $showntitle .= $word; + } elsif ($linklength > $limit) { + if ($num < @words) { + $showntitle .= $word.' ...'; + last; + } else { + $showntitle .= $word; + } + } else { + $showntitle .= $word.' '; + } + } + $showntitle =~ s/ $//; + } + return $showntitle; + } else { + return $title; + } +} + # # Determines the open/close dates for printing a map that # encloses a resource. @@ -3222,7 +3447,7 @@ getIterator behaves as follows: =over 4 -=item * B(firstResource, finishResource, filterHash, condition, forceTop, returnTopMap): +=item * B(firstResource, finishResource, filterHash, condition, forceTop, returnTopMap, $deeplinklisted): All parameters are optional. firstResource is a resource reference corresponding to where the iterator should start. It defaults to @@ -3239,7 +3464,10 @@ that is not just a single, 'redirecting' will return all information, starting with the top-level map, regardless of content. returnTopMap, if true (default false), will cause the iterator to return the top-level map object (resource 0.0) -before anything else. +before anything else. deeplinklisted if true (default false), will +check "listed" status of a resource with a deeplink, and unless "absent" +will exclude deeplink checking when retrieving the browsePriv from +lonnet::allowed(). Thus, by default, only top-level resources will be shown. Change the condition to a 1 without changing the hash, and all resources will be @@ -3376,6 +3604,10 @@ sub new { # have we done that yet? $self->{HAVE_RETURNED_0} = 0; + # Do we want to check the "listed" status for a resource for which + # deeplinking applies. + $self->{DEEPLINKLISTED} = shift; + # Now, we need to pre-process the map, by walking forward and backward # over the parts of the map we're going to look at. @@ -3467,7 +3699,8 @@ sub new { $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, $self->{CONDITION}, - $self->{FORCE_TOP}); + $self->{FORCE_TOP}, + undef,$self->{DEEPLINKLISTED}); } # Set up some bookkeeping information. @@ -3627,7 +3860,8 @@ sub next { # That ends the main iterator logic. Now, do we want to recurse # down this map (if this resource is a map)? if ( ($self->{HERE}->is_sequence() || (!$closeAllPages && $self->{HERE}->is_page())) && - (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { + (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION}) && + ($env{'request.role.adv'} || !$self->{HERE}->randomout())) { $self->{RECURSIVE_ITERATOR_FLAG} = 1; my $firstResource = $self->{HERE}->map_start(); my $finishResource = $self->{HERE}->map_finish(); @@ -3636,13 +3870,14 @@ sub next { $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, $self->{CONDITION}, - $self->{FORCE_TOP}); + $self->{FORCE_TOP}, + undef,$self->{DEEPLINKLISTED}); } # If this is a blank resource, don't actually return it. # Should you ever find you need it, make sure to add an option to the code # that you can use; other things depend on this behavior. - my $browsePriv = $self->{HERE}->browsePriv($noblockcheck); + my $browsePriv = $self->{HERE}->browsePriv($noblockcheck,$self->{DEEPLINKLISTED}); if (!$self->{HERE}->src() || (!($browsePriv eq 'F') && !($browsePriv eq '2')) ) { return $self->next($closeAllPages); @@ -4070,6 +4305,7 @@ sub from { my $self=shift; return $self- sub goesto { my $self=shift; return $self->navHash("goesto_", 1); } sub kind { my $self=shift; return $self->navHash("kind_", 1); } sub randomout { my $self=shift; return $self->navHash("randomout_", 1); } +sub deeplinkout { my $self=shift; return $self->navHash("deeplinkout_", 1); } sub randompick { my $self = shift; my $randompick = $self->parmval('randompick'); @@ -4325,7 +4561,6 @@ sub is_task { sub is_empty_sequence { my $self=shift; - my $src = $self->src(); return !$self->is_page() && $self->navHash("is_map_", 1) && !$self->navHash("map_type_" . $self->map_pc()); } @@ -4376,6 +4611,12 @@ Returns a string with a comma-separated for the hierarchy of maps containing a map, with the top level map first, then descending to deeper levels, with the enclosing map last. +=item * B: + +Same as map_hierarchy, except maps containing only a single itemm if +it's a map, or containing no items are omitted, unless it's the top +level map (map_pc = 1), which is always included. + =back =cut @@ -4411,6 +4652,11 @@ sub map_hierarchy { my $pc = $self->map_pc(); return $self->navHash("map_hierarchy_$pc", 0); } +sub map_breadcrumbs { + my $self = shift; + my $pc = $self->map_pc(); + return $self->navHash("map_breadcrumbs_$pc", 0); +} ##### # Property queries @@ -4635,11 +4881,12 @@ sub duedate { my $date; my @interval=$self->parmval("interval", $part); my $due_date=$self->parmval("duedate", $part); - if ($interval[0] =~ /\d+/) { - my $first_access=&Apache::lonnet::get_first_access($interval[1], + if ($interval[0] =~ /(\d+)/) { + my $timelimit = $1; + my $first_access=&Apache::lonnet::get_first_access($interval[1], $self->{SYMB}); if (defined($first_access)) { - my $interval = $first_access+$interval[0]; + my $interval = $first_access+$timelimit $date = (!$due_date || $interval < $due_date) ? $interval : $due_date; } else { @@ -4733,6 +4980,46 @@ sub slot_control { my $available = $self->parmval("available", $part); return ($useslots,$availablestudent,$available); } +sub deeplink { + my ($self,$caller,$action) = @_; + my $deeplink = $self->parmval("deeplink"); + if ($deeplink) { + my ($state,$others,$listed,$scope) = split(/,/,$deeplink); + if ($action eq 'getlisted') { + return $listed; + } + if ($env{'request.deeplink.login'}) { + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $deeplink_symb = &Apache::loncommon::deeplink_login_symb($cnum,$cdom); + if ($deeplink_symb) { + my ($loginmap,$mapname); + if ($deeplink_symb =~ /\.(page|sequence)$/) { + $mapname = $self->enclosing_map_src(); + $loginmap = &Apache::lonnet::clutter((&Apache::lonnet::decode_symb($deeplink_symb))[2]); + return if ($mapname eq $loginmap); + } else { + return if ($deeplink_symb eq $self->symb()); + if (($scope eq 'map') || ($scope eq 'rec')) { + $mapname = $self->enclosing_map_src(); + $loginmap = &Apache::lonnet::clutter((&Apache::lonnet::decode_symb($deeplink_symb))[0]); + return if ($mapname eq $loginmap); + } + } + if ($scope eq 'rec') { + my $map_pc = $self->navHash('map_pc_'.$mapname); + my @recurseup = split(/,/,$self->navHash('map_hierarchy_'.$map_pc)); + my $login_pc = $self->navHash('map_pc_'.$loginmap); + return if (grep(/^\Q$login_pc\E$/,@recurseup)); + } + } + } + unless (($caller eq 'sequence') || ($state eq 'both')) { + return $listed; + } + } + return; +} # Multiple things need this sub getReturnHash { @@ -5864,13 +6151,23 @@ sub getPrevious { sub browsePriv { my $self = shift; my $noblockcheck = shift; + my $deeplinklisted = shift; if (defined($self->{BROWSE_PRIV})) { return $self->{BROWSE_PRIV}; } - + my ($nodeeplinkcheck,$nodeeplinkout); + if ($deeplinklisted) { + my $deeplink = $self->deeplink(undef,'getlisted'); + if (($deeplink) && ($deeplink ne 'absent')) { + $nodeeplinkcheck = 1; + } + $nodeeplinkout = 1; + } $self->{BROWSE_PRIV} = &Apache::lonnet::allowed('bre',$self->src(), $self->{SYMB},undef, - undef,$noblockcheck); + undef,$noblockcheck, + undef,$nodeeplinkcheck, + $nodeeplinkout); } =pod