PageRenderTime 58ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/util/support.inc.php

https://github.com/sfsergey/knowledgetree
PHP | 836 lines | 519 code | 108 blank | 209 comment | 73 complexity | 745f5aec25a5c5070c890dbefd0a240b MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, GPL-3.0
  1. <?php
  2. /**
  3. * $Id:$
  4. *
  5. * KnowledgeTree Community Edition
  6. * Document Management Made Simple
  7. * Copyright (C) 2008, 2009 KnowledgeTree Inc.
  8. * Portions copyright The Jam Warehouse Software (Pty) Limited
  9. *
  10. * This program is free software; you can redistribute it and/or modify it under
  11. * the terms of the GNU General Public License version 3 as published by the
  12. * Free Software Foundation.
  13. *
  14. * This program is distributed in the hope that it will be useful, but WITHOUT
  15. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  16. * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  17. * details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. * You can contact KnowledgeTree Inc., PO Box 7775 #87847, San Francisco,
  23. * California 94120-7775, or email info@knowledgetree.com.
  24. *
  25. * The interactive user interfaces in modified source and object code versions
  26. * of this program must display Appropriate Legal Notices, as required under
  27. * Section 5 of the GNU General Public License version 3.
  28. *
  29. * In accordance with Section 7(b) of the GNU General Public License version 3,
  30. * these Appropriate Legal Notices must retain the display of the "Powered by
  31. * KnowledgeTree" logo and retain the original copyright notice. If the display of the
  32. * logo is not reasonably feasible for technical reasons, the Appropriate Legal Notices
  33. * must display the words "Powered by KnowledgeTree" and retain the original
  34. * copyright notice.
  35. * Contributor( s): ______________________________________
  36. *
  37. */
  38. /**
  39. * TODO: refactor into seperate comparison object
  40. *
  41. */
  42. class MD5SourceTree
  43. {
  44. private $rootDir;
  45. private $logFilename;
  46. private $logFile;
  47. private $numDirectories;
  48. private $numFiles;
  49. private $comparisonFailure;
  50. private $exclusions;
  51. public function __construct($exclusions = array())
  52. {
  53. $this->numDirectories = 0;
  54. $this->numFiles = 0;
  55. $this->exclusions = $exclusions;
  56. }
  57. /**
  58. * Helper function to traverse the directories. Called initially by scan()
  59. *
  60. * @param string $dir
  61. */
  62. private function _scan($dir)
  63. {
  64. if (in_array($dir, $this->exclusions))
  65. {
  66. return;
  67. }
  68. if (is_dir($dir))
  69. {
  70. if ($dh = opendir($dir))
  71. {
  72. while (($filename = readdir($dh)) !== false)
  73. {
  74. if (substr($filename,0,1) == '.')
  75. {
  76. continue;
  77. }
  78. $path = $dir . '/' . $filename;
  79. if (is_dir($path))
  80. {
  81. $this->numDirectories++;
  82. $this->_scan($path);
  83. }
  84. else
  85. {
  86. $this->numFiles++;
  87. if (is_readable($path))
  88. {
  89. $md5 = md5_file($path);
  90. $path = substr($path, strlen($this->rootDir) + 1);
  91. fwrite($this->logFile, "$md5:$path\n");
  92. }
  93. }
  94. }
  95. closedir($dh);
  96. }
  97. }
  98. }
  99. /**
  100. * This does the scan of the directory.
  101. *
  102. * @param string $rootDir
  103. * @param string $reportFile
  104. */
  105. public function scan($rootDir, $reportFile)
  106. {
  107. $this->rootDir = $rootDir;
  108. $this->logFilename = $reportFile;
  109. $this->logFile = fopen($reportFile,'wt');
  110. $this->_scan($rootDir);
  111. fclose($this->logFile);
  112. }
  113. /**
  114. * Used by the compare function, to load a md5 file
  115. *
  116. * @param string $path
  117. * @return array
  118. */
  119. private function _loadDirectory($path)
  120. {
  121. $dirs = array();
  122. $numFiles = 0;
  123. $numDirectories = 0;
  124. $fp = fopen($path, 'rt');
  125. while (!feof($fp))
  126. {
  127. $line = fgets($fp, 10240);
  128. list($md5, $path) = explode(':',$line);
  129. $dirname = dirname($path);
  130. $filename = basename($path);
  131. $numFiles++;
  132. $dirs[$dirname][$filename] = $md5;
  133. }
  134. fclose($fp);
  135. return array('numFiles'=>$numFiles, 'numDirectories'=>$numDirectories, 'dirs'=>$dirs);
  136. }
  137. /**
  138. * Internal function used to compare two md5 directory structures.
  139. *
  140. * @param array $prev
  141. * @param array $cur
  142. * @param string $msg
  143. */
  144. private function _compare($prev, $cur, $msg)
  145. {
  146. foreach($prev['dirs'] as $prevDir=>$prevDirFiles)
  147. {
  148. if (!array_key_exists($prevDir, $cur['dirs']))
  149. {
  150. print "$msg: $prevDir does not exist in target.\n";
  151. }
  152. else
  153. {
  154. foreach($prevDirFiles as $prevFilename=>$prevMD5)
  155. {
  156. if (!array_key_exists($prevFilename, $cur['dirs'][$prevDir]))
  157. {
  158. $prevFilename = substr($prevFilename,0,-1);
  159. print "$msg: $prevFilename does not exist in $prevDir.\n";
  160. }
  161. else
  162. {
  163. if (in_array($prevDir . '/' . $prevFilename, $this->comparisonFailure))
  164. {
  165. continue;
  166. }
  167. $newMD5 = $cur['dirs'][$prevDir][$prevFilename];
  168. if ($prevMD5 != $newMD5)
  169. {
  170. $this->comparisonFailure[] = $prevDir . '/' . $prevFilename;
  171. $prevFilename = substr($prevFilename,0,-1);
  172. print "$msg: $prevFilename does not match md5; $prevMD5 != $newMD5.\n";
  173. }
  174. }
  175. }
  176. }
  177. }
  178. }
  179. /**
  180. * Compare to md5 report files
  181. *
  182. * @param string $reportA
  183. * @param string $reportB
  184. */
  185. public function compare($reportA, $reportB)
  186. {
  187. if (is_null($reportB))
  188. {
  189. $reportB = $this->logFilename;
  190. }
  191. $this->comparisonFailure = array();
  192. $prev = $this->_loadDirectory($reportA);
  193. $cur = $this->_loadDirectory($reportB);
  194. if ($prev['numDirectories'] != $cur['numDirectories'])
  195. {
  196. print "Folder count mismatch!\n";
  197. }
  198. if ($prev['numFiles'] != $cur['numFiles'])
  199. {
  200. print "File count mismatch!\n";
  201. }
  202. $this->_compare($prev, $cur,'>');
  203. $this->_compare($cur,$prev,'<');
  204. }
  205. }
  206. class SupportUtil
  207. {
  208. private $path;
  209. private $innodb;
  210. private $noninnodb;
  211. /**
  212. * Constructor for SupportUtil. Creates a folder with format support-YYYY-MM-DD_HH-mm-ss
  213. *
  214. */
  215. function __construct()
  216. {
  217. $config = KTConfig::getSingleton();
  218. $tempdir = $config->get('urls/tmpDirectory');
  219. $this->path = $tempdir . "/support-" . date('Y-m-d_H-i-s');
  220. mkdir($this->path);
  221. }
  222. /**
  223. * Main function to capture as much info that is reasonable.
  224. *
  225. */
  226. public function capture()
  227. {
  228. // get php info
  229. $this->capture_phpinfo($this->path . '/phpinfo.htm');
  230. // get db schema
  231. $tables = $this->capture_db_schema($this->path);
  232. // get zseq counters from taables
  233. $this->capture_zseqs($tables, $this->path . '/zseqreport.htm');
  234. // get md5 on table
  235. $exclusions = array(
  236. KT_DIR . '/var',
  237. realpath(KT_DIR . '/../var')
  238. );
  239. $tree = new MD5SourceTree($exclusions);
  240. $config = KTConfig::getSingleton();
  241. $sourcePath = $config->get('KnowledgeTree/fileSystemRoot');
  242. $tree->scan($sourcePath, $this->path . '/md5report.txt');
  243. // get plugins
  244. $this->capture_plugins($this->path . '/plugins.htm');
  245. // get logs
  246. $this->capture_logs($this->path);
  247. // get sys info
  248. $this->get_sysinfo($this->path);
  249. // get storage engine list
  250. $this->create_storage_engine($this->path);
  251. // get disk space listing
  252. $this->capture_df($this->path);
  253. // get process listing
  254. $this->capture_ps($this->path);
  255. // get version files
  256. $this->capture_version_files($this->path);
  257. // get system settings
  258. $this->capture_system_settings($this->path);
  259. // create out index file
  260. $this->create_index($this->path);
  261. }
  262. /**
  263. * Main helper function to cleanup after creating zip file
  264. *
  265. * @param stirng $path
  266. */
  267. private function _cleanup($path)
  268. {
  269. $dh = opendir($path);
  270. while (($filename = readdir($dh)) !== false)
  271. {
  272. if (substr($filename,0,1) == '.') continue;
  273. $fullname = $path . '/' . $filename;
  274. if (is_dir($fullname))
  275. {
  276. $this->_cleanup($fullname);
  277. }
  278. else
  279. {
  280. unlink($fullname);
  281. }
  282. }
  283. closedir($dh);
  284. rmdir($path);
  285. }
  286. /**
  287. * Main cleanup function
  288. *
  289. */
  290. public function cleanup()
  291. {
  292. $this->_cleanup($this->path);
  293. }
  294. /**
  295. * Creates an archive file
  296. *
  297. * @return string
  298. */
  299. public function archive()
  300. {
  301. $zip = KTUtil::findCommand('export/zip', 'zip');
  302. chdir(dirname($this->path));
  303. $subdir = basename($this->path);
  304. $archivename = $this->path . '.zip';
  305. $cmd = "\"$zip\" -r \"$archivename\" \"$subdir\"";
  306. KTUtil::pexec($cmd);
  307. return $archivename;
  308. }
  309. /**
  310. * Tries to get list of running processes
  311. *
  312. * @param string $path
  313. */
  314. private function capture_ps($path)
  315. {
  316. $ps = KTUtil::findCommand('externalBinary/ps', 'ps');
  317. if (!file_exists($ps) || !is_executable($ps))
  318. {
  319. return;
  320. }
  321. $cmd = "'$ps' waux";
  322. // TODO: refactor to use KTUtil::pexec
  323. $ps = popen($cmd, 'r');
  324. $content = fread($ps , 10240);
  325. pclose($ps);
  326. file_put_contents($path . '/ps.txt', $content);
  327. }
  328. /**
  329. * Get list of KnowledgeTree version files
  330. *
  331. * @param string $path
  332. */
  333. private function capture_version_files($path)
  334. {
  335. $path = $path . '/versions';
  336. mkdir($path);
  337. $ver_path = KT_DIR . '/docs';
  338. $dh = opendir($ver_path);
  339. while (($filename = readdir($dh)) !== false)
  340. {
  341. if (substr($filename, 0, 7) == 'VERSION')
  342. {
  343. copy($ver_path . '/' . $filename, $path . '/' . $filename);
  344. }
  345. }
  346. closedir($dh);
  347. }
  348. /**
  349. * Dump the system_settings table, except for dashboard-state entries.
  350. *
  351. * @param string $path
  352. */
  353. private function capture_system_settings($path)
  354. {
  355. $sql = "SELECT id, name, value FROM system_settings";
  356. $rs = DBUtil::getResultArray($sql);
  357. $html = "<h1>System Settings</h1>";
  358. $html .= '<br><table border=1 cellpadding=0 cellspacing=0>';
  359. foreach($rs as $rec)
  360. {
  361. $id = $rec['id'];
  362. $name = $rec['name'];
  363. $value = $rec['value'];
  364. if (substr($name, 0, 15) == 'dashboard-state') continue;
  365. $html .= "<tr><td>$id<td>$name<td>$value\r\n";
  366. }
  367. $html .= '</table>';
  368. file_put_contents($path . '/systemsettings.htm', $html);
  369. }
  370. /**
  371. * Get disk usage
  372. *
  373. * @param string $path
  374. */
  375. private function capture_df($path)
  376. {
  377. $df = KTUtil::findCommand('externalBinary/df', 'df');
  378. if (!file_exists($df) || !is_executable($df))
  379. {
  380. return;
  381. }
  382. $df = popen($df, 'r');
  383. $content = fread($df, 10240);
  384. pclose($df);
  385. file_put_contents($path . '/df.txt', $content);
  386. }
  387. /**
  388. * Get php info
  389. *
  390. * @param string $filename
  391. */
  392. private function capture_phpinfo($filename)
  393. {
  394. ob_start();
  395. phpinfo();
  396. $phpinfo = ob_get_clean();
  397. file_put_contents($filename, $phpinfo);
  398. }
  399. /**
  400. * Helper table to get schema
  401. *
  402. * @param string $folder
  403. * @return string
  404. */
  405. private function capture_table_schema($folder)
  406. {
  407. $tables = array();
  408. $sql = 'show tables';
  409. $results = DBUtil::getResultArray($sql);
  410. foreach($results as $rec)
  411. {
  412. $rec = array_values($rec);
  413. $tablename = $rec[0];
  414. $sql = "show create table $tablename";
  415. $sql = DBUtil::getOneResultKey($sql,'Create Table');
  416. file_put_contents($folder . '/' . $tablename . '.sql.txt', $sql);
  417. $sql = strtolower($sql);
  418. if (strpos($sql, 'innodb') === false)
  419. $this->noninnodb[] = $tablename;
  420. else
  421. $this->innodb[] = $tablename;
  422. $tables[] = $tablename;
  423. }
  424. return $tables;
  425. }
  426. /**
  427. * Get database schema
  428. *
  429. * @param string $folder
  430. * @param string $suffix
  431. * @return array
  432. */
  433. private function capture_db_schema($folder, $suffix='')
  434. {
  435. $schema_folder = $folder . '/' . $suffix . 'schema';
  436. mkdir($schema_folder);
  437. return $this->capture_table_schema($schema_folder);
  438. }
  439. /**
  440. * Get list of plugins
  441. *
  442. * @param string $filename
  443. */
  444. private function capture_plugins($filename)
  445. {
  446. $sql = 'select namespace,path, disabled, unavailable,friendly_name from plugins';
  447. $result = DBUtil::getResultArray($sql);
  448. $plugins = "<h1>Plugin Status Report</h1>";
  449. $plugins .= '<table border=1 cellpadding=0 cellspacing=0u >';
  450. $plugins .= '<tr><th>Display Name<th>Availability<th>Namespace<th>Path';
  451. foreach($result as $rec)
  452. {
  453. $fileexists = file_exists(KT_DIR . '/' . $rec['path'])?'':'<font color="red">';
  454. $status = ($rec['disabled'] == 0)?'<font color="green">':'<font color="orange">';
  455. $unavailable = ($rec['unavailable'] == 0)?'available':'<font color="orange">unavailable';
  456. $plugins .= '<tr>';
  457. $plugins .= '<td>' . $status . $rec['friendly_name'];
  458. $plugins .= '<td>' . $unavailable;
  459. $plugins .= '<td>' . $rec['namespace'];
  460. $plugins .= '<td>' . $fileexists . $rec['path'] . "\r\n";
  461. }
  462. $plugins .= '</table>';
  463. $plugins .= '<br>Plugin name is <font color=green>green</font> if enabled and <font color=orange>orange</font> if disabled .';
  464. $plugins .= '<br>Availability indicates that KnowledgeTree has detected the plugin not to be available.';
  465. $plugins .= '<br>Path is coloured <font color=red>red</font> if the plugin file cannot be resolved. If the path is not resolved, it should be flagged unavailable.';
  466. file_put_contents($filename, $plugins);
  467. }
  468. /**
  469. * Make a zseq report
  470. *
  471. * @param string $tables
  472. * @param string $filename
  473. */
  474. private function capture_zseqs($tables, $filename)
  475. {
  476. $zseqs = '<h1>Table Counter Report</h1>';
  477. $zseqs .= '<table border=1 cellpadding=0 cellspacing=0>';
  478. $zseqs .= '<tr><td>Table<td>Max ID<td>ZSEQ<td>Status';
  479. foreach($tables as $ztablename)
  480. {
  481. if (substr($ztablename, 0, 5) != 'zseq_')
  482. {
  483. continue;
  484. }
  485. $tablename = substr($ztablename, 5);
  486. $sql = "SELECT max(id) as maxid FROM $tablename";
  487. $maxid = DBUtil::getOneResultKey($sql, 'maxid');
  488. $sql = "SELECT id FROM $ztablename";
  489. $zseqid = DBUtil::getOneResultKey($sql, 'id');
  490. $note = (is_null($maxid) || $maxid <= $zseqid)?'OK':'FAIL';
  491. if ($note == 'FAIL' && $maxid > $zseqid)
  492. {
  493. $note = 'COUNTER PROBLEM! maxid should be less than or equal to zseq';
  494. }
  495. if (PEAR::isError($maxid))
  496. {
  497. $maxid = '??';
  498. $note = "STRANGE - DB ERROR ON $tablename";
  499. }
  500. if (PEAR::isError($zseqid))
  501. {
  502. $zseqid = '??';
  503. $note = "STRANGE - DB ERROR ON $ztablename";
  504. }
  505. if (is_null($maxid))
  506. {
  507. $maxid='empty';
  508. }
  509. if (is_null($zseqid))
  510. {
  511. $zseqid='empty';
  512. $note = "STRANGE - ZSEQ SHOULD NOT BE EMPTY ON $ztablename";
  513. }
  514. $zseqs .= "<tr><td>$tablename<td>$maxid<td>$zseqid<td>$note\r\n";
  515. }
  516. $zseqs .= "</table>";
  517. file_put_contents($filename, $zseqs);
  518. }
  519. /**
  520. * Get log files
  521. *
  522. * @param string $path
  523. */
  524. private function capture_logs($path)
  525. {
  526. $path = $path . '/logs';
  527. mkdir($path);
  528. $this->capture_kt_log($path);
  529. $this->capture_apache_log($path);
  530. $this->capture_php_log($path);
  531. $this->capture_mysql_log($path);
  532. }
  533. /**
  534. * Get Php log file. KT makes a php_error_log when tweak setting is enabled.
  535. *
  536. * @param string $path
  537. */
  538. private function capture_php_log($path)
  539. {
  540. $config = KTConfig::getSingleton();
  541. $logdir = $config->get('urls/logDirectory');
  542. $logfile = $logdir . '/php_error_log';
  543. if (file_exists($logfile))
  544. {
  545. copy($logfile, $path . '/php-error_log.txt');
  546. }
  547. }
  548. /**
  549. * Get mysql log from stack. It is difficult to resolve otherwise.
  550. *
  551. * @param string $path
  552. */
  553. private function capture_mysql_log($path)
  554. {
  555. $stack_path = realpath(KT_DIR . '/../mysql/data');
  556. if ($stack_path === false || !is_dir($stack_path))
  557. {
  558. return;
  559. }
  560. $dh = opendir($stack_path);
  561. while (($filename = readdir($dh)) !== false)
  562. {
  563. if (substr($filename, -4) == '.log' && strpos($filename, 'err') !== false)
  564. {
  565. copy($stack_path . '/' . $filename, $path . '/mysql-' . $filename);
  566. }
  567. }
  568. closedir($dh);
  569. }
  570. /**
  571. * Get Apache log file from stack. It is difficult to resolve otherwise.
  572. *
  573. * @param string $path
  574. */
  575. private function capture_apache_log($path)
  576. {
  577. $stack_path = realpath(KT_DIR . '/../apache2/logs');
  578. if ($stack_path === false || !is_dir($stack_path))
  579. {
  580. return;
  581. }
  582. $dh = opendir($stack_path);
  583. while (($filename = readdir($dh)) !== false)
  584. {
  585. if (substr($filename, -4) == '.log' && strpos($filename, 'err') !== false)
  586. {
  587. copy($stack_path . '/' . $filename, $path . '/apache-' . $filename);
  588. }
  589. }
  590. closedir($dh);
  591. }
  592. /**
  593. * Get KT log file.
  594. *
  595. * @param string $path
  596. */
  597. private function capture_kt_log($path)
  598. {
  599. $date = date('Y-m-d');
  600. $config = KTConfig::getSingleton();
  601. $logdir = $config->get('urls/logDirectory');
  602. $dh = opendir($logdir);
  603. while (($filename = readdir($dh)) !== false)
  604. {
  605. if (substr($filename,0,14) != 'log-' . $date)
  606. {
  607. continue;
  608. }
  609. copy($logdir . '/' . $filename, $path . '/kt-' . $filename);
  610. }
  611. closedir($dh);
  612. }
  613. /**
  614. * Get some basic info on Linux if possible. Get cpuinfo, loadavg, meminfo
  615. *
  616. * @param string $path
  617. */
  618. private function get_sysinfo($path)
  619. {
  620. if (!OS_UNIX && !is_dir('/proc'))
  621. {
  622. return;
  623. }
  624. $path .= '/sysinfo';
  625. mkdir($path);
  626. $this->get_sysinfo_file('cpuinfo', $path);
  627. $this->get_sysinfo_file('loadavg', $path);
  628. $this->get_sysinfo_file('meminfo', $path);
  629. }
  630. /**
  631. * Helper to get linux sysinfo
  632. *
  633. * @param string $filename
  634. * @param string $path
  635. */
  636. private function get_sysinfo_file($filename, $path)
  637. {
  638. if (!is_readable('/proc/' . $filename))
  639. {
  640. return;
  641. }
  642. $content = file_get_contents('/proc/' . $filename);
  643. file_put_contents($path . '/' . $filename . '.txt', $content);
  644. }
  645. /**
  646. * Helper to create the index file for the support archive.
  647. *
  648. * @param string $title
  649. * @param string $path
  650. * @param boolean $relative
  651. * @return string
  652. */
  653. private function get_index_contents($title, $path, $relative = true)
  654. {
  655. if (!is_dir($path))
  656. {
  657. return '';
  658. }
  659. $contents = array();
  660. $dh = opendir($path);
  661. while (($filename = readdir($dh)) !== false)
  662. {
  663. if (substr($filename,0,1) == '.') continue;
  664. $fullname = $path . '/' . $filename;
  665. if (!file_exists($fullname) || is_dir($fullname))
  666. {
  667. continue;
  668. }
  669. $contents[] = $fullname;
  670. }
  671. closedir($dh);
  672. sort($contents);
  673. $html = $title;
  674. if (empty($contents))
  675. {
  676. $html .= 'There is no content for this section.';
  677. return $html;
  678. }
  679. $dir = '';
  680. if ($relative) $dir = basename($path) . '/';
  681. foreach($contents as $filename)
  682. {
  683. $corename = basename($filename);
  684. $ext = pathinfo($corename, PATHINFO_EXTENSION);
  685. $basename = substr($corename, 0, -strlen($ext)-1);
  686. $html .= "<a href=\"$dir$corename\">$basename</a><br>";
  687. }
  688. return $html;
  689. }
  690. /**
  691. * Create the support archvie index.htm
  692. *
  693. * @param string $path
  694. */
  695. private function create_index($path)
  696. {
  697. $contents = $this->get_index_contents('<h1>Support Info</h1><br>', $path, false);
  698. $contents .= $this->get_index_contents('<h2>System Info</h2>', $path . '/sysinfo');
  699. $contents .= $this->get_index_contents('<h2>Logs</h2>', $path . '/logs');
  700. $contents .= $this->get_index_contents('<h2>Schema</h2>', $path . '/schema');
  701. file_put_contents($path . '/index.htm', $contents);
  702. }
  703. /**
  704. * Get list of tables based on InnoDB
  705. *
  706. * @param string $path
  707. */
  708. private function create_storage_engine($path)
  709. {
  710. $html = '<h1>Table Storage Engines<h1>';
  711. $html .= '<table>';
  712. $html .= '<tr><td valign=top>';
  713. $html .= '<h2>InnoDB</h2>';
  714. foreach($this->innodb as $tablename)
  715. {
  716. $html .= "$tablename<br>";
  717. }
  718. $html .= '<td valign=top>';
  719. $html .= '<h2>Non-InnoDB</h2>';
  720. foreach($this->noninnodb as $tablename)
  721. {
  722. $html .= "$tablename<br>";
  723. }
  724. $html .= '</table>';
  725. file_put_contents($path . '/tablestorage.htm', $html);
  726. }
  727. }
  728. ?>