diff --git a/error_report.php b/error_report.php index 7bc310f067..756fa2f4a0 100644 --- a/error_report.php +++ b/error_report.php @@ -7,66 +7,124 @@ */ require_once 'libraries/common.inc.php'; require_once 'libraries/error_report.lib.php'; +require_once 'libraries/user_preferences.lib.php'; + +if (!isset($_REQUEST['exception_type']) + ||!in_array($_REQUEST['exception_type'], array('js', 'php')) +) { + die('Oops, something went wrong!!'); +} $response = PMA_Response::getInstance(); if (isset($_REQUEST['send_error_report']) - && $_REQUEST['send_error_report'] == true + && ($_REQUEST['send_error_report'] == true + || $_REQUEST['send_error_report'] == '1') ) { - $server_response = PMA_sendErrorReport(PMA_getReportData()); - - if ($server_response === false) { - $success = false; - } else { - $decoded_response = json_decode($server_response, true); - $success = !empty($decoded_response) ? $decoded_response["success"] : false; - } - - /* Message to show to the user */ - if ($success) { - if (isset($_REQUEST['automatic']) - && $_REQUEST['automatic'] === "true" + if ($_REQUEST['exception_type'] == 'php') { + /** + * Prevent inifnite error submission. + * Happens in case error submissions fails. + * If reporting is done in some time interval, + * just clear them & clear json data too. + */ + if (isset($_SESSION['prev_error_subm_time']) + && isset($_SESSION['error_subm_count']) + && $_SESSION['error_subm_count'] >= 3 + && ($_SESSION['prev_error_subm_time']-time()) <= 3000 ) { - $message = __( - 'An error has been detected and an error report has been ' - . 'automatically submitted based on your settings.' - ); + $_SESSION['error_subm_count'] = 0; + $_SESSION['prev_errors'] = ''; + $response = PMA_Response::getInstance(); + $response->addJSON('_stopErrorReportLoop', '1'); } else { - $message = __('Thank you for submitting this report.'); + $_SESSION['prev_error_subm_time'] = time(); + $_SESSION['error_subm_count'] = ( + (isset($_SESSION['error_subm_count'])) + ? ($_SESSION['error_subm_count']+1) + : (0) + ); } - } else { - $message = __( - 'An error has been detected and an error report has been ' - . 'generated but failed to be sent.' - ) - . ' ' - . __( - 'If you experience any ' - . 'problems please submit a bug report manually.' - ); } - $message .= ' ' . __('You may want to refresh the page.'); + $reportData = PMA_getReportData($_REQUEST['exception_type']); + // report if and only if there were 'actual' errors. + if (count($reportData) > 0) { + $server_response = PMA_sendErrorReport($reportData); + if ($server_response === false) { + $success = false; + } else { + $decoded_response = json_decode($server_response, true); + $success = !empty($decoded_response) ? + $decoded_response["success"] : false; + } - /* Create message object */ - if ($success) { - $message = PMA_Message::notice($message); - } else { - $message = PMA_Message::error($message); - } + /* Message to show to the user */ + if ($success) { + if ((isset($_REQUEST['automatic']) + && $_REQUEST['automatic'] === "true") + || $GLOBALS['cfg']['SendErrorReports'] == 'always' + ) { + $msg = __( + 'An error has been detected and an error report has been ' + . 'automatically submitted based on your settings.' + ); + } else { + $msg = __('Thank you for submitting this report.'); + } + } else { + $msg = __( + 'An error has been detected and an error report has been ' + . 'generated but failed to be sent.' + ) + . ' ' + . __( + 'If you experience any ' + . 'problems please submit a bug report manually.' + ); + } + $msg .= ' ' . __('You may want to refresh the page.'); - /* Add message to JSON response */ - $response->addJSON('message', $message); + /* Create message object */ + if ($success) { + $msg = PMA_Message::notice($msg); + } else { + $msg = PMA_Message::error($msg); + } - /* Persist always send settings */ - if (! isset($_REQUEST['automatic']) - && $_REQUEST['automatic'] !== "true" - && isset($_REQUEST['always_send']) - && $_REQUEST['always_send'] === "true" - ) { - PMA_persistOption("SendErrorReports", "always", "ask"); + /* Add message to response */ + if ($response->isAjax()) { + if ($_REQUEST['exception_type'] == 'js') { + $response->addJSON('message', $msg); + } else { + $response->addJSON('_errSubmitMsg', $msg); + } + } elseif ($_REQUEST['exception_type'] == 'php') { + $jsCode = 'PMA_ajaxShowMessage("
' + . $msg + . '
", false);'; + $response->getFooter()->getScripts()->addCode($jsCode); + } + + if ($_REQUEST['exception_type'] == 'php') { + // clear previous errors & save new ones. + $GLOBALS['error_handler']->savePreviousErrors(); + } + + /* Persist always send settings */ + if (isset($_REQUEST['always_send']) + && $_REQUEST['always_send'] === "true" + ) { + PMA_persistOption("SendErrorReports", "always", "ask"); + } } } elseif (! empty($_REQUEST['get_settings'])) { $response->addJSON('report_setting', $GLOBALS['cfg']['SendErrorReports']); } else { - $response->addHTML(PMA_getErrorReportForm()); + if ($_REQUEST['exception_type'] == 'js') { + $response->addHTML(PMA_getErrorReportForm()); + } else { + // clear previous errors & save new ones. + $GLOBALS['error_handler']->savePreviousErrors(); + } } +?> diff --git a/js/ajax.js b/js/ajax.js index cb41d2ed6d..e81eb8419c 100644 --- a/js/ajax.js +++ b/js/ajax.js @@ -388,11 +388,47 @@ var AJAX = { } $('#pma_errors').remove(); + + var msg = ''; + if(data._errSubmitMsg){ + msg = data._errSubmitMsg; + } if (data._errors) { $('
', {id : 'pma_errors'}) .insertAfter('#selflink') .append(data._errors); + // bind for php error reporting forms (bottom) + $("#pma_ignore_errors_bottom").bind("click", + function() { + PMA_ignorePhpErrors(); + }); + $("#pma_ignore_all_errors_bottom").bind("click", + function() { + PMA_ignorePhpErrors(false); + }); + // In case of 'sendErrorReport'='always' + // submit the hidden error reporting form. + if (data._sendErrorAlways == '1' + && data._stopErrorReportLoop != '1' + ) { + $("#pma_report_errors_form").submit(); + PMA_ajaxShowMessage(PMA_messages['phpErrorsBeingSubmitted'], false); + $('html, body').animate({scrollTop:$(document).height()}, 'slow'); + } else if (data._promptPhpErrors) { + // otherwise just prompt user if it is set so. + msg = msg + PMA_messages['phpErrorsFound']; + // scroll to bottom where all the erros are displayed. + $('html, body').animate({scrollTop:$(document).height()}, 'slow'); + } } + PMA_ajaxShowMessage(msg, false); + // bind for php error reporting forms (popup) + $("#pma_ignore_errors_popup").bind("click", function() { + PMA_ignorePhpErrors() + }); + $("#pma_ignore_all_errors_popup").bind("click", function() { + PMA_ignorePhpErrors(false) + }); if (typeof AJAX._callback === 'function') { AJAX._callback.call(); diff --git a/js/error_report.js b/js/error_report.js index 853a76cfed..a8e7da7dd1 100644 --- a/js/error_report.js +++ b/js/error_report.js @@ -23,7 +23,8 @@ var ErrorReport = { ajax_request: true, server: PMA_commonParams.get('server'), token: PMA_commonParams.get('token'), - get_settings: true + get_settings: true, + exception_type: 'js' }, function (data) { if (data.success !== true) { PMA_ajaxShowMessage(data.error, false); @@ -227,7 +228,8 @@ var ErrorReport = { "token": PMA_commonParams.get('token'), "exception": exception, "current_url": window.location.href, - "microhistory": ErrorReport._get_microhistory() + "microhistory": ErrorReport._get_microhistory(), + "exception_type": 'js' }; if (typeof AJAX.cache.pages[AJAX.cache.current - 1] !== 'undefined') { report_data.scripts = AJAX.cache.pages[AJAX.cache.current - 1].scripts.map( diff --git a/js/functions.js b/js/functions.js index 000704fee6..7f9a86694c 100644 --- a/js/functions.js +++ b/js/functions.js @@ -4379,3 +4379,29 @@ function PMA_previewSQL($form) } }); } + +/** + * Ignore the displayed php errors. + * Simply removes the displayed errors. + * + * @param clearPrevErrors whether to clear errors stored + * in $_SESSION['prev_errors'] at server + * + */ +function PMA_ignorePhpErrors(clearPrevErrors){ + if (typeof(clearPrevErrors) === "undefined" + || clearPrevErrors === null + ) { + str = false; + } + // send AJAX request to error_report.php with send_error_report=0, exception_type=php & token. + // It clears the prev_errors stored in session. + if(clearPrevErrors){ + $('#pma_report_errors_form input[name="send_error_report"]').val(0); // change send_error_report to '0' + $('#pma_report_errors_form').submit(); + } + + // remove dislayed errors + $('#pma_errors').fadeOut( "slow"); + $('#pma_errors').remove(); +} diff --git a/js/messages.php b/js/messages.php index 1f3c1edb85..a50184b485 100644 --- a/js/messages.php +++ b/js/messages.php @@ -426,6 +426,29 @@ $js_messages['strTooManyInputs'] = __( . "max_input_vars configuration." ); +$js_messages['phpErrorsFound'] = '
' + . __('Warning: Some errors have been detected on the server!!') + . '
' + . __('Please look at the bottom of this window.') + . '' + . '' + . '
'; + +$js_messages['phpErrorsBeingSubmitted'] = '
' + . __('Some errors have been detected on the server!!') + . '
' + . __('As per your settings, they are being submitted currently.') + . __(' Please be patient.') + . '
' + . 'ajax clock' + . '
'; + echo "var PMA_messages = new Array();\n"; foreach ($js_messages as $name => $js_message) { PMA_printJsValue("PMA_messages['" . $name . "']", $js_message); diff --git a/libraries/Error.class.php b/libraries/Error.class.php index fd4a2fc83c..842af8c66e 100644 --- a/libraries/Error.class.php +++ b/libraries/Error.class.php @@ -188,12 +188,19 @@ class PMA_Error extends PMA_Message } /** - * returns PMA_Error::$_backtrace + * returns PMA_Error::$_backtrace for first $count frames + * pass $count = -1 to get full backtrace. + * The same can be done by not passing $count at all. + * + * @param integer $count Number of stack frames. * * @return array PMA_Error::$_backtrace */ - public function getBacktrace() + public function getBacktrace($count = -1) { + if ($count != -1) { + return array_slice($this->backtrace, 0, $count); + } return $this->backtrace; } diff --git a/libraries/Error_Handler.class.php b/libraries/Error_Handler.class.php index c9c3176eee..7592c322cb 100644 --- a/libraries/Error_Handler.class.php +++ b/libraries/Error_Handler.class.php @@ -94,6 +94,17 @@ class PMA_Error_Handler return $this->errors; } + /** + * returns the errors occured in the current run only. + * Does not include the errors save din the SESSION + * + * @return array of current errors + */ + public function getCurrentErrors() + { + return $this->errors; + } + /** * Error handler - called when errors are triggered/occurred * @@ -281,7 +292,10 @@ class PMA_Error_Handler public function getDispErrors() { $retval = ''; - if ($GLOBALS['cfg']['Error_Handler']['display']) { + // display errors if SendErrorReports is set to 'ask'. + if ($GLOBALS['cfg']['SendErrorReports'] != 'never' + || $GLOBALS['cfg']['Error_Handler']['display'] + ) { foreach ($this->getErrors() as $error) { if ($error instanceof PMA_Error) { if (! $error->isDisplayed()) { @@ -297,6 +311,46 @@ class PMA_Error_Handler } else { $retval .= $this->getDispUserErrors(); } + // if preference is not 'never' and + // there are 'actual' errors to be reported + if ($GLOBALS['cfg']['SendErrorReports'] != 'never' + && $this->countErrors() != $this->countUserErrors() + ) { + // add report button. + $retval .= '
' + . '' + . '' + . '' + . '' + . '' + . '
'; + + if ($GLOBALS['cfg']['SendErrorReports'] == 'ask') { + // add ignore buttons + $retval .= ''; + } + $retval .= ''; + } return $retval; } @@ -389,7 +443,9 @@ class PMA_Error_Handler */ public function countDisplayErrors() { - if ($GLOBALS['cfg']['Error_Handler']['display']) { + if ($GLOBALS['cfg']['SendErrorReports'] != 'never' + || $GLOBALS['cfg']['Error_Handler']['display'] + ) { return $this->countErrors(); } else { return $this->countUserErrors(); @@ -405,5 +461,102 @@ class PMA_Error_Handler { return (bool) $this->countDisplayErrors(); } + + /** + * Deletes prevsiously stored errors in SESSION. + * Saves current errors in session as previous errros. + * Required to save current errors in case 'ask' + * + * @return void + */ + public function savePreviousErrors() + { + unset($_SESSION['prev_errors']); + $_SESSION['prev_errors'] = $GLOBALS['error_handler']->getCurrentErrors(); + } + + /** + * Function to check if there are any errors to be prompted. + * Needed because user warnings raised are + * also collected by global error handler. + * This dishtingushes between the actual errors + * and user errors raised to warn user. + * + *@return boolean: true if there are errors to be "prompted", false otherwise + */ + public function hasErrorsForPrompt() + { + return ( + ($GLOBALS['cfg']['SendErrorReports'] != 'never' + || $GLOBALS['cfg']['Error_Handler']['display']) + && $this->countErrors() != $this->countUserErrors() + ); + } + + /** + * Function to report all the collected php errors. + * Must be called at the end of each script + * by the $GLOBALS['error_handler'] only. + * + * @return void + */ + + public function reportErrors() + { + // if there're no actual errors, + if (!$this->hasErrors() + || $this->countErrors() == $this->countUserErrors() + ) { + // then simply return. + return; + } + // Delete all the prev_errors in session & store new prev_errors in session + $this->savePreviousErrors(); + $response = PMA_Response::getInstance(); + $jsCode = ''; + if ($GLOBALS['cfg']['SendErrorReports'] == 'always') { + if ($response->isAjax()) { + // set flag for automatic report submission. + $response->addJSON('_sendErrorAlways', '1'); + } else { + // send the error reports asynchronously & without asking user + $jsCode .= '$("#pma_report_errors_form").submit();' + . 'PMA_ajaxShowMessage( + PMA_messages["phpErrorsBeingSubmitted"], false + );'; + // js code to appropriate focusing, + $jsCode .= '$("html, body").animate({ + scrollTop:$(document).height() + }, "slow");'; + } + } elseif ($GLOBALS['cfg']['SendErrorReports'] == 'ask') { + //ask user whether to submit errors or not. + if (!$response->isAjax()) { + // js code to show appropriate msgs, event binding & focusing. + $jsCode = 'PMA_ajaxShowMessage(PMA_messages["phpErrorsFound"], ' + . ' 2000);' + . '$("#pma_ignore_errors_popup").bind("click", function() { + PMA_ignorePhpErrors() + });' + . '$("#pma_ignore_all_errors_popup").bind("click", + function() { + PMA_ignorePhpErrors(false) + });' + . '$("#pma_ignore_errors_bottom").bind("click", function() { + PMA_ignorePhpErrors() + });' + . '$("#pma_ignore_all_errors_bottom").bind("click", + function() { + PMA_ignorePhpErrors(false) + });' + . '$("html, body").animate({ + scrollTop:$(document).height() + }, "slow");'; + } + } + // The errors are already sent from the resnpose. + // Just focus on errors division upon load event. + $response->getFooter()->getScripts()->addCode($jsCode); + } } ?> diff --git a/libraries/Footer.class.php b/libraries/Footer.class.php index 095552e73c..a114d5e2f4 100644 --- a/libraries/Footer.class.php +++ b/libraries/Footer.class.php @@ -209,6 +209,12 @@ class PMA_Footer $retval .= $GLOBALS['error_handler']->getDispErrors(); $retval .= '
'; } + + /** + * Report php errors + */ + $GLOBALS['error_handler']->reportErrors(); + return $retval; } diff --git a/libraries/Message.class.php b/libraries/Message.class.php index 4132faa555..6bffe87b7c 100644 --- a/libraries/Message.class.php +++ b/libraries/Message.class.php @@ -655,6 +655,17 @@ class PMA_Message return $message; } + /** + * Returns only message string without image & other HTML. + * + * @return $message string + */ + public function getOnlyMessage() + { + return $this->message; + } + + /** * returns PMA_Message::$string * diff --git a/libraries/Response.class.php b/libraries/Response.class.php index 45a4fd2c98..c34cf23ddf 100644 --- a/libraries/Response.class.php +++ b/libraries/Response.class.php @@ -316,6 +316,9 @@ class PMA_Response if (strlen($errors)) { $this->addJSON('_errors', $errors); } + $promptPhpErrors = $GLOBALS['error_handler']->hasErrorsForPrompt(); + $this->addJSON('_promptPhpErrors', $promptPhpErrors); + if (empty($GLOBALS['error_message'])) { // set current db, table and sql query in the querywindow $query = ''; diff --git a/libraries/error_report.lib.php b/libraries/error_report.lib.php index 0df479c8fe..d2279f1c24 100644 --- a/libraries/error_report.lib.php +++ b/libraries/error_report.lib.php @@ -52,37 +52,80 @@ function PMA_getPrettyReportData() * returns the error report data collected from the current configuration or * from the request parameters sent by the error reporting js code. * - * @return Array the report + * @param string $exception_type whether exception is 'js' or 'php' + * + * @return Array error report if success, Empty Array otherwise */ -function PMA_getReportData() +function PMA_getReportData($exception_type = 'js') { - if (empty($_REQUEST['exception'])) { - return array(); - } - $exception = $_REQUEST['exception']; - $exception["stack"] = PMA_translateStacktrace($exception["stack"]); - List($uri, $script_name) = PMA_sanitizeUrl($exception["url"]); - $exception["uri"] = $uri; - unset($exception["url"]); + $relParams = PMA_getRelationsParam(); + // common params for both, php & js execptions $report = array( - "exception" => $exception, - "script_name" => $script_name, - "pma_version" => PMA_VERSION, - "browser_name" => PMA_USR_BROWSER_AGENT, - "browser_version" => PMA_USR_BROWSER_VER, - "user_os" => PMA_USR_OS, - "server_software" => $_SERVER['SERVER_SOFTWARE'], - "user_agent_string" => $_SERVER['HTTP_USER_AGENT'], - "locale" => $_COOKIE['pma_lang'], - "configuration_storage" => - empty($GLOBALS['cfg']['Servers'][1]['pmadb']) ? "disabled" : - "enabled", - "php_version" => phpversion(), - "microhistory" => $_REQUEST['microhistory'], - ); + "pma_version" => PMA_VERSION, + "browser_name" => PMA_USR_BROWSER_AGENT, + "browser_version" => PMA_USR_BROWSER_VER, + "user_os" => PMA_USR_OS, + "server_software" => $_SERVER['SERVER_SOFTWARE'], + "user_agent_string" => $_SERVER['HTTP_USER_AGENT'], + "locale" => $_COOKIE['pma_lang'], + "configuration_storage" => + is_null($relParams['db']) ? "disabled" : + "enabled", + "php_version" => phpversion() + ); - if (! empty($_REQUEST['description'])) { - $report['steps'] = $_REQUEST['description']; + if ($exception_type == 'js') { + if (empty($_REQUEST['exception'])) { + return array(); + } + $exception = $_REQUEST['exception']; + $exception["stack"] = PMA_translateStacktrace($exception["stack"]); + List($uri, $script_name) = PMA_sanitizeUrl($exception["url"]); + $exception["uri"] = $uri; + unset($exception["url"]); + + $report ["exception_type"] = 'js'; + $report ["exception"] = $exception; + $report ["script_name"] = $script_name; + $report ["microhistory"] = $_REQUEST['microhistory']; + + if (! empty($_REQUEST['description'])) { + $report['steps'] = $_REQUEST['description']; + } + } elseif ($exception_type == 'php') { + $errors = array(); + // create php error report + $i=0; + if (!isset($_SESSION['prev_errors']) + || $_SESSION['prev_errors'] == '' + ) { + return array(); + } + foreach ($_SESSION['prev_errors'] as $errorObj) { + if ($errorObj->getLine() + && $errorObj->getType() + && $errorObj->getNumber() != E_USER_WARNING + ) { + $errors[$i++] = array( + "lineNum" => $errorObj->getLine(), + "file" => $errorObj->getFile(), + "type" => $errorObj->getType(), + "msg" => $errorObj->getOnlyMessage(), + "stackTrace" => $errorObj->getBacktrace(5), + "stackhash" => $errorObj->getHash() + ); + + } + } + + // if there were no 'actual' errors to be submitted. + if ($i==0) { + return array(); // then return empty array + } + $report ["exception_type"] = 'php'; + $report["errors"] = $errors; + } else { + return array(); } return $report; diff --git a/libraries/plugins/auth/AuthenticationCookie.class.php b/libraries/plugins/auth/AuthenticationCookie.class.php index 0f2a883dac..96771075a0 100644 --- a/libraries/plugins/auth/AuthenticationCookie.class.php +++ b/libraries/plugins/auth/AuthenticationCookie.class.php @@ -284,7 +284,7 @@ class AuthenticationCookie extends AuthenticationPlugin // END Swekey Integration if ($GLOBALS['error_handler']->hasDisplayErrors()) { - echo '
'; + echo '
'; $GLOBALS['error_handler']->dispErrors(); echo '
'; } diff --git a/querywindow.php b/querywindow.php index 5090e3d14d..f14f600e66 100644 --- a/querywindow.php +++ b/querywindow.php @@ -205,3 +205,4 @@ echo ''; echo ''; echo '
'; +?> diff --git a/schema_export.php b/schema_export.php index e150db190d..aba067345b 100644 --- a/schema_export.php +++ b/schema_export.php @@ -72,3 +72,4 @@ if (isset($_REQUEST['do'])) { $user_schema->setAction($_REQUEST['do']); $user_schema->processUserChoice(); } +?> diff --git a/server_status_advisor.php b/server_status_advisor.php index 6f882033cc..0d6d3b94d0 100644 --- a/server_status_advisor.php +++ b/server_status_advisor.php @@ -32,8 +32,7 @@ $response->addHTML('
'); $response->addHTML($ServerStatusData->getMenuHtml()); $response->addHTML(PMA_getHtmlForAdvisor()); $response->addHTML('
'); + exit; - - ?> diff --git a/test/classes/PMA_Error_Handler_test.php b/test/classes/PMA_Error_Handler_test.php index 9a271c7737..1dac61d4c1 100644 --- a/test/classes/PMA_Error_Handler_test.php +++ b/test/classes/PMA_Error_Handler_test.php @@ -107,7 +107,7 @@ class PMA_Error_Handler_Test extends PHPUnit_Framework_TestCase * @param string $output_show expected output if showing of errors is * enabled * @param string $output_hide expected output if showing of errors is - * disabled + * disabled and 'sendErrorReports' is set to 'never' * * @return void * @@ -116,6 +116,8 @@ class PMA_Error_Handler_Test extends PHPUnit_Framework_TestCase public function testGetDispErrorsForDisplayFalse( $errno, $errstr, $errfile, $errline, $output_show, $output_hide ) { + // TODO: Add other test cases for all combination of 'sendErrorReports' + $GLOBALS['cfg']['SendErrorReports'] = 'never'; $GLOBALS['cfg']['Error_Handler']['gather'] = true; $GLOBALS['cfg']['Error_Handler']['display'] = false; diff --git a/test/classes/PMA_Error_test.php b/test/classes/PMA_Error_test.php index a789706348..8940ee973e 100644 --- a/test/classes/PMA_Error_test.php +++ b/test/classes/PMA_Error_test.php @@ -152,4 +152,19 @@ class PMA_Error_Test extends PHPUnit_Framework_TestCase { $this->assertEquals('Warning: Compile Error', $this->object->getTitle()); } + + /** + * Test for getBacktrace + * + * @return void + */ + public function testGetBacktrace() + { + $this->object->setBacktrace(array('bt1','bt2','bt3','bt4')); + // case: full backtrace + $this->assertEquals(array('bt1','bt2','bt3','bt4'), $this->object->getBacktrace()); + + // case: first 2 frames + $this->assertEquals(array('bt1','bt2'), $this->object->getBacktrace(2)); + } } diff --git a/view_create.php b/view_create.php index d59eb9c39b..2c8a410793 100644 --- a/view_create.php +++ b/view_create.php @@ -284,3 +284,4 @@ $htmlString .= '' . ''; echo $htmlString; +?> diff --git a/view_operations.php b/view_operations.php index 7616dbb119..153249a3df 100644 --- a/view_operations.php +++ b/view_operations.php @@ -136,3 +136,4 @@ echo PMA_getDeleteDataOrTableLink( echo ''; echo ''; echo ''; +?>