<?php
// Stands in for the Panorama Admin Console's
//    /php/device/config.export.php?name=running-config.xml
// which is the URL in the SV-33388 capture:
//
//    ["TAB_open",4,"https://ztn-panorama01.fenicsops.com/php/device/
//                   config.export.php?name=running-config.xml",
//     false,"",false,true]
//
// The response is a PAN-OS style configuration snapshot served as an
// attachment, so the navigation in the newly opened tab never commits and the
// surrogate takes the !HasCommitted() path in
// SafeviewNavigationController::DidFinishNavigation. Only the headers matter
// for the repro; the body is filler shaped like a real config so the file is
// recognisable after a successful download.
//
// ?name=<file>     attachment filename, default running-config.xml. Panorama
//                  passes the chosen snapshot name here, so vary it to avoid
//                  cache or on-disk collisions between attempts.
// ?size=<bytes>    approximate body size, default 65536
// ?delay_ms=<ms>   spread the body over this many ms. Not needed for the
//                  reported case (the failure happens before a single byte is
//                  delivered to the client) but useful to hold the surrogate
//                  download open while inspecting active_surr_download_items_.
set_time_limit(0);

// Everything this script does before header() must produce no output. A single
// notice or deprecation from the body-building loop counts as output, and PHP
// then refuses every header() below -- leaving a text/html page instead of an
// attachment. That does not look like a broken fixture, it looks like the bug
// went away: the navigation commits, the new tab renders, the download path is
// never entered. Buffer the build so a stray notice cannot do that silently,
// and surface anything caught as a response header instead.
ob_start();

$name = isset($_GET['name']) ? basename($_GET['name']) : 'running-config.xml';
$target_size = isset($_GET['size']) ? intval($_GET['size']) : 65536;
$delay_ms = isset($_GET['delay_ms']) ? intval($_GET['delay_ms']) : 0;

$head = "<?xml version=\"1.0\"?>\n"
      . "<config version=\"11.1.0\" urldb=\"paloaltonetworks\">\n"
      . "  <mgt-config>\n"
      . "    <users>\n"
      . "      <entry name=\"admin\">\n"
      . "        <permissions><role-based><superuser>yes</superuser>"
      . "</role-based></permissions>\n"
      . "      </entry>\n"
      . "    </users>\n"
      . "  </mgt-config>\n"
      . "  <devices>\n"
      . "    <entry name=\"localhost.localdomain\">\n"
      . "      <network>\n"
      . "        <interface>\n";
$tail = "        </interface>\n"
      . "      </network>\n"
      . "    </entry>\n"
      . "  </devices>\n"
      . "</config>\n";

$body = $head;
$if_no = 0;
while (true) {
   $if_no += 1;
   $entry = sprintf("          <entry name=\"ethernet1/%d\">\n"
                  . "            <layer3><ip><entry name=\"10.%d.%d.1/24\"/>"
                  . "</ip></layer3>\n"
                  . "            <comment>uplink-%03d</comment>\n"
                  . "          </entry>\n",
                  // intdiv, not "/": in PHP 8 the "/" operator yields a float
                  // and applying "%" to it raises an implicit-conversion
                  // deprecation. Any notice printed here is body output, which
                  // makes the header() calls below fail with "headers already
                  // sent" -- and without Content-Disposition the response is no
                  // longer a download, so the test silently stops reproducing.
                    $if_no, intdiv($if_no, 254) % 254, $if_no % 254, $if_no);
   if (strlen($body) + strlen($entry) + strlen($tail) > $target_size) {
      break;
   }
   $body .= $entry;
}
$body .= $tail;

// Discard anything the build leaked so the headers below can still be sent. If
// something was caught, report it in X-Fixture-Warning rather than swallowing
// it: the response stays a valid download and the problem is still visible in
// devtools / the HAR.
$leaked = ob_get_clean();
if ($leaked !== '' && $leaked !== false) {
   header("X-Fixture-Warning: " .
          str_replace(array("\r", "\n"), ' ', substr($leaked, 0, 400)));
}

// These are the headers that matter. Content-Disposition: attachment is what
// makes handle->IsDownload() true and HasCommitted() false, which is the whole
// bug -- without it the new tab commits normally and client_loc/Initial is
// sent.
header("HTTP/1.1 200 OK");
header("Content-Type: application/xml");
header("Content-Disposition: attachment; filename=\"" . $name . "\"");
header("Content-Length: " . strlen($body));
header("Cache-Control: no-cache");

if ($delay_ms <= 0) {
   echo $body;
} else {
   $chunks = 20;
   $chunk_size = (int)ceil(strlen($body) / $chunks);
   $chunk_delay_us = intval($delay_ms * 1000 / $chunks);
   for ($offset = 0; $offset < strlen($body); $offset += $chunk_size) {
      echo substr($body, $offset, $chunk_size);
      flush();
      usleep($chunk_delay_us);
   }
}
?>
