61 * and puts that list in the $load_list property. Plugin developers have absolutely no use for this whatsoever. |
61 * and puts that list in the $load_list property. Plugin developers have absolutely no use for this whatsoever. |
62 */ |
62 */ |
63 |
63 |
64 function loadAll() |
64 function loadAll() |
65 { |
65 { |
|
66 global $db, $session, $paths, $template, $plugins; // Common objects |
66 $dir = ENANO_ROOT.'/plugins/'; |
67 $dir = ENANO_ROOT.'/plugins/'; |
67 |
68 |
68 $this->load_list = Array(); |
69 $this->load_list = $this->system_plugins; |
69 |
70 $q = $db->sql_query('SELECT plugin_filename, plugin_version FROM ' . table_prefix . 'plugins WHERE plugin_flags & ~' . PLUGIN_DISABLED . ' = plugin_flags;'); |
70 $plugins = Array(); |
71 if ( !$q ) |
71 |
72 $db->_die(); |
72 // Open a known directory, and proceed to read its contents |
73 |
73 |
74 while ( $row = $db->fetchrow() ) |
74 if (is_dir($dir)) |
75 { |
75 { |
76 $this->load_list[] = $row['plugin_filename']; |
76 if ($dh = opendir($dir)) |
77 } |
77 { |
78 |
78 while (($file = readdir($dh)) !== false) |
79 $this->loaded_plugins = $this->get_plugin_list($this->load_list); |
79 { |
80 |
80 if(preg_match('#^(.*?)\.php$#is', $file)) |
81 // check for out-of-date plugins |
81 { |
82 foreach ( $this->load_list as $i => $plugin ) |
82 if(getConfig('plugin_'.$file) == '1' || in_array($file, $this->system_plugins)) |
83 { |
83 { |
84 if ( in_array($plugin, $this->system_plugins) ) |
84 $this->load_list[] = $dir . $file; |
85 continue; |
85 $plugid = substr($file, 0, strlen($file)-4); |
86 if ( $this->loaded_plugins[$plugin]['status'] & PLUGIN_OUTOFDATE ) |
86 $f = @file_get_contents($dir . $file); |
87 { |
87 if ( empty($f) ) |
88 // it's out of date, don't load |
88 continue; |
89 unset($this->load_list[$i]); |
89 $f = explode("\n", $f); |
90 unset($this->loaded_plugins[$plugin]); |
90 $f = array_slice($f, 2, 7); |
91 } |
91 $f[0] = substr($f[0], 13); |
92 } |
92 $f[1] = substr($f[1], 12); |
93 |
93 $f[2] = substr($f[2], 13); |
94 $this->load_list = array_unique($this->load_list); |
94 $f[3] = substr($f[3], 8 ); |
|
95 $f[4] = substr($f[4], 9 ); |
|
96 $f[5] = substr($f[5], 12); |
|
97 $plugins[$plugid] = Array(); |
|
98 $plugins[$plugid]['name'] = $f[0]; |
|
99 $plugins[$plugid]['uri'] = $f[1]; |
|
100 $plugins[$plugid]['desc'] = $f[2]; |
|
101 $plugins[$plugid]['auth'] = $f[3]; |
|
102 $plugins[$plugid]['vers'] = $f[4]; |
|
103 $plugins[$plugid]['aweb'] = $f[5]; |
|
104 } |
|
105 } |
|
106 } |
|
107 closedir($dh); |
|
108 } |
|
109 } |
|
110 $this->loaded_plugins = $plugins; |
|
111 //die('<pre>'.htmlspecialchars(print_r($plugins, true)).'</pre>'); |
|
112 } |
95 } |
113 |
96 |
114 /** |
97 /** |
115 * Name kept for compatibility. This method is used to add a new hook into the code somewhere. Plugins are encouraged |
98 * Name kept for compatibility. This method is used to add a new hook into the code somewhere. Plugins are encouraged |
116 * to set hooks and hook into other plugins in a fail-safe way, this encourages reuse of code. Returns an array, whose |
99 * to set hooks and hook into other plugins in a fail-safe way, this encourages reuse of code. Returns an array, whose |
254 { |
236 { |
255 $return[ $matches[1][$i] ] = $matches[2][$i]; |
237 $return[ $matches[1][$i] ] = $matches[2][$i]; |
256 } |
238 } |
257 return $return; |
239 return $return; |
258 } |
240 } |
|
241 |
|
242 /** |
|
243 * Reads all plugins in the filesystem and cross-references them with the database, providing a very complete summary of plugins |
|
244 * on the site. |
|
245 * @param array If specified, will restrict scanned files to this list. Defaults to null, which means all PHP files will be scanned. |
|
246 * @return array |
|
247 */ |
|
248 |
|
249 function get_plugin_list($restrict = null) |
|
250 { |
|
251 global $db, $session, $paths, $template, $plugins; // Common objects |
|
252 |
|
253 // Scan all plugins |
|
254 $plugin_list = array(); |
|
255 |
|
256 if ( $dirh = @opendir( ENANO_ROOT . '/plugins' ) ) |
|
257 { |
|
258 while ( $dh = @readdir($dirh) ) |
|
259 { |
|
260 if ( !preg_match('/\.php$/i', $dh) ) |
|
261 continue; |
|
262 |
|
263 if ( is_array($restrict) ) |
|
264 if ( !in_array($dh, $restrict) ) |
|
265 continue; |
|
266 |
|
267 $fullpath = ENANO_ROOT . "/plugins/$dh"; |
|
268 // it's a PHP file, attempt to read metadata |
|
269 // pass 1: try to read a !info block |
|
270 $blockdata = $this->parse_plugin_blocks($fullpath, 'info'); |
|
271 if ( empty($blockdata) ) |
|
272 { |
|
273 // no !info block, check for old header |
|
274 $fh = @fopen($fullpath, 'r'); |
|
275 if ( !$fh ) |
|
276 // can't read, bail out |
|
277 continue; |
|
278 $plugin_data = array(); |
|
279 for ( $i = 0; $i < 8; $i++ ) |
|
280 { |
|
281 $plugin_data[] = @fgets($fh, 8096); |
|
282 } |
|
283 // close our file handle |
|
284 fclose($fh); |
|
285 // is the header correct? |
|
286 if ( trim($plugin_data[0]) != '<?php' || trim($plugin_data[1]) != '/*' ) |
|
287 { |
|
288 // nope. get out. |
|
289 continue; |
|
290 } |
|
291 // parse all the variables |
|
292 $plugin_meta = array(); |
|
293 for ( $i = 2; $i <= 7; $i++ ) |
|
294 { |
|
295 if ( !preg_match('/^([A-z0-9 ]+?): (.+?)$/', trim($plugin_data[$i]), $match) ) |
|
296 continue 2; |
|
297 $plugin_meta[ strtolower($match[1]) ] = $match[2]; |
|
298 } |
|
299 } |
|
300 else |
|
301 { |
|
302 // parse JSON block |
|
303 $plugin_data =& $blockdata[0]['value']; |
|
304 $plugin_data = enano_clean_json(enano_trim_json($plugin_data)); |
|
305 try |
|
306 { |
|
307 $plugin_meta_uc = enano_json_decode($plugin_data); |
|
308 } |
|
309 catch ( Exception $e ) |
|
310 { |
|
311 continue; |
|
312 } |
|
313 // convert all the keys to lowercase |
|
314 $plugin_meta = array(); |
|
315 foreach ( $plugin_meta_uc as $key => $value ) |
|
316 { |
|
317 $plugin_meta[ strtolower($key) ] = $value; |
|
318 } |
|
319 } |
|
320 if ( !isset($plugin_meta) || !is_array(@$plugin_meta) ) |
|
321 { |
|
322 // parsing didn't work. |
|
323 continue; |
|
324 } |
|
325 // check for required keys |
|
326 $required_keys = array('plugin name', 'plugin uri', 'description', 'author', 'version', 'author uri'); |
|
327 foreach ( $required_keys as $key ) |
|
328 { |
|
329 if ( !isset($plugin_meta[$key]) ) |
|
330 // not set, skip this plugin |
|
331 continue 2; |
|
332 } |
|
333 // decide if it's a system plugin |
|
334 $plugin_meta['system plugin'] = in_array($dh, $this->system_plugins); |
|
335 // reset installed variable |
|
336 $plugin_meta['installed'] = false; |
|
337 $plugin_meta['status'] = 0; |
|
338 // all checks passed |
|
339 $plugin_list[$dh] = $plugin_meta; |
|
340 } |
|
341 } |
|
342 // gather info about installed plugins |
|
343 $q = $db->sql_query('SELECT plugin_id, plugin_filename, plugin_version, plugin_flags FROM ' . table_prefix . 'plugins;'); |
|
344 if ( !$q ) |
|
345 $db->_die(); |
|
346 while ( $row = $db->fetchrow() ) |
|
347 { |
|
348 if ( !isset($plugin_list[ $row['plugin_filename'] ]) ) |
|
349 { |
|
350 // missing plugin file, don't report (for now) |
|
351 continue; |
|
352 } |
|
353 $filename =& $row['plugin_filename']; |
|
354 $plugin_list[$filename]['installed'] = true; |
|
355 $plugin_list[$filename]['status'] = PLUGIN_INSTALLED; |
|
356 $plugin_list[$filename]['plugin id'] = $row['plugin_id']; |
|
357 if ( $row['plugin_version'] != $plugin_list[$filename]['version'] ) |
|
358 { |
|
359 $plugin_list[$filename]['status'] |= PLUGIN_OUTOFDATE; |
|
360 $plugin_list[$filename]['version installed'] = $row['plugin_version']; |
|
361 } |
|
362 if ( $row['plugin_flags'] & PLUGIN_DISABLED ) |
|
363 { |
|
364 $plugin_list[$filename]['status'] |= PLUGIN_DISABLED; |
|
365 } |
|
366 } |
|
367 $db->free_result(); |
|
368 |
|
369 // sort it all out by filename |
|
370 ksort($plugin_list); |
|
371 |
|
372 // done |
|
373 return $plugin_list; |
|
374 } |
|
375 |
|
376 /** |
|
377 * Installs a plugin. |
|
378 * @param string Filename of plugin. |
|
379 * @param array The list of plugins as output by pluginLoader::get_plugin_list(). If not passed, the function is called, possibly wasting time. |
|
380 * @return array JSON-formatted but not encoded response |
|
381 */ |
|
382 |
|
383 function install_plugin($filename, $plugin_list = null) |
|
384 { |
|
385 global $db, $session, $paths, $template, $plugins; // Common objects |
|
386 global $lang; |
|
387 |
|
388 if ( !$plugin_list ) |
|
389 $plugin_list = $this->get_plugin_list(); |
|
390 |
|
391 // we're gonna need this |
|
392 require_once ( ENANO_ROOT . '/includes/sql_parse.php' ); |
|
393 |
|
394 switch ( true ): case true: |
|
395 |
|
396 // is the plugin in the directory and awaiting installation? |
|
397 if ( !isset($plugin_list[$filename]) || ( |
|
398 isset($plugin_list[$filename]) && $plugin_list[$filename]['installed'] |
|
399 )) |
|
400 { |
|
401 $return = array( |
|
402 'mode' => 'error', |
|
403 'error' => 'Invalid plugin specified.', |
|
404 'debug' => $filename |
|
405 ); |
|
406 break; |
|
407 } |
|
408 |
|
409 $dataset =& $plugin_list[$filename]; |
|
410 |
|
411 // load up the installer schema |
|
412 $schema = $this->parse_plugin_blocks( ENANO_ROOT . '/plugins/' . $filename, 'install' ); |
|
413 |
|
414 $sql = array(); |
|
415 if ( !empty($schema) ) |
|
416 { |
|
417 // parse SQL |
|
418 $parser = new SQL_Parser($schema[0]['value'], true); |
|
419 $parser->assign_vars(array( |
|
420 'TABLE_PREFIX' => table_prefix |
|
421 )); |
|
422 $sql = $parser->parse(); |
|
423 } |
|
424 |
|
425 // schema is final, check queries |
|
426 foreach ( $sql as $query ) |
|
427 { |
|
428 if ( !$db->check_query($query) ) |
|
429 { |
|
430 // aww crap, a query is bad |
|
431 $return = array( |
|
432 'mode' => 'error', |
|
433 'error' => $lang->get('acppm_err_upgrade_bad_query'), |
|
434 ); |
|
435 break 2; |
|
436 } |
|
437 } |
|
438 |
|
439 // this is it, perform installation |
|
440 foreach ( $sql as $query ) |
|
441 { |
|
442 if ( substr($query, 0, 1) == '@' ) |
|
443 { |
|
444 $query = substr($query, 1); |
|
445 $db->sql_query($query); |
|
446 } |
|
447 else |
|
448 { |
|
449 if ( !$db->sql_query($query) ) |
|
450 $db->die_json(); |
|
451 } |
|
452 } |
|
453 |
|
454 // register plugin |
|
455 $version_db = $db->escape($dataset['version']); |
|
456 $filename_db = $db->escape($filename); |
|
457 $flags = PLUGIN_INSTALLED; |
|
458 |
|
459 $q = $db->sql_query('INSERT INTO ' . table_prefix . "plugins ( plugin_version, plugin_filename, plugin_flags )\n" |
|
460 . " VALUES ( '$version_db', '$filename_db', $flags );"); |
|
461 if ( !$q ) |
|
462 $db->die_json(); |
|
463 |
|
464 $return = array( |
|
465 'success' => true |
|
466 ); |
|
467 |
|
468 endswitch; |
|
469 |
|
470 return $return; |
|
471 } |
|
472 |
|
473 /** |
|
474 * Uninstalls a plugin, removing it completely from the database and calling any custom uninstallation code the plugin specifies. |
|
475 * @param string Filename of plugin. |
|
476 * @param array The list of plugins as output by pluginLoader::get_plugin_list(). If not passed, the function is called, possibly wasting time. |
|
477 * @return array JSON-formatted but not encoded response |
|
478 */ |
|
479 |
|
480 function uninstall_plugin($filename, $plugin_list = null) |
|
481 { |
|
482 global $db, $session, $paths, $template, $plugins; // Common objects |
|
483 global $lang; |
|
484 |
|
485 if ( !$plugin_list ) |
|
486 $plugin_list = $this->get_plugin_list(); |
|
487 |
|
488 // we're gonna need this |
|
489 require_once ( ENANO_ROOT . '/includes/sql_parse.php' ); |
|
490 |
|
491 switch ( true ): case true: |
|
492 |
|
493 // is the plugin in the directory and already installed? |
|
494 if ( !isset($plugin_list[$filename]) || ( |
|
495 isset($plugin_list[$filename]) && !$plugin_list[$filename]['installed'] |
|
496 )) |
|
497 { |
|
498 $return = array( |
|
499 'mode' => 'error', |
|
500 'error' => 'Invalid plugin specified.', |
|
501 ); |
|
502 break; |
|
503 } |
|
504 // get plugin id |
|
505 $dataset =& $plugin_list[$filename]; |
|
506 if ( empty($dataset['plugin id']) ) |
|
507 { |
|
508 $return = array( |
|
509 'mode' => 'error', |
|
510 'error' => 'Couldn\'t retrieve plugin ID.', |
|
511 ); |
|
512 break; |
|
513 } |
|
514 |
|
515 // load up the installer schema |
|
516 $schema = $this->parse_plugin_blocks( ENANO_ROOT . '/plugins/' . $filename, 'uninstall' ); |
|
517 |
|
518 $sql = array(); |
|
519 if ( !empty($schema) ) |
|
520 { |
|
521 // parse SQL |
|
522 $parser = new SQL_Parser($schema[0]['value'], true); |
|
523 $parser->assign_vars(array( |
|
524 'TABLE_PREFIX' => table_prefix |
|
525 )); |
|
526 $sql = $parser->parse(); |
|
527 } |
|
528 |
|
529 // schema is final, check queries |
|
530 foreach ( $sql as $query ) |
|
531 { |
|
532 if ( !$db->check_query($query) ) |
|
533 { |
|
534 // aww crap, a query is bad |
|
535 $return = array( |
|
536 'mode' => 'error', |
|
537 'error' => $lang->get('acppm_err_upgrade_bad_query'), |
|
538 ); |
|
539 break 2; |
|
540 } |
|
541 } |
|
542 |
|
543 // this is it, perform uninstallation |
|
544 foreach ( $sql as $query ) |
|
545 { |
|
546 if ( substr($query, 0, 1) == '@' ) |
|
547 { |
|
548 $query = substr($query, 1); |
|
549 $db->sql_query($query); |
|
550 } |
|
551 else |
|
552 { |
|
553 if ( !$db->sql_query($query) ) |
|
554 $db->die_json(); |
|
555 } |
|
556 } |
|
557 |
|
558 // deregister plugin |
|
559 $q = $db->sql_query('DELETE FROM ' . table_prefix . "plugins WHERE plugin_id = {$dataset['plugin id']};"); |
|
560 if ( !$q ) |
|
561 $db->die_json(); |
|
562 |
|
563 $return = array( |
|
564 'success' => true |
|
565 ); |
|
566 |
|
567 endswitch; |
|
568 |
|
569 return $return; |
|
570 } |
|
571 |
|
572 /** |
|
573 * Very intelligently upgrades a plugin to the version specified in the filesystem. |
|
574 * @param string Filename of plugin. |
|
575 * @param array The list of plugins as output by pluginLoader::get_plugin_list(). If not passed, the function is called, possibly wasting time. |
|
576 * @return array JSON-formatted but not encoded response |
|
577 */ |
|
578 |
|
579 function upgrade_plugin($filename, $plugin_list = null) |
|
580 { |
|
581 global $db, $session, $paths, $template, $plugins; // Common objects |
|
582 global $lang; |
|
583 |
|
584 if ( !$plugin_list ) |
|
585 $plugin_list = $this->get_plugin_list(); |
|
586 |
|
587 // we're gonna need this |
|
588 require_once ( ENANO_ROOT . '/includes/sql_parse.php' ); |
|
589 |
|
590 switch ( true ): case true: |
|
591 |
|
592 // is the plugin in the directory and already installed? |
|
593 if ( !isset($plugin_list[$filename]) || ( |
|
594 isset($plugin_list[$filename]) && !$plugin_list[$filename]['installed'] |
|
595 )) |
|
596 { |
|
597 $return = array( |
|
598 'mode' => 'error', |
|
599 'error' => 'Invalid plugin specified.', |
|
600 ); |
|
601 break; |
|
602 } |
|
603 // get plugin id |
|
604 $dataset =& $plugin_list[$filename]; |
|
605 if ( empty($dataset['plugin id']) ) |
|
606 { |
|
607 $return = array( |
|
608 'mode' => 'error', |
|
609 'error' => 'Couldn\'t retrieve plugin ID.', |
|
610 ); |
|
611 break; |
|
612 } |
|
613 |
|
614 // |
|
615 // Here we go with the main upgrade process. This is the same logic that the |
|
616 // Enano official upgrader uses, in fact it's the same SQL parser. We need |
|
617 // list of all versions of the plugin to continue, though. |
|
618 // |
|
619 |
|
620 if ( !isset($dataset['version list']) || ( isset($dataset['version list']) && !is_array($dataset['version list']) ) ) |
|
621 { |
|
622 // no version list - update the version number but leave the rest alone |
|
623 $version = $db->escape($dataset['version']); |
|
624 $q = $db->sql_query('UPDATE ' . table_prefix . "plugins SET plugin_version = '$version' WHERE plugin_id = {$dataset['plugin id']};"); |
|
625 if ( !$q ) |
|
626 $db->die_json(); |
|
627 |
|
628 // send an error and notify the user even though it was technically a success |
|
629 $return = array( |
|
630 'mode' => 'error', |
|
631 'error' => $lang->get('acppm_err_upgrade_not_supported'), |
|
632 ); |
|
633 break; |
|
634 } |
|
635 |
|
636 // build target list |
|
637 $versions = $dataset['version list']; |
|
638 $indices = array_flip($versions); |
|
639 $installed = $dataset['version installed']; |
|
640 |
|
641 // is the current version upgradeable? |
|
642 if ( !isset($indices[$installed]) ) |
|
643 { |
|
644 $return = array( |
|
645 'mode' => 'error', |
|
646 'error' => $lang->get('acppm_err_upgrade_bad_version'), |
|
647 ); |
|
648 break; |
|
649 } |
|
650 |
|
651 // does the plugin support upgrading to its own version? |
|
652 if ( !isset($indices[$installed]) ) |
|
653 { |
|
654 $return = array( |
|
655 'mode' => 'error', |
|
656 'error' => $lang->get('acppm_err_upgrade_bad_target_version'), |
|
657 ); |
|
658 break; |
|
659 } |
|
660 |
|
661 // list out which versions to do |
|
662 $index_start = @$indices[$installed] + 1; |
|
663 $index_stop = @$indices[$dataset['version']]; |
|
664 |
|
665 // Are we trying to go backwards? |
|
666 if ( $index_stop <= $index_start ) |
|
667 { |
|
668 $return = array( |
|
669 'mode' => 'error', |
|
670 'error' => $lang->get('acppm_err_upgrade_to_older'), |
|
671 ); |
|
672 break; |
|
673 } |
|
674 |
|
675 // build the list of version sets |
|
676 $ver_previous = $installed; |
|
677 $targets = array(); |
|
678 for ( $i = $index_start; $i <= $index_stop; $i++ ) |
|
679 { |
|
680 $targets[] = array($ver_previous, $versions[$i]); |
|
681 $ver_previous = $versions[$i]; |
|
682 } |
|
683 |
|
684 // parse out upgrade sections in plugin file |
|
685 $plugin_blocks = $this->parse_plugin_blocks( ENANO_ROOT . '/plugins/' . $filename, 'upgrade' ); |
|
686 $sql_blocks = array(); |
|
687 foreach ( $plugin_blocks as $block ) |
|
688 { |
|
689 if ( !isset($block['from']) || !isset($block['to']) ) |
|
690 { |
|
691 continue; |
|
692 } |
|
693 $key = "{$block['from']} TO {$block['to']}"; |
|
694 $sql_blocks[$key] = $block['value']; |
|
695 } |
|
696 |
|
697 // do version list check |
|
698 // for now we won't fret if a specific version set isn't found, we'll just |
|
699 // not do that version and assume there were no DB changes. |
|
700 foreach ( $targets as $i => $target ) |
|
701 { |
|
702 list($from, $to) = $target; |
|
703 $key = "$from TO $to"; |
|
704 if ( !isset($sql_blocks[$key]) ) |
|
705 { |
|
706 unset($targets[$i]); |
|
707 } |
|
708 } |
|
709 $targets = array_values($targets); |
|
710 |
|
711 // parse and finalize schema |
|
712 $schema = array(); |
|
713 foreach ( $targets as $i => $target ) |
|
714 { |
|
715 list($from, $to) = $target; |
|
716 $key = "$from TO $to"; |
|
717 try |
|
718 { |
|
719 $parser = new SQL_Parser($sql_blocks[$key], true); |
|
720 } |
|
721 catch ( Exception $e ) |
|
722 { |
|
723 $return = array( |
|
724 'mode' => 'error', |
|
725 'error' => 'SQL parser init exception', |
|
726 'debug' => "$e" |
|
727 ); |
|
728 break 2; |
|
729 } |
|
730 $parser->assign_vars(array( |
|
731 'TABLE_PREFIX' => table_prefix |
|
732 )); |
|
733 $parsed = $parser->parse(); |
|
734 foreach ( $parsed as $query ) |
|
735 { |
|
736 $schema[] = $query; |
|
737 } |
|
738 } |
|
739 |
|
740 // schema is final, check queries |
|
741 foreach ( $schema as $query ) |
|
742 { |
|
743 if ( !$db->check_query($query) ) |
|
744 { |
|
745 // aww crap, a query is bad |
|
746 $return = array( |
|
747 'mode' => 'error', |
|
748 'error' => $lang->get('acppm_err_upgrade_bad_query'), |
|
749 ); |
|
750 break 2; |
|
751 } |
|
752 } |
|
753 |
|
754 // this is it, perform upgrade |
|
755 foreach ( $schema as $query ) |
|
756 { |
|
757 if ( substr($query, 0, 1) == '@' ) |
|
758 { |
|
759 $query = substr($query, 1); |
|
760 $db->sql_query($query); |
|
761 } |
|
762 else |
|
763 { |
|
764 if ( !$db->sql_query($query) ) |
|
765 $db->die_json(); |
|
766 } |
|
767 } |
|
768 |
|
769 // update version number |
|
770 $version = $db->escape($dataset['version']); |
|
771 $q = $db->sql_query('UPDATE ' . table_prefix . "plugins SET plugin_version = '$version' WHERE plugin_id = {$dataset['plugin id']};"); |
|
772 if ( !$q ) |
|
773 $db->die_json(); |
|
774 |
|
775 // all done :-) |
|
776 $return = array( |
|
777 'success' => true |
|
778 ); |
|
779 |
|
780 endswitch; |
|
781 |
|
782 return $return; |
|
783 } |
259 } |
784 } |
260 |
785 |
261 ?> |
786 ?> |