[ Index ]

MailPress 7.1

[ Index ]     [ Classes ]     [ Functions ]     [ Variables ]     [ Constants ]     [ Statistics ]    

title

Body

[close]

/mp-includes/composer/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/Auth/ -> NTLMAuthenticator.php (source)

   1  <?php
   2  
   3  /*
   4   * This file is part of SwiftMailer.
   5   * (c) 2004-2009 Chris Corbyn
   6   *
   7   * This authentication is for Exchange servers. We support version 1 & 2.
   8   *
   9   * For the full copyright and license information, please view the LICENSE
  10   * file that was distributed with this source code.
  11   */
  12  
  13  /**
  14   * Handles NTLM authentication.
  15   *
  16   * @author     Ward Peeters <ward@coding-tech.com>
  17   */
  18  class Swift_Transport_Esmtp_Auth_NTLMAuthenticator implements Swift_Transport_Esmtp_Authenticator
  19  {
  20      const NTLMSIG = "NTLMSSP\x00";
  21      const DESCONST = 'KGS!@#$%';
  22  
  23      /**
  24       * Get the name of the AUTH mechanism this Authenticator handles.
  25       *
  26       * @return string
  27       */
  28      public function getAuthKeyword()
  29      {
  30          return 'NTLM';
  31      }
  32  
  33      /**
  34       * {@inheritdoc}
  35       *
  36       * @throws \LogicException
  37       */
  38      public function authenticate(Swift_Transport_SmtpAgent $agent, $username, $password)
  39      {
  40          if (!function_exists('openssl_encrypt')) {
  41              throw new LogicException('The OpenSSL extension must be enabled to use the NTLM authenticator.');
  42          }
  43  
  44          if (!function_exists('bcmul')) {
  45              throw new LogicException('The BCMath functions must be enabled to use the NTLM authenticator.');
  46          }
  47  
  48          try {
  49              // execute AUTH command and filter out the code at the beginning
  50              // AUTH NTLM xxxx
  51              $response = base64_decode(substr(trim($this->sendMessage1($agent)), 4));
  52  
  53              // extra parameters for our unit cases
  54              $timestamp = func_num_args() > 3 ? func_get_arg(3) : $this->getCorrectTimestamp(bcmul(microtime(true), '1000'));
  55              $client = func_num_args() > 4 ? func_get_arg(4) : random_bytes(8);
  56  
  57              // Message 3 response
  58              $this->sendMessage3($response, $username, $password, $timestamp, $client, $agent);
  59  
  60              return true;
  61          } catch (Swift_TransportException $e) {
  62              $agent->executeCommand("RSET\r\n", [250]);
  63  
  64              throw $e;
  65          }
  66      }
  67  
  68      protected function si2bin($si, $bits = 32)
  69      {
  70          $bin = null;
  71          if ($si >= -pow(2, $bits - 1) && ($si <= pow(2, $bits - 1))) {
  72              // positive or zero
  73              if ($si >= 0) {
  74                  $bin = base_convert($si, 10, 2);
  75                  // pad to $bits bit
  76                  $bin_length = strlen($bin);
  77                  if ($bin_length < $bits) {
  78                      $bin = str_repeat('0', $bits - $bin_length).$bin;
  79                  }
  80              } else {
  81                  // negative
  82                  $si = -$si - pow(2, $bits);
  83                  $bin = base_convert($si, 10, 2);
  84                  $bin_length = strlen($bin);
  85                  if ($bin_length > $bits) {
  86                      $bin = str_repeat('1', $bits - $bin_length).$bin;
  87                  }
  88              }
  89          }
  90  
  91          return $bin;
  92      }
  93  
  94      /**
  95       * Send our auth message and returns the response.
  96       *
  97       * @return string SMTP Response
  98       */
  99      protected function sendMessage1(Swift_Transport_SmtpAgent $agent)
 100      {
 101          $message = $this->createMessage1();
 102  
 103          return $agent->executeCommand(sprintf("AUTH %s %s\r\n", $this->getAuthKeyword(), base64_encode($message)), [334]);
 104      }
 105  
 106      /**
 107       * Fetch all details of our response (message 2).
 108       *
 109       * @param string $response
 110       *
 111       * @return array our response parsed
 112       */
 113      protected function parseMessage2($response)
 114      {
 115          $responseHex = bin2hex($response);
 116          $length = floor(hexdec(substr($responseHex, 28, 4)) / 256) * 2;
 117          $offset = floor(hexdec(substr($responseHex, 32, 4)) / 256) * 2;
 118          $challenge = hex2bin(substr($responseHex, 48, 16));
 119          $context = hex2bin(substr($responseHex, 64, 16));
 120          $targetInfoH = hex2bin(substr($responseHex, 80, 16));
 121          $targetName = hex2bin(substr($responseHex, $offset, $length));
 122          $offset = floor(hexdec(substr($responseHex, 88, 4)) / 256) * 2;
 123          $targetInfoBlock = substr($responseHex, $offset);
 124          list($domainName, $serverName, $DNSDomainName, $DNSServerName, $terminatorByte) = $this->readSubBlock($targetInfoBlock);
 125  
 126          return [
 127              $challenge,
 128              $context,
 129              $targetInfoH,
 130              $targetName,
 131              $domainName,
 132              $serverName,
 133              $DNSDomainName,
 134              $DNSServerName,
 135              hex2bin($targetInfoBlock),
 136              $terminatorByte,
 137          ];
 138      }
 139  
 140      /**
 141       * Read the blob information in from message2.
 142       *
 143       * @return array
 144       */
 145      protected function readSubBlock($block)
 146      {
 147          // remove terminatorByte cause it's always the same
 148          $block = substr($block, 0, -8);
 149  
 150          $length = strlen($block);
 151          $offset = 0;
 152          $data = [];
 153          while ($offset < $length) {
 154              $blockLength = hexdec(substr(substr($block, $offset, 8), -4)) / 256;
 155              $offset += 8;
 156              $data[] = hex2bin(substr($block, $offset, $blockLength * 2));
 157              $offset += $blockLength * 2;
 158          }
 159  
 160          if (3 == count($data)) {
 161              $data[] = $data[2];
 162              $data[2] = '';
 163          }
 164  
 165          $data[] = $this->createByte('00');
 166  
 167          return $data;
 168      }
 169  
 170      /**
 171       * Send our final message with all our data.
 172       *
 173       * @param string $response  Message 1 response (message 2)
 174       * @param string $username
 175       * @param string $password
 176       * @param string $timestamp
 177       * @param string $client
 178       * @param bool   $v2        Use version2 of the protocol
 179       *
 180       * @return string
 181       */
 182      protected function sendMessage3($response, $username, $password, $timestamp, $client, Swift_Transport_SmtpAgent $agent, $v2 = true)
 183      {
 184          list($domain, $username) = $this->getDomainAndUsername($username);
 185          //$challenge, $context, $targetInfoH, $targetName, $domainName, $workstation, $DNSDomainName, $DNSServerName, $blob, $ter
 186          list($challenge, , , , , $workstation, , , $blob) = $this->parseMessage2($response);
 187  
 188          if (!$v2) {
 189              // LMv1
 190              $lmResponse = $this->createLMPassword($password, $challenge);
 191              // NTLMv1
 192              $ntlmResponse = $this->createNTLMPassword($password, $challenge);
 193          } else {
 194              // LMv2
 195              $lmResponse = $this->createLMv2Password($password, $username, $domain, $challenge, $client);
 196              // NTLMv2
 197              $ntlmResponse = $this->createNTLMv2Hash($password, $username, $domain, $challenge, $blob, $timestamp, $client);
 198          }
 199  
 200          $message = $this->createMessage3($domain, $username, $workstation, $lmResponse, $ntlmResponse);
 201  
 202          return $agent->executeCommand(sprintf("%s\r\n", base64_encode($message)), [235]);
 203      }
 204  
 205      /**
 206       * Create our message 1.
 207       *
 208       * @return string
 209       */
 210      protected function createMessage1()
 211      {
 212          return self::NTLMSIG
 213          .$this->createByte('01') // Message 1
 214  .$this->createByte('0702'); // Flags
 215      }
 216  
 217      /**
 218       * Create our message 3.
 219       *
 220       * @param string $domain
 221       * @param string $username
 222       * @param string $workstation
 223       * @param string $lmResponse
 224       * @param string $ntlmResponse
 225       *
 226       * @return string
 227       */
 228      protected function createMessage3($domain, $username, $workstation, $lmResponse, $ntlmResponse)
 229      {
 230          // Create security buffers
 231          $domainSec = $this->createSecurityBuffer($domain, 64);
 232          $domainInfo = $this->readSecurityBuffer(bin2hex($domainSec));
 233          $userSec = $this->createSecurityBuffer($username, ($domainInfo[0] + $domainInfo[1]) / 2);
 234          $userInfo = $this->readSecurityBuffer(bin2hex($userSec));
 235          $workSec = $this->createSecurityBuffer($workstation, ($userInfo[0] + $userInfo[1]) / 2);
 236          $workInfo = $this->readSecurityBuffer(bin2hex($workSec));
 237          $lmSec = $this->createSecurityBuffer($lmResponse, ($workInfo[0] + $workInfo[1]) / 2, true);
 238          $lmInfo = $this->readSecurityBuffer(bin2hex($lmSec));
 239          $ntlmSec = $this->createSecurityBuffer($ntlmResponse, ($lmInfo[0] + $lmInfo[1]) / 2, true);
 240  
 241          return self::NTLMSIG
 242          .$this->createByte('03') // TYPE 3 message
 243  .$lmSec // LM response header
 244  .$ntlmSec // NTLM response header
 245  .$domainSec // Domain header
 246  .$userSec // User header
 247  .$workSec // Workstation header
 248  .$this->createByte('000000009a', 8) // session key header (empty)
 249  .$this->createByte('01020000') // FLAGS
 250  .$this->convertTo16bit($domain) // domain name
 251  .$this->convertTo16bit($username) // username
 252  .$this->convertTo16bit($workstation) // workstation
 253  .$lmResponse
 254          .$ntlmResponse;
 255      }
 256  
 257      /**
 258       * @param string $timestamp  Epoch timestamp in microseconds
 259       * @param string $client     Random bytes
 260       * @param string $targetInfo
 261       *
 262       * @return string
 263       */
 264      protected function createBlob($timestamp, $client, $targetInfo)
 265      {
 266          return $this->createByte('0101')
 267          .$this->createByte('00')
 268          .$timestamp
 269          .$client
 270          .$this->createByte('00')
 271          .$targetInfo
 272          .$this->createByte('00');
 273      }
 274  
 275      /**
 276       * Get domain and username from our username.
 277       *
 278       * @example DOMAIN\username
 279       *
 280       * @param string $name
 281       *
 282       * @return array
 283       */
 284      protected function getDomainAndUsername($name)
 285      {
 286          if (false !== strpos($name, '\\')) {
 287              return explode('\\', $name);
 288          }
 289  
 290          if (false !== strpos($name, '@')) {
 291              list($user, $domain) = explode('@', $name);
 292  
 293              return [$domain, $user];
 294          }
 295  
 296          // no domain passed
 297          return ['', $name];
 298      }
 299  
 300      /**
 301       * Create LMv1 response.
 302       *
 303       * @param string $password
 304       * @param string $challenge
 305       *
 306       * @return string
 307       */
 308      protected function createLMPassword($password, $challenge)
 309      {
 310          // FIRST PART
 311          $password = $this->createByte(strtoupper($password), 14, false);
 312          list($key1, $key2) = str_split($password, 7);
 313  
 314          $desKey1 = $this->createDesKey($key1);
 315          $desKey2 = $this->createDesKey($key2);
 316  
 317          $constantDecrypt = $this->createByte($this->desEncrypt(self::DESCONST, $desKey1).$this->desEncrypt(self::DESCONST, $desKey2), 21, false);
 318  
 319          // SECOND PART
 320          list($key1, $key2, $key3) = str_split($constantDecrypt, 7);
 321  
 322          $desKey1 = $this->createDesKey($key1);
 323          $desKey2 = $this->createDesKey($key2);
 324          $desKey3 = $this->createDesKey($key3);
 325  
 326          return $this->desEncrypt($challenge, $desKey1).$this->desEncrypt($challenge, $desKey2).$this->desEncrypt($challenge, $desKey3);
 327      }
 328  
 329      /**
 330       * Create NTLMv1 response.
 331       *
 332       * @param string $password
 333       * @param string $challenge
 334       *
 335       * @return string
 336       */
 337      protected function createNTLMPassword($password, $challenge)
 338      {
 339          // FIRST PART
 340          $ntlmHash = $this->createByte($this->md4Encrypt($password), 21, false);
 341          list($key1, $key2, $key3) = str_split($ntlmHash, 7);
 342  
 343          $desKey1 = $this->createDesKey($key1);
 344          $desKey2 = $this->createDesKey($key2);
 345          $desKey3 = $this->createDesKey($key3);
 346  
 347          return $this->desEncrypt($challenge, $desKey1).$this->desEncrypt($challenge, $desKey2).$this->desEncrypt($challenge, $desKey3);
 348      }
 349  
 350      /**
 351       * Convert a normal timestamp to a tenth of a microtime epoch time.
 352       *
 353       * @param string $time
 354       *
 355       * @return string
 356       */
 357      protected function getCorrectTimestamp($time)
 358      {
 359          // Get our timestamp (tricky!)
 360          $time = number_format($time, 0, '.', ''); // save microtime to string
 361          $time = bcadd($time, '11644473600000', 0); // add epoch time
 362          $time = bcmul($time, 10000, 0); // tenths of a microsecond.
 363  
 364          $binary = $this->si2bin($time, 64); // create 64 bit binary string
 365          $timestamp = '';
 366          for ($i = 0; $i < 8; ++$i) {
 367              $timestamp .= chr(bindec(substr($binary, -(($i + 1) * 8), 8)));
 368          }
 369  
 370          return $timestamp;
 371      }
 372  
 373      /**
 374       * Create LMv2 response.
 375       *
 376       * @param string $password
 377       * @param string $username
 378       * @param string $domain
 379       * @param string $challenge NTLM Challenge
 380       * @param string $client    Random string
 381       *
 382       * @return string
 383       */
 384      protected function createLMv2Password($password, $username, $domain, $challenge, $client)
 385      {
 386          $lmPass = '00'; // by default 00
 387          // if $password > 15 than we can't use this method
 388          if (strlen($password) <= 15) {
 389              $ntlmHash = $this->md4Encrypt($password);
 390              $ntml2Hash = $this->md5Encrypt($ntlmHash, $this->convertTo16bit(strtoupper($username).$domain));
 391  
 392              $lmPass = bin2hex($this->md5Encrypt($ntml2Hash, $challenge.$client).$client);
 393          }
 394  
 395          return $this->createByte($lmPass, 24);
 396      }
 397  
 398      /**
 399       * Create NTLMv2 response.
 400       *
 401       * @param string $password
 402       * @param string $username
 403       * @param string $domain
 404       * @param string $challenge  Hex values
 405       * @param string $targetInfo Hex values
 406       * @param string $timestamp
 407       * @param string $client     Random bytes
 408       *
 409       * @return string
 410       *
 411       * @see http://davenport.sourceforge.net/ntlm.html#theNtlmResponse
 412       */
 413      protected function createNTLMv2Hash($password, $username, $domain, $challenge, $targetInfo, $timestamp, $client)
 414      {
 415          $ntlmHash = $this->md4Encrypt($password);
 416          $ntml2Hash = $this->md5Encrypt($ntlmHash, $this->convertTo16bit(strtoupper($username).$domain));
 417  
 418          // create blob
 419          $blob = $this->createBlob($timestamp, $client, $targetInfo);
 420  
 421          $ntlmv2Response = $this->md5Encrypt($ntml2Hash, $challenge.$blob);
 422  
 423          return $ntlmv2Response.$blob;
 424      }
 425  
 426      protected function createDesKey($key)
 427      {
 428          $material = [bin2hex($key[0])];
 429          $len = strlen($key);
 430          for ($i = 1; $i < $len; ++$i) {
 431              list($high, $low) = str_split(bin2hex($key[$i]));
 432              $v = $this->castToByte(ord($key[$i - 1]) << (7 + 1 - $i) | $this->uRShift(hexdec(dechex(hexdec($high) & 0xf).dechex(hexdec($low) & 0xf)), $i));
 433              $material[] = str_pad(substr(dechex($v), -2), 2, '0', STR_PAD_LEFT); // cast to byte
 434          }
 435          $material[] = str_pad(substr(dechex($this->castToByte(ord($key[6]) << 1)), -2), 2, '0');
 436  
 437          // odd parity
 438          foreach ($material as $k => $v) {
 439              $b = $this->castToByte(hexdec($v));
 440              $needsParity = 0 == (($this->uRShift($b, 7) ^ $this->uRShift($b, 6) ^ $this->uRShift($b, 5)
 441                          ^ $this->uRShift($b, 4) ^ $this->uRShift($b, 3) ^ $this->uRShift($b, 2)
 442                          ^ $this->uRShift($b, 1)) & 0x01);
 443  
 444              list($high, $low) = str_split($v);
 445              if ($needsParity) {
 446                  $material[$k] = dechex(hexdec($high) | 0x0).dechex(hexdec($low) | 0x1);
 447              } else {
 448                  $material[$k] = dechex(hexdec($high) & 0xf).dechex(hexdec($low) & 0xe);
 449              }
 450          }
 451  
 452          return hex2bin(implode('', $material));
 453      }
 454  
 455      /** HELPER FUNCTIONS */
 456  
 457      /**
 458       * Create our security buffer depending on length and offset.
 459       *
 460       * @param string $value  Value we want to put in
 461       * @param int    $offset start of value
 462       * @param bool   $is16   Do we 16bit string or not?
 463       *
 464       * @return string
 465       */
 466      protected function createSecurityBuffer($value, $offset, $is16 = false)
 467      {
 468          $length = strlen(bin2hex($value));
 469          $length = $is16 ? $length / 2 : $length;
 470          $length = $this->createByte(str_pad(dechex($length), 2, '0', STR_PAD_LEFT), 2);
 471  
 472          return $length.$length.$this->createByte(dechex($offset), 4);
 473      }
 474  
 475      /**
 476       * Read our security buffer to fetch length and offset of our value.
 477       *
 478       * @param string $value Securitybuffer in hex
 479       *
 480       * @return array array with length and offset
 481       */
 482      protected function readSecurityBuffer($value)
 483      {
 484          $length = floor(hexdec(substr($value, 0, 4)) / 256) * 2;
 485          $offset = floor(hexdec(substr($value, 8, 4)) / 256) * 2;
 486  
 487          return [$length, $offset];
 488      }
 489  
 490      /**
 491       * Cast to byte java equivalent to (byte).
 492       *
 493       * @param int $v
 494       *
 495       * @return int
 496       */
 497      protected function castToByte($v)
 498      {
 499          return (($v + 128) % 256) - 128;
 500      }
 501  
 502      /**
 503       * Java unsigned right bitwise
 504       * $a >>> $b.
 505       *
 506       * @param int $a
 507       * @param int $b
 508       *
 509       * @return int
 510       */
 511      protected function uRShift($a, $b)
 512      {
 513          if (0 == $b) {
 514              return $a;
 515          }
 516  
 517          return ($a >> $b) & ~(1 << (8 * PHP_INT_SIZE - 1) >> ($b - 1));
 518      }
 519  
 520      /**
 521       * Right padding with 0 to certain length.
 522       *
 523       * @param string $input
 524       * @param int    $bytes Length of bytes
 525       * @param bool   $isHex Did we provided hex value
 526       *
 527       * @return string
 528       */
 529      protected function createByte($input, $bytes = 4, $isHex = true)
 530      {
 531          if ($isHex) {
 532              $byte = hex2bin(str_pad($input, $bytes * 2, '00'));
 533          } else {
 534              $byte = str_pad($input, $bytes, "\x00");
 535          }
 536  
 537          return $byte;
 538      }
 539  
 540      /** ENCRYPTION ALGORITHMS */
 541  
 542      /**
 543       * DES Encryption.
 544       *
 545       * @param string $value An 8-byte string
 546       * @param string $key
 547       *
 548       * @return string
 549       */
 550      protected function desEncrypt($value, $key)
 551      {
 552          return substr(openssl_encrypt($value, 'DES-ECB', $key, \OPENSSL_RAW_DATA), 0, 8);
 553      }
 554  
 555      /**
 556       * MD5 Encryption.
 557       *
 558       * @param string $key Encryption key
 559       * @param string $msg Message to encrypt
 560       *
 561       * @return string
 562       */
 563      protected function md5Encrypt($key, $msg)
 564      {
 565          $blocksize = 64;
 566          if (strlen($key) > $blocksize) {
 567              $key = pack('H*', md5($key));
 568          }
 569  
 570          $key = str_pad($key, $blocksize, "\0");
 571          $ipadk = $key ^ str_repeat("\x36", $blocksize);
 572          $opadk = $key ^ str_repeat("\x5c", $blocksize);
 573  
 574          return pack('H*', md5($opadk.pack('H*', md5($ipadk.$msg))));
 575      }
 576  
 577      /**
 578       * MD4 Encryption.
 579       *
 580       * @param string $input
 581       *
 582       * @return string
 583       *
 584       * @see https://secure.php.net/manual/en/ref.hash.php
 585       */
 586      protected function md4Encrypt($input)
 587      {
 588          $input = $this->convertTo16bit($input);
 589  
 590          return function_exists('hash') ? hex2bin(hash('md4', $input)) : mhash(MHASH_MD4, $input);
 591      }
 592  
 593      /**
 594       * Convert UTF-8 to UTF-16.
 595       *
 596       * @param string $input
 597       *
 598       * @return string
 599       */
 600      protected function convertTo16bit($input)
 601      {
 602          return iconv('UTF-8', 'UTF-16LE', $input);
 603      }
 604  
 605      /**
 606       * @param string $message
 607       */
 608      protected function debug($message)
 609      {
 610          $message = bin2hex($message);
 611          $messageId = substr($message, 16, 8);
 612          echo substr($message, 0, 16)." NTLMSSP Signature<br />\n";
 613          echo $messageId." Type Indicator<br />\n";
 614  
 615          if ('02000000' == $messageId) {
 616              $map = [
 617                  'Challenge',
 618                  'Context',
 619                  'Target Information Security Buffer',
 620                  'Target Name Data',
 621                  'NetBIOS Domain Name',
 622                  'NetBIOS Server Name',
 623                  'DNS Domain Name',
 624                  'DNS Server Name',
 625                  'BLOB',
 626                  'Target Information Terminator',
 627              ];
 628  
 629              $data = $this->parseMessage2(hex2bin($message));
 630  
 631              foreach ($map as $key => $value) {
 632                  echo bin2hex($data[$key]).' - '.$data[$key].' ||| '.$value."<br />\n";
 633              }
 634          } elseif ('03000000' == $messageId) {
 635              $i = 0;
 636              $data[$i++] = substr($message, 24, 16);
 637              list($lmLength, $lmOffset) = $this->readSecurityBuffer($data[$i - 1]);
 638  
 639              $data[$i++] = substr($message, 40, 16);
 640              list($ntmlLength, $ntmlOffset) = $this->readSecurityBuffer($data[$i - 1]);
 641  
 642              $data[$i++] = substr($message, 56, 16);
 643              list($targetLength, $targetOffset) = $this->readSecurityBuffer($data[$i - 1]);
 644  
 645              $data[$i++] = substr($message, 72, 16);
 646              list($userLength, $userOffset) = $this->readSecurityBuffer($data[$i - 1]);
 647  
 648              $data[$i++] = substr($message, 88, 16);
 649              list($workLength, $workOffset) = $this->readSecurityBuffer($data[$i - 1]);
 650  
 651              $data[$i++] = substr($message, 104, 16);
 652              $data[$i++] = substr($message, 120, 8);
 653              $data[$i++] = substr($message, $targetOffset, $targetLength);
 654              $data[$i++] = substr($message, $userOffset, $userLength);
 655              $data[$i++] = substr($message, $workOffset, $workLength);
 656              $data[$i++] = substr($message, $lmOffset, $lmLength);
 657              $data[$i] = substr($message, $ntmlOffset, $ntmlLength);
 658  
 659              $map = [
 660                  'LM Response Security Buffer',
 661                  'NTLM Response Security Buffer',
 662                  'Target Name Security Buffer',
 663                  'User Name Security Buffer',
 664                  'Workstation Name Security Buffer',
 665                  'Session Key Security Buffer',
 666                  'Flags',
 667                  'Target Name Data',
 668                  'User Name Data',
 669                  'Workstation Name Data',
 670                  'LM Response Data',
 671                  'NTLM Response Data',
 672              ];
 673  
 674              foreach ($map as $key => $value) {
 675                  echo $data[$key].' - '.hex2bin($data[$key]).' ||| '.$value."<br />\n";
 676              }
 677          }
 678  
 679          echo '<br /><br />';
 680      }
 681  }


Generated: Mon Mar 11 18:33:33 2019 Cross-referenced by PHPXref 0.7.1