Annotation of loncom/Lond.pm, revision 1.8.2.3.2.3
1.1 droeschl 1: # The LearningOnline Network
2: #
1.8.2.3.2.3! raeburn 3: # $Id: Lond.pm,v 1.8.2.3.2.2 2022/02/07 18:32:34 raeburn Exp $
1.1 droeschl 4: #
5: # Copyright Michigan State University Board of Trustees
6: #
7: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
8: #
9: # LON-CAPA is free software; you can redistribute it and/or modify
10: # it under the terms of the GNU General Public License as published by
11: # the Free Software Foundation; either version 2 of the License, or
12: # (at your option) any later version.
13: #
14: # LON-CAPA is distributed in the hope that it will be useful,
15: # but WITHOUT ANY WARRANTY; without even the implied warranty of
16: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17: # GNU General Public License for more details.
18: #
19: # You should have received a copy of the GNU General Public License
20: # along with LON-CAPA; if not, write to the Free Software
21: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22: #
23: # /home/httpd/html/adm/gpl.txt
24: #
25: # http://www.lon-capa.org/
26: #
27: ###
28:
29: #NOTE perldoc at the end of file
1.4 droeschl 30: #TODO move remaining lond functions into this
1.1 droeschl 31:
32: package LONCAPA::Lond;
33:
34: use strict;
35: use lib '/home/httpd/lib/perl/';
36:
37: use LONCAPA;
38: use Apache::lonnet;
39: use GDBM_File;
1.8.2.3.2.1 raeburn 40: use Net::OAuth;
1.8.2.3.2.3! raeburn 41: use Crypt::CBC;
1.1 droeschl 42:
43: sub dump_with_regexp {
1.4 droeschl 44: my ( $tail, $clientversion ) = @_;
1.2 droeschl 45: my ( $udom, $uname, $namespace, $regexp, $range ) =
46: split /:/, $tail;
1.1 droeschl 47:
1.4 droeschl 48: $regexp = $regexp ? unescape($regexp) : '.';
1.1 droeschl 49:
50: my ($start,$end);
1.2 droeschl 51:
1.1 droeschl 52: if (defined($range)) {
1.2 droeschl 53: if ($range =~ /^(\d+)\-(\d+)$/) {
54: ($start,$end) = ($1,$2);
55: } elsif ($range =~/^(\d+)$/) {
56: ($start,$end) = (0,$1);
57: } else {
58: undef($range);
59: }
60: }
61:
62: my $hashref = &tie_user_hash($udom, $uname, $namespace, &GDBM_READER()) or
63: return "error: ".($!+0)." tie(GDBM) Failed while attempting dump";
64:
65: my $qresult = '';
66: my $count = 0;
1.1 droeschl 67: #
68: # When dump is for roles.db, determine if LON-CAPA version checking is needed.
1.2 droeschl 69: # Sessions on 2.10 and later do not require version checking, as that occurs
1.1 droeschl 70: # on the server hosting the user session, when constructing the roles/courses
71: # screen).
72: #
1.2 droeschl 73: my $skipcheck;
74: my @ids = &Apache::lonnet::current_machine_ids();
75: my (%homecourses, $major, $minor, $now);
1.1 droeschl 76: #
77: # If dump is for roles.db from a pre-2.10 server, determine the LON-CAPA
1.2 droeschl 78: # version on the server which requested the data.
1.1 droeschl 79: #
1.2 droeschl 80: if ($namespace eq 'roles') {
81: if ($clientversion =~ /^\'?(\d+)\.(\d+)\.[\w.\-]+\'?/) {
82: $major = $1;
83: $minor = $2;
1.4 droeschl 84:
1.2 droeschl 85: }
86: if (($major > 2) || (($major == 2) && ($minor > 9))) {
87: $skipcheck = 1;
1.1 droeschl 88: }
1.2 droeschl 89: $now = time;
90: }
91: while (my ($key,$value) = each(%$hashref)) {
92: if ($namespace eq 'roles' && (!$skipcheck)) {
1.1 droeschl 93: if ($key =~ m{^/($LONCAPA::match_domain)/($LONCAPA::match_courseid)(/?[^_]*)_(cc|co|in|ta|ep|ad|st|cr)$}) {
94: my $cdom = $1;
95: my $cnum = $2;
1.2 droeschl 96: my ($role,$roleend,$rolestart) = split(/\_/,$value);
97: if (!$roleend || $roleend > $now) {
1.1 droeschl 98: #
99: # For active course roles, check that requesting server is running a LON-CAPA
100: # version which meets any version requirements for the course. Do not include
101: # the role amongst the results returned if the requesting server's version is
102: # too old.
103: #
104: # This determination is handled differently depending on whether the course's
105: # homeserver is the current server, or whether it is a different server.
106: # In both cases, the course's version requirement needs to be retrieved.
107: #
1.2 droeschl 108: next unless (&releasereqd_check($cnum,$cdom,$key,$value,$major,
109: $minor,\%homecourses,\@ids));
1.1 droeschl 110: }
111: }
112: }
1.2 droeschl 113: if ($regexp eq '.') {
114: $count++;
115: if (defined($range) && $count >= $end) { last; }
116: if (defined($range) && $count < $start) { next; }
117: $qresult.=$key.'='.$value.'&';
118: } else {
119: my $unescapeKey = &unescape($key);
120: if (eval('$unescapeKey=~/$regexp/')) {
121: $count++;
122: if (defined($range) && $count >= $end) { last; }
123: if (defined($range) && $count < $start) { next; }
124: $qresult.="$key=$value&";
125: }
126: }
127: }
128:
129: &untie_user_hash($hashref) or
130: return "error: ".($!+0)." untie(GDBM) Failed while attempting dump";
1.1 droeschl 131: #
132: # If dump is for roles.db from a pre-2.10 server, check if the LON-CAPA
133: # version requirements for courses for which the current server is the home
134: # server permit course roles to be usable on the client server hosting the
135: # user's session. If so, include those role results in the data returned to
136: # the client server.
137: #
1.2 droeschl 138: if (($namespace eq 'roles') && (!$skipcheck)) {
139: if (keys(%homecourses) > 0) {
140: $qresult .= &check_homecourses(\%homecourses,$regexp,$count,
141: $range,$start,$end,$major,$minor);
142: }
143: }
144: chop($qresult);
145: return $qresult;
146: }
147:
148:
149: sub releasereqd_check {
150: my ($cnum,$cdom,$key,$value,$major,$minor,$homecourses,$ids) = @_;
151: my $home = &Apache::lonnet::homeserver($cnum,$cdom);
152: return if ($home eq 'no_host');
153: my ($reqdmajor,$reqdminor,$displayrole);
154: if ($cnum =~ /$LONCAPA::match_community/) {
155: if ($major eq '' && $minor eq '') {
156: return unless ((ref($ids) eq 'ARRAY') &&
157: (grep(/^\Q$home\E$/,@{$ids})));
158: } else {
159: $reqdmajor = 2;
160: $reqdminor = 9;
161: return unless (&useable_role($reqdmajor,$reqdminor,$major,$minor));
162: }
163: }
164: my $hashid = $cdom.':'.$cnum;
165: my ($courseinfo,$cached) =
166: &Apache::lonnet::is_cached_new('courseinfo',$hashid);
167: if (defined($cached)) {
168: if (ref($courseinfo) eq 'HASH') {
169: if (exists($courseinfo->{'releaserequired'})) {
170: my ($reqdmajor,$reqdminor) = split(/\./,$courseinfo->{'releaserequired'});
171: return unless (&useable_role($reqdmajor,$reqdminor,$major,$minor));
172: }
173: }
174: } else {
175: if (ref($ids) eq 'ARRAY') {
176: if (grep(/^\Q$home\E$/,@{$ids})) {
177: if (ref($homecourses) eq 'HASH') {
178: if (ref($homecourses->{$cdom}) eq 'HASH') {
179: if (ref($homecourses->{$cdom}{$cnum}) eq 'HASH') {
180: if (ref($homecourses->{$cdom}{$cnum}) eq 'ARRAY') {
181: push(@{$homecourses->{$cdom}{$cnum}},{$key=>$value});
182: } else {
183: $homecourses->{$cdom}{$cnum} = [{$key=>$value}];
184: }
185: } else {
186: $homecourses->{$cdom}{$cnum} = [{$key=>$value}];
187: }
188: } else {
189: $homecourses->{$cdom}{$cnum} = [{$key=>$value}];
190: }
191: }
192: return;
193: }
194: }
195: my $courseinfo = &get_courseinfo_hash($cnum,$cdom,$home);
196: if (ref($courseinfo) eq 'HASH') {
197: if (exists($courseinfo->{'releaserequired'})) {
198: my ($reqdmajor,$reqdminor) = split(/\./,$courseinfo->{'releaserequired'});
199: return unless (&useable_role($reqdmajor,$reqdminor,$major,$minor));
200: }
201: } else {
202: return;
203: }
204: }
205: return 1;
206: }
207:
208:
209: sub check_homecourses {
210: my ($homecourses,$regexp,$count,$range,$start,$end,$major,$minor) = @_;
211: my ($result,%addtocache);
212: my $yesterday = time - 24*3600;
213: if (ref($homecourses) eq 'HASH') {
214: my (%okcourses,%courseinfo,%recent);
215: foreach my $domain (keys(%{$homecourses})) {
216: my $hashref =
217: &tie_domain_hash($domain, "nohist_courseids", &GDBM_WRCREAT());
218: if (ref($hashref) eq 'HASH') {
219: while (my ($key,$value) = each(%$hashref)) {
220: my $unesc_key = &unescape($key);
221: if ($unesc_key =~ /^lasttime:(\w+)$/) {
222: my $cid = $1;
223: $cid =~ s/_/:/;
224: if ($value > $yesterday ) {
225: $recent{$cid} = 1;
226: }
227: next;
228: }
229: my $items = &Apache::lonnet::thaw_unescape($value);
230: if (ref($items) eq 'HASH') {
231: my ($cdom,$cnum) = split(/_/,$unesc_key);
232: my $hashid = $cdom.':'.$cnum;
233: $courseinfo{$hashid} = $items;
234: if (ref($homecourses->{$cdom}{$cnum}) eq 'ARRAY') {
235: my ($reqdmajor,$reqdminor) = split(/\./,$items->{'releaserequired'});
236: if (&useable_role($reqdmajor,$reqdminor,$major,$minor)) {
237: $okcourses{$hashid} = 1;
238: }
239: }
240: }
241: }
242: unless (&untie_domain_hash($hashref)) {
1.8.2.3 raeburn 243: &Apache::lonnet::logthis("Failed to untie tied hash for nohist_courseids.db for $domain");
1.2 droeschl 244: }
245: } else {
1.8.2.3 raeburn 246: &Apache::lonnet::logthis("Failed to tie hash for nohist_courseids.db for $domain");
1.2 droeschl 247: }
248: }
249: foreach my $hashid (keys(%recent)) {
250: my ($result,$cached)=&Apache::lonnet::is_cached_new('courseinfo',$hashid);
251: unless ($cached) {
252: &Apache::lonnet::do_cache_new('courseinfo',$hashid,$courseinfo{$hashid},600);
253: }
254: }
255: foreach my $cdom (keys(%{$homecourses})) {
256: if (ref($homecourses->{$cdom}) eq 'HASH') {
257: foreach my $cnum (keys(%{$homecourses->{$cdom}})) {
258: my $hashid = $cdom.':'.$cnum;
259: next if ($recent{$hashid});
260: &Apache::lonnet::do_cache_new('courseinfo',$hashid,$courseinfo{$hashid},600);
261: }
262: }
263: }
264: foreach my $hashid (keys(%okcourses)) {
265: my ($cdom,$cnum) = split(/:/,$hashid);
266: if ((ref($homecourses->{$cdom}) eq 'HASH') &&
267: (ref($homecourses->{$cdom}{$cnum}) eq 'ARRAY')) {
268: foreach my $role (@{$homecourses->{$cdom}{$cnum}}) {
269: if (ref($role) eq 'HASH') {
270: while (my ($key,$value) = each(%{$role})) {
271: if ($regexp eq '.') {
272: $count++;
273: if (defined($range) && $count >= $end) { last; }
274: if (defined($range) && $count < $start) { next; }
275: $result.=$key.'='.$value.'&';
276: } else {
277: my $unescapeKey = &unescape($key);
278: if (eval('$unescapeKey=~/$regexp/')) {
279: $count++;
280: if (defined($range) && $count >= $end) { last; }
281: if (defined($range) && $count < $start) { next; }
282: $result.="$key=$value&";
283: }
284: }
285: }
286: }
1.1 droeschl 287: }
288: }
1.2 droeschl 289: }
1.1 droeschl 290: }
1.2 droeschl 291: return $result;
292: }
293:
1.1 droeschl 294:
1.2 droeschl 295: sub useable_role {
296: my ($reqdmajor,$reqdminor,$major,$minor) = @_;
297: if ($reqdmajor ne '' && $reqdminor ne '') {
298: return if (($major eq '' && $minor eq '') ||
299: ($major < $reqdmajor) ||
300: (($major == $reqdmajor) && ($minor < $reqdminor)));
301: }
1.1 droeschl 302: return 1;
303: }
304:
1.2 droeschl 305:
1.3 droeschl 306: sub get_courseinfo_hash {
307: my ($cnum,$cdom,$home) = @_;
308: my %info;
309: eval {
310: local($SIG{ALRM}) = sub { die "timeout\n"; };
311: local($SIG{__DIE__})='DEFAULT';
312: alarm(3);
313: %info = &Apache::lonnet::courseiddump($cdom,'.',1,'.','.',$cnum,1,[$home],'.');
314: alarm(0);
315: };
316: if ($@) {
317: if ($@ eq "timeout\n") {
1.8.2.3 raeburn 318: &Apache::lonnet::logthis("<font color='blue'>WARNING courseiddump for $cnum:$cdom from $home timedout</font>");
1.3 droeschl 319: } else {
1.8.2.3 raeburn 320: &Apache::lonnet::logthis("<font color='yellow'>WARNING unexpected error during eval of call for courseiddump from $home</font>");
1.3 droeschl 321: }
322: } else {
323: if (ref($info{$cdom.'_'.$cnum}) eq 'HASH') {
324: my $hashid = $cdom.':'.$cnum;
325: return &Apache::lonnet::do_cache_new('courseinfo',$hashid,$info{$cdom.'_'.$cnum},600);
326: }
327: }
328: return;
329: }
1.2 droeschl 330:
1.4 droeschl 331: sub dump_course_id_handler {
332: my ($tail) = @_;
333:
334: my ($udom,$since,$description,$instcodefilter,$ownerfilter,$coursefilter,
335: $typefilter,$regexp_ok,$rtn_as_hash,$selfenrollonly,$catfilter,$showhidden,
336: $caller,$cloner,$cc_clone_list,$cloneonly,$createdbefore,$createdafter,
1.7 raeburn 337: $creationcontext,$domcloner,$hasuniquecode,$reqcrsdom,$reqinstcode) = split(/:/,$tail);
1.4 droeschl 338: my $now = time;
339: my ($cloneruname,$clonerudom,%cc_clone);
340: if (defined($description)) {
341: $description=&unescape($description);
342: } else {
343: $description='.';
344: }
345: if (defined($instcodefilter)) {
346: $instcodefilter=&unescape($instcodefilter);
347: } else {
348: $instcodefilter='.';
349: }
350: my ($ownerunamefilter,$ownerdomfilter);
351: if (defined($ownerfilter)) {
352: $ownerfilter=&unescape($ownerfilter);
353: if ($ownerfilter ne '.' && defined($ownerfilter)) {
354: if ($ownerfilter =~ /^([^:]*):([^:]*)$/) {
355: $ownerunamefilter = $1;
356: $ownerdomfilter = $2;
357: } else {
358: $ownerunamefilter = $ownerfilter;
359: $ownerdomfilter = '';
360: }
361: }
362: } else {
363: $ownerfilter='.';
364: }
365:
366: if (defined($coursefilter)) {
367: $coursefilter=&unescape($coursefilter);
368: } else {
369: $coursefilter='.';
370: }
371: if (defined($typefilter)) {
372: $typefilter=&unescape($typefilter);
373: } else {
374: $typefilter='.';
375: }
376: if (defined($regexp_ok)) {
377: $regexp_ok=&unescape($regexp_ok);
378: }
379: if (defined($catfilter)) {
380: $catfilter=&unescape($catfilter);
381: }
382: if (defined($cloner)) {
383: $cloner = &unescape($cloner);
384: ($cloneruname,$clonerudom) = ($cloner =~ /^($LONCAPA::match_username):($LONCAPA::match_domain)$/);
385: }
386: if (defined($cc_clone_list)) {
387: $cc_clone_list = &unescape($cc_clone_list);
388: my @cc_cloners = split('&',$cc_clone_list);
389: foreach my $cid (@cc_cloners) {
390: my ($clonedom,$clonenum) = split(':',$cid);
391: next if ($clonedom ne $udom);
392: $cc_clone{$clonedom.'_'.$clonenum} = 1;
393: }
394: }
395: if ($createdbefore ne '') {
396: $createdbefore = &unescape($createdbefore);
397: } else {
398: $createdbefore = 0;
399: }
400: if ($createdafter ne '') {
401: $createdafter = &unescape($createdafter);
402: } else {
403: $createdafter = 0;
404: }
405: if ($creationcontext ne '') {
406: $creationcontext = &unescape($creationcontext);
407: } else {
408: $creationcontext = '.';
409: }
1.6 raeburn 410: unless ($hasuniquecode) {
411: $hasuniquecode = '.';
412: }
1.8 raeburn 413: if ($reqinstcode ne '') {
414: $reqinstcode = &unescape($reqinstcode);
415: }
1.4 droeschl 416: my $unpack = 1;
417: if ($description eq '.' && $instcodefilter eq '.' && $ownerfilter eq '.' &&
418: $typefilter eq '.') {
419: $unpack = 0;
420: }
421: if (!defined($since)) { $since=0; }
1.7 raeburn 422: my (%gotcodedefaults,%otcodedefaults);
1.4 droeschl 423: my $qresult='';
424:
425: my $hashref = &tie_domain_hash($udom, "nohist_courseids", &GDBM_WRCREAT())
426: or return "error: ".($!+0)." tie(GDBM) Failed while attempting courseiddump";
427:
428: while (my ($key,$value) = each(%$hashref)) {
429: my ($unesc_key,$lasttime_key,$lasttime,$is_hash,%val,
430: %unesc_val,$selfenroll_end,$selfenroll_types,$created,
431: $context);
432: $unesc_key = &unescape($key);
433: if ($unesc_key =~ /^lasttime:/) {
434: next;
435: } else {
436: $lasttime_key = &escape('lasttime:'.$unesc_key);
437: }
438: if ($hashref->{$lasttime_key} ne '') {
439: $lasttime = $hashref->{$lasttime_key};
440: next if ($lasttime<$since);
441: }
1.7 raeburn 442: my ($canclone,$valchange,$clonefromcode);
1.4 droeschl 443: my $items = &Apache::lonnet::thaw_unescape($value);
444: if (ref($items) eq 'HASH') {
445: if ($hashref->{$lasttime_key} eq '') {
446: next if ($since > 1);
447: }
1.7 raeburn 448: if ($items->{'inst_code'}) {
449: $clonefromcode = $items->{'inst_code'};
450: }
1.4 droeschl 451: $is_hash = 1;
452: if ($domcloner) {
453: $canclone = 1;
454: } elsif (defined($clonerudom)) {
455: if ($items->{'cloners'}) {
456: my @cloneable = split(',',$items->{'cloners'});
457: if (@cloneable) {
458: if (grep(/^\*$/,@cloneable)) {
459: $canclone = 1;
460: } elsif (grep(/^\*:\Q$clonerudom\E$/,@cloneable)) {
461: $canclone = 1;
462: } elsif (grep(/^\Q$cloneruname\E:\Q$clonerudom\E$/,@cloneable)) {
463: $canclone = 1;
464: }
465: }
466: unless ($canclone) {
467: if ($cloneruname ne '' && $clonerudom ne '') {
468: if ($cc_clone{$unesc_key}) {
469: $canclone = 1;
470: $items->{'cloners'} .= ','.$cloneruname.':'.
471: $clonerudom;
472: $valchange = 1;
473: }
474: }
475: }
1.7 raeburn 476: unless ($canclone) {
477: if (($reqcrsdom eq $udom) && ($reqinstcode) && ($clonefromcode)) {
478: if (grep(/\=/,@cloneable)) {
479: foreach my $cloner (@cloneable) {
480: if (($cloner ne '*') && ($cloner !~ /^\*\:$LONCAPA::match_domain$/) &&
481: ($cloner !~ /^$LONCAPA::match_username\:$LONCAPA::match_domain$/) && ($cloner ne '')) {
482: if ($cloner =~ /=/) {
483: my (%codedefaults,@code_order);
484: if (ref($gotcodedefaults{$udom}) eq 'HASH') {
485: if (ref($gotcodedefaults{$udom}{'defaults'}) eq 'HASH') {
486: %codedefaults = %{$gotcodedefaults{$udom}{'defaults'}};
487: }
488: if (ref($gotcodedefaults{$udom}{'order'}) eq 'ARRAY') {
489: @code_order = @{$gotcodedefaults{$udom}{'order'}};
490: }
491: } else {
492: &Apache::lonnet::auto_instcode_defaults($udom,
493: \%codedefaults,
494: \@code_order);
495: $gotcodedefaults{$udom}{'defaults'} = \%codedefaults;
496: $gotcodedefaults{$udom}{'order'} = \@code_order;
497: }
498: if (@code_order > 0) {
499: if (&Apache::lonnet::check_instcode_cloning(\%codedefaults,\@code_order,
500: $cloner,$clonefromcode,$reqinstcode)) {
501: $canclone = 1;
502: last;
503: }
504: }
505: }
506: }
507: }
508: }
509: }
510: }
1.4 droeschl 511: } elsif (defined($cloneruname)) {
512: if ($cc_clone{$unesc_key}) {
513: $canclone = 1;
514: $items->{'cloners'} = $cloneruname.':'.$clonerudom;
515: $valchange = 1;
516: }
517: unless ($canclone) {
518: if ($items->{'owner'} =~ /:/) {
519: if ($items->{'owner'} eq $cloner) {
520: $canclone = 1;
521: }
522: } elsif ($cloner eq $items->{'owner'}.':'.$udom) {
523: $canclone = 1;
524: }
525: if ($canclone) {
526: $items->{'cloners'} = $cloneruname.':'.$clonerudom;
527: $valchange = 1;
528: }
529: }
530: }
1.7 raeburn 531: unless (($canclone) || ($items->{'cloners'})) {
532: my %domdefs = &Apache::lonnet::get_domain_defaults($udom);
533: if ($domdefs{'canclone'}) {
534: unless ($domdefs{'canclone'} eq 'none') {
535: if ($domdefs{'canclone'} eq 'domain') {
536: if ($clonerudom eq $udom) {
537: $canclone = 1;
538: }
539: } elsif (($clonefromcode) && ($reqinstcode) &&
540: ($udom eq $reqcrsdom)) {
541: if (&Apache::lonnet::default_instcode_cloning($udom,$domdefs{'canclone'},
542: $clonefromcode,$reqinstcode)) {
543: $canclone = 1;
544: }
545: }
546: }
547: }
548: }
1.4 droeschl 549: }
550: if ($unpack || !$rtn_as_hash) {
551: $unesc_val{'descr'} = $items->{'description'};
552: $unesc_val{'inst_code'} = $items->{'inst_code'};
553: $unesc_val{'owner'} = $items->{'owner'};
554: $unesc_val{'type'} = $items->{'type'};
555: $unesc_val{'cloners'} = $items->{'cloners'};
556: $unesc_val{'created'} = $items->{'created'};
557: $unesc_val{'context'} = $items->{'context'};
558: }
559: $selfenroll_types = $items->{'selfenroll_types'};
560: $selfenroll_end = $items->{'selfenroll_end_date'};
561: $created = $items->{'created'};
562: $context = $items->{'context'};
563: if ($selfenrollonly) {
564: next if (!$selfenroll_types);
565: if (($selfenroll_end > 0) && ($selfenroll_end <= $now)) {
566: next;
567: }
568: }
569: if ($creationcontext ne '.') {
570: next if (($context ne '') && ($context ne $creationcontext));
571: }
572: if ($createdbefore > 0) {
573: next if (($created eq '') || ($created > $createdbefore));
574: }
575: if ($createdafter > 0) {
576: next if (($created eq '') || ($created <= $createdafter));
577: }
578: if ($catfilter ne '') {
579: next if ($items->{'categories'} eq '');
580: my @categories = split('&',$items->{'categories'});
581: next if (@categories == 0);
582: my @subcats = split('&',$catfilter);
583: my $matchcat = 0;
584: foreach my $cat (@categories) {
585: if (grep(/^\Q$cat\E$/,@subcats)) {
586: $matchcat = 1;
587: last;
588: }
589: }
590: next if (!$matchcat);
591: }
592: if ($caller eq 'coursecatalog') {
593: if ($items->{'hidefromcat'} eq 'yes') {
594: next if !$showhidden;
595: }
596: }
1.6 raeburn 597: if ($hasuniquecode ne '.') {
598: next unless ($items->{'uniquecode'});
599: }
1.4 droeschl 600: } else {
601: next if ($catfilter ne '');
602: next if ($selfenrollonly);
603: next if ($createdbefore || $createdafter);
604: next if ($creationcontext ne '.');
605: if ((defined($clonerudom)) && (defined($cloneruname))) {
606: if ($cc_clone{$unesc_key}) {
607: $canclone = 1;
608: $val{'cloners'} = &escape($cloneruname.':'.$clonerudom);
609: }
610: }
611: $is_hash = 0;
612: my @courseitems = split(/:/,$value);
613: $lasttime = pop(@courseitems);
614: if ($hashref->{$lasttime_key} eq '') {
615: next if ($lasttime<$since);
616: }
617: ($val{'descr'},$val{'inst_code'},$val{'owner'},$val{'type'}) = @courseitems;
618: }
619: if ($cloneonly) {
620: next unless ($canclone);
621: }
622: my $match = 1;
623: if ($description ne '.') {
624: if (!$is_hash) {
625: $unesc_val{'descr'} = &unescape($val{'descr'});
626: }
627: if (eval{$unesc_val{'descr'} !~ /\Q$description\E/i}) {
628: $match = 0;
629: }
630: }
631: if ($instcodefilter ne '.') {
632: if (!$is_hash) {
633: $unesc_val{'inst_code'} = &unescape($val{'inst_code'});
634: }
635: if ($regexp_ok == 1) {
636: if (eval{$unesc_val{'inst_code'} !~ /$instcodefilter/}) {
637: $match = 0;
638: }
639: } elsif ($regexp_ok == -1) {
640: if (eval{$unesc_val{'inst_code'} =~ /$instcodefilter/}) {
641: $match = 0;
642: }
643: } else {
644: if (eval{$unesc_val{'inst_code'} !~ /\Q$instcodefilter\E/i}) {
645: $match = 0;
646: }
647: }
648: }
649: if ($ownerfilter ne '.') {
650: if (!$is_hash) {
651: $unesc_val{'owner'} = &unescape($val{'owner'});
652: }
653: if (($ownerunamefilter ne '') && ($ownerdomfilter ne '')) {
654: if ($unesc_val{'owner'} =~ /:/) {
655: if (eval{$unesc_val{'owner'} !~
656: /\Q$ownerunamefilter\E:\Q$ownerdomfilter\E$/i}) {
657: $match = 0;
658: }
659: } else {
660: if (eval{$unesc_val{'owner'} !~ /\Q$ownerunamefilter\E/i}) {
661: $match = 0;
662: }
663: }
664: } elsif ($ownerunamefilter ne '') {
665: if ($unesc_val{'owner'} =~ /:/) {
666: if (eval{$unesc_val{'owner'} !~ /\Q$ownerunamefilter\E:[^:]+$/i}) {
667: $match = 0;
668: }
669: } else {
670: if (eval{$unesc_val{'owner'} !~ /\Q$ownerunamefilter\E/i}) {
671: $match = 0;
672: }
673: }
674: } elsif ($ownerdomfilter ne '') {
675: if ($unesc_val{'owner'} =~ /:/) {
676: if (eval{$unesc_val{'owner'} !~ /^[^:]+:\Q$ownerdomfilter\E/}) {
677: $match = 0;
678: }
679: } else {
680: if ($ownerdomfilter ne $udom) {
681: $match = 0;
682: }
683: }
684: }
685: }
686: if ($coursefilter ne '.') {
687: if (eval{$unesc_key !~ /^$udom(_)\Q$coursefilter\E$/}) {
688: $match = 0;
689: }
690: }
691: if ($typefilter ne '.') {
692: if (!$is_hash) {
693: $unesc_val{'type'} = &unescape($val{'type'});
694: }
695: if ($unesc_val{'type'} eq '') {
696: if ($typefilter ne 'Course') {
697: $match = 0;
698: }
699: } else {
700: if (eval{$unesc_val{'type'} !~ /^\Q$typefilter\E$/}) {
701: $match = 0;
702: }
703: }
704: }
705: if ($match == 1) {
706: if ($rtn_as_hash) {
707: if ($is_hash) {
708: if ($valchange) {
709: my $newvalue = &Apache::lonnet::freeze_escape($items);
710: $qresult.=$key.'='.$newvalue.'&';
711: } else {
712: $qresult.=$key.'='.$value.'&';
713: }
714: } else {
715: my %rtnhash = ( 'description' => &unescape($val{'descr'}),
716: 'inst_code' => &unescape($val{'inst_code'}),
717: 'owner' => &unescape($val{'owner'}),
718: 'type' => &unescape($val{'type'}),
719: 'cloners' => &unescape($val{'cloners'}),
720: );
721: my $items = &Apache::lonnet::freeze_escape(\%rtnhash);
722: $qresult.=$key.'='.$items.'&';
723: }
724: } else {
725: if ($is_hash) {
726: $qresult .= $key.'='.&escape($unesc_val{'descr'}).':'.
727: &escape($unesc_val{'inst_code'}).':'.
728: &escape($unesc_val{'owner'}).'&';
729: } else {
730: $qresult .= $key.'='.$val{'descr'}.':'.$val{'inst_code'}.
731: ':'.$val{'owner'}.'&';
732: }
733: }
734: }
735: }
736: &untie_domain_hash($hashref) or
737: return "error: ".($!+0)." untie(GDBM) Failed while attempting courseiddump";
738:
739: chop($qresult);
740: return $qresult;
741: }
742:
743: sub dump_profile_database {
744: my ($tail) = @_;
745:
746: my ($udom,$uname,$namespace) = split(/:/,$tail);
747:
748: my $hashref = &tie_user_hash($udom, $uname, $namespace, &GDBM_READER()) or
749: return "error: ".($!+0)." tie(GDBM) Failed while attempting currentdump";
750:
751: # Structure of %data:
752: # $data{$symb}->{$parameter}=$value;
753: # $data{$symb}->{'v.'.$parameter}=$version;
754: # since $parameter will be unescaped, we do not
755: # have to worry about silly parameter names...
756:
757: my $qresult='';
758: my %data = (); # A hash of anonymous hashes..
759: while (my ($key,$value) = each(%$hashref)) {
760: my ($v,$symb,$param) = split(/:/,$key);
761: next if ($v eq 'version' || $symb eq 'keys');
762: next if (exists($data{$symb}) &&
763: exists($data{$symb}->{$param}) &&
764: $data{$symb}->{'v.'.$param} > $v);
765: $data{$symb}->{$param}=$value;
766: $data{$symb}->{'v.'.$param}=$v;
767: }
768:
769: &untie_user_hash($hashref) or
770: return "error: ".($!+0)." untie(GDBM) Failed while attempting currentdump";
771:
772: while (my ($symb,$param_hash) = each(%data)) {
773: while(my ($param,$value) = each (%$param_hash)){
774: next if ($param =~ /^v\./); # Ignore versions...
775: #
776: # Just dump the symb=value pairs separated by &
777: #
778: $qresult.=$symb.':'.$param.'='.$value.'&';
779: }
780: }
1.2 droeschl 781:
1.4 droeschl 782: chop($qresult);
783: return $qresult;
784: }
1.2 droeschl 785:
1.8.2.1 raeburn 786: sub is_course {
787: my ($cdom,$cnum) = @_;
788:
789: return unless (($cdom =~ /^$LONCAPA::match_domain$/) &&
790: ($cnum =~ /^$LONCAPA::match_courseid$/));
791: my $hashid = $cdom.':'.$cnum;
792: my ($iscourse,$cached) =
793: &Apache::lonnet::is_cached_new('iscourse',$hashid);
794: unless (defined($cached)) {
795: my $hashref =
796: &tie_domain_hash($cdom, "nohist_courseids", &GDBM_WRCREAT());
797: if (ref($hashref) eq 'HASH') {
798: my $esc_key = &escape($cdom.'_'.$cnum);
799: if (exists($hashref->{$esc_key})) {
800: $iscourse = 1;
801: } else {
802: $iscourse = 0;
803: }
804: &Apache::lonnet::do_cache_new('iscourse',$hashid,$iscourse,3600);
805: unless (&untie_domain_hash($hashref)) {
1.8.2.3 raeburn 806: &Apache::lonnet::logthis("Failed to untie tied hash for nohist_courseids.db for $cdom");
1.8.2.1 raeburn 807: }
808: } else {
1.8.2.3 raeburn 809: &Apache::lonnet::logthis("Failed to tie hash for nohist_courseids.db for $cdom");
1.8.2.1 raeburn 810: }
811: }
812: return $iscourse;
813: }
1.2 droeschl 814:
1.8.2.2 raeburn 815: sub get_dom {
816: my ($userinput) = @_;
817: my ($cmd,$udom,$namespace,$what) =split(/:/,$userinput,4);
818: my $hashref = &tie_domain_hash($udom,$namespace,&GDBM_READER()) or
819: return "error: ".($!+0)." tie(GDBM) Failed while attempting $cmd";
820: my $qresult='';
821: if (ref($hashref)) {
822: chomp($what);
823: my @queries=split(/\&/,$what);
824: for (my $i=0;$i<=$#queries;$i++) {
825: $qresult.="$hashref->{$queries[$i]}&";
826: }
827: $qresult=~s/\&$//;
828: }
829: &untie_user_hash($hashref) or
830: return "error: ".($!+0)." untie(GDBM) Failed while attempting $cmd";
831: return $qresult;
832: }
833:
1.8.2.3.2.3! raeburn 834: sub store_dom {
! 835: my ($userinput) = @_;
! 836: my ($cmd,$dom,$namespace,$rid,$what) =split(/:/,$userinput);
! 837: my $hashref = &tie_domain_hash($dom,$namespace,&GDBM_WRCREAT(),"S","$rid:$what") or
! 838: return "error: ".($!+0)." tie(GDBM) Failed while attempting $cmd";
! 839: $hashref->{"version:$rid"}++;
! 840: my $version=$hashref->{"version:$rid"};
! 841: my $allkeys='';
! 842: my @pairs=split(/\&/,$what);
! 843: foreach my $pair (@pairs) {
! 844: my ($key,$value)=split(/=/,$pair);
! 845: $allkeys.=$key.':';
! 846: $hashref->{"$version:$rid:$key"}=$value;
! 847: }
! 848: my $now = time;
! 849: $hashref->{"$version:$rid:timestamp"}=$now;
! 850: $allkeys.='timestamp';
! 851: $hashref->{"$version:keys:$rid"}=$allkeys;
! 852: &untie_user_hash($hashref) or
! 853: return "error: ".($!+0)." untie(GDBM) Failed while attempting $cmd";
! 854: return 'ok';
! 855: }
! 856:
! 857: sub restore_dom {
! 858: my ($userinput) = @_;
! 859: my ($cmd,$dom,$namespace,$rid) = split(/:/,$userinput);
! 860: my $hashref = &tie_domain_hash($dom,$namespace,&GDBM_READER()) or
! 861: return "error: ".($!+0)." tie(GDBM) Failed while attempting $cmd";
! 862: my $qresult='';
! 863: if (ref($hashref)) {
! 864: chomp($rid);
! 865: my $version=$hashref->{"version:$rid"};
! 866: $qresult.="version=$version&";
! 867: my $scope;
! 868: for ($scope=1;$scope<=$version;$scope++) {
! 869: my $vkeys=$hashref->{"$scope:keys:$rid"};
! 870: my @keys=split(/:/,$vkeys);
! 871: my $key;
! 872: $qresult.="$scope:keys=$vkeys&";
! 873: foreach $key (@keys) {
! 874: $qresult.="$scope:$key=".$hashref->{"$scope:$rid:$key"}."&";
! 875: }
! 876: }
! 877: $qresult=~s/\&$//;
! 878: }
! 879: &untie_user_hash($hashref) or
! 880: return "error: ".($!+0)." untie(GDBM) Failed while attempting $cmd";
! 881: return $qresult;
! 882: }
! 883:
1.8.2.3.2.1 raeburn 884: sub crslti_itemid {
885: my ($cdom,$cnum,$url,$method,$params,$loncaparev) = @_;
886: unless (ref($params) eq 'HASH') {
887: return;
888: }
889: if (($cdom eq '') || ($cnum eq '')) {
890: return;
891: }
892: my ($itemid,$consumer_key,$secret);
893:
894: if (exists($params->{'oauth_callback'})) {
895: $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A;
896: } else {
897: $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0;
898: }
899:
900: my $consumer_key = $params->{'oauth_consumer_key'};
901: return if ($consumer_key eq '');
902:
903: my (%crslti,%crslti_by_key);
904: my $hashid=$cdom.'_'.$cnum;
905: my ($result,$cached)=&Apache::lonnet::is_cached_new('courseltienc',$hashid);
906: if (defined($cached)) {
907: if (ref($result) eq 'HASH') {
908: %crslti = %{$result};
909: }
910: } else {
911: my $reply = &dump_with_regexp(join(":",($cdom,$cnum,'nohist_ltienc','','')),$loncaparev);
912: %crslti = %{&Apache::lonnet::unserialize($reply)};
913: my $cachetime = 24*60*60;
914: &Apache::lonnet::do_cache_new('courseltienc',$hashid,\%crslti,$cachetime);
915: }
916:
917: return if (!keys(%crslti));
918:
919: foreach my $id (keys(%crslti)) {
920: if (ref($crslti{$id}) eq 'HASH') {
921: my $key = $crslti{$id}{'key'};
922: if (($key ne '') && ($crslti{$id}{'secret'} ne '')) {
923: push(@{$crslti_by_key{$key}},$id);
924: }
925: }
926: }
927:
928: return if (!keys(%crslti_by_key));
929:
1.8.2.3.2.3! raeburn 930: my %courselti = &Apache::lonnet::get_course_lti($cnum,$cdom,'provider');
! 931:
1.8.2.3.2.1 raeburn 932: if (ref($crslti_by_key{$consumer_key}) eq 'ARRAY') {
933: foreach my $id (@{$crslti_by_key{$consumer_key}}) {
934: my $secret = $crslti{$id}{'secret'};
1.8.2.3.2.3! raeburn 935: if (ref($courselti{$id}) eq 'HASH') {
! 936: if ((exists($courselti{$id}{'cipher'})) &&
! 937: ($courselti{$id}{'cipher'} =~ /^\d+$/)) {
! 938: my $keynum = $courselti{$id}{'cipher'};
! 939: my $privkey = &get_dom("getdom:$cdom:private:$keynum:lti:key");
! 940: if ($privkey ne '') {
! 941: my $cipher = new Crypt::CBC($privkey);
! 942: $secret = $cipher->decrypt_hex($secret);
! 943: }
! 944: }
! 945: }
1.8.2.3.2.1 raeburn 946: my $request = Net::OAuth->request('request token')->from_hash($params,
947: request_url => $url,
948: request_method => $method,
949: consumer_secret => $secret,);
950: if ($request->verify()) {
951: $itemid = $id;
952: last;
953: }
954: }
955: }
956: return $itemid;
957: }
958:
959: sub domlti_itemid {
960: my ($dom,$context,$url,$method,$params,$loncaparev) = @_;
961: unless (ref($params) eq 'HASH') {
962: return;
963: }
964: if ($dom eq '') {
965: return;
966: }
967: my ($itemid,$consumer_key,$secret);
968:
969: if (exists($params->{'oauth_callback'})) {
970: $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0A;
971: } else {
972: $Net::OAuth::PROTOCOL_VERSION = Net::OAuth::PROTOCOL_VERSION_1_0;
973: }
974:
975: my $consumer_key = $params->{'oauth_consumer_key'};
976: return if ($consumer_key eq '');
977:
1.8.2.3.2.3! raeburn 978: my ($name,$cachename);
! 979: if ($context eq 'linkprot') {
! 980: $name = $context;
! 981: } else {
! 982: $name = 'lti';
! 983: }
! 984: $cachename = $name.'enc';
1.8.2.3.2.1 raeburn 985: my %ltienc;
1.8.2.3.2.3! raeburn 986: my ($encresult,$enccached)=&Apache::lonnet::is_cached_new($cachename,$dom);
1.8.2.3.2.1 raeburn 987: if (defined($enccached)) {
988: if (ref($encresult) eq 'HASH') {
989: %ltienc = %{$encresult};
990: }
991: } else {
1.8.2.3.2.3! raeburn 992: my $reply = &get_dom("getdom:$dom:encconfig:$name");
1.8.2.3.2.1 raeburn 993: my $ltiencref = &Apache::lonnet::thaw_unescape($reply);
994: if (ref($ltiencref) eq 'HASH') {
995: %ltienc = %{$ltiencref};
996: }
997: my $cachetime = 24*60*60;
1.8.2.3.2.3! raeburn 998: &Apache::lonnet::do_cache_new($cachename,$dom,\%ltienc,$cachetime);
1.8.2.3.2.1 raeburn 999: }
1000:
1001: return if (!keys(%ltienc));
1002:
1003: my %lti_by_key;
1004: foreach my $id (keys(%ltienc)) {
1005: if (ref($ltienc{$id}) eq 'HASH') {
1006: my $key = $ltienc{$id}{'key'};
1007: if (($key ne '') && ($ltienc{$id}{'secret'} ne '')) {
1.8.2.3.2.3! raeburn 1008: push(@{$lti_by_key{$key}},$id);
1.8.2.3.2.1 raeburn 1009: }
1010: }
1011: }
1012: return if (!keys(%lti_by_key));
1013:
1.8.2.3.2.3! raeburn 1014: my %lti = &Apache::lonnet::get_domain_lti($dom,$context);
! 1015:
1.8.2.3.2.1 raeburn 1016: if (ref($lti_by_key{$consumer_key}) eq 'ARRAY') {
1017: foreach my $id (@{$lti_by_key{$consumer_key}}) {
1018: my $secret = $ltienc{$id}{'secret'};
1.8.2.3.2.3! raeburn 1019: if (ref($lti{$id}) eq 'HASH') {
! 1020: if ((exists($lti{$id}{'cipher'})) &&
! 1021: ($lti{$id}{'cipher'} =~ /^\d+$/)) {
! 1022: my $keynum = $lti{$id}{'cipher'};
! 1023: my $privkey = &get_dom("getdom:$dom:private:$keynum:lti:key");
! 1024: if ($privkey ne '') {
! 1025: my $cipher = new Crypt::CBC($privkey);
! 1026: $secret = $cipher->decrypt_hex($secret);
! 1027: }
! 1028: }
! 1029: }
1.8.2.3.2.1 raeburn 1030: my $request = Net::OAuth->request('request token')->from_hash($params,
1031: request_url => $url,
1032: request_method => $method,
1033: consumer_secret => $secret,);
1034: if ($request->verify()) {
1035: $itemid = $id;
1036: last;
1037: }
1038: }
1039: }
1040: return $itemid;
1041: }
1042:
1.1 droeschl 1043: 1;
1044:
1045: __END__
1046:
1047: =head1 NAME
1048:
1049: LONCAPA::Lond.pm
1050:
1051: =head1 SYNOPSIS
1052:
1053: #TODO
1054:
1055: =head1 DESCRIPTION
1056:
1057: #TODO
1058:
1059: =head1 METHODS
1060:
1061: =over 4
1062:
1.2 droeschl 1063: =item dump_with_regexp( $tail, $client )
1.1 droeschl 1064:
1065: Dump a profile database with an optional regular expression to match against
1066: the keys. In this dump, no effort is made to separate symb from version
1067: information. Presumably the databases that are dumped by this command are of a
1068: different structure. Need to look at this and improve the documentation of
1069: both this and the currentdump handler.
1070:
1071: $tail a colon separated list containing
1072:
1073: =over
1074:
1075: =item domain
1076:
1077: =item user
1078:
1079: identifying the user.
1080:
1081: =item namespace
1082:
1083: identifying the database.
1084:
1085: =item regexp
1086:
1087: optional regular expression that is matched against database keywords to do
1088: selective dumps.
1089:
1090: =item range
1091:
1092: optional range of entries e.g., 10-20 would return the 10th to 19th items, etc.
1093:
1094: =back
1095:
1096: $client is the channel open on the client.
1097:
1098: Returns: 1 (Continue processing).
1099:
1100: Side effects: response is written to $client.
1101:
1.5 bisitz 1102: =item dump_course_id_handler
1.4 droeschl 1103:
1104: #TODO copy from lond
1105:
1106: =item dump_profile_database
1107:
1108: #TODO copy from lond
1.2 droeschl 1109:
1110: =item releasereqd_check( $cnum, $cdom, $key, $value, $major, $minor,
1111: $homecourses, $ids )
1112:
1113: releasereqd_check() will determine if a LON-CAPA version (defined in the
1114: $major,$minor args passed) is not too old to allow use of a role in a
1115: course ($cnum,$cdom args passed), if at least one of the following applies:
1116: (a) the course is a Community, (b) the course's home server is *not* the
1117: current server, or (c) cached course information is not stale.
1118:
1119: For the case where none of these apply, the course is added to the
1120: $homecourse hash ref (keys = courseIDs, values = array of a hash of roles).
1121: The $homecourse hash ref is for courses for which the current server is the
1122: home server. LON-CAPA version requirements are checked elsewhere for the
1123: items in $homecourse.
1124:
1125:
1126: =item check_homecourses( $homecourses, $regexp, $count, $range, $start, $end,
1127: $major, $minor )
1128:
1129: check_homecourses() will retrieve course information for those courses which
1130: are keys of the $homecourses hash ref (first arg). The nohist_courseids.db
1131: GDBM file is tied and course information for each course retrieved. Last
1132: visit (lasttime key) is also retrieved for each, and cached values updated
1133: for any courses last visited less than 24 hours ago. Cached values are also
1134: updated for any courses included in the $homecourses hash ref.
1135:
1136: The reason for the 24 hours constraint is that the cron entry in
1137: /etc/cron.d/loncapa for /home/httpd/perl/refresh_courseids_db.pl causes
1138: cached course information to be updated nightly for courses with activity
1139: within the past 24 hours.
1140:
1141: Role information for the user (included in a ref to an array of hashes as the
1142: value for each key in $homecourses) is appended to the result returned by the
1143: routine, which will in turn be appended to the string returned to the client
1144: hosting the user's session.
1145:
1146:
1147: =item useable_role( $reqdmajor, $reqdminor, $major, $minor )
1148:
1149: useable_role() will compare the LON-CAPA version required by a course with
1150: the version available on the client server. If the client server's version
1151: is compatible, 1 will be returned.
1152:
1153:
1.3 droeschl 1154: =item get_courseinfo_hash( $cnum, $cdom, $home )
1155:
1156: get_courseinfo_hash() is used to retrieve course information from the db
1157: file: nohist_courseids.db for a course for which the current server is *not*
1158: the home server.
1159:
1160: A hash of a hash will be retrieved. The outer hash contains a single key --
1161: courseID -- for the course for which the data are being requested.
1162: The contents of the inner hash, for that single item in the outer hash
1163: are returned (and cached in memcache for 10 minutes).
1164:
1.8.2.2 raeburn 1165: =item get_dom ( $userinput )
1.3 droeschl 1166:
1.8.2.2 raeburn 1167: get_dom() will retrieve domain configuration information from a GDBM file
1168: in /home/httpd/lonUsers/$dom on the primary library server in a domain.
1169: The single argument passed is the string: $cmd:$udom:$namespace:$what
1170: where $cmd is the command historically passed to lond - i.e., getdom
1171: or egetdom, $udom is the domain, $namespace is the name of the GDBM file
1172: (encconfig or configuration), and $what is a string containing names of
1173: items to retrieve from the db file (each item name is escaped and separated
1174: from the next item name with an ampersand). The return value is either:
1175: error: followed by an error message, or a string containing the value (escaped)
1176: for each item, again separated from the next item with an ampersand.
1.3 droeschl 1177:
1.1 droeschl 1178: =back
1179:
1180: =head1 BUGS
1181:
1182: No known bugs at this time.
1183:
1184: =head1 SEE ALSO
1185:
1186: L<Apache::lonnet>, L<lond>
1187:
1188: =cut
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>