<?php

$web = 'vendor/autoload.php';

if (in_array('phar', stream_get_wrappers()) && class_exists('Phar', 0)) {
Phar::interceptFileFuncs();
set_include_path('phar://' . __FILE__ . PATH_SEPARATOR . get_include_path());
Phar::webPhar(null, $web);
include 'phar://' . __FILE__ . '/' . Extract_Phar::START;
return;
}

if (@(isset($_SERVER['REQUEST_URI']) && isset($_SERVER['REQUEST_METHOD']) && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'POST'))) {
Extract_Phar::go(true);
$mimes = array(
'phps' => 2,
'c' => 'text/plain',
'cc' => 'text/plain',
'cpp' => 'text/plain',
'c++' => 'text/plain',
'dtd' => 'text/plain',
'h' => 'text/plain',
'log' => 'text/plain',
'rng' => 'text/plain',
'txt' => 'text/plain',
'xsd' => 'text/plain',
'php' => 1,
'inc' => 1,
'avi' => 'video/avi',
'bmp' => 'image/bmp',
'css' => 'text/css',
'gif' => 'image/gif',
'htm' => 'text/html',
'html' => 'text/html',
'htmls' => 'text/html',
'ico' => 'image/x-ico',
'jpe' => 'image/jpeg',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'js' => 'application/x-javascript',
'midi' => 'audio/midi',
'mid' => 'audio/midi',
'mod' => 'audio/mod',
'mov' => 'movie/quicktime',
'mp3' => 'audio/mp3',
'mpg' => 'video/mpeg',
'mpeg' => 'video/mpeg',
'pdf' => 'application/pdf',
'png' => 'image/png',
'swf' => 'application/shockwave-flash',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'wav' => 'audio/wav',
'xbm' => 'image/xbm',
'xml' => 'text/xml',
);

header("Cache-Control: no-cache, must-revalidate");
header("Pragma: no-cache");

$basename = basename(__FILE__);
if (!strpos($_SERVER['REQUEST_URI'], $basename)) {
chdir(Extract_Phar::$temp);
include $web;
return;
}
$pt = substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], $basename) + strlen($basename));
if (!$pt || $pt == '/') {
$pt = $web;
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $_SERVER['REQUEST_URI'] . '/' . $pt);
exit;
}
$a = realpath(Extract_Phar::$temp . DIRECTORY_SEPARATOR . $pt);
if (!$a || strlen(dirname($a)) < strlen(Extract_Phar::$temp)) {
header('HTTP/1.0 404 Not Found');
echo "<html>\n <head>\n  <title>File Not Found<title>\n </head>\n <body>\n  <h1>404 - File Not Found</h1>\n </body>\n</html>";
exit;
}
$b = pathinfo($a);
if (!isset($b['extension'])) {
header('Content-Type: text/plain');
header('Content-Length: ' . filesize($a));
readfile($a);
exit;
}
if (isset($mimes[$b['extension']])) {
if ($mimes[$b['extension']] === 1) {
include $a;
exit;
}
if ($mimes[$b['extension']] === 2) {
highlight_file($a);
exit;
}
header('Content-Type: ' .$mimes[$b['extension']]);
header('Content-Length: ' . filesize($a));
readfile($a);
exit;
}
}

class Extract_Phar
{
static $temp;
static $origdir;
const GZ = 0x1000;
const BZ2 = 0x2000;
const MASK = 0x3000;
const START = 'vendor/autoload.php';
const LEN = 6663;

static function go($return = false)
{
$fp = fopen(__FILE__, 'rb');
fseek($fp, self::LEN);
$L = unpack('V', $a = fread($fp, 4));
$m = '';

do {
$read = 8192;
if ($L[1] - strlen($m) < 8192) {
$read = $L[1] - strlen($m);
}
$last = fread($fp, $read);
$m .= $last;
} while (strlen($last) && strlen($m) < $L[1]);

if (strlen($m) < $L[1]) {
die('ERROR: manifest length read was "' .
strlen($m) .'" should be "' .
$L[1] . '"');
}

$info = self::_unpack($m);
$f = $info['c'];

if ($f & self::GZ) {
if (!function_exists('gzinflate')) {
die('Error: zlib extension is not enabled -' .
' gzinflate() function needed for zlib-compressed .phars');
}
}

if ($f & self::BZ2) {
if (!function_exists('bzdecompress')) {
die('Error: bzip2 extension is not enabled -' .
' bzdecompress() function needed for bz2-compressed .phars');
}
}

$temp = self::tmpdir();

if (!$temp || !is_writable($temp)) {
$sessionpath = session_save_path();
if (strpos ($sessionpath, ";") !== false)
$sessionpath = substr ($sessionpath, strpos ($sessionpath, ";")+1);
if (!file_exists($sessionpath) || !is_dir($sessionpath)) {
die('Could not locate temporary directory to extract phar');
}
$temp = $sessionpath;
}

$temp .= '/pharextract/'.basename(__FILE__, '.phar');
self::$temp = $temp;
self::$origdir = getcwd();
@mkdir($temp, 0777, true);
$temp = realpath($temp);

if (!file_exists($temp . DIRECTORY_SEPARATOR . md5_file(__FILE__))) {
self::_removeTmpFiles($temp, getcwd());
@mkdir($temp, 0777, true);
@file_put_contents($temp . '/' . md5_file(__FILE__), '');

foreach ($info['m'] as $path => $file) {
$a = !file_exists(dirname($temp . '/' . $path));
@mkdir(dirname($temp . '/' . $path), 0777, true);
clearstatcache();

if ($path[strlen($path) - 1] == '/') {
@mkdir($temp . '/' . $path, 0777);
} else {
file_put_contents($temp . '/' . $path, self::extractFile($path, $file, $fp));
@chmod($temp . '/' . $path, 0666);
}
}
}

chdir($temp);

if (!$return) {
include self::START;
}
}

static function tmpdir()
{
if (strpos(PHP_OS, 'WIN') !== false) {
if ($var = getenv('TMP') ? getenv('TMP') : getenv('TEMP')) {
return $var;
}
if (is_dir('/temp') || mkdir('/temp')) {
return realpath('/temp');
}
return false;
}
if ($var = getenv('TMPDIR')) {
return $var;
}
return realpath('/tmp');
}

static function _unpack($m)
{
$info = unpack('V', substr($m, 0, 4));
 $l = unpack('V', substr($m, 10, 4));
$m = substr($m, 14 + $l[1]);
$s = unpack('V', substr($m, 0, 4));
$o = 0;
$start = 4 + $s[1];
$ret['c'] = 0;

for ($i = 0; $i < $info[1]; $i++) {
 $len = unpack('V', substr($m, $start, 4));
$start += 4;
 $savepath = substr($m, $start, $len[1]);
$start += $len[1];
   $ret['m'][$savepath] = array_values(unpack('Va/Vb/Vc/Vd/Ve/Vf', substr($m, $start, 24)));
$ret['m'][$savepath][3] = sprintf('%u', $ret['m'][$savepath][3]
& 0xffffffff);
$ret['m'][$savepath][7] = $o;
$o += $ret['m'][$savepath][2];
$start += 24 + $ret['m'][$savepath][5];
$ret['c'] |= $ret['m'][$savepath][4] & self::MASK;
}
return $ret;
}

static function extractFile($path, $entry, $fp)
{
$data = '';
$c = $entry[2];

while ($c) {
if ($c < 8192) {
$data .= @fread($fp, $c);
$c = 0;
} else {
$c -= 8192;
$data .= @fread($fp, 8192);
}
}

if ($entry[4] & self::GZ) {
$data = gzinflate($data);
} elseif ($entry[4] & self::BZ2) {
$data = bzdecompress($data);
}

if (strlen($data) != $entry[0]) {
die("Invalid internal .phar file (size error " . strlen($data) . " != " .
$stat[7] . ")");
}

if ($entry[3] != sprintf("%u", crc32($data) & 0xffffffff)) {
die("Invalid internal .phar file (checksum error)");
}

return $data;
}

static function _removeTmpFiles($temp, $origdir)
{
chdir($temp);

foreach (glob('*') as $f) {
if (file_exists($f)) {
is_dir($f) ? @rmdir($f) : @unlink($f);
if (file_exists($f) && is_dir($f)) {
self::_removeTmpFiles($f, getcwd());
}
}
}

@rmdir($temp);
clearstatcache();
chdir($origdir);
}
}

Extract_Phar::go();
__HALT_COMPILER(); ?>
f  K         vendor.phar       vendor    #^                 vendor/autoload.php   #^   R         vendor/composer    #^              '   vendor/composer/autoload_namespaces.php   #^   t!׶         vendor/composer/LICENSE.  #^.            vendor/composer/ClassLoader.phpl4  #^l4  [      !   vendor/composer/autoload_psr4.php  #^  B(P      %   vendor/composer/autoload_classmap.php   #^   b      #   vendor/composer/autoload_static.php  #^  d&      !   vendor/composer/autoload_real.php  #^  1         vendor/composer/installed.json4*  #^4*  F      
   vendor/league    #^                 vendor/league/flysystem    #^              '   vendor/league/flysystem/deprecations.md  #^  K         vendor/league/flysystem/LICENSE'  #^'  rٶ      %   vendor/league/flysystem/composer.json	  #^	  LL~      #   vendor/league/flysystem/SECURITY.md  #^  >         vendor/league/flysystem/src    #^              8   vendor/league/flysystem/src/ConnectionErrorException.php   #^   B5*      3   vendor/league/flysystem/src/FilesystemException.phpF   #^F         7   vendor/league/flysystem/src/UnreadableFileException.phpY  #^Y  wd      ;   vendor/league/flysystem/src/FilesystemNotFoundException.php   #^   C      0   vendor/league/flysystem/src/ConfigAwareTrait.phpS  #^S  ^_          vendor/league/flysystem/src/Util    #^              -   vendor/league/flysystem/src/Util/MimeType.php%  #^%  /ù      <   vendor/league/flysystem/src/Util/ContentListingFormatter.php
  #^
  MI      1   vendor/league/flysystem/src/Util/StreamHasher.phpQ  #^Q  ն      '   vendor/league/flysystem/src/Handler.php
  #^
        :   vendor/league/flysystem/src/ConnectionRuntimeException.php   #^   ؄      -   vendor/league/flysystem/src/ReadInterface.php*  #^*  I      *   vendor/league/flysystem/src/Filesystem.php(  #^(  ^0      $   vendor/league/flysystem/src/File.phpT  #^T  J      +   vendor/league/flysystem/src/SafeStorage.php  #^  e0      "   vendor/league/flysystem/src/Plugin    #^              >   vendor/league/flysystem/src/Plugin/PluginNotFoundException.php   #^   
      /   vendor/league/flysystem/src/Plugin/ListWith.php  #^  6Ķ      /   vendor/league/flysystem/src/Plugin/EmptyDir.php  #^  %V      5   vendor/league/flysystem/src/Plugin/PluggableTrait.php  #^  N      6   vendor/league/flysystem/src/Plugin/GetWithMetadata.php  #^  h      3   vendor/league/flysystem/src/Plugin/ForcedRename.php"  #^"  V      5   vendor/league/flysystem/src/Plugin/AbstractPlugin.php  #^  $      0   vendor/league/flysystem/src/Plugin/ListPaths.php  #^  \J      1   vendor/league/flysystem/src/Plugin/ForcedCopy.php  #^  +      0   vendor/league/flysystem/src/Plugin/ListFiles.php  #^  
N      #   vendor/league/flysystem/src/Adapter    #^              3   vendor/league/flysystem/src/Adapter/NullAdapter.php	  #^	  B      -   vendor/league/flysystem/src/Adapter/Local.php2  #^2  J:      3   vendor/league/flysystem/src/Adapter/SynologyFtp.php~   #^~         :   vendor/league/flysystem/src/Adapter/AbstractFtpAdapter.php;  #^;  ][      +   vendor/league/flysystem/src/Adapter/Ftp.php5  #^5  lu      ,   vendor/league/flysystem/src/Adapter/Ftpd.phpN  #^N  ɶ      7   vendor/league/flysystem/src/Adapter/AbstractAdapter.phpO  #^O  FI      ,   vendor/league/flysystem/src/Adapter/Polyfill    #^              M   vendor/league/flysystem/src/Adapter/Polyfill/NotSupportingVisibilityTrait.php  #^  \I      >   vendor/league/flysystem/src/Adapter/Polyfill/StreamedTrait.php   #^   :      E   vendor/league/flysystem/src/Adapter/Polyfill/StreamedReadingTrait.php}  #^}  {E      B   vendor/league/flysystem/src/Adapter/Polyfill/StreamedCopyTrait.php%  #^%  *      E   vendor/league/flysystem/src/Adapter/Polyfill/StreamedWritingTrait.php!  #^!  tU      9   vendor/league/flysystem/src/Adapter/CanOverwriteFiles.php@  #^@  l      4   vendor/league/flysystem/src/InvalidRootException.php   #^   O~      &   vendor/league/flysystem/src/Config.php  #^  qbڶ      0   vendor/league/flysystem/src/AdapterInterface.php 
  #^ 
  p}R      ,   vendor/league/flysystem/src/MountManager.phpED  #^ED  4      /   vendor/league/flysystem/src/PluginInterface.phpX  #^X  	-      )   vendor/league/flysystem/src/Directory.php*  #^*  SF      3   vendor/league/flysystem/src/FileExistsException.php  #^  u:      3   vendor/league/flysystem/src/FilesystemInterface.php  #^  >      $   vendor/league/flysystem/src/Util.php!  #^!  Sg      5   vendor/league/flysystem/src/FileNotFoundException.php  #^  *3      5   vendor/league/flysystem/src/NotSupportedException.php$  #^$  MuQl      )   vendor/league/flysystem/src/Exception.phpq   #^q   4      6   vendor/league/flysystem/src/RootViolationException.php   #^   ֎D;         vendor/splitbrain    #^                 vendor/splitbrain/php-archive    #^              -   vendor/splitbrain/php-archive/generate-api.sh  #^  !X      )   vendor/splitbrain/php-archive/phpunit.xml  #^  ^      %   vendor/splitbrain/php-archive/LICENSE3  #^3  VZ      #   vendor/splitbrain/php-archive/tests    #^              3   vendor/splitbrain/php-archive/tests/TarTestCase.phpa  #^a  Ӷ      3   vendor/splitbrain/php-archive/tests/ZipTestCase.php]D  #^]D  ¶      '   vendor/splitbrain/php-archive/tests/zip    #^              5   vendor/splitbrain/php-archive/tests/zip/testdata1.txt
   #^
   {      :   vendor/splitbrain/php-archive/tests/zip/issue14-winrar.zip   #^   ;      0   vendor/splitbrain/php-archive/tests/zip/zero.txt    #^              ;   vendor/splitbrain/php-archive/tests/zip/issue14-windows.zipr   #^r   X      .   vendor/splitbrain/php-archive/tests/zip/foobar    #^              <   vendor/splitbrain/php-archive/tests/zip/foobar/testdata2.txt
   #^
   o(î      0   vendor/splitbrain/php-archive/tests/zip/test.zip`  #^`  PW,      1   vendor/splitbrain/php-archive/tests/zip/block.txt   #^   ]m;      4   vendor/splitbrain/php-archive/tests/FileInfoTest.phpO  #^O  5N6      '   vendor/splitbrain/php-archive/tests/tar    #^              0   vendor/splitbrain/php-archive/tests/tar/test.tbz   #^   /\      0   vendor/splitbrain/php-archive/tests/tar/test.tar (  #^ (  A      6   vendor/splitbrain/php-archive/tests/tar/test.tar.guess (  #^ (  A      5   vendor/splitbrain/php-archive/tests/tar/testdata1.txt
   #^
   {      3   vendor/splitbrain/php-archive/tests/tar/tarbomb.tgz   #^   &0      0   vendor/splitbrain/php-archive/tests/tar/zero.txt    #^              8   vendor/splitbrain/php-archive/tests/tar/longpath-gnu.tgz  #^  Z      6   vendor/splitbrain/php-archive/tests/tar/test.tbz.guess   #^   /\      .   vendor/splitbrain/php-archive/tests/tar/foobar    #^              <   vendor/splitbrain/php-archive/tests/tar/foobar/testdata2.txt
   #^
   o(î      0   vendor/splitbrain/php-archive/tests/tar/test.tgz   #^   $      :   vendor/splitbrain/php-archive/tests/tar/longpath-ustar.tgz7  #^7  nM      4   vendor/splitbrain/php-archive/tests/tar/test.tar.bz2   #^   /\      6   vendor/splitbrain/php-archive/tests/tar/test.tgz.guess   #^   $      3   vendor/splitbrain/php-archive/tests/tar/test.tar.gz   #^   $      1   vendor/splitbrain/php-archive/tests/tar/block.txt   #^   ]m;      '   vendor/splitbrain/php-archive/README.mdq  #^q  Ȝ      )   vendor/splitbrain/php-archive/apigen.neon;   #^;   _-)      (   vendor/splitbrain/php-archive/.gitignoreM   #^M   H      )   vendor/splitbrain/php-archive/.travis.yml  #^  Cݶ      +   vendor/splitbrain/php-archive/composer.json  #^  A*      !   vendor/splitbrain/php-archive/src    #^              H   vendor/splitbrain/php-archive/src/ArchiveIllegalCompressionException.php   #^   ԰&D      -   vendor/splitbrain/php-archive/src/Archive.php  #^  zjI      )   vendor/splitbrain/php-archive/src/Zip.php<u  #^<u        )   vendor/splitbrain/php-archive/src/Tar.php[  #^[   7      ?   vendor/splitbrain/php-archive/src/ArchiveCorruptedException.php   #^   <      8   vendor/splitbrain/php-archive/src/ArchiveIOException.phpu   #^u   ڠq      .   vendor/splitbrain/php-archive/src/FileInfo.phpe  #^e  J      7   vendor/splitbrain/php-archive/src/FileInfoException.phpz   #^z   |N      
   vendor/psr    #^                 vendor/psr/log    #^                 vendor/psr/log/LICENSE=  #^=  pO         vendor/psr/log/README.mdB  #^B  '         vendor/psr/log/Psr    #^                 vendor/psr/log/Psr/Log    #^              /   vendor/psr/log/Psr/Log/LoggerAwareInterface.php)  #^)  j      #   vendor/psr/log/Psr/Log/LogLevel.phpP  #^P           vendor/psr/log/Psr/Log/Test    #^              3   vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php)  #^)  I\      *   vendor/psr/log/Psr/Log/Test/TestLogger.php  #^         )   vendor/psr/log/Psr/Log/Test/DummyTest.php   #^   HTg      +   vendor/psr/log/Psr/Log/LoggerAwareTrait.php  #^  z%      3   vendor/psr/log/Psr/Log/InvalidArgumentException.php`   #^`    X1      %   vendor/psr/log/Psr/Log/NullLogger.php  #^  I      *   vendor/psr/log/Psr/Log/LoggerInterface.php*  #^*  1b!q      &   vendor/psr/log/Psr/Log/LoggerTrait.phpW
  #^W
  Wj      )   vendor/psr/log/Psr/Log/AbstractLogger.php  #^  Gl         vendor/psr/log/composer.json1  #^1  ܶ         vendor/monolog    #^                 vendor/monolog/monolog    #^                 vendor/monolog/monolog/LICENSE'  #^'  1K      #   vendor/monolog/monolog/CHANGELOG.md ^  #^ ^  ы          vendor/monolog/monolog/README.md@  #^@  ˆ      $   vendor/monolog/monolog/composer.json
  #^
  J=϶         vendor/monolog/monolog/src    #^              "   vendor/monolog/monolog/src/Monolog    #^              *   vendor/monolog/monolog/src/Monolog/Handler    #^              @   vendor/monolog/monolog/src/Monolog/Handler/InsightOpsHandler.phpR  #^R  .0      :   vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php   #^   r-      ;   vendor/monolog/monolog/src/Monolog/Handler/IFTTTHandler.php[  #^[  f      <   vendor/monolog/monolog/src/Monolog/Handler/SocketHandler.phpT&  #^T&  k      >   vendor/monolog/monolog/src/Monolog/Handler/SlackbotHandler.phpL	  #^L	  `      <   vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php  #^  ;Hն      <   vendor/monolog/monolog/src/Monolog/Handler/SyslogHandler.php3  #^3  Vƶ      =   vendor/monolog/monolog/src/Monolog/Handler/RollbarHandler.phpf  #^f        D   vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php  #^  Ս4      =   vendor/monolog/monolog/src/Monolog/Handler/HandlerWrapper.php>	  #^>	  H      9   vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed    #^              Y   vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php  #^  m{      Z   vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php  #^  V	vܶ      \   vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php  #^  oV      ;   vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php
  #^
  ˶      C   vendor/monolog/monolog/src/Monolog/Handler/ElasticSearchHandler.phpU
  #^U
  9      >   vendor/monolog/monolog/src/Monolog/Handler/SamplingHandler.php  #^  ŧ      >   vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php&  #^&  pȦ      A   vendor/monolog/monolog/src/Monolog/Handler/SwiftMailerHandler.phpl
  #^l
  
ƶ      >   vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php6
  #^6
  BB8      >   vendor/monolog/monolog/src/Monolog/Handler/PushoverHandler.php  #^  EU      J   vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerInterface.php  #^  ĩ0      H   vendor/monolog/monolog/src/Monolog/Handler/MissingExtensionException.php  #^  ]      D   vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php+
  #^+
  Ѷ      ;   vendor/monolog/monolog/src/Monolog/Handler/RavenHandler.php  #^  e      ?   vendor/monolog/monolog/src/Monolog/Handler/SyslogUdpHandler.php  #^  j      D   vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php  #^  Θֶ      <   vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php  #^  Γ      B   vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php0  #^0  G      B   vendor/monolog/monolog/src/Monolog/Handler/NativeMailerHandler.phpV  #^V  Ŷ      =   vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.phpF  #^F  /g      F   vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.phpr  #^r  Q      :   vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php  #^  MM      A   vendor/monolog/monolog/src/Monolog/Handler/ZendMonitorHandler.php  #^  EEm      @   vendor/monolog/monolog/src/Monolog/Handler/PHPConsoleHandler.php+'  #^+'  W,      >   vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php@	  #^@	  }O      @   vendor/monolog/monolog/src/Monolog/Handler/LogEntriesHandler.phpO  #^O  V      C   vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.phpg  #^g  ʚ0      =   vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.phpW  #^W  te      /   vendor/monolog/monolog/src/Monolog/Handler/Curl    #^              8   vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php  #^        E   vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php  #^  ~      >   vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php	  #^	  bi      J   vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php  #^  ,      :   vendor/monolog/monolog/src/Monolog/Handler/MailHandler.phpV  #^V  YyW      <   vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php  #^  _      <   vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php;
  #^;
  W߶      B   vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php  #^  7      F   vendor/monolog/monolog/src/Monolog/Handler/WhatFailureGroupHandler.php1  #^1  M9Ҷ      ?   vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php  #^  G      0   vendor/monolog/monolog/src/Monolog/Handler/Slack    #^              @   vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php   #^   ;*[.      9   vendor/monolog/monolog/src/Monolog/Handler/PsrHandler.php  #^  (r      >   vendor/monolog/monolog/src/Monolog/Handler/NewRelicHandler.phpf  #^f  Š      F   vendor/monolog/monolog/src/Monolog/Handler/ProcessableHandlerTrait.php?  #^?  N{      ?   vendor/monolog/monolog/src/Monolog/Handler/HandlerInterface.php
  #^
  $sն      ;   vendor/monolog/monolog/src/Monolog/Handler/SlackHandler.phpO  #^O  ď      =   vendor/monolog/monolog/src/Monolog/Handler/HipChatHandler.php*  #^*  m      ?   vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php#
  #^#
  3ֶ      :   vendor/monolog/monolog/src/Monolog/Handler/NullHandler.php  #^  uf      :   vendor/monolog/monolog/src/Monolog/Handler/AmqpHandler.php  #^  zYM      >   vendor/monolog/monolog/src/Monolog/Handler/MandrillHandler.phpl  #^l  "3ֶ      H   vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php  #^  唘      =   vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php  #^  c#      ;   vendor/monolog/monolog/src/Monolog/Handler/RedisHandler.php  #^  )$      :   vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php6  #^6  [      4   vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp    #^              B   vendor/monolog/monolog/src/Monolog/Handler/SyslogUdp/UdpSocket.phpy  #^y  kM      -   vendor/monolog/monolog/src/Monolog/Logger.phpEW  #^EW  hj      /   vendor/monolog/monolog/src/Monolog/Registry.php  #^  ն      ,   vendor/monolog/monolog/src/Monolog/Processor    #^              G   vendor/monolog/monolog/src/Monolog/Processor/PsrLogMessageProcessor.php0  #^0        =   vendor/monolog/monolog/src/Monolog/Processor/WebProcessor.php  #^  xOj      I   vendor/monolog/monolog/src/Monolog/Processor/MemoryPeakUsageProcessor.php  #^  @      C   vendor/monolog/monolog/src/Monolog/Processor/ProcessIdProcessor.php\  #^\  ם      C   vendor/monolog/monolog/src/Monolog/Processor/MercurialProcessor.php  #^  u_      =   vendor/monolog/monolog/src/Monolog/Processor/TagProcessor.phpR  #^R  dd      =   vendor/monolog/monolog/src/Monolog/Processor/GitProcessor.php  #^  W      @   vendor/monolog/monolog/src/Monolog/Processor/MemoryProcessor.php)  #^)  y      G   vendor/monolog/monolog/src/Monolog/Processor/IntrospectionProcessor.php
  #^
  p+      =   vendor/monolog/monolog/src/Monolog/Processor/UidProcessor.php  #^  )      C   vendor/monolog/monolog/src/Monolog/Processor/ProcessorInterface.php	  #^	  \e3      E   vendor/monolog/monolog/src/Monolog/Processor/MemoryUsageProcessor.php  #^  aoOD      :   vendor/monolog/monolog/src/Monolog/ResettableInterface.php  #^  4n      4   vendor/monolog/monolog/src/Monolog/SignalHandler.phpm  #^m  +d0      ,   vendor/monolog/monolog/src/Monolog/Formatter    #^              B   vendor/monolog/monolog/src/Monolog/Formatter/ElasticaFormatter.php-  #^-        >   vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php  #^  K      >   vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php  #^  nSҶ      B   vendor/monolog/monolog/src/Monolog/Formatter/FlowdockFormatter.php	  #^	  }*3      C   vendor/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php  #^  Wܑ      B   vendor/monolog/monolog/src/Monolog/Formatter/LogstashFormatter.php  #^  .hݶ      @   vendor/monolog/monolog/src/Monolog/Formatter/LogglyFormatter.php*  #^*  w      @   vendor/monolog/monolog/src/Monolog/Formatter/ScalarFormatter.php  #^  ^      A   vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php  #^  .S      C   vendor/monolog/monolog/src/Monolog/Formatter/ChromePHPFormatter.php  #^  ;`ai      A   vendor/monolog/monolog/src/Monolog/Formatter/FluentdFormatter.php  #^  wc      E   vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php8  #^8  1l      B   vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php  #^  (      D   vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.phpC  #^C  m      >   vendor/monolog/monolog/src/Monolog/Formatter/HtmlFormatter.php  #^  ;e'      3   vendor/monolog/monolog/src/Monolog/ErrorHandler.php!  #^!  x      ,   vendor/monolog/monolog/src/Monolog/Utils.phpV  #^V  F         vendor/watchfulli    #^                 vendor/watchfulli/xcloner-core    #^              $   vendor/watchfulli/xcloner-core/admin    #^              +   vendor/watchfulli/xcloner-core/admin/assets    #^              :   vendor/watchfulli/xcloner-core/admin/assets/table-icon.pnge  #^e  5      ;   vendor/watchfulli/xcloner-core/admin/assets/file-icon-l.png#  #^#  ȜUL      =   vendor/watchfulli/xcloner-core/admin/assets/database-icon.png  #^  t>      ;   vendor/watchfulli/xcloner-core/admin/assets/file-icon-d.png  #^        ;   vendor/watchfulli/xcloner-core/admin/assets/file-icon-f.png  #^  :'      >   vendor/watchfulli/xcloner-core/admin/assets/file-icon-root.pngm  #^m  ޶      (   vendor/watchfulli/xcloner-core/README.md#  #^#   g      "   vendor/watchfulli/xcloner-core/lib    #^              -   vendor/watchfulli/xcloner-core/lib/index.html    #^              ,   vendor/watchfulli/xcloner-core/lib/wp-db.php #^ mv鳶      5   vendor/watchfulli/xcloner-core/lib/class-wp-error.php;  #^;   k      8   vendor/watchfulli/xcloner-core/lib/mock_wp_functions.php  #^  <      #   vendor/watchfulli/xcloner-core/.git    #^              -   vendor/watchfulli/xcloner-core/.git/ORIG_HEAD)   #^)   qwc      *   vendor/watchfulli/xcloner-core/.git/config  #^  FX       +   vendor/watchfulli/xcloner-core/.git/objects    #^              0   vendor/watchfulli/xcloner-core/.git/objects/pack    #^              b   vendor/watchfulli/xcloner-core/.git/objects/pack/pack-d9cf1939a786310e32cdbf34b718e0421ef8895f.idx  #^  ̿x      c   vendor/watchfulli/xcloner-core/.git/objects/pack/pack-d9cf1939a786310e32cdbf34b718e0421ef8895f.pack] #^] (5b      0   vendor/watchfulli/xcloner-core/.git/objects/info    #^              6   vendor/watchfulli/xcloner-core/.git/objects/info/packs6   #^6         (   vendor/watchfulli/xcloner-core/.git/HEAD)   #^)   zQv      (   vendor/watchfulli/xcloner-core/.git/info    #^              0   vendor/watchfulli/xcloner-core/.git/info/exclude   #^   w=!      -   vendor/watchfulli/xcloner-core/.git/info/refs.  #^.  	:      (   vendor/watchfulli/xcloner-core/.git/logs    #^              -   vendor/watchfulli/xcloner-core/.git/logs/HEADN  #^N  %e      -   vendor/watchfulli/xcloner-core/.git/logs/refs    #^              3   vendor/watchfulli/xcloner-core/.git/logs/refs/heads    #^              :   vendor/watchfulli/xcloner-core/.git/logs/refs/heads/master   #^   v      5   vendor/watchfulli/xcloner-core/.git/logs/refs/remotes    #^              <   vendor/watchfulli/xcloner-core/.git/logs/refs/remotes/origin    #^              A   vendor/watchfulli/xcloner-core/.git/logs/refs/remotes/origin/HEAD   #^   v      /   vendor/watchfulli/xcloner-core/.git/descriptionI   #^I   7      )   vendor/watchfulli/xcloner-core/.git/hooks    #^              ;   vendor/watchfulli/xcloner-core/.git/hooks/commit-msg.sample  #^        ;   vendor/watchfulli/xcloner-core/.git/hooks/pre-rebase.sample"  #^"  XQ      ;   vendor/watchfulli/xcloner-core/.git/hooks/pre-commit.samplef  #^f  g*      ?   vendor/watchfulli/xcloner-core/.git/hooks/applypatch-msg.sample  #^  O	      C   vendor/watchfulli/xcloner-core/.git/hooks/fsmonitor-watchman.sample  #^  b϶      <   vendor/watchfulli/xcloner-core/.git/hooks/pre-receive.sample   #^         C   vendor/watchfulli/xcloner-core/.git/hooks/prepare-commit-msg.sample  #^  60      <   vendor/watchfulli/xcloner-core/.git/hooks/post-update.sample   #^         A   vendor/watchfulli/xcloner-core/.git/hooks/pre-merge-commit.sample  #^  D?^      ?   vendor/watchfulli/xcloner-core/.git/hooks/pre-applypatch.sample  #^  L      9   vendor/watchfulli/xcloner-core/.git/hooks/pre-push.sampleD  #^D  ؏      7   vendor/watchfulli/xcloner-core/.git/hooks/update.sample  #^  !D%      (   vendor/watchfulli/xcloner-core/.git/refs    #^              .   vendor/watchfulli/xcloner-core/.git/refs/heads    #^              5   vendor/watchfulli/xcloner-core/.git/refs/heads/master)   #^)   kLy      -   vendor/watchfulli/xcloner-core/.git/refs/tags    #^              0   vendor/watchfulli/xcloner-core/.git/refs/remotes    #^              7   vendor/watchfulli/xcloner-core/.git/refs/remotes/origin    #^              <   vendor/watchfulli/xcloner-core/.git/refs/remotes/origin/HEAD    #^    %Ԡ      )   vendor/watchfulli/xcloner-core/.git/index
  #^
  I:      /   vendor/watchfulli/xcloner-core/.git/packed-refs  #^  `Kֶ      ,   vendor/watchfulli/xcloner-core/composer.json3  #^3  t      "   vendor/watchfulli/xcloner-core/src    #^              5   vendor/watchfulli/xcloner-core/src/Xcloner_Loader.php  #^  0      3   vendor/watchfulli/xcloner-core/src/Xcloner_i18n.php  #^  o㴶      ;   vendor/watchfulli/xcloner-core/src/Xcloner_Sanitization.php	  #^	  RƽԶ      5   vendor/watchfulli/xcloner-core/src/Xcloner_Logger.php]  #^]  +t      9   vendor/watchfulli/xcloner-core/src/Xcloner_Standalone.phpd  #^d   .u      7   vendor/watchfulli/xcloner-core/src/Xcloner_Database.php\  #^\  l      ;   vendor/watchfulli/xcloner-core/src/Xcloner_Requirements.php  #^  O      ,   vendor/watchfulli/xcloner-core/src/index.php   #^    ض      =   vendor/watchfulli/xcloner-core/src/Xcloner_Remote_Storage.php~  #^~  7=6      7   vendor/watchfulli/xcloner-core/src/Xcloner_Settings.php"  #^"        6   vendor/watchfulli/xcloner-core/src/Xcloner_Archive.php~  #^~  aڶ      8   vendor/watchfulli/xcloner-core/src/Xcloner_Scheduler.phpA  #^A  }      <   vendor/watchfulli/xcloner-core/src/Xcloner_File_Transfer.phpB  #^B  /      2   vendor/watchfulli/xcloner-core/src/Xcloner_Api.php   #^   !      .   vendor/watchfulli/xcloner-core/src/Xcloner.php@   #^@   $      :   vendor/watchfulli/xcloner-core/src/Xcloner_File_System.php  #^  f
z      9   vendor/watchfulli/xcloner-core/src/Xcloner_Encryption.phpy5  #^y5  e
Nm      <?php

// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php';

return ComposerAutoloaderInit128867811b0d39661ef2af30a383fdcc::getLoader();
<?php

// autoload_namespaces.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
);

Copyright (c) Nils Adermann, Jordi Boggiano

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

<?php

/*
 * This file is part of Composer.
 *
 * (c) Nils Adermann <naderman@naderman.de>
 *     Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Composer\Autoload;

/**
 * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
 *
 *     $loader = new \Composer\Autoload\ClassLoader();
 *
 *     // register classes with namespaces
 *     $loader->add('Symfony\Component', __DIR__.'/component');
 *     $loader->add('Symfony',           __DIR__.'/framework');
 *
 *     // activate the autoloader
 *     $loader->register();
 *
 *     // to enable searching the include path (eg. for PEAR packages)
 *     $loader->setUseIncludePath(true);
 *
 * In this example, if you try to use a class in the Symfony\Component
 * namespace or one of its children (Symfony\Component\Console for instance),
 * the autoloader will first look for the class under the component/
 * directory, and it will then fallback to the framework/ directory if not
 * found before giving up.
 *
 * This class is loosely based on the Symfony UniversalClassLoader.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @see    http://www.php-fig.org/psr/psr-0/
 * @see    http://www.php-fig.org/psr/psr-4/
 */
class ClassLoader
{
    // PSR-4
    private $prefixLengthsPsr4 = array();
    private $prefixDirsPsr4 = array();
    private $fallbackDirsPsr4 = array();

    // PSR-0
    private $prefixesPsr0 = array();
    private $fallbackDirsPsr0 = array();

    private $useIncludePath = false;
    private $classMap = array();
    private $classMapAuthoritative = false;
    private $missingClasses = array();
    private $apcuPrefix;

    public function getPrefixes()
    {
        if (!empty($this->prefixesPsr0)) {
            return call_user_func_array('array_merge', $this->prefixesPsr0);
        }

        return array();
    }

    public function getPrefixesPsr4()
    {
        return $this->prefixDirsPsr4;
    }

    public function getFallbackDirs()
    {
        return $this->fallbackDirsPsr0;
    }

    public function getFallbackDirsPsr4()
    {
        return $this->fallbackDirsPsr4;
    }

    public function getClassMap()
    {
        return $this->classMap;
    }

    /**
     * @param array $classMap Class to filename map
     */
    public function addClassMap(array $classMap)
    {
        if ($this->classMap) {
            $this->classMap = array_merge($this->classMap, $classMap);
        } else {
            $this->classMap = $classMap;
        }
    }

    /**
     * Registers a set of PSR-0 directories for a given prefix, either
     * appending or prepending to the ones previously set for this prefix.
     *
     * @param string       $prefix  The prefix
     * @param array|string $paths   The PSR-0 root directories
     * @param bool         $prepend Whether to prepend the directories
     */
    public function add($prefix, $paths, $prepend = false)
    {
        if (!$prefix) {
            if ($prepend) {
                $this->fallbackDirsPsr0 = array_merge(
                    (array) $paths,
                    $this->fallbackDirsPsr0
                );
            } else {
                $this->fallbackDirsPsr0 = array_merge(
                    $this->fallbackDirsPsr0,
                    (array) $paths
                );
            }

            return;
        }

        $first = $prefix[0];
        if (!isset($this->prefixesPsr0[$first][$prefix])) {
            $this->prefixesPsr0[$first][$prefix] = (array) $paths;

            return;
        }
        if ($prepend) {
            $this->prefixesPsr0[$first][$prefix] = array_merge(
                (array) $paths,
                $this->prefixesPsr0[$first][$prefix]
            );
        } else {
            $this->prefixesPsr0[$first][$prefix] = array_merge(
                $this->prefixesPsr0[$first][$prefix],
                (array) $paths
            );
        }
    }

    /**
     * Registers a set of PSR-4 directories for a given namespace, either
     * appending or prepending to the ones previously set for this namespace.
     *
     * @param string       $prefix  The prefix/namespace, with trailing '\\'
     * @param array|string $paths   The PSR-4 base directories
     * @param bool         $prepend Whether to prepend the directories
     *
     * @throws \InvalidArgumentException
     */
    public function addPsr4($prefix, $paths, $prepend = false)
    {
        if (!$prefix) {
            // Register directories for the root namespace.
            if ($prepend) {
                $this->fallbackDirsPsr4 = array_merge(
                    (array) $paths,
                    $this->fallbackDirsPsr4
                );
            } else {
                $this->fallbackDirsPsr4 = array_merge(
                    $this->fallbackDirsPsr4,
                    (array) $paths
                );
            }
        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
            // Register directories for a new namespace.
            $length = strlen($prefix);
            if ('\\' !== $prefix[$length - 1]) {
                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
            }
            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
            $this->prefixDirsPsr4[$prefix] = (array) $paths;
        } elseif ($prepend) {
            // Prepend directories for an already registered namespace.
            $this->prefixDirsPsr4[$prefix] = array_merge(
                (array) $paths,
                $this->prefixDirsPsr4[$prefix]
            );
        } else {
            // Append directories for an already registered namespace.
            $this->prefixDirsPsr4[$prefix] = array_merge(
                $this->prefixDirsPsr4[$prefix],
                (array) $paths
            );
        }
    }

    /**
     * Registers a set of PSR-0 directories for a given prefix,
     * replacing any others previously set for this prefix.
     *
     * @param string       $prefix The prefix
     * @param array|string $paths  The PSR-0 base directories
     */
    public function set($prefix, $paths)
    {
        if (!$prefix) {
            $this->fallbackDirsPsr0 = (array) $paths;
        } else {
            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
        }
    }

    /**
     * Registers a set of PSR-4 directories for a given namespace,
     * replacing any others previously set for this namespace.
     *
     * @param string       $prefix The prefix/namespace, with trailing '\\'
     * @param array|string $paths  The PSR-4 base directories
     *
     * @throws \InvalidArgumentException
     */
    public function setPsr4($prefix, $paths)
    {
        if (!$prefix) {
            $this->fallbackDirsPsr4 = (array) $paths;
        } else {
            $length = strlen($prefix);
            if ('\\' !== $prefix[$length - 1]) {
                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
            }
            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
            $this->prefixDirsPsr4[$prefix] = (array) $paths;
        }
    }

    /**
     * Turns on searching the include path for class files.
     *
     * @param bool $useIncludePath
     */
    public function setUseIncludePath($useIncludePath)
    {
        $this->useIncludePath = $useIncludePath;
    }

    /**
     * Can be used to check if the autoloader uses the include path to check
     * for classes.
     *
     * @return bool
     */
    public function getUseIncludePath()
    {
        return $this->useIncludePath;
    }

    /**
     * Turns off searching the prefix and fallback directories for classes
     * that have not been registered with the class map.
     *
     * @param bool $classMapAuthoritative
     */
    public function setClassMapAuthoritative($classMapAuthoritative)
    {
        $this->classMapAuthoritative = $classMapAuthoritative;
    }

    /**
     * Should class lookup fail if not found in the current class map?
     *
     * @return bool
     */
    public function isClassMapAuthoritative()
    {
        return $this->classMapAuthoritative;
    }

    /**
     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
     *
     * @param string|null $apcuPrefix
     */
    public function setApcuPrefix($apcuPrefix)
    {
        $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
    }

    /**
     * The APCu prefix in use, or null if APCu caching is not enabled.
     *
     * @return string|null
     */
    public function getApcuPrefix()
    {
        return $this->apcuPrefix;
    }

    /**
     * Registers this instance as an autoloader.
     *
     * @param bool $prepend Whether to prepend the autoloader or not
     */
    public function register($prepend = false)
    {
        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
    }

    /**
     * Unregisters this instance as an autoloader.
     */
    public function unregister()
    {
        spl_autoload_unregister(array($this, 'loadClass'));
    }

    /**
     * Loads the given class or interface.
     *
     * @param  string    $class The name of the class
     * @return bool|null True if loaded, null otherwise
     */
    public function loadClass($class)
    {
        if ($file = $this->findFile($class)) {
            includeFile($file);

            return true;
        }
    }

    /**
     * Finds the path to the file where the class is defined.
     *
     * @param string $class The name of the class
     *
     * @return string|false The path if found, false otherwise
     */
    public function findFile($class)
    {
        // class map lookup
        if (isset($this->classMap[$class])) {
            return $this->classMap[$class];
        }
        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
            return false;
        }
        if (null !== $this->apcuPrefix) {
            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
            if ($hit) {
                return $file;
            }
        }

        $file = $this->findFileWithExtension($class, '.php');

        // Search for Hack files if we are running on HHVM
        if (false === $file && defined('HHVM_VERSION')) {
            $file = $this->findFileWithExtension($class, '.hh');
        }

        if (null !== $this->apcuPrefix) {
            apcu_add($this->apcuPrefix.$class, $file);
        }

        if (false === $file) {
            // Remember that this class does not exist.
            $this->missingClasses[$class] = true;
        }

        return $file;
    }

    private function findFileWithExtension($class, $ext)
    {
        // PSR-4 lookup
        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

        $first = $class[0];
        if (isset($this->prefixLengthsPsr4[$first])) {
            $subPath = $class;
            while (false !== $lastPos = strrpos($subPath, '\\')) {
                $subPath = substr($subPath, 0, $lastPos);
                $search = $subPath.'\\';
                if (isset($this->prefixDirsPsr4[$search])) {
                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
                        if (file_exists($file = $dir . $pathEnd)) {
                            return $file;
                        }
                    }
                }
            }
        }

        // PSR-4 fallback dirs
        foreach ($this->fallbackDirsPsr4 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
                return $file;
            }
        }

        // PSR-0 lookup
        if (false !== $pos = strrpos($class, '\\')) {
            // namespaced class name
            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
        } else {
            // PEAR-like class name
            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
        }

        if (isset($this->prefixesPsr0[$first])) {
            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
                if (0 === strpos($class, $prefix)) {
                    foreach ($dirs as $dir) {
                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                            return $file;
                        }
                    }
                }
            }
        }

        // PSR-0 fallback dirs
        foreach ($this->fallbackDirsPsr0 as $dir) {
            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                return $file;
            }
        }

        // PSR-0 include paths.
        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
            return $file;
        }

        return false;
    }
}

/**
 * Scope isolated include.
 *
 * Prevents access to $this/self from included files.
 */
function includeFile($file)
{
    include $file;
}
<?php

// autoload_psr4.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'watchfulli\\XClonerCore\\' => array($vendorDir . '/watchfulli/xcloner-core/src'),
    'splitbrain\\PHPArchive\\' => array($vendorDir . '/splitbrain/php-archive/src'),
    'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
    'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
    'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'),
);
<?php

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
);
<?php

// autoload_static.php @generated by Composer

namespace Composer\Autoload;

class ComposerStaticInit128867811b0d39661ef2af30a383fdcc
{
    public static $prefixLengthsPsr4 = array (
        'w' => 
        array (
            'watchfulli\\XClonerCore\\' => 23,
        ),
        's' => 
        array (
            'splitbrain\\PHPArchive\\' => 22,
        ),
        'P' => 
        array (
            'Psr\\Log\\' => 8,
        ),
        'M' => 
        array (
            'Monolog\\' => 8,
        ),
        'L' => 
        array (
            'League\\Flysystem\\' => 17,
        ),
    );

    public static $prefixDirsPsr4 = array (
        'watchfulli\\XClonerCore\\' => 
        array (
            0 => __DIR__ . '/..' . '/watchfulli/xcloner-core/src',
        ),
        'splitbrain\\PHPArchive\\' => 
        array (
            0 => __DIR__ . '/..' . '/splitbrain/php-archive/src',
        ),
        'Psr\\Log\\' => 
        array (
            0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
        ),
        'Monolog\\' => 
        array (
            0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
        ),
        'League\\Flysystem\\' => 
        array (
            0 => __DIR__ . '/..' . '/league/flysystem/src',
        ),
    );

    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 = ComposerStaticInit128867811b0d39661ef2af30a383fdcc::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInit128867811b0d39661ef2af30a383fdcc::$prefixDirsPsr4;

        }, null, ClassLoader::class);
    }
}
<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInit128867811b0d39661ef2af30a383fdcc
{
    private static $loader;

    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }

        spl_autoload_register(array('ComposerAutoloaderInit128867811b0d39661ef2af30a383fdcc', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        spl_autoload_unregister(array('ComposerAutoloaderInit128867811b0d39661ef2af30a383fdcc', 'loadClassLoader'));

        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInit128867811b0d39661ef2af30a383fdcc::getInitializer($loader));
        } else {
            $map = require __DIR__ . '/autoload_namespaces.php';
            foreach ($map as $namespace => $path) {
                $loader->set($namespace, $path);
            }

            $map = require __DIR__ . '/autoload_psr4.php';
            foreach ($map as $namespace => $path) {
                $loader->setPsr4($namespace, $path);
            }

            $classMap = require __DIR__ . '/autoload_classmap.php';
            if ($classMap) {
                $loader->addClassMap($classMap);
            }
        }

        $loader->register(true);

        return $loader;
    }
}
[
    {
        "name": "league/flysystem",
        "version": "1.0.69",
        "version_normalized": "1.0.69.0",
        "source": {
            "type": "git",
            "url": "https://github.com/thephpleague/flysystem.git",
            "reference": "7106f78428a344bc4f643c233a94e48795f10967"
        },
        "dist": {
            "type": "zip",
            "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7106f78428a344bc4f643c233a94e48795f10967",
            "reference": "7106f78428a344bc4f643c233a94e48795f10967",
            "shasum": ""
        },
        "require": {
            "ext-fileinfo": "*",
            "php": ">=5.5.9"
        },
        "conflict": {
            "league/flysystem-sftp": "<1.0.6"
        },
        "require-dev": {
            "phpspec/phpspec": "^3.4",
            "phpunit/phpunit": "^5.7.26"
        },
        "suggest": {
            "ext-fileinfo": "Required for MimeType",
            "ext-ftp": "Allows you to use FTP server storage",
            "ext-openssl": "Allows you to use FTPS server storage",
            "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
            "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
            "league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
            "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
            "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
            "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
            "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
            "league/flysystem-webdav": "Allows you to use WebDAV storage",
            "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
            "spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
            "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
        },
        "time": "2020-05-18T15:13:39+00:00",
        "type": "library",
        "extra": {
            "branch-alias": {
                "dev-master": "1.1-dev"
            }
        },
        "installation-source": "dist",
        "autoload": {
            "psr-4": {
                "League\\Flysystem\\": "src/"
            }
        },
        "notification-url": "https://packagist.org/downloads/",
        "license": [
            "MIT"
        ],
        "authors": [
            {
                "name": "Frank de Jonge",
                "email": "info@frenky.net"
            }
        ],
        "description": "Filesystem abstraction: Many filesystems, one API.",
        "keywords": [
            "Cloud Files",
            "WebDAV",
            "abstraction",
            "aws",
            "cloud",
            "copy.com",
            "dropbox",
            "file systems",
            "files",
            "filesystem",
            "filesystems",
            "ftp",
            "rackspace",
            "remote",
            "s3",
            "sftp",
            "storage"
        ]
    },
    {
        "name": "monolog/monolog",
        "version": "1.25.4",
        "version_normalized": "1.25.4.0",
        "source": {
            "type": "git",
            "url": "https://github.com/Seldaek/monolog.git",
            "reference": "3022efff205e2448b560c833c6fbbf91c3139168"
        },
        "dist": {
            "type": "zip",
            "url": "https://api.github.com/repos/Seldaek/monolog/zipball/3022efff205e2448b560c833c6fbbf91c3139168",
            "reference": "3022efff205e2448b560c833c6fbbf91c3139168",
            "shasum": ""
        },
        "require": {
            "php": ">=5.3.0",
            "psr/log": "~1.0"
        },
        "provide": {
            "psr/log-implementation": "1.0.0"
        },
        "require-dev": {
            "aws/aws-sdk-php": "^2.4.9 || ^3.0",
            "doctrine/couchdb": "~1.0@dev",
            "graylog2/gelf-php": "~1.0",
            "php-amqplib/php-amqplib": "~2.4",
            "php-console/php-console": "^3.1.3",
            "php-parallel-lint/php-parallel-lint": "^1.0",
            "phpunit/phpunit": "~4.5",
            "ruflin/elastica": ">=0.90 <3.0",
            "sentry/sentry": "^0.13",
            "swiftmailer/swiftmailer": "^5.3|^6.0"
        },
        "suggest": {
            "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
            "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
            "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
            "ext-mongo": "Allow sending log messages to a MongoDB server",
            "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
            "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
            "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
            "php-console/php-console": "Allow sending log messages to Google Chrome",
            "rollbar/rollbar": "Allow sending log messages to Rollbar",
            "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
            "sentry/sentry": "Allow sending log messages to a Sentry server"
        },
        "time": "2020-05-22T07:31:27+00:00",
        "type": "library",
        "extra": {
            "branch-alias": {
                "dev-master": "2.0.x-dev"
            }
        },
        "installation-source": "dist",
        "autoload": {
            "psr-4": {
                "Monolog\\": "src/Monolog"
            }
        },
        "notification-url": "https://packagist.org/downloads/",
        "license": [
            "MIT"
        ],
        "authors": [
            {
                "name": "Jordi Boggiano",
                "email": "j.boggiano@seld.be",
                "homepage": "http://seld.be"
            }
        ],
        "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
        "homepage": "http://github.com/Seldaek/monolog",
        "keywords": [
            "log",
            "logging",
            "psr-3"
        ]
    },
    {
        "name": "psr/log",
        "version": "1.1.3",
        "version_normalized": "1.1.3.0",
        "source": {
            "type": "git",
            "url": "https://github.com/php-fig/log.git",
            "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
        },
        "dist": {
            "type": "zip",
            "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
            "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
            "shasum": ""
        },
        "require": {
            "php": ">=5.3.0"
        },
        "time": "2020-03-23T09:12:05+00:00",
        "type": "library",
        "extra": {
            "branch-alias": {
                "dev-master": "1.1.x-dev"
            }
        },
        "installation-source": "dist",
        "autoload": {
            "psr-4": {
                "Psr\\Log\\": "Psr/Log/"
            }
        },
        "notification-url": "https://packagist.org/downloads/",
        "license": [
            "MIT"
        ],
        "authors": [
            {
                "name": "PHP-FIG",
                "homepage": "http://www.php-fig.org/"
            }
        ],
        "description": "Common interface for logging libraries",
        "homepage": "https://github.com/php-fig/log",
        "keywords": [
            "log",
            "psr",
            "psr-3"
        ]
    },
    {
        "name": "splitbrain/php-archive",
        "version": "1.1.1",
        "version_normalized": "1.1.1.0",
        "source": {
            "type": "git",
            "url": "https://github.com/splitbrain/php-archive.git",
            "reference": "10d89013572ba1f4d4ad7fcb74860242f4c3860b"
        },
        "dist": {
            "type": "zip",
            "url": "https://api.github.com/repos/splitbrain/php-archive/zipball/10d89013572ba1f4d4ad7fcb74860242f4c3860b",
            "reference": "10d89013572ba1f4d4ad7fcb74860242f4c3860b",
            "shasum": ""
        },
        "require": {
            "php": ">=5.4"
        },
        "require-dev": {
            "ext-bz2": "*",
            "ext-zip": "*",
            "mikey179/vfsstream": "^1.6",
            "phpunit/phpunit": "^4.8"
        },
        "suggest": {
            "ext-iconv": "Used for proper filename encode handling",
            "ext-mbstring": "Can be used alternatively for handling filename encoding"
        },
        "time": "2018-09-09T12:13:53+00:00",
        "type": "library",
        "installation-source": "dist",
        "autoload": {
            "psr-4": {
                "splitbrain\\PHPArchive\\": "src"
            }
        },
        "notification-url": "https://packagist.org/downloads/",
        "license": [
            "MIT"
        ],
        "authors": [
            {
                "name": "Andreas Gohr",
                "email": "andi@splitbrain.org"
            }
        ],
        "description": "Pure-PHP implementation to read and write TAR and ZIP archives",
        "keywords": [
            "archive",
            "extract",
            "tar",
            "unpack",
            "unzip",
            "zip"
        ]
    },
    {
        "name": "watchfulli/xcloner-core",
        "version": "dev-restore",
        "version_normalized": "dev-restore",
        "source": {
            "type": "git",
            "url": "https://github.com/watchfulli/xcloner-core.git",
            "reference": "9b96bd0e0d7dd3c0bdbbdd438b268df86ca49ce1"
        },
        "dist": {
            "type": "zip",
            "url": "https://api.github.com/repos/watchfulli/xcloner-core/zipball/9b96bd0e0d7dd3c0bdbbdd438b268df86ca49ce1",
            "reference": "9b96bd0e0d7dd3c0bdbbdd438b268df86ca49ce1",
            "shasum": ""
        },
        "require": {
            "league/flysystem": "^1.0",
            "monolog/monolog": "^1.22",
            "splitbrain/php-archive": "^1.0"
        },
        "time": "2020-06-11T07:37:53+00:00",
        "type": "package",
        "installation-source": "source",
        "autoload": {
            "psr-4": {
                "watchfulli\\XClonerCore\\": "src/"
            }
        },
        "notification-url": "https://packagist.org/downloads/",
        "license": [
            "AGPL-3.0-or-later"
        ],
        "authors": [
            {
                "name": "Ovidiu Liuta",
                "email": "info@thinkovi.com"
            }
        ],
        "description": "XCloner Core Library for Backup and Restore"
    }
]
# Deprecations

This document lists all the planned deprecations.

## Handlers will be removed in 2.0

The `Handler` type and associated calls will be removed in version 2.0.

### Upgrade path

You should create your own implementation for handling OOP usage,
but it's recommended to move away from using an OOP-style wrapper entirely.

The reason for this is that it's too easy for implementation details (for
your application this is Flysystem) to leak into the application. The most
important part for Flysystem is that it improves portability and creates a
solid boundary between your application core and the infrastructure you use.
The OOP-style handling breaks this principle, therefore I want to stop
promoting it. 
Copyright (c) 2013-2019 Frank de Jonge

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
{
    "name": "league/flysystem",
    "type": "library",
    "description": "Filesystem abstraction: Many filesystems, one API.",
    "keywords": [
        "filesystem", "filesystems", "files", "storage", "dropbox", "aws",
        "abstraction", "s3", "ftp", "sftp", "remote", "webdav",
        "file systems", "cloud", "cloud files", "rackspace", "copy.com"
    ],
    "funding": [
        {
            "type": "other",
            "url": "https://offset.earth/frankdejonge"
        }
    ],
    "license": "MIT",
    "authors": [
        {
            "name": "Frank de Jonge",
            "email": "info@frenky.net"
        }
    ],
    "require": {
        "php": ">=5.5.9",
        "ext-fileinfo": "*"
    },
    "require-dev": {
        "phpspec/phpspec": "^3.4",
        "phpunit/phpunit": "^5.7.26"
    },
    "autoload": {
        "psr-4": {
            "League\\Flysystem\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "League\\Flysystem\\Stub\\": "stub/"
        },
        "files": [
          "tests/PHPUnitHacks.php"
        ]
    },
    "suggest": {
        "ext-fileinfo": "Required for MimeType",
        "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
        "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
        "league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
        "league/flysystem-webdav": "Allows you to use WebDAV storage",
        "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
        "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
        "spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
        "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications",
        "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
        "ext-ftp": "Allows you to use FTP server storage",
        "ext-openssl": "Allows you to use FTPS server storage",
        "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
        "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter"
    },
    "conflict": {
        "league/flysystem-sftp": "<1.0.6"
    },
    "extra": {
        "branch-alias": {
            "dev-master": "1.1-dev"
        }
    },
    "scripts": {
        "phpstan": "php phpstan.php"
    }
}
# Security Policy

## Supported Versions

| Version | Supported          |
| ------- | ------------------ |
| 1.0.x   | :white_check_mark: |
| 2.0.x   | :x:                |

## Reporting a Vulnerability

When you've encountered a security vulnerability, please disclose it securely.

The security process is described at: 
[https://flysystem.thephpleague.com/docs/security/](https://flysystem.thephpleague.com/docs/security/)

<?php

namespace League\Flysystem;

use ErrorException;

class ConnectionErrorException extends ErrorException implements FilesystemException
{
}
<?php

namespace League\Flysystem;

interface FilesystemException
{
}
<?php

namespace League\Flysystem;

use SplFileInfo;

class UnreadableFileException extends Exception
{
    public static function forFileInfo(SplFileInfo $fileInfo)
    {
        return new static(
            sprintf(
                'Unreadable file encountered: %s',
                $fileInfo->getRealPath()
            )
        );
    }
}
<?php

namespace League\Flysystem;

use LogicException;

/**
 * Thrown when the MountManager cannot find a filesystem.
 */
class FilesystemNotFoundException extends LogicException implements FilesystemException
{
}
<?php

namespace League\Flysystem;

/**
 * @internal
 */
trait ConfigAwareTrait
{
    /**
     * @var Config
     */
    protected $config;

    /**
     * Set the config.
     *
     * @param Config|array|null $config
     */
    protected function setConfig($config)
    {
        $this->config = $config ? Util::ensureConfig($config) : new Config;
    }

    /**
     * Get the Config.
     *
     * @return Config config object
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * Convert a config array to a Config object with the correct fallback.
     *
     * @param array $config
     *
     * @return Config
     */
    protected function prepareConfig(array $config)
    {
        $config = new Config($config);
        $config->setFallback($this->getConfig());

        return $config;
    }
}
<?php

namespace League\Flysystem\Util;

use ErrorException;
use finfo;

/**
 * @internal
 */
class MimeType
{
    protected static $extensionToMimeTypeMap = [
        'hqx' => 'application/mac-binhex40',
        'cpt' => 'application/mac-compactpro',
        'csv' => 'text/csv',
        'bin' => 'application/octet-stream',
        'dms' => 'application/octet-stream',
        'lha' => 'application/octet-stream',
        'lzh' => 'application/octet-stream',
        'exe' => 'application/octet-stream',
        'class' => 'application/octet-stream',
        'psd' => 'application/x-photoshop',
        'so' => 'application/octet-stream',
        'sea' => 'application/octet-stream',
        'dll' => 'application/octet-stream',
        'oda' => 'application/oda',
        'pdf' => 'application/pdf',
        'ai' => 'application/pdf',
        'eps' => 'application/postscript',
        'epub' => 'application/epub+zip',
        'ps' => 'application/postscript',
        'smi' => 'application/smil',
        'smil' => 'application/smil',
        'mif' => 'application/vnd.mif',
        'xls' => 'application/vnd.ms-excel',
        'xlt' => 'application/vnd.ms-excel',
        'xla' => 'application/vnd.ms-excel',
        'ppt' => 'application/powerpoint',
        'pot' => 'application/vnd.ms-powerpoint',
        'pps' => 'application/vnd.ms-powerpoint',
        'ppa' => 'application/vnd.ms-powerpoint',
        'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
        'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
        'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
        'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
        'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
        'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
        'wbxml' => 'application/wbxml',
        'wmlc' => 'application/wmlc',
        'dcr' => 'application/x-director',
        'dir' => 'application/x-director',
        'dxr' => 'application/x-director',
        'dvi' => 'application/x-dvi',
        'gtar' => 'application/x-gtar',
        'gz' => 'application/x-gzip',
        'gzip' => 'application/x-gzip',
        'php' => 'application/x-httpd-php',
        'php4' => 'application/x-httpd-php',
        'php3' => 'application/x-httpd-php',
        'phtml' => 'application/x-httpd-php',
        'phps' => 'application/x-httpd-php-source',
        'js' => 'application/javascript',
        'swf' => 'application/x-shockwave-flash',
        'sit' => 'application/x-stuffit',
        'tar' => 'application/x-tar',
        'tgz' => 'application/x-tar',
        'z' => 'application/x-compress',
        'xhtml' => 'application/xhtml+xml',
        'xht' => 'application/xhtml+xml',
        'rdf' => 'application/rdf+xml',
        'zip' => 'application/x-zip',
        'rar' => 'application/x-rar',
        'mid' => 'audio/midi',
        'midi' => 'audio/midi',
        'mpga' => 'audio/mpeg',
        'mp2' => 'audio/mpeg',
        'mp3' => 'audio/mpeg',
        'aif' => 'audio/x-aiff',
        'aiff' => 'audio/x-aiff',
        'aifc' => 'audio/x-aiff',
        'ram' => 'audio/x-pn-realaudio',
        'rm' => 'audio/x-pn-realaudio',
        'rpm' => 'audio/x-pn-realaudio-plugin',
        'ra' => 'audio/x-realaudio',
        'rv' => 'video/vnd.rn-realvideo',
        'wav' => 'audio/x-wav',
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'jpe' => 'image/jpeg',
        'png' => 'image/png',
        'gif' => 'image/gif',
        'bmp' => 'image/bmp',
        'tiff' => 'image/tiff',
        'tif' => 'image/tiff',
        'svg' => 'image/svg+xml',
        'css' => 'text/css',
        'html' => 'text/html',
        'htm' => 'text/html',
        'shtml' => 'text/html',
        'txt' => 'text/plain',
        'text' => 'text/plain',
        'log' => 'text/plain',
        'markdown' => 'text/markdown',
        'md' => 'text/markdown',
        'rtx' => 'text/richtext',
        'rtf' => 'text/rtf',
        'xml' => 'application/xml',
        'xsl' => 'application/xml',
        'dmn' => 'application/octet-stream',
        'bpmn' => 'application/octet-stream',
        'mpeg' => 'video/mpeg',
        'mpg' => 'video/mpeg',
        'mpe' => 'video/mpeg',
        'qt' => 'video/quicktime',
        'mov' => 'video/quicktime',
        'avi' => 'video/x-msvideo',
        'movie' => 'video/x-sgi-movie',
        'doc' => 'application/msword',
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'docm' => 'application/vnd.ms-word.template.macroEnabled.12',
        'dotm' => 'application/vnd.ms-word.template.macroEnabled.12',
        'dot' => 'application/msword',
        'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
        'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
        'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12',
        'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
        'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
        'word' => 'application/msword',
        'xl' => 'application/excel',
        'eml' => 'message/rfc822',
        'json' => 'application/json',
        'pem' => 'application/x-x509-user-cert',
        'p10' => 'application/x-pkcs10',
        'p12' => 'application/x-pkcs12',
        'p7a' => 'application/x-pkcs7-signature',
        'p7c' => 'application/pkcs7-mime',
        'p7m' => 'application/pkcs7-mime',
        'p7r' => 'application/x-pkcs7-certreqresp',
        'p7s' => 'application/pkcs7-signature',
        'crt' => 'application/x-x509-ca-cert',
        'crl' => 'application/pkix-crl',
        'der' => 'application/x-x509-ca-cert',
        'kdb' => 'application/octet-stream',
        'pgp' => 'application/pgp',
        'gpg' => 'application/gpg-keys',
        'sst' => 'application/octet-stream',
        'csr' => 'application/octet-stream',
        'rsa' => 'application/x-pkcs7',
        'cer' => 'application/pkix-cert',
        '3g2' => 'video/3gpp2',
        '3gp' => 'video/3gp',
        'mp4' => 'video/mp4',
        'm4a' => 'audio/x-m4a',
        'f4v' => 'video/mp4',
        'webm' => 'video/webm',
        'aac' => 'audio/x-acc',
        'm4u' => 'application/vnd.mpegurl',
        'm3u' => 'text/plain',
        'xspf' => 'application/xspf+xml',
        'vlc' => 'application/videolan',
        'wmv' => 'video/x-ms-wmv',
        'au' => 'audio/x-au',
        'ac3' => 'audio/ac3',
        'flac' => 'audio/x-flac',
        'ogg' => 'audio/ogg',
        'kmz' => 'application/vnd.google-earth.kmz',
        'kml' => 'application/vnd.google-earth.kml+xml',
        'ics' => 'text/calendar',
        'zsh' => 'text/x-scriptzsh',
        '7zip' => 'application/x-7z-compressed',
        'cdr' => 'application/cdr',
        'wma' => 'audio/x-ms-wma',
        'jar' => 'application/java-archive',
        'tex' => 'application/x-tex',
        'latex' => 'application/x-latex',
        'odt' => 'application/vnd.oasis.opendocument.text',
        'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
        'odp' => 'application/vnd.oasis.opendocument.presentation',
        'odg' => 'application/vnd.oasis.opendocument.graphics',
        'odc' => 'application/vnd.oasis.opendocument.chart',
        'odf' => 'application/vnd.oasis.opendocument.formula',
        'odi' => 'application/vnd.oasis.opendocument.image',
        'odm' => 'application/vnd.oasis.opendocument.text-master',
        'odb' => 'application/vnd.oasis.opendocument.database',
        'ott' => 'application/vnd.oasis.opendocument.text-template',
        'webp' => 'image/webp',
        'ico' => 'image/x-icon',
    ];

    /**
     * Detects MIME Type based on given content.
     *
     * @param mixed $content
     *
     * @return string|null MIME Type or NULL if no mime type detected
     */
    public static function detectByContent($content)
    {
        if ( ! class_exists('finfo') || ! is_string($content)) {
            return null;
        }
        try {
            $finfo = new finfo(FILEINFO_MIME_TYPE);

            return $finfo->buffer($content) ?: null;
            // @codeCoverageIgnoreStart
        } catch (ErrorException $e) {
            // This is caused by an array to string conversion error.
        }
    } // @codeCoverageIgnoreEnd

    /**
     * Detects MIME Type based on file extension.
     *
     * @param string $extension
     *
     * @return string|null MIME Type or NULL if no extension detected
     */
    public static function detectByFileExtension($extension)
    {
        return isset(static::$extensionToMimeTypeMap[$extension])
            ? static::$extensionToMimeTypeMap[$extension]
            : 'text/plain';
    }

    /**
     * @param string $filename
     *
     * @return string|null MIME Type or NULL if no extension detected
     */
    public static function detectByFilename($filename)
    {
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

        return empty($extension) ? 'text/plain' : static::detectByFileExtension($extension);
    }

    /**
     * @return array Map of file extension to MIME Type
     */
    public static function getExtensionToMimeTypeMap()
    {
        return static::$extensionToMimeTypeMap;
    }
}
<?php

namespace League\Flysystem\Util;

use League\Flysystem\Util;

/**
 * @internal
 */
class ContentListingFormatter
{
    /**
     * @var string
     */
    private $directory;

    /**
     * @var bool
     */
    private $recursive;

    /**
     * @var bool
     */
    private $caseSensitive;

    /**
     * @param string $directory
     * @param bool   $recursive
     */
    public function __construct($directory, $recursive, $caseSensitive = true)
    {
        $this->directory = rtrim($directory, '/');
        $this->recursive = $recursive;
        $this->caseSensitive = $caseSensitive;
    }

    /**
     * Format contents listing.
     *
     * @param array $listing
     *
     * @return array
     */
    public function formatListing(array $listing)
    {
        $listing = array_filter(array_map([$this, 'addPathInfo'], $listing), [$this, 'isEntryOutOfScope']);

        return $this->sortListing(array_values($listing));
    }

    private function addPathInfo(array $entry)
    {
        return $entry + Util::pathinfo($entry['path']);
    }

    /**
     * Determine if the entry is out of scope.
     *
     * @param array $entry
     *
     * @return bool
     */
    private function isEntryOutOfScope(array $entry)
    {
        if (empty($entry['path']) && $entry['path'] !== '0') {
            return false;
        }

        if ($this->recursive) {
            return $this->residesInDirectory($entry);
        }

        return $this->isDirectChild($entry);
    }

    /**
     * Check if the entry resides within the parent directory.
     *
     * @param array $entry
     *
     * @return bool
     */
    private function residesInDirectory(array $entry)
    {
        if ($this->directory === '') {
            return true;
        }

        return $this->caseSensitive
            ? strpos($entry['path'], $this->directory . '/') === 0
            : stripos($entry['path'], $this->directory . '/') === 0;
    }

    /**
     * Check if the entry is a direct child of the directory.
     *
     * @param array $entry
     *
     * @return bool
     */
    private function isDirectChild(array $entry)
    {
        return $this->caseSensitive
            ? $entry['dirname'] === $this->directory
            : strcasecmp($this->directory, $entry['dirname']) === 0;
    }

    /**
     * @param array $listing
     *
     * @return array
     */
    private function sortListing(array $listing)
    {
        usort($listing, function ($a, $b) {
            return strcasecmp($a['path'], $b['path']);
        });

        return $listing;
    }
}
<?php

namespace League\Flysystem\Util;

class StreamHasher
{
    /**
     * @var string
     */
    private $algo;

    /**
     * StreamHasher constructor.
     *
     * @param string $algo
     */
    public function __construct($algo)
    {
        $this->algo = $algo;
    }

    /**
     * @param resource $resource
     *
     * @return string
     */
    public function hash($resource)
    {
        rewind($resource);
        $context = hash_init($this->algo);
        hash_update_stream($context, $resource);
        fclose($resource);

        return hash_final($context);
    }
}
<?php

namespace League\Flysystem;

use BadMethodCallException;

/**
 * @deprecated
 */
abstract class Handler
{
    /**
     * @var string
     */
    protected $path;

    /**
     * @var FilesystemInterface
     */
    protected $filesystem;

    /**
     * Constructor.
     *
     * @param FilesystemInterface $filesystem
     * @param string              $path
     */
    public function __construct(FilesystemInterface $filesystem = null, $path = null)
    {
        $this->path = $path;
        $this->filesystem = $filesystem;
    }

    /**
     * Check whether the entree is a directory.
     *
     * @return bool
     */
    public function isDir()
    {
        return $this->getType() === 'dir';
    }

    /**
     * Check whether the entree is a file.
     *
     * @return bool
     */
    public function isFile()
    {
        return $this->getType() === 'file';
    }

    /**
     * Retrieve the entree type (file|dir).
     *
     * @return string file or dir
     */
    public function getType()
    {
        $metadata = $this->filesystem->getMetadata($this->path);

        return $metadata ? $metadata['type'] : 'dir';
    }

    /**
     * Set the Filesystem object.
     *
     * @param FilesystemInterface $filesystem
     *
     * @return $this
     */
    public function setFilesystem(FilesystemInterface $filesystem)
    {
        $this->filesystem = $filesystem;

        return $this;
    }
    
    /**
     * Retrieve the Filesystem object.
     *
     * @return FilesystemInterface
     */
    public function getFilesystem()
    {
        return $this->filesystem;
    }

    /**
     * Set the entree path.
     *
     * @param string $path
     *
     * @return $this
     */
    public function setPath($path)
    {
        $this->path = $path;

        return $this;
    }

    /**
     * Retrieve the entree path.
     *
     * @return string path
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * Plugins pass-through.
     *
     * @param string $method
     * @param array  $arguments
     *
     * @return mixed
     */
    public function __call($method, array $arguments)
    {
        array_unshift($arguments, $this->path);
        $callback = [$this->filesystem, $method];

        try {
            return call_user_func_array($callback, $arguments);
        } catch (BadMethodCallException $e) {
            throw new BadMethodCallException(
                'Call to undefined method '
                . get_called_class()
                . '::' . $method
            );
        }
    }
}
<?php

namespace League\Flysystem;

use RuntimeException;

class ConnectionRuntimeException extends RuntimeException implements FilesystemException
{
}
<?php

namespace League\Flysystem;

interface ReadInterface
{
    /**
     * Check whether a file exists.
     *
     * @param string $path
     *
     * @return array|bool|null
     */
    public function has($path);

    /**
     * Read a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function read($path);

    /**
     * Read a file as a stream.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function readStream($path);

    /**
     * List contents of a directory.
     *
     * @param string $directory
     * @param bool   $recursive
     *
     * @return array
     */
    public function listContents($directory = '', $recursive = false);

    /**
     * Get all the meta data of a file or directory.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getMetadata($path);

    /**
     * Get the size of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getSize($path);

    /**
     * Get the mimetype of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getMimetype($path);

    /**
     * Get the last modified time of a file as a timestamp.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getTimestamp($path);

    /**
     * Get the visibility of a file.
     *
     * @param string $path
     *
     * @return array|false
     */
    public function getVisibility($path);
}
<?php

namespace League\Flysystem;

use InvalidArgumentException;
use League\Flysystem\Adapter\CanOverwriteFiles;
use League\Flysystem\Plugin\PluggableTrait;
use League\Flysystem\Util\ContentListingFormatter;

/**
 * @method array getWithMetadata(string $path, array $metadata)
 * @method bool  forceCopy(string $path, string $newpath)
 * @method bool  forceRename(string $path, string $newpath)
 * @method array listFiles(string $path = '', boolean $recursive = false)
 * @method array listPaths(string $path = '', boolean $recursive = false)
 * @method array listWith(array $keys = [], $directory = '', $recursive = false)
 */
class Filesystem implements FilesystemInterface
{
    use PluggableTrait;
    use ConfigAwareTrait;

    /**
     * @var AdapterInterface
     */
    protected $adapter;

    /**
     * Constructor.
     *
     * @param AdapterInterface $adapter
     * @param Config|array     $config
     */
    public function __construct(AdapterInterface $adapter, $config = null)
    {
        $this->adapter = $adapter;
        $this->setConfig($config);
    }

    /**
     * Get the Adapter.
     *
     * @return AdapterInterface adapter
     */
    public function getAdapter()
    {
        return $this->adapter;
    }

    /**
     * @inheritdoc
     */
    public function has($path)
    {
        $path = Util::normalizePath($path);

        return strlen($path) === 0 ? false : (bool) $this->getAdapter()->has($path);
    }

    /**
     * @inheritdoc
     */
    public function write($path, $contents, array $config = [])
    {
        $path = Util::normalizePath($path);
        $this->assertAbsent($path);
        $config = $this->prepareConfig($config);

        return (bool) $this->getAdapter()->write($path, $contents, $config);
    }

    /**
     * @inheritdoc
     */
    public function writeStream($path, $resource, array $config = [])
    {
        if ( ! is_resource($resource)) {
            throw new InvalidArgumentException(__METHOD__ . ' expects argument #2 to be a valid resource.');
        }

        $path = Util::normalizePath($path);
        $this->assertAbsent($path);
        $config = $this->prepareConfig($config);

        Util::rewindStream($resource);

        return (bool) $this->getAdapter()->writeStream($path, $resource, $config);
    }

    /**
     * @inheritdoc
     */
    public function put($path, $contents, array $config = [])
    {
        $path = Util::normalizePath($path);
        $config = $this->prepareConfig($config);

        if ( ! $this->getAdapter() instanceof CanOverwriteFiles && $this->has($path)) {
            return (bool) $this->getAdapter()->update($path, $contents, $config);
        }

        return (bool) $this->getAdapter()->write($path, $contents, $config);
    }

    /**
     * @inheritdoc
     */
    public function putStream($path, $resource, array $config = [])
    {
        if ( ! is_resource($resource)) {
            throw new InvalidArgumentException(__METHOD__ . ' expects argument #2 to be a valid resource.');
        }

        $path = Util::normalizePath($path);
        $config = $this->prepareConfig($config);
        Util::rewindStream($resource);

        if ( ! $this->getAdapter() instanceof CanOverwriteFiles && $this->has($path)) {
            return (bool) $this->getAdapter()->updateStream($path, $resource, $config);
        }

        return (bool) $this->getAdapter()->writeStream($path, $resource, $config);
    }

    /**
     * @inheritdoc
     */
    public function readAndDelete($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);
        $contents = $this->read($path);

        if ($contents === false) {
            return false;
        }

        $this->delete($path);

        return $contents;
    }

    /**
     * @inheritdoc
     */
    public function update($path, $contents, array $config = [])
    {
        $path = Util::normalizePath($path);
        $config = $this->prepareConfig($config);

        $this->assertPresent($path);

        return (bool) $this->getAdapter()->update($path, $contents, $config);
    }

    /**
     * @inheritdoc
     */
    public function updateStream($path, $resource, array $config = [])
    {
        if ( ! is_resource($resource)) {
            throw new InvalidArgumentException(__METHOD__ . ' expects argument #2 to be a valid resource.');
        }

        $path = Util::normalizePath($path);
        $config = $this->prepareConfig($config);
        $this->assertPresent($path);
        Util::rewindStream($resource);

        return (bool) $this->getAdapter()->updateStream($path, $resource, $config);
    }

    /**
     * @inheritdoc
     */
    public function read($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        if ( ! ($object = $this->getAdapter()->read($path))) {
            return false;
        }

        return $object['contents'];
    }

    /**
     * @inheritdoc
     */
    public function readStream($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        if ( ! $object = $this->getAdapter()->readStream($path)) {
            return false;
        }

        return $object['stream'];
    }

    /**
     * @inheritdoc
     */
    public function rename($path, $newpath)
    {
        $path = Util::normalizePath($path);
        $newpath = Util::normalizePath($newpath);
        $this->assertPresent($path);
        $this->assertAbsent($newpath);

        return (bool) $this->getAdapter()->rename($path, $newpath);
    }

    /**
     * @inheritdoc
     */
    public function copy($path, $newpath)
    {
        $path = Util::normalizePath($path);
        $newpath = Util::normalizePath($newpath);
        $this->assertPresent($path);
        $this->assertAbsent($newpath);

        return $this->getAdapter()->copy($path, $newpath);
    }

    /**
     * @inheritdoc
     */
    public function delete($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        return $this->getAdapter()->delete($path);
    }

    /**
     * @inheritdoc
     */
    public function deleteDir($dirname)
    {
        $dirname = Util::normalizePath($dirname);

        if ($dirname === '') {
            throw new RootViolationException('Root directories can not be deleted.');
        }

        return (bool) $this->getAdapter()->deleteDir($dirname);
    }

    /**
     * @inheritdoc
     */
    public function createDir($dirname, array $config = [])
    {
        $dirname = Util::normalizePath($dirname);
        $config = $this->prepareConfig($config);

        return (bool) $this->getAdapter()->createDir($dirname, $config);
    }

    /**
     * @inheritdoc
     */
    public function listContents($directory = '', $recursive = false)
    {
        $directory = Util::normalizePath($directory);
        $contents = $this->getAdapter()->listContents($directory, $recursive);

        return (new ContentListingFormatter($directory, $recursive, $this->config->get('case_sensitive', true)))
            ->formatListing($contents);
    }

    /**
     * @inheritdoc
     */
    public function getMimetype($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        if (( ! $object = $this->getAdapter()->getMimetype($path)) || ! array_key_exists('mimetype', $object)) {
            return false;
        }

        return $object['mimetype'];
    }

    /**
     * @inheritdoc
     */
    public function getTimestamp($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        if (( ! $object = $this->getAdapter()->getTimestamp($path)) || ! array_key_exists('timestamp', $object)) {
            return false;
        }

        return (int) $object['timestamp'];
    }

    /**
     * @inheritdoc
     */
    public function getVisibility($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        if (( ! $object = $this->getAdapter()->getVisibility($path)) || ! array_key_exists('visibility', $object)) {
            return false;
        }

        return $object['visibility'];
    }

    /**
     * @inheritdoc
     */
    public function getSize($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        if (( ! $object = $this->getAdapter()->getSize($path)) || ! array_key_exists('size', $object)) {
            return false;
        }

        return (int) $object['size'];
    }

    /**
     * @inheritdoc
     */
    public function setVisibility($path, $visibility)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        return (bool) $this->getAdapter()->setVisibility($path, $visibility);
    }

    /**
     * @inheritdoc
     */
    public function getMetadata($path)
    {
        $path = Util::normalizePath($path);
        $this->assertPresent($path);

        return $this->getAdapter()->getMetadata($path);
    }

    /**
     * @inheritdoc
     */
    public function get($path, Handler $handler = null)
    {
        $path = Util::normalizePath($path);

        if ( ! $handler) {
            $metadata = $this->getMetadata($path);
            $handler = ($metadata && $metadata['type'] === 'file') ? new File($this, $path) : new Directory($this, $path);
        }

        $handler->setPath($path);
        $handler->setFilesystem($this);

        return $handler;
    }

    /**
     * Assert a file is present.
     *
     * @param string $path path to file
     *
     * @throws FileNotFoundException
     *
     * @return void
     */
    public function assertPresent($path)
    {
        if ($this->config->get('disable_asserts', false) === false && ! $this->has($path)) {
            throw new FileNotFoundException($path);
        }
    }

    /**
     * Assert a file is absent.
     *
     * @param string $path path to file
     *
     * @throws FileExistsException
     *
     * @return void
     */
    public function assertAbsent($path)
    {
        if ($this->config->get('disable_asserts', false) === false && $this->has($path)) {
            throw new FileExistsException($path);
        }
    }
}
<?php

namespace League\Flysystem;

/**
 * @deprecated
 */
class File extends Handler
{
    /**
     * Check whether the file exists.
     *
     * @return bool
     */
    public function exists()
    {
        return $this->filesystem->has($this->path);
    }

    /**
     * Read the file.
     *
     * @return string|false file contents
     */
    public function read()
    {
        return $this->filesystem->read($this->path);
    }

    /**
     * Read the file as a stream.
     *
     * @return resource|false file stream
     */
    public function readStream()
    {
        return $this->filesystem->readStream($this->path);
    }

    /**
     * Write the new file.
     *
     * @param string $content
     *
     * @return bool success boolean
     */
    public function write($content)
    {
        return $this->filesystem->write($this->path, $content);
    }

    /**
     * Write the new file using a stream.
     *
     * @param resource $resource
     *
     * @return bool success boolean
     */
    public function writeStream($resource)
    {
        return $this->filesystem->writeStream($this->path, $resource);
    }

    /**
     * Update the file contents.
     *
     * @param string $content
     *
     * @return bool success boolean
     */
    public function update($content)
    {
        return $this->filesystem->update($this->path, $content);
    }

    /**
     * Update the file contents with a stream.
     *
     * @param resource $resource
     *
     * @return bool success boolean
     */
    public function updateStream($resource)
    {
        return $this->filesystem->updateStream($this->path, $resource);
    }

    /**
     * Create the file or update if exists.
     *
     * @param string $content
     *
     * @return bool success boolean
     */
    public function put($content)
    {
        return $this->filesystem->put($this->path, $content);
    }

    /**
     * Create the file or update if exists using a stream.
     *
     * @param resource $resource
     *
     * @return bool success boolean
     */
    public function putStream($resource)
    {
        return $this->filesystem->putStream($this->path, $resource);
    }

    /**
     * Rename the file.
     *
     * @param string $newpath
     *
     * @return bool success boolean
     */
    public function rename($newpath)
    {
        if ($this->filesystem->rename($this->path, $newpath)) {
            $this->path = $newpath;

            return true;
        }

        return false;
    }

    /**
     * Copy the file.
     *
     * @param string $newpath
     *
     * @return File|false new file or false
     */
    public function copy($newpath)
    {
        if ($this->filesystem->copy($this->path, $newpath)) {
            return new File($this->filesystem, $newpath);
        }

        return false;
    }

    /**
     * Get the file's timestamp.
     *
     * @return string|false The timestamp or false on failure.
     */
    public function getTimestamp()
    {
        return $this->filesystem->getTimestamp($this->path);
    }

    /**
     * Get the file's mimetype.
     *
     * @return string|false The file mime-type or false on failure.
     */
    public function getMimetype()
    {
        return $this->filesystem->getMimetype($this->path);
    }

    /**
     * Get the file's visibility.
     *
     * @return string|false The visibility (public|private) or false on failure.
     */
    public function getVisibility()
    {
        return $this->filesystem->getVisibility($this->path);
    }

    /**
     * Get the file's metadata.
     *
     * @return array|false The file metadata or false on failure.
     */
    public function getMetadata()
    {
        return $this->filesystem->getMetadata($this->path);
    }

    /**
     * Get the file size.
     *
     * @return int|false The file size or false on failure.
     */
    public function getSize()
    {
        return $this->filesystem->getSize($this->path);
    }

    /**
     * Delete the file.
     *
     * @return bool success boolean
     */
    public function delete()
    {
        return $this->filesystem->delete($this->path);
    }
}
<?php

namespace League\Flysystem;

final class SafeStorage
{
    /**
     * @var string
     */
    private $hash;

    /**
     * @var array
     */
    protected static $safeStorage = [];

    public function __construct()
    {
        $this->hash = spl_object_hash($this);
        static::$safeStorage[$this->hash] = [];
    }

    public function storeSafely($key, $value)
    {
        static::$safeStorage[$this->hash][$key] = $value;
    }

    public function retrieveSafely($key)
    {
        if (array_key_exists($key, static::$safeStorage[$this->hash])) {
            return static::$safeStorage[$this->hash][$key];
        }
    }

    public function __destruct()
    {
        unset(static::$safeStorage[$this->hash]);
    }
}
<?php

namespace League\Flysystem\Plugin;

use LogicException;

class PluginNotFoundException extends LogicException
{
    // This exception doesn't require additional information.
}
<?php

namespace League\Flysystem\Plugin;

class ListWith extends AbstractPlugin
{
    /**
     * Get the method name.
     *
     * @return string
     */
    public function getMethod()
    {
        return 'listWith';
    }

    /**
     * List contents with metadata.
     *
     * @param array  $keys
     * @param string $directory
     * @param bool   $recursive
     *
     * @return array listing with metadata
     */
    public function handle(array $keys = [], $directory = '', $recursive = false)
    {
        $contents = $this->filesystem->listContents($directory, $recursive);

        foreach ($contents as $index => $object) {
            if ($object['type'] === 'file') {
                $missingKeys = array_diff($keys, array_keys($object));
                $contents[$index] = array_reduce($missingKeys, [$this, 'getMetadataByName'], $object);
            }
        }

        return $contents;
    }

    /**
     * Get a meta-data value by key name.
     *
     * @param array  $object
     * @param string $key
     *
     * @return array
     */
    protected function getMetadataByName(array $object, $key)
    {
        $method = 'get' . ucfirst($key);

        if ( ! method_exists($this->filesystem, $method)) {
            throw new \InvalidArgumentException('Could not get meta-data for key: ' . $key);
        }

        $object[$key] = $this->filesystem->{$method}($object['path']);

        return $object;
    }
}
<?php

namespace League\Flysystem\Plugin;

class EmptyDir extends AbstractPlugin
{
    /**
     * Get the method name.
     *
     * @return string
     */
    public function getMethod()
    {
        return 'emptyDir';
    }

    /**
     * Empty a directory's contents.
     *
     * @param string $dirname
     */
    public function handle($dirname)
    {
        $listing = $this->filesystem->listContents($dirname, false);

        foreach ($listing as $item) {
            if ($item['type'] === 'dir') {
                $this->filesystem->deleteDir($item['path']);
            } else {
                $this->filesystem->delete($item['path']);
            }
        }
    }
}
<?php

namespace League\Flysystem\Plugin;

use BadMethodCallException;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\PluginInterface;
use LogicException;

trait PluggableTrait
{
    /**
     * @var array
     */
    protected $plugins = [];

    /**
     * Register a plugin.
     *
     * @param PluginInterface $plugin
     *
     * @throws LogicException
     *
     * @return $this
     */
    public function addPlugin(PluginInterface $plugin)
    {
        if ( ! method_exists($plugin, 'handle')) {
            throw new LogicException(get_class($plugin) . ' does not have a handle method.');
        }

        $this->plugins[$plugin->getMethod()] = $plugin;

        return $this;
    }

    /**
     * Find a specific plugin.
     *
     * @param string $method
     *
     * @throws PluginNotFoundException
     *
     * @return PluginInterface
     */
    protected function findPlugin($method)
    {
        if ( ! isset($this->plugins[$method])) {
            throw new PluginNotFoundException('Plugin not found for method: ' . $method);
        }

        return $this->plugins[$method];
    }

    /**
     * Invoke a plugin by method name.
     *
     * @param string              $method
     * @param array               $arguments
     * @param FilesystemInterface $filesystem
     *
     * @throws PluginNotFoundException
     *
     * @return mixed
     */
    protected function invokePlugin($method, array $arguments, FilesystemInterface $filesystem)
    {
        $plugin = $this->findPlugin($method);
        $plugin->setFilesystem($filesystem);
        $callback = [$plugin, 'handle'];

        return call_user_func_array($callback, $arguments);
    }

    /**
     * Plugins pass-through.
     *
     * @param string $method
     * @param array  $arguments
     *
     * @throws BadMethodCallException
     *
     * @return mixed
     */
    public function __call($method, array $arguments)
    {
        try {
            return $this->invokePlugin($method, $arguments, $this);
        } catch (PluginNotFoundException $e) {
            throw new BadMethodCallException(
                'Call to undefined method '
                . get_class($this)
                . '::' . $method
            );
        }
    }
}
<?php

namespace League\Flysystem\Plugin;

use InvalidArgumentException;
use League\Flysystem\FileNotFoundException;

class GetWithMetadata extends AbstractPlugin
{
    /**
     * Get the method name.
     *
     * @return string
     */
    public function getMethod()
    {
        return 'getWithMetadata';
    }

    /**
     * Get metadata for an object with required metadata.
     *
     * @param string $path     path to file
     * @param array  $metadata metadata keys
     *
     * @throws InvalidArgumentException
     * @throws FileNotFoundException
     *
     * @return array|false metadata
     */
    public function handle($path, array $metadata)
    {
        $object = $this->filesystem->getMetadata($path);

        if ( ! $object) {
            return false;
        }

        $keys = array_diff($metadata, array_keys($object));

        foreach ($keys as $key) {
            if ( ! method_exists($this->filesystem, $method = 'get' . ucfirst($key))) {
                throw new InvalidArgumentException('Could not fetch metadata: ' . $key);
            }

            $object[$key] = $this->filesystem->{$method}($path);
        }

        return $object;
    }
}
<?php

namespace League\Flysystem\Plugin;

use League\Flysystem\FileExistsException;
use League\Flysystem\FileNotFoundException;

class ForcedRename extends AbstractPlugin
{
    /**
     * @inheritdoc
     */
    public function getMethod()
    {
        return 'forceRename';
    }

    /**
     * Renames a file, overwriting the destination if it exists.
     *
     * @param string $path    Path to the existing file.
     * @param string $newpath The new path of the file.
     *
     * @throws FileNotFoundException Thrown if $path does not exist.
     * @throws FileExistsException
     *
     * @return bool True on success, false on failure.
     */
    public function handle($path, $newpath)
    {
        try {
            $deleted = $this->filesystem->delete($newpath);
        } catch (FileNotFoundException $e) {
            // The destination path does not exist. That's ok.
            $deleted = true;
        }

        if ($deleted) {
            return $this->filesystem->rename($path, $newpath);
        }

        return false;
    }
}
<?php

namespace League\Flysystem\Plugin;

use League\Flysystem\FilesystemInterface;
use League\Flysystem\PluginInterface;

abstract class AbstractPlugin implements PluginInterface
{
    /**
     * @var FilesystemInterface
     */
    protected $filesystem;

    /**
     * Set the Filesystem object.
     *
     * @param FilesystemInterface $filesystem
     */
    public function setFilesystem(FilesystemInterface $filesystem)
    {
        $this->filesystem = $filesystem;
    }
}
<?php

namespace League\Flysystem\Plugin;

class ListPaths extends AbstractPlugin
{
    /**
     * Get the method name.
     *
     * @return string
     */
    public function getMethod()
    {
        return 'listPaths';
    }

    /**
     * List all paths.
     *
     * @param string $directory
     * @param bool   $recursive
     *
     * @return array paths
     */
    public function handle($directory = '', $recursive = false)
    {
        $result = [];
        $contents = $this->filesystem->listContents($directory, $recursive);

        foreach ($contents as $object) {
            $result[] = $object['path'];
        }

        return $result;
    }
}
<?php

namespace League\Flysystem\Plugin;

use League\Flysystem\FileExistsException;
use League\Flysystem\FileNotFoundException;

class ForcedCopy extends AbstractPlugin
{
    /**
     * @inheritdoc
     */
    public function getMethod()
    {
        return 'forceCopy';
    }

    /**
     * Copies a file, overwriting any existing files.
     *
     * @param string $path    Path to the existing file.
     * @param string $newpath The new path of the file.
     *
     * @throws FileExistsException
     * @throws FileNotFoundException Thrown if $path does not exist.
     *
     * @return bool True on success, false on failure.
     */
    public function handle($path, $newpath)
    {
        try {
            $deleted = $this->filesystem->delete($newpath);
        } catch (FileNotFoundException $e) {
            // The destination path does not exist. That's ok.
            $deleted = true;
        }

        if ($deleted) {
            return $this->filesystem->copy($path, $newpath);
        }

        return false;
    }
}
<?php

namespace League\Flysystem\Plugin;

class ListFiles extends AbstractPlugin
{
    /**
     * Get the method name.
     *
     * @return string
     */
    public function getMethod()
    {
        return 'listFiles';
    }

    /**
     * List all files in the directory.
     *
     * @param string $directory
     * @param bool   $recursive
     *
     * @return array
     */
    public function handle($directory = '', $recursive = false)
    {
        $contents = $this->filesystem->listContents($directory, $recursive);

        $filter = function ($object) {
            return $object['type'] === 'file';
        };

        return array_values(array_filter($contents, $filter));
    }
}
<?php

namespace League\Flysystem\Adapter;

use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
use League\Flysystem\Adapter\Polyfill\StreamedTrait;
use League\Flysystem\Config;

class NullAdapter extends AbstractAdapter
{
    use StreamedTrait;
    use StreamedCopyTrait;

    /**
     * Check whether a file is present.
     *
     * @param string $path
     *
     * @return bool
     */
    public function has($path)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function write($path, $contents, Config $config)
    {
        $type = 'file';
        $result = compact('contents', 'type', 'path');

        if ($visibility = $config->get('visibility')) {
            $result['visibility'] = $visibility;
        }

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function update($path, $contents, Config $config)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function read($path)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function rename($path, $newpath)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function delete($path)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function listContents($directory = '', $recursive = false)
    {
        return [];
    }

    /**
     * @inheritdoc
     */
    public function getMetadata($path)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function getSize($path)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function getMimetype($path)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function getTimestamp($path)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function getVisibility($path)
    {
        return false;
    }

    /**
     * @inheritdoc
     */
    public function setVisibility($path, $visibility)
    {
        return compact('visibility');
    }

    /**
     * @inheritdoc
     */
    public function createDir($dirname, Config $config)
    {
        return ['path' => $dirname, 'type' => 'dir'];
    }

    /**
     * @inheritdoc
     */
    public function deleteDir($dirname)
    {
        return false;
    }
}
<?php

namespace League\Flysystem\Adapter;

use DirectoryIterator;
use FilesystemIterator;
use finfo as Finfo;
use League\Flysystem\Config;
use League\Flysystem\Exception;
use League\Flysystem\NotSupportedException;
use League\Flysystem\UnreadableFileException;
use League\Flysystem\Util;
use LogicException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;

class Local extends AbstractAdapter
{
    /**
     * @var int
     */
    const SKIP_LINKS = 0001;

    /**
     * @var int
     */
    const DISALLOW_LINKS = 0002;

    /**
     * @var array
     */
    protected static $permissions = [
        'file' => [
            'public' => 0644,
            'private' => 0600,
        ],
        'dir' => [
            'public' => 0755,
            'private' => 0700,
        ],
    ];

    /**
     * @var string
     */
    protected $pathSeparator = DIRECTORY_SEPARATOR;

    /**
     * @var array
     */
    protected $permissionMap;

    /**
     * @var int
     */
    protected $writeFlags;

    /**
     * @var int
     */
    private $linkHandling;

    /**
     * Constructor.
     *
     * @param string $root
     * @param int    $writeFlags
     * @param int    $linkHandling
     * @param array  $permissions
     *
     * @throws LogicException
     */
    public function __construct($root, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS, array $permissions = [])
    {
        $root = is_link($root) ? realpath($root) : $root;
        $this->permissionMap = array_replace_recursive(static::$permissions, $permissions);
        $this->ensureDirectory($root);

        if ( ! is_dir($root) || ! is_readable($root)) {
            throw new LogicException('The root path ' . $root . ' is not readable.');
        }

        $this->setPathPrefix($root);
        $this->writeFlags = $writeFlags;
        $this->linkHandling = $linkHandling;
    }

    /**
     * Ensure the root directory exists.
     *
     * @param string $root root directory path
     *
     * @return void
     *
     * @throws Exception in case the root directory can not be created
     */
    protected function ensureDirectory($root)
    {
        if ( ! is_dir($root)) {
            $umask = umask(0);

            if ( ! @mkdir($root, $this->permissionMap['dir']['public'], true)) {
                $mkdirError = error_get_last();
            }

            umask($umask);
            clearstatcache(false, $root);

            if ( ! is_dir($root)) {
                $errorMessage = isset($mkdirError['message']) ? $mkdirError['message'] : '';
                throw new Exception(sprintf('Impossible to create the root directory "%s". %s', $root, $errorMessage));
            }
        }
    }

    /**
     * @inheritdoc
     */
    public function has($path)
    {
        $location = $this->applyPathPrefix($path);

        return file_exists($location);
    }

    /**
     * @inheritdoc
     */
    public function write($path, $contents, Config $config)
    {
        $location = $this->applyPathPrefix($path);
        $this->ensureDirectory(dirname($location));

        if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) {
            return false;
        }

        $type = 'file';
        $result = compact('contents', 'type', 'size', 'path');

        if ($visibility = $config->get('visibility')) {
            $result['visibility'] = $visibility;
            $this->setVisibility($path, $visibility);
        }

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function writeStream($path, $resource, Config $config)
    {
        $location = $this->applyPathPrefix($path);
        $this->ensureDirectory(dirname($location));
        $stream = fopen($location, 'w+b');

        if ( ! $stream || stream_copy_to_stream($resource, $stream) === false || ! fclose($stream)) {
            return false;
        }

        $type = 'file';
        $result = compact('type', 'path');

        if ($visibility = $config->get('visibility')) {
            $this->setVisibility($path, $visibility);
            $result['visibility'] = $visibility;
        }

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function readStream($path)
    {
        $location = $this->applyPathPrefix($path);
        $stream = fopen($location, 'rb');

        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
    }

    /**
     * @inheritdoc
     */
    public function updateStream($path, $resource, Config $config)
    {
        return $this->writeStream($path, $resource, $config);
    }

    /**
     * @inheritdoc
     */
    public function update($path, $contents, Config $config)
    {
        $location = $this->applyPathPrefix($path);
        $size = file_put_contents($location, $contents, $this->writeFlags);

        if ($size === false) {
            return false;
        }

        $type = 'file';

        $result = compact('type', 'path', 'size', 'contents');

        if ($mimetype = $config->get('mimetype') ?: Util::guessMimeType($path, $contents)) {
            $result['mimetype'] = $mimetype;
        }

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function read($path)
    {
        $location = $this->applyPathPrefix($path);
        $contents = @file_get_contents($location);

        if ($contents === false) {
            return false;
        }

        return ['type' => 'file', 'path' => $path, 'contents' => $contents];
    }

    /**
     * @inheritdoc
     */
    public function rename($path, $newpath)
    {
        $location = $this->applyPathPrefix($path);
        $destination = $this->applyPathPrefix($newpath);
        $parentDirectory = $this->applyPathPrefix(Util::dirname($newpath));
        $this->ensureDirectory($parentDirectory);

        return rename($location, $destination);
    }

    /**
     * @inheritdoc
     */
    public function copy($path, $newpath)
    {
        $location = $this->applyPathPrefix($path);
        $destination = $this->applyPathPrefix($newpath);
        $this->ensureDirectory(dirname($destination));

        return copy($location, $destination);
    }

    /**
     * @inheritdoc
     */
    public function delete($path)
    {
        $location = $this->applyPathPrefix($path);

        return @unlink($location);
    }

    /**
     * @inheritdoc
     */
    public function listContents($directory = '', $recursive = false)
    {
        $result = [];
        $location = $this->applyPathPrefix($directory);

        if ( ! is_dir($location)) {
            return [];
        }

        $iterator = $recursive ? $this->getRecursiveDirectoryIterator($location) : $this->getDirectoryIterator($location);

        foreach ($iterator as $file) {
            $path = $this->getFilePath($file);

            if (preg_match('#(^|/|\\\\)\.{1,2}$#', $path)) {
                continue;
            }

            $result[] = $this->normalizeFileInfo($file);
        }

        unset($iterator);

        return array_filter($result);
    }

    /**
     * @inheritdoc
     */
    public function getMetadata($path)
    {
        $location = $this->applyPathPrefix($path);
        clearstatcache(false, $location);
        $info = new SplFileInfo($location);

        return $this->normalizeFileInfo($info);
    }

    /**
     * @inheritdoc
     */
    public function getSize($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * @inheritdoc
     */
    public function getMimetype($path)
    {
        $location = $this->applyPathPrefix($path);
        $finfo = new Finfo(FILEINFO_MIME_TYPE);
        $mimetype = $finfo->file($location);

        if (in_array($mimetype, ['application/octet-stream', 'inode/x-empty', 'application/x-empty'])) {
            $mimetype = Util\MimeType::detectByFilename($location);
        }

        return ['path' => $path, 'type' => 'file', 'mimetype' => $mimetype];
    }

    /**
     * @inheritdoc
     */
    public function getTimestamp($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * @inheritdoc
     */
    public function getVisibility($path)
    {
        $location = $this->applyPathPrefix($path);
        clearstatcache(false, $location);
        $permissions = octdec(substr(sprintf('%o', fileperms($location)), -4));
        $type = is_dir($location) ? 'dir' : 'file';

        foreach ($this->permissionMap[$type] as $visibility => $visibilityPermissions) {
            if ($visibilityPermissions == $permissions) {
                return compact('path', 'visibility');
            }
        }

        $visibility = substr(sprintf('%o', fileperms($location)), -4);

        return compact('path', 'visibility');
    }

    /**
     * @inheritdoc
     */
    public function setVisibility($path, $visibility)
    {
        $location = $this->applyPathPrefix($path);
        $type = is_dir($location) ? 'dir' : 'file';
        $success = chmod($location, $this->permissionMap[$type][$visibility]);

        if ($success === false) {
            return false;
        }

        return compact('path', 'visibility');
    }

    /**
     * @inheritdoc
     */
    public function createDir($dirname, Config $config)
    {
        $location = $this->applyPathPrefix($dirname);
        $umask = umask(0);
        $visibility = $config->get('visibility', 'public');
        $return = ['path' => $dirname, 'type' => 'dir'];

        if ( ! is_dir($location)) {
            if (false === @mkdir($location, $this->permissionMap['dir'][$visibility], true)
                || false === is_dir($location)) {
                $return = false;
            }
        }

        umask($umask);

        return $return;
    }

    /**
     * @inheritdoc
     */
    public function deleteDir($dirname)
    {
        $location = $this->applyPathPrefix($dirname);

        if ( ! is_dir($location)) {
            return false;
        }

        $contents = $this->getRecursiveDirectoryIterator($location, RecursiveIteratorIterator::CHILD_FIRST);

        /** @var SplFileInfo $file */
        foreach ($contents as $file) {
            $this->guardAgainstUnreadableFileInfo($file);
            $this->deleteFileInfoObject($file);
        }

        unset($contents);

        return rmdir($location);
    }

    /**
     * @param SplFileInfo $file
     */
    protected function deleteFileInfoObject(SplFileInfo $file)
    {
        switch ($file->getType()) {
            case 'dir':
                rmdir($file->getRealPath());
                break;
            case 'link':
                unlink($file->getPathname());
                break;
            default:
                unlink($file->getRealPath());
        }
    }

    /**
     * Normalize the file info.
     *
     * @param SplFileInfo $file
     *
     * @return array|void
     *
     * @throws NotSupportedException
     */
    protected function normalizeFileInfo(SplFileInfo $file)
    {
        if ( ! $file->isLink()) {
            return $this->mapFileInfo($file);
        }

        if ($this->linkHandling & self::DISALLOW_LINKS) {
            throw NotSupportedException::forLink($file);
        }
    }

    /**
     * Get the normalized path from a SplFileInfo object.
     *
     * @param SplFileInfo $file
     *
     * @return string
     */
    protected function getFilePath(SplFileInfo $file)
    {
        $location = $file->getPathname();
        $path = $this->removePathPrefix($location);

        return trim(str_replace('\\', '/', $path), '/');
    }

    /**
     * @param string $path
     * @param int    $mode
     *
     * @return RecursiveIteratorIterator
     */
    protected function getRecursiveDirectoryIterator($path, $mode = RecursiveIteratorIterator::SELF_FIRST)
    {
        return new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
            $mode
        );
    }

    /**
     * @param string $path
     *
     * @return DirectoryIterator
     */
    protected function getDirectoryIterator($path)
    {
        $iterator = new DirectoryIterator($path);

        return $iterator;
    }

    /**
     * @param SplFileInfo $file
     *
     * @return array
     */
    protected function mapFileInfo(SplFileInfo $file)
    {
        $normalized = [
            'type' => $file->getType(),
            'path' => $this->getFilePath($file),
        ];

        $normalized['timestamp'] = $file->getMTime();

        if ($normalized['type'] === 'file') {
            $normalized['size'] = $file->getSize();
        }

        return $normalized;
    }

    /**
     * @param SplFileInfo $file
     *
     * @throws UnreadableFileException
     */
    protected function guardAgainstUnreadableFileInfo(SplFileInfo $file)
    {
        if ( ! $file->isReadable()) {
            throw UnreadableFileException::forFileInfo($file);
        }
    }
}
<?php

namespace League\Flysystem\Adapter;

class SynologyFtp extends Ftpd
{
    // This class merely exists because of BC.
}
<?php

namespace League\Flysystem\Adapter;

use DateTime;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Config;
use League\Flysystem\NotSupportedException;
use League\Flysystem\SafeStorage;
use RuntimeException;

abstract class AbstractFtpAdapter extends AbstractAdapter
{
    /**
     * @var mixed
     */
    protected $connection;

    /**
     * @var string
     */
    protected $host;

    /**
     * @var int
     */
    protected $port = 21;

    /**
     * @var bool
     */
    protected $ssl = false;

    /**
     * @var int
     */
    protected $timeout = 90;

    /**
     * @var bool
     */
    protected $passive = true;

    /**
     * @var string
     */
    protected $separator = '/';

    /**
     * @var string|null
     */
    protected $root;

    /**
     * @var int
     */
    protected $permPublic = 0744;

    /**
     * @var int
     */
    protected $permPrivate = 0700;

    /**
     * @var array
     */
    protected $configurable = [];

    /**
     * @var string
     */
    protected $systemType;

    /**
     * @var SafeStorage
     */
    protected $safeStorage;

    /**
     * True to enable timestamps for FTP servers that return unix-style listings.
     *
     * @var bool
     */
    protected $enableTimestampsOnUnixListings = false;

    /**
     * Constructor.
     *
     * @param array $config
     */
    public function __construct(array $config)
    {
        $this->safeStorage = new SafeStorage();
        $this->setConfig($config);
    }

    /**
     * Set the config.
     *
     * @param array $config
     *
     * @return $this
     */
    public function setConfig(array $config)
    {
        foreach ($this->configurable as $setting) {
            if ( ! isset($config[$setting])) {
                continue;
            }

            $method = 'set' . ucfirst($setting);

            if (method_exists($this, $method)) {
                $this->$method($config[$setting]);
            }
        }

        return $this;
    }

    /**
     * Returns the host.
     *
     * @return string
     */
    public function getHost()
    {
        return $this->host;
    }

    /**
     * Set the host.
     *
     * @param string $host
     *
     * @return $this
     */
    public function setHost($host)
    {
        $this->host = $host;

        return $this;
    }

    /**
     * Set the public permission value.
     *
     * @param int $permPublic
     *
     * @return $this
     */
    public function setPermPublic($permPublic)
    {
        $this->permPublic = $permPublic;

        return $this;
    }

    /**
     * Set the private permission value.
     *
     * @param int $permPrivate
     *
     * @return $this
     */
    public function setPermPrivate($permPrivate)
    {
        $this->permPrivate = $permPrivate;

        return $this;
    }

    /**
     * Returns the ftp port.
     *
     * @return int
     */
    public function getPort()
    {
        return $this->port;
    }

    /**
     * Returns the root folder to work from.
     *
     * @return string
     */
    public function getRoot()
    {
        return $this->root;
    }

    /**
     * Set the ftp port.
     *
     * @param int|string $port
     *
     * @return $this
     */
    public function setPort($port)
    {
        $this->port = (int) $port;

        return $this;
    }

    /**
     * Set the root folder to work from.
     *
     * @param string $root
     *
     * @return $this
     */
    public function setRoot($root)
    {
        $this->root = rtrim($root, '\\/') . $this->separator;

        return $this;
    }

    /**
     * Returns the ftp username.
     *
     * @return string username
     */
    public function getUsername()
    {
        $username = $this->safeStorage->retrieveSafely('username');

        return $username !== null ? $username : 'anonymous';
    }

    /**
     * Set ftp username.
     *
     * @param string $username
     *
     * @return $this
     */
    public function setUsername($username)
    {
        $this->safeStorage->storeSafely('username', $username);

        return $this;
    }

    /**
     * Returns the password.
     *
     * @return string password
     */
    public function getPassword()
    {
        return $this->safeStorage->retrieveSafely('password');
    }

    /**
     * Set the ftp password.
     *
     * @param string $password
     *
     * @return $this
     */
    public function setPassword($password)
    {
        $this->safeStorage->storeSafely('password', $password);

        return $this;
    }

    /**
     * Returns the amount of seconds before the connection will timeout.
     *
     * @return int
     */
    public function getTimeout()
    {
        return $this->timeout;
    }

    /**
     * Set the amount of seconds before the connection should timeout.
     *
     * @param int $timeout
     *
     * @return $this
     */
    public function setTimeout($timeout)
    {
        $this->timeout = (int) $timeout;

        return $this;
    }

    /**
     * Return the FTP system type.
     *
     * @return string
     */
    public function getSystemType()
    {
        return $this->systemType;
    }

    /**
     * Set the FTP system type (windows or unix).
     *
     * @param string $systemType
     *
     * @return $this
     */
    public function setSystemType($systemType)
    {
        $this->systemType = strtolower($systemType);

        return $this;
    }

    /**
     * True to enable timestamps for FTP servers that return unix-style listings.
     *
     * @param bool $bool
     *
     * @return $this
     */
    public function setEnableTimestampsOnUnixListings($bool = false)
    {
        $this->enableTimestampsOnUnixListings = $bool;

        return $this;
    }

    /**
     * @inheritdoc
     */
    public function listContents($directory = '', $recursive = false)
    {
        return $this->listDirectoryContents($directory, $recursive);
    }

    abstract protected function listDirectoryContents($directory, $recursive = false);

    /**
     * Normalize a directory listing.
     *
     * @param array  $listing
     * @param string $prefix
     *
     * @return array directory listing
     */
    protected function normalizeListing(array $listing, $prefix = '')
    {
        $base = $prefix;
        $result = [];
        $listing = $this->removeDotDirectories($listing);

        while ($item = array_shift($listing)) {
            if (preg_match('#^.*:$#', $item)) {
                $base = preg_replace('~^\./*|:$~', '', $item);
                continue;
            }

            $result[] = $this->normalizeObject($item, $base);
        }

        return $this->sortListing($result);
    }

    /**
     * Sort a directory listing.
     *
     * @param array $result
     *
     * @return array sorted listing
     */
    protected function sortListing(array $result)
    {
        $compare = function ($one, $two) {
            return strnatcmp($one['path'], $two['path']);
        };

        usort($result, $compare);

        return $result;
    }

    /**
     * Normalize a file entry.
     *
     * @param string $item
     * @param string $base
     *
     * @return array normalized file array
     *
     * @throws NotSupportedException
     */
    protected function normalizeObject($item, $base)
    {
        $systemType = $this->systemType ?: $this->detectSystemType($item);

        if ($systemType === 'unix') {
            return $this->normalizeUnixObject($item, $base);
        } elseif ($systemType === 'windows') {
            return $this->normalizeWindowsObject($item, $base);
        }

        throw NotSupportedException::forFtpSystemType($systemType);
    }

    /**
     * Normalize a Unix file entry.
     *
     * Given $item contains:
     *    '-rw-r--r--   1 ftp      ftp           409 Aug 19 09:01 file1.txt'
     *
     * This function will return:
     * [
     *   'type' => 'file',
     *   'path' => 'file1.txt',
     *   'visibility' => 'public',
     *   'size' => 409,
     *   'timestamp' => 1566205260
     * ]
     *
     * @param string $item
     * @param string $base
     *
     * @return array normalized file array
     */
    protected function normalizeUnixObject($item, $base)
    {
        $item = preg_replace('#\s+#', ' ', trim($item), 7);

        if (count(explode(' ', $item, 9)) !== 9) {
            throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
        }

        list($permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $timeOrYear, $name) = explode(' ', $item, 9);
        $type = $this->detectType($permissions);
        $path = $base === '' ? $name : $base . $this->separator . $name;

        if ($type === 'dir') {
            return compact('type', 'path');
        }

        $permissions = $this->normalizePermissions($permissions);
        $visibility = $permissions & 0044 ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
        $size = (int) $size;

        $result = compact('type', 'path', 'visibility', 'size');
        if ($this->enableTimestampsOnUnixListings) {
            $timestamp = $this->normalizeUnixTimestamp($month, $day, $timeOrYear);
            $result += compact('timestamp');
        }

        return $result;
    }

    /**
     * Only accurate to the minute (current year), or to the day.
     *
     * Inadequacies in timestamp accuracy are due to limitations of the FTP 'LIST' command
     *
     * Note: The 'MLSD' command is a machine-readable replacement for 'LIST'
     * but many FTP servers do not support it :(
     *
     * @param string $month      e.g. 'Aug'
     * @param string $day        e.g. '19'
     * @param string $timeOrYear e.g. '09:01' OR '2015'
     *
     * @return int
     */
    protected function normalizeUnixTimestamp($month, $day, $timeOrYear)
    {
        if (is_numeric($timeOrYear)) {
            $year = $timeOrYear;
            $hour = '00';
            $minute = '00';
            $seconds = '00';
        } else {
            $year = date('Y');
            list($hour, $minute) = explode(':', $timeOrYear);
            $seconds = '00';
        }
        $dateTime = DateTime::createFromFormat('Y-M-j-G:i:s', "{$year}-{$month}-{$day}-{$hour}:{$minute}:{$seconds}");

        return $dateTime->getTimestamp();
    }

    /**
     * Normalize a Windows/DOS file entry.
     *
     * @param string $item
     * @param string $base
     *
     * @return array normalized file array
     */
    protected function normalizeWindowsObject($item, $base)
    {
        $item = preg_replace('#\s+#', ' ', trim($item), 3);

        if (count(explode(' ', $item, 4)) !== 4) {
            throw new RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
        }

        list($date, $time, $size, $name) = explode(' ', $item, 4);
        $path = $base === '' ? $name : $base . $this->separator . $name;

        // Check for the correct date/time format
        $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';
        $dt = DateTime::createFromFormat($format, $date . $time);
        $timestamp = $dt ? $dt->getTimestamp() : (int) strtotime("$date $time");

        if ($size === '<DIR>') {
            $type = 'dir';

            return compact('type', 'path', 'timestamp');
        }

        $type = 'file';
        $visibility = AdapterInterface::VISIBILITY_PUBLIC;
        $size = (int) $size;

        return compact('type', 'path', 'visibility', 'size', 'timestamp');
    }

    /**
     * Get the system type from a listing item.
     *
     * @param string $item
     *
     * @return string the system type
     */
    protected function detectSystemType($item)
    {
        return preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item) ? 'windows' : 'unix';
    }

    /**
     * Get the file type from the permissions.
     *
     * @param string $permissions
     *
     * @return string file type
     */
    protected function detectType($permissions)
    {
        return substr($permissions, 0, 1) === 'd' ? 'dir' : 'file';
    }

    /**
     * Normalize a permissions string.
     *
     * @param string $permissions
     *
     * @return int
     */
    protected function normalizePermissions($permissions)
    {
        if (is_numeric($permissions)) {
            return ((int) $permissions) & 0777;
        }

        // remove the type identifier
        $permissions = substr($permissions, 1);

        // map the string rights to the numeric counterparts
        $map = ['-' => '0', 'r' => '4', 'w' => '2', 'x' => '1'];
        $permissions = strtr($permissions, $map);

        // split up the permission groups
        $parts = str_split($permissions, 3);

        // convert the groups
        $mapper = function ($part) {
            return array_sum(str_split($part));
        };

        // converts to decimal number
        return octdec(implode('', array_map($mapper, $parts)));
    }

    /**
     * Filter out dot-directories.
     *
     * @param array $list
     *
     * @return array
     */
    public function removeDotDirectories(array $list)
    {
        $filter = function ($line) {
            return $line !== '' && ! preg_match('#.* \.(\.)?$|^total#', $line);
        };

        return array_filter($list, $filter);
    }

    /**
     * @inheritdoc
     */
    public function has($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * @inheritdoc
     */
    public function getSize($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * @inheritdoc
     */
    public function getVisibility($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * Ensure a directory exists.
     *
     * @param string $dirname
     */
    public function ensureDirectory($dirname)
    {
        $dirname = (string) $dirname;

        if ($dirname !== '' && ! $this->has($dirname)) {
            $this->createDir($dirname, new Config());
        }
    }

    /**
     * @return mixed
     */
    public function getConnection()
    {
        $tries = 0;

        while ( ! $this->isConnected() && $tries < 3) {
            $tries++;
            $this->disconnect();
            $this->connect();
        }

        return $this->connection;
    }

    /**
     * Get the public permission value.
     *
     * @return int
     */
    public function getPermPublic()
    {
        return $this->permPublic;
    }

    /**
     * Get the private permission value.
     *
     * @return int
     */
    public function getPermPrivate()
    {
        return $this->permPrivate;
    }

    /**
     * Disconnect on destruction.
     */
    public function __destruct()
    {
        $this->disconnect();
    }

    /**
     * Establish a connection.
     */
    abstract public function connect();

    /**
     * Close the connection.
     */
    abstract public function disconnect();

    /**
     * Check if a connection is active.
     *
     * @return bool
     */
    abstract public function isConnected();
}
<?php

namespace League\Flysystem\Adapter;

use ErrorException;
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Config;
use League\Flysystem\ConnectionErrorException;
use League\Flysystem\ConnectionRuntimeException;
use League\Flysystem\InvalidRootException;
use League\Flysystem\Util;
use League\Flysystem\Util\MimeType;

class Ftp extends AbstractFtpAdapter
{
    use StreamedCopyTrait;

    /**
     * @var int
     */
    protected $transferMode = FTP_BINARY;

    /**
     * @var null|bool
     */
    protected $ignorePassiveAddress = null;

    /**
     * @var bool
     */
    protected $recurseManually = false;

    /**
     * @var bool
     */
    protected $utf8 = false;

    /**
     * @var array
     */
    protected $configurable = [
        'host',
        'port',
        'username',
        'password',
        'ssl',
        'timeout',
        'root',
        'permPrivate',
        'permPublic',
        'passive',
        'transferMode',
        'systemType',
        'ignorePassiveAddress',
        'recurseManually',
        'utf8',
        'enableTimestampsOnUnixListings',
    ];

    /**
     * @var bool
     */
    protected $isPureFtpd;

    /**
     * Set the transfer mode.
     *
     * @param int $mode
     *
     * @return $this
     */
    public function setTransferMode($mode)
    {
        $this->transferMode = $mode;

        return $this;
    }

    /**
     * Set if Ssl is enabled.
     *
     * @param bool $ssl
     *
     * @return $this
     */
    public function setSsl($ssl)
    {
        $this->ssl = (bool) $ssl;

        return $this;
    }

    /**
     * Set if passive mode should be used.
     *
     * @param bool $passive
     */
    public function setPassive($passive = true)
    {
        $this->passive = $passive;
    }

    /**
     * @param bool $ignorePassiveAddress
     */
    public function setIgnorePassiveAddress($ignorePassiveAddress)
    {
        $this->ignorePassiveAddress = $ignorePassiveAddress;
    }

    /**
     * @param bool $recurseManually
     */
    public function setRecurseManually($recurseManually)
    {
        $this->recurseManually = $recurseManually;
    }

    /**
     * @param bool $utf8
     */
    public function setUtf8($utf8)
    {
        $this->utf8 = (bool) $utf8;
    }

    /**
     * Connect to the FTP server.
     */
    public function connect()
    {
        if ($this->ssl) {
            $this->connection = @ftp_ssl_connect($this->getHost(), $this->getPort(), $this->getTimeout());
        } else {
            $this->connection = @ftp_connect($this->getHost(), $this->getPort(), $this->getTimeout());
        }

        if ( ! $this->connection) {
            throw new ConnectionRuntimeException('Could not connect to host: ' . $this->getHost() . ', port:' . $this->getPort());
        }

        $this->login();
        $this->setUtf8Mode();
        $this->setConnectionPassiveMode();
        $this->setConnectionRoot();
        $this->isPureFtpd = $this->isPureFtpdServer();
    }

    /**
     * Set the connection to UTF-8 mode.
     */
    protected function setUtf8Mode()
    {
        if ($this->utf8) {
            $response = ftp_raw($this->connection, "OPTS UTF8 ON");
            if (substr($response[0], 0, 3) !== '200') {
                throw new ConnectionRuntimeException(
                    'Could not set UTF-8 mode for connection: ' . $this->getHost() . '::' . $this->getPort()
                );
            }
        }
    }

    /**
     * Set the connections to passive mode.
     *
     * @throws ConnectionRuntimeException
     */
    protected function setConnectionPassiveMode()
    {
        if (is_bool($this->ignorePassiveAddress) && defined('FTP_USEPASVADDRESS')) {
            ftp_set_option($this->connection, FTP_USEPASVADDRESS, ! $this->ignorePassiveAddress);
        }

        if ( ! ftp_pasv($this->connection, $this->passive)) {
            throw new ConnectionRuntimeException(
                'Could not set passive mode for connection: ' . $this->getHost() . '::' . $this->getPort()
            );
        }
    }

    /**
     * Set the connection root.
     */
    protected function setConnectionRoot()
    {
        $root = $this->getRoot();
        $connection = $this->connection;

        if ($root && ! ftp_chdir($connection, $root)) {
            throw new InvalidRootException('Root is invalid or does not exist: ' . $this->getRoot());
        }

        // Store absolute path for further reference.
        // This is needed when creating directories and
        // initial root was a relative path, else the root
        // would be relative to the chdir'd path.
        $this->root = ftp_pwd($connection);
    }

    /**
     * Login.
     *
     * @throws ConnectionRuntimeException
     */
    protected function login()
    {
        set_error_handler(function () {
        });
        $isLoggedIn = ftp_login(
            $this->connection,
            $this->getUsername(),
            $this->getPassword()
        );
        restore_error_handler();

        if ( ! $isLoggedIn) {
            $this->disconnect();
            throw new ConnectionRuntimeException(
                'Could not login with connection: ' . $this->getHost() . '::' . $this->getPort(
                ) . ', username: ' . $this->getUsername()
            );
        }
    }

    /**
     * Disconnect from the FTP server.
     */
    public function disconnect()
    {
        if (is_resource($this->connection)) {
            @ftp_close($this->connection);
        }

        $this->connection = null;
    }

    /**
     * @inheritdoc
     */
    public function write($path, $contents, Config $config)
    {
        $stream = fopen('php://temp', 'w+b');
        fwrite($stream, $contents);
        rewind($stream);
        $result = $this->writeStream($path, $stream, $config);
        fclose($stream);

        if ($result === false) {
            return false;
        }

        $result['contents'] = $contents;
        $result['mimetype'] = $config->get('mimetype') ?: Util::guessMimeType($path, $contents);

        return $result;
    }

    /**
     * @inheritdoc
     */
    public function writeStream($path, $resource, Config $config)
    {
        $this->ensureDirectory(Util::dirname($path));

        if ( ! ftp_fput($this->getConnection(), $path, $resource, $this->transferMode)) {
            return false;
        }

        if ($visibility = $config->get('visibility')) {
            $this->setVisibility($path, $visibility);
        }

        $type = 'file';

        return compact('type', 'path', 'visibility');
    }

    /**
     * @inheritdoc
     */
    public function update($path, $contents, Config $config)
    {
        return $this->write($path, $contents, $config);
    }

    /**
     * @inheritdoc
     */
    public function updateStream($path, $resource, Config $config)
    {
        return $this->writeStream($path, $resource, $config);
    }

    /**
     * @inheritdoc
     */
    public function rename($path, $newpath)
    {
        return ftp_rename($this->getConnection(), $path, $newpath);
    }

    /**
     * @inheritdoc
     */
    public function delete($path)
    {
        return ftp_delete($this->getConnection(), $path);
    }

    /**
     * @inheritdoc
     */
    public function deleteDir($dirname)
    {
        $connection = $this->getConnection();
        $contents = array_reverse($this->listDirectoryContents($dirname, false));

        foreach ($contents as $object) {
            if ($object['type'] === 'file') {
                if ( ! ftp_delete($connection, $object['path'])) {
                    return false;
                }
            } elseif ( ! $this->deleteDir($object['path'])) {
                return false;
            }
        }

        return ftp_rmdir($connection, $dirname);
    }

    /**
     * @inheritdoc
     */
    public function createDir($dirname, Config $config)
    {
        $connection = $this->getConnection();
        $directories = explode('/', $dirname);

        foreach ($directories as $directory) {
            if (false === $this->createActualDirectory($directory, $connection)) {
                $this->setConnectionRoot();

                return false;
            }

            ftp_chdir($connection, $directory);
        }

        $this->setConnectionRoot();

        return ['type' => 'dir', 'path' => $dirname];
    }

    /**
     * Create a directory.
     *
     * @param string   $directory
     * @param resource $connection
     *
     * @return bool
     */
    protected function createActualDirectory($directory, $connection)
    {
        // List the current directory
        $listing = ftp_nlist($connection, '.') ?: [];

        foreach ($listing as $key => $item) {
            if (preg_match('~^\./.*~', $item)) {
                $listing[$key] = substr($item, 2);
            }
        }

        if (in_array($directory, $listing, true)) {
            return true;
        }

        return (boolean) ftp_mkdir($connection, $directory);
    }

    /**
     * @inheritdoc
     */
    public function getMetadata($path)
    {
        if ($path === '') {
            return ['type' => 'dir', 'path' => ''];
        }

        if (@ftp_chdir($this->getConnection(), $path) === true) {
            $this->setConnectionRoot();

            return ['type' => 'dir', 'path' => $path];
        }

        $listing = $this->ftpRawlist('-A', str_replace('*', '\\*', $path));

        if (empty($listing) || in_array('total 0', $listing, true)) {
            return false;
        }

        if (preg_match('/.* not found/', $listing[0])) {
            return false;
        }

        if (preg_match('/^total [0-9]*$/', $listing[0])) {
            array_shift($listing);
        }

        return $this->normalizeObject($listing[0], '');
    }

    /**
     * @inheritdoc
     */
    public function getMimetype($path)
    {
        if ( ! $metadata = $this->getMetadata($path)) {
            return false;
        }

        $metadata['mimetype'] = MimeType::detectByFilename($path);

        return $metadata;
    }

    /**
     * @inheritdoc
     */
    public function getTimestamp($path)
    {
        $timestamp = ftp_mdtm($this->getConnection(), $path);

        return ($timestamp !== -1) ? ['path' => $path, 'timestamp' => $timestamp] : false;
    }

    /**
     * @inheritdoc
     */
    public function read($path)
    {
        if ( ! $object = $this->readStream($path)) {
            return false;
        }

        $object['contents'] = stream_get_contents($object['stream']);
        fclose($object['stream']);
        unset($object['stream']);

        return $object;
    }

    /**
     * @inheritdoc
     */
    public function readStream($path)
    {
        $stream = fopen('php://temp', 'w+b');
        $result = ftp_fget($this->getConnection(), $stream, $path, $this->transferMode);
        rewind($stream);

        if ( ! $result) {
            fclose($stream);

            return false;
        }

        return ['type' => 'file', 'path' => $path, 'stream' => $stream];
    }

    /**
     * @inheritdoc
     */
    public function setVisibility($path, $visibility)
    {
        $mode = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? $this->getPermPublic() : $this->getPermPrivate();

        if ( ! ftp_chmod($this->getConnection(), $mode, $path)) {
            return false;
        }

        return compact('path', 'visibility');
    }

    /**
     * @inheritdoc
     *
     * @param string $directory
     */
    protected function listDirectoryContents($directory, $recursive = true)
    {
        $directory = str_replace('*', '\\*', $directory);

        if ($recursive && $this->recurseManually) {
            return $this->listDirectoryContentsRecursive($directory);
        }

        $options = $recursive ? '-alnR' : '-aln';
        $listing = $this->ftpRawlist($options, $directory);

        return $listing ? $this->normalizeListing($listing, $directory) : [];
    }

    /**
     * @inheritdoc
     *
     * @param string $directory
     */
    protected function listDirectoryContentsRecursive($directory)
    {
        $listing = $this->normalizeListing($this->ftpRawlist('-aln', $directory) ?: [], $directory);
        $output = [];

        foreach ($listing as $item) {
            $output[] = $item;
            if ($item['type'] !== 'dir') {
                continue;
            }
            $output = array_merge($output, $this->listDirectoryContentsRecursive($item['path']));
        }

        return $output;
    }

    /**
     * Check if the connection is open.
     *
     * @return bool
     *
     * @throws ConnectionErrorException
     */
    public function isConnected()
    {
        return is_resource($this->connection)
            && $this->getRawExecResponseCode('NOOP') === 200;
    }

    /**
     * @return bool
     */
    protected function isPureFtpdServer()
    {
        $response = ftp_raw($this->connection, 'HELP');

        return stripos(implode(' ', $response), 'Pure-FTPd') !== false;
    }

    /**
     * The ftp_rawlist function with optional escaping.
     *
     * @param string $options
     * @param string $path
     *
     * @return array
     */
    protected function ftpRawlist($options, $path)
    {
        $connection = $this->getConnection();

        if ($this->isPureFtpd) {
            $path = str_replace(' ', '\ ', $path);
        }

        return ftp_rawlist($connection, $options . ' ' . $path);
    }

    private function getRawExecResponseCode($command)
    {
        $response = @ftp_raw($this->connection, trim($command));

        return (int) preg_replace('/\D/', '', implode(' ', $response));
    }
}
<?php

namespace League\Flysystem\Adapter;

class Ftpd extends Ftp
{
    /**
     * @inheritdoc
     */
    public function getMetadata($path)
    {
        if ($path === '') {
            return ['type' => 'dir', 'path' => ''];
        }
        if (@ftp_chdir($this->getConnection(), $path) === true) {
            $this->setConnectionRoot();

            return ['type' => 'dir', 'path' => $path];
        }

        if ( ! ($object = ftp_raw($this->getConnection(), 'STAT ' . $path)) || count($object) < 3) {
            return false;
        }

        if (substr($object[1], 0, 5) === "ftpd:") {
            return false;
        }

        return $this->normalizeObject($object[1], '');
    }

    /**
     * @inheritdoc
     */
    protected function listDirectoryContents($directory, $recursive = true)
    {
        $listing = ftp_rawlist($this->getConnection(), $directory, $recursive);

        if ($listing === false || ( ! empty($listing) && substr($listing[0], 0, 5) === "ftpd:")) {
            return [];
        }

        return $this->normalizeListing($listing, $directory);
    }
}
<?php

namespace League\Flysystem\Adapter;

use League\Flysystem\AdapterInterface;

abstract class AbstractAdapter implements AdapterInterface
{
    /**
     * @var string|null path prefix
     */
    protected $pathPrefix;

    /**
     * @var string
     */
    protected $pathSeparator = '/';

    /**
     * Set the path prefix.
     *
     * @param string $prefix
     *
     * @return void
     */
    public function setPathPrefix($prefix)
    {
        $prefix = (string) $prefix;

        if ($prefix === '') {
            $this->pathPrefix = null;

            return;
        }

        $this->pathPrefix = rtrim($prefix, '\\/') . $this->pathSeparator;
    }

    /**
     * Get the path prefix.
     *
     * @return string|null path prefix or null if pathPrefix is empty
     */
    public function getPathPrefix()
    {
        return $this->pathPrefix;
    }

    /**
     * Prefix a path.
     *
     * @param string $path
     *
     * @return string prefixed path
     */
    public function applyPathPrefix($path)
    {
        return $this->getPathPrefix() . ltrim($path, '\\/');
    }

    /**
     * Remove a path prefix.
     *
     * @param string $path
     *
     * @return string path without the prefix
     */
    public function removePathPrefix($path)
    {
        return substr($path, strlen($this->getPathPrefix()));
    }
}
<?php

namespace League\Flysystem\Adapter\Polyfill;

use LogicException;

trait NotSupportingVisibilityTrait
{
    /**
     * Get the visibility of a file.
     *
     * @param string $path
     *
     * @throws LogicException
     */
    public function getVisibility($path)
    {
        throw new LogicException(get_class($this) . ' does not support visibility. Path: ' . $path);
    }

    /**
     * Set the visibility for a file.
     *
     * @param string $path
     * @param string $visibility
     *
     * @throws LogicException
     */
    public function setVisibility($path, $visibility)
    {
        throw new LogicException(get_class($this) . ' does not support visibility. Path: ' . $path . ', visibility: ' . $visibility);
    }
}
<?php

namespace League\Flysystem\Adapter\Polyfill;

trait StreamedTrait
{
    use StreamedReadingTrait;
    use StreamedWritingTrait;
}
<?php

namespace League\Flysystem\Adapter\Polyfill;

/**
 * A helper for adapters that only handle strings to provide read streams.
 */
trait StreamedReadingTrait
{
    /**
     * Reads a file as a stream.
     *
     * @param string $path
     *
     * @return array|false
     *
     * @see League\Flysystem\ReadInterface::readStream()
     */
    public function readStream($path)
    {
        if ( ! $data = $this->read($path)) {
            return false;
        }

        $stream = fopen('php://temp', 'w+b');
        fwrite($stream, $data['contents']);
        rewind($stream);
        $data['stream'] = $stream;
        unset($data['contents']);

        return $data;
    }

    /**
     * Reads a file.
     *
     * @param string $path
     *
     * @return array|false
     *
     * @see League\Flysystem\ReadInterface::read()
     */
    abstract public function read($path);
}
<?php

namespace League\Flysystem\Adapter\Polyfill;

use League\Flysystem\Config;

trait StreamedCopyTrait
{
    /**
     * Copy a file.
     *
     * @param string $path
     * @param string $newpath
     *
     * @return bool
     */
    public function copy($path, $newpath)
    {
        $response = $this->readStream($path);

        if ($response === false || ! is_resource($response['stream'])) {
            return false;
        }

        $result = $this->writeStream($newpath, $response['stream'], new Config());

        if ($result !== false && is_resource($response['stream'])) {
            fclose($response['stream']);
        }

        return $result !== false;
    }

    // Required abstract method

    /**
     * @param string $path
     *
     * @return resource
     */
    abstract public function readStream($path);

    /**
     * @param string   $path
     * @param resource $resource
     * @param Config   $config
     *
     * @return resource
     */
    abstract public function writeStream($path, $resource, Config $config);
}
<?php

namespace League\Flysystem\Adapter\Polyfill;

use League\Flysystem\Config;
use League\Flysystem\Util;

trait StreamedWritingTrait
{
    /**
     * Stream fallback delegator.
     *
     * @param string   $path
     * @param resource $resource
     * @param Config   $config
     * @param string   $fallback
     *
     * @return mixed fallback result
     */
    protected function stream($path, $resource, Config $config, $fallback)
    {
        Util::rewindStream($resource);
        $contents = stream_get_contents($resource);
        $fallbackCall = [$this, $fallback];

        return call_user_func($fallbackCall, $path, $contents, $config);
    }

    /**
     * Write using a stream.
     *
     * @param string   $path
     * @param resource $resource
     * @param Config   $config
     *
     * @return mixed false or file metadata
     */
    public function writeStream($path, $resource, Config $config)
    {
        return $this->stream($path, $resource, $config, 'write');
    }

    /**
     * Update a file using a stream.
     *
     * @param string   $path
     * @param resource $resource
     * @param Config   $config   Config object or visibility setting
     *
     * @return mixed false of file metadata
     */
    public function updateStream($path, $resource, Config $config)
    {
        return $this->stream($path, $resource, $config, 'update');
    }

    // Required abstract methods
    abstract public function write($pash, $contents, Config $config);
    abstract public function update($pash, $contents, Config $config);
}
<?php


namespace League\Flysystem\Adapter;

/**
 * Adapters that implement this interface let the Filesystem know that files can be overwritten using the write
 * functions and don't need the update function to be called. This can help improve performance when asserts are disabled.
 */
interface CanOverwriteFiles
{
}
<?php

namespace League\Flysystem;

use RuntimeException;

class InvalidRootException extends RuntimeException implements FilesystemException
{
}
<?php

namespace League\Flysystem;

class Config
{
    /**
     * @var array
     */
    protected $settings = [];

    /**
     * @var Config|null
     */
    protected $fallback;

    /**
     * Constructor.
     *
     * @param array $settings
     */
    public function __construct(array $settings = [])
    {
        $this->settings = $settings;
    }

    /**
     * Get a setting.
     *
     * @param string $key
     * @param mixed  $default
     *
     * @return mixed config setting or default when not found
     */
    public function get($key, $default = null)
    {
        if ( ! array_key_exists($key, $this->settings)) {
            return $this->getDefault($key, $default);
        }

        return $this->settings[$key];
    }

    /**
     * Check if an item exists by key.
     *
     * @param string $key
     *
     * @return bool
     */
    public function has($key)
    {
        if (array_key_exists($key, $this->settings)) {
            return true;
        }

        return $this->fallback instanceof Config
            ? $this->fallback->has($key)
            : false;
    }

    /**
     * Try to retrieve a default setting from a config fallback.
     *
     * @param string $key
     * @param mixed  $default
     *
     * @return mixed config setting or default when not found
     */
    protected function getDefault($key, $default)
    {
        if ( ! $this->fallback) {
            return $default;
        }

        return $this->fallback->get($key, $default);
    }

    /**
     * Set a setting.
     *
     * @param string $key
     * @param mixed  $value
     *
     * @return $this
     */
    public function set($key, $value)
    {
        $this->settings[$key] = $value;

        return $this;
    }

    /**
     * Set the fallback.
     *
     * @param Config $fallback
     *
     * @return $this
     */
    public function setFallback(Config $fallback)
    {
        $this->fallback = $fallback;

        return $this;
    }
}
<?php

namespace League\Flysystem;

interface AdapterInterface extends ReadInterface
{
    /**
     * @const  VISIBILITY_PUBLIC  public visibility
     */
    const VISIBILITY_PUBLIC = 'public';

    /**
     * @const  VISIBILITY_PRIVATE  private visibility
     */
    const VISIBILITY_PRIVATE = 'private';

    /**
     * Write a new file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config   Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function write($path, $contents, Config $config);

    /**
     * Write a new file using a stream.
     *
     * @param string   $path
     * @param resource $resource
     * @param Config   $config   Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function writeStream($path, $resource, Config $config);

    /**
     * Update a file.
     *
     * @param string $path
     * @param string $contents
     * @param Config $config   Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function update($path, $contents, Config $config);

    /**
     * Update a file using a stream.
     *
     * @param string   $path
     * @param resource $resource
     * @param Config   $config   Config object
     *
     * @return array|false false on failure file meta data on success
     */
    public function updateStream($path, $resource, Config $config);

    /**
     * Rename a file.
     *
     * @param string $path
     * @param string $newpath
     *
     * @return bool
     */
    public function rename($path, $newpath);

    /**
     * Copy a file.
     *
     * @param string $path
     * @param string $newpath
     *
     * @return bool
     */
    public function copy($path, $newpath);

    /**
     * Delete a file.
     *
     * @param string $path
     *
     * @return bool
     */
    public function delete($path);

    /**
     * Delete a directory.
     *
     * @param string $dirname
     *
     * @return bool
     */
    public function deleteDir($dirname);

    /**
     * Create a directory.
     *
     * @param string $dirname directory name
     * @param Config $config
     *
     * @return array|false
     */
    public function createDir($dirname, Config $config);

    /**
     * Set the visibility for a file.
     *
     * @param string $path
     * @param string $visibility
     *
     * @return array|false file meta data
     */
    public function setVisibility($path, $visibility);
}
<?php

namespace League\Flysystem;

use InvalidArgumentException;
use League\Flysystem\Plugin\PluggableTrait;
use League\Flysystem\Plugin\PluginNotFoundException;

/**
 * Class MountManager.
 *
 * Proxies methods to Filesystem (@see __call):
 *
 * @method AdapterInterface getAdapter($prefix)
 * @method Config getConfig($prefix)
 * @method array listFiles($directory = '', $recursive = false)
 * @method array listPaths($directory = '', $recursive = false)
 * @method array getWithMetadata($path, array $metadata)
 * @method Filesystem flushCache()
 * @method void assertPresent($path)
 * @method void assertAbsent($path)
 * @method Filesystem addPlugin(PluginInterface $plugin)
 *
 * @deprecated This functionality will be removed in 2.0
 */
class MountManager implements FilesystemInterface
{
    use PluggableTrait;

    /**
     * @var FilesystemInterface[]
     */
    protected $filesystems = [];

    /**
     * Constructor.
     *
     * @param FilesystemInterface[] $filesystems [:prefix => Filesystem,]
     *
     * @throws InvalidArgumentException
     */
    public function __construct(array $filesystems = [])
    {
        $this->mountFilesystems($filesystems);
    }

    /**
     * Mount filesystems.
     *
     * @param FilesystemInterface[] $filesystems [:prefix => Filesystem,]
     *
     * @throws InvalidArgumentException
     *
     * @return $this
     */
    public function mountFilesystems(array $filesystems)
    {
        foreach ($filesystems as $prefix => $filesystem) {
            $this->mountFilesystem($prefix, $filesystem);
        }

        return $this;
    }

    /**
     * Mount filesystems.
     *
     * @param string              $prefix
     * @param FilesystemInterface $filesystem
     *
     * @throws InvalidArgumentException
     *
     * @return $this
     */
    public function mountFilesystem($prefix, FilesystemInterface $filesystem)
    {
        if ( ! is_string($prefix)) {
            throw new InvalidArgumentException(__METHOD__ . ' expects argument #1 to be a string.');
        }

        $this->filesystems[$prefix] = $filesystem;

        return $this;
    }

    /**
     * Get the filesystem with the corresponding prefix.
     *
     * @param string $prefix
     *
     * @throws FilesystemNotFoundException
     *
     * @return FilesystemInterface
     */
    public function getFilesystem($prefix)
    {
        if ( ! isset($this->filesystems[$prefix])) {
            throw new FilesystemNotFoundException('No filesystem mounted with prefix ' . $prefix);
        }

        return $this->filesystems[$prefix];
    }

    /**
     * Retrieve the prefix from an arguments array.
     *
     * @param array $arguments
     *
     * @throws InvalidArgumentException
     *
     * @return array [:prefix, :arguments]
     */
    public function filterPrefix(array $arguments)
    {
        if (empty($arguments)) {
            throw new InvalidArgumentException('At least one argument needed');
        }

        $path = array_shift($arguments);

        if ( ! is_string($path)) {
            throw new InvalidArgumentException('First argument should be a string');
        }

        list($prefix, $path) = $this->getPrefixAndPath($path);
        array_unshift($arguments, $path);

        return [$prefix, $arguments];
    }

    /**
     * @param string $directory
     * @param bool   $recursive
     *
     * @throws InvalidArgumentException
     * @throws FilesystemNotFoundException
     *
     * @return array
     */
    public function listContents($directory = '', $recursive = false)
    {
        list($prefix, $directory) = $this->getPrefixAndPath($directory);
        $filesystem = $this->getFilesystem($prefix);
        $result = $filesystem->listContents($directory, $recursive);

        foreach ($result as &$file) {
            $file['filesystem'] = $prefix;
        }

        return $result;
    }

    /**
     * Call forwarder.
     *
     * @param string $method
     * @param array  $arguments
     *
     * @throws InvalidArgumentException
     * @throws FilesystemNotFoundException
     *
     * @return mixed
     */
    public function __call($method, $arguments)
    {
        list($prefix, $arguments) = $this->filterPrefix($arguments);

        return $this->invokePluginOnFilesystem($method, $arguments, $prefix);
    }

    /**
     * @param string $from
     * @param string $to
     * @param array  $config
     *
     * @throws InvalidArgumentException
     * @throws FilesystemNotFoundException
     * @throws FileExistsException
     *
     * @return bool
     */
    public function copy($from, $to, array $config = [])
    {
        list($prefixFrom, $from) = $this->getPrefixAndPath($from);

        $buffer = $this->getFilesystem($prefixFrom)->readStream($from);

        if ($buffer === false) {
            return false;
        }

        list($prefixTo, $to) = $this->getPrefixAndPath($to);

        $result = $this->getFilesystem($prefixTo)->writeStream($to, $buffer, $config);

        if (is_resource($buffer)) {
            fclose($buffer);
        }

        return $result;
    }

    /**
     * List with plugin adapter.
     *
     * @param array  $keys
     * @param string $directory
     * @param bool   $recursive
     *
     * @throws InvalidArgumentException
     * @throws FilesystemNotFoundException
     *
     * @return array
     */
    public function listWith(array $keys = [], $directory = '', $recursive = false)
    {
        list($prefix, $directory) = $this->getPrefixAndPath($directory);
        $arguments = [$keys, $directory, $recursive];

        return $this->invokePluginOnFilesystem('listWith', $arguments, $prefix);
    }

    /**
     * Move a file.
     *
     * @param string $from
     * @param string $to
     * @param array  $config
     *
     * @throws InvalidArgumentException
     * @throws FilesystemNotFoundException
     *
     * @return bool
     */
    public function move($from, $to, array $config = [])
    {
        list($prefixFrom, $pathFrom) = $this->getPrefixAndPath($from);
        list($prefixTo, $pathTo) = $this->getPrefixAndPath($to);

        if ($prefixFrom === $prefixTo) {
            $filesystem = $this->getFilesystem($prefixFrom);
            $renamed = $filesystem->rename($pathFrom, $pathTo);

            if ($renamed && isset($config['visibility'])) {
                return $filesystem->setVisibility($pathTo, $config['visibility']);
            }

            return $renamed;
        }

        $copied = $this->copy($from, $to, $config);

        if ($copied) {
            return $this->delete($from);
        }

        return false;
    }

    /**
     * Invoke a plugin on a filesystem mounted on a given prefix.
     *
     * @param string $method
     * @param array  $arguments
     * @param string $prefix
     *
     * @throws FilesystemNotFoundException
     *
     * @return mixed
     */
    public function invokePluginOnFilesystem($method, $arguments, $prefix)
    {
        $filesystem = $this->getFilesystem($prefix);

        try {
            return $this->invokePlugin($method, $arguments, $filesystem);
        } catch (PluginNotFoundException $e) {
            // Let it pass, it's ok, don't panic.
        }

        $callback = [$filesystem, $method];

        return call_user_func_array($callback, $arguments);
    }

    /**
     * @param string $path
     *
     * @throws InvalidArgumentException
     *
     * @return string[] [:prefix, :path]
     */
    protected function getPrefixAndPath($path)
    {
        if (strpos($path, '://') < 1) {
            throw new InvalidArgumentException('No prefix detected in path: ' . $path);
        }

        return explode('://', $path, 2);
    }

    /**
     * Check whether a file exists.
     *
     * @param string $path
     *
     * @return bool
     */
    public function has($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->has($path);
    }

    /**
     * Read a file.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The file contents or false on failure.
     */
    public function read($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->read($path);
    }

    /**
     * Retrieves a read-stream for a path.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return resource|false The path resource or false on failure.
     */
    public function readStream($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->readStream($path);
    }

    /**
     * Get a file's metadata.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return array|false The file metadata or false on failure.
     */
    public function getMetadata($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->getMetadata($path);
    }

    /**
     * Get a file's size.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return int|false The file size or false on failure.
     */
    public function getSize($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->getSize($path);
    }

    /**
     * Get a file's mime-type.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The file mime-type or false on failure.
     */
    public function getMimetype($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->getMimetype($path);
    }

    /**
     * Get a file's timestamp.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The timestamp or false on failure.
     */
    public function getTimestamp($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->getTimestamp($path);
    }

    /**
     * Get a file's visibility.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The visibility (public|private) or false on failure.
     */
    public function getVisibility($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->getVisibility($path);
    }

    /**
     * Write a new file.
     *
     * @param string $path     The path of the new file.
     * @param string $contents The file contents.
     * @param array  $config   An optional configuration array.
     *
     * @throws FileExistsException
     *
     * @return bool True on success, false on failure.
     */
    public function write($path, $contents, array $config = [])
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->write($path, $contents, $config);
    }

    /**
     * Write a new file using a stream.
     *
     * @param string   $path     The path of the new file.
     * @param resource $resource The file handle.
     * @param array    $config   An optional configuration array.
     *
     * @throws InvalidArgumentException If $resource is not a file handle.
     * @throws FileExistsException
     *
     * @return bool True on success, false on failure.
     */
    public function writeStream($path, $resource, array $config = [])
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->writeStream($path, $resource, $config);
    }

    /**
     * Update an existing file.
     *
     * @param string $path     The path of the existing file.
     * @param string $contents The file contents.
     * @param array  $config   An optional configuration array.
     *
     * @throws FileNotFoundException
     *
     * @return bool True on success, false on failure.
     */
    public function update($path, $contents, array $config = [])
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->update($path, $contents, $config);
    }

    /**
     * Update an existing file using a stream.
     *
     * @param string   $path     The path of the existing file.
     * @param resource $resource The file handle.
     * @param array    $config   An optional configuration array.
     *
     * @throws InvalidArgumentException If $resource is not a file handle.
     * @throws FileNotFoundException
     *
     * @return bool True on success, false on failure.
     */
    public function updateStream($path, $resource, array $config = [])
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->updateStream($path, $resource, $config);
    }

    /**
     * Rename a file.
     *
     * @param string $path    Path to the existing file.
     * @param string $newpath The new path of the file.
     *
     * @throws FileExistsException   Thrown if $newpath exists.
     * @throws FileNotFoundException Thrown if $path does not exist.
     *
     * @return bool True on success, false on failure.
     */
    public function rename($path, $newpath)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->rename($path, $newpath);
    }

    /**
     * Delete a file.
     *
     * @param string $path
     *
     * @throws FileNotFoundException
     *
     * @return bool True on success, false on failure.
     */
    public function delete($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->delete($path);
    }

    /**
     * Delete a directory.
     *
     * @param string $dirname
     *
     * @throws RootViolationException Thrown if $dirname is empty.
     *
     * @return bool True on success, false on failure.
     */
    public function deleteDir($dirname)
    {
        list($prefix, $dirname) = $this->getPrefixAndPath($dirname);

        return $this->getFilesystem($prefix)->deleteDir($dirname);
    }

    /**
     * Create a directory.
     *
     * @param string $dirname The name of the new directory.
     * @param array  $config  An optional configuration array.
     *
     * @return bool True on success, false on failure.
     */
    public function createDir($dirname, array $config = [])
    {
        list($prefix, $dirname) = $this->getPrefixAndPath($dirname);

        return $this->getFilesystem($prefix)->createDir($dirname);
    }

    /**
     * Set the visibility for a file.
     *
     * @param string $path       The path to the file.
     * @param string $visibility One of 'public' or 'private'.
     *
     * @throws FileNotFoundException
     *
     * @return bool True on success, false on failure.
     */
    public function setVisibility($path, $visibility)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->setVisibility($path, $visibility);
    }

    /**
     * Create a file or update if exists.
     *
     * @param string $path     The path to the file.
     * @param string $contents The file contents.
     * @param array  $config   An optional configuration array.
     *
     * @return bool True on success, false on failure.
     */
    public function put($path, $contents, array $config = [])
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->put($path, $contents, $config);
    }

    /**
     * Create a file or update if exists.
     *
     * @param string   $path     The path to the file.
     * @param resource $resource The file handle.
     * @param array    $config   An optional configuration array.
     *
     * @throws InvalidArgumentException Thrown if $resource is not a resource.
     *
     * @return bool True on success, false on failure.
     */
    public function putStream($path, $resource, array $config = [])
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->putStream($path, $resource, $config);
    }

    /**
     * Read and delete a file.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The file contents, or false on failure.
     */
    public function readAndDelete($path)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->readAndDelete($path);
    }

    /**
     * Get a file/directory handler.
     *
     * @deprecated
     *
     * @param string  $path    The path to the file.
     * @param Handler $handler An optional existing handler to populate.
     *
     * @return Handler Either a file or directory handler.
     */
    public function get($path, Handler $handler = null)
    {
        list($prefix, $path) = $this->getPrefixAndPath($path);

        return $this->getFilesystem($prefix)->get($path);
    }
}
<?php

namespace League\Flysystem;

interface PluginInterface
{
    /**
     * Get the method name.
     *
     * @return string
     */
    public function getMethod();

    /**
     * Set the Filesystem object.
     *
     * @param FilesystemInterface $filesystem
     */
    public function setFilesystem(FilesystemInterface $filesystem);
}
<?php

namespace League\Flysystem;

/**
 * @deprecated
 */
class Directory extends Handler
{
    /**
     * Delete the directory.
     *
     * @return bool
     */
    public function delete()
    {
        return $this->filesystem->deleteDir($this->path);
    }

    /**
     * List the directory contents.
     *
     * @param bool $recursive
     *
     * @return array|bool directory contents or false
     */
    public function getContents($recursive = false)
    {
        return $this->filesystem->listContents($this->path, $recursive);
    }
}
<?php

namespace League\Flysystem;

use Exception as BaseException;

class FileExistsException extends Exception
{
    /**
     * @var string
     */
    protected $path;

    /**
     * Constructor.
     *
     * @param string        $path
     * @param int           $code
     * @param BaseException $previous
     */
    public function __construct($path, $code = 0, BaseException $previous = null)
    {
        $this->path = $path;

        parent::__construct('File already exists at path: ' . $this->getPath(), $code, $previous);
    }

    /**
     * Get the path which was found.
     *
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }
}
<?php

namespace League\Flysystem;

use InvalidArgumentException;

interface FilesystemInterface
{
    /**
     * Check whether a file exists.
     *
     * @param string $path
     *
     * @return bool
     */
    public function has($path);

    /**
     * Read a file.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The file contents or false on failure.
     */
    public function read($path);

    /**
     * Retrieves a read-stream for a path.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return resource|false The path resource or false on failure.
     */
    public function readStream($path);

    /**
     * List contents of a directory.
     *
     * @param string $directory The directory to list.
     * @param bool   $recursive Whether to list recursively.
     *
     * @return array A list of file metadata.
     */
    public function listContents($directory = '', $recursive = false);

    /**
     * Get a file's metadata.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return array|false The file metadata or false on failure.
     */
    public function getMetadata($path);

    /**
     * Get a file's size.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return int|false The file size or false on failure.
     */
    public function getSize($path);

    /**
     * Get a file's mime-type.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The file mime-type or false on failure.
     */
    public function getMimetype($path);

    /**
     * Get a file's timestamp.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return int|false The timestamp or false on failure.
     */
    public function getTimestamp($path);

    /**
     * Get a file's visibility.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The visibility (public|private) or false on failure.
     */
    public function getVisibility($path);

    /**
     * Write a new file.
     *
     * @param string $path     The path of the new file.
     * @param string $contents The file contents.
     * @param array  $config   An optional configuration array.
     *
     * @throws FileExistsException
     *
     * @return bool True on success, false on failure.
     */
    public function write($path, $contents, array $config = []);

    /**
     * Write a new file using a stream.
     *
     * @param string   $path     The path of the new file.
     * @param resource $resource The file handle.
     * @param array    $config   An optional configuration array.
     *
     * @throws InvalidArgumentException If $resource is not a file handle.
     * @throws FileExistsException
     *
     * @return bool True on success, false on failure.
     */
    public function writeStream($path, $resource, array $config = []);

    /**
     * Update an existing file.
     *
     * @param string $path     The path of the existing file.
     * @param string $contents The file contents.
     * @param array  $config   An optional configuration array.
     *
     * @throws FileNotFoundException
     *
     * @return bool True on success, false on failure.
     */
    public function update($path, $contents, array $config = []);

    /**
     * Update an existing file using a stream.
     *
     * @param string   $path     The path of the existing file.
     * @param resource $resource The file handle.
     * @param array    $config   An optional configuration array.
     *
     * @throws InvalidArgumentException If $resource is not a file handle.
     * @throws FileNotFoundException
     *
     * @return bool True on success, false on failure.
     */
    public function updateStream($path, $resource, array $config = []);

    /**
     * Rename a file.
     *
     * @param string $path    Path to the existing file.
     * @param string $newpath The new path of the file.
     *
     * @throws FileExistsException   Thrown if $newpath exists.
     * @throws FileNotFoundException Thrown if $path does not exist.
     *
     * @return bool True on success, false on failure.
     */
    public function rename($path, $newpath);

    /**
     * Copy a file.
     *
     * @param string $path    Path to the existing file.
     * @param string $newpath The new path of the file.
     *
     * @throws FileExistsException   Thrown if $newpath exists.
     * @throws FileNotFoundException Thrown if $path does not exist.
     *
     * @return bool True on success, false on failure.
     */
    public function copy($path, $newpath);

    /**
     * Delete a file.
     *
     * @param string $path
     *
     * @throws FileNotFoundException
     *
     * @return bool True on success, false on failure.
     */
    public function delete($path);

    /**
     * Delete a directory.
     *
     * @param string $dirname
     *
     * @throws RootViolationException Thrown if $dirname is empty.
     *
     * @return bool True on success, false on failure.
     */
    public function deleteDir($dirname);

    /**
     * Create a directory.
     *
     * @param string $dirname The name of the new directory.
     * @param array  $config  An optional configuration array.
     *
     * @return bool True on success, false on failure.
     */
    public function createDir($dirname, array $config = []);

    /**
     * Set the visibility for a file.
     *
     * @param string $path       The path to the file.
     * @param string $visibility One of 'public' or 'private'.
     *
     * @throws FileNotFoundException
     *
     * @return bool True on success, false on failure.
     */
    public function setVisibility($path, $visibility);

    /**
     * Create a file or update if exists.
     *
     * @param string $path     The path to the file.
     * @param string $contents The file contents.
     * @param array  $config   An optional configuration array.
     *
     * @return bool True on success, false on failure.
     */
    public function put($path, $contents, array $config = []);

    /**
     * Create a file or update if exists.
     *
     * @param string   $path     The path to the file.
     * @param resource $resource The file handle.
     * @param array    $config   An optional configuration array.
     *
     * @throws InvalidArgumentException Thrown if $resource is not a resource.
     *
     * @return bool True on success, false on failure.
     */
    public function putStream($path, $resource, array $config = []);

    /**
     * Read and delete a file.
     *
     * @param string $path The path to the file.
     *
     * @throws FileNotFoundException
     *
     * @return string|false The file contents, or false on failure.
     */
    public function readAndDelete($path);

    /**
     * Get a file/directory handler.
     *
     * @deprecated
     *
     * @param string  $path    The path to the file.
     * @param Handler $handler An optional existing handler to populate.
     *
     * @return Handler Either a file or directory handler.
     */
    public function get($path, Handler $handler = null);

    /**
     * Register a plugin.
     *
     * @param PluginInterface $plugin The plugin to register.
     *
     * @return $this
     */
    public function addPlugin(PluginInterface $plugin);
}
<?php

namespace League\Flysystem;

use League\Flysystem\Util\MimeType;
use LogicException;

class Util
{
    /**
     * Get normalized pathinfo.
     *
     * @param string $path
     *
     * @return array pathinfo
     */
    public static function pathinfo($path)
    {
        $pathinfo = compact('path');

        if ('' !== $dirname = dirname($path)) {
            $pathinfo['dirname'] = static::normalizeDirname($dirname);
        }

        $pathinfo['basename'] = static::basename($path);

        $pathinfo += pathinfo($pathinfo['basename']);

        return $pathinfo + ['dirname' => ''];
    }

    /**
     * Normalize a dirname return value.
     *
     * @param string $dirname
     *
     * @return string normalized dirname
     */
    public static function normalizeDirname($dirname)
    {
        return $dirname === '.' ? '' : $dirname;
    }

    /**
     * Get a normalized dirname from a path.
     *
     * @param string $path
     *
     * @return string dirname
     */
    public static function dirname($path)
    {
        return static::normalizeDirname(dirname($path));
    }

    /**
     * Map result arrays.
     *
     * @param array $object
     * @param array $map
     *
     * @return array mapped result
     */
    public static function map(array $object, array $map)
    {
        $result = [];

        foreach ($map as $from => $to) {
            if ( ! isset($object[$from])) {
                continue;
            }

            $result[$to] = $object[$from];
        }

        return $result;
    }

    /**
     * Normalize path.
     *
     * @param string $path
     *
     * @throws LogicException
     *
     * @return string
     */
    public static function normalizePath($path)
    {
        return static::normalizeRelativePath($path);
    }

    /**
     * Normalize relative directories in a path.
     *
     * @param string $path
     *
     * @throws LogicException
     *
     * @return string
     */
    public static function normalizeRelativePath($path)
    {
        $path = str_replace('\\', '/', $path);
        $path = static::removeFunkyWhiteSpace($path);

        $parts = [];

        foreach (explode('/', $path) as $part) {
            switch ($part) {
                case '':
                case '.':
                break;

            case '..':
                if (empty($parts)) {
                    throw new LogicException(
                        'Path is outside of the defined root, path: [' . $path . ']'
                    );
                }
                array_pop($parts);
                break;

            default:
                $parts[] = $part;
                break;
            }
        }

        return implode('/', $parts);
    }

    /**
     * Removes unprintable characters and invalid unicode characters.
     *
     * @param string $path
     *
     * @return string $path
     */
    protected static function removeFunkyWhiteSpace($path)
    {
        // We do this check in a loop, since removing invalid unicode characters
        // can lead to new characters being created.
        while (preg_match('#\p{C}+|^\./#u', $path)) {
            $path = preg_replace('#\p{C}+|^\./#u', '', $path);
        }

        return $path;
    }

    /**
     * Normalize prefix.
     *
     * @param string $prefix
     * @param string $separator
     *
     * @return string normalized path
     */
    public static function normalizePrefix($prefix, $separator)
    {
        return rtrim($prefix, $separator) . $separator;
    }

    /**
     * Get content size.
     *
     * @param string $contents
     *
     * @return int content size
     */
    public static function contentSize($contents)
    {
        return defined('MB_OVERLOAD_STRING') ? mb_strlen($contents, '8bit') : strlen($contents);
    }

    /**
     * Guess MIME Type based on the path of the file and it's content.
     *
     * @param string          $path
     * @param string|resource $content
     *
     * @return string|null MIME Type or NULL if no extension detected
     */
    public static function guessMimeType($path, $content)
    {
        $mimeType = MimeType::detectByContent($content);

        if ( ! (empty($mimeType) || in_array($mimeType, ['application/x-empty', 'text/plain', 'text/x-asm']))) {
            return $mimeType;
        }

        return MimeType::detectByFilename($path);
    }

    /**
     * Emulate directories.
     *
     * @param array $listing
     *
     * @return array listing with emulated directories
     */
    public static function emulateDirectories(array $listing)
    {
        $directories = [];
        $listedDirectories = [];

        foreach ($listing as $object) {
            list($directories, $listedDirectories) = static::emulateObjectDirectories($object, $directories, $listedDirectories);
        }

        $directories = array_diff(array_unique($directories), array_unique($listedDirectories));

        foreach ($directories as $directory) {
            $listing[] = static::pathinfo($directory) + ['type' => 'dir'];
        }

        return $listing;
    }

    /**
     * Ensure a Config instance.
     *
     * @param null|array|Config $config
     *
     * @return Config config instance
     *
     * @throw  LogicException
     */
    public static function ensureConfig($config)
    {
        if ($config === null) {
            return new Config();
        }

        if ($config instanceof Config) {
            return $config;
        }

        if (is_array($config)) {
            return new Config($config);
        }

        throw new LogicException('A config should either be an array or a Flysystem\Config object.');
    }

    /**
     * Rewind a stream.
     *
     * @param resource $resource
     */
    public static function rewindStream($resource)
    {
        if (ftell($resource) !== 0 && static::isSeekableStream($resource)) {
            rewind($resource);
        }
    }

    public static function isSeekableStream($resource)
    {
        $metadata = stream_get_meta_data($resource);

        return $metadata['seekable'];
    }

    /**
     * Get the size of a stream.
     *
     * @param resource $resource
     *
     * @return int|null stream size
     */
    public static function getStreamSize($resource)
    {
        $stat = fstat($resource);

        if ( ! is_array($stat) || ! isset($stat['size'])) {
            return null;
        }

        return $stat['size'];
    }

    /**
     * Emulate the directories of a single object.
     *
     * @param array $object
     * @param array $directories
     * @param array $listedDirectories
     *
     * @return array
     */
    protected static function emulateObjectDirectories(array $object, array $directories, array $listedDirectories)
    {
        if ($object['type'] === 'dir') {
            $listedDirectories[] = $object['path'];
        }

        if ( ! isset($object['dirname']) || trim($object['dirname']) === '') {
            return [$directories, $listedDirectories];
        }

        $parent = $object['dirname'];

        while (isset($parent) && trim($parent) !== '' && ! in_array($parent, $directories)) {
            $directories[] = $parent;
            $parent = static::dirname($parent);
        }

        if (isset($object['type']) && $object['type'] === 'dir') {
            $listedDirectories[] = $object['path'];

            return [$directories, $listedDirectories];
        }

        return [$directories, $listedDirectories];
    }

    /**
     * Returns the trailing name component of the path.
     *
     * @param string $path
     *
     * @return string
     */
    private static function basename($path)
    {
        $separators = DIRECTORY_SEPARATOR === '/' ? '/' : '\/';

        $path = rtrim($path, $separators);

        $basename = preg_replace('#.*?([^' . preg_quote($separators, '#') . ']+$)#', '$1', $path);

        if (DIRECTORY_SEPARATOR === '/') {
            return $basename;
        }
        // @codeCoverageIgnoreStart
        // Extra Windows path munging. This is tested via AppVeyor, but code
        // coverage is not reported.

        // Handle relative paths with drive letters. c:file.txt.
        while (preg_match('#^[a-zA-Z]{1}:[^\\\/]#', $basename)) {
            $basename = substr($basename, 2);
        }

        // Remove colon for standalone drive letter names.
        if (preg_match('#^[a-zA-Z]{1}:$#', $basename)) {
            $basename = rtrim($basename, ':');
        }

        return $basename;
        // @codeCoverageIgnoreEnd
    }
}
<?php

namespace League\Flysystem;

use Exception as BaseException;

class FileNotFoundException extends Exception
{
    /**
     * @var string
     */
    protected $path;

    /**
     * Constructor.
     *
     * @param string     $path
     * @param int        $code
     * @param \Exception $previous
     */
    public function __construct($path, $code = 0, BaseException $previous = null)
    {
        $this->path = $path;

        parent::__construct('File not found at path: ' . $this->getPath(), $code, $previous);
    }

    /**
     * Get the path which was not found.
     *
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }
}
<?php

namespace League\Flysystem;

use RuntimeException;
use SplFileInfo;

class NotSupportedException extends RuntimeException implements FilesystemException
{
    /**
     * Create a new exception for a link.
     *
     * @param SplFileInfo $file
     *
     * @return static
     */
    public static function forLink(SplFileInfo $file)
    {
        $message = 'Links are not supported, encountered link at ';

        return new static($message . $file->getPathname());
    }

    /**
     * Create a new exception for a link.
     *
     * @param string $systemType
     *
     * @return static
     */
    public static function forFtpSystemType($systemType)
    {
        $message = "The FTP system type '$systemType' is currently not supported.";

        return new static($message);
    }
}
<?php

namespace League\Flysystem;

class Exception extends \Exception implements FilesystemException
{
    //
}
<?php

namespace League\Flysystem;

use LogicException;

class RootViolationException extends LogicException implements FilesystemException
{
    //
}
#!/bin/sh

# where's the source files?
SRC='src'

# for what branch to trigger
BRANCH='master'

# github repo
REPO='splitbrain/php-archive'

# ---- About -------------------------------------------------------
#
# This script use apigen to generate the documentation for the
# repository configured above. When run locally, the documentation
# will be placed in the 'docs' folder.
# However this script can also be run from travis. This requires
# the setup of a secret token as described at http://bit.ly/1MNbPn0
#
# Additional configuration can be done within an apigen.neon file
#
# ---- no modifications below ---------------------------------------

# when on travis, build outside of repository, otherwise locally
if [ -z "$TRAVIS" ]; then
    DST='docs'
else
    DST='../gh-pages'
    if [ "$TRAVIS_PHP_VERSION"  != '5.6'     ]; then exit; fi
    if [ "$TRAVIS_BRANCH"       != "$BRANCH" ]; then exit; fi
    if [ "$TRAVIS_PULL_REQUEST" != 'false'   ]; then exit; fi
    if [ -z "$GH_TOKEN"                      ]; then
        echo "GH_TOKEN not set! See: http://bit.ly/1MNbPn0"
        exit
    fi
fi

# Get ApiGen.phar
wget http://www.apigen.org/apigen.phar -O apigen.phar

# Generate SDK Docs
php apigen.phar generate --template-theme="bootstrap" -s $SRC -d $DST


### if we're not on travis, we're done
if [ -z "$TRAVIS" ]; then exit; fi

# go to the generated docs
cd $DST || exit

# Set identity
git config --global user.email "travis@travis-ci.org"
git config --global user.name "Travis"

# Add branch
git init
git remote add origin https://${GH_TOKEN}@github.com/${REPO}.git > /dev/null
git checkout -B gh-pages

# Push generated files
git add .
git commit -m "Docs updated by Travis"
git push origin gh-pages -fq > /dev/null
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false">
    <testsuites>
        <testsuite name="Test Suite">
            <directory suffix=".php">./tests/</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="false">
            <directory suffix=".php">src</directory>
        </whitelist>
    </filter>
</phpunit>
Copyright (c) 2015 Andreas Gohr <gohr@cosmocode.de>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.<?php /** @noinspection PhpUnhandledExceptionInspection */

namespace splitbrain\PHPArchive;

use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;

class TarTestCase extends TestCase
{
    /** @var int callback counter */
    protected $counter = 0;

    /**
     * file extensions that several tests use
     */
    protected $extensions = array('tar');

    /** @inheritdoc */
    protected function setUp()
    {
        parent::setUp();
        if (extension_loaded('zlib')) {
            $this->extensions[] = 'tgz';
            $this->extensions[] = 'tar.gz';
        }
        if (extension_loaded('bz2')) {
            $this->extensions[] = 'tbz';
            $this->extensions[] = 'tar.bz2';
        }
        vfsStream::setup('home_root_path');
    }

    /** @inheritdoc */
    protected function tearDown()
    {
        parent::tearDown();
        $this->extensions[] = null;
    }

    /**
     * Callback check function
     * @param FileInfo $fileinfo
     */
    public function increaseCounter($fileinfo) {
        $this->assertInstanceOf('\\splitbrain\\PHPArchive\\FileInfo', $fileinfo);
        $this->counter++;
    }

    /*
     * dependency for tests needing zlib extension to pass
     */
    public function testExtZlibIsInstalled()
    {
        $this->assertTrue(function_exists('gzopen'));
    }

    /*
     * dependency for tests needing bz2 extension to pass
     */
    public function testExtBz2IsInstalled()
    {
        $this->assertTrue(function_exists('bzopen'));
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testTarFileIsNotExisted()
    {
        $tar = new Tar();
        $tar->open('non_existed_file.tar');
    }

    /**
     * simple test that checks that the given filenames and contents can be grepped from
     * the uncompressed tar stream
     *
     * No check for format correctness
     */
    public function testCreateDynamic()
    {
        $tar = new Tar();

        $dir = dirname(__FILE__) . '/tar';
        $tdir = ltrim($dir, '/');

        $tar->create();
        $tar->addFile("$dir/testdata1.txt");
        $tar->addFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
        $tar->addData('another/testdata3.txt', 'testcontent3');

        $data = $tar->getArchive();

        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content in TAR');
        $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content in TAR');
        $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content in TAR');

        // fullpath might be too long to be stored as full path FS#2802
        $this->assertTrue(strpos($data, "$tdir") !== false, 'Path in TAR');
        $this->assertTrue(strpos($data, "testdata1.txt") !== false, 'File in TAR');

        $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in TAR');
        $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in TAR');

        // fullpath might be too long to be stored as full path FS#2802
        $this->assertTrue(strpos($data, "$tdir/foobar") === false, 'Path not in TAR');
        $this->assertTrue(strpos($data, "foobar.txt") === false, 'File not in TAR');

        $this->assertTrue(strpos($data, "foobar") === false, 'Path not in TAR');
    }

    /**
     * simple test that checks that the given filenames and contents can be grepped from the
     * uncompressed tar file
     *
     * No check for format correctness
     */
    public function testCreateFile()
    {
        $tar = new Tar();

        $dir = dirname(__FILE__) . '/tar';
        $tdir = ltrim($dir, '/');
        $tmp = vfsStream::url('home_root_path/test.tar');

        $tar->create($tmp);
        $tar->addFile("$dir/testdata1.txt");
        $tar->addFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
        $tar->addData('another/testdata3.txt', 'testcontent3');
        $tar->close();

        $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
        $data = file_get_contents($tmp);

        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content in TAR');
        $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content in TAR');
        $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content in TAR');

        // fullpath might be too long to be stored as full path FS#2802
        $this->assertTrue(strpos($data, "$tdir") !== false, "Path in TAR '$tdir'");
        $this->assertTrue(strpos($data, "testdata1.txt") !== false, 'File in TAR');

        $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in TAR');
        $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in TAR');

        // fullpath might be too long to be stored as full path FS#2802
        $this->assertTrue(strpos($data, "$tdir/foobar") === false, 'Path not in TAR');
        $this->assertTrue(strpos($data, "foobar.txt") === false, 'File not in TAR');

        $this->assertTrue(strpos($data, "foobar") === false, 'Path not in TAR');
    }

    /**
     * List the contents of the prebuilt TAR files
     */
    public function testTarcontent()
    {
        $dir = dirname(__FILE__) . '/tar';

        foreach ($this->extensions as $ext) {
            $tar = new Tar();
            $file = "$dir/test.$ext";

            $tar->open($file);
            /** @var FileInfo[] $content */
            $content = $tar->contents();

            $this->assertCount(4, $content, "Contents of $file");
            $this->assertEquals('tar/testdata1.txt', $content[1]->getPath(), "Contents of $file");
            $this->assertEquals(13, $content[1]->getSize(), "Contents of $file");

            $this->assertEquals('tar/foobar/testdata2.txt', $content[3]->getPath(), "Contents of $file");
            $this->assertEquals(13, $content[3]->getSize(), "Contents of $file");
        }
    }

    /**
     * Create an archive and unpack it again
     */
    public function testDogfood()
    {
        foreach ($this->extensions as $ext) {
            $input = glob(dirname(__FILE__) . '/../src/*');
            $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.' . $ext;
            $extract = sys_get_temp_dir() . '/dwtartest' . md5(time() + 1);

            $this->counter = 0;
            $tar = new Tar();
            $tar->setCallback(array($this, 'increaseCounter'));
            $tar->create($archive);
            foreach ($input as $path) {
                $file = basename($path);
                $tar->addFile($path, $file);
            }
            $tar->close();
            $this->assertFileExists($archive);
            $this->assertEquals(count($input), $this->counter);

            $this->counter = 0;
            $tar = new Tar();
            $tar->setCallback(array($this, 'increaseCounter'));
            $tar->open($archive);
            $tar->extract($extract, '', '/FileInfo\\.php/', '/.*\\.php/');

            $this->assertFileExists("$extract/Tar.php");
            $this->assertFileExists("$extract/Zip.php");
            $this->assertFileNotExists("$extract/FileInfo.php");

            $this->assertEquals(count($input) - 1, $this->counter);

            $this->nativeCheck($archive, $ext);

            self::RDelete($extract);
            unlink($archive);
        }
    }

    /**
     * Test the given archive with a native tar installation (if available)
     *
     * @param $archive
     * @param $ext
     */
    protected function nativeCheck($archive, $ext)
    {
        if (!is_executable('/usr/bin/tar')) {
            return;
        }

        $switch = array(
            'tar' => '-tf',
            'tgz' => '-tzf',
            'tar.gz' => '-tzf',
            'tbz' => '-tjf',
            'tar.bz2' => '-tjf',
        );
        $arg = $switch[$ext];
        $archive = escapeshellarg($archive);

        $return = 0;
        $output = array();
        $ok = exec("/usr/bin/tar $arg $archive 2>&1 >/dev/null", $output, $return);
        $output = join("\n", $output);

        $this->assertNotFalse($ok, "native tar execution for $archive failed:\n$output");
        $this->assertSame(0, $return, "native tar execution for $archive had non-zero exit code $return:\n$output");
        $this->assertSame('', $output, "native tar execution for $archive had non-empty output:\n$output");
    }

    /**
     * Extract the prebuilt tar files
     */
    public function testTarExtract()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = sys_get_temp_dir() . '/dwtartest' . md5(time());

        foreach ($this->extensions as $ext) {
            $tar = new Tar();
            $file = "$dir/test.$ext";

            $tar->open($file);
            $tar->extract($out);

            clearstatcache();

            $this->assertFileExists($out . '/tar/testdata1.txt', "Extracted $file");
            $this->assertEquals(13, filesize($out . '/tar/testdata1.txt'), "Extracted $file");

            $this->assertFileExists($out . '/tar/foobar/testdata2.txt', "Extracted $file");
            $this->assertEquals(13, filesize($out . '/tar/foobar/testdata2.txt'), "Extracted $file");

            self::RDelete($out);
        }
    }

    /**
     * Extract the prebuilt tar files with component stripping
     */
    public function testCompStripExtract()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = sys_get_temp_dir() . '/dwtartest' . md5(time());

        foreach ($this->extensions as $ext) {
            $tar = new Tar();
            $file = "$dir/test.$ext";

            $tar->open($file);
            $tar->extract($out, 1);

            clearstatcache();

            $this->assertFileExists($out . '/testdata1.txt', "Extracted $file");
            $this->assertEquals(13, filesize($out . '/testdata1.txt'), "Extracted $file");

            $this->assertFileExists($out . '/foobar/testdata2.txt', "Extracted $file");
            $this->assertEquals(13, filesize($out . '/foobar/testdata2.txt'), "Extracted $file");

            self::RDelete($out);
        }
    }

    /**
     * Extract the prebuilt tar files with prefix stripping
     */
    public function testPrefixStripExtract()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = sys_get_temp_dir() . '/dwtartest' . md5(time());

        foreach ($this->extensions as $ext) {
            $tar = new Tar();
            $file = "$dir/test.$ext";

            $tar->open($file);
            $tar->extract($out, 'tar/foobar/');

            clearstatcache();

            $this->assertFileExists($out . '/tar/testdata1.txt', "Extracted $file");
            $this->assertEquals(13, filesize($out . '/tar/testdata1.txt'), "Extracted $file");

            $this->assertFileExists($out . '/testdata2.txt', "Extracted $file");
            $this->assertEquals(13, filesize($out . '/testdata2.txt'), "Extracted $file");

            self::RDelete($out);
        }
    }

    /**
     * Extract the prebuilt tar files with include regex
     */
    public function testIncludeExtract()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = sys_get_temp_dir() . '/dwtartest' . md5(time());

        foreach ($this->extensions as $ext) {
            $tar = new Tar();
            $file = "$dir/test.$ext";

            $tar->open($file);
            $tar->extract($out, '', '', '/\/foobar\//');

            clearstatcache();

            $this->assertFileNotExists($out . '/tar/testdata1.txt', "Extracted $file");

            $this->assertFileExists($out . '/tar/foobar/testdata2.txt', "Extracted $file");
            $this->assertEquals(13, filesize($out . '/tar/foobar/testdata2.txt'), "Extracted $file");

            self::RDelete($out);
        }
    }

    /**
     * Extract the prebuilt tar files with exclude regex
     */
    public function testExcludeExtract()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = sys_get_temp_dir() . '/dwtartest' . md5(time());

        foreach ($this->extensions as $ext) {
            $tar = new Tar();
            $file = "$dir/test.$ext";

            $tar->open($file);
            $tar->extract($out, '', '/\/foobar\//');

            clearstatcache();

            $this->assertFileExists($out . '/tar/testdata1.txt', "Extracted $file");
            $this->assertEquals(13, filesize($out . '/tar/testdata1.txt'), "Extracted $file");

            $this->assertFileNotExists($out . '/tar/foobar/testdata2.txt', "Extracted $file");

            self::RDelete($out);
        }
    }

    /**
     * Check the extension to compression guesser
     */
    public function testFileType()
    {
        $tar = new Tar();
        $this->assertEquals(Tar::COMPRESS_NONE, $tar->filetype('foo'));
        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tgz'));
        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tGZ'));
        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tar.GZ'));
        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype('foo.tar.gz'));
        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tbz'));
        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tBZ'));
        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tar.BZ2'));
        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype('foo.tar.bz2'));

        $dir = dirname(__FILE__) . '/tar';
        $this->assertEquals(Tar::COMPRESS_NONE, $tar->filetype("$dir/test.tar"));
        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype("$dir/test.tgz"));
        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype("$dir/test.tbz"));
        $this->assertEquals(Tar::COMPRESS_NONE, $tar->filetype("$dir/test.tar.guess"));
        $this->assertEquals(Tar::COMPRESS_GZIP, $tar->filetype("$dir/test.tgz.guess"));
        $this->assertEquals(Tar::COMPRESS_BZIP, $tar->filetype("$dir/test.tbz.guess"));
    }

    /**
     * @depends testExtZlibIsInstalled
     */
    public function testLongPathExtract()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = vfsStream::url('home_root_path/dwtartest' . md5(time()));

        foreach (array('ustar', 'gnu') as $format) {
            $tar = new Tar();
            $tar->open("$dir/longpath-$format.tgz");
            $tar->extract($out);

            $this->assertFileExists(
                $out . '/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/1234567890/test.txt'
            );
        }
    }

    // FS#1442
    public function testCreateLongFile()
    {
        $tar = new Tar();
        $tar->setCompression(0);
        $tmp = vfsStream::url('home_root_path/dwtartest');

        $path = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789.txt';

        $tar->create($tmp);
        $tar->addData($path, 'testcontent1');
        $tar->close();

        $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
        $data = file_get_contents($tmp);

        // We should find the complete path and a longlink entry
        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'content in TAR');
        $this->assertTrue(strpos($data, $path) !== false, 'path in TAR');
        $this->assertTrue(strpos($data, '@LongLink') !== false, '@LongLink in TAR');
    }

    public function testCreateLongPathTar()
    {
        $tar = new Tar();
        $tar->setCompression(0);
        $tmp = vfsStream::url('home_root_path/dwtartest');

        $path = '';
        for ($i = 0; $i < 11; $i++) {
            $path .= '1234567890/';
        }
        $path = rtrim($path, '/');

        $tar->create($tmp);
        $tar->addData("$path/test.txt", 'testcontent1');
        $tar->close();

        $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
        $data = file_get_contents($tmp);

        // We should find the path and filename separated, no longlink entry
        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'content in TAR');
        $this->assertTrue(strpos($data, 'test.txt') !== false, 'filename in TAR');
        $this->assertTrue(strpos($data, $path) !== false, 'path in TAR');
        $this->assertFalse(strpos($data, "$path/test.txt") !== false, 'full filename in TAR');
        $this->assertFalse(strpos($data, '@LongLink') !== false, '@LongLink in TAR');
    }

    public function testCreateLongPathGnu()
    {
        $tar = new Tar();
        $tar->setCompression(0);
        $tmp = vfsStream::url('home_root_path/dwtartest');

        $path = '';
        for ($i = 0; $i < 20; $i++) {
            $path .= '1234567890/';
        }
        $path = rtrim($path, '/');

        $tar->create($tmp);
        $tar->addData("$path/test.txt", 'testcontent1');
        $tar->close();

        $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
        $data = file_get_contents($tmp);

        // We should find the complete path/filename and a longlink entry
        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'content in TAR');
        $this->assertTrue(strpos($data, 'test.txt') !== false, 'filename in TAR');
        $this->assertTrue(strpos($data, $path) !== false, 'path in TAR');
        $this->assertTrue(strpos($data, "$path/test.txt") !== false, 'full filename in TAR');
        $this->assertTrue(strpos($data, '@LongLink') !== false, '@LongLink in TAR');
    }

    /**
     * Extract a tarbomomb
     * @depends testExtZlibIsInstalled
     */
    public function testTarBomb()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = vfsStream::url('home_root_path/dwtartest' . md5(time()));

        $tar = new Tar();

        $tar->open("$dir/tarbomb.tgz");
        $tar->extract($out);

        clearstatcache();

        $this->assertFileExists(
            $out . '/AAAAAAAAAAAAAAAAA/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB.txt'
        );
    }

    /**
     * A single zero file should be just a header block + the footer
     */
    public function testZeroFile()
    {
        $dir = dirname(__FILE__) . '/tar';
        $tar = new Tar();
        $tar->setCompression(0);
        $tar->create();
        $tar->addFile("$dir/zero.txt", 'zero.txt');
        $file = $tar->getArchive();

        $this->assertEquals(512 * 3, strlen($file)); // 1 header block + 2 footer blocks
    }

    public function testZeroData()
    {
        $tar = new Tar();
        $tar->setCompression(0);
        $tar->create();
        $tar->addData('zero.txt', '');
        $file = $tar->getArchive();

        $this->assertEquals(512 * 3, strlen($file)); // 1 header block + 2 footer blocks
    }

    /**
     * A file of exactly one block should be just a header block + data block + the footer
     */
    public function testBlockFile()
    {
        $dir = dirname(__FILE__) . '/tar';
        $tar = new Tar();
        $tar->setCompression(0);
        $tar->create();
        $tar->addFile("$dir/block.txt", 'block.txt');
        $file = $tar->getArchive();

        $this->assertEquals(512 * 4, strlen($file)); // 1 header block + data block + 2 footer blocks
    }

    public function testBlockData()
    {
        $tar = new Tar();
        $tar->setCompression(0);
        $tar->create();
        $tar->addData('block.txt', str_pad('', 512, 'x'));
        $file = $tar->getArchive();

        $this->assertEquals(512 * 4, strlen($file)); // 1 header block + data block + 2 footer blocks
    }

    /**
     * @depends testExtZlibIsInstalled
     */
    public function testGzipIsValid()
    {
        foreach (['tgz', 'tar.gz'] as $ext) {
            $input = glob(dirname(__FILE__) . '/../src/*');
            $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.' . $ext;
            $extract = sys_get_temp_dir() . '/dwtartest' . md5(time() + 1);

            $tar = new Tar();
            $tar->setCompression(9, Tar::COMPRESS_GZIP);
            $tar->create();
            foreach ($input as $path) {
                $file = basename($path);
                $tar->addFile($path, $file);
            }
            $tar->save($archive);
            $this->assertFileExists($archive);

            try {
                $phar = new \PharData($archive);
                $phar->extractTo($extract);
            } catch(\Exception $e) {
            };

            $this->assertFileExists("$extract/Tar.php");
            $this->assertFileExists("$extract/Zip.php");

            $this->nativeCheck($archive, $ext);

            self::RDelete($extract);
            unlink($archive);
        }
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testContentsWithInvalidArchiveStream()
    {
        $tar = new Tar();
        $tar->contents();
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testExtractWithInvalidOutDir()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = '/root/invalid_out_dir';

        $tar = new Tar();

        $tar->open("$dir/tarbomb.tgz");
        $tar->extract($out);
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testExtractWithArchiveStreamIsClosed()
    {
        $dir = dirname(__FILE__) . '/tar';
        $out = '/root/invalid_out_dir';

        $tar = new Tar();

        $tar->open("$dir/tarbomb.tgz");
        $tar->close();
        $tar->extract($out);
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testCreateWithInvalidFile()
    {
        $dir = dirname(__FILE__) . '/tar';
        $tar = new Tar();

        $tar->open("$dir/tarbomb.tgz");
        $tar->create('/root/invalid_file');
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testAddFileWithArchiveStreamIsClosed()
    {
        $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';

        $tar = new Tar();
        $tar->create($archive);
        $tar->close();
        $tar->addFile('archive_file', false);
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testAddFileWithInvalidFile()
    {
        $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';

        $tar = new Tar();
        $tar->create($archive);
        $tar->addFile('archive_file', false);
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testAddDataWithArchiveStreamIsClosed()
    {
        $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';

        $tar = new Tar();
        $tar->create($archive);
        $tar->close();
        $tar->addData(false, '');
    }

    public function testCloseHasBeenClosed()
    {
        $archive = sys_get_temp_dir() . '/dwtartest' . md5(time()) . '.tar';

        $tar = new Tar();
        $tar->create($archive);
        $tar->close();

        $tar->close();
        $this->assertTrue(true); // succeed if no exception, yet
    }

    /**
     * @depends testExtBz2IsInstalled
     */
    public function testGetArchiveWithBzipCompress()
    {
        $dir = dirname(__FILE__) . '/tar';
        $tar = new Tar();
        $tar->setCompression(9, Tar::COMPRESS_BZIP);
        $tar->create();
        $tar->addFile("$dir/zero.txt", 'zero.txt');
        $file = $tar->getArchive();

        $this->assertInternalType('string', $file); // 1 header block + 2 footer blocks
    }

    public function testSaveWithCompressionAuto()
    {
        $dir = dirname(__FILE__) . '/tar';
        $tar = new Tar();
        $tar->setCompression(-1);
        $tar->create();
        $tar->addFile("$dir/zero.txt", 'zero.txt');

        $tar->save(vfsStream::url('home_root_path/archive_file'));
        $this->assertTrue(true); // succeed if no exception, yet
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testSaveWithInvalidDestinationFile()
    {
        $dir = dirname(__FILE__) . '/tar';
        $tar = new Tar();
        $tar->setCompression();
        $tar->create();
        $tar->addFile("$dir/zero.txt", 'zero.txt');

        $tar->save(vfsStream::url('archive_file'));
        $this->assertTrue(true); // succeed if no exception, yet
    }

    /**
     * recursive rmdir()/unlink()
     *
     * @static
     * @param $target string
     */
    public static function RDelete($target)
    {
        if (!is_dir($target)) {
            unlink($target);
        } else {
            $dh = dir($target);
            while (false !== ($entry = $dh->read())) {
                if ($entry == '.' || $entry == '..') {
                    continue;
                }
                self::RDelete("$target/$entry");
            }
            $dh->close();
            rmdir($target);
        }
    }
}
<?php /** @noinspection PhpUnhandledExceptionInspection */

namespace splitbrain\PHPArchive;

use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;

class ZipTestCase extends TestCase
{
    /** @var int callback counter */
    protected $counter = 0;

    /** @inheritdoc */
    protected function setUp()
    {
        vfsStream::setup('home_root_path');
    }

    /**
     * Callback check function
     * @param FileInfo $fileinfo
     */
    public function increaseCounter($fileinfo) {
        $this->assertInstanceOf('\\splitbrain\\PHPArchive\\FileInfo', $fileinfo);
        $this->counter++;
    }

    /*
     * dependency for tests needing zip extension to pass
     */
    public function testExtZipIsInstalled()
    {
        $this->assertTrue(function_exists('zip_open'));
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testMissing()
    {
        $tar = new Zip();
        $tar->open('nope.zip');
    }

    /**
     * simple test that checks that the given filenames and contents can be grepped from
     * the uncompressed zip stream
     *
     * No check for format correctness
     * @depends testExtZipIsInstalled
     */
    public function testCreateDynamic()
    {
        $zip = new Zip();

        $dir = dirname(__FILE__) . '/zip';
        $tdir = ltrim($dir, '/');

        $zip->create();
        $zip->setCompression(0);
        $zip->AddFile("$dir/testdata1.txt", "$dir/testdata1.txt");
        $zip->AddFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
        $zip->addData('another/testdata3.txt', 'testcontent3');

        $data = $zip->getArchive();

        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content 1 in ZIP');
        $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content 2 in ZIP');
        $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content 3 in ZIP');

        // fullpath might be too long to be stored as full path FS#2802
        $this->assertTrue(strpos($data, "$tdir") !== false, 'Path in ZIP');
        $this->assertTrue(strpos($data, "testdata1.txt") !== false, 'File in ZIP');

        $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in ZIP');
        $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in ZIP');

        // fullpath might be too long to be stored as full path FS#2802
        $this->assertTrue(strpos($data, "$tdir/foobar") === false, 'Path not in ZIP');
        $this->assertTrue(strpos($data, "foobar.txt") === false, 'File not in ZIP');

        $this->assertTrue(strpos($data, "foobar") === false, 'Path not in ZIP');
    }

    /**
     * simple test that checks that the given filenames and contents can be grepped from the
     * uncompressed zip file
     *
     * No check for format correctness
     * @depends testExtZipIsInstalled
     */
    public function testCreateFile()
    {
        $zip = new Zip();

        $dir = dirname(__FILE__) . '/zip';
        $tdir = ltrim($dir, '/');
        $tmp = vfsStream::url('home_root_path/test.zip');

        $zip->create($tmp);
        $zip->setCompression(0);
        $zip->addFile("$dir/testdata1.txt", "$dir/testdata1.txt");
        $zip->addFile("$dir/foobar/testdata2.txt", 'noway/testdata2.txt');
        $zip->addData('another/testdata3.txt', 'testcontent3');
        $zip->close();

        $this->assertTrue(filesize($tmp) > 30); //arbitrary non-zero number
        $data = file_get_contents($tmp);

        $this->assertTrue(strpos($data, 'testcontent1') !== false, 'Content in ZIP');
        $this->assertTrue(strpos($data, 'testcontent2') !== false, 'Content in ZIP');
        $this->assertTrue(strpos($data, 'testcontent3') !== false, 'Content in ZIP');

        // fullpath might be too long to be stored as full path FS#2802
        $this->assertTrue(strpos($data, "$tdir") !== false, "Path in ZIP '$tdir'");
        $this->assertTrue(strpos($data, "testdata1.txt") !== false, 'File in ZIP');

        $this->assertTrue(strpos($data, 'noway/testdata2.txt') !== false, 'Path in ZIP');
        $this->assertTrue(strpos($data, 'another/testdata3.txt') !== false, 'Path in ZIP');

        // fullpath might be too long to be stored as full path FS#2802
        $this->assertTrue(strpos($data, "$tdir/foobar") === false, 'Path not in ZIP');
        $this->assertTrue(strpos($data, "foobar.txt") === false, 'File not in ZIP');

        $this->assertTrue(strpos($data, "foobar") === false, 'Path not in ZIP');
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testCreateWithInvalidFilePath()
    {
        $zip = new Zip();

        $tmp = vfsStream::url('invalid_root_path/test.zip');

        $zip->create($tmp);
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testAddFileWithArchiveStreamIsClosed()
    {
        $zip = new Zip();

        $dir = dirname(__FILE__) . '/zip';

        $zip->setCompression(0);
        $zip->close();
        $zip->addFile("$dir/testdata1.txt", "$dir/testdata1.txt");
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testAddFileWithInvalidFile()
    {
        $zip = new Zip();

        $tmp = vfsStream::url('home_root_path/test.zip');

        $zip->create($tmp);
        $zip->setCompression(0);
        $zip->addFile('invalid_file', false);
        $zip->close();
    }

    /**
     * List the contents of the prebuilt ZIP file
     * @depends testExtZipIsInstalled
     */
    public function testZipContent()
    {
        $dir = dirname(__FILE__) . '/zip';

        $zip = new Zip();
        $file = "$dir/test.zip";

        $zip->open($file);
        $content = $zip->contents();

        $this->assertCount(5, $content, "Contents of $file");
        $this->assertEquals('zip/testdata1.txt', $content[2]->getPath(), "Contents of $file");
        $this->assertEquals(13, $content[2]->getSize(), "Contents of $file");

        $this->assertEquals('zip/foobar/testdata2.txt', $content[4]->getPath(), "Contents of $file");
        $this->assertEquals(13, $content[4]->getSize(), "Contents of $file");
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testZipContentWithArchiveStreamIsClosed()
    {
        $dir = dirname(__FILE__) . '/zip';

        $zip = new Zip();
        $file = "$dir/test.zip";

        $zip->open($file);
        $zip->close();
        $zip->contents();
    }

    /**
     * Create an archive and unpack it again
     * @depends testExtZipIsInstalled
     */
    public function testDogFood()
    {
        $input = glob(dirname(__FILE__) . '/../src/*');
        $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
        $extract = sys_get_temp_dir() . '/dwziptest' . md5(time() + 1);

        $this->counter = 0;
        $zip = new Zip();
        $zip->setCallback(array($this, 'increaseCounter'));
        $zip->create($archive);
        foreach ($input as $path) {
            $file = basename($path);
            $zip->addFile($path, $file);
        }
        $zip->close();
        $this->assertFileExists($archive);
        $this->assertEquals(count($input), $this->counter);

        $this->counter = 0;
        $zip = new Zip();
        $zip->setCallback(array($this, 'increaseCounter'));
        $zip->open($archive);
        $zip->extract($extract, '', '/FileInfo\\.php/', '/.*\\.php/');

        $this->assertFileExists("$extract/Tar.php");
        $this->assertFileExists("$extract/Zip.php");
        $this->assertFileNotExists("$extract/FileInfo.php");

        $this->assertEquals(count($input) - 1, $this->counter);

        $this->nativeCheck($archive);
        $this->native7ZipCheck($archive);

        self::RDelete($extract);
        unlink($archive);
    }

    /**
     * @depends testExtZipIsInstalled
     */
    public function testUtf8()
    {
        $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';
        $extract = sys_get_temp_dir() . '/dwziptest' . md5(time() + 1);

        $zip = new Zip();
        $zip->create($archive);
        $zip->addData('tüst.txt', 'test');
        $zip->addData('snowy☃.txt', 'test');
        $zip->close();
        $this->assertFileExists($archive);

        $zip = new Zip();
        $zip->open($archive);
        $zip->extract($extract);

        $this->assertFileExists($extract . '/tüst.txt');
        $this->assertFileExists($extract . '/snowy☃.txt');

        $this->nativeCheck($archive);
        $this->native7ZipCheck($archive);

        self::RDelete($extract);
        unlink($archive);
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testAddDataWithArchiveStreamIsClosed()
    {
        $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';

        $zip = new Zip();
        $zip->create($archive);
        $zip->close();
        $zip->addData('tüst.txt', 'test');
    }

    public function testCloseWithArchiveStreamIsClosed()
    {
        $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';

        $zip = new Zip();
        $zip->create($archive);
        $zip->close();

        $zip->close();
        $this->assertTrue(true); // succeed if no exception, yet
    }

    public function testSaveArchiveFile()
    {
        $dir = dirname(__FILE__) . '/tar';
        $zip = new zip();
        $zip->setCompression(-1);
        $zip->create();
        $zip->addFile("$dir/zero.txt", 'zero.txt');

        $zip->save(vfsStream::url('home_root_path/archive_file'));
        $this->assertTrue(true); // succeed if no exception, yet
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testSaveWithInvalidFilePath()
    {
        $archive = sys_get_temp_dir() . '/dwziptest' . md5(time()) . '.zip';

        $zip = new Zip();
        $zip->create($archive);
        $zip->save(vfsStream::url('invalid_root_path/save.zip'));
    }

    /**
     * Test the given archive with a native zip installation (if available)
     *
     * @param $archive
     */
    protected function nativeCheck($archive)
    {
        if (!is_executable('/usr/bin/zipinfo')) {
            return;
        }
        $archive = escapeshellarg($archive);

        $return = 0;
        $output = array();
        $ok = exec("/usr/bin/zipinfo $archive 2>&1 >/dev/null", $output, $return);
        $output = join("\n", $output);

        $this->assertNotFalse($ok, "native zip execution for $archive failed:\n$output");
        $this->assertSame(0, $return, "native zip execution for $archive had non-zero exit code $return:\n$output");
        $this->assertSame('', $output, "native zip execution for $archive had non-empty output:\n$output");
    }

    /**
     * Test the given archive with a native 7zip installation (if available)
     *
     * @param $archive
     */
    protected function native7ZipCheck($archive)
    {
        if (!is_executable('/usr/bin/7z')) {
            return;
        }
        $archive = escapeshellarg($archive);

        $return = 0;
        $output = array();
        $ok = exec("/usr/bin/7z t $archive 2>&1 >/dev/null", $output, $return);
        $output = join("\n", $output);

        $this->assertNotFalse($ok, "native 7zip execution for $archive failed:\n$output");
        $this->assertSame(0, $return, "native 7zip execution for $archive had non-zero exit code $return:\n$output");
        $this->assertSame('', $output, "native 7zip execution for $archive had non-empty output:\n$output");
    }

    /**
     * Extract the prebuilt zip files
     * @depends testExtZipIsInstalled
     */
    public function testZipExtract()
    {
        $dir = dirname(__FILE__) . '/zip';
        $out = sys_get_temp_dir() . '/dwziptest' . md5(time());

        $zip = new Zip();
        $file = "$dir/test.zip";

        $zip->open($file);
        $zip->extract($out);

        clearstatcache();

        $this->assertFileExists($out . '/zip/testdata1.txt', "Extracted $file");
        $this->assertEquals(13, filesize($out . '/zip/testdata1.txt'), "Extracted $file");

        $this->assertFileExists($out . '/zip/foobar/testdata2.txt', "Extracted $file");
        $this->assertEquals(13, filesize($out . '/zip/foobar/testdata2.txt'), "Extracted $file");

        $this->assertFileExists($out . '/zip/compressable.txt', "Extracted $file");
        $this->assertEquals(1836, filesize($out . '/zip/compressable.txt'), "Extracted $file");
        $this->assertFileNotExists($out . '/zip/compressable.txt.gz', "Extracted $file");

        self::RDelete($out);
    }

    /**
     * @expectedException \splitbrain\PHPArchive\ArchiveIOException
     */
    public function testZipExtractWithArchiveStreamIsClosed()
    {
        $dir = dirname(__FILE__) . '/zip';
        $out = sys_get_temp_dir() . '/dwziptest' . md5(time());

        $zip = new Zip();
        $file = "$dir/test.zip";

        $zip->open($file);
        $zip->close();
        $zip->extract($out);
    }

    /**
     * Extract the prebuilt zip files with component stripping
     * @depends testExtZipIsInstalled
     */
    public function testCompStripExtract()
    {
        $dir = dirname(__FILE__) . '/zip';
        $out = sys_get_temp_dir() . '/dwziptest' . md5(time());

        $zip = new Zip();
        $file = "$dir/test.zip";

        $zip->open($file);
        $zip->extract($out, 1);

        clearstatcache();

        $this->assertFileExists($out . '/testdata1.txt', "Extracted $file");
        $this->assertEquals(13, filesize($out . '/testdata1.txt'), "Extracted $file");

        $this->assertFileExists($out . '/foobar/testdata2.txt', "Extracted $file");
        $this->assertEquals(13, filesize($out . '/foobar/testdata2.txt'), "Extracted $file");

        self::RDelete($out);
    }

    /**
     * Extract the prebuilt zip files with prefix stripping
     * @depends testExtZipIsInstalled
     */
    public function testPrefixStripExtract()
    {
        $dir = dirname(__FILE__) . '/zip';
        $out = sys_get_temp_dir() . '/dwziptest' . md5(time());

        $zip = new Zip();
        $file = "$dir/test.zip";

        $zip->open($file);
        $zip->extract($out, 'zip/foobar/');

        clearstatcache();

        $this->assertFileExists($out . '/zip/testdata1.txt', "Extracted $file");
        $this->assertEquals(13, filesize($out . '/zip/testdata1.txt'), "Extracted $file");

        $this->assertFileExists($out . '/testdata2.txt', "Extracted $file");
        $this->assertEquals(13, filesize($out . '/testdata2.txt'), "Extracted $file");

        self::RDelete($out);
    }

    /**
     * Extract the prebuilt zip files with include regex
     * @depends testExtZipIsInstalled
     */
    public function testIncludeExtract()
    {
        $dir = dirname(__FILE__) . '/zip';
        $out = sys_get_temp_dir() . '/dwziptest' . md5(time());

        $zip = new Zip();
        $file = "$dir/test.zip";

        $zip->open($file);
        $zip->extract($out, '', '', '/\/foobar\//');

        clearstatcache();

        $this->assertFileNotExists($out . '/zip/testdata1.txt', "Extracted $file");

        $this->assertFileExists($out . '/zip/foobar/testdata2.txt', "Extracted $file");
        $this->assertEquals(13, filesize($out . '/zip/foobar/testdata2.txt'), "Extracted $file");

        self::RDelete($out);
    }

    /**
     * Extract the prebuilt zip files with exclude regex
     * @depends testExtZipIsInstalled
     */
    public function testExcludeExtract()
    {
        $dir = dirname(__FILE__) . '/zip';
        $out = sys_get_temp_dir() . '/dwziptest' . md5(time());

        $zip = new Zip();
        $file = "$dir/test.zip";

        $zip->open($file);
        $zip->extract($out, '', '/\/foobar\//');

        clearstatcache();

        $this->assertFileExists($out . '/zip/testdata1.txt', "Extracted $file");
        $this->assertEquals(13, filesize($out . '/zip/testdata1.txt'), "Extracted $file");

        $this->assertFileNotExists($out . '/zip/foobar/testdata2.txt', "Extracted $file");

        self::RDelete($out);
    }

    /**
     * @depends testExtZipIsInstalled
     */
    public function testUmlautWinrar()
    {
        $out = vfsStream::url('home_root_path/dwtartest' . md5(time()));

        $zip = new Zip();
        $zip->open(__DIR__ . '/zip/issue14-winrar.zip');
        $zip->extract($out);
        $this->assertFileExists("$out/tüst.txt");
    }

    /**
     * @depends testExtZipIsInstalled
     */
    public function testUmlautWindows()
    {
        $out = vfsStream::url('home_root_path/dwtartest' . md5(time()));

        $zip = new Zip();
        $zip->open(__DIR__ . '/zip/issue14-windows.zip');
        $zip->extract($out);
        $this->assertFileExists("$out/täst.txt");
    }

    /**
     * recursive rmdir()/unlink()
     *
     * @static
     * @param $target string
     */
    public static function RDelete($target)
    {
        if (!is_dir($target)) {
            unlink($target);
        } else {
            $dh = dir($target);
            while (false !== ($entry = $dh->read())) {
                if ($entry == '.' || $entry == '..') {
                    continue;
                }
                self::RDelete("$target/$entry");
            }
            $dh->close();
            rmdir($target);
        }
    }

}
testcontent1
PK
     s2J              tst.txtup &Atüst.txtPK 
     s2J             6               tst.txt
         2q2q2qup &Atüst.txtPK      l   8     PK     Sn2J               tst.txtPK      Sn2J                             tst.txtPK      6   &     testcontent2
PK
     8kF              zip/UT	  U"Uux   d   PK   8kFgz   ,    zip/compressable.txtUT	  U Uux   d   34$ 

i! PK
    tcA{
   
     zip/testdata1.txtUT	 Pv Uux   d   testcontent1
PK
     tcA              zip/foobar/UT	 P"Uux   d   PK
    tcAo(î
   
     zip/foobar/testdata2.txtUT	 Pv Uux   d   testcontent2
PK
     8kF                     A    zip/UT  Uux   d   PK   8kFgz   ,           >   zip/compressable.txtUT  Uux   d   PK
    tcA{
   
               zip/testdata1.txtUT Pux   d   PK
     tcA                     A   zip/foobar/UT Pux   d   PK
    tcAo(î
   
            A  zip/foobar/testdata2.txtUT Pux   d   PK            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<?php /** @noinspection PhpUnhandledExceptionInspection */

namespace splitbrain\PHPArchive;

use PHPUnit\Framework\TestCase;

class FileInfoTest extends TestCase
{

    public function testDefaults()
    {
        $fileinfo = new FileInfo('foobar');

        $this->assertEquals('foobar', $fileinfo->getPath());
        $this->assertTrue($fileinfo->getMtime() > time() - 30);
        $this->assertFalse($fileinfo->getIsdir());
        $this->assertEquals(0, $fileinfo->getSize());
        $this->assertEquals(0, $fileinfo->getCompressedSize());
        $this->assertEquals(0664, $fileinfo->getMode());
        $this->assertEquals(0, $fileinfo->getGid());
        $this->assertEquals(0, $fileinfo->getUid());
        $this->assertEquals('', $fileinfo->getOwner());
        $this->assertEquals('', $fileinfo->getGroup());
        $this->assertEquals('', $fileinfo->getComment());
    }

    public function testClean()
    {
        $data = array(
            array('foo', 'foo'),
            array('/foo/', 'foo'),
            array('/foo/../bar', 'bar'),
            array('/foo/../../bar', 'bar'),
            array('/foo/../baz/../bar', 'bar'),
            array('/foo/baz/../bar', 'foo/bar'),
            array('\\foo/baz\\../bar', 'foo/bar'),
            array('/foo/bar', 'foo/bar'),
            array('/foo/bar/', 'foo/bar'),
            array('foo//bar', 'foo/bar'),
            array('foo/0/bar', 'foo/0/bar'),
            array('foo/../bar', 'bar'),
            array('foo/bang/bang/../../bar', 'foo/bar'),
            array('foo/../../bar', 'bar'),
            array('foo/.././../bar', 'bar'),

        );

        $fileinfo = new FileInfo();
        foreach ($data as $test) {
            $fileinfo->setPath($test[0]);
            $this->assertEquals($test[1], $fileinfo->getPath());
        }
    }

    public function testStrip()
    {
        $fileinfo = new FileInfo('foo/bar/baz/bang');
        $this->assertEquals('foo/bar/baz/bang', $fileinfo->getPath());

        $fileinfo->strip(1);
        $this->assertEquals('bar/baz/bang', $fileinfo->getPath());

        $fileinfo->strip(2);
        $this->assertEquals('bang', $fileinfo->getPath());

        $fileinfo = new FileInfo('foo/bar/baz/bang');
        $fileinfo->strip('nomatch');
        $this->assertEquals('foo/bar/baz/bang', $fileinfo->getPath());

        $fileinfo->strip('foo/bar');
        $this->assertEquals('baz/bang', $fileinfo->getPath());
    }

    public function testMatch()
    {
        $fileinfo = new FileInfo('foo/bar/baz/bang');

        $this->assertTrue($fileinfo->match());
        $this->assertTrue($fileinfo->match('/bang/'));
        $this->assertFalse($fileinfo->match('/bark/'));

        $this->assertFalse($fileinfo->match('', '/bang/'));
        $this->assertTrue($fileinfo->match('', '/bark/'));

        $this->assertFalse($fileinfo->match('/bang/', '/foo/'));
        $this->assertTrue($fileinfo->match('/bang/', '/bark/'));
    }

    public function testFromPath()
    {
        $fileinfo = FileInfo::fromPath(__DIR__ . '/zip/block.txt', 'test.txt');
        $this->assertEquals('test.txt', $fileinfo->getPath());
        $this->assertFalse($fileinfo->getIsdir());
        $this->assertSame(512, $fileinfo->getSize());

        $fileinfo = FileInfo::fromPath(__DIR__ . '/zip', 'zip');
        $this->assertEquals('zip', $fileinfo->getPath());
        $this->assertTrue($fileinfo->getIsdir());
        $this->assertSame(0, $fileinfo->getSize());
    }

    /**
     * @expectedException \splitbrain\PHPArchive\FileInfoException
     */
    public function testFromPathWithFileNotExisted()
    {
        FileInfo::fromPath('invalid_file_path');
    }
}
BZh91AY&SY2i|  @@ @!@ @0 Miɀ	E@  S"54&iڌjjDy.DbX;D$h%mU,rKdqy|Y*edHm LXT$mDA}5TԆ'UέVQc$o`uS;;New$S	.&tar/                                                                                                0000755 0001750 0000144 00000000000 12045216715 010501  5                                                                                                    ustar   andi                            users                                                                                                                                                                                                                  tar/testdata1.txt                                                                                   0000644 0001750 0000144 00000000015 12045216715 013130  0                                                                                                    ustar   andi                            users                                                                                                                                                                                                                  testcontent1
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   tar/foobar/                                                                                         0000755 0001750 0000144 00000000000 12045216747 011756  5                                                                                                    ustar   andi                            users                                                                                                                                                                                                                  tar/foobar/testdata2.txt                                                                            0000644 0001750 0000144 00000000015 12045216747 014406  0                                                                                                    ustar   andi                            users                                                                                                                                                                                                                  testcontent2
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   tar/                                                                                                0000755 0001750 0000144 00000000000 12045216715 010501  5                                                                                                    ustar   andi                            users                                                                                                                                                                                                                  tar/testdata1.txt                                                                                   0000644 0001750 0000144 00000000015 12045216715 013130  0                                                                                                    ustar   andi                            users                                                                                                                                                                                                                  testcontent1
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   tar/foobar/                                                                                         0000755 0001750 0000144 00000000000 12045216747 011756  5                                                                                                    ustar   andi                            users                                                                                                                                                                                                                  tar/foobar/testdata2.txt                                                                            0000644 0001750 0000144 00000000015 12045216747 014406  0                                                                                                    ustar   andi                            users                                                                                                                                                                                                                  testcontent2
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   testcontent1
LP hamidTARtester2.tar M
0`2i:uK"MD.Rߐ7NXz pԘ4H@I:8\ߴ#O9e=>?g)hǼxڶ
 ͷyۛ/_WK*{?1;]>    _.P ]N@^ ΙδzD;D`U4aGA]C?HE&Q P'>xHpVcgeY'Ofsҟ5(T-#'(M(oF!=zgM\5:MsƳM`:y'>cckfA4"E9ȴ-gi{SR?JENv϶*u;	7
:kWoߎm^Wbqy/w^              w; P  BZh91AY&SY2i|  @@ @!@ @0 Miɀ	E@  S"54&iڌjjDy.DbX;D$h%mU,rKdqy|Y*edHm LXT$mDA}5TԆ'UέVQc$o`uS;;New$S	.&testcontent2
 V+P M
0=EN`g'
nZhG&U߷	LydFX(֦5[,6wci0jBuTWM	d#y?f
)MzyC/]/v3P\za\2_"?V9ca5?              A (   3P J@EW:3; U|b25;4r˒d{%%rwO]+hmF.FkVݎ_t,RȤ#D][x_~5,_><X<od?ϭ#8(?'߿YAC?7
~9g?3_JFCQp&CC/Q6?VQO~GdŚdߒe¥gKaD.~Tk.6wg?GwFuק+ס       W_ (  BZh91AY&SY2i|  @@ @!@ @0 Miɀ	E@  S"54&iڌjjDy.DbX;D$h%mU,rKdqy|Y*edHm LXT$mDA}5TԆ'UέVQc$o`uS;;New$S	.& V+P M
0=EN`g'
nZhG&U߷	LydFX(֦5[,6wci0jBuTWM	d#y?f
)MzyC/]/v3P\za\2_"?V9ca5?              A (   V+P M
0=EN`g'
nZhG&U߷	LydFX(֦5[,6wci0jBuTWM	d#y?f
)MzyC/]/v3P\za\2_"?V9ca5?              A (  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPHPArchive - Pure PHP ZIP and TAR handling
==========================================

This library allows to handle new ZIP and TAR archives without the need for any special PHP extensions (gz and bzip are
needed for compression). It can create new files or extract existing ones.

To keep things simple, the modification (adding or removing files) of existing archives is not supported.

[![Build Status](https://travis-ci.org/splitbrain/php-archive.svg)](https://travis-ci.org/splitbrain/php-archive)

Install
-------

Use composer:

```php composer.phar require splitbrain/php-archive```

Usage
-----

The usage for the Zip and Tar classes are basically the same. Here are some
examples for working with TARs to get you started.

Check the [API docs](https://splitbrain.github.io/php-archive/) for more
info.


```php
require_once 'vendor/autoload.php';
use splitbrain\PHPArchive\Tar;

// To list the contents of an existing TAR archive, open() it and use
// contents() on it:
$tar = new Tar();
$tar->open('myfile.tgz');
$toc = $tar->contents();
print_r($toc); // array of FileInfo objects

// To extract the contents of an existing TAR archive, open() it and use
// extract() on it:
$tar = new Tar();
$tar->open('myfile.tgz');
$tar->extract('/tmp');

// To create a new TAR archive directly on the filesystem (low memory
// requirements), create() it:
$tar = new Tar();
$tar->create('myfile.tgz');
$tar->addFile(...);
$tar->addData(...);
...
$tar->close();

// To create a TAR archive directly in memory, create() it, add*()
// files and then either save() or getArchive() it:
$tar = new Tar();
$tar->setCompression(9, Archive::COMPRESS_BZIP);
$tar->create();
$tar->addFile(...);
$tar->addData(...);
...
$tar->save('myfile.tbz'); // compresses and saves it
echo $tar->getArchive(); // compresses and returns it
```

Differences between Tar and Zip: Tars are compressed as a whole, while Zips compress each file individually. Therefore
you can call ```setCompression``` before each ```addFile()``` and ```addData()``` function call.

The FileInfo class can be used to specify additional info like ownership or permissions when adding a file to
an archive. 
tree: Yes
deprecated: Yes
accessLevels: [public]
todo: Yes
*.iml
.idea/
composer.phar
vendor/
composer.lock
apigen.phar
docs/
nbproject/language: php
php:
  - 5.4
  - 5.5
  - 5.6
  - 7.0
  - 7.1
  - 7.2
  - nightly
  - hhvm
before_script:
  - composer install
script: ./vendor/bin/phpunit
after_success:
  - sh generate-api.sh
env:
  global:
    secure: ctCQVPQgQziwIZf5QGHcnhHlXsyauG0W3AWF/6R8cTP+in2S/RygunPp7CkXiqA1YMluGr2Lo9h4DTVg7oqeXl79FXFXedijQmQEu3g3f4iDWtxbQhGf4bJQk57jXFldge4rQedlOJDzwGzJ1abrimJQlu090BZNeonzWL5cRK4=
{
    "name": "splitbrain/php-archive",
    "description": "Pure-PHP implementation to read and write TAR and ZIP archives",
    "keywords": ["zip", "tar", "archive", "unpack", "extract", "unzip"],
    "authors": [
        {
            "name": "Andreas Gohr",
            "email": "andi@splitbrain.org"
        }
    ],
    "license": "MIT",

    "require": {
        "php": ">=5.4"
    },

    "suggest": {
        "ext-iconv": "Used for proper filename encode handling",
        "ext-mbstring": "Can be used alternatively for handling filename encoding"
    },

    "require-dev": {
        "phpunit/phpunit": "^4.8",
        "mikey179/vfsStream": "^1.6",
        "ext-zip": "*",
        "ext-bz2": "*"
    },

    "autoload": {
        "psr-4": {
            "splitbrain\\PHPArchive\\": "src"
        }
    },

    "autoload-dev": {
        "psr-4": {
            "splitbrain\\PHPArchive\\": "tests"
        }
    }
}
<?php

namespace splitbrain\PHPArchive;

/**
 * Bad or unsupported compression settings requested
 */
class ArchiveIllegalCompressionException extends \Exception
{
}<?php

namespace splitbrain\PHPArchive;

abstract class Archive
{

    const COMPRESS_AUTO = -1;
    const COMPRESS_NONE = 0;
    const COMPRESS_GZIP = 1;
    const COMPRESS_BZIP = 2;

    /** @var callable */
    protected $callback;

    /**
     * Set the compression level and type
     *
     * @param int $level Compression level (0 to 9)
     * @param int $type  Type of compression to use (use COMPRESS_* constants)
     * @throws ArchiveIllegalCompressionException
     */
    abstract public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO);

    /**
     * Open an existing archive file for reading
     *
     * @param string $file
     * @throws ArchiveIOException
     */
    abstract public function open($file);

    /**
     * Read the contents of an archive
     *
     * This function lists the files stored in the archive, and returns an indexed array of FileInfo objects
     *
     * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
     * Reopen the file with open() again if you want to do additional operations
     *
     * @return FileInfo[]
     */
    abstract public function contents();

    /**
     * Extract an existing archive
     *
     * The $strip parameter allows you to strip a certain number of path components from the filenames
     * found in the archive file, similar to the --strip-components feature of GNU tar. This is triggered when
     * an integer is passed as $strip.
     * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
     * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
     *
     * By default this will extract all files found in the archive. You can restrict the output using the $include
     * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
     * $include is set, only files that match this expression will be extracted. Files that match the $exclude
     * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
     * stripped filenames as described above.
     *
     * The archive is closed afterwards. Reopen the file with open() again if you want to do additional operations
     *
     * @param string     $outdir  the target directory for extracting
     * @param int|string $strip   either the number of path components or a fixed prefix to strip
     * @param string     $exclude a regular expression of files to exclude
     * @param string     $include a regular expression of files to include
     * @throws ArchiveIOException
     * @return array
     */
    abstract public function extract($outdir, $strip = '', $exclude = '', $include = '');

    /**
     * Create a new archive file
     *
     * If $file is empty, the archive file will be created in memory
     *
     * @param string $file
     */
    abstract public function create($file = '');

    /**
     * Add a file to the current archive using an existing file in the filesystem
     *
     * @param string          $file     path to the original file
     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
     * @throws ArchiveIOException
     */
    abstract public function addFile($file, $fileinfo = '');

    /**
     * Add a file to the current archive using the given $data as content
     *
     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
     * @param string          $data     binary content of the file to add
     * @throws ArchiveIOException
     */
    abstract public function addData($fileinfo, $data);

    /**
     * Close the archive, close all file handles
     *
     * After a call to this function no more data can be added to the archive, for
     * read access no reading is allowed anymore
     */
    abstract public function close();

    /**
     * Returns the created in-memory archive data
     *
     * This implicitly calls close() on the Archive
     */
    abstract public function getArchive();

    /**
     * Save the created in-memory archive data
     *
     * Note: It is more memory effective to specify the filename in the create() function and
     * let the library work on the new file directly.
     *
     * @param string $file
     */
    abstract public function save($file);

    /**
     * Set a callback function to be called whenever a file is added or extracted.
     *
     * The callback is called with a FileInfo object as parameter. You can use this to show progress
     * info during an operation.
     *
     * @param callable $callback
     */
    public function setCallback($callback)
    {
        $this->callback = $callback;
    }
}
<?php

namespace splitbrain\PHPArchive;

/**
 * Class Zip
 *
 * Creates or extracts Zip archives
 *
 * for specs see http://www.pkware.com/appnote
 *
 * @author  Andreas Gohr <andi@splitbrain.org>
 * @package splitbrain\PHPArchive
 * @license MIT
 */
class Zip extends Archive
{

    protected $file = '';
    protected $fh;
    protected $memory = '';
    protected $closed = true;
    protected $writeaccess = false;
    protected $ctrl_dir;
    protected $complevel = 9;

    /**
     * Set the compression level.
     *
     * Compression Type is ignored for ZIP
     *
     * You can call this function before adding each file to set differen compression levels
     * for each file.
     *
     * @param int $level Compression level (0 to 9)
     * @param int $type  Type of compression to use ignored for ZIP
     * @throws ArchiveIllegalCompressionException
     */
    public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
    {
        if ($level < -1 || $level > 9) {
            throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9');
        }
        $this->complevel = $level;
    }

    /**
     * Open an existing ZIP file for reading
     *
     * @param string $file
     * @throws ArchiveIOException
     */
    public function open($file)
    {
        $this->file = $file;
        $this->fh   = @fopen($this->file, 'rb');
        if (!$this->fh) {
            throw new ArchiveIOException('Could not open file for reading: '.$this->file);
        }
        $this->closed = false;
    }

    /**
     * Read the contents of a ZIP archive
     *
     * This function lists the files stored in the archive, and returns an indexed array of FileInfo objects
     *
     * The archive is closed afer reading the contents, for API compatibility with TAR files
     * Reopen the file with open() again if you want to do additional operations
     *
     * @throws ArchiveIOException
     * @return FileInfo[]
     */
    public function contents()
    {
        if ($this->closed || !$this->file) {
            throw new ArchiveIOException('Can not read from a closed archive');
        }

        $result = array();

        $centd = $this->readCentralDir();

        @rewind($this->fh);
        @fseek($this->fh, $centd['offset']);

        for ($i = 0; $i < $centd['entries']; $i++) {
            $result[] = $this->header2fileinfo($this->readCentralFileHeader());
        }

        $this->close();
        return $result;
    }

    /**
     * Extract an existing ZIP archive
     *
     * The $strip parameter allows you to strip a certain number of path components from the filenames
     * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
     * an integer is passed as $strip.
     * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
     * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
     *
     * By default this will extract all files found in the archive. You can restrict the output using the $include
     * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
     * $include is set only files that match this expression will be extracted. Files that match the $exclude
     * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
     * stripped filenames as described above.
     *
     * @param string     $outdir  the target directory for extracting
     * @param int|string $strip   either the number of path components or a fixed prefix to strip
     * @param string     $exclude a regular expression of files to exclude
     * @param string     $include a regular expression of files to include
     * @throws ArchiveIOException
     * @return FileInfo[]
     */
    public function extract($outdir, $strip = '', $exclude = '', $include = '')
    {
        if ($this->closed || !$this->file) {
            throw new ArchiveIOException('Can not read from a closed archive');
        }

        $outdir = rtrim($outdir, '/');
        @mkdir($outdir, 0777, true);

        $extracted = array();

        $cdir      = $this->readCentralDir();
        $pos_entry = $cdir['offset']; // begin of the central file directory

        for ($i = 0; $i < $cdir['entries']; $i++) {
            // read file header
            @fseek($this->fh, $pos_entry);
            $header          = $this->readCentralFileHeader();
            $header['index'] = $i;
            $pos_entry       = ftell($this->fh); // position of the next file in central file directory
            fseek($this->fh, $header['offset']); // seek to beginning of file header
            $header   = $this->readFileHeader($header);
            $fileinfo = $this->header2fileinfo($header);

            // apply strip rules
            $fileinfo->strip($strip);

            // skip unwanted files
            if (!strlen($fileinfo->getPath()) || !$fileinfo->match($include, $exclude)) {
                continue;
            }

            $extracted[] = $fileinfo;

            // create output directory
            $output    = $outdir.'/'.$fileinfo->getPath();
            $directory = ($header['folder']) ? $output : dirname($output);
            @mkdir($directory, 0777, true);

            // nothing more to do for directories
            if ($fileinfo->getIsdir()) {
                if(is_callable($this->callback)) {
                    call_user_func($this->callback, $fileinfo);
                }
                continue;
            }

            // compressed files are written to temporary .gz file first
            if ($header['compression'] == 0) {
                $extractto = $output;
            } else {
                $extractto = $output.'.gz';
            }

            // open file for writing
            $fp = @fopen($extractto, "wb");
            if (!$fp) {
                throw new ArchiveIOException('Could not open file for writing: '.$extractto);
            }

            // prepend compression header
            if ($header['compression'] != 0) {
                $binary_data = pack(
                    'va1a1Va1a1',
                    0x8b1f,
                    chr($header['compression']),
                    chr(0x00),
                    time(),
                    chr(0x00),
                    chr(3)
                );
                fwrite($fp, $binary_data, 10);
            }

            // read the file and store it on disk
            $size = $header['compressed_size'];
            while ($size != 0) {
                $read_size   = ($size < 2048 ? $size : 2048);
                $buffer      = fread($this->fh, $read_size);
                $binary_data = pack('a'.$read_size, $buffer);
                fwrite($fp, $binary_data, $read_size);
                $size -= $read_size;
            }

            // finalize compressed file
            if ($header['compression'] != 0) {
                $binary_data = pack('VV', $header['crc'], $header['size']);
                fwrite($fp, $binary_data, 8);
            }

            // close file
            fclose($fp);

            // unpack compressed file
            if ($header['compression'] != 0) {
                $gzp = @gzopen($extractto, 'rb');
                if (!$gzp) {
                    @unlink($extractto);
                    throw new ArchiveIOException('Failed file extracting. gzip support missing?');
                }
                $fp = @fopen($output, 'wb');
                if (!$fp) {
                    throw new ArchiveIOException('Could not open file for writing: '.$extractto);
                }

                $size = $header['size'];
                while ($size != 0) {
                    $read_size   = ($size < 2048 ? $size : 2048);
                    $buffer      = gzread($gzp, $read_size);
                    $binary_data = pack('a'.$read_size, $buffer);
                    @fwrite($fp, $binary_data, $read_size);
                    $size -= $read_size;
                }
                fclose($fp);
                gzclose($gzp);
                unlink($extractto); // remove temporary gz file
            }

            @touch($output, $fileinfo->getMtime());
            //FIXME what about permissions?
            if(is_callable($this->callback)) {
                call_user_func($this->callback, $fileinfo);
            }
        }

        $this->close();
        return $extracted;
    }

    /**
     * Create a new ZIP file
     *
     * If $file is empty, the zip file will be created in memory
     *
     * @param string $file
     * @throws ArchiveIOException
     */
    public function create($file = '')
    {
        $this->file   = $file;
        $this->memory = '';
        $this->fh     = 0;

        if ($this->file) {
            $this->fh = @fopen($this->file, 'wb');

            if (!$this->fh) {
                throw new ArchiveIOException('Could not open file for writing: '.$this->file);
            }
        }
        $this->writeaccess = true;
        $this->closed      = false;
        $this->ctrl_dir    = array();
    }

    /**
     * Add a file to the current ZIP archive using an existing file in the filesystem
     *
     * @param string          $file     path to the original file
     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
     * @throws ArchiveIOException
     */

    /**
     * Add a file to the current archive using an existing file in the filesystem
     *
     * @param string $file path to the original file
     * @param string|FileInfo $fileinfo either the name to use in archive (string) or a FileInfo oject with all meta data, empty to take from original
     * @throws ArchiveIOException
     * @throws FileInfoException
     */
    public function addFile($file, $fileinfo = '')
    {
        if (is_string($fileinfo)) {
            $fileinfo = FileInfo::fromPath($file, $fileinfo);
        }

        if ($this->closed) {
            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
        }

        $data = @file_get_contents($file);
        if ($data === false) {
            throw new ArchiveIOException('Could not open file for reading: '.$file);
        }

        // FIXME could we stream writing compressed data? gzwrite on a fopen handle?
        $this->addData($fileinfo, $data);
    }

    /**
     * Add a file to the current Zip archive using the given $data as content
     *
     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
     * @param string          $data     binary content of the file to add
     * @throws ArchiveIOException
     */
    public function addData($fileinfo, $data)
    {
        if (is_string($fileinfo)) {
            $fileinfo = new FileInfo($fileinfo);
        }

        if ($this->closed) {
            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
        }

        // prepare info and compress data
        $size     = strlen($data);
        $crc      = crc32($data);
        if ($this->complevel) {
            $data = gzcompress($data, $this->complevel);
            $data = substr($data, 2, -4); // strip compression headers
        }
        $csize  = strlen($data);
        $offset = $this->dataOffset();
        $name   = $fileinfo->getPath();
        $time   = $fileinfo->getMtime();

        // write local file header
        $this->writebytes($this->makeLocalFileHeader(
            $time,
            $crc,
            $size,
            $csize,
            $name,
            (bool) $this->complevel
        ));

        // we store no encryption header

        // write data
        $this->writebytes($data);

        // we store no data descriptor

        // add info to central file directory
        $this->ctrl_dir[] = $this->makeCentralFileRecord(
            $offset,
            $time,
            $crc,
            $size,
            $csize,
            $name,
            (bool) $this->complevel
        );

        if(is_callable($this->callback)) {
            call_user_func($this->callback, $fileinfo);
        }
    }

    /**
     * Add the closing footer to the archive if in write mode, close all file handles
     *
     * After a call to this function no more data can be added to the archive, for
     * read access no reading is allowed anymore
     * @throws ArchiveIOException
     */
    public function close()
    {
        if ($this->closed) {
            return;
        } // we did this already

        if ($this->writeaccess) {
            // write central directory
            $offset = $this->dataOffset();
            $ctrldir = join('', $this->ctrl_dir);
            $this->writebytes($ctrldir);

            // write end of central directory record
            $this->writebytes("\x50\x4b\x05\x06"); // end of central dir signature
            $this->writebytes(pack('v', 0)); // number of this disk
            $this->writebytes(pack('v', 0)); // number of the disk with the start of the central directory
            $this->writebytes(pack('v',
                count($this->ctrl_dir))); // total number of entries in the central directory on this disk
            $this->writebytes(pack('v', count($this->ctrl_dir))); // total number of entries in the central directory
            $this->writebytes(pack('V', strlen($ctrldir))); // size of the central directory
            $this->writebytes(pack('V',
                $offset)); // offset of start of central directory with respect to the starting disk number
            $this->writebytes(pack('v', 0)); // .ZIP file comment length

            $this->ctrl_dir = array();
        }

        // close file handles
        if ($this->file) {
            fclose($this->fh);
            $this->file = '';
            $this->fh   = 0;
        }

        $this->writeaccess = false;
        $this->closed      = true;
    }

    /**
     * Returns the created in-memory archive data
     *
     * This implicitly calls close() on the Archive
     * @throws ArchiveIOException
     */
    public function getArchive()
    {
        $this->close();

        return $this->memory;
    }

    /**
     * Save the created in-memory archive data
     *
     * Note: It's more memory effective to specify the filename in the create() function and
     * let the library work on the new file directly.
     *
     * @param     $file
     * @throws ArchiveIOException
     */
    public function save($file)
    {
        if (!@file_put_contents($file, $this->getArchive())) {
            throw new ArchiveIOException('Could not write to file: '.$file);
        }
    }

    /**
     * Read the central directory
     *
     * This key-value list contains general information about the ZIP file
     *
     * @return array
     */
    protected function readCentralDir()
    {
        $size = filesize($this->file);
        if ($size < 277) {
            $maximum_size = $size;
        } else {
            $maximum_size = 277;
        }

        @fseek($this->fh, $size - $maximum_size);
        $pos   = ftell($this->fh);
        $bytes = 0x00000000;

        while ($pos < $size) {
            $byte  = @fread($this->fh, 1);
            $bytes = (($bytes << 8) & 0xFFFFFFFF) | ord($byte);
            if ($bytes == 0x504b0506) {
                break;
            }
            $pos++;
        }

        $data = unpack(
            'vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size',
            fread($this->fh, 18)
        );

        if ($data['comment_size'] != 0) {
            $centd['comment'] = fread($this->fh, $data['comment_size']);
        } else {
            $centd['comment'] = '';
        }
        $centd['entries']      = $data['entries'];
        $centd['disk_entries'] = $data['disk_entries'];
        $centd['offset']       = $data['offset'];
        $centd['disk_start']   = $data['disk_start'];
        $centd['size']         = $data['size'];
        $centd['disk']         = $data['disk'];
        return $centd;
    }

    /**
     * Read the next central file header
     *
     * Assumes the current file pointer is pointing at the right position
     *
     * @return array
     */
    protected function readCentralFileHeader()
    {
        $binary_data = fread($this->fh, 46);
        $header      = unpack(
            'vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset',
            $binary_data
        );

        if ($header['filename_len'] != 0) {
            $header['filename'] = fread($this->fh, $header['filename_len']);
        } else {
            $header['filename'] = '';
        }

        if ($header['extra_len'] != 0) {
            $header['extra'] = fread($this->fh, $header['extra_len']);
            $header['extradata'] = $this->parseExtra($header['extra']);
        } else {
            $header['extra'] = '';
            $header['extradata'] = array();
        }

        if ($header['comment_len'] != 0) {
            $header['comment'] = fread($this->fh, $header['comment_len']);
        } else {
            $header['comment'] = '';
        }

        $header['mtime']           = $this->makeUnixTime($header['mdate'], $header['mtime']);
        $header['stored_filename'] = $header['filename'];
        $header['status']          = 'ok';
        if (substr($header['filename'], -1) == '/') {
            $header['external'] = 0x41FF0010;
        }
        $header['folder'] = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0;

        return $header;
    }

    /**
     * Reads the local file header
     *
     * This header precedes each individual file inside the zip file. Assumes the current file pointer is pointing at
     * the right position already. Enhances the given central header with the data found at the local header.
     *
     * @param array $header the central file header read previously (see above)
     * @return array
     */
    protected function readFileHeader($header)
    {
        $binary_data = fread($this->fh, 30);
        $data        = unpack(
            'vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len',
            $binary_data
        );

        $header['filename'] = fread($this->fh, $data['filename_len']);
        if ($data['extra_len'] != 0) {
            $header['extra'] = fread($this->fh, $data['extra_len']);
            $header['extradata'] = array_merge($header['extradata'],  $this->parseExtra($header['extra']));
        } else {
            $header['extra'] = '';
            $header['extradata'] = array();
        }

        $header['compression'] = $data['compression'];
        foreach (array(
                     'size',
                     'compressed_size',
                     'crc'
                 ) as $hd) { // On ODT files, these headers are 0. Keep the previous value.
            if ($data[$hd] != 0) {
                $header[$hd] = $data[$hd];
            }
        }
        $header['flag']  = $data['flag'];
        $header['mtime'] = $this->makeUnixTime($data['mdate'], $data['mtime']);

        $header['stored_filename'] = $header['filename'];
        $header['status']          = "ok";
        $header['folder']          = ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0;
        return $header;
    }

    /**
     * Parse the extra headers into fields
     *
     * @param string $header
     * @return array
     */
    protected function parseExtra($header)
    {
        $extra = array();
        // parse all extra fields as raw values
        while (strlen($header) !== 0) {
            $set = unpack('vid/vlen', $header);
            $header = substr($header, 4);
            $value = substr($header, 0, $set['len']);
            $header = substr($header, $set['len']);
            $extra[$set['id']] = $value;
        }

        // handle known ones
        if(isset($extra[0x6375])) {
            $extra['utf8comment'] = substr($extra[0x7075], 5); // strip version and crc
        }
        if(isset($extra[0x7075])) {
            $extra['utf8path'] = substr($extra[0x7075], 5); // strip version and crc
        }

        return $extra;
    }

    /**
     * Create fileinfo object from header data
     *
     * @param $header
     * @return FileInfo
     */
    protected function header2fileinfo($header)
    {
        $fileinfo = new FileInfo();
        $fileinfo->setSize($header['size']);
        $fileinfo->setCompressedSize($header['compressed_size']);
        $fileinfo->setMtime($header['mtime']);
        $fileinfo->setComment($header['comment']);
        $fileinfo->setIsdir($header['external'] == 0x41FF0010 || $header['external'] == 16);

        if(isset($header['extradata']['utf8path'])) {
            $fileinfo->setPath($header['extradata']['utf8path']);
        } else {
            $fileinfo->setPath($this->cpToUtf8($header['filename']));
        }

        if(isset($header['extradata']['utf8comment'])) {
            $fileinfo->setComment($header['extradata']['utf8comment']);
        } else {
            $fileinfo->setComment($this->cpToUtf8($header['comment']));
        }

        return $fileinfo;
    }

    /**
     * Convert the given CP437 encoded string to UTF-8
     *
     * Tries iconv with the correct encoding first, falls back to mbstring with CP850 which is
     * similar enough. CP437 seems not to be available in mbstring. Lastly falls back to keeping the
     * string as is, which is still better than nothing.
     *
     * On some systems iconv is available, but the codepage is not. We also check for that.
     *
     * @param $string
     * @return string
     */
    protected function cpToUtf8($string)
    {
        if (function_exists('iconv') && @iconv_strlen('', 'CP437') !== false) {
            return iconv('CP437', 'UTF-8', $string);
        } elseif (function_exists('mb_convert_encoding')) {
            return mb_convert_encoding($string, 'UTF-8', 'CP850');
        } else {
            return $string;
        }
    }

    /**
     * Convert the given UTF-8 encoded string to CP437
     *
     * Same caveats as for cpToUtf8() apply
     *
     * @param $string
     * @return string
     */
    protected function utf8ToCp($string)
    {
        // try iconv first
        if (function_exists('iconv')) {
            $conv = @iconv('UTF-8', 'CP437//IGNORE', $string);
            if($conv) return $conv; // it worked
        }

        // still here? iconv failed to convert the string. Try another method
        // see http://php.net/manual/en/function.iconv.php#108643

        if (function_exists('mb_convert_encoding')) {
            return mb_convert_encoding($string, 'CP850', 'UTF-8');
        } else {
            return $string;
        }
    }


    /**
     * Write to the open filepointer or memory
     *
     * @param string $data
     * @throws ArchiveIOException
     * @return int number of bytes written
     */
    protected function writebytes($data)
    {
        if (!$this->file) {
            $this->memory .= $data;
            $written = strlen($data);
        } else {
            $written = @fwrite($this->fh, $data);
        }
        if ($written === false) {
            throw new ArchiveIOException('Failed to write to archive stream');
        }
        return $written;
    }

    /**
     * Current data pointer position
     *
     * @fixme might need a -1
     * @return int
     */
    protected function dataOffset()
    {
        if ($this->file) {
            return ftell($this->fh);
        } else {
            return strlen($this->memory);
        }
    }

    /**
     * Create a DOS timestamp from a UNIX timestamp
     *
     * DOS timestamps start at 1980-01-01, earlier UNIX stamps will be set to this date
     *
     * @param $time
     * @return int
     */
    protected function makeDosTime($time)
    {
        $timearray = getdate($time);
        if ($timearray['year'] < 1980) {
            $timearray['year']    = 1980;
            $timearray['mon']     = 1;
            $timearray['mday']    = 1;
            $timearray['hours']   = 0;
            $timearray['minutes'] = 0;
            $timearray['seconds'] = 0;
        }
        return (($timearray['year'] - 1980) << 25) |
        ($timearray['mon'] << 21) |
        ($timearray['mday'] << 16) |
        ($timearray['hours'] << 11) |
        ($timearray['minutes'] << 5) |
        ($timearray['seconds'] >> 1);
    }

    /**
     * Create a UNIX timestamp from a DOS timestamp
     *
     * @param $mdate
     * @param $mtime
     * @return int
     */
    protected function makeUnixTime($mdate = null, $mtime = null)
    {
        if ($mdate && $mtime) {
            $year = (($mdate & 0xFE00) >> 9) + 1980;
            $month = ($mdate & 0x01E0) >> 5;
            $day = $mdate & 0x001F;

            $hour = ($mtime & 0xF800) >> 11;
            $minute = ($mtime & 0x07E0) >> 5;
            $seconde = ($mtime & 0x001F) << 1;

            $mtime = mktime($hour, $minute, $seconde, $month, $day, $year);
        } else {
            $mtime = time();
        }

        return $mtime;
    }

    /**
     * Returns a local file header for the given data
     *
     * @param int $offset location of the local header
     * @param int $ts unix timestamp
     * @param int $crc CRC32 checksum of the uncompressed data
     * @param int $len length of the uncompressed data
     * @param int $clen length of the compressed data
     * @param string $name file name
     * @param boolean|null $comp if compression is used, if null it's determined from $len != $clen
     * @return string
     */
    protected function makeCentralFileRecord($offset, $ts, $crc, $len, $clen, $name, $comp = null)
    {
        if(is_null($comp)) $comp = $len != $clen;
        $comp = $comp ? 8 : 0;
        $dtime = dechex($this->makeDosTime($ts));

        list($name, $extra) = $this->encodeFilename($name);

        $header = "\x50\x4b\x01\x02"; // central file header signature
        $header .= pack('v', 14); // version made by - VFAT
        $header .= pack('v', 20); // version needed to extract - 2.0
        $header .= pack('v', 0); // general purpose flag - no flags set
        $header .= pack('v', $comp); // compression method - deflate|none
        $header .= pack(
            'H*',
            $dtime[6] . $dtime[7] .
            $dtime[4] . $dtime[5] .
            $dtime[2] . $dtime[3] .
            $dtime[0] . $dtime[1]
        ); //  last mod file time and date
        $header .= pack('V', $crc); // crc-32
        $header .= pack('V', $clen); // compressed size
        $header .= pack('V', $len); // uncompressed size
        $header .= pack('v', strlen($name)); // file name length
        $header .= pack('v', strlen($extra)); // extra field length
        $header .= pack('v', 0); // file comment length
        $header .= pack('v', 0); // disk number start
        $header .= pack('v', 0); // internal file attributes
        $header .= pack('V', 0); // external file attributes  @todo was 0x32!?
        $header .= pack('V', $offset); // relative offset of local header
        $header .= $name; // file name
        $header .= $extra; // extra (utf-8 filename)

        return $header;
    }

    /**
     * Returns a local file header for the given data
     *
     * @param int $ts unix timestamp
     * @param int $crc CRC32 checksum of the uncompressed data
     * @param int $len length of the uncompressed data
     * @param int $clen length of the compressed data
     * @param string $name file name
     * @param boolean|null $comp if compression is used, if null it's determined from $len != $clen
     * @return string
     */
    protected function makeLocalFileHeader($ts, $crc, $len, $clen, $name, $comp = null)
    {
        if(is_null($comp)) $comp = $len != $clen;
        $comp = $comp ? 8 : 0;
        $dtime = dechex($this->makeDosTime($ts));

        list($name, $extra) = $this->encodeFilename($name);

        $header = "\x50\x4b\x03\x04"; //  local file header signature
        $header .= pack('v', 20); // version needed to extract - 2.0
        $header .= pack('v', 0); // general purpose flag - no flags set
        $header .= pack('v', $comp); // compression method - deflate|none
        $header .= pack(
            'H*',
            $dtime[6] . $dtime[7] .
            $dtime[4] . $dtime[5] .
            $dtime[2] . $dtime[3] .
            $dtime[0] . $dtime[1]
        ); //  last mod file time and date
        $header .= pack('V', $crc); // crc-32
        $header .= pack('V', $clen); // compressed size
        $header .= pack('V', $len); // uncompressed size
        $header .= pack('v', strlen($name)); // file name length
        $header .= pack('v', strlen($extra)); // extra field length
        $header .= $name; // file name
        $header .= $extra; // extra (utf-8 filename)
        return $header;
    }

    /**
     * Returns an allowed filename and an extra field header
     *
     * When encoding stuff outside the 7bit ASCII range it needs to be placed in a separate
     * extra field
     *
     * @param $original
     * @return array($filename, $extra)
     */
    protected function encodeFilename($original)
    {
        $cp437 = $this->utf8ToCp($original);
        if ($cp437 === $original) {
            return array($original, '');
        }

        $extra = pack(
            'vvCV',
            0x7075, // tag
            strlen($original) + 5, // length of file + version + crc
            1, // version
            crc32($original) // crc
        );
        $extra .= $original;

        return array($cp437, $extra);
    }
}
<?php

namespace splitbrain\PHPArchive;

/**
 * Class Tar
 *
 * Creates or extracts Tar archives. Supports gz and bzip compression
 *
 * Long pathnames (>100 chars) are supported in POSIX ustar and GNU longlink formats.
 *
 * @author  Andreas Gohr <andi@splitbrain.org>
 * @package splitbrain\PHPArchive
 * @license MIT
 */
class Tar extends Archive
{

    protected $file = '';
    protected $comptype = Archive::COMPRESS_AUTO;
    protected $complevel = 9;
    protected $fh;
    protected $memory = '';
    protected $closed = true;
    protected $writeaccess = false;

    /**
     * Sets the compression to use
     *
     * @param int $level Compression level (0 to 9)
     * @param int $type Type of compression to use (use COMPRESS_* constants)
     * @throws ArchiveIllegalCompressionException
     */
    public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
    {
        $this->compressioncheck($type);
        if ($level < -1 || $level > 9) {
            throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9');
        }
        $this->comptype  = $type;
        $this->complevel = $level;
        if($level == 0) $this->comptype = Archive::COMPRESS_NONE;
        if($type == Archive::COMPRESS_NONE) $this->complevel = 0;
    }

    /**
     * Open an existing TAR file for reading
     *
     * @param string $file
     * @throws ArchiveIOException
     * @throws ArchiveIllegalCompressionException
     */
    public function open($file)
    {
        $this->file = $file;

        // update compression to mach file
        if ($this->comptype == Tar::COMPRESS_AUTO) {
            $this->setCompression($this->complevel, $this->filetype($file));
        }

        // open file handles
        if ($this->comptype === Archive::COMPRESS_GZIP) {
            $this->fh = @gzopen($this->file, 'rb');
        } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
            $this->fh = @bzopen($this->file, 'r');
        } else {
            $this->fh = @fopen($this->file, 'rb');
        }

        if (!$this->fh) {
            throw new ArchiveIOException('Could not open file for reading: '.$this->file);
        }
        $this->closed = false;
    }

    /**
     * Read the contents of a TAR archive
     *
     * This function lists the files stored in the archive
     *
     * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
     * Reopen the file with open() again if you want to do additional operations
     *
     * @throws ArchiveIOException
     * @throws ArchiveCorruptedException
     * @returns FileInfo[]
     */
    public function contents()
    {
        if ($this->closed || !$this->file) {
            throw new ArchiveIOException('Can not read from a closed archive');
        }

        $result = array();
        while ($read = $this->readbytes(512)) {
            $header = $this->parseHeader($read);
            if (!is_array($header)) {
                continue;
            }

            $this->skipbytes(ceil($header['size'] / 512) * 512);
            $result[] = $this->header2fileinfo($header);
        }

        $this->close();
        return $result;
    }

    /**
     * Extract an existing TAR archive
     *
     * The $strip parameter allows you to strip a certain number of path components from the filenames
     * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
     * an integer is passed as $strip.
     * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
     * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
     *
     * By default this will extract all files found in the archive. You can restrict the output using the $include
     * and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
     * $include is set only files that match this expression will be extracted. Files that match the $exclude
     * expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
     * stripped filenames as described above.
     *
     * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
     * Reopen the file with open() again if you want to do additional operations
     *
     * @param string $outdir the target directory for extracting
     * @param int|string $strip either the number of path components or a fixed prefix to strip
     * @param string $exclude a regular expression of files to exclude
     * @param string $include a regular expression of files to include
     * @throws ArchiveIOException
     * @throws ArchiveCorruptedException
     * @return FileInfo[]
     */
    public function extract($outdir, $strip = '', $exclude = '', $include = '')
    {
        if ($this->closed || !$this->file) {
            throw new ArchiveIOException('Can not read from a closed archive');
        }

        $outdir = rtrim($outdir, '/');
        @mkdir($outdir, 0777, true);
        if (!is_dir($outdir)) {
            throw new ArchiveIOException("Could not create directory '$outdir'");
        }

        $extracted = array();
        while ($dat = $this->readbytes(512)) {
            // read the file header
            $header = $this->parseHeader($dat);
            if (!is_array($header)) {
                continue;
            }
            $fileinfo = $this->header2fileinfo($header);

            // apply strip rules
            $fileinfo->strip($strip);

            // skip unwanted files
            if (!strlen($fileinfo->getPath()) || !$fileinfo->match($include, $exclude)) {
                $this->skipbytes(ceil($header['size'] / 512) * 512);
                continue;
            }

            // create output directory
            $output    = $outdir.'/'.$fileinfo->getPath();
            $directory = ($fileinfo->getIsdir()) ? $output : dirname($output);
            @mkdir($directory, 0777, true);

            // extract data
            if (!$fileinfo->getIsdir()) {
                $fp = @fopen($output, "wb");
                if (!$fp) {
                    throw new ArchiveIOException('Could not open file for writing: '.$output);
                }

                $size = floor($header['size'] / 512);
                for ($i = 0; $i < $size; $i++) {
                    fwrite($fp, $this->readbytes(512), 512);
                }
                if (($header['size'] % 512) != 0) {
                    fwrite($fp, $this->readbytes(512), $header['size'] % 512);
                }

                fclose($fp);
                @touch($output, $fileinfo->getMtime());
                @chmod($output, $fileinfo->getMode());
            } else {
                $this->skipbytes(ceil($header['size'] / 512) * 512); // the size is usually 0 for directories
            }

            if(is_callable($this->callback)) {
                call_user_func($this->callback, $fileinfo);
            }
            $extracted[] = $fileinfo;
        }

        $this->close();
        return $extracted;
    }

    /**
     * Create a new TAR file
     *
     * If $file is empty, the tar file will be created in memory
     *
     * @param string $file
     * @throws ArchiveIOException
     * @throws ArchiveIllegalCompressionException
     */
    public function create($file = '')
    {
        $this->file   = $file;
        $this->memory = '';
        $this->fh     = 0;

        if ($this->file) {
            // determine compression
            if ($this->comptype == Archive::COMPRESS_AUTO) {
                $this->setCompression($this->complevel, $this->filetype($file));
            }

            if ($this->comptype === Archive::COMPRESS_GZIP) {
                $this->fh = @gzopen($this->file, 'wb'.$this->complevel);
            } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
                $this->fh = @bzopen($this->file, 'w');
            } else {
                $this->fh = @fopen($this->file, 'wb');
            }

            if (!$this->fh) {
                throw new ArchiveIOException('Could not open file for writing: '.$this->file);
            }
        }
        $this->writeaccess = true;
        $this->closed      = false;
    }

    /**
     * Add a file to the current TAR archive using an existing file in the filesystem
     *
     * @param string $file path to the original file
     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
     * @throws ArchiveCorruptedException when the file changes while reading it, the archive will be corrupt and should be deleted
     * @throws ArchiveIOException there was trouble reading the given file, it was not added
     * @throws FileInfoException trouble reading file info, it was not added
     */
    public function addFile($file, $fileinfo = '')
    {
        if (is_string($fileinfo)) {
            $fileinfo = FileInfo::fromPath($file, $fileinfo);
        }

        if ($this->closed) {
            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
        }

        $fp = @fopen($file, 'rb');
        if (!$fp) {
            throw new ArchiveIOException('Could not open file for reading: '.$file);
        }

        // create file header
        $this->writeFileHeader($fileinfo);

        // write data
        $read = 0;
        while (!feof($fp)) {
            $data = fread($fp, 512);
            $read += strlen($data);
            if ($data === false) {
                break;
            }
            if ($data === '') {
                break;
            }
            $packed = pack("a512", $data);
            $this->writebytes($packed);
        }
        fclose($fp);

        if($read != $fileinfo->getSize()) {
            $this->close();
            throw new ArchiveCorruptedException("The size of $file changed while reading, archive corrupted. read $read expected ".$fileinfo->getSize());
        }

        if(is_callable($this->callback)) {
            call_user_func($this->callback, $fileinfo);
        }
    }

    /**
     * Add a file to the current TAR archive using the given $data as content
     *
     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
     * @param string          $data     binary content of the file to add
     * @throws ArchiveIOException
     */
    public function addData($fileinfo, $data)
    {
        if (is_string($fileinfo)) {
            $fileinfo = new FileInfo($fileinfo);
        }

        if ($this->closed) {
            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
        }

        $len = strlen($data);
        $fileinfo->setSize($len);
        $this->writeFileHeader($fileinfo);

        for ($s = 0; $s < $len; $s += 512) {
            $this->writebytes(pack("a512", substr($data, $s, 512)));
        }

        if (is_callable($this->callback)) {
            call_user_func($this->callback, $fileinfo);
        }
    }

    /**
     * Add the closing footer to the archive if in write mode, close all file handles
     *
     * After a call to this function no more data can be added to the archive, for
     * read access no reading is allowed anymore
     *
     * "Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which
     * consists of two 512 blocks of zero bytes"
     *
     * @link http://www.gnu.org/software/tar/manual/html_chapter/tar_8.html#SEC134
     * @throws ArchiveIOException
     */
    public function close()
    {
        if ($this->closed) {
            return;
        } // we did this already

        // write footer
        if ($this->writeaccess) {
            $this->writebytes(pack("a512", ""));
            $this->writebytes(pack("a512", ""));
        }

        // close file handles
        if ($this->file) {
            if ($this->comptype === Archive::COMPRESS_GZIP) {
                gzclose($this->fh);
            } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
                bzclose($this->fh);
            } else {
                fclose($this->fh);
            }

            $this->file = '';
            $this->fh   = 0;
        }

        $this->writeaccess = false;
        $this->closed      = true;
    }

    /**
     * Returns the created in-memory archive data
     *
     * This implicitly calls close() on the Archive
     * @throws ArchiveIOException
     */
    public function getArchive()
    {
        $this->close();

        if ($this->comptype === Archive::COMPRESS_AUTO) {
            $this->comptype = Archive::COMPRESS_NONE;
        }

        if ($this->comptype === Archive::COMPRESS_GZIP) {
            return gzencode($this->memory, $this->complevel);
        }
        if ($this->comptype === Archive::COMPRESS_BZIP) {
            return bzcompress($this->memory);
        }
        return $this->memory;
    }

    /**
     * Save the created in-memory archive data
     *
     * Note: It more memory effective to specify the filename in the create() function and
     * let the library work on the new file directly.
     *
     * @param string $file
     * @throws ArchiveIOException
     * @throws ArchiveIllegalCompressionException
     */
    public function save($file)
    {
        if ($this->comptype === Archive::COMPRESS_AUTO) {
            $this->setCompression($this->complevel, $this->filetype($file));
        }

        if (!@file_put_contents($file, $this->getArchive())) {
            throw new ArchiveIOException('Could not write to file: '.$file);
        }
    }

    /**
     * Read from the open file pointer
     *
     * @param int $length bytes to read
     * @return string
     */
    protected function readbytes($length)
    {
        if ($this->comptype === Archive::COMPRESS_GZIP) {
            return @gzread($this->fh, $length);
        } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
            return @bzread($this->fh, $length);
        } else {
            return @fread($this->fh, $length);
        }
    }

    /**
     * Write to the open filepointer or memory
     *
     * @param string $data
     * @throws ArchiveIOException
     * @return int number of bytes written
     */
    protected function writebytes($data)
    {
        if (!$this->file) {
            $this->memory .= $data;
            $written = strlen($data);
        } elseif ($this->comptype === Archive::COMPRESS_GZIP) {
            $written = @gzwrite($this->fh, $data);
        } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
            $written = @bzwrite($this->fh, $data);
        } else {
            $written = @fwrite($this->fh, $data);
        }
        if ($written === false) {
            throw new ArchiveIOException('Failed to write to archive stream');
        }
        return $written;
    }

    /**
     * Skip forward in the open file pointer
     *
     * This is basically a wrapper around seek() (and a workaround for bzip2)
     *
     * @param int $bytes seek to this position
     */
    protected function skipbytes($bytes)
    {
        if ($this->comptype === Archive::COMPRESS_GZIP) {
            @gzseek($this->fh, $bytes, SEEK_CUR);
        } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
            // there is no seek in bzip2, we simply read on
            // bzread allows to read a max of 8kb at once
            while($bytes) {
                $toread = min(8192, $bytes);
                @bzread($this->fh, $toread);
                $bytes -= $toread;
            }
        } else {
            @fseek($this->fh, $bytes, SEEK_CUR);
        }
    }

    /**
     * Write the given file meta data as header
     *
     * @param FileInfo $fileinfo
     * @throws ArchiveIOException
     */
    protected function writeFileHeader(FileInfo $fileinfo)
    {
        $this->writeRawFileHeader(
            $fileinfo->getPath(),
            $fileinfo->getUid(),
            $fileinfo->getGid(),
            $fileinfo->getMode(),
            $fileinfo->getSize(),
            $fileinfo->getMtime(),
            $fileinfo->getIsdir() ? '5' : '0'
        );
    }

    /**
     * Write a file header to the stream
     *
     * @param string $name
     * @param int $uid
     * @param int $gid
     * @param int $perm
     * @param int $size
     * @param int $mtime
     * @param string $typeflag Set to '5' for directories
     * @throws ArchiveIOException
     */
    protected function writeRawFileHeader($name, $uid, $gid, $perm, $size, $mtime, $typeflag = '')
    {
        // handle filename length restrictions
        $prefix  = '';
        $namelen = strlen($name);
        if ($namelen > 100) {
            $file = basename($name);
            $dir  = dirname($name);
            if (strlen($file) > 100 || strlen($dir) > 155) {
                // we're still too large, let's use GNU longlink
                $this->writeRawFileHeader('././@LongLink', 0, 0, 0, $namelen, 0, 'L');
                for ($s = 0; $s < $namelen; $s += 512) {
                    $this->writebytes(pack("a512", substr($name, $s, 512)));
                }
                $name = substr($name, 0, 100); // cut off name
            } else {
                // we're fine when splitting, use POSIX ustar
                $prefix = $dir;
                $name   = $file;
            }
        }

        // values are needed in octal
        $uid   = sprintf("%6s ", decoct($uid));
        $gid   = sprintf("%6s ", decoct($gid));
        $perm  = sprintf("%6s ", decoct($perm));
        $size  = sprintf("%11s ", decoct($size));
        $mtime = sprintf("%11s", decoct($mtime));

        $data_first = pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime);
        $data_last  = pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, "");

        for ($i = 0, $chks = 0; $i < 148; $i++) {
            $chks += ord($data_first[$i]);
        }

        for ($i = 156, $chks += 256, $j = 0; $i < 512; $i++, $j++) {
            $chks += ord($data_last[$j]);
        }

        $this->writebytes($data_first);

        $chks = pack("a8", sprintf("%6s ", decoct($chks)));
        $this->writebytes($chks.$data_last);
    }

    /**
     * Decode the given tar file header
     *
     * @param string $block a 512 byte block containing the header data
     * @return array|false returns false when this was a null block
     * @throws ArchiveCorruptedException
     */
    protected function parseHeader($block)
    {
        if (!$block || strlen($block) != 512) {
            throw new ArchiveCorruptedException('Unexpected length of header');
        }

        // null byte blocks are ignored
        if(trim($block) === '') return false;

        for ($i = 0, $chks = 0; $i < 148; $i++) {
            $chks += ord($block[$i]);
        }

        for ($i = 156, $chks += 256; $i < 512; $i++) {
            $chks += ord($block[$i]);
        }

        $header = @unpack(
            "a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix",
            $block
        );
        if (!$header) {
            throw new ArchiveCorruptedException('Failed to parse header');
        }

        $return['checksum'] = OctDec(trim($header['checksum']));
        if ($return['checksum'] != $chks) {
            throw new ArchiveCorruptedException('Header does not match it\'s checksum');
        }

        $return['filename'] = trim($header['filename']);
        $return['perm']     = OctDec(trim($header['perm']));
        $return['uid']      = OctDec(trim($header['uid']));
        $return['gid']      = OctDec(trim($header['gid']));
        $return['size']     = OctDec(trim($header['size']));
        $return['mtime']    = OctDec(trim($header['mtime']));
        $return['typeflag'] = $header['typeflag'];
        $return['link']     = trim($header['link']);
        $return['uname']    = trim($header['uname']);
        $return['gname']    = trim($header['gname']);

        // Handle ustar Posix compliant path prefixes
        if (trim($header['prefix'])) {
            $return['filename'] = trim($header['prefix']).'/'.$return['filename'];
        }

        // Handle Long-Link entries from GNU Tar
        if ($return['typeflag'] == 'L') {
            // following data block(s) is the filename
            $filename = trim($this->readbytes(ceil($return['size'] / 512) * 512));
            // next block is the real header
            $block  = $this->readbytes(512);
            $return = $this->parseHeader($block);
            // overwrite the filename
            $return['filename'] = $filename;
        }

        return $return;
    }

    /**
     * Creates a FileInfo object from the given parsed header
     *
     * @param $header
     * @return FileInfo
     */
    protected function header2fileinfo($header)
    {
        $fileinfo = new FileInfo();
        $fileinfo->setPath($header['filename']);
        $fileinfo->setMode($header['perm']);
        $fileinfo->setUid($header['uid']);
        $fileinfo->setGid($header['gid']);
        $fileinfo->setSize($header['size']);
        $fileinfo->setMtime($header['mtime']);
        $fileinfo->setOwner($header['uname']);
        $fileinfo->setGroup($header['gname']);
        $fileinfo->setIsdir((bool) $header['typeflag']);

        return $fileinfo;
    }

    /**
     * Checks if the given compression type is available and throws an exception if not
     *
     * @param $comptype
     * @throws ArchiveIllegalCompressionException
     */
    protected function compressioncheck($comptype)
    {
        if ($comptype === Archive::COMPRESS_GZIP && !function_exists('gzopen')) {
            throw new ArchiveIllegalCompressionException('No gzip support available');
        }

        if ($comptype === Archive::COMPRESS_BZIP && !function_exists('bzopen')) {
            throw new ArchiveIllegalCompressionException('No bzip2 support available');
        }
    }

    /**
     * Guesses the wanted compression from the given file
     *
     * Uses magic bytes for existing files, the file extension otherwise
     *
     * You don't need to call this yourself. It's used when you pass Archive::COMPRESS_AUTO somewhere
     *
     * @param string $file
     * @return int
     */
    public function filetype($file)
    {
        // for existing files, try to read the magic bytes
        if(file_exists($file) && is_readable($file) && filesize($file) > 5) {
            $fh = @fopen($file, 'rb');
            if(!$fh) return false;
            $magic = fread($fh, 5);
            fclose($fh);

            if(strpos($magic, "\x42\x5a") === 0) return Archive::COMPRESS_BZIP;
            if(strpos($magic, "\x1f\x8b") === 0) return Archive::COMPRESS_GZIP;
        }

        // otherwise rely on file name
        $file = strtolower($file);
        if (substr($file, -3) == '.gz' || substr($file, -4) == '.tgz') {
            return Archive::COMPRESS_GZIP;
        } elseif (substr($file, -4) == '.bz2' || substr($file, -4) == '.tbz') {
            return Archive::COMPRESS_BZIP;
        }

        return Archive::COMPRESS_NONE;
    }

}
<?php

namespace splitbrain\PHPArchive;

/**
 * The archive is unreadable
 */
class ArchiveCorruptedException extends \Exception
{
}<?php

namespace splitbrain\PHPArchive;

/**
 * Read/Write Errors
 */
class ArchiveIOException extends \Exception
{
}<?php

namespace splitbrain\PHPArchive;

/**
 * Class FileInfo
 *
 * stores meta data about a file in an Archive
 *
 * @author  Andreas Gohr <andi@splitbrain.org>
 * @package splitbrain\PHPArchive
 * @license MIT
 */
class FileInfo
{

    protected $isdir = false;
    protected $path = '';
    protected $size = 0;
    protected $csize = 0;
    protected $mtime = 0;
    protected $mode = 0664;
    protected $owner = '';
    protected $group = '';
    protected $uid = 0;
    protected $gid = 0;
    protected $comment = '';

    /**
     * initialize dynamic defaults
     *
     * @param string $path The path of the file, can also be set later through setPath()
     */
    public function __construct($path = '')
    {
        $this->mtime = time();
        $this->setPath($path);
    }

    /**
     * Factory to build FileInfo from existing file or directory
     *
     * @param string $path path to a file on the local file system
     * @param string $as   optional path to use inside the archive
     * @throws FileInfoException
     * @return FileInfo
     */
    public static function fromPath($path, $as = '')
    {
        clearstatcache(false, $path);

        if (!file_exists($path)) {
            throw new FileInfoException("$path does not exist");
        }

        $stat = stat($path);
        $file = new FileInfo();

        $file->setPath($path);
        $file->setIsdir(is_dir($path));
        $file->setMode(fileperms($path));
        $file->setOwner(fileowner($path));
        $file->setGroup(filegroup($path));
        $file->setSize(filesize($path));
        $file->setUid($stat['uid']);
        $file->setGid($stat['gid']);
        $file->setMtime($stat['mtime']);

        if ($as) {
            $file->setPath($as);
        }

        return $file;
    }

    /**
     * @return int the filesize. always 0 for directories
     */
    public function getSize()
    {
        if($this->isdir) return 0;
        return $this->size;
    }

    /**
     * @param int $size
     */
    public function setSize($size)
    {
        $this->size = $size;
    }

    /**
     * @return int
     */
    public function getCompressedSize()
    {
        return $this->csize;
    }

    /**
     * @param int $csize
     */
    public function setCompressedSize($csize)
    {
        $this->csize = $csize;
    }

    /**
     * @return int
     */
    public function getMtime()
    {
        return $this->mtime;
    }

    /**
     * @param int $mtime
     */
    public function setMtime($mtime)
    {
        $this->mtime = $mtime;
    }

    /**
     * @return int
     */
    public function getGid()
    {
        return $this->gid;
    }

    /**
     * @param int $gid
     */
    public function setGid($gid)
    {
        $this->gid = $gid;
    }

    /**
     * @return int
     */
    public function getUid()
    {
        return $this->uid;
    }

    /**
     * @param int $uid
     */
    public function setUid($uid)
    {
        $this->uid = $uid;
    }

    /**
     * @return string
     */
    public function getComment()
    {
        return $this->comment;
    }

    /**
     * @param string $comment
     */
    public function setComment($comment)
    {
        $this->comment = $comment;
    }

    /**
     * @return string
     */
    public function getGroup()
    {
        return $this->group;
    }

    /**
     * @param string $group
     */
    public function setGroup($group)
    {
        $this->group = $group;
    }

    /**
     * @return boolean
     */
    public function getIsdir()
    {
        return $this->isdir;
    }

    /**
     * @param boolean $isdir
     */
    public function setIsdir($isdir)
    {
        // default mode for directories
        if ($isdir && $this->mode === 0664) {
            $this->mode = 0775;
        }
        $this->isdir = $isdir;
    }

    /**
     * @return int
     */
    public function getMode()
    {
        return $this->mode;
    }

    /**
     * @param int $mode
     */
    public function setMode($mode)
    {
        $this->mode = $mode;
    }

    /**
     * @return string
     */
    public function getOwner()
    {
        return $this->owner;
    }

    /**
     * @param string $owner
     */
    public function setOwner($owner)
    {
        $this->owner = $owner;
    }

    /**
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * @param string $path
     */
    public function setPath($path)
    {
        $this->path = $this->cleanPath($path);
    }

    /**
     * Cleans up a path and removes relative parts, also strips leading slashes
     *
     * @param string $path
     * @return string
     */
    protected function cleanPath($path)
    {
        $path    = str_replace('\\', '/', $path);
        $path    = explode('/', $path);
        $newpath = array();
        foreach ($path as $p) {
            if ($p === '' || $p === '.') {
                continue;
            }
            if ($p === '..') {
                array_pop($newpath);
                continue;
            }
            array_push($newpath, $p);
        }
        return trim(implode('/', $newpath), '/');
    }

    /**
     * Strip given prefix or number of path segments from the filename
     *
     * The $strip parameter allows you to strip a certain number of path components from the filenames
     * found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
     * an integer is passed as $strip.
     * Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
     * the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
     *
     * @param  int|string $strip
     */
    public function strip($strip)
    {
        $filename = $this->getPath();
        $striplen = strlen($strip);
        if (is_int($strip)) {
            // if $strip is an integer we strip this many path components
            $parts = explode('/', $filename);
            if (!$this->getIsdir()) {
                $base = array_pop($parts); // keep filename itself
            } else {
                $base = '';
            }
            $filename = join('/', array_slice($parts, $strip));
            if ($base) {
                $filename .= "/$base";
            }
        } else {
            // if strip is a string, we strip a prefix here
            if (substr($filename, 0, $striplen) == $strip) {
                $filename = substr($filename, $striplen);
            }
        }

        $this->setPath($filename);
    }

    /**
     * Does the file match the given include and exclude expressions?
     *
     * Exclude rules take precedence over include rules
     *
     * @param string $include Regular expression of files to include
     * @param string $exclude Regular expression of files to exclude
     * @return bool
     */
    public function match($include = '', $exclude = '')
    {
        $extract = true;
        if ($include && !preg_match($include, $this->getPath())) {
            $extract = false;
        }
        if ($exclude && preg_match($exclude, $this->getPath())) {
            $extract = false;
        }

        return $extract;
    }
}

<?php

namespace splitbrain\PHPArchive;

/**
 * File meta data problems
 */
class FileInfoException extends \Exception
{
}Copyright (c) 2012 PHP Framework Interoperability Group

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is 
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in 
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
PSR Log
=======

This repository holds all interfaces/classes/traits related to
[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).

Note that this is not a logger of its own. It is merely an interface that
describes a logger. See the specification for more details.

Installation
------------

```bash
composer require psr/log
```

Usage
-----

If you need a logger, you can use the interface like this:

```php
<?php

use Psr\Log\LoggerInterface;

class Foo
{
    private $logger;

    public function __construct(LoggerInterface $logger = null)
    {
        $this->logger = $logger;
    }

    public function doSomething()
    {
        if ($this->logger) {
            $this->logger->info('Doing work');
        }
           
        try {
            $this->doSomethingElse();
        } catch (Exception $exception) {
            $this->logger->error('Oh no!', array('exception' => $exception));
        }

        // do something useful
    }
}
```

You can then pick one of the implementations of the interface to get a logger.

If you want to implement the interface, you can require this package and
implement `Psr\Log\LoggerInterface` in your code. Please read the
[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
for details.
<?php

namespace Psr\Log;

/**
 * Describes a logger-aware instance.
 */
interface LoggerAwareInterface
{
    /**
     * Sets a logger instance on the object.
     *
     * @param LoggerInterface $logger
     *
     * @return void
     */
    public function setLogger(LoggerInterface $logger);
}
<?php

namespace Psr\Log;

/**
 * Describes log levels.
 */
class LogLevel
{
    const EMERGENCY = 'emergency';
    const ALERT     = 'alert';
    const CRITICAL  = 'critical';
    const ERROR     = 'error';
    const WARNING   = 'warning';
    const NOTICE    = 'notice';
    const INFO      = 'info';
    const DEBUG     = 'debug';
}
<?php

namespace Psr\Log\Test;

use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use PHPUnit\Framework\TestCase;

/**
 * Provides a base test class for ensuring compliance with the LoggerInterface.
 *
 * Implementors can extend the class and implement abstract methods to run this
 * as part of their test suite.
 */
abstract class LoggerInterfaceTest extends TestCase
{
    /**
     * @return LoggerInterface
     */
    abstract public function getLogger();

    /**
     * This must return the log messages in order.
     *
     * The simple formatting of the messages is: "<LOG LEVEL> <MESSAGE>".
     *
     * Example ->error('Foo') would yield "error Foo".
     *
     * @return string[]
     */
    abstract public function getLogs();

    public function testImplements()
    {
        $this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger());
    }

    /**
     * @dataProvider provideLevelsAndMessages
     */
    public function testLogsAtAllLevels($level, $message)
    {
        $logger = $this->getLogger();
        $logger->{$level}($message, array('user' => 'Bob'));
        $logger->log($level, $message, array('user' => 'Bob'));

        $expected = array(
            $level.' message of level '.$level.' with context: Bob',
            $level.' message of level '.$level.' with context: Bob',
        );
        $this->assertEquals($expected, $this->getLogs());
    }

    public function provideLevelsAndMessages()
    {
        return array(
            LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'),
            LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'),
            LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'),
            LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'),
            LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'),
            LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'),
            LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'),
            LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'),
        );
    }

    /**
     * @expectedException \Psr\Log\InvalidArgumentException
     */
    public function testThrowsOnInvalidLevel()
    {
        $logger = $this->getLogger();
        $logger->log('invalid level', 'Foo');
    }

    public function testContextReplacement()
    {
        $logger = $this->getLogger();
        $logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar'));

        $expected = array('info {Message {nothing} Bob Bar a}');
        $this->assertEquals($expected, $this->getLogs());
    }

    public function testObjectCastToString()
    {
        if (method_exists($this, 'createPartialMock')) {
            $dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString'));
        } else {
            $dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString'));
        }
        $dummy->expects($this->once())
            ->method('__toString')
            ->will($this->returnValue('DUMMY'));

        $this->getLogger()->warning($dummy);

        $expected = array('warning DUMMY');
        $this->assertEquals($expected, $this->getLogs());
    }

    public function testContextCanContainAnything()
    {
        $closed = fopen('php://memory', 'r');
        fclose($closed);

        $context = array(
            'bool' => true,
            'null' => null,
            'string' => 'Foo',
            'int' => 0,
            'float' => 0.5,
            'nested' => array('with object' => new DummyTest),
            'object' => new \DateTime,
            'resource' => fopen('php://memory', 'r'),
            'closed' => $closed,
        );

        $this->getLogger()->warning('Crazy context data', $context);

        $expected = array('warning Crazy context data');
        $this->assertEquals($expected, $this->getLogs());
    }

    public function testContextExceptionKeyCanBeExceptionOrOtherValues()
    {
        $logger = $this->getLogger();
        $logger->warning('Random message', array('exception' => 'oops'));
        $logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail')));

        $expected = array(
            'warning Random message',
            'critical Uncaught Exception!'
        );
        $this->assertEquals($expected, $this->getLogs());
    }
}
<?php

namespace Psr\Log\Test;

use Psr\Log\AbstractLogger;

/**
 * Used for testing purposes.
 *
 * It records all records and gives you access to them for verification.
 *
 * @method bool hasEmergency($record)
 * @method bool hasAlert($record)
 * @method bool hasCritical($record)
 * @method bool hasError($record)
 * @method bool hasWarning($record)
 * @method bool hasNotice($record)
 * @method bool hasInfo($record)
 * @method bool hasDebug($record)
 *
 * @method bool hasEmergencyRecords()
 * @method bool hasAlertRecords()
 * @method bool hasCriticalRecords()
 * @method bool hasErrorRecords()
 * @method bool hasWarningRecords()
 * @method bool hasNoticeRecords()
 * @method bool hasInfoRecords()
 * @method bool hasDebugRecords()
 *
 * @method bool hasEmergencyThatContains($message)
 * @method bool hasAlertThatContains($message)
 * @method bool hasCriticalThatContains($message)
 * @method bool hasErrorThatContains($message)
 * @method bool hasWarningThatContains($message)
 * @method bool hasNoticeThatContains($message)
 * @method bool hasInfoThatContains($message)
 * @method bool hasDebugThatContains($message)
 *
 * @method bool hasEmergencyThatMatches($message)
 * @method bool hasAlertThatMatches($message)
 * @method bool hasCriticalThatMatches($message)
 * @method bool hasErrorThatMatches($message)
 * @method bool hasWarningThatMatches($message)
 * @method bool hasNoticeThatMatches($message)
 * @method bool hasInfoThatMatches($message)
 * @method bool hasDebugThatMatches($message)
 *
 * @method bool hasEmergencyThatPasses($message)
 * @method bool hasAlertThatPasses($message)
 * @method bool hasCriticalThatPasses($message)
 * @method bool hasErrorThatPasses($message)
 * @method bool hasWarningThatPasses($message)
 * @method bool hasNoticeThatPasses($message)
 * @method bool hasInfoThatPasses($message)
 * @method bool hasDebugThatPasses($message)
 */
class TestLogger extends AbstractLogger
{
    /**
     * @var array
     */
    public $records = [];

    public $recordsByLevel = [];

    /**
     * @inheritdoc
     */
    public function log($level, $message, array $context = [])
    {
        $record = [
            'level' => $level,
            'message' => $message,
            'context' => $context,
        ];

        $this->recordsByLevel[$record['level']][] = $record;
        $this->records[] = $record;
    }

    public function hasRecords($level)
    {
        return isset($this->recordsByLevel[$level]);
    }

    public function hasRecord($record, $level)
    {
        if (is_string($record)) {
            $record = ['message' => $record];
        }
        return $this->hasRecordThatPasses(function ($rec) use ($record) {
            if ($rec['message'] !== $record['message']) {
                return false;
            }
            if (isset($record['context']) && $rec['context'] !== $record['context']) {
                return false;
            }
            return true;
        }, $level);
    }

    public function hasRecordThatContains($message, $level)
    {
        return $this->hasRecordThatPasses(function ($rec) use ($message) {
            return strpos($rec['message'], $message) !== false;
        }, $level);
    }

    public function hasRecordThatMatches($regex, $level)
    {
        return $this->hasRecordThatPasses(function ($rec) use ($regex) {
            return preg_match($regex, $rec['message']) > 0;
        }, $level);
    }

    public function hasRecordThatPasses(callable $predicate, $level)
    {
        if (!isset($this->recordsByLevel[$level])) {
            return false;
        }
        foreach ($this->recordsByLevel[$level] as $i => $rec) {
            if (call_user_func($predicate, $rec, $i)) {
                return true;
            }
        }
        return false;
    }

    public function __call($method, $args)
    {
        if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) {
            $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
            $level = strtolower($matches[2]);
            if (method_exists($this, $genericMethod)) {
                $args[] = $level;
                return call_user_func_array([$this, $genericMethod], $args);
            }
        }
        throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
    }

    public function reset()
    {
        $this->records = [];
        $this->recordsByLevel = [];
    }
}
<?php

namespace Psr\Log\Test;

/**
 * This class is internal and does not follow the BC promise.
 *
 * Do NOT use this class in any way.
 *
 * @internal
 */
class DummyTest
{
    public function __toString()
    {
        return 'DummyTest';
    }
}
<?php

namespace Psr\Log;

/**
 * Basic Implementation of LoggerAwareInterface.
 */
trait LoggerAwareTrait
{
    /**
     * The logger instance.
     *
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * Sets a logger.
     *
     * @param LoggerInterface $logger
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
}
<?php

namespace Psr\Log;

class InvalidArgumentException extends \InvalidArgumentException
{
}
<?php

namespace Psr\Log;

/**
 * This Logger can be used to avoid conditional log calls.
 *
 * Logging should always be optional, and if no logger is provided to your
 * library creating a NullLogger instance to have something to throw logs at
 * is a good way to avoid littering your code with `if ($this->logger) { }`
 * blocks.
 */
class NullLogger extends AbstractLogger
{
    /**
     * Logs with an arbitrary level.
     *
     * @param mixed  $level
     * @param string $message
     * @param array  $context
     *
     * @return void
     *
     * @throws \Psr\Log\InvalidArgumentException
     */
    public function log($level, $message, array $context = array())
    {
        // noop
    }
}
<?php

namespace Psr\Log;

/**
 * Describes a logger instance.
 *
 * The message MUST be a string or object implementing __toString().
 *
 * The message MAY contain placeholders in the form: {foo} where foo
 * will be replaced by the context data in key "foo".
 *
 * The context array can contain arbitrary data. The only assumption that
 * can be made by implementors is that if an Exception instance is given
 * to produce a stack trace, it MUST be in a key named "exception".
 *
 * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
 * for the full interface specification.
 */
interface LoggerInterface
{
    /**
     * System is unusable.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function emergency($message, array $context = array());

    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function alert($message, array $context = array());

    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function critical($message, array $context = array());

    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function error($message, array $context = array());

    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function warning($message, array $context = array());

    /**
     * Normal but significant events.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function notice($message, array $context = array());

    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function info($message, array $context = array());

    /**
     * Detailed debug information.
     *
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     */
    public function debug($message, array $context = array());

    /**
     * Logs with an arbitrary level.
     *
     * @param mixed   $level
     * @param string  $message
     * @param mixed[] $context
     *
     * @return void
     *
     * @throws \Psr\Log\InvalidArgumentException
     */
    public function log($level, $message, array $context = array());
}
<?php

namespace Psr\Log;

/**
 * This is a simple Logger trait that classes unable to extend AbstractLogger
 * (because they extend another class, etc) can include.
 *
 * It simply delegates all log-level-specific methods to the `log` method to
 * reduce boilerplate code that a simple Logger that does the same thing with
 * messages regardless of the error level has to implement.
 */
trait LoggerTrait
{
    /**
     * System is unusable.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function emergency($message, array $context = array())
    {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }

    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function alert($message, array $context = array())
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }

    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function critical($message, array $context = array())
    {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }

    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function error($message, array $context = array())
    {
        $this->log(LogLevel::ERROR, $message, $context);
    }

    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function warning($message, array $context = array())
    {
        $this->log(LogLevel::WARNING, $message, $context);
    }

    /**
     * Normal but significant events.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function notice($message, array $context = array())
    {
        $this->log(LogLevel::NOTICE, $message, $context);
    }

    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function info($message, array $context = array())
    {
        $this->log(LogLevel::INFO, $message, $context);
    }

    /**
     * Detailed debug information.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function debug($message, array $context = array())
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }

    /**
     * Logs with an arbitrary level.
     *
     * @param mixed  $level
     * @param string $message
     * @param array  $context
     *
     * @return void
     *
     * @throws \Psr\Log\InvalidArgumentException
     */
    abstract public function log($level, $message, array $context = array());
}
<?php

namespace Psr\Log;

/**
 * This is a simple Logger implementation that other Loggers can inherit from.
 *
 * It simply delegates all log-level-specific methods to the `log` method to
 * reduce boilerplate code that a simple Logger that does the same thing with
 * messages regardless of the error level has to implement.
 */
abstract class AbstractLogger implements LoggerInterface
{
    /**
     * System is unusable.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function emergency($message, array $context = array())
    {
        $this->log(LogLevel::EMERGENCY, $message, $context);
    }

    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function alert($message, array $context = array())
    {
        $this->log(LogLevel::ALERT, $message, $context);
    }

    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function critical($message, array $context = array())
    {
        $this->log(LogLevel::CRITICAL, $message, $context);
    }

    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function error($message, array $context = array())
    {
        $this->log(LogLevel::ERROR, $message, $context);
    }

    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function warning($message, array $context = array())
    {
        $this->log(LogLevel::WARNING, $message, $context);
    }

    /**
     * Normal but significant events.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function notice($message, array $context = array())
    {
        $this->log(LogLevel::NOTICE, $message, $context);
    }

    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function info($message, array $context = array())
    {
        $this->log(LogLevel::INFO, $message, $context);
    }

    /**
     * Detailed debug information.
     *
     * @param string $message
     * @param array  $context
     *
     * @return void
     */
    public function debug($message, array $context = array())
    {
        $this->log(LogLevel::DEBUG, $message, $context);
    }
}
{
    "name": "psr/log",
    "description": "Common interface for logging libraries",
    "keywords": ["psr", "psr-3", "log"],
    "homepage": "https://github.com/php-fig/log",
    "license": "MIT",
    "authors": [
        {
            "name": "PHP-FIG",
            "homepage": "http://www.php-fig.org/"
        }
    ],
    "require": {
        "php": ">=5.3.0"
    },
    "autoload": {
        "psr-4": {
            "Psr\\Log\\": "Psr/Log/"
        }
    },
    "extra": {
        "branch-alias": {
            "dev-master": "1.1.x-dev"
        }
    }
}
Copyright (c) 2011-2016 Jordi Boggiano

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
### 1.25.4 (2020-05-22)

  * Fixed GitProcessor type error when there is no git repo present
  * Fixed normalization of SoapFault objects containing deeply nested objects as "detail"
  * Fixed support for relative paths in RotatingFileHandler

### 1.25.3 (2019-12-20)

  * Fixed formatting of resources in JsonFormatter
  * Fixed RedisHandler failing to use MULTI properly when passed a proxied Redis instance (e.g. in Symfony with lazy services)
  * Fixed FilterHandler triggering a notice when handleBatch was filtering all records passed to it
  * Fixed Turkish locale messing up the conversion of level names to their constant values

### 1.25.2 (2019-11-13)

  * Fixed normalization of Traversables to avoid traversing them as not all of them are rewindable
  * Fixed setFormatter/getFormatter to forward to the nested handler in FilterHandler, FingersCrossedHandler, BufferHandler and SamplingHandler
  * Fixed BrowserConsoleHandler formatting when using multiple styles
  * Fixed normalization of exception codes to be always integers even for PDOException which have them as numeric strings
  * Fixed normalization of SoapFault objects containing non-strings as "detail"
  * Fixed json encoding across all handlers to always attempt recovery of non-UTF-8 strings instead of failing the whole encoding

### 1.25.1 (2019-09-06)

  * Fixed forward-compatible interfaces to be compatible with Monolog 1.x too.

### 1.25.0 (2019-09-06)

  * Deprecated SlackbotHandler, use SlackWebhookHandler or SlackHandler instead
  * Deprecated RavenHandler, use sentry/sentry 2.x and their Sentry\Monolog\Handler instead
  * Deprecated HipChatHandler, migrate to Slack and use SlackWebhookHandler or SlackHandler instead
  * Added forward-compatible interfaces and traits FormattableHandlerInterface, FormattableHandlerTrait, ProcessableHandlerInterface, ProcessableHandlerTrait. If you use modern PHP and want to make code compatible with Monolog 1 and 2 this can help. You will have to require at least Monolog 1.25 though.
  * Added support for RFC3164 (outdated BSD syslog protocol) to SyslogUdpHandler
  * Fixed issue in GroupHandler and WhatFailureGroupHandler where setting multiple processors would duplicate records
  * Fixed issue in SignalHandler restarting syscalls functionality
  * Fixed normalizers handling of exception backtraces to avoid serializing arguments in some cases
  * Fixed ZendMonitorHandler to work with the latest Zend Server versions
  * Fixed ChromePHPHandler to avoid sending more data than latest Chrome versions allow in headers (4KB down from 256KB).

### 1.24.0 (2018-11-05)

  * BC Notice: If you are extending any of the Monolog's Formatters' `normalize` method, make sure you add the new `$depth = 0` argument to your function signature to avoid strict PHP warnings.
  * Added a `ResettableInterface` in order to reset/reset/clear/flush handlers and processors
  * Added a `ProcessorInterface` as an optional way to label a class as being a processor (mostly useful for autowiring dependency containers)
  * Added a way to log signals being received using Monolog\SignalHandler
  * Added ability to customize error handling at the Logger level using Logger::setExceptionHandler
  * Added InsightOpsHandler to migrate users of the LogEntriesHandler
  * Added protection to NormalizerHandler against circular and very deep structures, it now stops normalizing at a depth of 9
  * Added capture of stack traces to ErrorHandler when logging PHP errors
  * Added RavenHandler support for a `contexts` context or extra key to forward that to Sentry's contexts
  * Added forwarding of context info to FluentdFormatter
  * Added SocketHandler::setChunkSize to override the default chunk size in case you must send large log lines to rsyslog for example
  * Added ability to extend/override BrowserConsoleHandler
  * Added SlackWebhookHandler::getWebhookUrl and SlackHandler::getToken to enable class extensibility
  * Added SwiftMailerHandler::getSubjectFormatter to enable class extensibility
  * Dropped official support for HHVM in test builds
  * Fixed normalization of exception traces when call_user_func is used to avoid serializing objects and the data they contain
  * Fixed naming of fields in Slack handler, all field names are now capitalized in all cases
  * Fixed HipChatHandler bug where slack dropped messages randomly
  * Fixed normalization of objects in Slack handlers
  * Fixed support for PHP7's Throwable in NewRelicHandler
  * Fixed race bug when StreamHandler sometimes incorrectly reported it failed to create a directory
  * Fixed table row styling issues in HtmlFormatter
  * Fixed RavenHandler dropping the message when logging exception
  * Fixed WhatFailureGroupHandler skipping processors when using handleBatch
    and implement it where possible
  * Fixed display of anonymous class names

### 1.23.0 (2017-06-19)

  * Improved SyslogUdpHandler's support for RFC5424 and added optional `$ident` argument
  * Fixed GelfHandler truncation to be per field and not per message
  * Fixed compatibility issue with PHP <5.3.6
  * Fixed support for headless Chrome in ChromePHPHandler
  * Fixed support for latest Aws SDK in DynamoDbHandler
  * Fixed support for SwiftMailer 6.0+ in SwiftMailerHandler

### 1.22.1 (2017-03-13)

  * Fixed lots of minor issues in the new Slack integrations
  * Fixed support for allowInlineLineBreaks in LineFormatter when formatting exception backtraces

### 1.22.0 (2016-11-26)

  * Added SlackbotHandler and SlackWebhookHandler to set up Slack integration more easily
  * Added MercurialProcessor to add mercurial revision and branch names to log records
  * Added support for AWS SDK v3 in DynamoDbHandler
  * Fixed fatal errors occuring when normalizing generators that have been fully consumed
  * Fixed RollbarHandler to include a level (rollbar level), monolog_level (original name), channel and datetime (unix)
  * Fixed RollbarHandler not flushing records automatically, calling close() explicitly is not necessary anymore
  * Fixed SyslogUdpHandler to avoid sending empty frames
  * Fixed a few PHP 7.0 and 7.1 compatibility issues

### 1.21.0 (2016-07-29)

  * Break: Reverted the addition of $context when the ErrorHandler handles regular php errors from 1.20.0 as it was causing issues
  * Added support for more formats in RotatingFileHandler::setFilenameFormat as long as they have Y, m and d in order
  * Added ability to format the main line of text the SlackHandler sends by explictly setting a formatter on the handler
  * Added information about SoapFault instances in NormalizerFormatter
  * Added $handleOnlyReportedErrors option on ErrorHandler::registerErrorHandler (default true) to allow logging of all errors no matter the error_reporting level

### 1.20.0 (2016-07-02)

  * Added FingersCrossedHandler::activate() to manually trigger the handler regardless of the activation policy
  * Added StreamHandler::getUrl to retrieve the stream's URL
  * Added ability to override addRow/addTitle in HtmlFormatter
  * Added the $context to context information when the ErrorHandler handles a regular php error
  * Deprecated RotatingFileHandler::setFilenameFormat to only support 3 formats: Y, Y-m and Y-m-d
  * Fixed WhatFailureGroupHandler to work with PHP7 throwables
  * Fixed a few minor bugs

### 1.19.0 (2016-04-12)

  * Break: StreamHandler will not close streams automatically that it does not own. If you pass in a stream (not a path/url), then it will not close it for you. You can retrieve those using getStream() if needed
  * Added DeduplicationHandler to remove duplicate records from notifications across multiple requests, useful for email or other notifications on errors
  * Added ability to use `%message%` and other LineFormatter replacements in the subject line of emails sent with NativeMailHandler and SwiftMailerHandler
  * Fixed HipChatHandler handling of long messages

### 1.18.2 (2016-04-02)

  * Fixed ElasticaFormatter to use more precise dates
  * Fixed GelfMessageFormatter sending too long messages

### 1.18.1 (2016-03-13)

  * Fixed SlackHandler bug where slack dropped messages randomly
  * Fixed RedisHandler issue when using with the PHPRedis extension
  * Fixed AmqpHandler content-type being incorrectly set when using with the AMQP extension
  * Fixed BrowserConsoleHandler regression

### 1.18.0 (2016-03-01)

  * Added optional reduction of timestamp precision via `Logger->useMicrosecondTimestamps(false)`, disabling it gets you a bit of performance boost but reduces the precision to the second instead of microsecond
  * Added possibility to skip some extra stack frames in IntrospectionProcessor if you have some library wrapping Monolog that is always adding frames
  * Added `Logger->withName` to clone a logger (keeping all handlers) with a new name
  * Added FluentdFormatter for the Fluentd unix socket protocol
  * Added HandlerWrapper base class to ease the creation of handler wrappers, just extend it and override as needed
  * Added support for replacing context sub-keys using `%context.*%` in LineFormatter
  * Added support for `payload` context value in RollbarHandler
  * Added setRelease to RavenHandler to describe the application version, sent with every log
  * Added support for `fingerprint` context value in RavenHandler
  * Fixed JSON encoding errors that would gobble up the whole log record, we now handle those more gracefully by dropping chars as needed
  * Fixed write timeouts in SocketHandler and derivatives, set to 10sec by default, lower it with `setWritingTimeout()`
  * Fixed PHP7 compatibility with regard to Exception/Throwable handling in a few places

### 1.17.2 (2015-10-14)

  * Fixed ErrorHandler compatibility with non-Monolog PSR-3 loggers
  * Fixed SlackHandler handling to use slack functionalities better
  * Fixed SwiftMailerHandler bug when sending multiple emails they all had the same id
  * Fixed 5.3 compatibility regression

### 1.17.1 (2015-08-31)

  * Fixed RollbarHandler triggering PHP notices

### 1.17.0 (2015-08-30)

  * Added support for `checksum` and `release` context/extra values in RavenHandler
  * Added better support for exceptions in RollbarHandler
  * Added UidProcessor::getUid
  * Added support for showing the resource type in NormalizedFormatter
  * Fixed IntrospectionProcessor triggering PHP notices

### 1.16.0 (2015-08-09)

  * Added IFTTTHandler to notify ifttt.com triggers
  * Added Logger::setHandlers() to allow setting/replacing all handlers
  * Added $capSize in RedisHandler to cap the log size
  * Fixed StreamHandler creation of directory to only trigger when the first log write happens
  * Fixed bug in the handling of curl failures
  * Fixed duplicate logging of fatal errors when both error and fatal error handlers are registered in monolog's ErrorHandler
  * Fixed missing fatal errors records with handlers that need to be closed to flush log records
  * Fixed TagProcessor::addTags support for associative arrays

### 1.15.0 (2015-07-12)

  * Added addTags and setTags methods to change a TagProcessor
  * Added automatic creation of directories if they are missing for a StreamHandler to open a log file
  * Added retry functionality to Loggly, Cube and Mandrill handlers so they retry up to 5 times in case of network failure
  * Fixed process exit code being incorrectly reset to 0 if ErrorHandler::registerExceptionHandler was used
  * Fixed HTML/JS escaping in BrowserConsoleHandler
  * Fixed JSON encoding errors being silently suppressed (PHP 5.5+ only)

### 1.14.0 (2015-06-19)

  * Added PHPConsoleHandler to send record to Chrome's PHP Console extension and library
  * Added support for objects implementing __toString in the NormalizerFormatter
  * Added support for HipChat's v2 API in HipChatHandler
  * Added Logger::setTimezone() to initialize the timezone monolog should use in case date.timezone isn't correct for your app
  * Added an option to send formatted message instead of the raw record on PushoverHandler via ->useFormattedMessage(true)
  * Fixed curl errors being silently suppressed

### 1.13.1 (2015-03-09)

  * Fixed regression in HipChat requiring a new token to be created

### 1.13.0 (2015-03-05)

  * Added Registry::hasLogger to check for the presence of a logger instance
  * Added context.user support to RavenHandler
  * Added HipChat API v2 support in the HipChatHandler
  * Added NativeMailerHandler::addParameter to pass params to the mail() process
  * Added context data to SlackHandler when $includeContextAndExtra is true
  * Added ability to customize the Swift_Message per-email in SwiftMailerHandler
  * Fixed SwiftMailerHandler to lazily create message instances if a callback is provided
  * Fixed serialization of INF and NaN values in Normalizer and LineFormatter

### 1.12.0 (2014-12-29)

  * Break: HandlerInterface::isHandling now receives a partial record containing only a level key. This was always the intent and does not break any Monolog handler but is strictly speaking a BC break and you should check if you relied on any other field in your own handlers.
  * Added PsrHandler to forward records to another PSR-3 logger
  * Added SamplingHandler to wrap around a handler and include only every Nth record
  * Added MongoDBFormatter to support better storage with MongoDBHandler (it must be enabled manually for now)
  * Added exception codes in the output of most formatters
  * Added LineFormatter::includeStacktraces to enable exception stack traces in logs (uses more than one line)
  * Added $useShortAttachment to SlackHandler to minify attachment size and $includeExtra to append extra data
  * Added $host to HipChatHandler for users of private instances
  * Added $transactionName to NewRelicHandler and support for a transaction_name context value
  * Fixed MandrillHandler to avoid outputing API call responses
  * Fixed some non-standard behaviors in SyslogUdpHandler

### 1.11.0 (2014-09-30)

  * Break: The NewRelicHandler extra and context data are now prefixed with extra_ and context_ to avoid clashes. Watch out if you have scripts reading those from the API and rely on names
  * Added WhatFailureGroupHandler to suppress any exception coming from the wrapped handlers and avoid chain failures if a logging service fails
  * Added MandrillHandler to send emails via the Mandrillapp.com API
  * Added SlackHandler to log records to a Slack.com account
  * Added FleepHookHandler to log records to a Fleep.io account
  * Added LogglyHandler::addTag to allow adding tags to an existing handler
  * Added $ignoreEmptyContextAndExtra to LineFormatter to avoid empty [] at the end
  * Added $useLocking to StreamHandler and RotatingFileHandler to enable flock() while writing
  * Added support for PhpAmqpLib in the AmqpHandler
  * Added FingersCrossedHandler::clear and BufferHandler::clear to reset them between batches in long running jobs
  * Added support for adding extra fields from $_SERVER in the WebProcessor
  * Fixed support for non-string values in PrsLogMessageProcessor
  * Fixed SwiftMailer messages being sent with the wrong date in long running scripts
  * Fixed minor PHP 5.6 compatibility issues
  * Fixed BufferHandler::close being called twice

### 1.10.0 (2014-06-04)

  * Added Logger::getHandlers() and Logger::getProcessors() methods
  * Added $passthruLevel argument to FingersCrossedHandler to let it always pass some records through even if the trigger level is not reached
  * Added support for extra data in NewRelicHandler
  * Added $expandNewlines flag to the ErrorLogHandler to create multiple log entries when a message has multiple lines

### 1.9.1 (2014-04-24)

  * Fixed regression in RotatingFileHandler file permissions
  * Fixed initialization of the BufferHandler to make sure it gets flushed after receiving records
  * Fixed ChromePHPHandler and FirePHPHandler's activation strategies to be more conservative

### 1.9.0 (2014-04-20)

  * Added LogEntriesHandler to send logs to a LogEntries account
  * Added $filePermissions to tweak file mode on StreamHandler and RotatingFileHandler
  * Added $useFormatting flag to MemoryProcessor to make it send raw data in bytes
  * Added support for table formatting in FirePHPHandler via the table context key
  * Added a TagProcessor to add tags to records, and support for tags in RavenHandler
  * Added $appendNewline flag to the JsonFormatter to enable using it when logging to files
  * Added sound support to the PushoverHandler
  * Fixed multi-threading support in StreamHandler
  * Fixed empty headers issue when ChromePHPHandler received no records
  * Fixed default format of the ErrorLogHandler

### 1.8.0 (2014-03-23)

  * Break: the LineFormatter now strips newlines by default because this was a bug, set $allowInlineLineBreaks to true if you need them
  * Added BrowserConsoleHandler to send logs to any browser's console via console.log() injection in the output
  * Added FilterHandler to filter records and only allow those of a given list of levels through to the wrapped handler
  * Added FlowdockHandler to send logs to a Flowdock account
  * Added RollbarHandler to send logs to a Rollbar account
  * Added HtmlFormatter to send prettier log emails with colors for each log level
  * Added GitProcessor to add the current branch/commit to extra record data
  * Added a Monolog\Registry class to allow easier global access to pre-configured loggers
  * Added support for the new official graylog2/gelf-php lib for GelfHandler, upgrade if you can by replacing the mlehner/gelf-php requirement
  * Added support for HHVM
  * Added support for Loggly batch uploads
  * Added support for tweaking the content type and encoding in NativeMailerHandler
  * Added $skipClassesPartials to tweak the ignored classes in the IntrospectionProcessor
  * Fixed batch request support in GelfHandler

### 1.7.0 (2013-11-14)

  * Added ElasticSearchHandler to send logs to an Elastic Search server
  * Added DynamoDbHandler and ScalarFormatter to send logs to Amazon's Dynamo DB
  * Added SyslogUdpHandler to send logs to a remote syslogd server
  * Added LogglyHandler to send logs to a Loggly account
  * Added $level to IntrospectionProcessor so it only adds backtraces when needed
  * Added $version to LogstashFormatter to allow using the new v1 Logstash format
  * Added $appName to NewRelicHandler
  * Added configuration of Pushover notification retries/expiry
  * Added $maxColumnWidth to NativeMailerHandler to change the 70 chars default
  * Added chainability to most setters for all handlers
  * Fixed RavenHandler batch processing so it takes the message from the record with highest priority
  * Fixed HipChatHandler batch processing so it sends all messages at once
  * Fixed issues with eAccelerator
  * Fixed and improved many small things

### 1.6.0 (2013-07-29)

  * Added HipChatHandler to send logs to a HipChat chat room
  * Added ErrorLogHandler to send logs to PHP's error_log function
  * Added NewRelicHandler to send logs to NewRelic's service
  * Added Monolog\ErrorHandler helper class to register a Logger as exception/error/fatal handler
  * Added ChannelLevelActivationStrategy for the FingersCrossedHandler to customize levels by channel
  * Added stack traces output when normalizing exceptions (json output & co)
  * Added Monolog\Logger::API constant (currently 1)
  * Added support for ChromePHP's v4.0 extension
  * Added support for message priorities in PushoverHandler, see $highPriorityLevel and $emergencyLevel
  * Added support for sending messages to multiple users at once with the PushoverHandler
  * Fixed RavenHandler's support for batch sending of messages (when behind a Buffer or FingersCrossedHandler)
  * Fixed normalization of Traversables with very large data sets, only the first 1000 items are shown now
  * Fixed issue in RotatingFileHandler when an open_basedir restriction is active
  * Fixed minor issues in RavenHandler and bumped the API to Raven 0.5.0
  * Fixed SyslogHandler issue when many were used concurrently with different facilities

### 1.5.0 (2013-04-23)

  * Added ProcessIdProcessor to inject the PID in log records
  * Added UidProcessor to inject a unique identifier to all log records of one request/run
  * Added support for previous exceptions in the LineFormatter exception serialization
  * Added Monolog\Logger::getLevels() to get all available levels
  * Fixed ChromePHPHandler so it avoids sending headers larger than Chrome can handle

### 1.4.1 (2013-04-01)

  * Fixed exception formatting in the LineFormatter to be more minimalistic
  * Fixed RavenHandler's handling of context/extra data, requires Raven client >0.1.0
  * Fixed log rotation in RotatingFileHandler to work with long running scripts spanning multiple days
  * Fixed WebProcessor array access so it checks for data presence
  * Fixed Buffer, Group and FingersCrossed handlers to make use of their processors

### 1.4.0 (2013-02-13)

  * Added RedisHandler to log to Redis via the Predis library or the phpredis extension
  * Added ZendMonitorHandler to log to the Zend Server monitor
  * Added the possibility to pass arrays of handlers and processors directly in the Logger constructor
  * Added `$useSSL` option to the PushoverHandler which is enabled by default
  * Fixed ChromePHPHandler and FirePHPHandler issue when multiple instances are used simultaneously
  * Fixed header injection capability in the NativeMailHandler

### 1.3.1 (2013-01-11)

  * Fixed LogstashFormatter to be usable with stream handlers
  * Fixed GelfMessageFormatter levels on Windows

### 1.3.0 (2013-01-08)

  * Added PSR-3 compliance, the `Monolog\Logger` class is now an instance of `Psr\Log\LoggerInterface`
  * Added PsrLogMessageProcessor that you can selectively enable for full PSR-3 compliance
  * Added LogstashFormatter (combine with SocketHandler or StreamHandler to send logs to Logstash)
  * Added PushoverHandler to send mobile notifications
  * Added CouchDBHandler and DoctrineCouchDBHandler
  * Added RavenHandler to send data to Sentry servers
  * Added support for the new MongoClient class in MongoDBHandler
  * Added microsecond precision to log records' timestamps
  * Added `$flushOnOverflow` param to BufferHandler to flush by batches instead of losing
    the oldest entries
  * Fixed normalization of objects with cyclic references

### 1.2.1 (2012-08-29)

  * Added new $logopts arg to SyslogHandler to provide custom openlog options
  * Fixed fatal error in SyslogHandler

### 1.2.0 (2012-08-18)

  * Added AmqpHandler (for use with AMQP servers)
  * Added CubeHandler
  * Added NativeMailerHandler::addHeader() to send custom headers in mails
  * Added the possibility to specify more than one recipient in NativeMailerHandler
  * Added the possibility to specify float timeouts in SocketHandler
  * Added NOTICE and EMERGENCY levels to conform with RFC 5424
  * Fixed the log records to use the php default timezone instead of UTC
  * Fixed BufferHandler not being flushed properly on PHP fatal errors
  * Fixed normalization of exotic resource types
  * Fixed the default format of the SyslogHandler to avoid duplicating datetimes in syslog

### 1.1.0 (2012-04-23)

  * Added Monolog\Logger::isHandling() to check if a handler will
    handle the given log level
  * Added ChromePHPHandler
  * Added MongoDBHandler
  * Added GelfHandler (for use with Graylog2 servers)
  * Added SocketHandler (for use with syslog-ng for example)
  * Added NormalizerFormatter
  * Added the possibility to change the activation strategy of the FingersCrossedHandler
  * Added possibility to show microseconds in logs
  * Added `server` and `referer` to WebProcessor output

### 1.0.2 (2011-10-24)

  * Fixed bug in IE with large response headers and FirePHPHandler

### 1.0.1 (2011-08-25)

  * Added MemoryPeakUsageProcessor and MemoryUsageProcessor
  * Added Monolog\Logger::getName() to get a logger's channel name

### 1.0.0 (2011-07-06)

  * Added IntrospectionProcessor to get info from where the logger was called
  * Fixed WebProcessor in CLI

### 1.0.0-RC1 (2011-07-01)

  * Initial release
# Monolog - Logging for PHP [![Build Status](https://img.shields.io/travis/Seldaek/monolog.svg)](https://travis-ci.org/Seldaek/monolog)

[![Total Downloads](https://img.shields.io/packagist/dt/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog)
[![Latest Stable Version](https://img.shields.io/packagist/v/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog)


Monolog sends your logs to files, sockets, inboxes, databases and various
web services. See the complete list of handlers below. Special handlers
allow you to build advanced logging strategies.

This library implements the [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
interface that you can type-hint against in your own libraries to keep
a maximum of interoperability. You can also use it in your applications to
make sure you can always use another compatible logger at a later time.
As of 1.11.0 Monolog public APIs will also accept PSR-3 log levels.
Internally Monolog still uses its own level scheme since it predates PSR-3.

## Installation

Install the latest version with

```bash
$ composer require monolog/monolog
```

## Basic Usage

```php
<?php

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

// create a log channel
$log = new Logger('name');
$log->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));

// add records to the log
$log->addWarning('Foo');
$log->addError('Bar');
```

## Documentation

- [Usage Instructions](doc/01-usage.md)
- [Handlers, Formatters and Processors](doc/02-handlers-formatters-processors.md)
- [Utility classes](doc/03-utilities.md)
- [Extending Monolog](doc/04-extending.md)

## Third Party Packages

Third party handlers, formatters and processors are
[listed in the wiki](https://github.com/Seldaek/monolog/wiki/Third-Party-Packages). You
can also add your own there if you publish one.

## About

### Requirements

- Monolog works with PHP 5.3 or above, and is also tested to work with HHVM.

### Submitting bugs and feature requests

Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/monolog/issues)

### Framework Integrations

- Frameworks and libraries using [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
  can be used very easily with Monolog since it implements the interface.
- [Symfony2](http://symfony.com) comes out of the box with Monolog.
- [Silex](http://silex.sensiolabs.org/) comes out of the box with Monolog.
- [Laravel 4 & 5](http://laravel.com/) come out of the box with Monolog.
- [Lumen](http://lumen.laravel.com/) comes out of the box with Monolog.
- [PPI](http://www.ppi.io/) comes out of the box with Monolog.
- [CakePHP](http://cakephp.org/) is usable with Monolog via the [cakephp-monolog](https://github.com/jadb/cakephp-monolog) plugin.
- [Slim](http://www.slimframework.com/) is usable with Monolog via the [Slim-Monolog](https://github.com/Flynsarmy/Slim-Monolog) log writer.
- [XOOPS 2.6](http://xoops.org/) comes out of the box with Monolog.
- [Aura.Web_Project](https://github.com/auraphp/Aura.Web_Project) comes out of the box with Monolog.
- [Nette Framework](http://nette.org/en/) can be used with Monolog via [Kdyby/Monolog](https://github.com/Kdyby/Monolog) extension.
- [Proton Micro Framework](https://github.com/alexbilbie/Proton) comes out of the box with Monolog.

### Author

Jordi Boggiano - <j.boggiano@seld.be> - <http://twitter.com/seldaek><br />
See also the list of [contributors](https://github.com/Seldaek/monolog/contributors) which participated in this project.

### License

Monolog is licensed under the MIT License - see the `LICENSE` file for details

### Acknowledgements

This library is heavily inspired by Python's [Logbook](https://logbook.readthedocs.io/en/stable/)
library, although most concepts have been adjusted to fit to the PHP world.
{
    "name": "monolog/monolog",
    "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
    "keywords": ["log", "logging", "psr-3"],
    "homepage": "http://github.com/Seldaek/monolog",
    "type": "library",
    "license": "MIT",
    "authors": [
        {
            "name": "Jordi Boggiano",
            "email": "j.boggiano@seld.be",
            "homepage": "http://seld.be"
        }
    ],
    "require": {
        "php": ">=5.3.0",
        "psr/log": "~1.0"
    },
    "require-dev": {
        "phpunit/phpunit": "~4.5",
        "graylog2/gelf-php": "~1.0",
        "sentry/sentry": "^0.13",
        "ruflin/elastica": ">=0.90 <3.0",
        "doctrine/couchdb": "~1.0@dev",
        "aws/aws-sdk-php": "^2.4.9 || ^3.0",
        "php-amqplib/php-amqplib": "~2.4",
        "swiftmailer/swiftmailer": "^5.3|^6.0",
        "php-console/php-console": "^3.1.3",
        "php-parallel-lint/php-parallel-lint": "^1.0"
    },
    "suggest": {
        "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
        "sentry/sentry": "Allow sending log messages to a Sentry server",
        "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
        "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
        "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
        "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
        "ext-mongo": "Allow sending log messages to a MongoDB server",
        "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
        "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
        "rollbar/rollbar": "Allow sending log messages to Rollbar",
        "php-console/php-console": "Allow sending log messages to Google Chrome"
    },
    "autoload": {
        "psr-4": {"Monolog\\": "src/Monolog"}
    },
    "autoload-dev": {
        "psr-4": {"Monolog\\": "tests/Monolog"}
    },
    "provide": {
        "psr/log-implementation": "1.0.0"
    },
    "extra": {
        "branch-alias": {
            "dev-master": "2.0.x-dev"
        }
    },
    "scripts": {
        "test": [
            "parallel-lint . --exclude vendor --exclude src/Monolog/Handler/FormattableHandlerInterface.php  --exclude src/Monolog/Handler/FormattableHandlerTrait.php --exclude src/Monolog/Handler/ProcessableHandlerInterface.php --exclude src/Monolog/Handler/ProcessableHandlerTrait.php",
            "phpunit"
        ]
    },
    "lock": false
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

 namespace Monolog\Handler;
 
 use Monolog\Logger;

/**
 * Inspired on LogEntriesHandler.
 *
 * @author Robert Kaufmann III <rok3@rok3.me>
 * @author Gabriel Machado <gabriel.ms1@hotmail.com>
 */
class InsightOpsHandler extends SocketHandler
{
    /**
     * @var string
     */
    protected $logToken;

    /**
     * @param string $token  Log token supplied by InsightOps
     * @param string $region Region where InsightOps account is hosted. Could be 'us' or 'eu'.
     * @param bool   $useSSL Whether or not SSL encryption should be used
     * @param int    $level  The minimum logging level to trigger this handler
     * @param bool   $bubble Whether or not messages that are handled should bubble up the stack.
     *
     * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing
     */
    public function __construct($token, $region = 'us', $useSSL = true, $level = Logger::DEBUG, $bubble = true)
    {
        if ($useSSL && !extension_loaded('openssl')) {
            throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for InsightOpsHandler');
        }

        $endpoint = $useSSL
            ? 'ssl://' . $region . '.data.logs.insight.rapid7.com:443'
            : $region . '.data.logs.insight.rapid7.com:80';

        parent::__construct($endpoint, $level, $bubble);
        $this->logToken = $token;
    }

    /**
     * {@inheritdoc}
     *
     * @param  array  $record
     * @return string
     */
    protected function generateDataStream($record)
    {
        return $this->logToken . ' ' . $record['formatted'];
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

/**
 * Used for testing purposes.
 *
 * It records all records and gives you access to them for verification.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 *
 * @method bool hasEmergency($record)
 * @method bool hasAlert($record)
 * @method bool hasCritical($record)
 * @method bool hasError($record)
 * @method bool hasWarning($record)
 * @method bool hasNotice($record)
 * @method bool hasInfo($record)
 * @method bool hasDebug($record)
 *
 * @method bool hasEmergencyRecords()
 * @method bool hasAlertRecords()
 * @method bool hasCriticalRecords()
 * @method bool hasErrorRecords()
 * @method bool hasWarningRecords()
 * @method bool hasNoticeRecords()
 * @method bool hasInfoRecords()
 * @method bool hasDebugRecords()
 *
 * @method bool hasEmergencyThatContains($message)
 * @method bool hasAlertThatContains($message)
 * @method bool hasCriticalThatContains($message)
 * @method bool hasErrorThatContains($message)
 * @method bool hasWarningThatContains($message)
 * @method bool hasNoticeThatContains($message)
 * @method bool hasInfoThatContains($message)
 * @method bool hasDebugThatContains($message)
 *
 * @method bool hasEmergencyThatMatches($message)
 * @method bool hasAlertThatMatches($message)
 * @method bool hasCriticalThatMatches($message)
 * @method bool hasErrorThatMatches($message)
 * @method bool hasWarningThatMatches($message)
 * @method bool hasNoticeThatMatches($message)
 * @method bool hasInfoThatMatches($message)
 * @method bool hasDebugThatMatches($message)
 *
 * @method bool hasEmergencyThatPasses($message)
 * @method bool hasAlertThatPasses($message)
 * @method bool hasCriticalThatPasses($message)
 * @method bool hasErrorThatPasses($message)
 * @method bool hasWarningThatPasses($message)
 * @method bool hasNoticeThatPasses($message)
 * @method bool hasInfoThatPasses($message)
 * @method bool hasDebugThatPasses($message)
 */
class TestHandler extends AbstractProcessingHandler
{
    protected $records = array();
    protected $recordsByLevel = array();
    private $skipReset = false;

    public function getRecords()
    {
        return $this->records;
    }

    public function clear()
    {
        $this->records = array();
        $this->recordsByLevel = array();
    }

    public function reset()
    {
        if (!$this->skipReset) {
            $this->clear();
        }
    }

    public function setSkipReset($skipReset)
    {
        $this->skipReset = $skipReset;
    }

    public function hasRecords($level)
    {
        return isset($this->recordsByLevel[$level]);
    }

    /**
     * @param string|array $record Either a message string or an array containing message and optionally context keys that will be checked against all records
     * @param int          $level  Logger::LEVEL constant value
     */
    public function hasRecord($record, $level)
    {
        if (is_string($record)) {
            $record = array('message' => $record);
        }

        return $this->hasRecordThatPasses(function ($rec) use ($record) {
            if ($rec['message'] !== $record['message']) {
                return false;
            }
            if (isset($record['context']) && $rec['context'] !== $record['context']) {
                return false;
            }
            return true;
        }, $level);
    }

    public function hasRecordThatContains($message, $level)
    {
        return $this->hasRecordThatPasses(function ($rec) use ($message) {
            return strpos($rec['message'], $message) !== false;
        }, $level);
    }

    public function hasRecordThatMatches($regex, $level)
    {
        return $this->hasRecordThatPasses(function ($rec) use ($regex) {
            return preg_match($regex, $rec['message']) > 0;
        }, $level);
    }

    public function hasRecordThatPasses($predicate, $level)
    {
        if (!is_callable($predicate)) {
            throw new \InvalidArgumentException("Expected a callable for hasRecordThatSucceeds");
        }

        if (!isset($this->recordsByLevel[$level])) {
            return false;
        }

        foreach ($this->recordsByLevel[$level] as $i => $rec) {
            if (call_user_func($predicate, $rec, $i)) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        $this->recordsByLevel[$record['level']][] = $record;
        $this->records[] = $record;
    }

    public function __call($method, $args)
    {
        if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) {
            $genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
            $level = constant('Monolog\Logger::' . strtoupper($matches[2]));
            if (method_exists($this, $genericMethod)) {
                $args[] = $level;

                return call_user_func_array(array($this, $genericMethod), $args);
            }
        }

        throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Utils;

/**
 * IFTTTHandler uses cURL to trigger IFTTT Maker actions
 *
 * Register a secret key and trigger/event name at https://ifttt.com/maker
 *
 * value1 will be the channel from monolog's Logger constructor,
 * value2 will be the level name (ERROR, WARNING, ..)
 * value3 will be the log record's message
 *
 * @author Nehal Patel <nehal@nehalpatel.me>
 */
class IFTTTHandler extends AbstractProcessingHandler
{
    private $eventName;
    private $secretKey;

    /**
     * @param string $eventName The name of the IFTTT Maker event that should be triggered
     * @param string $secretKey A valid IFTTT secret key
     * @param int    $level     The minimum logging level at which this handler will be triggered
     * @param bool   $bubble    Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($eventName, $secretKey, $level = Logger::ERROR, $bubble = true)
    {
        $this->eventName = $eventName;
        $this->secretKey = $secretKey;

        parent::__construct($level, $bubble);
    }

    /**
     * {@inheritdoc}
     */
    public function write(array $record)
    {
        $postData = array(
            "value1" => $record["channel"],
            "value2" => $record["level_name"],
            "value3" => $record["message"],
        );
        $postString = Utils::jsonEncode($postData);

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, "https://maker.ifttt.com/trigger/" . $this->eventName . "/with/key/" . $this->secretKey);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $postString);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array(
            "Content-Type: application/json",
        ));

        Curl\Util::execute($ch);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * Stores to any socket - uses fsockopen() or pfsockopen().
 *
 * @author Pablo de Leon Belloc <pablolb@gmail.com>
 * @see    http://php.net/manual/en/function.fsockopen.php
 */
class SocketHandler extends AbstractProcessingHandler
{
    private $connectionString;
    private $connectionTimeout;
    private $resource;
    private $timeout = 0;
    private $writingTimeout = 10;
    private $lastSentBytes = null;
    private $chunkSize = null;
    private $persistent = false;
    private $errno;
    private $errstr;
    private $lastWritingAt;

    /**
     * @param string $connectionString Socket connection string
     * @param int    $level            The minimum logging level at which this handler will be triggered
     * @param bool   $bubble           Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($connectionString, $level = Logger::DEBUG, $bubble = true)
    {
        parent::__construct($level, $bubble);
        $this->connectionString = $connectionString;
        $this->connectionTimeout = (float) ini_get('default_socket_timeout');
    }

    /**
     * Connect (if necessary) and write to the socket
     *
     * @param array $record
     *
     * @throws \UnexpectedValueException
     * @throws \RuntimeException
     */
    protected function write(array $record)
    {
        $this->connectIfNotConnected();
        $data = $this->generateDataStream($record);
        $this->writeToSocket($data);
    }

    /**
     * We will not close a PersistentSocket instance so it can be reused in other requests.
     */
    public function close()
    {
        if (!$this->isPersistent()) {
            $this->closeSocket();
        }
    }

    /**
     * Close socket, if open
     */
    public function closeSocket()
    {
        if (is_resource($this->resource)) {
            fclose($this->resource);
            $this->resource = null;
        }
    }

    /**
     * Set socket connection to nbe persistent. It only has effect before the connection is initiated.
     *
     * @param bool $persistent
     */
    public function setPersistent($persistent)
    {
        $this->persistent = (bool) $persistent;
    }

    /**
     * Set connection timeout.  Only has effect before we connect.
     *
     * @param float $seconds
     *
     * @see http://php.net/manual/en/function.fsockopen.php
     */
    public function setConnectionTimeout($seconds)
    {
        $this->validateTimeout($seconds);
        $this->connectionTimeout = (float) $seconds;
    }

    /**
     * Set write timeout. Only has effect before we connect.
     *
     * @param float $seconds
     *
     * @see http://php.net/manual/en/function.stream-set-timeout.php
     */
    public function setTimeout($seconds)
    {
        $this->validateTimeout($seconds);
        $this->timeout = (float) $seconds;
    }

    /**
     * Set writing timeout. Only has effect during connection in the writing cycle.
     *
     * @param float $seconds 0 for no timeout
     */
    public function setWritingTimeout($seconds)
    {
        $this->validateTimeout($seconds);
        $this->writingTimeout = (float) $seconds;
    }

    /**
     * Set chunk size. Only has effect during connection in the writing cycle.
     *
     * @param float $bytes
     */
    public function setChunkSize($bytes)
    {
        $this->chunkSize = $bytes;
    }

    /**
     * Get current connection string
     *
     * @return string
     */
    public function getConnectionString()
    {
        return $this->connectionString;
    }

    /**
     * Get persistent setting
     *
     * @return bool
     */
    public function isPersistent()
    {
        return $this->persistent;
    }

    /**
     * Get current connection timeout setting
     *
     * @return float
     */
    public function getConnectionTimeout()
    {
        return $this->connectionTimeout;
    }

    /**
     * Get current in-transfer timeout
     *
     * @return float
     */
    public function getTimeout()
    {
        return $this->timeout;
    }

    /**
     * Get current local writing timeout
     *
     * @return float
     */
    public function getWritingTimeout()
    {
        return $this->writingTimeout;
    }

    /**
     * Get current chunk size
     *
     * @return float
     */
    public function getChunkSize()
    {
        return $this->chunkSize;
    }

    /**
     * Check to see if the socket is currently available.
     *
     * UDP might appear to be connected but might fail when writing.  See http://php.net/fsockopen for details.
     *
     * @return bool
     */
    public function isConnected()
    {
        return is_resource($this->resource)
            && !feof($this->resource);  // on TCP - other party can close connection.
    }

    /**
     * Wrapper to allow mocking
     */
    protected function pfsockopen()
    {
        return @pfsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
    }

    /**
     * Wrapper to allow mocking
     */
    protected function fsockopen()
    {
        return @fsockopen($this->connectionString, -1, $this->errno, $this->errstr, $this->connectionTimeout);
    }

    /**
     * Wrapper to allow mocking
     *
     * @see http://php.net/manual/en/function.stream-set-timeout.php
     */
    protected function streamSetTimeout()
    {
        $seconds = floor($this->timeout);
        $microseconds = round(($this->timeout - $seconds) * 1e6);

        return stream_set_timeout($this->resource, $seconds, $microseconds);
    }

    /**
     * Wrapper to allow mocking
     *
     * @see http://php.net/manual/en/function.stream-set-chunk-size.php
     */
    protected function streamSetChunkSize()
    {
        return stream_set_chunk_size($this->resource, $this->chunkSize);
    }

    /**
     * Wrapper to allow mocking
     */
    protected function fwrite($data)
    {
        return @fwrite($this->resource, $data);
    }

    /**
     * Wrapper to allow mocking
     */
    protected function streamGetMetadata()
    {
        return stream_get_meta_data($this->resource);
    }

    private function validateTimeout($value)
    {
        $ok = filter_var($value, FILTER_VALIDATE_FLOAT);
        if ($ok === false || $value < 0) {
            throw new \InvalidArgumentException("Timeout must be 0 or a positive float (got $value)");
        }
    }

    private function connectIfNotConnected()
    {
        if ($this->isConnected()) {
            return;
        }
        $this->connect();
    }

    protected function generateDataStream($record)
    {
        return (string) $record['formatted'];
    }

    /**
     * @return resource|null
     */
    protected function getResource()
    {
        return $this->resource;
    }

    private function connect()
    {
        $this->createSocketResource();
        $this->setSocketTimeout();
        $this->setStreamChunkSize();
    }

    private function createSocketResource()
    {
        if ($this->isPersistent()) {
            $resource = $this->pfsockopen();
        } else {
            $resource = $this->fsockopen();
        }
        if (!$resource) {
            throw new \UnexpectedValueException("Failed connecting to $this->connectionString ($this->errno: $this->errstr)");
        }
        $this->resource = $resource;
    }

    private function setSocketTimeout()
    {
        if (!$this->streamSetTimeout()) {
            throw new \UnexpectedValueException("Failed setting timeout with stream_set_timeout()");
        }
    }

    private function setStreamChunkSize()
    {
        if ($this->chunkSize && !$this->streamSetChunkSize()) {
            throw new \UnexpectedValueException("Failed setting chunk size with stream_set_chunk_size()");
        }
    }

    private function writeToSocket($data)
    {
        $length = strlen($data);
        $sent = 0;
        $this->lastSentBytes = $sent;
        while ($this->isConnected() && $sent < $length) {
            if (0 == $sent) {
                $chunk = $this->fwrite($data);
            } else {
                $chunk = $this->fwrite(substr($data, $sent));
            }
            if ($chunk === false) {
                throw new \RuntimeException("Could not write to socket");
            }
            $sent += $chunk;
            $socketInfo = $this->streamGetMetadata();
            if ($socketInfo['timed_out']) {
                throw new \RuntimeException("Write timed-out");
            }

            if ($this->writingIsTimedOut($sent)) {
                throw new \RuntimeException("Write timed-out, no data sent for `{$this->writingTimeout}` seconds, probably we got disconnected (sent $sent of $length)");
            }
        }
        if (!$this->isConnected() && $sent < $length) {
            throw new \RuntimeException("End-of-file reached, probably we got disconnected (sent $sent of $length)");
        }
    }

    private function writingIsTimedOut($sent)
    {
        $writingTimeout = (int) floor($this->writingTimeout);
        if (0 === $writingTimeout) {
            return false;
        }

        if ($sent !== $this->lastSentBytes) {
            $this->lastWritingAt = time();
            $this->lastSentBytes = $sent;

            return false;
        } else {
            usleep(100);
        }

        if ((time() - $this->lastWritingAt) >= $writingTimeout) {
            $this->closeSocket();

            return true;
        }

        return false;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * Sends notifications through Slack's Slackbot
 *
 * @author     Haralan Dobrev <hkdobrev@gmail.com>
 * @see        https://slack.com/apps/A0F81R8ET-slackbot
 * @deprecated According to Slack the API used on this handler it is deprecated.
 *             Therefore this handler will be removed on 2.x
 *             Slack suggests to use webhooks instead. Please contact slack for more information.
 */
class SlackbotHandler extends AbstractProcessingHandler
{
    /**
     * The slug of the Slack team
     * @var string
     */
    private $slackTeam;

    /**
     * Slackbot token
     * @var string
     */
    private $token;

    /**
     * Slack channel name
     * @var string
     */
    private $channel;

    /**
     * @param  string $slackTeam Slack team slug
     * @param  string $token     Slackbot token
     * @param  string $channel   Slack channel (encoded ID or name)
     * @param  int    $level     The minimum logging level at which this handler will be triggered
     * @param  bool   $bubble    Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($slackTeam, $token, $channel, $level = Logger::CRITICAL, $bubble = true)
    {
        @trigger_error('SlackbotHandler is deprecated and will be removed on 2.x', E_USER_DEPRECATED);
        parent::__construct($level, $bubble);

        $this->slackTeam = $slackTeam;
        $this->token = $token;
        $this->channel = $channel;
    }

    /**
     * {@inheritdoc}
     *
     * @param array $record
     */
    protected function write(array $record)
    {
        $slackbotUrl = sprintf(
            'https://%s.slack.com/services/hooks/slackbot?token=%s&channel=%s',
            $this->slackTeam,
            $this->token,
            $this->channel
        );

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $slackbotUrl);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $record['message']);

        Curl\Util::execute($ch);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\ResettableInterface;
use Monolog\Formatter\FormatterInterface;

/**
 * Buffers all records until closing the handler and then pass them as batch.
 *
 * This is useful for a MailHandler to send only one mail per request instead of
 * sending one per log message.
 *
 * @author Christophe Coevoet <stof@notk.org>
 */
class BufferHandler extends AbstractHandler
{
    protected $handler;
    protected $bufferSize = 0;
    protected $bufferLimit;
    protected $flushOnOverflow;
    protected $buffer = array();
    protected $initialized = false;

    /**
     * @param HandlerInterface $handler         Handler.
     * @param int              $bufferLimit     How many entries should be buffered at most, beyond that the oldest items are removed from the buffer.
     * @param int              $level           The minimum logging level at which this handler will be triggered
     * @param bool             $bubble          Whether the messages that are handled can bubble up the stack or not
     * @param bool             $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded
     */
    public function __construct(HandlerInterface $handler, $bufferLimit = 0, $level = Logger::DEBUG, $bubble = true, $flushOnOverflow = false)
    {
        parent::__construct($level, $bubble);
        $this->handler = $handler;
        $this->bufferLimit = (int) $bufferLimit;
        $this->flushOnOverflow = $flushOnOverflow;
    }

    /**
     * {@inheritdoc}
     */
    public function handle(array $record)
    {
        if ($record['level'] < $this->level) {
            return false;
        }

        if (!$this->initialized) {
            // __destructor() doesn't get called on Fatal errors
            register_shutdown_function(array($this, 'close'));
            $this->initialized = true;
        }

        if ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) {
            if ($this->flushOnOverflow) {
                $this->flush();
            } else {
                array_shift($this->buffer);
                $this->bufferSize--;
            }
        }

        if ($this->processors) {
            foreach ($this->processors as $processor) {
                $record = call_user_func($processor, $record);
            }
        }

        $this->buffer[] = $record;
        $this->bufferSize++;

        return false === $this->bubble;
    }

    public function flush()
    {
        if ($this->bufferSize === 0) {
            return;
        }

        $this->handler->handleBatch($this->buffer);
        $this->clear();
    }

    public function __destruct()
    {
        // suppress the parent behavior since we already have register_shutdown_function()
        // to call close(), and the reference contained there will prevent this from being
        // GC'd until the end of the request
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        $this->flush();
    }

    /**
     * Clears the buffer without flushing any messages down to the wrapped handler.
     */
    public function clear()
    {
        $this->bufferSize = 0;
        $this->buffer = array();
    }

    public function reset()
    {
        $this->flush();

        parent::reset();

        if ($this->handler instanceof ResettableInterface) {
            $this->handler->reset();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        $this->handler->setFormatter($formatter);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        return $this->handler->getFormatter();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * Logs to syslog service.
 *
 * usage example:
 *
 *   $log = new Logger('application');
 *   $syslog = new SyslogHandler('myfacility', 'local6');
 *   $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%");
 *   $syslog->setFormatter($formatter);
 *   $log->pushHandler($syslog);
 *
 * @author Sven Paulus <sven@karlsruhe.org>
 */
class SyslogHandler extends AbstractSyslogHandler
{
    protected $ident;
    protected $logopts;

    /**
     * @param string $ident
     * @param mixed  $facility
     * @param int    $level    The minimum logging level at which this handler will be triggered
     * @param bool   $bubble   Whether the messages that are handled can bubble up the stack or not
     * @param int    $logopts  Option flags for the openlog() call, defaults to LOG_PID
     */
    public function __construct($ident, $facility = LOG_USER, $level = Logger::DEBUG, $bubble = true, $logopts = LOG_PID)
    {
        parent::__construct($facility, $level, $bubble);

        $this->ident = $ident;
        $this->logopts = $logopts;
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        closelog();
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        if (!openlog($this->ident, $this->logopts, $this->facility)) {
            throw new \LogicException('Can\'t open syslog for ident "'.$this->ident.'" and facility "'.$this->facility.'"');
        }
        syslog($this->logLevels[$record['level']], (string) $record['formatted']);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use RollbarNotifier;
use Exception;
use Monolog\Logger;

/**
 * Sends errors to Rollbar
 *
 * If the context data contains a `payload` key, that is used as an array
 * of payload options to RollbarNotifier's report_message/report_exception methods.
 *
 * Rollbar's context info will contain the context + extra keys from the log record
 * merged, and then on top of that a few keys:
 *
 *  - level (rollbar level name)
 *  - monolog_level (monolog level name, raw level, as rollbar only has 5 but monolog 8)
 *  - channel
 *  - datetime (unix timestamp)
 *
 * @author Paul Statezny <paulstatezny@gmail.com>
 */
class RollbarHandler extends AbstractProcessingHandler
{
    /**
     * Rollbar notifier
     *
     * @var RollbarNotifier
     */
    protected $rollbarNotifier;

    protected $levelMap = array(
        Logger::DEBUG     => 'debug',
        Logger::INFO      => 'info',
        Logger::NOTICE    => 'info',
        Logger::WARNING   => 'warning',
        Logger::ERROR     => 'error',
        Logger::CRITICAL  => 'critical',
        Logger::ALERT     => 'critical',
        Logger::EMERGENCY => 'critical',
    );

    /**
     * Records whether any log records have been added since the last flush of the rollbar notifier
     *
     * @var bool
     */
    private $hasRecords = false;

    protected $initialized = false;

    /**
     * @param RollbarNotifier $rollbarNotifier RollbarNotifier object constructed with valid token
     * @param int             $level           The minimum logging level at which this handler will be triggered
     * @param bool            $bubble          Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct(RollbarNotifier $rollbarNotifier, $level = Logger::ERROR, $bubble = true)
    {
        $this->rollbarNotifier = $rollbarNotifier;

        parent::__construct($level, $bubble);
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        if (!$this->initialized) {
            // __destructor() doesn't get called on Fatal errors
            register_shutdown_function(array($this, 'close'));
            $this->initialized = true;
        }

        $context = $record['context'];
        $payload = array();
        if (isset($context['payload'])) {
            $payload = $context['payload'];
            unset($context['payload']);
        }
        $context = array_merge($context, $record['extra'], array(
            'level' => $this->levelMap[$record['level']],
            'monolog_level' => $record['level_name'],
            'channel' => $record['channel'],
            'datetime' => $record['datetime']->format('U'),
        ));

        if (isset($context['exception']) && $context['exception'] instanceof Exception) {
            $payload['level'] = $context['level'];
            $exception = $context['exception'];
            unset($context['exception']);

            $this->rollbarNotifier->report_exception($exception, $context, $payload);
        } else {
            $this->rollbarNotifier->report_message(
                $record['message'],
                $context['level'],
                $context,
                $payload
            );
        }

        $this->hasRecords = true;
    }

    public function flush()
    {
        if ($this->hasRecords) {
            $this->rollbarNotifier->flush();
            $this->hasRecords = false;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        $this->flush();
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        $this->flush();

        parent::reset();
    }


}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy;
use Monolog\Handler\FingersCrossed\ActivationStrategyInterface;
use Monolog\Logger;
use Monolog\ResettableInterface;
use Monolog\Formatter\FormatterInterface;

/**
 * Buffers all records until a certain level is reached
 *
 * The advantage of this approach is that you don't get any clutter in your log files.
 * Only requests which actually trigger an error (or whatever your actionLevel is) will be
 * in the logs, but they will contain all records, not only those above the level threshold.
 *
 * You can find the various activation strategies in the
 * Monolog\Handler\FingersCrossed\ namespace.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class FingersCrossedHandler extends AbstractHandler
{
    protected $handler;
    protected $activationStrategy;
    protected $buffering = true;
    protected $bufferSize;
    protected $buffer = array();
    protected $stopBuffering;
    protected $passthruLevel;

    /**
     * @param callable|HandlerInterface       $handler            Handler or factory callable($record|null, $fingersCrossedHandler).
     * @param int|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action
     * @param int                             $bufferSize         How many entries should be buffered at most, beyond that the oldest items are removed from the buffer.
     * @param bool                            $bubble             Whether the messages that are handled can bubble up the stack or not
     * @param bool                            $stopBuffering      Whether the handler should stop buffering after being triggered (default true)
     * @param int                             $passthruLevel      Minimum level to always flush to handler on close, even if strategy not triggered
     */
    public function __construct($handler, $activationStrategy = null, $bufferSize = 0, $bubble = true, $stopBuffering = true, $passthruLevel = null)
    {
        if (null === $activationStrategy) {
            $activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING);
        }

        // convert simple int activationStrategy to an object
        if (!$activationStrategy instanceof ActivationStrategyInterface) {
            $activationStrategy = new ErrorLevelActivationStrategy($activationStrategy);
        }

        $this->handler = $handler;
        $this->activationStrategy = $activationStrategy;
        $this->bufferSize = $bufferSize;
        $this->bubble = $bubble;
        $this->stopBuffering = $stopBuffering;

        if ($passthruLevel !== null) {
            $this->passthruLevel = Logger::toMonologLevel($passthruLevel);
        }

        if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
            throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
        }
    }

    /**
     * {@inheritdoc}
     */
    public function isHandling(array $record)
    {
        return true;
    }

    /**
     * Manually activate this logger regardless of the activation strategy
     */
    public function activate()
    {
        if ($this->stopBuffering) {
            $this->buffering = false;
        }
        $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer);
        $this->buffer = array();
    }

    /**
     * {@inheritdoc}
     */
    public function handle(array $record)
    {
        if ($this->processors) {
            foreach ($this->processors as $processor) {
                $record = call_user_func($processor, $record);
            }
        }

        if ($this->buffering) {
            $this->buffer[] = $record;
            if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) {
                array_shift($this->buffer);
            }
            if ($this->activationStrategy->isHandlerActivated($record)) {
                $this->activate();
            }
        } else {
            $this->getHandler($record)->handle($record);
        }

        return false === $this->bubble;
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        $this->flushBuffer();
    }

    public function reset()
    {
        $this->flushBuffer();

        parent::reset();

        if ($this->getHandler() instanceof ResettableInterface) {
            $this->getHandler()->reset();
        }
    }

    /**
     * Clears the buffer without flushing any messages down to the wrapped handler.
     *
     * It also resets the handler to its initial buffering state.
     */
    public function clear()
    {
        $this->buffer = array();
        $this->reset();
    }

    /**
     * Resets the state of the handler. Stops forwarding records to the wrapped handler.
     */
    private function flushBuffer()
    {
        if (null !== $this->passthruLevel) {
            $level = $this->passthruLevel;
            $this->buffer = array_filter($this->buffer, function ($record) use ($level) {
                return $record['level'] >= $level;
            });
            if (count($this->buffer) > 0) {
                $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer);
            }
        }

        $this->buffer = array();
        $this->buffering = true;
    }

    /**
     * Return the nested handler
     *
     * If the handler was provided as a factory callable, this will trigger the handler's instantiation.
     *
     * @return HandlerInterface
     */
    public function getHandler(array $record = null)
    {
        if (!$this->handler instanceof HandlerInterface) {
            $this->handler = call_user_func($this->handler, $record, $this);
            if (!$this->handler instanceof HandlerInterface) {
                throw new \RuntimeException("The factory callable should return a HandlerInterface");
            }
        }

        return $this->handler;
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        $this->getHandler()->setFormatter($formatter);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        return $this->getHandler()->getFormatter();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\ResettableInterface;
use Monolog\Formatter\FormatterInterface;

/**
 * This simple wrapper class can be used to extend handlers functionality.
 *
 * Example: A custom filtering that can be applied to any handler.
 *
 * Inherit from this class and override handle() like this:
 *
 *   public function handle(array $record)
 *   {
 *        if ($record meets certain conditions) {
 *            return false;
 *        }
 *        return $this->handler->handle($record);
 *   }
 *
 * @author Alexey Karapetov <alexey@karapetov.com>
 */
class HandlerWrapper implements HandlerInterface, ResettableInterface
{
    /**
     * @var HandlerInterface
     */
    protected $handler;

    /**
     * HandlerWrapper constructor.
     * @param HandlerInterface $handler
     */
    public function __construct(HandlerInterface $handler)
    {
        $this->handler = $handler;
    }

    /**
     * {@inheritdoc}
     */
    public function isHandling(array $record)
    {
        return $this->handler->isHandling($record);
    }

    /**
     * {@inheritdoc}
     */
    public function handle(array $record)
    {
        return $this->handler->handle($record);
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        return $this->handler->handleBatch($records);
    }

    /**
     * {@inheritdoc}
     */
    public function pushProcessor($callback)
    {
        $this->handler->pushProcessor($callback);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function popProcessor()
    {
        return $this->handler->popProcessor();
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        $this->handler->setFormatter($formatter);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        return $this->handler->getFormatter();
    }

    public function reset()
    {
        if ($this->handler instanceof ResettableInterface) {
            return $this->handler->reset();
        }
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler\FingersCrossed;

/**
 * Interface for activation strategies for the FingersCrossedHandler.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
interface ActivationStrategyInterface
{
    /**
     * Returns whether the given record activates the handler.
     *
     * @param  array   $record
     * @return bool
     */
    public function isHandlerActivated(array $record);
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler\FingersCrossed;

use Monolog\Logger;

/**
 * Error level based activation strategy.
 *
 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
 */
class ErrorLevelActivationStrategy implements ActivationStrategyInterface
{
    private $actionLevel;

    public function __construct($actionLevel)
    {
        $this->actionLevel = Logger::toMonologLevel($actionLevel);
    }

    public function isHandlerActivated(array $record)
    {
        return $record['level'] >= $this->actionLevel;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler\FingersCrossed;

use Monolog\Logger;

/**
 * Channel and Error level based monolog activation strategy. Allows to trigger activation
 * based on level per channel. e.g. trigger activation on level 'ERROR' by default, except
 * for records of the 'sql' channel; those should trigger activation on level 'WARN'.
 *
 * Example:
 *
 * <code>
 *   $activationStrategy = new ChannelLevelActivationStrategy(
 *       Logger::CRITICAL,
 *       array(
 *           'request' => Logger::ALERT,
 *           'sensitive' => Logger::ERROR,
 *       )
 *   );
 *   $handler = new FingersCrossedHandler(new StreamHandler('php://stderr'), $activationStrategy);
 * </code>
 *
 * @author Mike Meessen <netmikey@gmail.com>
 */
class ChannelLevelActivationStrategy implements ActivationStrategyInterface
{
    private $defaultActionLevel;
    private $channelToActionLevel;

    /**
     * @param int   $defaultActionLevel   The default action level to be used if the record's category doesn't match any
     * @param array $channelToActionLevel An array that maps channel names to action levels.
     */
    public function __construct($defaultActionLevel, $channelToActionLevel = array())
    {
        $this->defaultActionLevel = Logger::toMonologLevel($defaultActionLevel);
        $this->channelToActionLevel = array_map('Monolog\Logger::toMonologLevel', $channelToActionLevel);
    }

    public function isHandlerActivated(array $record)
    {
        if (isset($this->channelToActionLevel[$record['channel']])) {
            return $record['level'] >= $this->channelToActionLevel[$record['channel']];
        }

        return $record['level'] >= $this->defaultActionLevel;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;
use Monolog\ResettableInterface;

/**
 * Forwards records to multiple handlers
 *
 * @author Lenar Lõhmus <lenar@city.ee>
 */
class GroupHandler extends AbstractHandler
{
    protected $handlers;

    /**
     * @param array $handlers Array of Handlers.
     * @param bool  $bubble   Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct(array $handlers, $bubble = true)
    {
        foreach ($handlers as $handler) {
            if (!$handler instanceof HandlerInterface) {
                throw new \InvalidArgumentException('The first argument of the GroupHandler must be an array of HandlerInterface instances.');
            }
        }

        $this->handlers = $handlers;
        $this->bubble = $bubble;
    }

    /**
     * {@inheritdoc}
     */
    public function isHandling(array $record)
    {
        foreach ($this->handlers as $handler) {
            if ($handler->isHandling($record)) {
                return true;
            }
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function handle(array $record)
    {
        if ($this->processors) {
            foreach ($this->processors as $processor) {
                $record = call_user_func($processor, $record);
            }
        }

        foreach ($this->handlers as $handler) {
            $handler->handle($record);
        }

        return false === $this->bubble;
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        if ($this->processors) {
            $processed = array();
            foreach ($records as $record) {
                foreach ($this->processors as $processor) {
                    $record = call_user_func($processor, $record);
                }
                $processed[] = $record;
            }
            $records = $processed;
        }

        foreach ($this->handlers as $handler) {
            $handler->handleBatch($records);
        }
    }

    public function reset()
    {
        parent::reset();

        foreach ($this->handlers as $handler) {
            if ($handler instanceof ResettableInterface) {
                $handler->reset();
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        foreach ($this->handlers as $handler) {
            $handler->setFormatter($formatter);
        }

        return $this;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\ElasticaFormatter;
use Monolog\Logger;
use Elastica\Client;
use Elastica\Exception\ExceptionInterface;

/**
 * Elastic Search handler
 *
 * Usage example:
 *
 *    $client = new \Elastica\Client();
 *    $options = array(
 *        'index' => 'elastic_index_name',
 *        'type' => 'elastic_doc_type',
 *    );
 *    $handler = new ElasticSearchHandler($client, $options);
 *    $log = new Logger('application');
 *    $log->pushHandler($handler);
 *
 * @author Jelle Vink <jelle.vink@gmail.com>
 */
class ElasticSearchHandler extends AbstractProcessingHandler
{
    /**
     * @var Client
     */
    protected $client;

    /**
     * @var array Handler config options
     */
    protected $options = array();

    /**
     * @param Client $client  Elastica Client object
     * @param array  $options Handler configuration
     * @param int    $level   The minimum logging level at which this handler will be triggered
     * @param bool   $bubble  Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct(Client $client, array $options = array(), $level = Logger::DEBUG, $bubble = true)
    {
        parent::__construct($level, $bubble);
        $this->client = $client;
        $this->options = array_merge(
            array(
                'index'          => 'monolog',      // Elastic index name
                'type'           => 'record',       // Elastic document type
                'ignore_error'   => false,          // Suppress Elastica exceptions
            ),
            $options
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record)
    {
        $this->bulkSend(array($record['formatted']));
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        if ($formatter instanceof ElasticaFormatter) {
            return parent::setFormatter($formatter);
        }
        throw new \InvalidArgumentException('ElasticSearchHandler is only compatible with ElasticaFormatter');
    }

    /**
     * Getter options
     * @return array
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new ElasticaFormatter($this->options['index'], $this->options['type']);
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        $documents = $this->getFormatter()->formatBatch($records);
        $this->bulkSend($documents);
    }

    /**
     * Use Elasticsearch bulk API to send list of documents
     * @param  array             $documents
     * @throws \RuntimeException
     */
    protected function bulkSend(array $documents)
    {
        try {
            $this->client->addDocuments($documents);
        } catch (ExceptionInterface $e) {
            if (!$this->options['ignore_error']) {
                throw new \RuntimeException("Error sending messages to Elasticsearch", 0, $e);
            }
        }
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;

/**
 * Sampling handler
 *
 * A sampled event stream can be useful for logging high frequency events in
 * a production environment where you only need an idea of what is happening
 * and are not concerned with capturing every occurrence. Since the decision to
 * handle or not handle a particular event is determined randomly, the
 * resulting sampled log is not guaranteed to contain 1/N of the events that
 * occurred in the application, but based on the Law of large numbers, it will
 * tend to be close to this ratio with a large number of attempts.
 *
 * @author Bryan Davis <bd808@wikimedia.org>
 * @author Kunal Mehta <legoktm@gmail.com>
 */
class SamplingHandler extends AbstractHandler
{
    /**
     * @var callable|HandlerInterface $handler
     */
    protected $handler;

    /**
     * @var int $factor
     */
    protected $factor;

    /**
     * @param callable|HandlerInterface $handler Handler or factory callable($record|null, $samplingHandler).
     * @param int                       $factor  Sample factor
     */
    public function __construct($handler, $factor)
    {
        parent::__construct();
        $this->handler = $handler;
        $this->factor = $factor;

        if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
            throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
        }
    }

    public function isHandling(array $record)
    {
        return $this->getHandler($record)->isHandling($record);
    }

    public function handle(array $record)
    {
        if ($this->isHandling($record) && mt_rand(1, $this->factor) === 1) {
            if ($this->processors) {
                foreach ($this->processors as $processor) {
                    $record = call_user_func($processor, $record);
                }
            }

            $this->getHandler($record)->handle($record);
        }

        return false === $this->bubble;
    }

    /**
     * Return the nested handler
     *
     * If the handler was provided as a factory callable, this will trigger the handler's instantiation.
     *
     * @return HandlerInterface
     */
    public function getHandler(array $record = null)
    {
        if (!$this->handler instanceof HandlerInterface) {
            $this->handler = call_user_func($this->handler, $record, $this);
            if (!$this->handler instanceof HandlerInterface) {
                throw new \RuntimeException("The factory callable should return a HandlerInterface");
            }
        }

        return $this->handler;
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        $this->getHandler()->setFormatter($formatter);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        return $this->getHandler()->getFormatter();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Monolog\ResettableInterface;

/**
 * Base Handler class providing the Handler structure
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
abstract class AbstractHandler implements HandlerInterface, ResettableInterface
{
    protected $level = Logger::DEBUG;
    protected $bubble = true;

    /**
     * @var FormatterInterface
     */
    protected $formatter;
    protected $processors = array();

    /**
     * @param int|string $level  The minimum logging level at which this handler will be triggered
     * @param bool       $bubble Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($level = Logger::DEBUG, $bubble = true)
    {
        $this->setLevel($level);
        $this->bubble = $bubble;
    }

    /**
     * {@inheritdoc}
     */
    public function isHandling(array $record)
    {
        return $record['level'] >= $this->level;
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        foreach ($records as $record) {
            $this->handle($record);
        }
    }

    /**
     * Closes the handler.
     *
     * This will be called automatically when the object is destroyed
     */
    public function close()
    {
    }

    /**
     * {@inheritdoc}
     */
    public function pushProcessor($callback)
    {
        if (!is_callable($callback)) {
            throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given');
        }
        array_unshift($this->processors, $callback);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function popProcessor()
    {
        if (!$this->processors) {
            throw new \LogicException('You tried to pop from an empty processor stack.');
        }

        return array_shift($this->processors);
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        $this->formatter = $formatter;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        if (!$this->formatter) {
            $this->formatter = $this->getDefaultFormatter();
        }

        return $this->formatter;
    }

    /**
     * Sets minimum logging level at which this handler will be triggered.
     *
     * @param  int|string $level Level or level name
     * @return self
     */
    public function setLevel($level)
    {
        $this->level = Logger::toMonologLevel($level);

        return $this;
    }

    /**
     * Gets minimum logging level at which this handler will be triggered.
     *
     * @return int
     */
    public function getLevel()
    {
        return $this->level;
    }

    /**
     * Sets the bubbling behavior.
     *
     * @param  bool $bubble true means that this handler allows bubbling.
     *                      false means that bubbling is not permitted.
     * @return self
     */
    public function setBubble($bubble)
    {
        $this->bubble = $bubble;

        return $this;
    }

    /**
     * Gets the bubbling behavior.
     *
     * @return bool true means that this handler allows bubbling.
     *              false means that bubbling is not permitted.
     */
    public function getBubble()
    {
        return $this->bubble;
    }

    public function __destruct()
    {
        try {
            $this->close();
        } catch (\Exception $e) {
            // do nothing
        } catch (\Throwable $e) {
            // do nothing
        }
    }

    public function reset()
    {
        foreach ($this->processors as $processor) {
            if ($processor instanceof ResettableInterface) {
                $processor->reset();
            }
        }
    }

    /**
     * Gets the default formatter.
     *
     * @return FormatterInterface
     */
    protected function getDefaultFormatter()
    {
        return new LineFormatter();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Swift;

/**
 * SwiftMailerHandler uses Swift_Mailer to send the emails
 *
 * @author Gyula Sallai
 */
class SwiftMailerHandler extends MailHandler
{
    protected $mailer;
    private $messageTemplate;

    /**
     * @param \Swift_Mailer           $mailer  The mailer to use
     * @param callable|\Swift_Message $message An example message for real messages, only the body will be replaced
     * @param int                     $level   The minimum logging level at which this handler will be triggered
     * @param bool                    $bubble  Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct(\Swift_Mailer $mailer, $message, $level = Logger::ERROR, $bubble = true)
    {
        parent::__construct($level, $bubble);

        $this->mailer = $mailer;
        $this->messageTemplate = $message;
    }

    /**
     * {@inheritdoc}
     */
    protected function send($content, array $records)
    {
        $this->mailer->send($this->buildMessage($content, $records));
    }

    /**
     * Gets the formatter for the Swift_Message subject.
     *
     * @param  string             $format The format of the subject
     * @return FormatterInterface
     */
    protected function getSubjectFormatter($format)
    {
        return new LineFormatter($format);
    }

    /**
     * Creates instance of Swift_Message to be sent
     *
     * @param  string         $content formatted email body to be sent
     * @param  array          $records Log records that formed the content
     * @return \Swift_Message
     */
    protected function buildMessage($content, array $records)
    {
        $message = null;
        if ($this->messageTemplate instanceof \Swift_Message) {
            $message = clone $this->messageTemplate;
            $message->generateId();
        } elseif (is_callable($this->messageTemplate)) {
            $message = call_user_func($this->messageTemplate, $content, $records);
        }

        if (!$message instanceof \Swift_Message) {
            throw new \InvalidArgumentException('Could not resolve message as instance of Swift_Message or a callable returning it');
        }

        if ($records) {
            $subjectFormatter = $this->getSubjectFormatter($message->getSubject());
            $message->setSubject($subjectFormatter->format($this->getHighestRecord($records)));
        }

        $message->setBody($content);
        if (version_compare(Swift::VERSION, '6.0.0', '>=')) {
            $message->setDate(new \DateTimeImmutable());
        } else {
            $message->setDate(time());
        }

        return $message;
    }

    /**
     * BC getter, to be removed in 2.0
     */
    public function __get($name)
    {
        if ($name === 'message') {
            trigger_error('SwiftMailerHandler->message is deprecated, use ->buildMessage() instead to retrieve the message', E_USER_DEPRECATED);

            return $this->buildMessage(null, array());
        }

        throw new \InvalidArgumentException('Invalid property '.$name);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Utils;
use Monolog\Formatter\FlowdockFormatter;
use Monolog\Formatter\FormatterInterface;

/**
 * Sends notifications through the Flowdock push API
 *
 * This must be configured with a FlowdockFormatter instance via setFormatter()
 *
 * Notes:
 * API token - Flowdock API token
 *
 * @author Dominik Liebler <liebler.dominik@gmail.com>
 * @see https://www.flowdock.com/api/push
 */
class FlowdockHandler extends SocketHandler
{
    /**
     * @var string
     */
    protected $apiToken;

    /**
     * @param string   $apiToken
     * @param bool|int $level    The minimum logging level at which this handler will be triggered
     * @param bool     $bubble   Whether the messages that are handled can bubble up the stack or not
     *
     * @throws MissingExtensionException if OpenSSL is missing
     */
    public function __construct($apiToken, $level = Logger::DEBUG, $bubble = true)
    {
        if (!extension_loaded('openssl')) {
            throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FlowdockHandler');
        }

        parent::__construct('ssl://api.flowdock.com:443', $level, $bubble);
        $this->apiToken = $apiToken;
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        if (!$formatter instanceof FlowdockFormatter) {
            throw new \InvalidArgumentException('The FlowdockHandler requires an instance of Monolog\Formatter\FlowdockFormatter to function correctly');
        }

        return parent::setFormatter($formatter);
    }

    /**
     * Gets the default formatter.
     *
     * @return FormatterInterface
     */
    protected function getDefaultFormatter()
    {
        throw new \InvalidArgumentException('The FlowdockHandler must be configured (via setFormatter) with an instance of Monolog\Formatter\FlowdockFormatter to function correctly');
    }

    /**
     * {@inheritdoc}
     *
     * @param array $record
     */
    protected function write(array $record)
    {
        parent::write($record);

        $this->closeSocket();
    }

    /**
     * {@inheritdoc}
     *
     * @param  array  $record
     * @return string
     */
    protected function generateDataStream($record)
    {
        $content = $this->buildContent($record);

        return $this->buildHeader($content) . $content;
    }

    /**
     * Builds the body of API call
     *
     * @param  array  $record
     * @return string
     */
    private function buildContent($record)
    {
        return Utils::jsonEncode($record['formatted']['flowdock']);
    }

    /**
     * Builds the header of the API Call
     *
     * @param  string $content
     * @return string
     */
    private function buildHeader($content)
    {
        $header = "POST /v1/messages/team_inbox/" . $this->apiToken . " HTTP/1.1\r\n";
        $header .= "Host: api.flowdock.com\r\n";
        $header .= "Content-Type: application/json\r\n";
        $header .= "Content-Length: " . strlen($content) . "\r\n";
        $header .= "\r\n";

        return $header;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * Sends notifications through the pushover api to mobile phones
 *
 * @author Sebastian Göttschkes <sebastian.goettschkes@googlemail.com>
 * @see    https://www.pushover.net/api
 */
class PushoverHandler extends SocketHandler
{
    private $token;
    private $users;
    private $title;
    private $user;
    private $retry;
    private $expire;

    private $highPriorityLevel;
    private $emergencyLevel;
    private $useFormattedMessage = false;

    /**
     * All parameters that can be sent to Pushover
     * @see https://pushover.net/api
     * @var array
     */
    private $parameterNames = array(
        'token' => true,
        'user' => true,
        'message' => true,
        'device' => true,
        'title' => true,
        'url' => true,
        'url_title' => true,
        'priority' => true,
        'timestamp' => true,
        'sound' => true,
        'retry' => true,
        'expire' => true,
        'callback' => true,
    );

    /**
     * Sounds the api supports by default
     * @see https://pushover.net/api#sounds
     * @var array
     */
    private $sounds = array(
        'pushover', 'bike', 'bugle', 'cashregister', 'classical', 'cosmic', 'falling', 'gamelan', 'incoming',
        'intermission', 'magic', 'mechanical', 'pianobar', 'siren', 'spacealarm', 'tugboat', 'alien', 'climb',
        'persistent', 'echo', 'updown', 'none',
    );

    /**
     * @param string       $token             Pushover api token
     * @param string|array $users             Pushover user id or array of ids the message will be sent to
     * @param string       $title             Title sent to the Pushover API
     * @param int          $level             The minimum logging level at which this handler will be triggered
     * @param bool         $bubble            Whether the messages that are handled can bubble up the stack or not
     * @param bool         $useSSL            Whether to connect via SSL. Required when pushing messages to users that are not
     *                                        the pushover.net app owner. OpenSSL is required for this option.
     * @param int          $highPriorityLevel The minimum logging level at which this handler will start
     *                                        sending "high priority" requests to the Pushover API
     * @param int          $emergencyLevel    The minimum logging level at which this handler will start
     *                                        sending "emergency" requests to the Pushover API
     * @param int          $retry             The retry parameter specifies how often (in seconds) the Pushover servers will send the same notification to the user.
     * @param int          $expire            The expire parameter specifies how many seconds your notification will continue to be retried for (every retry seconds).
     */
    public function __construct($token, $users, $title = null, $level = Logger::CRITICAL, $bubble = true, $useSSL = true, $highPriorityLevel = Logger::CRITICAL, $emergencyLevel = Logger::EMERGENCY, $retry = 30, $expire = 25200)
    {
        $connectionString = $useSSL ? 'ssl://api.pushover.net:443' : 'api.pushover.net:80';
        parent::__construct($connectionString, $level, $bubble);

        $this->token = $token;
        $this->users = (array) $users;
        $this->title = $title ?: gethostname();
        $this->highPriorityLevel = Logger::toMonologLevel($highPriorityLevel);
        $this->emergencyLevel = Logger::toMonologLevel($emergencyLevel);
        $this->retry = $retry;
        $this->expire = $expire;
    }

    protected function generateDataStream($record)
    {
        $content = $this->buildContent($record);

        return $this->buildHeader($content) . $content;
    }

    private function buildContent($record)
    {
        // Pushover has a limit of 512 characters on title and message combined.
        $maxMessageLength = 512 - strlen($this->title);

        $message = ($this->useFormattedMessage) ? $record['formatted'] : $record['message'];
        $message = substr($message, 0, $maxMessageLength);

        $timestamp = $record['datetime']->getTimestamp();

        $dataArray = array(
            'token' => $this->token,
            'user' => $this->user,
            'message' => $message,
            'title' => $this->title,
            'timestamp' => $timestamp,
        );

        if (isset($record['level']) && $record['level'] >= $this->emergencyLevel) {
            $dataArray['priority'] = 2;
            $dataArray['retry'] = $this->retry;
            $dataArray['expire'] = $this->expire;
        } elseif (isset($record['level']) && $record['level'] >= $this->highPriorityLevel) {
            $dataArray['priority'] = 1;
        }

        // First determine the available parameters
        $context = array_intersect_key($record['context'], $this->parameterNames);
        $extra = array_intersect_key($record['extra'], $this->parameterNames);

        // Least important info should be merged with subsequent info
        $dataArray = array_merge($extra, $context, $dataArray);

        // Only pass sounds that are supported by the API
        if (isset($dataArray['sound']) && !in_array($dataArray['sound'], $this->sounds)) {
            unset($dataArray['sound']);
        }

        return http_build_query($dataArray);
    }

    private function buildHeader($content)
    {
        $header = "POST /1/messages.json HTTP/1.1\r\n";
        $header .= "Host: api.pushover.net\r\n";
        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $header .= "Content-Length: " . strlen($content) . "\r\n";
        $header .= "\r\n";

        return $header;
    }

    protected function write(array $record)
    {
        foreach ($this->users as $user) {
            $this->user = $user;

            parent::write($record);
            $this->closeSocket();
        }

        $this->user = null;
    }

    public function setHighPriorityLevel($value)
    {
        $this->highPriorityLevel = $value;
    }

    public function setEmergencyLevel($value)
    {
        $this->emergencyLevel = $value;
    }

    /**
     * Use the formatted message?
     * @param bool $value
     */
    public function useFormattedMessage($value)
    {
        $this->useFormattedMessage = (bool) $value;
    }
}
<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Processor\ProcessorInterface;

/**
 * Interface to describe loggers that have processors
 *
 * This interface is present in monolog 1.x to ease forward compatibility.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
interface ProcessableHandlerInterface
{
    /**
     * Adds a processor in the stack.
     *
     * @param  ProcessorInterface|callable $callback
     * @return HandlerInterface            self
     */
    public function pushProcessor($callback): HandlerInterface;

    /**
     * Removes the processor on top of the stack and returns it.
     *
     * @throws \LogicException In case the processor stack is empty
     * @return callable
     */
    public function popProcessor(): callable;
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

/**
 * Exception can be thrown if an extension for an handler is missing
 *
 * @author  Christian Bergau <cbergau86@gmail.com>
 */
class MissingExtensionException extends \Exception
{
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Formatter\LineFormatter;

/**
 * Common syslog functionality
 */
abstract class AbstractSyslogHandler extends AbstractProcessingHandler
{
    protected $facility;

    /**
     * Translates Monolog log levels to syslog log priorities.
     */
    protected $logLevels = array(
        Logger::DEBUG     => LOG_DEBUG,
        Logger::INFO      => LOG_INFO,
        Logger::NOTICE    => LOG_NOTICE,
        Logger::WARNING   => LOG_WARNING,
        Logger::ERROR     => LOG_ERR,
        Logger::CRITICAL  => LOG_CRIT,
        Logger::ALERT     => LOG_ALERT,
        Logger::EMERGENCY => LOG_EMERG,
    );

    /**
     * List of valid log facility names.
     */
    protected $facilities = array(
        'auth'     => LOG_AUTH,
        'authpriv' => LOG_AUTHPRIV,
        'cron'     => LOG_CRON,
        'daemon'   => LOG_DAEMON,
        'kern'     => LOG_KERN,
        'lpr'      => LOG_LPR,
        'mail'     => LOG_MAIL,
        'news'     => LOG_NEWS,
        'syslog'   => LOG_SYSLOG,
        'user'     => LOG_USER,
        'uucp'     => LOG_UUCP,
    );

    /**
     * @param mixed $facility
     * @param int   $level The minimum logging level at which this handler will be triggered
     * @param bool  $bubble Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($facility = LOG_USER, $level = Logger::DEBUG, $bubble = true)
    {
        parent::__construct($level, $bubble);

        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
            $this->facilities['local0'] = LOG_LOCAL0;
            $this->facilities['local1'] = LOG_LOCAL1;
            $this->facilities['local2'] = LOG_LOCAL2;
            $this->facilities['local3'] = LOG_LOCAL3;
            $this->facilities['local4'] = LOG_LOCAL4;
            $this->facilities['local5'] = LOG_LOCAL5;
            $this->facilities['local6'] = LOG_LOCAL6;
            $this->facilities['local7'] = LOG_LOCAL7;
        } else {
            $this->facilities['local0'] = 128; // LOG_LOCAL0
            $this->facilities['local1'] = 136; // LOG_LOCAL1
            $this->facilities['local2'] = 144; // LOG_LOCAL2
            $this->facilities['local3'] = 152; // LOG_LOCAL3
            $this->facilities['local4'] = 160; // LOG_LOCAL4
            $this->facilities['local5'] = 168; // LOG_LOCAL5
            $this->facilities['local6'] = 176; // LOG_LOCAL6
            $this->facilities['local7'] = 184; // LOG_LOCAL7
        }

        // convert textual description of facility to syslog constant
        if (array_key_exists(strtolower($facility), $this->facilities)) {
            $facility = $this->facilities[strtolower($facility)];
        } elseif (!in_array($facility, array_values($this->facilities), true)) {
            throw new \UnexpectedValueException('Unknown facility value "'.$facility.'" given');
        }

        $this->facility = $facility;
    }

    /**
     * {@inheritdoc}
     */
    protected function getDefaultFormatter()
    {
        return new LineFormatter('%channel%.%level_name%: %message% %context% %extra%');
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\LineFormatter;
use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Raven_Client;

/**
 * Handler to send messages to a Sentry (https://github.com/getsentry/sentry) server
 * using sentry-php (https://github.com/getsentry/sentry-php)
 *
 * @author Marc Abramowitz <marc@marc-abramowitz.com>
 */
class RavenHandler extends AbstractProcessingHandler
{
    /**
     * Translates Monolog log levels to Raven log levels.
     */
    protected $logLevels = array(
        Logger::DEBUG     => Raven_Client::DEBUG,
        Logger::INFO      => Raven_Client::INFO,
        Logger::NOTICE    => Raven_Client::INFO,
        Logger::WARNING   => Raven_Client::WARNING,
        Logger::ERROR     => Raven_Client::ERROR,
        Logger::CRITICAL  => Raven_Client::FATAL,
        Logger::ALERT     => Raven_Client::FATAL,
        Logger::EMERGENCY => Raven_Client::FATAL,
    );

    /**
     * @var string should represent the current version of the calling
     *             software. Can be any string (git commit, version number)
     */
    protected $release;

    /**
     * @var Raven_Client the client object that sends the message to the server
     */
    protected $ravenClient;

    /**
     * @var LineFormatter The formatter to use for the logs generated via handleBatch()
     */
    protected $batchFormatter;

    /**
     * @param Raven_Client $ravenClient
     * @param int          $level       The minimum logging level at which this handler will be triggered
     * @param bool         $bubble      Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct(Raven_Client $ravenClient, $level = Logger::DEBUG, $bubble = true)
    {
        @trigger_error('The Monolog\Handler\RavenHandler class is deprecated. You should rather upgrade to the sentry/sentry 2.x and use Sentry\Monolog\Handler, see https://github.com/getsentry/sentry-php/blob/master/src/Monolog/Handler.php', E_USER_DEPRECATED);

        parent::__construct($level, $bubble);

        $this->ravenClient = $ravenClient;
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        $level = $this->level;

        // filter records based on their level
        $records = array_filter($records, function ($record) use ($level) {
            return $record['level'] >= $level;
        });

        if (!$records) {
            return;
        }

        // the record with the highest severity is the "main" one
        $record = array_reduce($records, function ($highest, $record) {
            if ($record['level'] > $highest['level']) {
                return $record;
            }

            return $highest;
        });

        // the other ones are added as a context item
        $logs = array();
        foreach ($records as $r) {
            $logs[] = $this->processRecord($r);
        }

        if ($logs) {
            $record['context']['logs'] = (string) $this->getBatchFormatter()->formatBatch($logs);
        }

        $this->handle($record);
    }

    /**
     * Sets the formatter for the logs generated by handleBatch().
     *
     * @param FormatterInterface $formatter
     */
    public function setBatchFormatter(FormatterInterface $formatter)
    {
        $this->batchFormatter = $formatter;
    }

    /**
     * Gets the formatter for the logs generated by handleBatch().
     *
     * @return FormatterInterface
     */
    public function getBatchFormatter()
    {
        if (!$this->batchFormatter) {
            $this->batchFormatter = $this->getDefaultBatchFormatter();
        }

        return $this->batchFormatter;
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        $previousUserContext = false;
        $options = array();
        $options['level'] = $this->logLevels[$record['level']];
        $options['tags'] = array();
        if (!empty($record['extra']['tags'])) {
            $options['tags'] = array_merge($options['tags'], $record['extra']['tags']);
            unset($record['extra']['tags']);
        }
        if (!empty($record['context']['tags'])) {
            $options['tags'] = array_merge($options['tags'], $record['context']['tags']);
            unset($record['context']['tags']);
        }
        if (!empty($record['context']['fingerprint'])) {
            $options['fingerprint'] = $record['context']['fingerprint'];
            unset($record['context']['fingerprint']);
        }
        if (!empty($record['context']['logger'])) {
            $options['logger'] = $record['context']['logger'];
            unset($record['context']['logger']);
        } else {
            $options['logger'] = $record['channel'];
        }
        foreach ($this->getExtraParameters() as $key) {
            foreach (array('extra', 'context') as $source) {
                if (!empty($record[$source][$key])) {
                    $options[$key] = $record[$source][$key];
                    unset($record[$source][$key]);
                }
            }
        }
        if (!empty($record['context'])) {
            $options['extra']['context'] = $record['context'];
            if (!empty($record['context']['user'])) {
                $previousUserContext = $this->ravenClient->context->user;
                $this->ravenClient->user_context($record['context']['user']);
                unset($options['extra']['context']['user']);
            }
        }
        if (!empty($record['extra'])) {
            $options['extra']['extra'] = $record['extra'];
        }

        if (!empty($this->release) && !isset($options['release'])) {
            $options['release'] = $this->release;
        }

        if (isset($record['context']['exception']) && ($record['context']['exception'] instanceof \Exception || (PHP_VERSION_ID >= 70000 && $record['context']['exception'] instanceof \Throwable))) {
            $options['message'] = $record['formatted'];
            $this->ravenClient->captureException($record['context']['exception'], $options);
        } else {
            $this->ravenClient->captureMessage($record['formatted'], array(), $options);
        }

        if ($previousUserContext !== false) {
            $this->ravenClient->user_context($previousUserContext);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new LineFormatter('[%channel%] %message%');
    }

    /**
     * Gets the default formatter for the logs generated by handleBatch().
     *
     * @return FormatterInterface
     */
    protected function getDefaultBatchFormatter()
    {
        return new LineFormatter();
    }

    /**
     * Gets extra parameters supported by Raven that can be found in "extra" and "context"
     *
     * @return array
     */
    protected function getExtraParameters()
    {
        return array('contexts', 'checksum', 'release', 'event_id');
    }

    /**
     * @param string $value
     * @return self
     */
    public function setRelease($value)
    {
        $this->release = $value;

        return $this;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Handler\SyslogUdp\UdpSocket;

/**
 * A Handler for logging to a remote syslogd server.
 *
 * @author Jesper Skovgaard Nielsen <nulpunkt@gmail.com>
 * @author Dominik Kukacka <dominik.kukacka@gmail.com>
 */
class SyslogUdpHandler extends AbstractSyslogHandler
{
    const RFC3164 = 0;
    const RFC5424 = 1;

    private $dateFormats = array(
        self::RFC3164 => 'M d H:i:s',
        self::RFC5424 => \DateTime::RFC3339,
    );

    protected $socket;
    protected $ident;
    protected $rfc;

    /**
     * @param string $host
     * @param int    $port
     * @param mixed  $facility
     * @param int    $level    The minimum logging level at which this handler will be triggered
     * @param bool   $bubble   Whether the messages that are handled can bubble up the stack or not
     * @param string $ident    Program name or tag for each log message.
     * @param int    $rfc      RFC to format the message for.
     */
    public function __construct($host, $port = 514, $facility = LOG_USER, $level = Logger::DEBUG, $bubble = true, $ident = 'php', $rfc = self::RFC5424)
    {
        parent::__construct($facility, $level, $bubble);

        $this->ident = $ident;
        $this->rfc = $rfc;

        $this->socket = new UdpSocket($host, $port ?: 514);
    }

    protected function write(array $record)
    {
        $lines = $this->splitMessageIntoLines($record['formatted']);

        $header = $this->makeCommonSyslogHeader($this->logLevels[$record['level']]);

        foreach ($lines as $line) {
            $this->socket->write($line, $header);
        }
    }

    public function close()
    {
        $this->socket->close();
    }

    private function splitMessageIntoLines($message)
    {
        if (is_array($message)) {
            $message = implode("\n", $message);
        }

        return preg_split('/$\R?^/m', $message, -1, PREG_SPLIT_NO_EMPTY);
    }

    /**
     * Make common syslog header (see rfc5424 or rfc3164)
     */
    protected function makeCommonSyslogHeader($severity)
    {
        $priority = $severity + $this->facility;

        if (!$pid = getmypid()) {
            $pid = '-';
        }

        if (!$hostname = gethostname()) {
            $hostname = '-';
        }

        $date = $this->getDateTime();

        if ($this->rfc === self::RFC3164) {
            return "<$priority>" .
                $date . " " .
                $hostname . " " .
                $this->ident . "[" . $pid . "]: ";
        } else {
            return "<$priority>1 " .
                $date . " " .
                $hostname . " " .
                $this->ident . " " .
                $pid . " - - ";
        }
    }

    protected function getDateTime()
    {
        return date($this->dateFormats[$this->rfc]);
    }

    /**
     * Inject your own socket, mainly used for testing
     */
    public function setSocket($socket)
    {
        $this->socket = $socket;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\LineFormatter;

/**
 * Handler sending logs to browser's javascript console with no browser extension required
 *
 * @author Olivier Poitrey <rs@dailymotion.com>
 */
class BrowserConsoleHandler extends AbstractProcessingHandler
{
    protected static $initialized = false;
    protected static $records = array();

    /**
     * {@inheritDoc}
     *
     * Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format.
     *
     * Example of formatted string:
     *
     *     You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white}
     */
    protected function getDefaultFormatter()
    {
        return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%');
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record)
    {
        // Accumulate records
        static::$records[] = $record;

        // Register shutdown handler if not already done
        if (!static::$initialized) {
            static::$initialized = true;
            $this->registerShutdownFunction();
        }
    }

    /**
     * Convert records to javascript console commands and send it to the browser.
     * This method is automatically called on PHP shutdown if output is HTML or Javascript.
     */
    public static function send()
    {
        $format = static::getResponseFormat();
        if ($format === 'unknown') {
            return;
        }

        if (count(static::$records)) {
            if ($format === 'html') {
                static::writeOutput('<script>' . static::generateScript() . '</script>');
            } elseif ($format === 'js') {
                static::writeOutput(static::generateScript());
            }
            static::resetStatic();
        }
    }

    public function close()
    {
        self::resetStatic();
    }

    public function reset()
    {
        self::resetStatic();
    }

    /**
     * Forget all logged records
     */
    public static function resetStatic()
    {
        static::$records = array();
    }

    /**
     * Wrapper for register_shutdown_function to allow overriding
     */
    protected function registerShutdownFunction()
    {
        if (PHP_SAPI !== 'cli') {
            register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send'));
        }
    }

    /**
     * Wrapper for echo to allow overriding
     *
     * @param string $str
     */
    protected static function writeOutput($str)
    {
        echo $str;
    }

    /**
     * Checks the format of the response
     *
     * If Content-Type is set to application/javascript or text/javascript -> js
     * If Content-Type is set to text/html, or is unset -> html
     * If Content-Type is anything else -> unknown
     *
     * @return string One of 'js', 'html' or 'unknown'
     */
    protected static function getResponseFormat()
    {
        // Check content type
        foreach (headers_list() as $header) {
            if (stripos($header, 'content-type:') === 0) {
                // This handler only works with HTML and javascript outputs
                // text/javascript is obsolete in favour of application/javascript, but still used
                if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) {
                    return 'js';
                }
                if (stripos($header, 'text/html') === false) {
                    return 'unknown';
                }
                break;
            }
        }

        return 'html';
    }

    private static function generateScript()
    {
        $script = array();
        foreach (static::$records as $record) {
            $context = static::dump('Context', $record['context']);
            $extra = static::dump('Extra', $record['extra']);

            if (empty($context) && empty($extra)) {
                $script[] = static::call_array('log', static::handleStyles($record['formatted']));
            } else {
                $script = array_merge($script,
                    array(static::call_array('groupCollapsed', static::handleStyles($record['formatted']))),
                    $context,
                    $extra,
                    array(static::call('groupEnd'))
                );
            }
        }

        return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
    }

    private static function handleStyles($formatted)
    {
        $args = array();
        $format = '%c' . $formatted;
        preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);

        foreach (array_reverse($matches) as $match) {
            $args[] = '"font-weight: normal"';
            $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0]));

            $pos = $match[0][1];
            $format = substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . substr($format, $pos + strlen($match[0][0]));
        }

        $args[] = static::quote('font-weight: normal');
        $args[] = static::quote($format);

        return array_reverse($args);
    }

    private static function handleCustomStyles($style, $string)
    {
        static $colors = array('blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey');
        static $labels = array();

        return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function ($m) use ($string, &$colors, &$labels) {
            if (trim($m[1]) === 'autolabel') {
                // Format the string as a label with consistent auto assigned background color
                if (!isset($labels[$string])) {
                    $labels[$string] = $colors[count($labels) % count($colors)];
                }
                $color = $labels[$string];

                return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px";
            }

            return $m[1];
        }, $style);
    }

    private static function dump($title, array $dict)
    {
        $script = array();
        $dict = array_filter($dict);
        if (empty($dict)) {
            return $script;
        }
        $script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title));
        foreach ($dict as $key => $value) {
            $value = json_encode($value);
            if (empty($value)) {
                $value = static::quote('');
            }
            $script[] = static::call('log', static::quote('%s: %o'), static::quote($key), $value);
        }

        return $script;
    }

    private static function quote($arg)
    {
        return '"' . addcslashes($arg, "\"\n\\") . '"';
    }

    private static function call()
    {
        $args = func_get_args();
        $method = array_shift($args);

        return static::call_array($method, $args);
    }

    private static function call_array($method, array $args)
    {
        return 'c.' . $method . '(' . implode(', ', $args) . ');';
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;

/**
 * Simple handler wrapper that filters records based on a list of levels
 *
 * It can be configured with an exact list of levels to allow, or a min/max level.
 *
 * @author Hennadiy Verkh
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class FilterHandler extends AbstractHandler
{
    /**
     * Handler or factory callable($record, $this)
     *
     * @var callable|\Monolog\Handler\HandlerInterface
     */
    protected $handler;

    /**
     * Minimum level for logs that are passed to handler
     *
     * @var int[]
     */
    protected $acceptedLevels;

    /**
     * Whether the messages that are handled can bubble up the stack or not
     *
     * @var bool
     */
    protected $bubble;

    /**
     * @param callable|HandlerInterface $handler        Handler or factory callable($record|null, $filterHandler).
     * @param int|array                 $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided
     * @param int                       $maxLevel       Maximum level to accept, only used if $minLevelOrList is not an array
     * @param bool                      $bubble         Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, $bubble = true)
    {
        $this->handler  = $handler;
        $this->bubble   = $bubble;
        $this->setAcceptedLevels($minLevelOrList, $maxLevel);

        if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
            throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
        }
    }

    /**
     * @return array
     */
    public function getAcceptedLevels()
    {
        return array_flip($this->acceptedLevels);
    }

    /**
     * @param int|string|array $minLevelOrList A list of levels to accept or a minimum level or level name if maxLevel is provided
     * @param int|string       $maxLevel       Maximum level or level name to accept, only used if $minLevelOrList is not an array
     */
    public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY)
    {
        if (is_array($minLevelOrList)) {
            $acceptedLevels = array_map('Monolog\Logger::toMonologLevel', $minLevelOrList);
        } else {
            $minLevelOrList = Logger::toMonologLevel($minLevelOrList);
            $maxLevel = Logger::toMonologLevel($maxLevel);
            $acceptedLevels = array_values(array_filter(Logger::getLevels(), function ($level) use ($minLevelOrList, $maxLevel) {
                return $level >= $minLevelOrList && $level <= $maxLevel;
            }));
        }
        $this->acceptedLevels = array_flip($acceptedLevels);
    }

    /**
     * {@inheritdoc}
     */
    public function isHandling(array $record)
    {
        return isset($this->acceptedLevels[$record['level']]);
    }

    /**
     * {@inheritdoc}
     */
    public function handle(array $record)
    {
        if (!$this->isHandling($record)) {
            return false;
        }

        if ($this->processors) {
            foreach ($this->processors as $processor) {
                $record = call_user_func($processor, $record);
            }
        }

        $this->getHandler($record)->handle($record);

        return false === $this->bubble;
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        $filtered = array();
        foreach ($records as $record) {
            if ($this->isHandling($record)) {
                $filtered[] = $record;
            }
        }

        if (count($filtered) > 0) {
            $this->getHandler($filtered[count($filtered) - 1])->handleBatch($filtered);
        }
    }

    /**
     * Return the nested handler
     *
     * If the handler was provided as a factory callable, this will trigger the handler's instantiation.
     *
     * @return HandlerInterface
     */
    public function getHandler(array $record = null)
    {
        if (!$this->handler instanceof HandlerInterface) {
            $this->handler = call_user_func($this->handler, $record, $this);
            if (!$this->handler instanceof HandlerInterface) {
                throw new \RuntimeException("The factory callable should return a HandlerInterface");
            }
        }

        return $this->handler;
    }

    /**
     * {@inheritdoc}
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        $this->getHandler()->setFormatter($formatter);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter()
    {
        return $this->getHandler()->getFormatter();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Utils;

/**
 * Stores logs to files that are rotated every day and a limited number of files are kept.
 *
 * This rotation is only intended to be used as a workaround. Using logrotate to
 * handle the rotation is strongly encouraged when you can use it.
 *
 * @author Christophe Coevoet <stof@notk.org>
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class RotatingFileHandler extends StreamHandler
{
    const FILE_PER_DAY = 'Y-m-d';
    const FILE_PER_MONTH = 'Y-m';
    const FILE_PER_YEAR = 'Y';

    protected $filename;
    protected $maxFiles;
    protected $mustRotate;
    protected $nextRotation;
    protected $filenameFormat;
    protected $dateFormat;

    /**
     * @param string   $filename
     * @param int      $maxFiles       The maximal amount of files to keep (0 means unlimited)
     * @param int      $level          The minimum logging level at which this handler will be triggered
     * @param bool     $bubble         Whether the messages that are handled can bubble up the stack or not
     * @param int|null $filePermission Optional file permissions (default (0644) are only for owner read/write)
     * @param bool     $useLocking     Try to lock log file before doing any writes
     */
    public function __construct($filename, $maxFiles = 0, $level = Logger::DEBUG, $bubble = true, $filePermission = null, $useLocking = false)
    {
        $this->filename = Utils::canonicalizePath($filename);
        $this->maxFiles = (int) $maxFiles;
        $this->nextRotation = new \DateTime('tomorrow');
        $this->filenameFormat = '{filename}-{date}';
        $this->dateFormat = 'Y-m-d';

        parent::__construct($this->getTimedFilename(), $level, $bubble, $filePermission, $useLocking);
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        parent::close();

        if (true === $this->mustRotate) {
            $this->rotate();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function reset()
    {
        parent::reset();

        if (true === $this->mustRotate) {
            $this->rotate();
        }
    }

    public function setFilenameFormat($filenameFormat, $dateFormat)
    {
        if (!preg_match('{^Y(([/_.-]?m)([/_.-]?d)?)?$}', $dateFormat)) {
            trigger_error(
                'Invalid date format - format must be one of '.
                'RotatingFileHandler::FILE_PER_DAY ("Y-m-d"), RotatingFileHandler::FILE_PER_MONTH ("Y-m") '.
                'or RotatingFileHandler::FILE_PER_YEAR ("Y"), or you can set one of the '.
                'date formats using slashes, underscores and/or dots instead of dashes.',
                E_USER_DEPRECATED
            );
        }
        if (substr_count($filenameFormat, '{date}') === 0) {
            trigger_error(
                'Invalid filename format - format should contain at least `{date}`, because otherwise rotating is impossible.',
                E_USER_DEPRECATED
            );
        }
        $this->filenameFormat = $filenameFormat;
        $this->dateFormat = $dateFormat;
        $this->url = $this->getTimedFilename();
        $this->close();
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        // on the first record written, if the log is new, we should rotate (once per day)
        if (null === $this->mustRotate) {
            $this->mustRotate = !file_exists($this->url);
        }

        if ($this->nextRotation < $record['datetime']) {
            $this->mustRotate = true;
            $this->close();
        }

        parent::write($record);
    }

    /**
     * Rotates the files.
     */
    protected function rotate()
    {
        // update filename
        $this->url = $this->getTimedFilename();
        $this->nextRotation = new \DateTime('tomorrow');

        // skip GC of old logs if files are unlimited
        if (0 === $this->maxFiles) {
            return;
        }

        $logFiles = glob($this->getGlobPattern());
        if ($this->maxFiles >= count($logFiles)) {
            // no files to remove
            return;
        }

        // Sorting the files by name to remove the older ones
        usort($logFiles, function ($a, $b) {
            return strcmp($b, $a);
        });

        foreach (array_slice($logFiles, $this->maxFiles) as $file) {
            if (is_writable($file)) {
                // suppress errors here as unlink() might fail if two processes
                // are cleaning up/rotating at the same time
                set_error_handler(function ($errno, $errstr, $errfile, $errline) {});
                unlink($file);
                restore_error_handler();
            }
        }

        $this->mustRotate = false;
    }

    protected function getTimedFilename()
    {
        $fileInfo = pathinfo($this->filename);
        $timedFilename = str_replace(
            array('{filename}', '{date}'),
            array($fileInfo['filename'], date($this->dateFormat)),
            $fileInfo['dirname'] . '/' . $this->filenameFormat
        );

        if (!empty($fileInfo['extension'])) {
            $timedFilename .= '.'.$fileInfo['extension'];
        }

        return $timedFilename;
    }

    protected function getGlobPattern()
    {
        $fileInfo = pathinfo($this->filename);
        $glob = str_replace(
            array('{filename}', '{date}'),
            array($fileInfo['filename'], '[0-9][0-9][0-9][0-9]*'),
            $fileInfo['dirname'] . '/' . $this->filenameFormat
        );
        if (!empty($fileInfo['extension'])) {
            $glob .= '.'.$fileInfo['extension'];
        }

        return $glob;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Formatter\LineFormatter;

/**
 * NativeMailerHandler uses the mail() function to send the emails
 *
 * @author Christophe Coevoet <stof@notk.org>
 * @author Mark Garrett <mark@moderndeveloperllc.com>
 */
class NativeMailerHandler extends MailHandler
{
    /**
     * The email addresses to which the message will be sent
     * @var array
     */
    protected $to;

    /**
     * The subject of the email
     * @var string
     */
    protected $subject;

    /**
     * Optional headers for the message
     * @var array
     */
    protected $headers = array();

    /**
     * Optional parameters for the message
     * @var array
     */
    protected $parameters = array();

    /**
     * The wordwrap length for the message
     * @var int
     */
    protected $maxColumnWidth;

    /**
     * The Content-type for the message
     * @var string
     */
    protected $contentType = 'text/plain';

    /**
     * The encoding for the message
     * @var string
     */
    protected $encoding = 'utf-8';

    /**
     * @param string|array $to             The receiver of the mail
     * @param string       $subject        The subject of the mail
     * @param string       $from           The sender of the mail
     * @param int          $level          The minimum logging level at which this handler will be triggered
     * @param bool         $bubble         Whether the messages that are handled can bubble up the stack or not
     * @param int          $maxColumnWidth The maximum column width that the message lines will have
     */
    public function __construct($to, $subject, $from, $level = Logger::ERROR, $bubble = true, $maxColumnWidth = 70)
    {
        parent::__construct($level, $bubble);
        $this->to = is_array($to) ? $to : array($to);
        $this->subject = $subject;
        $this->addHeader(sprintf('From: %s', $from));
        $this->maxColumnWidth = $maxColumnWidth;
    }

    /**
     * Add headers to the message
     *
     * @param  string|array $headers Custom added headers
     * @return self
     */
    public function addHeader($headers)
    {
        foreach ((array) $headers as $header) {
            if (strpos($header, "\n") !== false || strpos($header, "\r") !== false) {
                throw new \InvalidArgumentException('Headers can not contain newline characters for security reasons');
            }
            $this->headers[] = $header;
        }

        return $this;
    }

    /**
     * Add parameters to the message
     *
     * @param  string|array $parameters Custom added parameters
     * @return self
     */
    public function addParameter($parameters)
    {
        $this->parameters = array_merge($this->parameters, (array) $parameters);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    protected function send($content, array $records)
    {
        $content = wordwrap($content, $this->maxColumnWidth);
        $headers = ltrim(implode("\r\n", $this->headers) . "\r\n", "\r\n");
        $headers .= 'Content-type: ' . $this->getContentType() . '; charset=' . $this->getEncoding() . "\r\n";
        if ($this->getContentType() == 'text/html' && false === strpos($headers, 'MIME-Version:')) {
            $headers .= 'MIME-Version: 1.0' . "\r\n";
        }

        $subject = $this->subject;
        if ($records) {
            $subjectFormatter = new LineFormatter($this->subject);
            $subject = $subjectFormatter->format($this->getHighestRecord($records));
        }

        $parameters = implode(' ', $this->parameters);
        foreach ($this->to as $to) {
            mail($to, $subject, $content, $headers, $parameters);
        }
    }

    /**
     * @return string $contentType
     */
    public function getContentType()
    {
        return $this->contentType;
    }

    /**
     * @return string $encoding
     */
    public function getEncoding()
    {
        return $this->encoding;
    }

    /**
     * @param  string $contentType The content type of the email - Defaults to text/plain. Use text/html for HTML
     *                             messages.
     * @return self
     */
    public function setContentType($contentType)
    {
        if (strpos($contentType, "\n") !== false || strpos($contentType, "\r") !== false) {
            throw new \InvalidArgumentException('The content type can not contain newline characters to prevent email header injection');
        }

        $this->contentType = $contentType;

        return $this;
    }

    /**
     * @param  string $encoding
     * @return self
     */
    public function setEncoding($encoding)
    {
        if (strpos($encoding, "\n") !== false || strpos($encoding, "\r") !== false) {
            throw new \InvalidArgumentException('The encoding can not contain newline characters to prevent email header injection');
        }

        $this->encoding = $encoding;

        return $this;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Formatter\NormalizerFormatter;

/**
 * Logs to a MongoDB database.
 *
 * usage example:
 *
 *   $log = new Logger('application');
 *   $mongodb = new MongoDBHandler(new \Mongo("mongodb://localhost:27017"), "logs", "prod");
 *   $log->pushHandler($mongodb);
 *
 * @author Thomas Tourlourat <thomas@tourlourat.com>
 */
class MongoDBHandler extends AbstractProcessingHandler
{
    protected $mongoCollection;

    public function __construct($mongo, $database, $collection, $level = Logger::DEBUG, $bubble = true)
    {
        if (!($mongo instanceof \MongoClient || $mongo instanceof \Mongo || $mongo instanceof \MongoDB\Client)) {
            throw new \InvalidArgumentException('MongoClient, Mongo or MongoDB\Client instance required');
        }

        $this->mongoCollection = $mongo->selectCollection($database, $collection);

        parent::__construct($level, $bubble);
    }

    protected function write(array $record)
    {
        if ($this->mongoCollection instanceof \MongoDB\Collection) {
            $this->mongoCollection->insertOne($record["formatted"]);
        } else {
            $this->mongoCollection->save($record["formatted"]);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new NormalizerFormatter();
    }
}
<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;

/**
 * Helper trait for implementing FormattableInterface
 *
 * This trait is present in monolog 1.x to ease forward compatibility.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
trait FormattableHandlerTrait
{
    /**
     * @var FormatterInterface
     */
    protected $formatter;

    /**
     * {@inheritdoc}
     * @suppress PhanTypeMismatchReturn
     */
    public function setFormatter(FormatterInterface $formatter): HandlerInterface
    {
        $this->formatter = $formatter;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getFormatter(): FormatterInterface
    {
        if (!$this->formatter) {
            $this->formatter = $this->getDefaultFormatter();
        }

        return $this->formatter;
    }

    /**
     * Gets the default formatter.
     *
     * Overwrite this if the LineFormatter is not a good default for your handler.
     */
    protected function getDefaultFormatter(): FormatterInterface
    {
        return new LineFormatter();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Gelf\IMessagePublisher;
use Gelf\PublisherInterface;
use Gelf\Publisher;
use InvalidArgumentException;
use Monolog\Logger;
use Monolog\Formatter\GelfMessageFormatter;

/**
 * Handler to send messages to a Graylog2 (http://www.graylog2.org) server
 *
 * @author Matt Lehner <mlehner@gmail.com>
 * @author Benjamin Zikarsky <benjamin@zikarsky.de>
 */
class GelfHandler extends AbstractProcessingHandler
{
    /**
     * @var Publisher the publisher object that sends the message to the server
     */
    protected $publisher;

    /**
     * @param PublisherInterface|IMessagePublisher|Publisher $publisher a publisher object
     * @param int                                            $level     The minimum logging level at which this handler will be triggered
     * @param bool                                           $bubble    Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($publisher, $level = Logger::DEBUG, $bubble = true)
    {
        parent::__construct($level, $bubble);

        if (!$publisher instanceof Publisher && !$publisher instanceof IMessagePublisher && !$publisher instanceof PublisherInterface) {
            throw new InvalidArgumentException('Invalid publisher, expected a Gelf\Publisher, Gelf\IMessagePublisher or Gelf\PublisherInterface instance');
        }

        $this->publisher = $publisher;
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        $this->publisher->publish($record['formatted']);
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new GelfMessageFormatter();
    }
}
<?php
/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\NormalizerFormatter;
use Monolog\Logger;

/**
 * Handler sending logs to Zend Monitor
 *
 * @author  Christian Bergau <cbergau86@gmail.com>
 * @author  Jason Davis <happydude@jasondavis.net>
 */
class ZendMonitorHandler extends AbstractProcessingHandler
{
    /**
     * Monolog level / ZendMonitor Custom Event priority map
     *
     * @var array
     */
    protected $levelMap = array();

    /**
     * Construct
     *
     * @param  int                       $level
     * @param  bool                      $bubble
     * @throws MissingExtensionException
     */
    public function __construct($level = Logger::DEBUG, $bubble = true)
    {
        if (!function_exists('zend_monitor_custom_event')) {
            throw new MissingExtensionException(
                'You must have Zend Server installed with Zend Monitor enabled in order to use this handler'
            );
        }
        //zend monitor constants are not defined if zend monitor is not enabled.
        $this->levelMap = array(
            Logger::DEBUG     => \ZEND_MONITOR_EVENT_SEVERITY_INFO,
            Logger::INFO      => \ZEND_MONITOR_EVENT_SEVERITY_INFO,
            Logger::NOTICE    => \ZEND_MONITOR_EVENT_SEVERITY_INFO,
            Logger::WARNING   => \ZEND_MONITOR_EVENT_SEVERITY_WARNING,
            Logger::ERROR     => \ZEND_MONITOR_EVENT_SEVERITY_ERROR,
            Logger::CRITICAL  => \ZEND_MONITOR_EVENT_SEVERITY_ERROR,
            Logger::ALERT     => \ZEND_MONITOR_EVENT_SEVERITY_ERROR,
            Logger::EMERGENCY => \ZEND_MONITOR_EVENT_SEVERITY_ERROR,
        );
        parent::__construct($level, $bubble);
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        $this->writeZendMonitorCustomEvent(
            Logger::getLevelName($record['level']),
            $record['message'],
            $record['formatted'],
            $this->levelMap[$record['level']]
        );
    }

    /**
     * Write to Zend Monitor Events
     * @param string $type Text displayed in "Class Name (custom)" field
     * @param string $message Text displayed in "Error String"
     * @param mixed $formatted Displayed in Custom Variables tab
     * @param int $severity Set the event severity level (-1,0,1)
     */
    protected function writeZendMonitorCustomEvent($type, $message, $formatted, $severity)
    {
        zend_monitor_custom_event($type, $message, $formatted, $severity);
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultFormatter()
    {
        return new NormalizerFormatter();
    }

    /**
     * Get the level map
     *
     * @return array
     */
    public function getLevelMap()
    {
        return $this->levelMap;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Exception;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Monolog\Utils;
use PhpConsole\Connector;
use PhpConsole\Handler;
use PhpConsole\Helper;

/**
 * Monolog handler for Google Chrome extension "PHP Console"
 *
 * Display PHP error/debug log messages in Google Chrome console and notification popups, executes PHP code remotely
 *
 * Usage:
 * 1. Install Google Chrome extension https://chrome.google.com/webstore/detail/php-console/nfhmhhlpfleoednkpnnnkolmclajemef
 * 2. See overview https://github.com/barbushin/php-console#overview
 * 3. Install PHP Console library https://github.com/barbushin/php-console#installation
 * 4. Example (result will looks like http://i.hizliresim.com/vg3Pz4.png)
 *
 *      $logger = new \Monolog\Logger('all', array(new \Monolog\Handler\PHPConsoleHandler()));
 *      \Monolog\ErrorHandler::register($logger);
 *      echo $undefinedVar;
 *      $logger->addDebug('SELECT * FROM users', array('db', 'time' => 0.012));
 *      PC::debug($_SERVER); // PHP Console debugger for any type of vars
 *
 * @author Sergey Barbushin https://www.linkedin.com/in/barbushin
 */
class PHPConsoleHandler extends AbstractProcessingHandler
{
    private $options = array(
        'enabled' => true, // bool Is PHP Console server enabled
        'classesPartialsTraceIgnore' => array('Monolog\\'), // array Hide calls of classes started with...
        'debugTagsKeysInContext' => array(0, 'tag'), // bool Is PHP Console server enabled
        'useOwnErrorsHandler' => false, // bool Enable errors handling
        'useOwnExceptionsHandler' => false, // bool Enable exceptions handling
        'sourcesBasePath' => null, // string Base path of all project sources to strip in errors source paths
        'registerHelper' => true, // bool Register PhpConsole\Helper that allows short debug calls like PC::debug($var, 'ta.g.s')
        'serverEncoding' => null, // string|null Server internal encoding
        'headersLimit' => null, // int|null Set headers size limit for your web-server
        'password' => null, // string|null Protect PHP Console connection by password
        'enableSslOnlyMode' => false, // bool Force connection by SSL for clients with PHP Console installed
        'ipMasks' => array(), // array Set IP masks of clients that will be allowed to connect to PHP Console: array('192.168.*.*', '127.0.0.1')
        'enableEvalListener' => false, // bool Enable eval request to be handled by eval dispatcher(if enabled, 'password' option is also required)
        'dumperDetectCallbacks' => false, // bool Convert callback items in dumper vars to (callback SomeClass::someMethod) strings
        'dumperLevelLimit' => 5, // int Maximum dumped vars array or object nested dump level
        'dumperItemsCountLimit' => 100, // int Maximum dumped var same level array items or object properties number
        'dumperItemSizeLimit' => 5000, // int Maximum length of any string or dumped array item
        'dumperDumpSizeLimit' => 500000, // int Maximum approximate size of dumped vars result formatted in JSON
        'detectDumpTraceAndSource' => false, // bool Autodetect and append trace data to debug
        'dataStorage' => null, // PhpConsole\Storage|null Fixes problem with custom $_SESSION handler(see http://goo.gl/Ne8juJ)
    );

    /** @var Connector */
    private $connector;

    /**
     * @param  array          $options   See \Monolog\Handler\PHPConsoleHandler::$options for more details
     * @param  Connector|null $connector Instance of \PhpConsole\Connector class (optional)
     * @param  int            $level
     * @param  bool           $bubble
     * @throws Exception
     */
    public function __construct(array $options = array(), Connector $connector = null, $level = Logger::DEBUG, $bubble = true)
    {
        if (!class_exists('PhpConsole\Connector')) {
            throw new Exception('PHP Console library not found. See https://github.com/barbushin/php-console#installation');
        }
        parent::__construct($level, $bubble);
        $this->options = $this->initOptions($options);
        $this->connector = $this->initConnector($connector);
    }

    private function initOptions(array $options)
    {
        $wrongOptions = array_diff(array_keys($options), array_keys($this->options));
        if ($wrongOptions) {
            throw new Exception('Unknown options: ' . implode(', ', $wrongOptions));
        }

        return array_replace($this->options, $options);
    }

    private function initConnector(Connector $connector = null)
    {
        if (!$connector) {
            if ($this->options['dataStorage']) {
                Connector::setPostponeStorage($this->options['dataStorage']);
            }
            $connector = Connector::getInstance();
        }

        if ($this->options['registerHelper'] && !Helper::isRegistered()) {
            Helper::register();
        }

        if ($this->options['enabled'] && $connector->isActiveClient()) {
            if ($this->options['useOwnErrorsHandler'] || $this->options['useOwnExceptionsHandler']) {
                $handler = Handler::getInstance();
                $handler->setHandleErrors($this->options['useOwnErrorsHandler']);
                $handler->setHandleExceptions($this->options['useOwnExceptionsHandler']);
                $handler->start();
            }
            if ($this->options['sourcesBasePath']) {
                $connector->setSourcesBasePath($this->options['sourcesBasePath']);
            }
            if ($this->options['serverEncoding']) {
                $connector->setServerEncoding($this->options['serverEncoding']);
            }
            if ($this->options['password']) {
                $connector->setPassword($this->options['password']);
            }
            if ($this->options['enableSslOnlyMode']) {
                $connector->enableSslOnlyMode();
            }
            if ($this->options['ipMasks']) {
                $connector->setAllowedIpMasks($this->options['ipMasks']);
            }
            if ($this->options['headersLimit']) {
                $connector->setHeadersLimit($this->options['headersLimit']);
            }
            if ($this->options['detectDumpTraceAndSource']) {
                $connector->getDebugDispatcher()->detectTraceAndSource = true;
            }
            $dumper = $connector->getDumper();
            $dumper->levelLimit = $this->options['dumperLevelLimit'];
            $dumper->itemsCountLimit = $this->options['dumperItemsCountLimit'];
            $dumper->itemSizeLimit = $this->options['dumperItemSizeLimit'];
            $dumper->dumpSizeLimit = $this->options['dumperDumpSizeLimit'];
            $dumper->detectCallbacks = $this->options['dumperDetectCallbacks'];
            if ($this->options['enableEvalListener']) {
                $connector->startEvalRequestsListener();
            }
        }

        return $connector;
    }

    public function getConnector()
    {
        return $this->connector;
    }

    public function getOptions()
    {
        return $this->options;
    }

    public function handle(array $record)
    {
        if ($this->options['enabled'] && $this->connector->isActiveClient()) {
            return parent::handle($record);
        }

        return !$this->bubble;
    }

    /**
     * Writes the record down to the log of the implementing handler
     *
     * @param  array $record
     * @return void
     */
    protected function write(array $record)
    {
        if ($record['level'] < Logger::NOTICE) {
            $this->handleDebugRecord($record);
        } elseif (isset($record['context']['exception']) && $record['context']['exception'] instanceof Exception) {
            $this->handleExceptionRecord($record);
        } else {
            $this->handleErrorRecord($record);
        }
    }

    private function handleDebugRecord(array $record)
    {
        $tags = $this->getRecordTags($record);
        $message = $record['message'];
        if ($record['context']) {
            $message .= ' ' . Utils::jsonEncode($this->connector->getDumper()->dump(array_filter($record['context'])), null, true);
        }
        $this->connector->getDebugDispatcher()->dispatchDebug($message, $tags, $this->options['classesPartialsTraceIgnore']);
    }

    private function handleExceptionRecord(array $record)
    {
        $this->connector->getErrorsDispatcher()->dispatchException($record['context']['exception']);
    }

    private function handleErrorRecord(array $record)
    {
        $context = $record['context'];

        $this->connector->getErrorsDispatcher()->dispatchError(
            isset($context['code']) ? $context['code'] : null,
            isset($context['message']) ? $context['message'] : $record['message'],
            isset($context['file']) ? $context['file'] : null,
            isset($context['line']) ? $context['line'] : null,
            $this->options['classesPartialsTraceIgnore']
        );
    }

    private function getRecordTags(array &$record)
    {
        $tags = null;
        if (!empty($record['context'])) {
            $context = & $record['context'];
            foreach ($this->options['debugTagsKeysInContext'] as $key) {
                if (!empty($context[$key])) {
                    $tags = $context[$key];
                    if ($key === 0) {
                        array_shift($context);
                    } else {
                        unset($context[$key]);
                    }
                    break;
                }
            }
        }

        return $tags ?: strtolower($record['level_name']);
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new LineFormatter('%message%');
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\LineFormatter;
use Monolog\Logger;

/**
 * Stores to PHP error_log() handler.
 *
 * @author Elan Ruusamäe <glen@delfi.ee>
 */
class ErrorLogHandler extends AbstractProcessingHandler
{
    const OPERATING_SYSTEM = 0;
    const SAPI = 4;

    protected $messageType;
    protected $expandNewlines;

    /**
     * @param int  $messageType    Says where the error should go.
     * @param int  $level          The minimum logging level at which this handler will be triggered
     * @param bool $bubble         Whether the messages that are handled can bubble up the stack or not
     * @param bool $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries
     */
    public function __construct($messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, $bubble = true, $expandNewlines = false)
    {
        parent::__construct($level, $bubble);

        if (false === in_array($messageType, self::getAvailableTypes())) {
            $message = sprintf('The given message type "%s" is not supported', print_r($messageType, true));
            throw new \InvalidArgumentException($message);
        }

        $this->messageType = $messageType;
        $this->expandNewlines = $expandNewlines;
    }

    /**
     * @return array With all available types
     */
    public static function getAvailableTypes()
    {
        return array(
            self::OPERATING_SYSTEM,
            self::SAPI,
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new LineFormatter('[%datetime%] %channel%.%level_name%: %message% %context% %extra%');
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        if ($this->expandNewlines) {
            $lines = preg_split('{[\r\n]+}', (string) $record['formatted']);
            foreach ($lines as $line) {
                error_log($line, $this->messageType);
            }
        } else {
            error_log((string) $record['formatted'], $this->messageType);
        }
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * @author Robert Kaufmann III <rok3@rok3.me>
 */
class LogEntriesHandler extends SocketHandler
{
    /**
     * @var string
     */
    protected $logToken;

    /**
     * @param string $token  Log token supplied by LogEntries
     * @param bool   $useSSL Whether or not SSL encryption should be used.
     * @param int    $level  The minimum logging level to trigger this handler
     * @param bool   $bubble Whether or not messages that are handled should bubble up the stack.
     *
     * @throws MissingExtensionException If SSL encryption is set to true and OpenSSL is missing
     */
    public function __construct($token, $useSSL = true, $level = Logger::DEBUG, $bubble = true, $host = 'data.logentries.com')
    {
        if ($useSSL && !extension_loaded('openssl')) {
            throw new MissingExtensionException('The OpenSSL PHP plugin is required to use SSL encrypted connection for LogEntriesHandler');
        }

        $endpoint = $useSSL ? 'ssl://' . $host . ':443' : $host . ':80';
        parent::__construct($endpoint, $level, $bubble);
        $this->logToken = $token;
    }

    /**
     * {@inheritdoc}
     *
     * @param  array  $record
     * @return string
     */
    protected function generateDataStream($record)
    {
        return $this->logToken . ' ' . $record['formatted'];
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * Simple handler wrapper that deduplicates log records across multiple requests
 *
 * It also includes the BufferHandler functionality and will buffer
 * all messages until the end of the request or flush() is called.
 *
 * This works by storing all log records' messages above $deduplicationLevel
 * to the file specified by $deduplicationStore. When further logs come in at the end of the
 * request (or when flush() is called), all those above $deduplicationLevel are checked
 * against the existing stored logs. If they match and the timestamps in the stored log is
 * not older than $time seconds, the new log record is discarded. If no log record is new, the
 * whole data set is discarded.
 *
 * This is mainly useful in combination with Mail handlers or things like Slack or HipChat handlers
 * that send messages to people, to avoid spamming with the same message over and over in case of
 * a major component failure like a database server being down which makes all requests fail in the
 * same way.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class DeduplicationHandler extends BufferHandler
{
    /**
     * @var string
     */
    protected $deduplicationStore;

    /**
     * @var int
     */
    protected $deduplicationLevel;

    /**
     * @var int
     */
    protected $time;

    /**
     * @var bool
     */
    private $gc = false;

    /**
     * @param HandlerInterface $handler            Handler.
     * @param string           $deduplicationStore The file/path where the deduplication log should be kept
     * @param int              $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes
     * @param int              $time               The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through
     * @param bool             $bubble             Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct(HandlerInterface $handler, $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, $time = 60, $bubble = true)
    {
        parent::__construct($handler, 0, Logger::DEBUG, $bubble, false);

        $this->deduplicationStore = $deduplicationStore === null ? sys_get_temp_dir() . '/monolog-dedup-' . substr(md5(__FILE__), 0, 20) .'.log' : $deduplicationStore;
        $this->deduplicationLevel = Logger::toMonologLevel($deduplicationLevel);
        $this->time = $time;
    }

    public function flush()
    {
        if ($this->bufferSize === 0) {
            return;
        }

        $passthru = null;

        foreach ($this->buffer as $record) {
            if ($record['level'] >= $this->deduplicationLevel) {

                $passthru = $passthru || !$this->isDuplicate($record);
                if ($passthru) {
                    $this->appendRecord($record);
                }
            }
        }

        // default of null is valid as well as if no record matches duplicationLevel we just pass through
        if ($passthru === true || $passthru === null) {
            $this->handler->handleBatch($this->buffer);
        }

        $this->clear();

        if ($this->gc) {
            $this->collectLogs();
        }
    }

    private function isDuplicate(array $record)
    {
        if (!file_exists($this->deduplicationStore)) {
            return false;
        }

        $store = file($this->deduplicationStore, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if (!is_array($store)) {
            return false;
        }

        $yesterday = time() - 86400;
        $timestampValidity = $record['datetime']->getTimestamp() - $this->time;
        $expectedMessage = preg_replace('{[\r\n].*}', '', $record['message']);

        for ($i = count($store) - 1; $i >= 0; $i--) {
            list($timestamp, $level, $message) = explode(':', $store[$i], 3);

            if ($level === $record['level_name'] && $message === $expectedMessage && $timestamp > $timestampValidity) {
                return true;
            }

            if ($timestamp < $yesterday) {
                $this->gc = true;
            }
        }

        return false;
    }

    private function collectLogs()
    {
        if (!file_exists($this->deduplicationStore)) {
            return false;
        }

        $handle = fopen($this->deduplicationStore, 'rw+');
        flock($handle, LOCK_EX);
        $validLogs = array();

        $timestampValidity = time() - $this->time;

        while (!feof($handle)) {
            $log = fgets($handle);
            if (substr($log, 0, 10) >= $timestampValidity) {
                $validLogs[] = $log;
            }
        }

        ftruncate($handle, 0);
        rewind($handle);
        foreach ($validLogs as $log) {
            fwrite($handle, $log);
        }

        flock($handle, LOCK_UN);
        fclose($handle);

        $this->gc = false;
    }

    private function appendRecord(array $record)
    {
        file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\WildfireFormatter;

/**
 * Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol.
 *
 * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
 */
class FirePHPHandler extends AbstractProcessingHandler
{
    /**
     * WildFire JSON header message format
     */
    const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2';

    /**
     * FirePHP structure for parsing messages & their presentation
     */
    const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1';

    /**
     * Must reference a "known" plugin, otherwise headers won't display in FirePHP
     */
    const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3';

    /**
     * Header prefix for Wildfire to recognize & parse headers
     */
    const HEADER_PREFIX = 'X-Wf';

    /**
     * Whether or not Wildfire vendor-specific headers have been generated & sent yet
     */
    protected static $initialized = false;

    /**
     * Shared static message index between potentially multiple handlers
     * @var int
     */
    protected static $messageIndex = 1;

    protected static $sendHeaders = true;

    /**
     * Base header creation function used by init headers & record headers
     *
     * @param  array  $meta    Wildfire Plugin, Protocol & Structure Indexes
     * @param  string $message Log message
     * @return array  Complete header string ready for the client as key and message as value
     */
    protected function createHeader(array $meta, $message)
    {
        $header = sprintf('%s-%s', self::HEADER_PREFIX, join('-', $meta));

        return array($header => $message);
    }

    /**
     * Creates message header from record
     *
     * @see createHeader()
     * @param  array  $record
     * @return string
     */
    protected function createRecordHeader(array $record)
    {
        // Wildfire is extensible to support multiple protocols & plugins in a single request,
        // but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake.
        return $this->createHeader(
            array(1, 1, 1, self::$messageIndex++),
            $record['formatted']
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new WildfireFormatter();
    }

    /**
     * Wildfire initialization headers to enable message parsing
     *
     * @see createHeader()
     * @see sendHeader()
     * @return array
     */
    protected function getInitHeaders()
    {
        // Initial payload consists of required headers for Wildfire
        return array_merge(
            $this->createHeader(array('Protocol', 1), self::PROTOCOL_URI),
            $this->createHeader(array(1, 'Structure', 1), self::STRUCTURE_URI),
            $this->createHeader(array(1, 'Plugin', 1), self::PLUGIN_URI)
        );
    }

    /**
     * Send header string to the client
     *
     * @param string $header
     * @param string $content
     */
    protected function sendHeader($header, $content)
    {
        if (!headers_sent() && self::$sendHeaders) {
            header(sprintf('%s: %s', $header, $content));
        }
    }

    /**
     * Creates & sends header for a record, ensuring init headers have been sent prior
     *
     * @see sendHeader()
     * @see sendInitHeaders()
     * @param array $record
     */
    protected function write(array $record)
    {
        if (!self::$sendHeaders) {
            return;
        }

        // WildFire-specific headers must be sent prior to any messages
        if (!self::$initialized) {
            self::$initialized = true;

            self::$sendHeaders = $this->headersAccepted();
            if (!self::$sendHeaders) {
                return;
            }

            foreach ($this->getInitHeaders() as $header => $content) {
                $this->sendHeader($header, $content);
            }
        }

        $header = $this->createRecordHeader($record);
        if (trim(current($header)) !== '') {
            $this->sendHeader(key($header), current($header));
        }
    }

    /**
     * Verifies if the headers are accepted by the current user agent
     *
     * @return bool
     */
    protected function headersAccepted()
    {
        if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) {
            return true;
        }

        return isset($_SERVER['HTTP_X_FIREPHP_VERSION']);
    }

    /**
     * BC getter for the sendHeaders property that has been made static
     */
    public function __get($property)
    {
        if ('sendHeaders' !== $property) {
            throw new \InvalidArgumentException('Undefined property '.$property);
        }

        return static::$sendHeaders;
    }

    /**
     * BC setter for the sendHeaders property that has been made static
     */
    public function __set($property, $value)
    {
        if ('sendHeaders' !== $property) {
            throw new \InvalidArgumentException('Undefined property '.$property);
        }

        static::$sendHeaders = $value;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler\Curl;

class Util
{
    private static $retriableErrorCodes = array(
        CURLE_COULDNT_RESOLVE_HOST,
        CURLE_COULDNT_CONNECT,
        CURLE_HTTP_NOT_FOUND,
        CURLE_READ_ERROR,
        CURLE_OPERATION_TIMEOUTED,
        CURLE_HTTP_POST_ERROR,
        CURLE_SSL_CONNECT_ERROR,
    );

    /**
     * Executes a CURL request with optional retries and exception on failure
     *
     * @param  resource          $ch curl handler
     * @throws \RuntimeException
     */
    public static function execute($ch, $retries = 5, $closeAfterDone = true)
    {
        while ($retries--) {
            if (curl_exec($ch) === false) {
                $curlErrno = curl_errno($ch);

                if (false === in_array($curlErrno, self::$retriableErrorCodes, true) || !$retries) {
                    $curlError = curl_error($ch);

                    if ($closeAfterDone) {
                        curl_close($ch);
                    }

                    throw new \RuntimeException(sprintf('Curl error (code %s): %s', $curlErrno, $curlError));
                }

                continue;
            }

            if ($closeAfterDone) {
                curl_close($ch);
            }
            break;
        }
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Formatter\NormalizerFormatter;
use Doctrine\CouchDB\CouchDBClient;

/**
 * CouchDB handler for Doctrine CouchDB ODM
 *
 * @author Markus Bachmann <markus.bachmann@bachi.biz>
 */
class DoctrineCouchDBHandler extends AbstractProcessingHandler
{
    private $client;

    public function __construct(CouchDBClient $client, $level = Logger::DEBUG, $bubble = true)
    {
        $this->client = $client;
        parent::__construct($level, $bubble);
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record)
    {
        $this->client->postDocument($record['formatted']);
    }

    protected function getDefaultFormatter()
    {
        return new NormalizerFormatter;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Aws\Sdk;
use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\Marshaler;
use Monolog\Formatter\ScalarFormatter;
use Monolog\Logger;

/**
 * Amazon DynamoDB handler (http://aws.amazon.com/dynamodb/)
 *
 * @link https://github.com/aws/aws-sdk-php/
 * @author Andrew Lawson <adlawson@gmail.com>
 */
class DynamoDbHandler extends AbstractProcessingHandler
{
    const DATE_FORMAT = 'Y-m-d\TH:i:s.uO';

    /**
     * @var DynamoDbClient
     */
    protected $client;

    /**
     * @var string
     */
    protected $table;

    /**
     * @var int
     */
    protected $version;

    /**
     * @var Marshaler
     */
    protected $marshaler;

    /**
     * @param DynamoDbClient $client
     * @param string         $table
     * @param int            $level
     * @param bool           $bubble
     */
    public function __construct(DynamoDbClient $client, $table, $level = Logger::DEBUG, $bubble = true)
    {
        if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) {
            $this->version = 3;
            $this->marshaler = new Marshaler;
        } else {
            $this->version = 2;
        }

        $this->client = $client;
        $this->table = $table;

        parent::__construct($level, $bubble);
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        $filtered = $this->filterEmptyFields($record['formatted']);
        if ($this->version === 3) {
            $formatted = $this->marshaler->marshalItem($filtered);
        } else {
            $formatted = $this->client->formatAttributes($filtered);
        }

        $this->client->putItem(array(
            'TableName' => $this->table,
            'Item' => $formatted,
        ));
    }

    /**
     * @param  array $record
     * @return array
     */
    protected function filterEmptyFields(array $record)
    {
        return array_filter($record, function ($value) {
            return !empty($value) || false === $value || 0 === $value;
        });
    }

    /**
     * {@inheritdoc}
     */
    protected function getDefaultFormatter()
    {
        return new ScalarFormatter(self::DATE_FORMAT);
    }
}
<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;

/**
 * Interface to describe loggers that have a formatter
 *
 * This interface is present in monolog 1.x to ease forward compatibility.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
interface FormattableHandlerInterface
{
    /**
     * Sets the formatter.
     *
     * @param  FormatterInterface $formatter
     * @return HandlerInterface   self
     */
    public function setFormatter(FormatterInterface $formatter): HandlerInterface;

    /**
     * Gets the formatter.
     *
     * @return FormatterInterface
     */
    public function getFormatter(): FormatterInterface;
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

/**
 * Base class for all mail handlers
 *
 * @author Gyula Sallai
 */
abstract class MailHandler extends AbstractProcessingHandler
{
    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        $messages = array();

        foreach ($records as $record) {
            if ($record['level'] < $this->level) {
                continue;
            }
            $messages[] = $this->processRecord($record);
        }

        if (!empty($messages)) {
            $this->send((string) $this->getFormatter()->formatBatch($messages), $messages);
        }
    }

    /**
     * Send a mail with the given content
     *
     * @param string $content formatted email body to be sent
     * @param array  $records the array of log records that formed this content
     */
    abstract protected function send($content, array $records);

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        $this->send((string) $record['formatted'], array($record));
    }

    protected function getHighestRecord(array $records)
    {
        $highestRecord = null;
        foreach ($records as $record) {
            if ($highestRecord === null || $highestRecord['level'] < $record['level']) {
                $highestRecord = $record;
            }
        }

        return $highestRecord;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Utils;

/**
 * Stores to any stream resource
 *
 * Can be used to store into php://stderr, remote and local files, etc.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class StreamHandler extends AbstractProcessingHandler
{
    protected $stream;
    protected $url;
    private $errorMessage;
    protected $filePermission;
    protected $useLocking;
    private $dirCreated;

    /**
     * @param resource|string $stream
     * @param int             $level          The minimum logging level at which this handler will be triggered
     * @param bool            $bubble         Whether the messages that are handled can bubble up the stack or not
     * @param int|null        $filePermission Optional file permissions (default (0644) are only for owner read/write)
     * @param bool            $useLocking     Try to lock log file before doing any writes
     *
     * @throws \Exception                If a missing directory is not buildable
     * @throws \InvalidArgumentException If stream is not a resource or string
     */
    public function __construct($stream, $level = Logger::DEBUG, $bubble = true, $filePermission = null, $useLocking = false)
    {
        parent::__construct($level, $bubble);
        if (is_resource($stream)) {
            $this->stream = $stream;
        } elseif (is_string($stream)) {
            $this->url = Utils::canonicalizePath($stream);
        } else {
            throw new \InvalidArgumentException('A stream must either be a resource or a string.');
        }

        $this->filePermission = $filePermission;
        $this->useLocking = $useLocking;
    }

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        if ($this->url && is_resource($this->stream)) {
            fclose($this->stream);
        }
        $this->stream = null;
        $this->dirCreated = null;
    }

    /**
     * Return the currently active stream if it is open
     *
     * @return resource|null
     */
    public function getStream()
    {
        return $this->stream;
    }

    /**
     * Return the stream URL if it was configured with a URL and not an active resource
     *
     * @return string|null
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        if (!is_resource($this->stream)) {
            if (null === $this->url || '' === $this->url) {
                throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
            }
            $this->createDir();
            $this->errorMessage = null;
            set_error_handler(array($this, 'customErrorHandler'));
            $this->stream = fopen($this->url, 'a');
            if ($this->filePermission !== null) {
                @chmod($this->url, $this->filePermission);
            }
            restore_error_handler();
            if (!is_resource($this->stream)) {
                $this->stream = null;
                throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: '.$this->errorMessage, $this->url));
            }
        }

        if ($this->useLocking) {
            // ignoring errors here, there's not much we can do about them
            flock($this->stream, LOCK_EX);
        }

        $this->streamWrite($this->stream, $record);

        if ($this->useLocking) {
            flock($this->stream, LOCK_UN);
        }
    }

    /**
     * Write to stream
     * @param resource $stream
     * @param array $record
     */
    protected function streamWrite($stream, array $record)
    {
        fwrite($stream, (string) $record['formatted']);
    }

    private function customErrorHandler($code, $msg)
    {
        $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg);
    }

    /**
     * @param string $stream
     *
     * @return null|string
     */
    private function getDirFromStream($stream)
    {
        $pos = strpos($stream, '://');
        if ($pos === false) {
            return dirname($stream);
        }

        if ('file://' === substr($stream, 0, 7)) {
            return dirname(substr($stream, 7));
        }

        return;
    }

    private function createDir()
    {
        // Do not try to create dir if it has already been tried.
        if ($this->dirCreated) {
            return;
        }

        $dir = $this->getDirFromStream($this->url);
        if (null !== $dir && !is_dir($dir)) {
            $this->errorMessage = null;
            set_error_handler(array($this, 'customErrorHandler'));
            $status = mkdir($dir, 0777, true);
            restore_error_handler();
            if (false === $status && !is_dir($dir)) {
                throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and its not buildable: '.$this->errorMessage, $dir));
            }
        }
        $this->dirCreated = true;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Formatter\LogglyFormatter;

/**
 * Sends errors to Loggly.
 *
 * @author Przemek Sobstel <przemek@sobstel.org>
 * @author Adam Pancutt <adam@pancutt.com>
 * @author Gregory Barchard <gregory@barchard.net>
 */
class LogglyHandler extends AbstractProcessingHandler
{
    const HOST = 'logs-01.loggly.com';
    const ENDPOINT_SINGLE = 'inputs';
    const ENDPOINT_BATCH = 'bulk';

    protected $token;

    protected $tag = array();

    public function __construct($token, $level = Logger::DEBUG, $bubble = true)
    {
        if (!extension_loaded('curl')) {
            throw new \LogicException('The curl extension is needed to use the LogglyHandler');
        }

        $this->token = $token;

        parent::__construct($level, $bubble);
    }

    public function setTag($tag)
    {
        $tag = !empty($tag) ? $tag : array();
        $this->tag = is_array($tag) ? $tag : array($tag);
    }

    public function addTag($tag)
    {
        if (!empty($tag)) {
            $tag = is_array($tag) ? $tag : array($tag);
            $this->tag = array_unique(array_merge($this->tag, $tag));
        }
    }

    protected function write(array $record)
    {
        $this->send($record["formatted"], self::ENDPOINT_SINGLE);
    }

    public function handleBatch(array $records)
    {
        $level = $this->level;

        $records = array_filter($records, function ($record) use ($level) {
            return ($record['level'] >= $level);
        });

        if ($records) {
            $this->send($this->getFormatter()->formatBatch($records), self::ENDPOINT_BATCH);
        }
    }

    protected function send($data, $endpoint)
    {
        $url = sprintf("https://%s/%s/%s/", self::HOST, $endpoint, $this->token);

        $headers = array('Content-Type: application/json');

        if (!empty($this->tag)) {
            $headers[] = 'X-LOGGLY-TAG: '.implode(',', $this->tag);
        }

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        Curl\Util::execute($ch);
    }

    protected function getDefaultFormatter()
    {
        return new LogglyFormatter();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
use Monolog\Handler\Slack\SlackRecord;

/**
 * Sends notifications through Slack Webhooks
 *
 * @author Haralan Dobrev <hkdobrev@gmail.com>
 * @see    https://api.slack.com/incoming-webhooks
 */
class SlackWebhookHandler extends AbstractProcessingHandler
{
    /**
     * Slack Webhook token
     * @var string
     */
    private $webhookUrl;

    /**
     * Instance of the SlackRecord util class preparing data for Slack API.
     * @var SlackRecord
     */
    private $slackRecord;

    /**
     * @param  string      $webhookUrl             Slack Webhook URL
     * @param  string|null $channel                Slack channel (encoded ID or name)
     * @param  string|null $username               Name of a bot
     * @param  bool        $useAttachment          Whether the message should be added to Slack as attachment (plain text otherwise)
     * @param  string|null $iconEmoji              The emoji name to use (or null)
     * @param  bool        $useShortAttachment     Whether the the context/extra messages added to Slack as attachments are in a short style
     * @param  bool        $includeContextAndExtra Whether the attachment should include context and extra data
     * @param  int         $level                  The minimum logging level at which this handler will be triggered
     * @param  bool        $bubble                 Whether the messages that are handled can bubble up the stack or not
     * @param  array       $excludeFields          Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2']
     */
    public function __construct($webhookUrl, $channel = null, $username = null, $useAttachment = true, $iconEmoji = null, $useShortAttachment = false, $includeContextAndExtra = false, $level = Logger::CRITICAL, $bubble = true, array $excludeFields = array())
    {
        parent::__construct($level, $bubble);

        $this->webhookUrl = $webhookUrl;

        $this->slackRecord = new SlackRecord(
            $channel,
            $username,
            $useAttachment,
            $iconEmoji,
            $useShortAttachment,
            $includeContextAndExtra,
            $excludeFields,
            $this->formatter
        );
    }

    public function getSlackRecord()
    {
        return $this->slackRecord;
    }

    public function getWebhookUrl()
    {
        return $this->webhookUrl;
    }

    /**
     * {@inheritdoc}
     *
     * @param array $record
     */
    protected function write(array $record)
    {
        $postData = $this->slackRecord->getSlackData($record);
        $postString = Utils::jsonEncode($postData);

        $ch = curl_init();
        $options = array(
            CURLOPT_URL => $this->webhookUrl,
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => array('Content-type: application/json'),
            CURLOPT_POSTFIELDS => $postString
        );
        if (defined('CURLOPT_SAFE_UPLOAD')) {
            $options[CURLOPT_SAFE_UPLOAD] = true;
        }

        curl_setopt_array($ch, $options);

        Curl\Util::execute($ch);
    }

    public function setFormatter(FormatterInterface $formatter)
    {
        parent::setFormatter($formatter);
        $this->slackRecord->setFormatter($formatter);

        return $this;
    }

    public function getFormatter()
    {
        $formatter = parent::getFormatter();
        $this->slackRecord->setFormatter($formatter);

        return $formatter;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

/**
 * Forwards records to multiple handlers suppressing failures of each handler
 * and continuing through to give every handler a chance to succeed.
 *
 * @author Craig D'Amelio <craig@damelio.ca>
 */
class WhatFailureGroupHandler extends GroupHandler
{
    /**
     * {@inheritdoc}
     */
    public function handle(array $record)
    {
        if ($this->processors) {
            foreach ($this->processors as $processor) {
                $record = call_user_func($processor, $record);
            }
        }

        foreach ($this->handlers as $handler) {
            try {
                $handler->handle($record);
            } catch (\Exception $e) {
                // What failure?
            } catch (\Throwable $e) {
                // What failure?
            }
        }

        return false === $this->bubble;
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        if ($this->processors) {
            $processed = array();
            foreach ($records as $record) {
                foreach ($this->processors as $processor) {
                    $record = call_user_func($processor, $record);
                }
                $processed[] = $record;
            }
            $records = $processed;
        }

        foreach ($this->handlers as $handler) {
            try {
                $handler->handleBatch($records);
            } catch (\Exception $e) {
                // What failure?
            } catch (\Throwable $e) {
                // What failure?
            }
        }
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\ChromePHPFormatter;
use Monolog\Logger;
use Monolog\Utils;

/**
 * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/)
 *
 * This also works out of the box with Firefox 43+
 *
 * @author Christophe Coevoet <stof@notk.org>
 */
class ChromePHPHandler extends AbstractProcessingHandler
{
    /**
     * Version of the extension
     */
    const VERSION = '4.0';

    /**
     * Header name
     */
    const HEADER_NAME = 'X-ChromeLogger-Data';

    /**
     * Regular expression to detect supported browsers (matches any Chrome, or Firefox 43+)
     */
    const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|HeadlessChrome|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}';

    protected static $initialized = false;

    /**
     * Tracks whether we sent too much data
     *
     * Chrome limits the headers to 4KB, so when we sent 3KB we stop sending
     *
     * @var bool
     */
    protected static $overflowed = false;

    protected static $json = array(
        'version' => self::VERSION,
        'columns' => array('label', 'log', 'backtrace', 'type'),
        'rows' => array(),
    );

    protected static $sendHeaders = true;

    /**
     * @param int  $level  The minimum logging level at which this handler will be triggered
     * @param bool $bubble Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($level = Logger::DEBUG, $bubble = true)
    {
        parent::__construct($level, $bubble);
        if (!function_exists('json_encode')) {
            throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s ChromePHPHandler');
        }
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        $messages = array();

        foreach ($records as $record) {
            if ($record['level'] < $this->level) {
                continue;
            }
            $messages[] = $this->processRecord($record);
        }

        if (!empty($messages)) {
            $messages = $this->getFormatter()->formatBatch($messages);
            self::$json['rows'] = array_merge(self::$json['rows'], $messages);
            $this->send();
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new ChromePHPFormatter();
    }

    /**
     * Creates & sends header for a record
     *
     * @see sendHeader()
     * @see send()
     * @param array $record
     */
    protected function write(array $record)
    {
        self::$json['rows'][] = $record['formatted'];

        $this->send();
    }

    /**
     * Sends the log header
     *
     * @see sendHeader()
     */
    protected function send()
    {
        if (self::$overflowed || !self::$sendHeaders) {
            return;
        }

        if (!self::$initialized) {
            self::$initialized = true;

            self::$sendHeaders = $this->headersAccepted();
            if (!self::$sendHeaders) {
                return;
            }

            self::$json['request_uri'] = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
        }

        $json = Utils::jsonEncode(self::$json, null, true);
        $data = base64_encode(utf8_encode($json));
        if (strlen($data) > 3 * 1024) {
            self::$overflowed = true;

            $record = array(
                'message' => 'Incomplete logs, chrome header size limit reached',
                'context' => array(),
                'level' => Logger::WARNING,
                'level_name' => Logger::getLevelName(Logger::WARNING),
                'channel' => 'monolog',
                'datetime' => new \DateTime(),
                'extra' => array(),
            );
            self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record);
            $json = Utils::jsonEncode(self::$json, null, true);
            $data = base64_encode(utf8_encode($json));
        }

        if (trim($data) !== '') {
            $this->sendHeader(self::HEADER_NAME, $data);
        }
    }

    /**
     * Send header string to the client
     *
     * @param string $header
     * @param string $content
     */
    protected function sendHeader($header, $content)
    {
        if (!headers_sent() && self::$sendHeaders) {
            header(sprintf('%s: %s', $header, $content));
        }
    }

    /**
     * Verifies if the headers are accepted by the current user agent
     *
     * @return bool
     */
    protected function headersAccepted()
    {
        if (empty($_SERVER['HTTP_USER_AGENT'])) {
            return false;
        }

        return preg_match(self::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']);
    }

    /**
     * BC getter for the sendHeaders property that has been made static
     */
    public function __get($property)
    {
        if ('sendHeaders' !== $property) {
            throw new \InvalidArgumentException('Undefined property '.$property);
        }

        return static::$sendHeaders;
    }

    /**
     * BC setter for the sendHeaders property that has been made static
     */
    public function __set($property, $value)
    {
        if ('sendHeaders' !== $property) {
            throw new \InvalidArgumentException('Undefined property '.$property);
        }

        static::$sendHeaders = $value;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler\Slack;

use Monolog\Logger;
use Monolog\Utils;
use Monolog\Formatter\NormalizerFormatter;
use Monolog\Formatter\FormatterInterface;

/**
 * Slack record utility helping to log to Slack webhooks or API.
 *
 * @author Greg Kedzierski <greg@gregkedzierski.com>
 * @author Haralan Dobrev <hkdobrev@gmail.com>
 * @see    https://api.slack.com/incoming-webhooks
 * @see    https://api.slack.com/docs/message-attachments
 */
class SlackRecord
{
    const COLOR_DANGER = 'danger';

    const COLOR_WARNING = 'warning';

    const COLOR_GOOD = 'good';

    const COLOR_DEFAULT = '#e3e4e6';

    /**
     * Slack channel (encoded ID or name)
     * @var string|null
     */
    private $channel;

    /**
     * Name of a bot
     * @var string|null
     */
    private $username;

    /**
     * User icon e.g. 'ghost', 'http://example.com/user.png'
     * @var string
     */
    private $userIcon;

    /**
     * Whether the message should be added to Slack as attachment (plain text otherwise)
     * @var bool
     */
    private $useAttachment;

    /**
     * Whether the the context/extra messages added to Slack as attachments are in a short style
     * @var bool
     */
    private $useShortAttachment;

    /**
     * Whether the attachment should include context and extra data
     * @var bool
     */
    private $includeContextAndExtra;

    /**
     * Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2']
     * @var array
     */
    private $excludeFields;

    /**
     * @var FormatterInterface
     */
    private $formatter;

    /**
     * @var NormalizerFormatter
     */
    private $normalizerFormatter;

    public function __construct($channel = null, $username = null, $useAttachment = true, $userIcon = null, $useShortAttachment = false, $includeContextAndExtra = false, array $excludeFields = array(), FormatterInterface $formatter = null)
    {
        $this->channel = $channel;
        $this->username = $username;
        $this->userIcon = trim($userIcon, ':');
        $this->useAttachment = $useAttachment;
        $this->useShortAttachment = $useShortAttachment;
        $this->includeContextAndExtra = $includeContextAndExtra;
        $this->excludeFields = $excludeFields;
        $this->formatter = $formatter;

        if ($this->includeContextAndExtra) {
            $this->normalizerFormatter = new NormalizerFormatter();
        }
    }

    public function getSlackData(array $record)
    {
        $dataArray = array();
        $record = $this->excludeFields($record);

        if ($this->username) {
            $dataArray['username'] = $this->username;
        }

        if ($this->channel) {
            $dataArray['channel'] = $this->channel;
        }

        if ($this->formatter && !$this->useAttachment) {
            $message = $this->formatter->format($record);
        } else {
            $message = $record['message'];
        }

        if ($this->useAttachment) {
            $attachment = array(
                'fallback'  => $message,
                'text'      => $message,
                'color'     => $this->getAttachmentColor($record['level']),
                'fields'    => array(),
                'mrkdwn_in' => array('fields'),
                'ts'        => $record['datetime']->getTimestamp()
            );

            if ($this->useShortAttachment) {
                $attachment['title'] = $record['level_name'];
            } else {
                $attachment['title'] = 'Message';
                $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name']);
            }


            if ($this->includeContextAndExtra) {
                foreach (array('extra', 'context') as $key) {
                    if (empty($record[$key])) {
                        continue;
                    }

                    if ($this->useShortAttachment) {
                        $attachment['fields'][] = $this->generateAttachmentField(
                            $key,
                            $record[$key]
                        );
                    } else {
                        // Add all extra fields as individual fields in attachment
                        $attachment['fields'] = array_merge(
                            $attachment['fields'],
                            $this->generateAttachmentFields($record[$key])
                        );
                    }
                }
            }

            $dataArray['attachments'] = array($attachment);
        } else {
            $dataArray['text'] = $message;
        }

        if ($this->userIcon) {
            if (filter_var($this->userIcon, FILTER_VALIDATE_URL)) {
                $dataArray['icon_url'] = $this->userIcon;
            } else {
                $dataArray['icon_emoji'] = ":{$this->userIcon}:";
            }
        }

        return $dataArray;
    }

    /**
     * Returned a Slack message attachment color associated with
     * provided level.
     *
     * @param  int    $level
     * @return string
     */
    public function getAttachmentColor($level)
    {
        switch (true) {
            case $level >= Logger::ERROR:
                return self::COLOR_DANGER;
            case $level >= Logger::WARNING:
                return self::COLOR_WARNING;
            case $level >= Logger::INFO:
                return self::COLOR_GOOD;
            default:
                return self::COLOR_DEFAULT;
        }
    }

    /**
     * Stringifies an array of key/value pairs to be used in attachment fields
     *
     * @param array $fields
     *
     * @return string
     */
    public function stringify($fields)
    {
        $normalized = $this->normalizerFormatter->format($fields);
        $prettyPrintFlag = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 128;
        $flags = 0;
        if (PHP_VERSION_ID >= 50400) {
            $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
        }

        $hasSecondDimension = count(array_filter($normalized, 'is_array'));
        $hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric'));

        return $hasSecondDimension || $hasNonNumericKeys
            ? Utils::jsonEncode($normalized, $prettyPrintFlag | $flags)
            : Utils::jsonEncode($normalized, $flags);
    }

    /**
     * Sets the formatter
     *
     * @param FormatterInterface $formatter
     */
    public function setFormatter(FormatterInterface $formatter)
    {
        $this->formatter = $formatter;
    }

    /**
     * Generates attachment field
     *
     * @param string       $title
     * @param string|array $value
     *
     * @return array
     */
    private function generateAttachmentField($title, $value)
    {
        $value = is_array($value)
            ? sprintf('```%s```', $this->stringify($value))
            : $value;

        return array(
            'title' => ucfirst($title),
            'value' => $value,
            'short' => false
        );
    }

    /**
     * Generates a collection of attachment fields from array
     *
     * @param array $data
     *
     * @return array
     */
    private function generateAttachmentFields(array $data)
    {
        $fields = array();
        foreach ($this->normalizerFormatter->format($data) as $key => $value) {
            $fields[] = $this->generateAttachmentField($key, $value);
        }

        return $fields;
    }

    /**
     * Get a copy of record with fields excluded according to $this->excludeFields
     *
     * @param array $record
     *
     * @return array
     */
    private function excludeFields(array $record)
    {
        foreach ($this->excludeFields as $field) {
            $keys = explode('.', $field);
            $node = &$record;
            $lastKey = end($keys);
            foreach ($keys as $key) {
                if (!isset($node[$key])) {
                    break;
                }
                if ($lastKey === $key) {
                    unset($node[$key]);
                    break;
                }
                $node = &$node[$key];
            }
        }

        return $record;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Psr\Log\LoggerInterface;

/**
 * Proxies log messages to an existing PSR-3 compliant logger.
 *
 * @author Michael Moussa <michael.moussa@gmail.com>
 */
class PsrHandler extends AbstractHandler
{
    /**
     * PSR-3 compliant logger
     *
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * @param LoggerInterface $logger The underlying PSR-3 compliant logger to which messages will be proxied
     * @param int             $level  The minimum logging level at which this handler will be triggered
     * @param bool            $bubble Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct(LoggerInterface $logger, $level = Logger::DEBUG, $bubble = true)
    {
        parent::__construct($level, $bubble);

        $this->logger = $logger;
    }

    /**
     * {@inheritDoc}
     */
    public function handle(array $record)
    {
        if (!$this->isHandling($record)) {
            return false;
        }

        $this->logger->log(strtolower($record['level_name']), $record['message'], $record['context']);

        return false === $this->bubble;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Utils;
use Monolog\Formatter\NormalizerFormatter;

/**
 * Class to record a log on a NewRelic application.
 * Enabling New Relic High Security mode may prevent capture of useful information.
 *
 * This handler requires a NormalizerFormatter to function and expects an array in $record['formatted']
 *
 * @see https://docs.newrelic.com/docs/agents/php-agent
 * @see https://docs.newrelic.com/docs/accounts-partnerships/accounts/security/high-security
 */
class NewRelicHandler extends AbstractProcessingHandler
{
    /**
     * Name of the New Relic application that will receive logs from this handler.
     *
     * @var string
     */
    protected $appName;

    /**
     * Name of the current transaction
     *
     * @var string
     */
    protected $transactionName;

    /**
     * Some context and extra data is passed into the handler as arrays of values. Do we send them as is
     * (useful if we are using the API), or explode them for display on the NewRelic RPM website?
     *
     * @var bool
     */
    protected $explodeArrays;

    /**
     * {@inheritDoc}
     *
     * @param string $appName
     * @param bool   $explodeArrays
     * @param string $transactionName
     */
    public function __construct(
        $level = Logger::ERROR,
        $bubble = true,
        $appName = null,
        $explodeArrays = false,
        $transactionName = null
    ) {
        parent::__construct($level, $bubble);

        $this->appName       = $appName;
        $this->explodeArrays = $explodeArrays;
        $this->transactionName = $transactionName;
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record)
    {
        if (!$this->isNewRelicEnabled()) {
            throw new MissingExtensionException('The newrelic PHP extension is required to use the NewRelicHandler');
        }

        if ($appName = $this->getAppName($record['context'])) {
            $this->setNewRelicAppName($appName);
        }

        if ($transactionName = $this->getTransactionName($record['context'])) {
            $this->setNewRelicTransactionName($transactionName);
            unset($record['formatted']['context']['transaction_name']);
        }

        if (isset($record['context']['exception']) && ($record['context']['exception'] instanceof \Exception || (PHP_VERSION_ID >= 70000 && $record['context']['exception'] instanceof \Throwable))) {
            newrelic_notice_error($record['message'], $record['context']['exception']);
            unset($record['formatted']['context']['exception']);
        } else {
            newrelic_notice_error($record['message']);
        }

        if (isset($record['formatted']['context']) && is_array($record['formatted']['context'])) {
            foreach ($record['formatted']['context'] as $key => $parameter) {
                if (is_array($parameter) && $this->explodeArrays) {
                    foreach ($parameter as $paramKey => $paramValue) {
                        $this->setNewRelicParameter('context_' . $key . '_' . $paramKey, $paramValue);
                    }
                } else {
                    $this->setNewRelicParameter('context_' . $key, $parameter);
                }
            }
        }

        if (isset($record['formatted']['extra']) && is_array($record['formatted']['extra'])) {
            foreach ($record['formatted']['extra'] as $key => $parameter) {
                if (is_array($parameter) && $this->explodeArrays) {
                    foreach ($parameter as $paramKey => $paramValue) {
                        $this->setNewRelicParameter('extra_' . $key . '_' . $paramKey, $paramValue);
                    }
                } else {
                    $this->setNewRelicParameter('extra_' . $key, $parameter);
                }
            }
        }
    }

    /**
     * Checks whether the NewRelic extension is enabled in the system.
     *
     * @return bool
     */
    protected function isNewRelicEnabled()
    {
        return extension_loaded('newrelic');
    }

    /**
     * Returns the appname where this log should be sent. Each log can override the default appname, set in this
     * handler's constructor, by providing the appname in it's context.
     *
     * @param  array       $context
     * @return null|string
     */
    protected function getAppName(array $context)
    {
        if (isset($context['appname'])) {
            return $context['appname'];
        }

        return $this->appName;
    }

    /**
     * Returns the name of the current transaction. Each log can override the default transaction name, set in this
     * handler's constructor, by providing the transaction_name in it's context
     *
     * @param array $context
     *
     * @return null|string
     */
    protected function getTransactionName(array $context)
    {
        if (isset($context['transaction_name'])) {
            return $context['transaction_name'];
        }

        return $this->transactionName;
    }

    /**
     * Sets the NewRelic application that should receive this log.
     *
     * @param string $appName
     */
    protected function setNewRelicAppName($appName)
    {
        newrelic_set_appname($appName);
    }

    /**
     * Overwrites the name of the current transaction
     *
     * @param string $transactionName
     */
    protected function setNewRelicTransactionName($transactionName)
    {
        newrelic_name_transaction($transactionName);
    }

    /**
     * @param string $key
     * @param mixed  $value
     */
    protected function setNewRelicParameter($key, $value)
    {
        if (null === $value || is_scalar($value)) {
            newrelic_add_custom_parameter($key, $value);
        } else {
            newrelic_add_custom_parameter($key, Utils::jsonEncode($value, null, true));
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new NormalizerFormatter();
    }
}
<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\ResettableInterface;

/**
 * Helper trait for implementing ProcessableInterface
 *
 * This trait is present in monolog 1.x to ease forward compatibility.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
trait ProcessableHandlerTrait
{
    /**
     * @var callable[]
     */
    protected $processors = [];

    /**
     * {@inheritdoc}
     * @suppress PhanTypeMismatchReturn
     */
    public function pushProcessor($callback): HandlerInterface
    {
        array_unshift($this->processors, $callback);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function popProcessor(): callable
    {
        if (!$this->processors) {
            throw new \LogicException('You tried to pop from an empty processor stack.');
        }

        return array_shift($this->processors);
    }

    /**
     * Processes a record.
     */
    protected function processRecord(array $record): array
    {
        foreach ($this->processors as $processor) {
            $record = $processor($record);
        }

        return $record;
    }

    protected function resetProcessors(): void
    {
        foreach ($this->processors as $processor) {
            if ($processor instanceof ResettableInterface) {
                $processor->reset();
            }
        }
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;

/**
 * Interface that all Monolog Handlers must implement
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
interface HandlerInterface
{
    /**
     * Checks whether the given record will be handled by this handler.
     *
     * This is mostly done for performance reasons, to avoid calling processors for nothing.
     *
     * Handlers should still check the record levels within handle(), returning false in isHandling()
     * is no guarantee that handle() will not be called, and isHandling() might not be called
     * for a given record.
     *
     * @param array $record Partial log record containing only a level key
     *
     * @return bool
     */
    public function isHandling(array $record);

    /**
     * Handles a record.
     *
     * All records may be passed to this method, and the handler should discard
     * those that it does not want to handle.
     *
     * The return value of this function controls the bubbling process of the handler stack.
     * Unless the bubbling is interrupted (by returning true), the Logger class will keep on
     * calling further handlers in the stack with a given log record.
     *
     * @param  array   $record The record to handle
     * @return bool true means that this handler handled the record, and that bubbling is not permitted.
     *                        false means the record was either not processed or that this handler allows bubbling.
     */
    public function handle(array $record);

    /**
     * Handles a set of records at once.
     *
     * @param array $records The records to handle (an array of record arrays)
     */
    public function handleBatch(array $records);

    /**
     * Adds a processor in the stack.
     *
     * @param  callable $callback
     * @return self
     */
    public function pushProcessor($callback);

    /**
     * Removes the processor on top of the stack and returns it.
     *
     * @return callable
     */
    public function popProcessor();

    /**
     * Sets the formatter.
     *
     * @param  FormatterInterface $formatter
     * @return self
     */
    public function setFormatter(FormatterInterface $formatter);

    /**
     * Gets the formatter.
     *
     * @return FormatterInterface
     */
    public function getFormatter();
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\FormatterInterface;
use Monolog\Logger;
use Monolog\Utils;
use Monolog\Handler\Slack\SlackRecord;

/**
 * Sends notifications through Slack API
 *
 * @author Greg Kedzierski <greg@gregkedzierski.com>
 * @see    https://api.slack.com/
 */
class SlackHandler extends SocketHandler
{
    /**
     * Slack API token
     * @var string
     */
    private $token;

    /**
     * Instance of the SlackRecord util class preparing data for Slack API.
     * @var SlackRecord
     */
    private $slackRecord;

    /**
     * @param  string                    $token                  Slack API token
     * @param  string                    $channel                Slack channel (encoded ID or name)
     * @param  string|null               $username               Name of a bot
     * @param  bool                      $useAttachment          Whether the message should be added to Slack as attachment (plain text otherwise)
     * @param  string|null               $iconEmoji              The emoji name to use (or null)
     * @param  int                       $level                  The minimum logging level at which this handler will be triggered
     * @param  bool                      $bubble                 Whether the messages that are handled can bubble up the stack or not
     * @param  bool                      $useShortAttachment     Whether the the context/extra messages added to Slack as attachments are in a short style
     * @param  bool                      $includeContextAndExtra Whether the attachment should include context and extra data
     * @param  array                     $excludeFields          Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2']
     * @throws MissingExtensionException If no OpenSSL PHP extension configured
     */
    public function __construct($token, $channel, $username = null, $useAttachment = true, $iconEmoji = null, $level = Logger::CRITICAL, $bubble = true, $useShortAttachment = false, $includeContextAndExtra = false, array $excludeFields = array())
    {
        if (!extension_loaded('openssl')) {
            throw new MissingExtensionException('The OpenSSL PHP extension is required to use the SlackHandler');
        }

        parent::__construct('ssl://slack.com:443', $level, $bubble);

        $this->slackRecord = new SlackRecord(
            $channel,
            $username,
            $useAttachment,
            $iconEmoji,
            $useShortAttachment,
            $includeContextAndExtra,
            $excludeFields,
            $this->formatter
        );

        $this->token = $token;
    }

    public function getSlackRecord()
    {
        return $this->slackRecord;
    }

    public function getToken()
    {
        return $this->token;
    }

    /**
     * {@inheritdoc}
     *
     * @param  array  $record
     * @return string
     */
    protected function generateDataStream($record)
    {
        $content = $this->buildContent($record);

        return $this->buildHeader($content) . $content;
    }

    /**
     * Builds the body of API call
     *
     * @param  array  $record
     * @return string
     */
    private function buildContent($record)
    {
        $dataArray = $this->prepareContentData($record);

        return http_build_query($dataArray);
    }

    /**
     * Prepares content data
     *
     * @param  array $record
     * @return array
     */
    protected function prepareContentData($record)
    {
        $dataArray = $this->slackRecord->getSlackData($record);
        $dataArray['token'] = $this->token;

        if (!empty($dataArray['attachments'])) {
            $dataArray['attachments'] = Utils::jsonEncode($dataArray['attachments']);
        }

        return $dataArray;
    }

    /**
     * Builds the header of the API Call
     *
     * @param  string $content
     * @return string
     */
    private function buildHeader($content)
    {
        $header = "POST /api/chat.postMessage HTTP/1.1\r\n";
        $header .= "Host: slack.com\r\n";
        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $header .= "Content-Length: " . strlen($content) . "\r\n";
        $header .= "\r\n";

        return $header;
    }

    /**
     * {@inheritdoc}
     *
     * @param array $record
     */
    protected function write(array $record)
    {
        parent::write($record);
        $this->finalizeWrite();
    }

    /**
     * Finalizes the request by reading some bytes and then closing the socket
     *
     * If we do not read some but close the socket too early, slack sometimes
     * drops the request entirely.
     */
    protected function finalizeWrite()
    {
        $res = $this->getResource();
        if (is_resource($res)) {
            @fread($res, 2048);
        }
        $this->closeSocket();
    }

    /**
     * Returned a Slack message attachment color associated with
     * provided level.
     *
     * @param  int    $level
     * @return string
     * @deprecated Use underlying SlackRecord instead
     */
    protected function getAttachmentColor($level)
    {
        trigger_error(
            'SlackHandler::getAttachmentColor() is deprecated. Use underlying SlackRecord instead.',
            E_USER_DEPRECATED
        );

        return $this->slackRecord->getAttachmentColor($level);
    }

    /**
     * Stringifies an array of key/value pairs to be used in attachment fields
     *
     * @param  array  $fields
     * @return string
     * @deprecated Use underlying SlackRecord instead
     */
    protected function stringify($fields)
    {
        trigger_error(
            'SlackHandler::stringify() is deprecated. Use underlying SlackRecord instead.',
            E_USER_DEPRECATED
        );

        return $this->slackRecord->stringify($fields);
    }

    public function setFormatter(FormatterInterface $formatter)
    {
        parent::setFormatter($formatter);
        $this->slackRecord->setFormatter($formatter);

        return $this;
    }

    public function getFormatter()
    {
        $formatter = parent::getFormatter();
        $this->slackRecord->setFormatter($formatter);

        return $formatter;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * Sends notifications through the hipchat api to a hipchat room
 *
 * Notes:
 * API token - HipChat API token
 * Room      - HipChat Room Id or name, where messages are sent
 * Name      - Name used to send the message (from)
 * notify    - Should the message trigger a notification in the clients
 * version   - The API version to use (HipChatHandler::API_V1 | HipChatHandler::API_V2)
 *
 * @author Rafael Dohms <rafael@doh.ms>
 * @see    https://www.hipchat.com/docs/api
 */
class HipChatHandler extends SocketHandler
{
    /**
     * Use API version 1
     */
    const API_V1 = 'v1';

    /**
     * Use API version v2
     */
    const API_V2 = 'v2';

    /**
     * The maximum allowed length for the name used in the "from" field.
     */
    const MAXIMUM_NAME_LENGTH = 15;

    /**
     * The maximum allowed length for the message.
     */
    const MAXIMUM_MESSAGE_LENGTH = 9500;

    /**
     * @var string
     */
    private $token;

    /**
     * @var string
     */
    private $room;

    /**
     * @var string
     */
    private $name;

    /**
     * @var bool
     */
    private $notify;

    /**
     * @var string
     */
    private $format;

    /**
     * @var string
     */
    private $host;

    /**
     * @var string
     */
    private $version;

    /**
     * @param string $token   HipChat API Token
     * @param string $room    The room that should be alerted of the message (Id or Name)
     * @param string $name    Name used in the "from" field.
     * @param bool   $notify  Trigger a notification in clients or not
     * @param int    $level   The minimum logging level at which this handler will be triggered
     * @param bool   $bubble  Whether the messages that are handled can bubble up the stack or not
     * @param bool   $useSSL  Whether to connect via SSL.
     * @param string $format  The format of the messages (default to text, can be set to html if you have html in the messages)
     * @param string $host    The HipChat server hostname.
     * @param string $version The HipChat API version (default HipChatHandler::API_V1)
     */
    public function __construct($token, $room, $name = 'Monolog', $notify = false, $level = Logger::CRITICAL, $bubble = true, $useSSL = true, $format = 'text', $host = 'api.hipchat.com', $version = self::API_V1)
    {
        @trigger_error('The Monolog\Handler\HipChatHandler class is deprecated. You should migrate to Slack and the SlackWebhookHandler / SlackbotHandler, see https://www.atlassian.com/partnerships/slack', E_USER_DEPRECATED);

        if ($version == self::API_V1 && !$this->validateStringLength($name, static::MAXIMUM_NAME_LENGTH)) {
            throw new \InvalidArgumentException('The supplied name is too long. HipChat\'s v1 API supports names up to 15 UTF-8 characters.');
        }

        $connectionString = $useSSL ? 'ssl://'.$host.':443' : $host.':80';
        parent::__construct($connectionString, $level, $bubble);

        $this->token = $token;
        $this->name = $name;
        $this->notify = $notify;
        $this->room = $room;
        $this->format = $format;
        $this->host = $host;
        $this->version = $version;
    }

    /**
     * {@inheritdoc}
     *
     * @param  array  $record
     * @return string
     */
    protected function generateDataStream($record)
    {
        $content = $this->buildContent($record);

        return $this->buildHeader($content) . $content;
    }

    /**
     * Builds the body of API call
     *
     * @param  array  $record
     * @return string
     */
    private function buildContent($record)
    {
        $dataArray = array(
            'notify' => $this->version == self::API_V1 ?
                ($this->notify ? 1 : 0) :
                ($this->notify ? 'true' : 'false'),
            'message' => $record['formatted'],
            'message_format' => $this->format,
            'color' => $this->getAlertColor($record['level']),
        );

        if (!$this->validateStringLength($dataArray['message'], static::MAXIMUM_MESSAGE_LENGTH)) {
            if (function_exists('mb_substr')) {
                $dataArray['message'] = mb_substr($dataArray['message'], 0, static::MAXIMUM_MESSAGE_LENGTH).' [truncated]';
            } else {
                $dataArray['message'] = substr($dataArray['message'], 0, static::MAXIMUM_MESSAGE_LENGTH).' [truncated]';
            }
        }

        // if we are using the legacy API then we need to send some additional information
        if ($this->version == self::API_V1) {
            $dataArray['room_id'] = $this->room;
        }

        // append the sender name if it is set
        // always append it if we use the v1 api (it is required in v1)
        if ($this->version == self::API_V1 || $this->name !== null) {
            $dataArray['from'] = (string) $this->name;
        }

        return http_build_query($dataArray);
    }

    /**
     * Builds the header of the API Call
     *
     * @param  string $content
     * @return string
     */
    private function buildHeader($content)
    {
        if ($this->version == self::API_V1) {
            $header = "POST /v1/rooms/message?format=json&auth_token={$this->token} HTTP/1.1\r\n";
        } else {
            // needed for rooms with special (spaces, etc) characters in the name
            $room = rawurlencode($this->room);
            $header = "POST /v2/room/{$room}/notification?auth_token={$this->token} HTTP/1.1\r\n";
        }

        $header .= "Host: {$this->host}\r\n";
        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $header .= "Content-Length: " . strlen($content) . "\r\n";
        $header .= "\r\n";

        return $header;
    }

    /**
     * Assigns a color to each level of log records.
     *
     * @param  int    $level
     * @return string
     */
    protected function getAlertColor($level)
    {
        switch (true) {
            case $level >= Logger::ERROR:
                return 'red';
            case $level >= Logger::WARNING:
                return 'yellow';
            case $level >= Logger::INFO:
                return 'green';
            case $level == Logger::DEBUG:
                return 'gray';
            default:
                return 'yellow';
        }
    }

    /**
     * {@inheritdoc}
     *
     * @param array $record
     */
    protected function write(array $record)
    {
        parent::write($record);
        $this->finalizeWrite();
    }

    /**
     * Finalizes the request by reading some bytes and then closing the socket
     *
     * If we do not read some but close the socket too early, hipchat sometimes
     * drops the request entirely.
     */
    protected function finalizeWrite()
    {
        $res = $this->getResource();
        if (is_resource($res)) {
            @fread($res, 2048);
        }
        $this->closeSocket();
    }

    /**
     * {@inheritdoc}
     */
    public function handleBatch(array $records)
    {
        if (count($records) == 0) {
            return true;
        }

        $batchRecords = $this->combineRecords($records);

        $handled = false;
        foreach ($batchRecords as $batchRecord) {
            if ($this->isHandling($batchRecord)) {
                $this->write($batchRecord);
                $handled = true;
            }
        }

        if (!$handled) {
            return false;
        }

        return false === $this->bubble;
    }

    /**
     * Combines multiple records into one. Error level of the combined record
     * will be the highest level from the given records. Datetime will be taken
     * from the first record.
     *
     * @param $records
     * @return array
     */
    private function combineRecords($records)
    {
        $batchRecord = null;
        $batchRecords = array();
        $messages = array();
        $formattedMessages = array();
        $level = 0;
        $levelName = null;
        $datetime = null;

        foreach ($records as $record) {
            $record = $this->processRecord($record);

            if ($record['level'] > $level) {
                $level = $record['level'];
                $levelName = $record['level_name'];
            }

            if (null === $datetime) {
                $datetime = $record['datetime'];
            }

            $messages[] = $record['message'];
            $messageStr = implode(PHP_EOL, $messages);
            $formattedMessages[] = $this->getFormatter()->format($record);
            $formattedMessageStr = implode('', $formattedMessages);

            $batchRecord = array(
                'message'   => $messageStr,
                'formatted' => $formattedMessageStr,
                'context'   => array(),
                'extra'     => array(),
            );

            if (!$this->validateStringLength($batchRecord['formatted'], static::MAXIMUM_MESSAGE_LENGTH)) {
                // Pop the last message and implode the remaining messages
                $lastMessage = array_pop($messages);
                $lastFormattedMessage = array_pop($formattedMessages);
                $batchRecord['message'] = implode(PHP_EOL, $messages);
                $batchRecord['formatted'] = implode('', $formattedMessages);

                $batchRecords[] = $batchRecord;
                $messages = array($lastMessage);
                $formattedMessages = array($lastFormattedMessage);

                $batchRecord = null;
            }
        }

        if (null !== $batchRecord) {
            $batchRecords[] = $batchRecord;
        }

        // Set the max level and datetime for all records
        foreach ($batchRecords as &$batchRecord) {
            $batchRecord = array_merge(
                $batchRecord,
                array(
                    'level'      => $level,
                    'level_name' => $levelName,
                    'datetime'   => $datetime,
                )
            );
        }

        return $batchRecords;
    }

    /**
     * Validates the length of a string.
     *
     * If the `mb_strlen()` function is available, it will use that, as HipChat
     * allows UTF-8 characters. Otherwise, it will fall back to `strlen()`.
     *
     * Note that this might cause false failures in the specific case of using
     * a valid name with less than 16 characters, but 16 or more bytes, on a
     * system where `mb_strlen()` is unavailable.
     *
     * @param string $str
     * @param int    $length
     *
     * @return bool
     */
    private function validateStringLength($str, $length)
    {
        if (function_exists('mb_strlen')) {
            return (mb_strlen($str) <= $length);
        }

        return (strlen($str) <= $length);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\LineFormatter;
use Monolog\Logger;

/**
 * Sends logs to Fleep.io using Webhook integrations
 *
 * You'll need a Fleep.io account to use this handler.
 *
 * @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation
 * @author Ando Roots <ando@sqroot.eu>
 */
class FleepHookHandler extends SocketHandler
{
    const FLEEP_HOST = 'fleep.io';

    const FLEEP_HOOK_URI = '/hook/';

    /**
     * @var string Webhook token (specifies the conversation where logs are sent)
     */
    protected $token;

    /**
     * Construct a new Fleep.io Handler.
     *
     * For instructions on how to create a new web hook in your conversations
     * see https://fleep.io/integrations/webhooks/
     *
     * @param  string                    $token  Webhook token
     * @param  bool|int                  $level  The minimum logging level at which this handler will be triggered
     * @param  bool                      $bubble Whether the messages that are handled can bubble up the stack or not
     * @throws MissingExtensionException
     */
    public function __construct($token, $level = Logger::DEBUG, $bubble = true)
    {
        if (!extension_loaded('openssl')) {
            throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FleepHookHandler');
        }

        $this->token = $token;

        $connectionString = 'ssl://' . self::FLEEP_HOST . ':443';
        parent::__construct($connectionString, $level, $bubble);
    }

    /**
     * Returns the default formatter to use with this handler
     *
     * Overloaded to remove empty context and extra arrays from the end of the log message.
     *
     * @return LineFormatter
     */
    protected function getDefaultFormatter()
    {
        return new LineFormatter(null, null, true, true);
    }

    /**
     * Handles a log record
     *
     * @param array $record
     */
    public function write(array $record)
    {
        parent::write($record);
        $this->closeSocket();
    }

    /**
     * {@inheritdoc}
     *
     * @param  array  $record
     * @return string
     */
    protected function generateDataStream($record)
    {
        $content = $this->buildContent($record);

        return $this->buildHeader($content) . $content;
    }

    /**
     * Builds the header of the API Call
     *
     * @param  string $content
     * @return string
     */
    private function buildHeader($content)
    {
        $header = "POST " . self::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n";
        $header .= "Host: " . self::FLEEP_HOST . "\r\n";
        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
        $header .= "Content-Length: " . strlen($content) . "\r\n";
        $header .= "\r\n";

        return $header;
    }

    /**
     * Builds the body of API call
     *
     * @param  array  $record
     * @return string
     */
    private function buildContent($record)
    {
        $dataArray = array(
            'message' => $record['formatted'],
        );

        return http_build_query($dataArray);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * Blackhole
 *
 * Any record it can handle will be thrown away. This can be used
 * to put on top of an existing stack to override it temporarily.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class NullHandler extends AbstractHandler
{
    /**
     * @param int $level The minimum logging level at which this handler will be triggered
     */
    public function __construct($level = Logger::DEBUG)
    {
        parent::__construct($level, false);
    }

    /**
     * {@inheritdoc}
     */
    public function handle(array $record)
    {
        if ($record['level'] < $this->level) {
            return false;
        }

        return true;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Formatter\JsonFormatter;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Channel\AMQPChannel;
use AMQPExchange;

class AmqpHandler extends AbstractProcessingHandler
{
    /**
     * @var AMQPExchange|AMQPChannel $exchange
     */
    protected $exchange;

    /**
     * @var string
     */
    protected $exchangeName;

    /**
     * @param AMQPExchange|AMQPChannel $exchange     AMQPExchange (php AMQP ext) or PHP AMQP lib channel, ready for use
     * @param string                   $exchangeName
     * @param int                      $level
     * @param bool                     $bubble       Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($exchange, $exchangeName = 'log', $level = Logger::DEBUG, $bubble = true)
    {
        if ($exchange instanceof AMQPExchange) {
            $exchange->setName($exchangeName);
        } elseif ($exchange instanceof AMQPChannel) {
            $this->exchangeName = $exchangeName;
        } else {
            throw new \InvalidArgumentException('PhpAmqpLib\Channel\AMQPChannel or AMQPExchange instance required');
        }
        $this->exchange = $exchange;

        parent::__construct($level, $bubble);
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record)
    {
        $data = $record["formatted"];
        $routingKey = $this->getRoutingKey($record);

        if ($this->exchange instanceof AMQPExchange) {
            $this->exchange->publish(
                $data,
                $routingKey,
                0,
                array(
                    'delivery_mode' => 2,
                    'content_type' => 'application/json',
                )
            );
        } else {
            $this->exchange->basic_publish(
                $this->createAmqpMessage($data),
                $this->exchangeName,
                $routingKey
            );
        }
    }

    /**
     * {@inheritDoc}
     */
    public function handleBatch(array $records)
    {
        if ($this->exchange instanceof AMQPExchange) {
            parent::handleBatch($records);

            return;
        }

        foreach ($records as $record) {
            if (!$this->isHandling($record)) {
                continue;
            }

            $record = $this->processRecord($record);
            $data = $this->getFormatter()->format($record);

            $this->exchange->batch_basic_publish(
                $this->createAmqpMessage($data),
                $this->exchangeName,
                $this->getRoutingKey($record)
            );
        }

        $this->exchange->publish_batch();
    }

    /**
     * Gets the routing key for the AMQP exchange
     *
     * @param  array  $record
     * @return string
     */
    protected function getRoutingKey(array $record)
    {
        $routingKey = sprintf(
            '%s.%s',
            // TODO 2.0 remove substr call
            substr($record['level_name'], 0, 4),
            $record['channel']
        );

        return strtolower($routingKey);
    }

    /**
     * @param  string      $data
     * @return AMQPMessage
     */
    private function createAmqpMessage($data)
    {
        return new AMQPMessage(
            (string) $data,
            array(
                'delivery_mode' => 2,
                'content_type' => 'application/json',
            )
        );
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;

/**
 * MandrillHandler uses cURL to send the emails to the Mandrill API
 *
 * @author Adam Nicholson <adamnicholson10@gmail.com>
 */
class MandrillHandler extends MailHandler
{
    protected $message;
    protected $apiKey;

    /**
     * @param string                  $apiKey  A valid Mandrill API key
     * @param callable|\Swift_Message $message An example message for real messages, only the body will be replaced
     * @param int                     $level   The minimum logging level at which this handler will be triggered
     * @param bool                    $bubble  Whether the messages that are handled can bubble up the stack or not
     */
    public function __construct($apiKey, $message, $level = Logger::ERROR, $bubble = true)
    {
        parent::__construct($level, $bubble);

        if (!$message instanceof \Swift_Message && is_callable($message)) {
            $message = call_user_func($message);
        }
        if (!$message instanceof \Swift_Message) {
            throw new \InvalidArgumentException('You must provide either a Swift_Message instance or a callable returning it');
        }
        $this->message = $message;
        $this->apiKey = $apiKey;
    }

    /**
     * {@inheritdoc}
     */
    protected function send($content, array $records)
    {
        $message = clone $this->message;
        $message->setBody($content);
        $message->setDate(time());

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, 'https://mandrillapp.com/api/1.0/messages/send-raw.json');
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
            'key' => $this->apiKey,
            'raw_message' => (string) $message,
            'async' => false,
        )));

        Curl\Util::execute($ch);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\ResettableInterface;

/**
 * Base Handler class providing the Handler structure
 *
 * Classes extending it should (in most cases) only implement write($record)
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @author Christophe Coevoet <stof@notk.org>
 */
abstract class AbstractProcessingHandler extends AbstractHandler
{
    /**
     * {@inheritdoc}
     */
    public function handle(array $record)
    {
        if (!$this->isHandling($record)) {
            return false;
        }

        $record = $this->processRecord($record);

        $record['formatted'] = $this->getFormatter()->format($record);

        $this->write($record);

        return false === $this->bubble;
    }

    /**
     * Writes the record down to the log of the implementing handler
     *
     * @param  array $record
     * @return void
     */
    abstract protected function write(array $record);

    /**
     * Processes a record.
     *
     * @param  array $record
     * @return array
     */
    protected function processRecord(array $record)
    {
        if ($this->processors) {
            foreach ($this->processors as $processor) {
                $record = call_user_func($processor, $record);
            }
        }

        return $record;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\JsonFormatter;
use Monolog\Logger;

/**
 * CouchDB handler
 *
 * @author Markus Bachmann <markus.bachmann@bachi.biz>
 */
class CouchDBHandler extends AbstractProcessingHandler
{
    private $options;

    public function __construct(array $options = array(), $level = Logger::DEBUG, $bubble = true)
    {
        $this->options = array_merge(array(
            'host'     => 'localhost',
            'port'     => 5984,
            'dbname'   => 'logger',
            'username' => null,
            'password' => null,
        ), $options);

        parent::__construct($level, $bubble);
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record)
    {
        $basicAuth = null;
        if ($this->options['username']) {
            $basicAuth = sprintf('%s:%s@', $this->options['username'], $this->options['password']);
        }

        $url = 'http://'.$basicAuth.$this->options['host'].':'.$this->options['port'].'/'.$this->options['dbname'];
        $context = stream_context_create(array(
            'http' => array(
                'method'        => 'POST',
                'content'       => $record['formatted'],
                'ignore_errors' => true,
                'max_redirects' => 0,
                'header'        => 'Content-type: application/json',
            ),
        ));

        if (false === @file_get_contents($url, null, $context)) {
            throw new \RuntimeException(sprintf('Could not connect to %s', $url));
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Formatter\LineFormatter;
use Monolog\Logger;

/**
 * Logs to a Redis key using rpush
 *
 * usage example:
 *
 *   $log = new Logger('application');
 *   $redis = new RedisHandler(new Predis\Client("tcp://localhost:6379"), "logs", "prod");
 *   $log->pushHandler($redis);
 *
 * @author Thomas Tourlourat <thomas@tourlourat.com>
 */
class RedisHandler extends AbstractProcessingHandler
{
    private $redisClient;
    private $redisKey;
    protected $capSize;

    /**
     * @param \Predis\Client|\Redis $redis   The redis instance
     * @param string                $key     The key name to push records to
     * @param int                   $level   The minimum logging level at which this handler will be triggered
     * @param bool                  $bubble  Whether the messages that are handled can bubble up the stack or not
     * @param int                   $capSize Number of entries to limit list size to
     */
    public function __construct($redis, $key, $level = Logger::DEBUG, $bubble = true, $capSize = false)
    {
        if (!(($redis instanceof \Predis\Client) || ($redis instanceof \Redis))) {
            throw new \InvalidArgumentException('Predis\Client or Redis instance required');
        }

        $this->redisClient = $redis;
        $this->redisKey = $key;
        $this->capSize = $capSize;

        parent::__construct($level, $bubble);
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record)
    {
        if ($this->capSize) {
            $this->writeCapped($record);
        } else {
            $this->redisClient->rpush($this->redisKey, $record["formatted"]);
        }
    }

    /**
     * Write and cap the collection
     * Writes the record to the redis list and caps its
     *
     * @param  array $record associative record array
     * @return void
     */
    protected function writeCapped(array $record)
    {
        if ($this->redisClient instanceof \Redis) {
            $mode = defined('\Redis::MULTI') ? \Redis::MULTI : 1;
            $this->redisClient->multi($mode)
                ->rpush($this->redisKey, $record["formatted"])
                ->ltrim($this->redisKey, -$this->capSize, -1)
                ->exec();
        } else {
            $redisKey = $this->redisKey;
            $capSize = $this->capSize;
            $this->redisClient->transaction(function ($tx) use ($record, $redisKey, $capSize) {
                $tx->rpush($redisKey, $record["formatted"]);
                $tx->ltrim($redisKey, -$capSize, -1);
            });
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter()
    {
        return new LineFormatter();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Utils;

/**
 * Logs to Cube.
 *
 * @link http://square.github.com/cube/
 * @author Wan Chen <kami@kamisama.me>
 */
class CubeHandler extends AbstractProcessingHandler
{
    private $udpConnection;
    private $httpConnection;
    private $scheme;
    private $host;
    private $port;
    private $acceptedSchemes = array('http', 'udp');

    /**
     * Create a Cube handler
     *
     * @throws \UnexpectedValueException when given url is not a valid url.
     *                                   A valid url must consist of three parts : protocol://host:port
     *                                   Only valid protocols used by Cube are http and udp
     */
    public function __construct($url, $level = Logger::DEBUG, $bubble = true)
    {
        $urlInfo = parse_url($url);

        if (!isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) {
            throw new \UnexpectedValueException('URL "'.$url.'" is not valid');
        }

        if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) {
            throw new \UnexpectedValueException(
                'Invalid protocol (' . $urlInfo['scheme']  . ').'
                . ' Valid options are ' . implode(', ', $this->acceptedSchemes));
        }

        $this->scheme = $urlInfo['scheme'];
        $this->host = $urlInfo['host'];
        $this->port = $urlInfo['port'];

        parent::__construct($level, $bubble);
    }

    /**
     * Establish a connection to an UDP socket
     *
     * @throws \LogicException           when unable to connect to the socket
     * @throws MissingExtensionException when there is no socket extension
     */
    protected function connectUdp()
    {
        if (!extension_loaded('sockets')) {
            throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler');
        }

        $this->udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0);
        if (!$this->udpConnection) {
            throw new \LogicException('Unable to create a socket');
        }

        if (!socket_connect($this->udpConnection, $this->host, $this->port)) {
            throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port);
        }
    }

    /**
     * Establish a connection to a http server
     * @throws \LogicException when no curl extension
     */
    protected function connectHttp()
    {
        if (!extension_loaded('curl')) {
            throw new \LogicException('The curl extension is needed to use http URLs with the CubeHandler');
        }

        $this->httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put');

        if (!$this->httpConnection) {
            throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port);
        }

        curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST");
        curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true);
    }

    /**
     * {@inheritdoc}
     */
    protected function write(array $record)
    {
        $date = $record['datetime'];

        $data = array('time' => $date->format('Y-m-d\TH:i:s.uO'));
        unset($record['datetime']);

        if (isset($record['context']['type'])) {
            $data['type'] = $record['context']['type'];
            unset($record['context']['type']);
        } else {
            $data['type'] = $record['channel'];
        }

        $data['data'] = $record['context'];
        $data['data']['level'] = $record['level'];

        if ($this->scheme === 'http') {
            $this->writeHttp(Utils::jsonEncode($data));
        } else {
            $this->writeUdp(Utils::jsonEncode($data));
        }
    }

    private function writeUdp($data)
    {
        if (!$this->udpConnection) {
            $this->connectUdp();
        }

        socket_send($this->udpConnection, $data, strlen($data), 0);
    }

    private function writeHttp($data)
    {
        if (!$this->httpConnection) {
            $this->connectHttp();
        }

        curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']');
        curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, array(
            'Content-Type: application/json',
            'Content-Length: ' . strlen('['.$data.']'),
        ));

        Curl\Util::execute($this->httpConnection, 5, false);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler\SyslogUdp;

class UdpSocket
{
    const DATAGRAM_MAX_LENGTH = 65023;

    protected $ip;
    protected $port;
    protected $socket;

    public function __construct($ip, $port = 514)
    {
        $this->ip = $ip;
        $this->port = $port;
        $this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
    }

    public function write($line, $header = "")
    {
        $this->send($this->assembleMessage($line, $header));
    }

    public function close()
    {
        if (is_resource($this->socket)) {
            socket_close($this->socket);
            $this->socket = null;
        }
    }

    protected function send($chunk)
    {
        if (!is_resource($this->socket)) {
            throw new \LogicException('The UdpSocket to '.$this->ip.':'.$this->port.' has been closed and can not be written to anymore');
        }
        socket_sendto($this->socket, $chunk, strlen($chunk), $flags = 0, $this->ip, $this->port);
    }

    protected function assembleMessage($line, $header)
    {
        $chunkSize = self::DATAGRAM_MAX_LENGTH - strlen($header);

        return $header . substr($line, 0, $chunkSize);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog;

use Monolog\Handler\HandlerInterface;
use Monolog\Handler\StreamHandler;
use Psr\Log\LoggerInterface;
use Psr\Log\InvalidArgumentException;
use Exception;

/**
 * Monolog log channel
 *
 * It contains a stack of Handlers and a stack of Processors,
 * and uses them to store records that are added to it.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class Logger implements LoggerInterface, ResettableInterface
{
    /**
     * Detailed debug information
     */
    const DEBUG = 100;

    /**
     * Interesting events
     *
     * Examples: User logs in, SQL logs.
     */
    const INFO = 200;

    /**
     * Uncommon events
     */
    const NOTICE = 250;

    /**
     * Exceptional occurrences that are not errors
     *
     * Examples: Use of deprecated APIs, poor use of an API,
     * undesirable things that are not necessarily wrong.
     */
    const WARNING = 300;

    /**
     * Runtime errors
     */
    const ERROR = 400;

    /**
     * Critical conditions
     *
     * Example: Application component unavailable, unexpected exception.
     */
    const CRITICAL = 500;

    /**
     * Action must be taken immediately
     *
     * Example: Entire website down, database unavailable, etc.
     * This should trigger the SMS alerts and wake you up.
     */
    const ALERT = 550;

    /**
     * Urgent alert.
     */
    const EMERGENCY = 600;

    /**
     * Monolog API version
     *
     * This is only bumped when API breaks are done and should
     * follow the major version of the library
     *
     * @var int
     */
    const API = 1;

    /**
     * Logging levels from syslog protocol defined in RFC 5424
     *
     * @var array $levels Logging levels
     */
    protected static $levels = array(
        self::DEBUG     => 'DEBUG',
        self::INFO      => 'INFO',
        self::NOTICE    => 'NOTICE',
        self::WARNING   => 'WARNING',
        self::ERROR     => 'ERROR',
        self::CRITICAL  => 'CRITICAL',
        self::ALERT     => 'ALERT',
        self::EMERGENCY => 'EMERGENCY',
    );

    /**
     * @var \DateTimeZone
     */
    protected static $timezone;

    /**
     * @var string
     */
    protected $name;

    /**
     * The handler stack
     *
     * @var HandlerInterface[]
     */
    protected $handlers;

    /**
     * Processors that will process all log records
     *
     * To process records of a single handler instead, add the processor on that specific handler
     *
     * @var callable[]
     */
    protected $processors;

    /**
     * @var bool
     */
    protected $microsecondTimestamps = true;

    /**
     * @var callable
     */
    protected $exceptionHandler;

    /**
     * @param string             $name       The logging channel
     * @param HandlerInterface[] $handlers   Optional stack of handlers, the first one in the array is called first, etc.
     * @param callable[]         $processors Optional array of processors
     */
    public function __construct($name, array $handlers = array(), array $processors = array())
    {
        $this->name = $name;
        $this->setHandlers($handlers);
        $this->processors = $processors;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Return a new cloned instance with the name changed
     *
     * @return static
     */
    public function withName($name)
    {
        $new = clone $this;
        $new->name = $name;

        return $new;
    }

    /**
     * Pushes a handler on to the stack.
     *
     * @param  HandlerInterface $handler
     * @return $this
     */
    public function pushHandler(HandlerInterface $handler)
    {
        array_unshift($this->handlers, $handler);

        return $this;
    }

    /**
     * Pops a handler from the stack
     *
     * @return HandlerInterface
     */
    public function popHandler()
    {
        if (!$this->handlers) {
            throw new \LogicException('You tried to pop from an empty handler stack.');
        }

        return array_shift($this->handlers);
    }

    /**
     * Set handlers, replacing all existing ones.
     *
     * If a map is passed, keys will be ignored.
     *
     * @param  HandlerInterface[] $handlers
     * @return $this
     */
    public function setHandlers(array $handlers)
    {
        $this->handlers = array();
        foreach (array_reverse($handlers) as $handler) {
            $this->pushHandler($handler);
        }

        return $this;
    }

    /**
     * @return HandlerInterface[]
     */
    public function getHandlers()
    {
        return $this->handlers;
    }

    /**
     * Adds a processor on to the stack.
     *
     * @param  callable $callback
     * @return $this
     */
    public function pushProcessor($callback)
    {
        if (!is_callable($callback)) {
            throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given');
        }
        array_unshift($this->processors, $callback);

        return $this;
    }

    /**
     * Removes the processor on top of the stack and returns it.
     *
     * @return callable
     */
    public function popProcessor()
    {
        if (!$this->processors) {
            throw new \LogicException('You tried to pop from an empty processor stack.');
        }

        return array_shift($this->processors);
    }

    /**
     * @return callable[]
     */
    public function getProcessors()
    {
        return $this->processors;
    }

    /**
     * Control the use of microsecond resolution timestamps in the 'datetime'
     * member of new records.
     *
     * Generating microsecond resolution timestamps by calling
     * microtime(true), formatting the result via sprintf() and then parsing
     * the resulting string via \DateTime::createFromFormat() can incur
     * a measurable runtime overhead vs simple usage of DateTime to capture
     * a second resolution timestamp in systems which generate a large number
     * of log events.
     *
     * @param bool $micro True to use microtime() to create timestamps
     */
    public function useMicrosecondTimestamps($micro)
    {
        $this->microsecondTimestamps = (bool) $micro;
    }

    /**
     * Adds a log record.
     *
     * @param  int     $level   The logging level
     * @param  string  $message The log message
     * @param  array   $context The log context
     * @return bool Whether the record has been processed
     */
    public function addRecord($level, $message, array $context = array())
    {
        if (!$this->handlers) {
            $this->pushHandler(new StreamHandler('php://stderr', static::DEBUG));
        }

        $levelName = static::getLevelName($level);

        // check if any handler will handle this message so we can return early and save cycles
        $handlerKey = null;
        reset($this->handlers);
        while ($handler = current($this->handlers)) {
            if ($handler->isHandling(array('level' => $level))) {
                $handlerKey = key($this->handlers);
                break;
            }

            next($this->handlers);
        }

        if (null === $handlerKey) {
            return false;
        }

        if (!static::$timezone) {
            static::$timezone = new \DateTimeZone(date_default_timezone_get() ?: 'UTC');
        }

        // php7.1+ always has microseconds enabled, so we do not need this hack
        if ($this->microsecondTimestamps && PHP_VERSION_ID < 70100) {
            $ts = \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true)), static::$timezone);
        } else {
            $ts = new \DateTime(null, static::$timezone);
        }
        $ts->setTimezone(static::$timezone);

        $record = array(
            'message' => (string) $message,
            'context' => $context,
            'level' => $level,
            'level_name' => $levelName,
            'channel' => $this->name,
            'datetime' => $ts,
            'extra' => array(),
        );

        try {
            foreach ($this->processors as $processor) {
                $record = call_user_func($processor, $record);
            }

            while ($handler = current($this->handlers)) {
                if (true === $handler->handle($record)) {
                    break;
                }

                next($this->handlers);
            }
        } catch (Exception $e) {
            $this->handleException($e, $record);
        }

        return true;
    }

    /**
     * Ends a log cycle and frees all resources used by handlers.
     *
     * Closing a Handler means flushing all buffers and freeing any open resources/handles.
     * Handlers that have been closed should be able to accept log records again and re-open
     * themselves on demand, but this may not always be possible depending on implementation.
     *
     * This is useful at the end of a request and will be called automatically on every handler
     * when they get destructed.
     */
    public function close()
    {
        foreach ($this->handlers as $handler) {
            if (method_exists($handler, 'close')) {
                $handler->close();
            }
        }
    }

    /**
     * Ends a log cycle and resets all handlers and processors to their initial state.
     *
     * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal
     * state, and getting it back to a state in which it can receive log records again.
     *
     * This is useful in case you want to avoid logs leaking between two requests or jobs when you
     * have a long running process like a worker or an application server serving multiple requests
     * in one process.
     */
    public function reset()
    {
        foreach ($this->handlers as $handler) {
            if ($handler instanceof ResettableInterface) {
                $handler->reset();
            }
        }

        foreach ($this->processors as $processor) {
            if ($processor instanceof ResettableInterface) {
                $processor->reset();
            }
        }
    }

    /**
     * Adds a log record at the DEBUG level.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function addDebug($message, array $context = array())
    {
        return $this->addRecord(static::DEBUG, $message, $context);
    }

    /**
     * Adds a log record at the INFO level.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function addInfo($message, array $context = array())
    {
        return $this->addRecord(static::INFO, $message, $context);
    }

    /**
     * Adds a log record at the NOTICE level.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function addNotice($message, array $context = array())
    {
        return $this->addRecord(static::NOTICE, $message, $context);
    }

    /**
     * Adds a log record at the WARNING level.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function addWarning($message, array $context = array())
    {
        return $this->addRecord(static::WARNING, $message, $context);
    }

    /**
     * Adds a log record at the ERROR level.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function addError($message, array $context = array())
    {
        return $this->addRecord(static::ERROR, $message, $context);
    }

    /**
     * Adds a log record at the CRITICAL level.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function addCritical($message, array $context = array())
    {
        return $this->addRecord(static::CRITICAL, $message, $context);
    }

    /**
     * Adds a log record at the ALERT level.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function addAlert($message, array $context = array())
    {
        return $this->addRecord(static::ALERT, $message, $context);
    }

    /**
     * Adds a log record at the EMERGENCY level.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function addEmergency($message, array $context = array())
    {
        return $this->addRecord(static::EMERGENCY, $message, $context);
    }

    /**
     * Gets all supported logging levels.
     *
     * @return array Assoc array with human-readable level names => level codes.
     */
    public static function getLevels()
    {
        return array_flip(static::$levels);
    }

    /**
     * Gets the name of the logging level.
     *
     * @param  int    $level
     * @return string
     */
    public static function getLevelName($level)
    {
        if (!isset(static::$levels[$level])) {
            throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels)));
        }

        return static::$levels[$level];
    }

    /**
     * Converts PSR-3 levels to Monolog ones if necessary
     *
     * @param string|int Level number (monolog) or name (PSR-3)
     * @return int
     */
    public static function toMonologLevel($level)
    {
        if (is_string($level)) {
            // Contains chars of all log levels and avoids using strtoupper() which may have
            // strange results depending on locale (for example, "i" will become "İ")
            $upper = strtr($level, 'abcdefgilmnortuwy', 'ABCDEFGILMNORTUWY');
            if (defined(__CLASS__.'::'.$upper)) {
                return constant(__CLASS__ . '::' . $upper);
            }
        }

        return $level;
    }

    /**
     * Checks whether the Logger has a handler that listens on the given level
     *
     * @param  int     $level
     * @return bool
     */
    public function isHandling($level)
    {
        $record = array(
            'level' => $level,
        );

        foreach ($this->handlers as $handler) {
            if ($handler->isHandling($record)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Set a custom exception handler
     *
     * @param  callable $callback
     * @return $this
     */
    public function setExceptionHandler($callback)
    {
        if (!is_callable($callback)) {
            throw new \InvalidArgumentException('Exception handler must be valid callable (callback or object with an __invoke method), '.var_export($callback, true).' given');
        }
        $this->exceptionHandler = $callback;

        return $this;
    }

    /**
     * @return callable
     */
    public function getExceptionHandler()
    {
        return $this->exceptionHandler;
    }

    /**
     * Delegates exception management to the custom exception handler,
     * or throws the exception if no custom handler is set.
     */
    protected function handleException(Exception $e, array $record)
    {
        if (!$this->exceptionHandler) {
            throw $e;
        }

        call_user_func($this->exceptionHandler, $e, $record);
    }

    /**
     * Adds a log record at an arbitrary level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  mixed   $level   The log level
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function log($level, $message, array $context = array())
    {
        $level = static::toMonologLevel($level);

        return $this->addRecord($level, $message, $context);
    }

    /**
     * Adds a log record at the DEBUG level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function debug($message, array $context = array())
    {
        return $this->addRecord(static::DEBUG, $message, $context);
    }

    /**
     * Adds a log record at the INFO level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function info($message, array $context = array())
    {
        return $this->addRecord(static::INFO, $message, $context);
    }

    /**
     * Adds a log record at the NOTICE level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function notice($message, array $context = array())
    {
        return $this->addRecord(static::NOTICE, $message, $context);
    }

    /**
     * Adds a log record at the WARNING level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function warn($message, array $context = array())
    {
        return $this->addRecord(static::WARNING, $message, $context);
    }

    /**
     * Adds a log record at the WARNING level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function warning($message, array $context = array())
    {
        return $this->addRecord(static::WARNING, $message, $context);
    }

    /**
     * Adds a log record at the ERROR level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function err($message, array $context = array())
    {
        return $this->addRecord(static::ERROR, $message, $context);
    }

    /**
     * Adds a log record at the ERROR level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function error($message, array $context = array())
    {
        return $this->addRecord(static::ERROR, $message, $context);
    }

    /**
     * Adds a log record at the CRITICAL level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function crit($message, array $context = array())
    {
        return $this->addRecord(static::CRITICAL, $message, $context);
    }

    /**
     * Adds a log record at the CRITICAL level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function critical($message, array $context = array())
    {
        return $this->addRecord(static::CRITICAL, $message, $context);
    }

    /**
     * Adds a log record at the ALERT level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function alert($message, array $context = array())
    {
        return $this->addRecord(static::ALERT, $message, $context);
    }

    /**
     * Adds a log record at the EMERGENCY level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function emerg($message, array $context = array())
    {
        return $this->addRecord(static::EMERGENCY, $message, $context);
    }

    /**
     * Adds a log record at the EMERGENCY level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param  string $message The log message
     * @param  array  $context The log context
     * @return bool   Whether the record has been processed
     */
    public function emergency($message, array $context = array())
    {
        return $this->addRecord(static::EMERGENCY, $message, $context);
    }

    /**
     * Set the timezone to be used for the timestamp of log records.
     *
     * This is stored globally for all Logger instances
     *
     * @param \DateTimeZone $tz Timezone object
     */
    public static function setTimezone(\DateTimeZone $tz)
    {
        self::$timezone = $tz;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog;

use InvalidArgumentException;

/**
 * Monolog log registry
 *
 * Allows to get `Logger` instances in the global scope
 * via static method calls on this class.
 *
 * <code>
 * $application = new Monolog\Logger('application');
 * $api = new Monolog\Logger('api');
 *
 * Monolog\Registry::addLogger($application);
 * Monolog\Registry::addLogger($api);
 *
 * function testLogger()
 * {
 *     Monolog\Registry::api()->addError('Sent to $api Logger instance');
 *     Monolog\Registry::application()->addError('Sent to $application Logger instance');
 * }
 * </code>
 *
 * @author Tomas Tatarko <tomas@tatarko.sk>
 */
class Registry
{
    /**
     * List of all loggers in the registry (by named indexes)
     *
     * @var Logger[]
     */
    private static $loggers = array();

    /**
     * Adds new logging channel to the registry
     *
     * @param  Logger                    $logger    Instance of the logging channel
     * @param  string|null               $name      Name of the logging channel ($logger->getName() by default)
     * @param  bool                      $overwrite Overwrite instance in the registry if the given name already exists?
     * @throws \InvalidArgumentException If $overwrite set to false and named Logger instance already exists
     */
    public static function addLogger(Logger $logger, $name = null, $overwrite = false)
    {
        $name = $name ?: $logger->getName();

        if (isset(self::$loggers[$name]) && !$overwrite) {
            throw new InvalidArgumentException('Logger with the given name already exists');
        }

        self::$loggers[$name] = $logger;
    }

    /**
     * Checks if such logging channel exists by name or instance
     *
     * @param string|Logger $logger Name or logger instance
     */
    public static function hasLogger($logger)
    {
        if ($logger instanceof Logger) {
            $index = array_search($logger, self::$loggers, true);

            return false !== $index;
        } else {
            return isset(self::$loggers[$logger]);
        }
    }

    /**
     * Removes instance from registry by name or instance
     *
     * @param string|Logger $logger Name or logger instance
     */
    public static function removeLogger($logger)
    {
        if ($logger instanceof Logger) {
            if (false !== ($idx = array_search($logger, self::$loggers, true))) {
                unset(self::$loggers[$idx]);
            }
        } else {
            unset(self::$loggers[$logger]);
        }
    }

    /**
     * Clears the registry
     */
    public static function clear()
    {
        self::$loggers = array();
    }

    /**
     * Gets Logger instance from the registry
     *
     * @param  string                    $name Name of the requested Logger instance
     * @throws \InvalidArgumentException If named Logger instance is not in the registry
     * @return Logger                    Requested instance of Logger
     */
    public static function getInstance($name)
    {
        if (!isset(self::$loggers[$name])) {
            throw new InvalidArgumentException(sprintf('Requested "%s" logger instance is not in the registry', $name));
        }

        return self::$loggers[$name];
    }

    /**
     * Gets Logger instance from the registry via static method call
     *
     * @param  string                    $name      Name of the requested Logger instance
     * @param  array                     $arguments Arguments passed to static method call
     * @throws \InvalidArgumentException If named Logger instance is not in the registry
     * @return Logger                    Requested instance of Logger
     */
    public static function __callStatic($name, $arguments)
    {
        return self::getInstance($name);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

use Monolog\Utils;

/**
 * Processes a record's message according to PSR-3 rules
 *
 * It replaces {foo} with the value from $context['foo']
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class PsrLogMessageProcessor implements ProcessorInterface
{
    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        if (false === strpos($record['message'], '{')) {
            return $record;
        }

        $replacements = array();
        foreach ($record['context'] as $key => $val) {
            if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) {
                $replacements['{'.$key.'}'] = $val;
            } elseif (is_object($val)) {
                $replacements['{'.$key.'}'] = '[object '.Utils::getClass($val).']';
            } else {
                $replacements['{'.$key.'}'] = '['.gettype($val).']';
            }
        }

        $record['message'] = strtr($record['message'], $replacements);

        return $record;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

/**
 * Injects url/method and remote IP of the current web request in all records
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class WebProcessor implements ProcessorInterface
{
    /**
     * @var array|\ArrayAccess
     */
    protected $serverData;

    /**
     * Default fields
     *
     * Array is structured as [key in record.extra => key in $serverData]
     *
     * @var array
     */
    protected $extraFields = array(
        'url'         => 'REQUEST_URI',
        'ip'          => 'REMOTE_ADDR',
        'http_method' => 'REQUEST_METHOD',
        'server'      => 'SERVER_NAME',
        'referrer'    => 'HTTP_REFERER',
    );

    /**
     * @param array|\ArrayAccess $serverData  Array or object w/ ArrayAccess that provides access to the $_SERVER data
     * @param array|null         $extraFields Field names and the related key inside $serverData to be added. If not provided it defaults to: url, ip, http_method, server, referrer
     */
    public function __construct($serverData = null, array $extraFields = null)
    {
        if (null === $serverData) {
            $this->serverData = &$_SERVER;
        } elseif (is_array($serverData) || $serverData instanceof \ArrayAccess) {
            $this->serverData = $serverData;
        } else {
            throw new \UnexpectedValueException('$serverData must be an array or object implementing ArrayAccess.');
        }

        if (null !== $extraFields) {
            if (isset($extraFields[0])) {
                foreach (array_keys($this->extraFields) as $fieldName) {
                    if (!in_array($fieldName, $extraFields)) {
                        unset($this->extraFields[$fieldName]);
                    }
                }
            } else {
                $this->extraFields = $extraFields;
            }
        }
    }

    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        // skip processing if for some reason request data
        // is not present (CLI or wonky SAPIs)
        if (!isset($this->serverData['REQUEST_URI'])) {
            return $record;
        }

        $record['extra'] = $this->appendExtraFields($record['extra']);

        return $record;
    }

    /**
     * @param  string $extraName
     * @param  string $serverName
     * @return $this
     */
    public function addExtraField($extraName, $serverName)
    {
        $this->extraFields[$extraName] = $serverName;

        return $this;
    }

    /**
     * @param  array $extra
     * @return array
     */
    private function appendExtraFields(array $extra)
    {
        foreach ($this->extraFields as $extraName => $serverName) {
            $extra[$extraName] = isset($this->serverData[$serverName]) ? $this->serverData[$serverName] : null;
        }

        if (isset($this->serverData['UNIQUE_ID'])) {
            $extra['unique_id'] = $this->serverData['UNIQUE_ID'];
        }

        return $extra;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

/**
 * Injects memory_get_peak_usage in all records
 *
 * @see Monolog\Processor\MemoryProcessor::__construct() for options
 * @author Rob Jensen
 */
class MemoryPeakUsageProcessor extends MemoryProcessor
{
    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        $bytes = memory_get_peak_usage($this->realUsage);
        $formatted = $this->formatBytes($bytes);

        $record['extra']['memory_peak_usage'] = $formatted;

        return $record;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

/**
 * Adds value of getmypid into records
 *
 * @author Andreas Hörnicke
 */
class ProcessIdProcessor implements ProcessorInterface
{
    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        $record['extra']['process_id'] = getmypid();

        return $record;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jonathan A. Schweder <jonathanschweder@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

use Monolog\Logger;

/**
 * Injects Hg branch and Hg revision number in all records
 *
 * @author Jonathan A. Schweder <jonathanschweder@gmail.com>
 */
class MercurialProcessor implements ProcessorInterface
{
    private $level;
    private static $cache;

    public function __construct($level = Logger::DEBUG)
    {
        $this->level = Logger::toMonologLevel($level);
    }

    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        // return if the level is not high enough
        if ($record['level'] < $this->level) {
            return $record;
        }

        $record['extra']['hg'] = self::getMercurialInfo();

        return $record;
    }

    private static function getMercurialInfo()
    {
        if (self::$cache) {
            return self::$cache;
        }

        $result = explode(' ', trim(`hg id -nb`));
        if (count($result) >= 3) {
            return self::$cache = array(
                'branch' => $result[1],
                'revision' => $result[2],
            );
        }

        return self::$cache = array();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

/**
 * Adds a tags array into record
 *
 * @author Martijn Riemers
 */
class TagProcessor implements ProcessorInterface
{
    private $tags;

    public function __construct(array $tags = array())
    {
        $this->setTags($tags);
    }

    public function addTags(array $tags = array())
    {
        $this->tags = array_merge($this->tags, $tags);
    }

    public function setTags(array $tags = array())
    {
        $this->tags = $tags;
    }

    public function __invoke(array $record)
    {
        $record['extra']['tags'] = $this->tags;

        return $record;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

use Monolog\Logger;

/**
 * Injects Git branch and Git commit SHA in all records
 *
 * @author Nick Otter
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class GitProcessor implements ProcessorInterface
{
    private $level;
    private static $cache;

    public function __construct($level = Logger::DEBUG)
    {
        $this->level = Logger::toMonologLevel($level);
    }

    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        // return if the level is not high enough
        if ($record['level'] < $this->level) {
            return $record;
        }

        $record['extra']['git'] = self::getGitInfo();

        return $record;
    }

    private static function getGitInfo()
    {
        if (self::$cache) {
            return self::$cache;
        }

        $branches = `git branch -v --no-abbrev`;
        if ($branches && preg_match('{^\* (.+?)\s+([a-f0-9]{40})(?:\s|$)}m', $branches, $matches)) {
            return self::$cache = array(
                'branch' => $matches[1],
                'commit' => $matches[2],
            );
        }

        return self::$cache = array();
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

/**
 * Some methods that are common for all memory processors
 *
 * @author Rob Jensen
 */
abstract class MemoryProcessor implements ProcessorInterface
{
    /**
     * @var bool If true, get the real size of memory allocated from system. Else, only the memory used by emalloc() is reported.
     */
    protected $realUsage;

    /**
     * @var bool If true, then format memory size to human readable string (MB, KB, B depending on size)
     */
    protected $useFormatting;

    /**
     * @param bool $realUsage     Set this to true to get the real size of memory allocated from system.
     * @param bool $useFormatting If true, then format memory size to human readable string (MB, KB, B depending on size)
     */
    public function __construct($realUsage = true, $useFormatting = true)
    {
        $this->realUsage = (bool) $realUsage;
        $this->useFormatting = (bool) $useFormatting;
    }

    /**
     * Formats bytes into a human readable string if $this->useFormatting is true, otherwise return $bytes as is
     *
     * @param  int        $bytes
     * @return string|int Formatted string if $this->useFormatting is true, otherwise return $bytes as is
     */
    protected function formatBytes($bytes)
    {
        $bytes = (int) $bytes;

        if (!$this->useFormatting) {
            return $bytes;
        }

        if ($bytes > 1024 * 1024) {
            return round($bytes / 1024 / 1024, 2).' MB';
        } elseif ($bytes > 1024) {
            return round($bytes / 1024, 2).' KB';
        }

        return $bytes . ' B';
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

use Monolog\Logger;

/**
 * Injects line/file:class/function where the log message came from
 *
 * Warning: This only works if the handler processes the logs directly.
 * If you put the processor on a handler that is behind a FingersCrossedHandler
 * for example, the processor will only be called once the trigger level is reached,
 * and all the log records will have the same file/line/.. data from the call that
 * triggered the FingersCrossedHandler.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class IntrospectionProcessor implements ProcessorInterface
{
    private $level;

    private $skipClassesPartials;

    private $skipStackFramesCount;

    private $skipFunctions = array(
        'call_user_func',
        'call_user_func_array',
    );

    public function __construct($level = Logger::DEBUG, array $skipClassesPartials = array(), $skipStackFramesCount = 0)
    {
        $this->level = Logger::toMonologLevel($level);
        $this->skipClassesPartials = array_merge(array('Monolog\\'), $skipClassesPartials);
        $this->skipStackFramesCount = $skipStackFramesCount;
    }

    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        // return if the level is not high enough
        if ($record['level'] < $this->level) {
            return $record;
        }

        /*
        * http://php.net/manual/en/function.debug-backtrace.php
        * As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added.
        * Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'.
        */
        $trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS);

        // skip first since it's always the current method
        array_shift($trace);
        // the call_user_func call is also skipped
        array_shift($trace);

        $i = 0;

        while ($this->isTraceClassOrSkippedFunction($trace, $i)) {
            if (isset($trace[$i]['class'])) {
                foreach ($this->skipClassesPartials as $part) {
                    if (strpos($trace[$i]['class'], $part) !== false) {
                        $i++;
                        continue 2;
                    }
                }
            } elseif (in_array($trace[$i]['function'], $this->skipFunctions)) {
                $i++;
                continue;
            }

            break;
        }

        $i += $this->skipStackFramesCount;

        // we should have the call source now
        $record['extra'] = array_merge(
            $record['extra'],
            array(
                'file'      => isset($trace[$i - 1]['file']) ? $trace[$i - 1]['file'] : null,
                'line'      => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null,
                'class'     => isset($trace[$i]['class']) ? $trace[$i]['class'] : null,
                'function'  => isset($trace[$i]['function']) ? $trace[$i]['function'] : null,
            )
        );

        return $record;
    }

    private function isTraceClassOrSkippedFunction(array $trace, $index)
    {
        if (!isset($trace[$index])) {
            return false;
        }

        return isset($trace[$index]['class']) || in_array($trace[$index]['function'], $this->skipFunctions);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

use Monolog\ResettableInterface;

/**
 * Adds a unique identifier into records
 *
 * @author Simon Mönch <sm@webfactory.de>
 */
class UidProcessor implements ProcessorInterface, ResettableInterface
{
    private $uid;

    public function __construct($length = 7)
    {
        if (!is_int($length) || $length > 32 || $length < 1) {
            throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32');
        }


        $this->uid = $this->generateUid($length);
    }

    public function __invoke(array $record)
    {
        $record['extra']['uid'] = $this->uid;

        return $record;
    }

    /**
     * @return string
     */
    public function getUid()
    {
        return $this->uid;
    }

    public function reset()
    {
        $this->uid = $this->generateUid(strlen($this->uid));
    }

    private function generateUid($length)
    {
        return substr(hash('md5', uniqid('', true)), 0, $length);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

/**
 * An optional interface to allow labelling Monolog processors.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 */
interface ProcessorInterface
{
    /**
     * @return array The processed records
     */
    public function __invoke(array $records);
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Processor;

/**
 * Injects memory_get_usage in all records
 *
 * @see Monolog\Processor\MemoryProcessor::__construct() for options
 * @author Rob Jensen
 */
class MemoryUsageProcessor extends MemoryProcessor
{
    /**
     * @param  array $record
     * @return array
     */
    public function __invoke(array $record)
    {
        $bytes = memory_get_usage($this->realUsage);
        $formatted = $this->formatBytes($bytes);

        $record['extra']['memory_usage'] = $formatted;

        return $record;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog;

/**
 * Handler or Processor implementing this interface will be reset when Logger::reset() is called.
 *
 * Resetting ends a log cycle gets them back to their initial state.
 *
 * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal
 * state, and getting it back to a state in which it can receive log records again.
 *
 * This is useful in case you want to avoid logs leaking between two requests or jobs when you
 * have a long running process like a worker or an application server serving multiple requests
 * in one process.
 *
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
 */
interface ResettableInterface
{
    public function reset();
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog;

use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ReflectionExtension;

/**
 * Monolog POSIX signal handler
 *
 * @author Robert Gust-Bardon <robert@gust-bardon.org>
 */
class SignalHandler
{
    private $logger;

    private $previousSignalHandler = array();
    private $signalLevelMap = array();
    private $signalRestartSyscalls = array();

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function registerSignalHandler($signo, $level = LogLevel::CRITICAL, $callPrevious = true, $restartSyscalls = true, $async = true)
    {
        if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) {
            return $this;
        }

        if ($callPrevious) {
            if (function_exists('pcntl_signal_get_handler')) {
                $handler = pcntl_signal_get_handler($signo);
                if ($handler === false) {
                    return $this;
                }
                $this->previousSignalHandler[$signo] = $handler;
            } else {
                $this->previousSignalHandler[$signo] = true;
            }
        } else {
            unset($this->previousSignalHandler[$signo]);
        }
        $this->signalLevelMap[$signo] = $level;
        $this->signalRestartSyscalls[$signo] = $restartSyscalls;

        if (function_exists('pcntl_async_signals') && $async !== null) {
            pcntl_async_signals($async);
        }

        pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls);

        return $this;
    }

    public function handleSignal($signo, array $siginfo = null)
    {
        static $signals = array();

        if (!$signals && extension_loaded('pcntl')) {
            $pcntl = new ReflectionExtension('pcntl');
            $constants = $pcntl->getConstants();
            if (!$constants) {
                // HHVM 3.24.2 returns an empty array.
                $constants = get_defined_constants(true);
                $constants = $constants['Core'];
            }
            foreach ($constants as $name => $value) {
                if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) {
                    $signals[$value] = $name;
                }
            }
            unset($constants);
        }

        $level = isset($this->signalLevelMap[$signo]) ? $this->signalLevelMap[$signo] : LogLevel::CRITICAL;
        $signal = isset($signals[$signo]) ? $signals[$signo] : $signo;
        $context = isset($siginfo) ? $siginfo : array();
        $this->logger->log($level, sprintf('Program received signal %s', $signal), $context);

        if (!isset($this->previousSignalHandler[$signo])) {
            return;
        }

        if ($this->previousSignalHandler[$signo] === true || $this->previousSignalHandler[$signo] === SIG_DFL) {
            if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch')
                && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill')) {
                    $restartSyscalls = isset($this->signalRestartSyscalls[$signo]) ? $this->signalRestartSyscalls[$signo] : true;
                    pcntl_signal($signo, SIG_DFL, $restartSyscalls);
                    pcntl_sigprocmask(SIG_UNBLOCK, array($signo), $oldset);
                    posix_kill(posix_getpid(), $signo);
                    pcntl_signal_dispatch();
                    pcntl_sigprocmask(SIG_SETMASK, $oldset);
                    pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls);
                }
        } elseif (is_callable($this->previousSignalHandler[$signo])) {
            if (PHP_VERSION_ID >= 70100) {
                $this->previousSignalHandler[$signo]($signo, $siginfo);
            } else {
                $this->previousSignalHandler[$signo]($signo);
            }
        }
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Elastica\Document;

/**
 * Format a log message into an Elastica Document
 *
 * @author Jelle Vink <jelle.vink@gmail.com>
 */
class ElasticaFormatter extends NormalizerFormatter
{
    /**
     * @var string Elastic search index name
     */
    protected $index;

    /**
     * @var string Elastic search document type
     */
    protected $type;

    /**
     * @param string $index Elastic Search index name
     * @param string $type  Elastic Search document type
     */
    public function __construct($index, $type)
    {
        // elasticsearch requires a ISO 8601 format date with optional millisecond precision.
        parent::__construct('Y-m-d\TH:i:s.uP');

        $this->index = $index;
        $this->type = $type;
    }

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        $record = parent::format($record);

        return $this->getDocument($record);
    }

    /**
     * Getter index
     * @return string
     */
    public function getIndex()
    {
        return $this->index;
    }

    /**
     * Getter type
     * @return string
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Convert a log message into an Elastica Document
     *
     * @param  array    $record Log message
     * @return Document
     */
    protected function getDocument($record)
    {
        $document = new Document();
        $document->setData($record);
        $document->setType($this->type);
        $document->setIndex($this->index);

        return $document;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Monolog\Utils;

/**
 * Formats incoming records into a one-line string
 *
 * This is especially useful for logging to files
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @author Christophe Coevoet <stof@notk.org>
 */
class LineFormatter extends NormalizerFormatter
{
    const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n";

    protected $format;
    protected $allowInlineLineBreaks;
    protected $ignoreEmptyContextAndExtra;
    protected $includeStacktraces;

    /**
     * @param string $format                     The format of the message
     * @param string $dateFormat                 The format of the timestamp: one supported by DateTime::format
     * @param bool   $allowInlineLineBreaks      Whether to allow inline line breaks in log entries
     * @param bool   $ignoreEmptyContextAndExtra
     */
    public function __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = false, $ignoreEmptyContextAndExtra = false)
    {
        $this->format = $format ?: static::SIMPLE_FORMAT;
        $this->allowInlineLineBreaks = $allowInlineLineBreaks;
        $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
        parent::__construct($dateFormat);
    }

    public function includeStacktraces($include = true)
    {
        $this->includeStacktraces = $include;
        if ($this->includeStacktraces) {
            $this->allowInlineLineBreaks = true;
        }
    }

    public function allowInlineLineBreaks($allow = true)
    {
        $this->allowInlineLineBreaks = $allow;
    }

    public function ignoreEmptyContextAndExtra($ignore = true)
    {
        $this->ignoreEmptyContextAndExtra = $ignore;
    }

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        $vars = parent::format($record);

        $output = $this->format;

        foreach ($vars['extra'] as $var => $val) {
            if (false !== strpos($output, '%extra.'.$var.'%')) {
                $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output);
                unset($vars['extra'][$var]);
            }
        }


        foreach ($vars['context'] as $var => $val) {
            if (false !== strpos($output, '%context.'.$var.'%')) {
                $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output);
                unset($vars['context'][$var]);
            }
        }

        if ($this->ignoreEmptyContextAndExtra) {
            if (empty($vars['context'])) {
                unset($vars['context']);
                $output = str_replace('%context%', '', $output);
            }

            if (empty($vars['extra'])) {
                unset($vars['extra']);
                $output = str_replace('%extra%', '', $output);
            }
        }

        foreach ($vars as $var => $val) {
            if (false !== strpos($output, '%'.$var.'%')) {
                $output = str_replace('%'.$var.'%', $this->stringify($val), $output);
            }
        }

        // remove leftover %extra.xxx% and %context.xxx% if any
        if (false !== strpos($output, '%')) {
            $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output);
        }

        return $output;
    }

    public function formatBatch(array $records)
    {
        $message = '';
        foreach ($records as $record) {
            $message .= $this->format($record);
        }

        return $message;
    }

    public function stringify($value)
    {
        return $this->replaceNewlines($this->convertToString($value));
    }

    protected function normalizeException($e)
    {
        // TODO 2.0 only check for Throwable
        if (!$e instanceof \Exception && !$e instanceof \Throwable) {
            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e));
        }

        $previousText = '';
        if ($previous = $e->getPrevious()) {
            do {
                $previousText .= ', '.Utils::getClass($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine();
            } while ($previous = $previous->getPrevious());
        }

        $str = '[object] ('.Utils::getClass($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')';
        if ($this->includeStacktraces) {
            $str .= "\n[stacktrace]\n".$e->getTraceAsString()."\n";
        }

        return $str;
    }

    protected function convertToString($data)
    {
        if (null === $data || is_bool($data)) {
            return var_export($data, true);
        }

        if (is_scalar($data)) {
            return (string) $data;
        }

        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
            return $this->toJson($data, true);
        }

        return str_replace('\\/', '/', $this->toJson($data, true));
    }

    protected function replaceNewlines($str)
    {
        if ($this->allowInlineLineBreaks) {
            if (0 === strpos($str, '{')) {
                return str_replace(array('\r', '\n'), array("\r", "\n"), $str);
            }

            return $str;
        }

        return str_replace(array("\r\n", "\r", "\n"), ' ', $str);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Exception;
use Monolog\Utils;
use Throwable;

/**
 * Encodes whatever record data is passed to it as json
 *
 * This can be useful to log to databases or remote APIs
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class JsonFormatter extends NormalizerFormatter
{
    const BATCH_MODE_JSON = 1;
    const BATCH_MODE_NEWLINES = 2;

    protected $batchMode;
    protected $appendNewline;

    /**
     * @var bool
     */
    protected $includeStacktraces = false;

    /**
     * @param int $batchMode
     * @param bool $appendNewline
     */
    public function __construct($batchMode = self::BATCH_MODE_JSON, $appendNewline = true)
    {
        $this->batchMode = $batchMode;
        $this->appendNewline = $appendNewline;
    }

    /**
     * The batch mode option configures the formatting style for
     * multiple records. By default, multiple records will be
     * formatted as a JSON-encoded array. However, for
     * compatibility with some API endpoints, alternative styles
     * are available.
     *
     * @return int
     */
    public function getBatchMode()
    {
        return $this->batchMode;
    }

    /**
     * True if newlines are appended to every formatted record
     *
     * @return bool
     */
    public function isAppendingNewlines()
    {
        return $this->appendNewline;
    }

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        return $this->toJson($this->normalize($record), true) . ($this->appendNewline ? "\n" : '');
    }

    /**
     * {@inheritdoc}
     */
    public function formatBatch(array $records)
    {
        switch ($this->batchMode) {
            case static::BATCH_MODE_NEWLINES:
                return $this->formatBatchNewlines($records);

            case static::BATCH_MODE_JSON:
            default:
                return $this->formatBatchJson($records);
        }
    }

    /**
     * @param bool $include
     */
    public function includeStacktraces($include = true)
    {
        $this->includeStacktraces = $include;
    }

    /**
     * Return a JSON-encoded array of records.
     *
     * @param  array  $records
     * @return string
     */
    protected function formatBatchJson(array $records)
    {
        return $this->toJson($this->normalize($records), true);
    }

    /**
     * Use new lines to separate records instead of a
     * JSON-encoded array.
     *
     * @param  array  $records
     * @return string
     */
    protected function formatBatchNewlines(array $records)
    {
        $instance = $this;

        $oldNewline = $this->appendNewline;
        $this->appendNewline = false;
        array_walk($records, function (&$value, $key) use ($instance) {
            $value = $instance->format($value);
        });
        $this->appendNewline = $oldNewline;

        return implode("\n", $records);
    }

    /**
     * Normalizes given $data.
     *
     * @param mixed $data
     *
     * @return mixed
     */
    protected function normalize($data, $depth = 0)
    {
        if ($depth > 9) {
            return 'Over 9 levels deep, aborting normalization';
        }

        if (is_array($data)) {
            $normalized = array();

            $count = 1;
            foreach ($data as $key => $value) {
                if ($count++ > 1000) {
                    $normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization';
                    break;
                }

                $normalized[$key] = $this->normalize($value, $depth+1);
            }

            return $normalized;
        }

        if ($data instanceof Exception || $data instanceof Throwable) {
            return $this->normalizeException($data);
        }

        if (is_resource($data)) {
            return parent::normalize($data);
        }

        return $data;
    }

    /**
     * Normalizes given exception with or without its own stack trace based on
     * `includeStacktraces` property.
     *
     * @param Exception|Throwable $e
     *
     * @return array
     */
    protected function normalizeException($e)
    {
        // TODO 2.0 only check for Throwable
        if (!$e instanceof Exception && !$e instanceof Throwable) {
            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e));
        }

        $data = array(
            'class' => Utils::getClass($e),
            'message' => $e->getMessage(),
            'code' => (int) $e->getCode(),
            'file' => $e->getFile().':'.$e->getLine(),
        );

        if ($this->includeStacktraces) {
            $trace = $e->getTrace();
            foreach ($trace as $frame) {
                if (isset($frame['file'])) {
                    $data['trace'][] = $frame['file'].':'.$frame['line'];
                }
            }
        }

        if ($previous = $e->getPrevious()) {
            $data['previous'] = $this->normalizeException($previous);
        }

        return $data;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

/**
 * formats the record to be used in the FlowdockHandler
 *
 * @author Dominik Liebler <liebler.dominik@gmail.com>
 */
class FlowdockFormatter implements FormatterInterface
{
    /**
     * @var string
     */
    private $source;

    /**
     * @var string
     */
    private $sourceEmail;

    /**
     * @param string $source
     * @param string $sourceEmail
     */
    public function __construct($source, $sourceEmail)
    {
        $this->source = $source;
        $this->sourceEmail = $sourceEmail;
    }

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        $tags = array(
            '#logs',
            '#' . strtolower($record['level_name']),
            '#' . $record['channel'],
        );

        foreach ($record['extra'] as $value) {
            $tags[] = '#' . $value;
        }

        $subject = sprintf(
            'in %s: %s - %s',
            $this->source,
            $record['level_name'],
            $this->getShortMessage($record['message'])
        );

        $record['flowdock'] = array(
            'source' => $this->source,
            'from_address' => $this->sourceEmail,
            'subject' => $subject,
            'content' => $record['message'],
            'tags' => $tags,
            'project' => $this->source,
        );

        return $record;
    }

    /**
     * {@inheritdoc}
     */
    public function formatBatch(array $records)
    {
        $formatted = array();

        foreach ($records as $record) {
            $formatted[] = $this->format($record);
        }

        return $formatted;
    }

    /**
     * @param string $message
     *
     * @return string
     */
    public function getShortMessage($message)
    {
        static $hasMbString;

        if (null === $hasMbString) {
            $hasMbString = function_exists('mb_strlen');
        }

        $maxLength = 45;

        if ($hasMbString) {
            if (mb_strlen($message, 'UTF-8') > $maxLength) {
                $message = mb_substr($message, 0, $maxLength - 4, 'UTF-8') . ' ...';
            }
        } else {
            if (strlen($message) > $maxLength) {
                $message = substr($message, 0, $maxLength - 4) . ' ...';
            }
        }

        return $message;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

/**
 * Interface for formatters
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
interface FormatterInterface
{
    /**
     * Formats a log record.
     *
     * @param  array $record A record to format
     * @return mixed The formatted record
     */
    public function format(array $record);

    /**
     * Formats a set of log records.
     *
     * @param  array $records A set of records to format
     * @return mixed The formatted set of records
     */
    public function formatBatch(array $records);
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

/**
 * Serializes a log message to Logstash Event Format
 *
 * @see http://logstash.net/
 * @see https://github.com/logstash/logstash/blob/master/lib/logstash/event.rb
 *
 * @author Tim Mower <timothy.mower@gmail.com>
 */
class LogstashFormatter extends NormalizerFormatter
{
    const V0 = 0;
    const V1 = 1;

    /**
     * @var string the name of the system for the Logstash log message, used to fill the @source field
     */
    protected $systemName;

    /**
     * @var string an application name for the Logstash log message, used to fill the @type field
     */
    protected $applicationName;

    /**
     * @var string a prefix for 'extra' fields from the Monolog record (optional)
     */
    protected $extraPrefix;

    /**
     * @var string a prefix for 'context' fields from the Monolog record (optional)
     */
    protected $contextPrefix;

    /**
     * @var int logstash format version to use
     */
    protected $version;

    /**
     * @param string $applicationName the application that sends the data, used as the "type" field of logstash
     * @param string $systemName      the system/machine name, used as the "source" field of logstash, defaults to the hostname of the machine
     * @param string $extraPrefix     prefix for extra keys inside logstash "fields"
     * @param string $contextPrefix   prefix for context keys inside logstash "fields", defaults to ctxt_
     * @param int    $version         the logstash format version to use, defaults to 0
     */
    public function __construct($applicationName, $systemName = null, $extraPrefix = null, $contextPrefix = 'ctxt_', $version = self::V0)
    {
        // logstash requires a ISO 8601 format date with optional millisecond precision.
        parent::__construct('Y-m-d\TH:i:s.uP');

        $this->systemName = $systemName ?: gethostname();
        $this->applicationName = $applicationName;
        $this->extraPrefix = $extraPrefix;
        $this->contextPrefix = $contextPrefix;
        $this->version = $version;
    }

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        $record = parent::format($record);

        if ($this->version === self::V1) {
            $message = $this->formatV1($record);
        } else {
            $message = $this->formatV0($record);
        }

        return $this->toJson($message) . "\n";
    }

    protected function formatV0(array $record)
    {
        if (empty($record['datetime'])) {
            $record['datetime'] = gmdate('c');
        }
        $message = array(
            '@timestamp' => $record['datetime'],
            '@source' => $this->systemName,
            '@fields' => array(),
        );
        if (isset($record['message'])) {
            $message['@message'] = $record['message'];
        }
        if (isset($record['channel'])) {
            $message['@tags'] = array($record['channel']);
            $message['@fields']['channel'] = $record['channel'];
        }
        if (isset($record['level'])) {
            $message['@fields']['level'] = $record['level'];
        }
        if ($this->applicationName) {
            $message['@type'] = $this->applicationName;
        }
        if (isset($record['extra']['server'])) {
            $message['@source_host'] = $record['extra']['server'];
        }
        if (isset($record['extra']['url'])) {
            $message['@source_path'] = $record['extra']['url'];
        }
        if (!empty($record['extra'])) {
            foreach ($record['extra'] as $key => $val) {
                $message['@fields'][$this->extraPrefix . $key] = $val;
            }
        }
        if (!empty($record['context'])) {
            foreach ($record['context'] as $key => $val) {
                $message['@fields'][$this->contextPrefix . $key] = $val;
            }
        }

        return $message;
    }

    protected function formatV1(array $record)
    {
        if (empty($record['datetime'])) {
            $record['datetime'] = gmdate('c');
        }
        $message = array(
            '@timestamp' => $record['datetime'],
            '@version' => 1,
            'host' => $this->systemName,
        );
        if (isset($record['message'])) {
            $message['message'] = $record['message'];
        }
        if (isset($record['channel'])) {
            $message['type'] = $record['channel'];
            $message['channel'] = $record['channel'];
        }
        if (isset($record['level_name'])) {
            $message['level'] = $record['level_name'];
        }
        if ($this->applicationName) {
            $message['type'] = $this->applicationName;
        }
        if (!empty($record['extra'])) {
            foreach ($record['extra'] as $key => $val) {
                $message[$this->extraPrefix . $key] = $val;
            }
        }
        if (!empty($record['context'])) {
            foreach ($record['context'] as $key => $val) {
                $message[$this->contextPrefix . $key] = $val;
            }
        }

        return $message;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

/**
 * Encodes message information into JSON in a format compatible with Loggly.
 *
 * @author Adam Pancutt <adam@pancutt.com>
 */
class LogglyFormatter extends JsonFormatter
{
    /**
     * Overrides the default batch mode to new lines for compatibility with the
     * Loggly bulk API.
     *
     * @param int $batchMode
     */
    public function __construct($batchMode = self::BATCH_MODE_NEWLINES, $appendNewline = false)
    {
        parent::__construct($batchMode, $appendNewline);
    }

    /**
     * Appends the 'timestamp' parameter for indexing by Loggly.
     *
     * @see https://www.loggly.com/docs/automated-parsing/#json
     * @see \Monolog\Formatter\JsonFormatter::format()
     */
    public function format(array $record)
    {
        if (isset($record["datetime"]) && ($record["datetime"] instanceof \DateTime)) {
            $record["timestamp"] = $record["datetime"]->format("Y-m-d\TH:i:s.uO");
            // TODO 2.0 unset the 'datetime' parameter, retained for BC
        }

        return parent::format($record);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

/**
 * Formats data into an associative array of scalar values.
 * Objects and arrays will be JSON encoded.
 *
 * @author Andrew Lawson <adlawson@gmail.com>
 */
class ScalarFormatter extends NormalizerFormatter
{
    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        foreach ($record as $key => $value) {
            $record[$key] = $this->normalizeValue($value);
        }

        return $record;
    }

    /**
     * @param  mixed $value
     * @return mixed
     */
    protected function normalizeValue($value)
    {
        $normalized = $this->normalize($value);

        if (is_array($normalized) || is_object($normalized)) {
            return $this->toJson($normalized, true);
        }

        return $normalized;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Monolog\Utils;

/**
 * Formats a record for use with the MongoDBHandler.
 *
 * @author Florian Plattner <me@florianplattner.de>
 */
class MongoDBFormatter implements FormatterInterface
{
    private $exceptionTraceAsString;
    private $maxNestingLevel;

    /**
     * @param int  $maxNestingLevel        0 means infinite nesting, the $record itself is level 1, $record['context'] is 2
     * @param bool $exceptionTraceAsString set to false to log exception traces as a sub documents instead of strings
     */
    public function __construct($maxNestingLevel = 3, $exceptionTraceAsString = true)
    {
        $this->maxNestingLevel = max($maxNestingLevel, 0);
        $this->exceptionTraceAsString = (bool) $exceptionTraceAsString;
    }

    /**
     * {@inheritDoc}
     */
    public function format(array $record)
    {
        return $this->formatArray($record);
    }

    /**
     * {@inheritDoc}
     */
    public function formatBatch(array $records)
    {
        foreach ($records as $key => $record) {
            $records[$key] = $this->format($record);
        }

        return $records;
    }

    protected function formatArray(array $record, $nestingLevel = 0)
    {
        if ($this->maxNestingLevel == 0 || $nestingLevel <= $this->maxNestingLevel) {
            foreach ($record as $name => $value) {
                if ($value instanceof \DateTime) {
                    $record[$name] = $this->formatDate($value, $nestingLevel + 1);
                } elseif ($value instanceof \Exception) {
                    $record[$name] = $this->formatException($value, $nestingLevel + 1);
                } elseif (is_array($value)) {
                    $record[$name] = $this->formatArray($value, $nestingLevel + 1);
                } elseif (is_object($value)) {
                    $record[$name] = $this->formatObject($value, $nestingLevel + 1);
                }
            }
        } else {
            $record = '[...]';
        }

        return $record;
    }

    protected function formatObject($value, $nestingLevel)
    {
        $objectVars = get_object_vars($value);
        $objectVars['class'] = Utils::getClass($value);

        return $this->formatArray($objectVars, $nestingLevel);
    }

    protected function formatException(\Exception $exception, $nestingLevel)
    {
        $formattedException = array(
            'class' => Utils::getClass($exception),
            'message' => $exception->getMessage(),
            'code' => (int) $exception->getCode(),
            'file' => $exception->getFile() . ':' . $exception->getLine(),
        );

        if ($this->exceptionTraceAsString === true) {
            $formattedException['trace'] = $exception->getTraceAsString();
        } else {
            $formattedException['trace'] = $exception->getTrace();
        }

        return $this->formatArray($formattedException, $nestingLevel);
    }

    protected function formatDate(\DateTime $value, $nestingLevel)
    {
        return new \MongoDate($value->getTimestamp());
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Monolog\Logger;

/**
 * Formats a log message according to the ChromePHP array format
 *
 * @author Christophe Coevoet <stof@notk.org>
 */
class ChromePHPFormatter implements FormatterInterface
{
    /**
     * Translates Monolog log levels to Wildfire levels.
     */
    private $logLevels = array(
        Logger::DEBUG     => 'log',
        Logger::INFO      => 'info',
        Logger::NOTICE    => 'info',
        Logger::WARNING   => 'warn',
        Logger::ERROR     => 'error',
        Logger::CRITICAL  => 'error',
        Logger::ALERT     => 'error',
        Logger::EMERGENCY => 'error',
    );

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        // Retrieve the line and file if set and remove them from the formatted extra
        $backtrace = 'unknown';
        if (isset($record['extra']['file'], $record['extra']['line'])) {
            $backtrace = $record['extra']['file'].' : '.$record['extra']['line'];
            unset($record['extra']['file'], $record['extra']['line']);
        }

        $message = array('message' => $record['message']);
        if ($record['context']) {
            $message['context'] = $record['context'];
        }
        if ($record['extra']) {
            $message['extra'] = $record['extra'];
        }
        if (count($message) === 1) {
            $message = reset($message);
        }

        return array(
            $record['channel'],
            $message,
            $backtrace,
            $this->logLevels[$record['level']],
        );
    }

    public function formatBatch(array $records)
    {
        $formatted = array();

        foreach ($records as $record) {
            $formatted[] = $this->format($record);
        }

        return $formatted;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Monolog\Utils;

/**
 * Class FluentdFormatter
 *
 * Serializes a log message to Fluentd unix socket protocol
 *
 * Fluentd config:
 *
 * <source>
 *  type unix
 *  path /var/run/td-agent/td-agent.sock
 * </source>
 *
 * Monolog setup:
 *
 * $logger = new Monolog\Logger('fluent.tag');
 * $fluentHandler = new Monolog\Handler\SocketHandler('unix:///var/run/td-agent/td-agent.sock');
 * $fluentHandler->setFormatter(new Monolog\Formatter\FluentdFormatter());
 * $logger->pushHandler($fluentHandler);
 *
 * @author Andrius Putna <fordnox@gmail.com>
 */
class FluentdFormatter implements FormatterInterface
{
    /**
     * @var bool $levelTag should message level be a part of the fluentd tag
     */
    protected $levelTag = false;

    public function __construct($levelTag = false)
    {
        if (!function_exists('json_encode')) {
            throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s FluentdUnixFormatter');
        }

        $this->levelTag = (bool) $levelTag;
    }

    public function isUsingLevelsInTag()
    {
        return $this->levelTag;
    }

    public function format(array $record)
    {
        $tag = $record['channel'];
        if ($this->levelTag) {
            $tag .= '.' . strtolower($record['level_name']);
        }

        $message = array(
            'message' => $record['message'],
            'context' => $record['context'],
            'extra' => $record['extra'],
        );

        if (!$this->levelTag) {
            $message['level'] = $record['level'];
            $message['level_name'] = $record['level_name'];
        }

        return Utils::jsonEncode(array($tag, $record['datetime']->getTimestamp(), $message));
    }

    public function formatBatch(array $records)
    {
        $message = '';
        foreach ($records as $record) {
            $message .= $this->format($record);
        }

        return $message;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Monolog\Logger;
use Gelf\Message;

/**
 * Serializes a log message to GELF
 * @see http://www.graylog2.org/about/gelf
 *
 * @author Matt Lehner <mlehner@gmail.com>
 */
class GelfMessageFormatter extends NormalizerFormatter
{
    const DEFAULT_MAX_LENGTH = 32766;

    /**
     * @var string the name of the system for the Gelf log message
     */
    protected $systemName;

    /**
     * @var string a prefix for 'extra' fields from the Monolog record (optional)
     */
    protected $extraPrefix;

    /**
     * @var string a prefix for 'context' fields from the Monolog record (optional)
     */
    protected $contextPrefix;

    /**
     * @var int max length per field
     */
    protected $maxLength;

    /**
     * Translates Monolog log levels to Graylog2 log priorities.
     */
    private $logLevels = array(
        Logger::DEBUG     => 7,
        Logger::INFO      => 6,
        Logger::NOTICE    => 5,
        Logger::WARNING   => 4,
        Logger::ERROR     => 3,
        Logger::CRITICAL  => 2,
        Logger::ALERT     => 1,
        Logger::EMERGENCY => 0,
    );

    public function __construct($systemName = null, $extraPrefix = null, $contextPrefix = 'ctxt_', $maxLength = null)
    {
        parent::__construct('U.u');

        $this->systemName = $systemName ?: gethostname();

        $this->extraPrefix = $extraPrefix;
        $this->contextPrefix = $contextPrefix;
        $this->maxLength = is_null($maxLength) ? self::DEFAULT_MAX_LENGTH : $maxLength;
    }

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        $record = parent::format($record);

        if (!isset($record['datetime'], $record['message'], $record['level'])) {
            throw new \InvalidArgumentException('The record should at least contain datetime, message and level keys, '.var_export($record, true).' given');
        }

        $message = new Message();
        $message
            ->setTimestamp($record['datetime'])
            ->setShortMessage((string) $record['message'])
            ->setHost($this->systemName)
            ->setLevel($this->logLevels[$record['level']]);

        // message length + system name length + 200 for padding / metadata 
        $len = 200 + strlen((string) $record['message']) + strlen($this->systemName);

        if ($len > $this->maxLength) {
            $message->setShortMessage(substr($record['message'], 0, $this->maxLength));
        }

        if (isset($record['channel'])) {
            $message->setFacility($record['channel']);
        }
        if (isset($record['extra']['line'])) {
            $message->setLine($record['extra']['line']);
            unset($record['extra']['line']);
        }
        if (isset($record['extra']['file'])) {
            $message->setFile($record['extra']['file']);
            unset($record['extra']['file']);
        }

        foreach ($record['extra'] as $key => $val) {
            $val = is_scalar($val) || null === $val ? $val : $this->toJson($val);
            $len = strlen($this->extraPrefix . $key . $val);
            if ($len > $this->maxLength) {
                $message->setAdditional($this->extraPrefix . $key, substr($val, 0, $this->maxLength));
                break;
            }
            $message->setAdditional($this->extraPrefix . $key, $val);
        }

        foreach ($record['context'] as $key => $val) {
            $val = is_scalar($val) || null === $val ? $val : $this->toJson($val);
            $len = strlen($this->contextPrefix . $key . $val);
            if ($len > $this->maxLength) {
                $message->setAdditional($this->contextPrefix . $key, substr($val, 0, $this->maxLength));
                break;
            }
            $message->setAdditional($this->contextPrefix . $key, $val);
        }

        if (null === $message->getFile() && isset($record['context']['exception']['file'])) {
            if (preg_match("/^(.+):([0-9]+)$/", $record['context']['exception']['file'], $matches)) {
                $message->setFile($matches[1]);
                $message->setLine($matches[2]);
            }
        }

        return $message;
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Monolog\Logger;

/**
 * Serializes a log message according to Wildfire's header requirements
 *
 * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
 * @author Christophe Coevoet <stof@notk.org>
 * @author Kirill chEbba Chebunin <iam@chebba.org>
 */
class WildfireFormatter extends NormalizerFormatter
{
    const TABLE = 'table';

    /**
     * Translates Monolog log levels to Wildfire levels.
     */
    private $logLevels = array(
        Logger::DEBUG     => 'LOG',
        Logger::INFO      => 'INFO',
        Logger::NOTICE    => 'INFO',
        Logger::WARNING   => 'WARN',
        Logger::ERROR     => 'ERROR',
        Logger::CRITICAL  => 'ERROR',
        Logger::ALERT     => 'ERROR',
        Logger::EMERGENCY => 'ERROR',
    );

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        // Retrieve the line and file if set and remove them from the formatted extra
        $file = $line = '';
        if (isset($record['extra']['file'])) {
            $file = $record['extra']['file'];
            unset($record['extra']['file']);
        }
        if (isset($record['extra']['line'])) {
            $line = $record['extra']['line'];
            unset($record['extra']['line']);
        }

        $record = $this->normalize($record);
        $message = array('message' => $record['message']);
        $handleError = false;
        if ($record['context']) {
            $message['context'] = $record['context'];
            $handleError = true;
        }
        if ($record['extra']) {
            $message['extra'] = $record['extra'];
            $handleError = true;
        }
        if (count($message) === 1) {
            $message = reset($message);
        }

        if (isset($record['context'][self::TABLE])) {
            $type  = 'TABLE';
            $label = $record['channel'] .': '. $record['message'];
            $message = $record['context'][self::TABLE];
        } else {
            $type  = $this->logLevels[$record['level']];
            $label = $record['channel'];
        }

        // Create JSON object describing the appearance of the message in the console
        $json = $this->toJson(array(
            array(
                'Type'  => $type,
                'File'  => $file,
                'Line'  => $line,
                'Label' => $label,
            ),
            $message,
        ), $handleError);

        // The message itself is a serialization of the above JSON object + it's length
        return sprintf(
            '%s|%s|',
            strlen($json),
            $json
        );
    }

    public function formatBatch(array $records)
    {
        throw new \BadMethodCallException('Batch formatting does not make sense for the WildfireFormatter');
    }

    protected function normalize($data, $depth = 0)
    {
        if (is_object($data) && !$data instanceof \DateTime) {
            return $data;
        }

        return parent::normalize($data, $depth);
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Exception;
use Monolog\Utils;

/**
 * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class NormalizerFormatter implements FormatterInterface
{
    const SIMPLE_DATE = "Y-m-d H:i:s";

    protected $dateFormat;

    /**
     * @param string $dateFormat The format of the timestamp: one supported by DateTime::format
     */
    public function __construct($dateFormat = null)
    {
        $this->dateFormat = $dateFormat ?: static::SIMPLE_DATE;
        if (!function_exists('json_encode')) {
            throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter');
        }
    }

    /**
     * {@inheritdoc}
     */
    public function format(array $record)
    {
        return $this->normalize($record);
    }

    /**
     * {@inheritdoc}
     */
    public function formatBatch(array $records)
    {
        foreach ($records as $key => $record) {
            $records[$key] = $this->format($record);
        }

        return $records;
    }

    protected function normalize($data, $depth = 0)
    {
        if ($depth > 9) {
            return 'Over 9 levels deep, aborting normalization';
        }

        if (null === $data || is_scalar($data)) {
            if (is_float($data)) {
                if (is_infinite($data)) {
                    return ($data > 0 ? '' : '-') . 'INF';
                }
                if (is_nan($data)) {
                    return 'NaN';
                }
            }

            return $data;
        }

        if (is_array($data)) {
            $normalized = array();

            $count = 1;
            foreach ($data as $key => $value) {
                if ($count++ > 1000) {
                    $normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization';
                    break;
                }

                $normalized[$key] = $this->normalize($value, $depth+1);
            }

            return $normalized;
        }

        if ($data instanceof \DateTime) {
            return $data->format($this->dateFormat);
        }

        if (is_object($data)) {
            // TODO 2.0 only check for Throwable
            if ($data instanceof Exception || (PHP_VERSION_ID > 70000 && $data instanceof \Throwable)) {
                return $this->normalizeException($data);
            }

            // non-serializable objects that implement __toString stringified
            if (method_exists($data, '__toString') && !$data instanceof \JsonSerializable) {
                $value = $data->__toString();
            } else {
                // the rest is json-serialized in some way
                $value = $this->toJson($data, true);
            }

            return sprintf("[object] (%s: %s)", Utils::getClass($data), $value);
        }

        if (is_resource($data)) {
            return sprintf('[resource] (%s)', get_resource_type($data));
        }

        return '[unknown('.gettype($data).')]';
    }

    protected function normalizeException($e)
    {
        // TODO 2.0 only check for Throwable
        if (!$e instanceof Exception && !$e instanceof \Throwable) {
            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e));
        }

        $data = array(
            'class' => Utils::getClass($e),
            'message' => $e->getMessage(),
            'code' => (int) $e->getCode(),
            'file' => $e->getFile().':'.$e->getLine(),
        );

        if ($e instanceof \SoapFault) {
            if (isset($e->faultcode)) {
                $data['faultcode'] = $e->faultcode;
            }

            if (isset($e->faultactor)) {
                $data['faultactor'] = $e->faultactor;
            }

            if (isset($e->detail)) {
                if  (is_string($e->detail)) {
                    $data['detail'] = $e->detail;
                } elseif (is_object($e->detail) || is_array($e->detail)) {
                    $data['detail'] = $this->toJson($e->detail, true);
                }
            }
        }

        $trace = $e->getTrace();
        foreach ($trace as $frame) {
            if (isset($frame['file'])) {
                $data['trace'][] = $frame['file'].':'.$frame['line'];
            }
        }

        if ($previous = $e->getPrevious()) {
            $data['previous'] = $this->normalizeException($previous);
        }

        return $data;
    }

    /**
     * Return the JSON representation of a value
     *
     * @param  mixed             $data
     * @param  bool              $ignoreErrors
     * @throws \RuntimeException if encoding fails and errors are not ignored
     * @return string
     */
    protected function toJson($data, $ignoreErrors = false)
    {
        return Utils::jsonEncode($data, null, $ignoreErrors);
    }
}
<?php
/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Formatter;

use Monolog\Logger;
use Monolog\Utils;

/**
 * Formats incoming records into an HTML table
 *
 * This is especially useful for html email logging
 *
 * @author Tiago Brito <tlfbrito@gmail.com>
 */
class HtmlFormatter extends NormalizerFormatter
{
    /**
     * Translates Monolog log levels to html color priorities.
     */
    protected $logLevels = array(
        Logger::DEBUG     => '#cccccc',
        Logger::INFO      => '#468847',
        Logger::NOTICE    => '#3a87ad',
        Logger::WARNING   => '#c09853',
        Logger::ERROR     => '#f0ad4e',
        Logger::CRITICAL  => '#FF7708',
        Logger::ALERT     => '#C12A19',
        Logger::EMERGENCY => '#000000',
    );

    /**
     * @param string $dateFormat The format of the timestamp: one supported by DateTime::format
     */
    public function __construct($dateFormat = null)
    {
        parent::__construct($dateFormat);
    }

    /**
     * Creates an HTML table row
     *
     * @param  string $th       Row header content
     * @param  string $td       Row standard cell content
     * @param  bool   $escapeTd false if td content must not be html escaped
     * @return string
     */
    protected function addRow($th, $td = ' ', $escapeTd = true)
    {
        $th = htmlspecialchars($th, ENT_NOQUOTES, 'UTF-8');
        if ($escapeTd) {
            $td = '<pre>'.htmlspecialchars($td, ENT_NOQUOTES, 'UTF-8').'</pre>';
        }

        return "<tr style=\"padding: 4px;text-align: left;\">\n<th style=\"vertical-align: top;background: #ccc;color: #000\" width=\"100\">$th:</th>\n<td style=\"padding: 4px;text-align: left;vertical-align: top;background: #eee;color: #000\">".$td."</td>\n</tr>";
    }

    /**
     * Create a HTML h1 tag
     *
     * @param  string $title Text to be in the h1
     * @param  int    $level Error level
     * @return string
     */
    protected function addTitle($title, $level)
    {
        $title = htmlspecialchars($title, ENT_NOQUOTES, 'UTF-8');

        return '<h1 style="background: '.$this->logLevels[$level].';color: #ffffff;padding: 5px;" class="monolog-output">'.$title.'</h1>';
    }

    /**
     * Formats a log record.
     *
     * @param  array $record A record to format
     * @return mixed The formatted record
     */
    public function format(array $record)
    {
        $output = $this->addTitle($record['level_name'], $record['level']);
        $output .= '<table cellspacing="1" width="100%" class="monolog-output">';

        $output .= $this->addRow('Message', (string) $record['message']);
        $output .= $this->addRow('Time', $record['datetime']->format($this->dateFormat));
        $output .= $this->addRow('Channel', $record['channel']);
        if ($record['context']) {
            $embeddedTable = '<table cellspacing="1" width="100%">';
            foreach ($record['context'] as $key => $value) {
                $embeddedTable .= $this->addRow($key, $this->convertToString($value));
            }
            $embeddedTable .= '</table>';
            $output .= $this->addRow('Context', $embeddedTable, false);
        }
        if ($record['extra']) {
            $embeddedTable = '<table cellspacing="1" width="100%">';
            foreach ($record['extra'] as $key => $value) {
                $embeddedTable .= $this->addRow($key, $this->convertToString($value));
            }
            $embeddedTable .= '</table>';
            $output .= $this->addRow('Extra', $embeddedTable, false);
        }

        return $output.'</table>';
    }

    /**
     * Formats a set of log records.
     *
     * @param  array $records A set of records to format
     * @return mixed The formatted set of records
     */
    public function formatBatch(array $records)
    {
        $message = '';
        foreach ($records as $record) {
            $message .= $this->format($record);
        }

        return $message;
    }

    protected function convertToString($data)
    {
        if (null === $data || is_scalar($data)) {
            return (string) $data;
        }

        $data = $this->normalize($data);
        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
            return Utils::jsonEncode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, true);
        }

        return str_replace('\\/', '/', Utils::jsonEncode($data, null, true));
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog;

use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Monolog\Handler\AbstractHandler;

/**
 * Monolog error handler
 *
 * A facility to enable logging of runtime errors, exceptions and fatal errors.
 *
 * Quick setup: <code>ErrorHandler::register($logger);</code>
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
class ErrorHandler
{
    private $logger;

    private $previousExceptionHandler;
    private $uncaughtExceptionLevel;

    private $previousErrorHandler;
    private $errorLevelMap;
    private $handleOnlyReportedErrors;

    private $hasFatalErrorHandler;
    private $fatalLevel;
    private $reservedMemory;
    private $lastFatalTrace;
    private static $fatalErrors = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR);

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * Registers a new ErrorHandler for a given Logger
     *
     * By default it will handle errors, exceptions and fatal errors
     *
     * @param  LoggerInterface $logger
     * @param  array|false     $errorLevelMap  an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
     * @param  int|false       $exceptionLevel a LogLevel::* constant, or false to disable exception handling
     * @param  int|false       $fatalLevel     a LogLevel::* constant, or false to disable fatal error handling
     * @return ErrorHandler
     */
    public static function register(LoggerInterface $logger, $errorLevelMap = array(), $exceptionLevel = null, $fatalLevel = null)
    {
        //Forces the autoloader to run for LogLevel. Fixes an autoload issue at compile-time on PHP5.3. See https://github.com/Seldaek/monolog/pull/929
        class_exists('\\Psr\\Log\\LogLevel', true);

        $handler = new static($logger);
        if ($errorLevelMap !== false) {
            $handler->registerErrorHandler($errorLevelMap);
        }
        if ($exceptionLevel !== false) {
            $handler->registerExceptionHandler($exceptionLevel);
        }
        if ($fatalLevel !== false) {
            $handler->registerFatalHandler($fatalLevel);
        }

        return $handler;
    }

    public function registerExceptionHandler($level = null, $callPrevious = true)
    {
        $prev = set_exception_handler(array($this, 'handleException'));
        $this->uncaughtExceptionLevel = $level;
        if ($callPrevious && $prev) {
            $this->previousExceptionHandler = $prev;
        }
    }

    public function registerErrorHandler(array $levelMap = array(), $callPrevious = true, $errorTypes = -1, $handleOnlyReportedErrors = true)
    {
        $prev = set_error_handler(array($this, 'handleError'), $errorTypes);
        $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
        if ($callPrevious) {
            $this->previousErrorHandler = $prev ?: true;
        }

        $this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
    }

    public function registerFatalHandler($level = null, $reservedMemorySize = 20)
    {
        register_shutdown_function(array($this, 'handleFatalError'));

        $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
        $this->fatalLevel = $level;
        $this->hasFatalErrorHandler = true;
    }

    protected function defaultErrorLevelMap()
    {
        return array(
            E_ERROR             => LogLevel::CRITICAL,
            E_WARNING           => LogLevel::WARNING,
            E_PARSE             => LogLevel::ALERT,
            E_NOTICE            => LogLevel::NOTICE,
            E_CORE_ERROR        => LogLevel::CRITICAL,
            E_CORE_WARNING      => LogLevel::WARNING,
            E_COMPILE_ERROR     => LogLevel::ALERT,
            E_COMPILE_WARNING   => LogLevel::WARNING,
            E_USER_ERROR        => LogLevel::ERROR,
            E_USER_WARNING      => LogLevel::WARNING,
            E_USER_NOTICE       => LogLevel::NOTICE,
            E_STRICT            => LogLevel::NOTICE,
            E_RECOVERABLE_ERROR => LogLevel::ERROR,
            E_DEPRECATED        => LogLevel::NOTICE,
            E_USER_DEPRECATED   => LogLevel::NOTICE,
        );
    }

    /**
     * @private
     */
    public function handleException($e)
    {
        $this->logger->log(
            $this->uncaughtExceptionLevel === null ? LogLevel::ERROR : $this->uncaughtExceptionLevel,
            sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
            array('exception' => $e)
        );

        if ($this->previousExceptionHandler) {
            call_user_func($this->previousExceptionHandler, $e);
        }

        exit(255);
    }

    /**
     * @private
     */
    public function handleError($code, $message, $file = '', $line = 0, $context = array())
    {
        if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) {
            return;
        }

        // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
        if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) {
            $level = isset($this->errorLevelMap[$code]) ? $this->errorLevelMap[$code] : LogLevel::CRITICAL;
            $this->logger->log($level, self::codeToString($code).': '.$message, array('code' => $code, 'message' => $message, 'file' => $file, 'line' => $line));
        } else {
            // http://php.net/manual/en/function.debug-backtrace.php
            // As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added.
            // Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'.
            $trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS);
            array_shift($trace); // Exclude handleError from trace
            $this->lastFatalTrace = $trace;
        }

        if ($this->previousErrorHandler === true) {
            return false;
        } elseif ($this->previousErrorHandler) {
            return call_user_func($this->previousErrorHandler, $code, $message, $file, $line, $context);
        }
    }

    /**
     * @private
     */
    public function handleFatalError()
    {
        $this->reservedMemory = null;

        $lastError = error_get_last();
        if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
            $this->logger->log(
                $this->fatalLevel === null ? LogLevel::ALERT : $this->fatalLevel,
                'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
                array('code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace)
            );

            if ($this->logger instanceof Logger) {
                foreach ($this->logger->getHandlers() as $handler) {
                    if ($handler instanceof AbstractHandler) {
                        $handler->close();
                    }
                }
            }
        }
    }

    private static function codeToString($code)
    {
        switch ($code) {
            case E_ERROR:
                return 'E_ERROR';
            case E_WARNING:
                return 'E_WARNING';
            case E_PARSE:
                return 'E_PARSE';
            case E_NOTICE:
                return 'E_NOTICE';
            case E_CORE_ERROR:
                return 'E_CORE_ERROR';
            case E_CORE_WARNING:
                return 'E_CORE_WARNING';
            case E_COMPILE_ERROR:
                return 'E_COMPILE_ERROR';
            case E_COMPILE_WARNING:
                return 'E_COMPILE_WARNING';
            case E_USER_ERROR:
                return 'E_USER_ERROR';
            case E_USER_WARNING:
                return 'E_USER_WARNING';
            case E_USER_NOTICE:
                return 'E_USER_NOTICE';
            case E_STRICT:
                return 'E_STRICT';
            case E_RECOVERABLE_ERROR:
                return 'E_RECOVERABLE_ERROR';
            case E_DEPRECATED:
                return 'E_DEPRECATED';
            case E_USER_DEPRECATED:
                return 'E_USER_DEPRECATED';
        }

        return 'Unknown PHP error';
    }
}
<?php

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog;

class Utils
{
    /**
     * @internal
     */
    public static function getClass($object)
    {
        $class = \get_class($object);

        return 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
    }

    /**
     * Makes sure if a relative path is passed in it is turned into an absolute path
     *
     * @param string $streamUrl stream URL or path without protocol
     *
     * @return string
     */
    public static function canonicalizePath($streamUrl)
    {
        $prefix = '';
        if ('file://' === substr($streamUrl, 0, 7)) {
            $streamUrl = substr($streamUrl, 7);
            $prefix = 'file://';
        }

        // other type of stream, not supported
        if (false !== strpos($streamUrl, '://')) {
            return $streamUrl;
        }

        // already absolute
        if (substr($streamUrl, 0, 1) === '/' || substr($streamUrl, 1, 1) === ':' || substr($streamUrl, 0, 2) === '\\\\') {
            return $prefix.$streamUrl;
        }

        $streamUrl = getcwd() . '/' . $streamUrl;

        return $prefix.$streamUrl;
    }

    /**
     * Return the JSON representation of a value
     *
     * @param  mixed             $data
     * @param  int               $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
     * @param  bool              $ignoreErrors whether to ignore encoding errors or to throw on error, when ignored and the encoding fails, "null" is returned which is valid json for null
     * @throws \RuntimeException if encoding fails and errors are not ignored
     * @return string
     */
    public static function jsonEncode($data, $encodeFlags = null, $ignoreErrors = false)
    {
        if (null === $encodeFlags && version_compare(PHP_VERSION, '5.4.0', '>=')) {
            $encodeFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
        }

        if ($ignoreErrors) {
            $json = @json_encode($data, $encodeFlags);
            if (false === $json) {
                return 'null';
            }

            return $json;
        }

        $json = json_encode($data, $encodeFlags);
        if (false === $json) {
            $json = self::handleJsonError(json_last_error(), $data);
        }

        return $json;
    }

    /**
     * Handle a json_encode failure.
     *
     * If the failure is due to invalid string encoding, try to clean the
     * input and encode again. If the second encoding attempt fails, the
     * inital error is not encoding related or the input can't be cleaned then
     * raise a descriptive exception.
     *
     * @param  int               $code return code of json_last_error function
     * @param  mixed             $data data that was meant to be encoded
     * @param  int               $encodeFlags flags to pass to json encode, defaults to JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
     * @throws \RuntimeException if failure can't be corrected
     * @return string            JSON encoded data after error correction
     */
    public static function handleJsonError($code, $data, $encodeFlags = null)
    {
        if ($code !== JSON_ERROR_UTF8) {
            self::throwEncodeError($code, $data);
        }

        if (is_string($data)) {
            self::detectAndCleanUtf8($data);
        } elseif (is_array($data)) {
            array_walk_recursive($data, array('Monolog\Utils', 'detectAndCleanUtf8'));
        } else {
            self::throwEncodeError($code, $data);
        }

        if (null === $encodeFlags && version_compare(PHP_VERSION, '5.4.0', '>=')) {
            $encodeFlags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
        }

        $json = json_encode($data, $encodeFlags);

        if ($json === false) {
            self::throwEncodeError(json_last_error(), $data);
        }

        return $json;
    }

    /**
     * Throws an exception according to a given code with a customized message
     *
     * @param  int               $code return code of json_last_error function
     * @param  mixed             $data data that was meant to be encoded
     * @throws \RuntimeException
     */
    private static function throwEncodeError($code, $data)
    {
        switch ($code) {
            case JSON_ERROR_DEPTH:
                $msg = 'Maximum stack depth exceeded';
                break;
            case JSON_ERROR_STATE_MISMATCH:
                $msg = 'Underflow or the modes mismatch';
                break;
            case JSON_ERROR_CTRL_CHAR:
                $msg = 'Unexpected control character found';
                break;
            case JSON_ERROR_UTF8:
                $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
                break;
            default:
                $msg = 'Unknown error';
        }

        throw new \RuntimeException('JSON encoding failed: '.$msg.'. Encoding: '.var_export($data, true));
    }

    /**
     * Detect invalid UTF-8 string characters and convert to valid UTF-8.
     *
     * Valid UTF-8 input will be left unmodified, but strings containing
     * invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed
     * original encoding of ISO-8859-15. This conversion may result in
     * incorrect output if the actual encoding was not ISO-8859-15, but it
     * will be clean UTF-8 output and will not rely on expensive and fragile
     * detection algorithms.
     *
     * Function converts the input in place in the passed variable so that it
     * can be used as a callback for array_walk_recursive.
     *
     * @param mixed &$data Input to check and convert if needed
     * @private
     */
    public static function detectAndCleanUtf8(&$data)
    {
        if (is_string($data) && !preg_match('//u', $data)) {
            $data = preg_replace_callback(
                '/[\x80-\xFF]+/',
                function ($m) { return utf8_encode($m[0]); },
                $data
            );
            $data = str_replace(
                array('¤', '¦', '¨', '´', '¸', '¼', '½', '¾'),
                array('€', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'),
                $data
            );
        }
    }
}
PNG

   
IHDR         
   gAMA  a    cHRM  z&         u0  `  :  pQ<   	pHYs       iTXtXML:com.adobe.xmp     <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:tiff="http://ns.adobe.com/tiff/1.0/"
            xmlns:exif="http://ns.adobe.com/exif/1.0/">
         <tiff:YResolution>72/1</tiff:YResolution>
         <tiff:Orientation>1</tiff:Orientation>
         <tiff:ResolutionUnit>2</tiff:ResolutionUnit>
         <tiff:XResolution>72/1</tiff:XResolution>
         <exif:PixelXDimension>25</exif:PixelXDimension>
         <exif:ColorSpace>1</exif:ColorSpace>
         <exif:PixelYDimension>25</exif:PixelYDimension>
      </rdf:Description>
   </rdf:RDF>
</x:xmpmeta>
LN  IDAT8JkQq@EhL/إJ`;+_A+hbB@Pp=ߺw.x`s>{5N)ahO<svjomm^__ѡwԤOs,'NOOKt:<˚
Uc𧳨twwlV@dRTJ8Bjnnv^rZ^^EMLL(F-e---urrs^%	$ Iq
He#F{؞$! >$񆏭+
Ձ Jb	mLznT*YЧ'E<X˷
J¹9#S!Jfff,bV%	hssS.heeEccc
Rp}}]r-h{{[]XX2k92@
SlB__2~TI{oq>g612-.#yrxǗ qZZ0?THȃ{5 j)Terذh'ᡱ=<<";


i~~g][[Ȉ
aK2ΎM⢱Kq` eaoc'&Gh=G,0\fF1>0f8 D0Y|@8r	twH	D!"FW~XF Zfyyi
/E0&Bc}t    IENDB`PNG

   
IHDR         
   IDAT8ұJa_8DcK A--]]Q[(	`C-
a~/~o

=p9u-c#|,Vi 6bgxfC5}y%\No8Bq0TBP2|}S<Lۨc5h2DiqL[m
>"▷)3
r!.86-(QkyOx@LKS&    IENDB`PNG

   
IHDR         c   gAMA  |Q  iCCPICC Profile  8UoT?o\?US[IB*unS6mUoxBISA$=t@hpS]Ƹ9w>5@WI`]5;V!	A'@{N\..ƅG_!7suV$BlW=}i; F)A<.&Xax,38S(b׵*%31l#O-zQvaXOP5o6Zz&⻏^wkI/#&\%x/@{7S މjPh͔&mry>k7=ߪB#@fs_{덱п0-LZ~%Gpˈ{YXf^+_s-T>D@קƸ-9!r[2]3BcnCs?>*ԮeD|%4`:X2pQSLPRaeyqĘ
י5Fit )CdL$o$rpӶb>4+̹F_{Яki+x.+B.{L<۩
=UHcnf<>F ^e||pyv%b:iX'%8Iߔ?rw[vITVQN^dpYI"|#\cz[2M^S0[zIJ/HHȟ-
Ic<xx-8ZNxA-8mCkKHaYn1Ĝ {qHgu#L
hs :6z!y@}zgqٺ/S4~\~Y3M9PyK=.  -z$8Y7"tkHևwⳟ\87܅O$~j]n5`ffsKpY qx    cHRM  z%        u0  `  :  o_F   	pHYs       YiTXtXML:com.adobe.xmp     <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:tiff="http://ns.adobe.com/tiff/1.0/">
         <tiff:Orientation>1</tiff:Orientation>
      </rdf:Description>
   </rdf:RDF>
</x:xmpmeta>
L'Y  IDATH
ˋau6MfJ.,"5YYa?PRFX*J$I!Yeur7|9O=s3MQP_kd!n~L#{o
ڨ:`.L1}xJn+;
_!Q8tBH8uGe}P4(^?͠?6v
;'aXU(Fs#p98RPLʞ<T*ς0Q3Ɵs`fe II\!1h4P~|A$^1۠x	Aޛ	F]eNCw)6Q6@̯K]#8(#6$:bC{9TI0k_)[V
ZoF	|}p.Uw~ր/"؇`Y>7>@'27lX:zF,Yw𞟿z7Ҝ-06ڇZz=W%JN\Pyo!|XŰVBDE@p|K2\hwI }%~7	=pᯍאxͨ#G?/F-/4F6@%ka}(O2X_uY&17ga?\%HCڡ
Fw@G!e;_5֊J΢潚)Lc8yk>    IENDB`PNG

   
IHDR         
   sBIT|d   	pHYs        IDAT81@E
(ChbǱ{X[YHa"XPd3
?;C`'ncl0*ql2w7 n43< 	sEnt'+ΰt=`	&CUpKdj h<k>H+    IENDB`PNG

   
IHDR         
   IDAT8ԱJC1պXpDQ\a*]&.NZ:x?|9ɁT2Hh=6
fǘa3
ਰ+c+v2I'y=Y2|u-Klٴ1ES~
{
c)1g!0Ք˔ئ)1oZTX5Y7uSHPz54K'nJ    IENDB`PNG

   
IHDR         
  4IDAT8O+Q&n3XaAB^ڊRl	o $k˱wdoz9=Cik ;U#ithG<	n	\ib1&5R%؁3HN)ioQh@m\lKA0c/Ql\hoYtP{I,BN0&>seJ	0&^MZGETXAuB'ˌc!a	;r23(*lO<]X¾ 07    IENDB`# xcloner-core

## Installation

Local composer.json should contain this custom repository for now

```
"repositories": [
        {
            "type": "vcs",
            "packagist": false,
            "url":  "https://github.com/watchfulli/xcloner-core.git"
        }
    ]
 ```

then 

```composer require watchfulli/xcloner-core```

## Usage

Trigger standalone backup with profile stored in standalone_backup_trigger_config.json

```
<?php
define('WP_DEBUG', true);
define('WP_DEBUG_DISPLAY', true);

require_once('../vendor/autoload.php');

$profile = [
    'id' => 0
];

//loading the default xcloner settings in format [{'option_name':'value', {'option_value': 'value'}}]
$json_config = json_decode(file_get_contents(__DIR__ . '/standalone_backup_trigger_config.json'));

if (!$json_config) {
    die('Could not parse default JSON config, i will shutdown for now...');
}

//pass json config to Xcloner_Standalone lib
$xcloner_backup = new watchfulli\XClonerCore\Xcloner_Standalone($json_config);

$xcloner_backup->start($profile['id']);

```


### standalone_backup_trigger_config.json

```
[
{"option_name":"xcloner_cleanup_retention_limit_archives","option_value":"100"},
{"option_name":"xcloner_cleanup_retention_limit_days","option_value":"60"},
{"option_name":"xcloner_cleanup_settings_page","option_value":""},
{"option_name":"xcloner_cron_settings_page","option_value":""},
{"option_name":"xcloner_database_records_per_request","option_value":"1000"},
{"option_name":"xcloner_db_version","option_value":"1.1.7"},
{"option_name":"xcloner_directories_to_scan_per_request","option_value":"1000"},
{"option_name":"xcloner_disable_email_notification","option_value":"1"},
{"option_name":"xcloner_enable_log","option_value":"1"},
{"option_name":"xcloner_enable_mysql_backup","option_value":"1"},
{"option_name":"xcloner_enable_pre_update_backup","option_value":""},
{"option_name":"xcloner_encryption_key","option_value":"FMo5vh64rNCrTo8zcmTsrzV88nnHj6BGOAK"},
{"option_name":"xcloner_standalone_api_key","option_value":""},
{"option_name":"xcloner_exclude_files_larger_than_mb","option_value":"0"},
{"option_name":"xcloner_files_to_process_per_request","option_value":"328"},
{"option_name":"xcloner_force_tmp_path_site_root","option_value":"1"},
{"option_name":"xcloner_mysql_database","option_value":"wordpress"},
{"option_name":"xcloner_mysql_hostname","option_value":"localhost"},
{"option_name":"xcloner_mysql_password","option_value":"root"},
{"option_name":"xcloner_mysql_prefix","option_value":"wp_"},
{"option_name":"xcloner_mysql_settings_page","option_value":""},
{"option_name":"xcloner_mysql_username","option_value":"root"},
{"option_name":"xcloner_size_limit_per_request","option_value":"50"},
{"option_name":"xcloner_split_backup_limit","option_value":"2048"},
{"option_name":"xcloner_start_path","option_value":"\/Applications\/MAMP\/htdocs\/wordpress/"},
{"option_name":"xcloner_store_path","option_value":"\/Applications\/MAMP\/htdocs\/wordpress\/wp-content\/backups\/"},
{"option_name":"xcloner_system_settings_page","option_value":"100"},
{"option_name":"xcloner_text","option_value":"0"},
{"option_name":"profile", "option_value":{
  "extra": [],
  "backup_params": {
    "backup_name": "backup_[domain]_8888-[time]-sql",
    "email_notification": "info@noreply.com",
    "diff_start_date": "",
    "schedule_name": "test2",
    "backup_encrypt": false,
    "start_at": false,
    "schedule_frequency": "daily",
    "schedule_storage": ""
  },
  "database": {
    "#": [
      "wordpress",
      "joomla"
    ]
  },
  "excluded_files": [
    "wp-content",
    "wp-includes"
  ]
}}
]
```
<?php
/**
 * WordPress DB Class
 *
 * Original code from {@link http://php.justinvincent.com Justin Vincent (justin@visunet.ie)}
 *
 * @package WordPress
 * @subpackage Database
 * @since 0.71
 */

/**
 * @since 0.71
 */
define( 'EZSQL_VERSION', 'WP1.25' );

/**
 * @since 0.71
 */
define( 'OBJECT', 'OBJECT' );
// phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ConstantNotUpperCase
define( 'object', 'OBJECT' ); // Back compat.

/**
 * @since 2.5.0
 */
define( 'OBJECT_K', 'OBJECT_K' );

/**
 * @since 0.71
 */
define( 'ARRAY_A', 'ARRAY_A' );

/**
 * @since 0.71
 */
define( 'ARRAY_N', 'ARRAY_N' );

/**
 * WordPress Database Access Abstraction Object
 *
 * It is possible to replace this class with your own
 * by setting the $wpdb global variable in wp-content/db.php
 * file to your class. The wpdb class will still be included,
 * so you can extend it or simply use your own.
 *
 * @link https://codex.wordpress.org/Function_Reference/wpdb_Class
 *
 * @since 0.71
 */
class wpdb {

	/**
	 * Whether to show SQL/DB errors.
	 *
	 * Default behavior is to show errors if both WP_DEBUG and WP_DEBUG_DISPLAY
	 * evaluated to true.
	 *
	 * @since 0.71
	 * @var bool
	 */
	var $show_errors = false;

	/**
	 * Whether to suppress errors during the DB bootstrapping.
	 *
	 * @since 2.5.0
	 * @var bool
	 */
	var $suppress_errors = false;

	/**
	 * The last error during query.
	 *
	 * @since 2.5.0
	 * @var string
	 */
	public $last_error = '';

	/**
	 * Amount of queries made
	 *
	 * @since 1.2.0
	 * @var int
	 */
	public $num_queries = 0;

	/**
	 * Count of rows returned by previous query
	 *
	 * @since 0.71
	 * @var int
	 */
	public $num_rows = 0;

	/**
	 * Count of affected rows by previous query
	 *
	 * @since 0.71
	 * @var int
	 */
	var $rows_affected = 0;

	/**
	 * The ID generated for an AUTO_INCREMENT column by the previous query (usually INSERT).
	 *
	 * @since 0.71
	 * @var int
	 */
	public $insert_id = 0;

	/**
	 * Last query made
	 *
	 * @since 0.71
	 * @var array
	 */
	var $last_query;

	/**
	 * Results of the last query made
	 *
	 * @since 0.71
	 * @var array|null
	 */
	var $last_result;

	/**
	 * MySQL result, which is either a resource or boolean.
	 *
	 * @since 0.71
	 * @var mixed
	 */
	protected $result;

	/**
	 * Cached column info, for sanity checking data before inserting
	 *
	 * @since 4.2.0
	 * @var array
	 */
	protected $col_meta = array();

	/**
	 * Calculated character sets on tables
	 *
	 * @since 4.2.0
	 * @var array
	 */
	protected $table_charset = array();

	/**
	 * Whether text fields in the current query need to be sanity checked.
	 *
	 * @since 4.2.0
	 * @var bool
	 */
	protected $check_current_query = true;

	/**
	 * Flag to ensure we don't run into recursion problems when checking the collation.
	 *
	 * @since 4.2.0
	 * @see wpdb::check_safe_collation()
	 * @var bool
	 */
	private $checking_collation = false;

	/**
	 * Saved info on the table column
	 *
	 * @since 0.71
	 * @var array
	 */
	protected $col_info;

	/**
	 * Log of queries that were executed, for debugging purposes.
	 *
	 * @since 1.5.0
	 * @since 2.5.0 The third element in each query log was added to record the calling functions.
	 * @since 5.1.0 The fourth element in each query log was added to record the start time.
	 *
	 * @var array[] {
	 *     Array of queries that were executed.
	 *
	 *     @type array ...$0 {
	 *         Data for each query.
	 *
	 *         @type string $0 The query's SQL.
	 *         @type float  $1 Total time spent on the query, in seconds.
	 *         @type string $2 Comma separated list of the calling functions.
	 *         @type float  $3 Unix timestamp of the time at the start of the query.
	 *     }
	 * }
	 */
	var $queries;

	/**
	 * The number of times to retry reconnecting before dying.
	 *
	 * @since 3.9.0
	 * @see wpdb::check_connection()
	 * @var int
	 */
	protected $reconnect_retries = 5;

	/**
	 * WordPress table prefix
	 *
	 * You can set this to have multiple WordPress installations
	 * in a single database. The second reason is for possible
	 * security precautions.
	 *
	 * @since 2.5.0
	 * @var string
	 */
	public $prefix = '';

	/**
	 * WordPress base table prefix.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $base_prefix;

	/**
	 * Whether the database queries are ready to start executing.
	 *
	 * @since 2.3.2
	 * @var bool
	 */
	var $ready = false;

	/**
	 * Blog ID.
	 *
	 * @since 3.0.0
	 * @var int
	 */
	public $blogid = 0;

	/**
	 * Site ID.
	 *
	 * @since 3.0.0
	 * @var int
	 */
	public $siteid = 0;

	/**
	 * List of WordPress per-blog tables
	 *
	 * @since 2.5.0
	 * @see wpdb::tables()
	 * @var array
	 */
	var $tables = array(
		'posts',
		'comments',
		'links',
		'options',
		'postmeta',
		'terms',
		'term_taxonomy',
		'term_relationships',
		'termmeta',
		'commentmeta',
	);

	/**
	 * List of deprecated WordPress tables
	 *
	 * categories, post2cat, and link2cat were deprecated in 2.3.0, db version 5539
	 *
	 * @since 2.9.0
	 * @see wpdb::tables()
	 * @var array
	 */
	var $old_tables = array( 'categories', 'post2cat', 'link2cat' );

	/**
	 * List of WordPress global tables
	 *
	 * @since 3.0.0
	 * @see wpdb::tables()
	 * @var array
	 */
	var $global_tables = array( 'users', 'usermeta' );

	/**
	 * List of Multisite global tables
	 *
	 * @since 3.0.0
	 * @see wpdb::tables()
	 * @var array
	 */
	var $ms_global_tables = array(
		'blogs',
		'blogmeta',
		'signups',
		'site',
		'sitemeta',
		'sitecategories',
		'registration_log',
		'blog_versions',
	);

	/**
	 * WordPress Comments table
	 *
	 * @since 1.5.0
	 * @var string
	 */
	public $comments;

	/**
	 * WordPress Comment Metadata table
	 *
	 * @since 2.9.0
	 * @var string
	 */
	public $commentmeta;

	/**
	 * WordPress Links table
	 *
	 * @since 1.5.0
	 * @var string
	 */
	public $links;

	/**
	 * WordPress Options table
	 *
	 * @since 1.5.0
	 * @var string
	 */
	public $options;

	/**
	 * WordPress Post Metadata table
	 *
	 * @since 1.5.0
	 * @var string
	 */
	public $postmeta;

	/**
	 * WordPress Posts table
	 *
	 * @since 1.5.0
	 * @var string
	 */
	public $posts;

	/**
	 * WordPress Terms table
	 *
	 * @since 2.3.0
	 * @var string
	 */
	public $terms;

	/**
	 * WordPress Term Relationships table
	 *
	 * @since 2.3.0
	 * @var string
	 */
	public $term_relationships;

	/**
	 * WordPress Term Taxonomy table
	 *
	 * @since 2.3.0
	 * @var string
	 */
	public $term_taxonomy;

	/**
	 * WordPress Term Meta table.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	public $termmeta;

	//
	// Global and Multisite tables
	//

	/**
	 * WordPress User Metadata table
	 *
	 * @since 2.3.0
	 * @var string
	 */
	public $usermeta;

	/**
	 * WordPress Users table
	 *
	 * @since 1.5.0
	 * @var string
	 */
	public $users;

	/**
	 * Multisite Blogs table
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $blogs;

	/**
	 * Multisite Blog Metadata table
	 *
	 * @since 5.1.0
	 * @var string
	 */
	public $blogmeta;

	/**
	 * Multisite Blog Versions table
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $blog_versions;

	/**
	 * Multisite Registration Log table
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $registration_log;

	/**
	 * Multisite Signups table
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $signups;

	/**
	 * Multisite Sites table
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $site;

	/**
	 * Multisite Sitewide Terms table
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $sitecategories;

	/**
	 * Multisite Site Metadata table
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $sitemeta;

	/**
	 * Format specifiers for DB columns. Columns not listed here default to %s. Initialized during WP load.
	 *
	 * Keys are column names, values are format types: 'ID' => '%d'
	 *
	 * @since 2.8.0
	 * @see wpdb::prepare()
	 * @see wpdb::insert()
	 * @see wpdb::update()
	 * @see wpdb::delete()
	 * @see wp_set_wpdb_vars()
	 * @var array
	 */
	public $field_types = array();

	/**
	 * Database table columns charset
	 *
	 * @since 2.2.0
	 * @var string
	 */
	public $charset;

	/**
	 * Database table columns collate
	 *
	 * @since 2.2.0
	 * @var string
	 */
	public $collate;

	/**
	 * Database Username
	 *
	 * @since 2.9.0
	 * @var string
	 */
	protected $dbuser;

	/**
	 * Database Password
	 *
	 * @since 3.1.0
	 * @var string
	 */
	protected $dbpassword;

	/**
	 * Database Name
	 *
	 * @since 3.1.0
	 * @var string
	 */
	protected $dbname;

	/**
	 * Database Host
	 *
	 * @since 3.1.0
	 * @var string
	 */
	protected $dbhost;

	/**
	 * Database Handle
	 *
	 * @since 0.71
	 * @var string
	 */
	protected $dbh;

	/**
	 * A textual description of the last query/get_row/get_var call
	 *
	 * @since 3.0.0
	 * @var string
	 */
	public $func_call;

	/**
	 * Whether MySQL is used as the database engine.
	 *
	 * Set in WPDB::db_connect() to true, by default. This is used when checking
	 * against the required MySQL version for WordPress. Normally, a replacement
	 * database drop-in (db.php) will skip these checks, but setting this to true
	 * will force the checks to occur.
	 *
	 * @since 3.3.0
	 * @var bool
	 */
	public $is_mysql = null;

	/**
	 * A list of incompatible SQL modes.
	 *
	 * @since 3.9.0
	 * @var array
	 */
	protected $incompatible_modes = array(
		'NO_ZERO_DATE',
		'ONLY_FULL_GROUP_BY',
		'STRICT_TRANS_TABLES',
		'STRICT_ALL_TABLES',
		'TRADITIONAL',
	);

	/**
	 * Whether to use mysqli over mysql.
	 *
	 * @since 3.9.0
	 * @var bool
	 */
	private $use_mysqli = false;

	/**
	 * Whether we've managed to successfully connect at some point
	 *
	 * @since 3.9.0
	 * @var bool
	 */
	private $has_connected = false;

	/**
	 * Connects to the database server and selects a database
	 *
	 * PHP5 style constructor for compatibility with PHP5. Does
	 * the actual setting up of the class properties and connection
	 * to the database.
	 *
	 * @link https://core.trac.wordpress.org/ticket/3354
	 * @since 2.0.8
	 *
	 * @global string $wp_version
	 *
	 * @param string $dbuser     MySQL database user
	 * @param string $dbpassword MySQL database password
	 * @param string $dbname     MySQL database name
	 * @param string $dbhost     MySQL database host
	 */
	public function __construct( $dbuser, $dbpassword, $dbname, $dbhost ) {
		register_shutdown_function( array( $this, '__destruct' ) );

		if ( WP_DEBUG && WP_DEBUG_DISPLAY ) {
			$this->show_errors();
		}

		// Use ext/mysqli if it exists unless WP_USE_EXT_MYSQL is defined as true
		if ( function_exists( 'mysqli_connect' ) ) {
			$this->use_mysqli = true;

			if ( defined( 'WP_USE_EXT_MYSQL' ) ) {
				$this->use_mysqli = ! WP_USE_EXT_MYSQL;
			}
		}

		$this->dbuser     = $dbuser;
		$this->dbpassword = $dbpassword;
		$this->dbname     = $dbname;
		$this->dbhost     = $dbhost;

		// wp-config.php creation will manually connect when ready.
		if ( defined( 'WP_SETUP_CONFIG' ) ) {
			return;
		}

		$this->db_connect();
	}

	/**
	 * PHP5 style destructor and will run when database object is destroyed.
	 *
	 * @see wpdb::__construct()
	 * @since 2.0.8
	 * @return true
	 */
	public function __destruct() {
		return true;
	}

	/**
	 * Makes private properties readable for backward compatibility.
	 *
	 * @since 3.5.0
	 *
	 * @param string $name The private member to get, and optionally process
	 * @return mixed The private member
	 */
	public function __get( $name ) {
		if ( 'col_info' === $name ) {
			$this->load_col_info();
		}

		return $this->$name;
	}

	/**
	 * Makes private properties settable for backward compatibility.
	 *
	 * @since 3.5.0
	 *
	 * @param string $name  The private member to set
	 * @param mixed  $value The value to set
	 */
	public function __set( $name, $value ) {
		$protected_members = array(
			'col_meta',
			'table_charset',
			'check_current_query',
		);
		if ( in_array( $name, $protected_members, true ) ) {
			return;
		}
		$this->$name = $value;
	}

	/**
	 * Makes private properties check-able for backward compatibility.
	 *
	 * @since 3.5.0
	 *
	 * @param string $name  The private member to check
	 *
	 * @return bool If the member is set or not
	 */
	public function __isset( $name ) {
		return isset( $this->$name );
	}

	/**
	 * Makes private properties un-settable for backward compatibility.
	 *
	 * @since 3.5.0
	 *
	 * @param string $name  The private member to unset
	 */
	public function __unset( $name ) {
		unset( $this->$name );
	}

	/**
	 * Set $this->charset and $this->collate
	 *
	 * @since 3.1.0
	 */
	public function init_charset() {
		$charset = '';
		$collate = '';

		if ( function_exists( 'is_multisite' ) && is_multisite() ) {
			$charset = 'utf8';
			if ( defined( 'DB_COLLATE' ) && DB_COLLATE ) {
				$collate = DB_COLLATE;
			} else {
				$collate = 'utf8_general_ci';
			}
		} elseif ( defined( 'DB_COLLATE' ) ) {
			$collate = DB_COLLATE;
		}

		if ( defined( 'DB_CHARSET' ) ) {
			$charset = DB_CHARSET;
		}

		$charset_collate = $this->determine_charset( $charset, $collate );

		$this->charset = $charset_collate['charset'];
		$this->collate = $charset_collate['collate'];
	}

	/**
	 * Determines the best charset and collation to use given a charset and collation.
	 *
	 * For example, when able, utf8mb4 should be used instead of utf8.
	 *
	 * @since 4.6.0
	 *
	 * @param string $charset The character set to check.
	 * @param string $collate The collation to check.
	 * @return array The most appropriate character set and collation to use.
	 */
	public function determine_charset( $charset, $collate ) {
		if ( ( $this->use_mysqli && ! ( $this->dbh instanceof mysqli ) ) || empty( $this->dbh ) ) {
			return compact( 'charset', 'collate' );
		}

		if ( 'utf8' === $charset && $this->has_cap( 'utf8mb4' ) ) {
			$charset = 'utf8mb4';
		}

		if ( 'utf8mb4' === $charset && ! $this->has_cap( 'utf8mb4' ) ) {
			$charset = 'utf8';
			$collate = str_replace( 'utf8mb4_', 'utf8_', $collate );
		}

		if ( 'utf8mb4' === $charset ) {
			// _general_ is outdated, so we can upgrade it to _unicode_, instead.
			if ( ! $collate || 'utf8_general_ci' === $collate ) {
				$collate = 'utf8mb4_unicode_ci';
			} else {
				$collate = str_replace( 'utf8_', 'utf8mb4_', $collate );
			}
		}

		// _unicode_520_ is a better collation, we should use that when it's available.
		if ( $this->has_cap( 'utf8mb4_520' ) && 'utf8mb4_unicode_ci' === $collate ) {
			$collate = 'utf8mb4_unicode_520_ci';
		}

		return compact( 'charset', 'collate' );
	}

	/**
	 * Sets the connection's character set.
	 *
	 * @since 3.1.0
	 *
	 * @param resource $dbh     The resource given by mysql_connect
	 * @param string   $charset Optional. The character set. Default null.
	 * @param string   $collate Optional. The collation. Default null.
	 */
	public function set_charset( $dbh, $charset = null, $collate = null ) {
		if ( ! isset( $charset ) ) {
			$charset = $this->charset;
		}
		if ( ! isset( $collate ) ) {
			$collate = $this->collate;
		}
		if ( $this->has_cap( 'collation' ) && ! empty( $charset ) ) {
			$set_charset_succeeded = true;

			if ( $this->use_mysqli ) {
				if ( function_exists( 'mysqli_set_charset' ) && $this->has_cap( 'set_charset' ) ) {
					$set_charset_succeeded = mysqli_set_charset( $dbh, $charset );
				}

				if ( $set_charset_succeeded ) {
					$query = $this->prepare( 'SET NAMES %s', $charset );
					if ( ! empty( $collate ) ) {
						$query .= $this->prepare( ' COLLATE %s', $collate );
					}
					mysqli_query( $dbh, $query );
				}
			} else {
				if ( function_exists( 'mysql_set_charset' ) && $this->has_cap( 'set_charset' ) ) {
					$set_charset_succeeded = mysql_set_charset( $charset, $dbh );
				}
				if ( $set_charset_succeeded ) {
					$query = $this->prepare( 'SET NAMES %s', $charset );
					if ( ! empty( $collate ) ) {
						$query .= $this->prepare( ' COLLATE %s', $collate );
					}
					mysql_query( $query, $dbh );
				}
			}
		}
	}

	/**
	 * Change the current SQL mode, and ensure its WordPress compatibility.
	 *
	 * If no modes are passed, it will ensure the current MySQL server
	 * modes are compatible.
	 *
	 * @since 3.9.0
	 *
	 * @param array $modes Optional. A list of SQL modes to set.
	 */
	public function set_sql_mode( $modes = array() ) {
		if ( empty( $modes ) ) {
			if ( $this->use_mysqli ) {
				$res = mysqli_query( $this->dbh, 'SELECT @@SESSION.sql_mode' );
			} else {
				$res = mysql_query( 'SELECT @@SESSION.sql_mode', $this->dbh );
			}

			if ( empty( $res ) ) {
				return;
			}

			if ( $this->use_mysqli ) {
				$modes_array = mysqli_fetch_array( $res );
				if ( empty( $modes_array[0] ) ) {
					return;
				}
				$modes_str = $modes_array[0];
			} else {
				$modes_str = mysql_result( $res, 0 );
			}

			if ( empty( $modes_str ) ) {
				return;
			}

			$modes = explode( ',', $modes_str );
		}

		$modes = array_change_key_case( $modes, CASE_UPPER );

		/**
		 * Filters the list of incompatible SQL modes to exclude.
		 *
		 * @since 3.9.0
		 *
		 * @param array $incompatible_modes An array of incompatible modes.
		 */
		$incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes );

		foreach ( $modes as $i => $mode ) {
			if ( in_array( $mode, $incompatible_modes ) ) {
				unset( $modes[ $i ] );
			}
		}

		$modes_str = implode( ',', $modes );

		if ( $this->use_mysqli ) {
			mysqli_query( $this->dbh, "SET SESSION sql_mode='$modes_str'" );
		} else {
			mysql_query( "SET SESSION sql_mode='$modes_str'", $this->dbh );
		}
	}

	/**
	 * Sets the table prefix for the WordPress tables.
	 *
	 * @since 2.5.0
	 *
	 * @param string $prefix          Alphanumeric name for the new prefix.
	 * @param bool   $set_table_names Optional. Whether the table names, e.g. wpdb::$posts, should be updated or not.
	 * @return string|WP_Error Old prefix or WP_Error on error
	 */
	public function set_prefix( $prefix, $set_table_names = true ) {

		if ( preg_match( '|[^a-z0-9_]|i', $prefix ) ) {
			return new WP_Error( 'invalid_db_prefix', 'Invalid database prefix' );
		}

		$old_prefix = is_multisite() ? '' : $prefix;

		if ( isset( $this->base_prefix ) ) {
			$old_prefix = $this->base_prefix;
		}

		$this->base_prefix = $prefix;

		if ( $set_table_names ) {
			foreach ( $this->tables( 'global' ) as $table => $prefixed_table ) {
				$this->$table = $prefixed_table;
			}

			if ( is_multisite() && empty( $this->blogid ) ) {
				return $old_prefix;
			}

			$this->prefix = $this->get_blog_prefix();

			foreach ( $this->tables( 'blog' ) as $table => $prefixed_table ) {
				$this->$table = $prefixed_table;
			}

			foreach ( $this->tables( 'old' ) as $table => $prefixed_table ) {
				$this->$table = $prefixed_table;
			}
		}
		return $old_prefix;
	}

	/**
	 * Sets blog id.
	 *
	 * @since 3.0.0
	 *
	 * @param int $blog_id
	 * @param int $network_id Optional.
	 * @return int previous blog id
	 */
	public function set_blog_id( $blog_id, $network_id = 0 ) {
		if ( ! empty( $network_id ) ) {
			$this->siteid = $network_id;
		}

		$old_blog_id  = $this->blogid;
		$this->blogid = $blog_id;

		$this->prefix = $this->get_blog_prefix();

		foreach ( $this->tables( 'blog' ) as $table => $prefixed_table ) {
			$this->$table = $prefixed_table;
		}

		foreach ( $this->tables( 'old' ) as $table => $prefixed_table ) {
			$this->$table = $prefixed_table;
		}

		return $old_blog_id;
	}

	/**
	 * Gets blog prefix.
	 *
	 * @since 3.0.0
	 * @param int $blog_id Optional.
	 * @return string Blog prefix.
	 */
	public function get_blog_prefix( $blog_id = null ) {
		if ( is_multisite() ) {
			if ( null === $blog_id ) {
				$blog_id = $this->blogid;
			}
			$blog_id = (int) $blog_id;
			if ( defined( 'MULTISITE' ) && ( 0 == $blog_id || 1 == $blog_id ) ) {
				return $this->base_prefix;
			} else {
				return $this->base_prefix . $blog_id . '_';
			}
		} else {
			return $this->base_prefix;
		}
	}

	/**
	 * Returns an array of WordPress tables.
	 *
	 * Also allows for the CUSTOM_USER_TABLE and CUSTOM_USER_META_TABLE to
	 * override the WordPress users and usermeta tables that would otherwise
	 * be determined by the prefix.
	 *
	 * The scope argument can take one of the following:
	 *
	 * 'all' - returns 'all' and 'global' tables. No old tables are returned.
	 * 'blog' - returns the blog-level tables for the queried blog.
	 * 'global' - returns the global tables for the installation, returning multisite tables only if running multisite.
	 * 'ms_global' - returns the multisite global tables, regardless if current installation is multisite.
	 * 'old' - returns tables which are deprecated.
	 *
	 * @since 3.0.0
	 * @uses wpdb::$tables
	 * @uses wpdb::$old_tables
	 * @uses wpdb::$global_tables
	 * @uses wpdb::$ms_global_tables
	 *
	 * @param string $scope   Optional. Can be all, global, ms_global, blog, or old tables. Defaults to all.
	 * @param bool   $prefix  Optional. Whether to include table prefixes. Default true. If blog
	 *                        prefix is requested, then the custom users and usermeta tables will be mapped.
	 * @param int    $blog_id Optional. The blog_id to prefix. Defaults to wpdb::$blogid. Used only when prefix is requested.
	 * @return array Table names. When a prefix is requested, the key is the unprefixed table name.
	 */
	public function tables( $scope = 'all', $prefix = true, $blog_id = 0 ) {
		switch ( $scope ) {
			case 'all':
				$tables = array_merge( $this->global_tables, $this->tables );
				if ( is_multisite() ) {
					$tables = array_merge( $tables, $this->ms_global_tables );
				}
				break;
			case 'blog':
				$tables = $this->tables;
				break;
			case 'global':
				$tables = $this->global_tables;
				if ( is_multisite() ) {
					$tables = array_merge( $tables, $this->ms_global_tables );
				}
				break;
			case 'ms_global':
				$tables = $this->ms_global_tables;
				break;
			case 'old':
				$tables = $this->old_tables;
				break;
			default:
				return array();
		}

		if ( $prefix ) {
			if ( ! $blog_id ) {
				$blog_id = $this->blogid;
			}
			$blog_prefix   = $this->get_blog_prefix( $blog_id );
			$base_prefix   = $this->base_prefix;
			$global_tables = array_merge( $this->global_tables, $this->ms_global_tables );
			foreach ( $tables as $k => $table ) {
				if ( in_array( $table, $global_tables ) ) {
					$tables[ $table ] = $base_prefix . $table;
				} else {
					$tables[ $table ] = $blog_prefix . $table;
				}
				unset( $tables[ $k ] );
			}

			if ( isset( $tables['users'] ) && defined( 'CUSTOM_USER_TABLE' ) ) {
				$tables['users'] = CUSTOM_USER_TABLE;
			}

			if ( isset( $tables['usermeta'] ) && defined( 'CUSTOM_USER_META_TABLE' ) ) {
				$tables['usermeta'] = CUSTOM_USER_META_TABLE;
			}
		}

		return $tables;
	}

	/**
	 * Selects a database using the current database connection.
	 *
	 * The database name will be changed based on the current database
	 * connection. On failure, the execution will bail and display an DB error.
	 *
	 * @since 0.71
	 *
	 * @param string        $db  MySQL database name
	 * @param resource|null $dbh Optional link identifier.
	 */
	public function select( $db, $dbh = null ) {
		if ( is_null( $dbh ) ) {
			$dbh = $this->dbh;
		}

		if ( $this->use_mysqli ) {
			$success = mysqli_select_db( $dbh, $db );
		} else {
			$success = mysql_select_db( $db, $dbh );
		}
		if ( ! $success ) {
			$this->ready = false;
			if ( ! did_action( 'template_redirect' ) ) {
				wp_load_translations_early();

				$message = '<h1>' . __( 'Can&#8217;t select database' ) . "</h1>\n";

				$message .= '<p>' . sprintf(
					/* translators: %s: database name */
					__( 'We were able to connect to the database server (which means your username and password is okay) but not able to select the %s database.' ),
					'<code>' . htmlspecialchars( $db, ENT_QUOTES ) . '</code>'
				) . "</p>\n";

				$message .= "<ul>\n";
				$message .= '<li>' . __( 'Are you sure it exists?' ) . "</li>\n";

				$message .= '<li>' . sprintf(
					/* translators: 1: database user, 2: database name */
					__( 'Does the user %1$s have permission to use the %2$s database?' ),
					'<code>' . htmlspecialchars( $this->dbuser, ENT_QUOTES ) . '</code>',
					'<code>' . htmlspecialchars( $db, ENT_QUOTES ) . '</code>'
				) . "</li>\n";

				$message .= '<li>' . sprintf(
					/* translators: %s: database name */
					__( 'On some systems the name of your database is prefixed with your username, so it would be like <code>username_%1$s</code>. Could that be the problem?' ),
					htmlspecialchars( $db, ENT_QUOTES )
				) . "</li>\n";

				$message .= "</ul>\n";

				$message .= '<p>' . sprintf(
					/* translators: %s: support forums URL */
					__( 'If you don&#8217;t know how to set up a database you should <strong>contact your host</strong>. If all else fails you may find help at the <a href="%s">WordPress Support Forums</a>.' ),
					__( 'https://wordpress.org/support/forums/' )
				) . "</p>\n";

				$this->bail( $message, 'db_select_fail' );
			}
		}
	}

	/**
	 * Do not use, deprecated.
	 *
	 * Use esc_sql() or wpdb::prepare() instead.
	 *
	 * @since 2.8.0
	 * @deprecated 3.6.0 Use wpdb::prepare()
	 * @see wpdb::prepare
	 * @see esc_sql()
	 *
	 * @param string $string
	 * @return string
	 */
	function _weak_escape( $string ) {
		if ( func_num_args() === 1 && function_exists( '_deprecated_function' ) ) {
			_deprecated_function( __METHOD__, '3.6.0', 'wpdb::prepare() or esc_sql()' );
		}
		return addslashes( $string );
	}

	/**
	 * Real escape, using mysqli_real_escape_string() or mysql_real_escape_string()
	 *
	 * @see mysqli_real_escape_string()
	 * @see mysql_real_escape_string()
	 * @since 2.8.0
	 *
	 * @param  string $string to escape
	 * @return string escaped
	 */
	function _real_escape( $string ) {
		if ( $this->dbh ) {
			if ( $this->use_mysqli ) {
				$escaped = mysqli_real_escape_string( $this->dbh, $string );
			} else {
				$escaped = mysql_real_escape_string( $string, $this->dbh );
			}
		} else {
			$class = get_class( $this );
			if ( function_exists( '__' ) ) {
				/* translators: %s: database access abstraction class, usually wpdb or a class extending wpdb */
				_doing_it_wrong( $class, sprintf( __( '%s must set a database connection for use with escaping.' ), $class ), '3.6.0' );
			} else {
				_doing_it_wrong( $class, sprintf( '%s must set a database connection for use with escaping.', $class ), '3.6.0' );
			}
			$escaped = addslashes( $string );
		}

		return $this->add_placeholder_escape( $escaped );
	}

	/**
	 * Escape data. Works on arrays.
	 *
	 * @uses wpdb::_real_escape()
	 * @since  2.8.0
	 *
	 * @param  string|array $data
	 * @return string|array escaped
	 */
	public function _escape( $data ) {
		if ( is_array( $data ) ) {
			foreach ( $data as $k => $v ) {
				if ( is_array( $v ) ) {
					$data[ $k ] = $this->_escape( $v );
				} else {
					$data[ $k ] = $this->_real_escape( $v );
				}
			}
		} else {
			$data = $this->_real_escape( $data );
		}

		return $data;
	}

	/**
	 * Do not use, deprecated.
	 *
	 * Use esc_sql() or wpdb::prepare() instead.
	 *
	 * @since 0.71
	 * @deprecated 3.6.0 Use wpdb::prepare()
	 * @see wpdb::prepare()
	 * @see esc_sql()
	 *
	 * @param mixed $data
	 * @return mixed
	 */
	public function escape( $data ) {
		if ( func_num_args() === 1 && function_exists( '_deprecated_function' ) ) {
			_deprecated_function( __METHOD__, '3.6.0', 'wpdb::prepare() or esc_sql()' );
		}
		if ( is_array( $data ) ) {
			foreach ( $data as $k => $v ) {
				if ( is_array( $v ) ) {
					$data[ $k ] = $this->escape( $v, 'recursive' );
				} else {
					$data[ $k ] = $this->_weak_escape( $v, 'internal' );
				}
			}
		} else {
			$data = $this->_weak_escape( $data, 'internal' );
		}

		return $data;
	}

	/**
	 * Escapes content by reference for insertion into the database, for security
	 *
	 * @uses wpdb::_real_escape()
	 *
	 * @since 2.3.0
	 *
	 * @param string $string to escape
	 */
	public function escape_by_ref( &$string ) {
		if ( ! is_float( $string ) ) {
			$string = $this->_real_escape( $string );
		}
	}

	/**
	 * Prepares a SQL query for safe execution. Uses sprintf()-like syntax.
	 *
	 * The following placeholders can be used in the query string:
	 *   %d (integer)
	 *   %f (float)
	 *   %s (string)
	 *
	 * All placeholders MUST be left unquoted in the query string. A corresponding argument MUST be passed for each placeholder.
	 *
	 * For compatibility with old behavior, numbered or formatted string placeholders (eg, %1$s, %5s) will not have quotes
	 * added by this function, so should be passed with appropriate quotes around them for your usage.
	 *
	 * Literal percentage signs (%) in the query string must be written as %%. Percentage wildcards (for example,
	 * to use in LIKE syntax) must be passed via a substitution argument containing the complete LIKE string, these
	 * cannot be inserted directly in the query string. Also see wpdb::esc_like().
	 *
	 * Arguments may be passed as individual arguments to the method, or as a single array containing all arguments. A combination
	 * of the two is not supported.
	 *
	 * Examples:
	 *     $wpdb->prepare( "SELECT * FROM `table` WHERE `column` = %s AND `field` = %d OR `other_field` LIKE %s", array( 'foo', 1337, '%bar' ) );
	 *     $wpdb->prepare( "SELECT DATE_FORMAT(`field`, '%%c') FROM `table` WHERE `column` = %s", 'foo' );
	 *
	 * @link https://secure.php.net/sprintf Description of syntax.
	 * @since 2.3.0
	 *
	 * @param string      $query    Query statement with sprintf()-like placeholders
	 * @param array|mixed $args     The array of variables to substitute into the query's placeholders if being called with an array of arguments,
	 *                              or the first variable to substitute into the query's placeholders if being called with individual arguments.
	 * @param mixed       $args,... further variables to substitute into the query's placeholders if being called wih individual arguments.
	 * @return string|void Sanitized query string, if there is a query to prepare.
	 */
	public function prepare( $query, $args ) {
		if ( is_null( $query ) ) {
			return;
		}

		// This is not meant to be foolproof -- but it will catch obviously incorrect usage.
		if ( strpos( $query, '%' ) === false ) {
			wp_load_translations_early();
			_doing_it_wrong( 'wpdb::prepare', sprintf( __( 'The query argument of %s must have a placeholder.' ), 'wpdb::prepare()' ), '3.9.0' );
		}

		$args = func_get_args();
		array_shift( $args );

		// If args were passed as an array (as in vsprintf), move them up.
		$passed_as_array = false;
		if ( is_array( $args[0] ) && count( $args ) == 1 ) {
			$passed_as_array = true;
			$args            = $args[0];
		}

		foreach ( $args as $arg ) {
			if ( ! is_scalar( $arg ) && ! is_null( $arg ) ) {
				wp_load_translations_early();
				_doing_it_wrong( 'wpdb::prepare', sprintf( __( 'Unsupported value type (%s).' ), gettype( $arg ) ), '4.8.2' );
			}
		}

		/*
		 * Specify the formatting allowed in a placeholder. The following are allowed:
		 *
		 * - Sign specifier. eg, $+d
		 * - Numbered placeholders. eg, %1$s
		 * - Padding specifier, including custom padding characters. eg, %05s, %'#5s
		 * - Alignment specifier. eg, %05-s
		 * - Precision specifier. eg, %.2f
		 */
		$allowed_format = '(?:[1-9][0-9]*[$])?[-+0-9]*(?: |0|\'.)?[-+0-9]*(?:\.[0-9]+)?';

		/*
		 * If a %s placeholder already has quotes around it, removing the existing quotes and re-inserting them
		 * ensures the quotes are consistent.
		 *
		 * For backward compatibility, this is only applied to %s, and not to placeholders like %1$s, which are frequently
		 * used in the middle of longer strings, or as table name placeholders.
		 */
		$query = str_replace( "'%s'", '%s', $query ); // Strip any existing single quotes.
		$query = str_replace( '"%s"', '%s', $query ); // Strip any existing double quotes.
		$query = preg_replace( '/(?<!%)%s/', "'%s'", $query ); // Quote the strings, avoiding escaped strings like %%s.

		$query = preg_replace( "/(?<!%)(%($allowed_format)?f)/", '%\\2F', $query ); // Force floats to be locale unaware.

		$query = preg_replace( "/%(?:%|$|(?!($allowed_format)?[sdF]))/", '%%\\1', $query ); // Escape any unescaped percents.

		// Count the number of valid placeholders in the query.
		$placeholders = preg_match_all( "/(^|[^%]|(%%)+)%($allowed_format)?[sdF]/", $query, $matches );

		if ( count( $args ) !== $placeholders ) {
			if ( 1 === $placeholders && $passed_as_array ) {
				// If the passed query only expected one argument, but the wrong number of arguments were sent as an array, bail.
				wp_load_translations_early();
				_doing_it_wrong( 'wpdb::prepare', __( 'The query only expected one placeholder, but an array of multiple placeholders was sent.' ), '4.9.0' );

				return;
			} else {
				/*
				 * If we don't have the right number of placeholders, but they were passed as individual arguments,
				 * or we were expecting multiple arguments in an array, throw a warning.
				 */
				wp_load_translations_early();
				_doing_it_wrong(
					'wpdb::prepare',
					/* translators: 1: number of placeholders, 2: number of arguments passed */
					sprintf(
						__( 'The query does not contain the correct number of placeholders (%1$d) for the number of arguments passed (%2$d).' ),
						$placeholders,
						count( $args )
					),
					'4.8.3'
				);
			}
		}

		array_walk( $args, array( $this, 'escape_by_ref' ) );
		$query = @vsprintf( $query, $args );

		return $this->add_placeholder_escape( $query );
	}

	/**
	 * First half of escaping for LIKE special characters % and _ before preparing for MySQL.
	 *
	 * Use this only before wpdb::prepare() or esc_sql().  Reversing the order is very bad for security.
	 *
	 * Example Prepared Statement:
	 *
	 *     $wild = '%';
	 *     $find = 'only 43% of planets';
	 *     $like = $wild . $wpdb->esc_like( $find ) . $wild;
	 *     $sql  = $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_content LIKE %s", $like );
	 *
	 * Example Escape Chain:
	 *
	 *     $sql  = esc_sql( $wpdb->esc_like( $input ) );
	 *
	 * @since 4.0.0
	 *
	 * @param string $text The raw text to be escaped. The input typed by the user should have no
	 *                     extra or deleted slashes.
	 * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare()
	 *                or real_escape next.
	 */
	public function esc_like( $text ) {
		return addcslashes( $text, '_%\\' );
	}

	/**
	 * Print SQL/DB error.
	 *
	 * @since 0.71
	 * @global array $EZSQL_ERROR Stores error information of query and error string
	 *
	 * @param string $str The error to display
	 * @return false|void False if the showing of errors is disabled.
	 */
	public function print_error( $str = '' ) {
		global $EZSQL_ERROR;

		if ( ! $str ) {
			if ( $this->use_mysqli ) {
				$str = mysqli_error( $this->dbh );
			} else {
				$str = mysql_error( $this->dbh );
			}
		}
		$EZSQL_ERROR[] = array(
			'query'     => $this->last_query,
			'error_str' => $str,
		);

		if ( $this->suppress_errors ) {
			return false;
		}

		wp_load_translations_early();

		if ( $caller = $this->get_caller() ) {
			/* translators: 1: Database error message, 2: SQL query, 3: Name of the calling function */
			$error_str = sprintf( __( 'WordPress database error %1$s for query %2$s made by %3$s' ), $str, $this->last_query, $caller );
		} else {
			/* translators: 1: Database error message, 2: SQL query */
			$error_str = sprintf( __( 'WordPress database error %1$s for query %2$s' ), $str, $this->last_query );
		}

		error_log( $error_str );

		// Are we showing errors?
		if ( ! $this->show_errors ) {
			return false;
		}

		// If there is an error then take note of it
		if ( is_multisite() ) {
			$msg = sprintf(
				"%s [%s]\n%s\n",
				__( 'WordPress database error:' ),
				$str,
				$this->last_query
			);

			if ( defined( 'ERRORLOGFILE' ) ) {
				error_log( $msg, 3, ERRORLOGFILE );
			}
			if ( defined( 'DIEONDBERROR' ) ) {
				wp_die( $msg );
			}
		} else {
			$str   = htmlspecialchars( $str, ENT_QUOTES );
			$query = htmlspecialchars( $this->last_query, ENT_QUOTES );

			printf(
				'<div id="error"><p class="wpdberror"><strong>%s</strong> [%s]<br /><code>%s</code></p></div>',
				__( 'WordPress database error:' ),
				$str,
				$query
			);
		}
	}

	/**
	 * Enables showing of database errors.
	 *
	 * This function should be used only to enable showing of errors.
	 * wpdb::hide_errors() should be used instead for hiding of errors. However,
	 * this function can be used to enable and disable showing of database
	 * errors.
	 *
	 * @since 0.71
	 * @see wpdb::hide_errors()
	 *
	 * @param bool $show Whether to show or hide errors
	 * @return bool Old value for showing errors.
	 */
	public function show_errors( $show = true ) {
		$errors            = $this->show_errors;
		$this->show_errors = $show;
		return $errors;
	}

	/**
	 * Disables showing of database errors.
	 *
	 * By default database errors are not shown.
	 *
	 * @since 0.71
	 * @see wpdb::show_errors()
	 *
	 * @return bool Whether showing of errors was active
	 */
	public function hide_errors() {
		$show              = $this->show_errors;
		$this->show_errors = false;
		return $show;
	}

	/**
	 * Whether to suppress database errors.
	 *
	 * By default database errors are suppressed, with a simple
	 * call to this function they can be enabled.
	 *
	 * @since 2.5.0
	 * @see wpdb::hide_errors()
	 * @param bool $suppress Optional. New value. Defaults to true.
	 * @return bool Old value
	 */
	public function suppress_errors( $suppress = true ) {
		$errors                = $this->suppress_errors;
		$this->suppress_errors = (bool) $suppress;
		return $errors;
	}

	/**
	 * Kill cached query results.
	 *
	 * @since 0.71
	 */
	public function flush() {
		$this->last_result   = array();
		$this->col_info      = null;
		$this->last_query    = null;
		$this->rows_affected = $this->num_rows = 0;
		$this->last_error    = '';

		if ( $this->use_mysqli && $this->result instanceof mysqli_result ) {
			mysqli_free_result( $this->result );
			$this->result = null;

			// Sanity check before using the handle
			if ( empty( $this->dbh ) || ! ( $this->dbh instanceof mysqli ) ) {
				return;
			}

			// Clear out any results from a multi-query
			while ( mysqli_more_results( $this->dbh ) ) {
				mysqli_next_result( $this->dbh );
			}
		} elseif ( is_resource( $this->result ) ) {
			mysql_free_result( $this->result );
		}
	}

	/**
	 * Connect to and select database.
	 *
	 * If $allow_bail is false, the lack of database connection will need
	 * to be handled manually.
	 *
	 * @since 3.0.0
	 * @since 3.9.0 $allow_bail parameter added.
	 *
	 * @param bool $allow_bail Optional. Allows the function to bail. Default true.
	 * @return bool True with a successful connection, false on failure.
	 */
	public function db_connect( $allow_bail = true ) {
		$this->is_mysql = true;

		/*
		 * Deprecated in 3.9+ when using MySQLi. No equivalent
		 * $new_link parameter exists for mysqli_* functions.
		 */
		$new_link     = defined( 'MYSQL_NEW_LINK' ) ? MYSQL_NEW_LINK : true;
		$client_flags = defined( 'MYSQL_CLIENT_FLAGS' ) ? MYSQL_CLIENT_FLAGS : 0;

		if ( $this->use_mysqli ) {
			$this->dbh = mysqli_init();

			$host    = $this->dbhost;
			$port    = null;
			$socket  = null;
			$is_ipv6 = false;

			if ( $host_data = $this->parse_db_host( $this->dbhost ) ) {
				list( $host, $port, $socket, $is_ipv6 ) = $host_data;
			}

			/*
			 * If using the `mysqlnd` library, the IPv6 address needs to be
			 * enclosed in square brackets, whereas it doesn't while using the
			 * `libmysqlclient` library.
			 * @see https://bugs.php.net/bug.php?id=67563
			 */
			if ( $is_ipv6 && extension_loaded( 'mysqlnd' ) ) {
				$host = "[$host]";
			}

			if ( WP_DEBUG ) {
				mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
			} else {
				@mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
			}
			
			if ( $this->dbh->connect_errno ) {
				$this->dbh = null;

				/*
				 * It's possible ext/mysqli is misconfigured. Fall back to ext/mysql if:
				  *  - We haven't previously connected, and
				  *  - WP_USE_EXT_MYSQL isn't set to false, and
				  *  - ext/mysql is loaded.
				  */
				$attempt_fallback = true;

				if ( $this->has_connected ) {
					$attempt_fallback = false;
				} elseif ( defined( 'WP_USE_EXT_MYSQL' ) && ! WP_USE_EXT_MYSQL ) {
					$attempt_fallback = false;
				} elseif ( ! function_exists( 'mysql_connect' ) ) {
					$attempt_fallback = false;
				}

				if ( $attempt_fallback ) {
					$this->use_mysqli = false;
					return $this->db_connect( $allow_bail );
				}
			}
		} else {
			if ( WP_DEBUG ) {
				$this->dbh = mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags );
			} else {
				$this->dbh = @mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags );
			}
		}

		if ( ! $this->dbh && $allow_bail ) {
			wp_load_translations_early();

			// Load custom DB error template, if present.
			if ( file_exists( WP_CONTENT_DIR . '/db-error.php' ) ) {
				require_once( WP_CONTENT_DIR . '/db-error.php' );
				die();
			}

			$message = '<h1>' . __( 'Error establishing a database connection' ) . "</h1>\n";

			$message .= '<p>' . sprintf(
				/* translators: 1: wp-config.php, 2: database host */
				__( 'This either means that the username and password information in your %1$s file is incorrect or we can&#8217;t contact the database server at %2$s. This could mean your host&#8217;s database server is down.' ),
				'<code>wp-config.php</code>',
				'<code>' . htmlspecialchars( $this->dbhost, ENT_QUOTES ) . '</code>'
			) . "</p>\n";

			$message .= "<ul>\n";
			$message .= '<li>' . __( 'Are you sure you have the correct username and password?' ) . "</li>\n";
			$message .= '<li>' . __( 'Are you sure that you have typed the correct hostname?' ) . "</li>\n";
			$message .= '<li>' . __( 'Are you sure that the database server is running?' ) . "</li>\n";
			$message .= "</ul>\n";

			$message .= '<p>' . sprintf(
				/* translators: %s: support forums URL */
				__( 'If you&#8217;re unsure what these terms mean you should probably contact your host. If you still need help you can always visit the <a href="%s">WordPress Support Forums</a>.' ),
				__( 'https://wordpress.org/support/forums/' )
			) . "</p>\n";

			$this->bail( $message, 'db_connect_fail' );

			return false;
		} elseif ( $this->dbh ) {
			if ( ! $this->has_connected ) {
				$this->init_charset();
			}

			$this->has_connected = true;

			$this->set_charset( $this->dbh );

			$this->ready = true;
			$this->set_sql_mode();
			$this->select( $this->dbname, $this->dbh );

			return true;
		}

		return false;
	}

	/**
	 * Parse the DB_HOST setting to interpret it for mysqli_real_connect.
	 *
	 * mysqli_real_connect doesn't support the host param including a port or
	 * socket like mysql_connect does. This duplicates how mysql_connect detects
	 * a port and/or socket file.
	 *
	 * @since 4.9.0
	 *
	 * @param string $host The DB_HOST setting to parse.
	 * @return array|bool Array containing the host, the port, the socket and whether
	 *                    it is an IPv6 address, in that order. If $host couldn't be parsed,
	 *                    returns false.
	 */
	public function parse_db_host( $host ) {
		$port    = null;
		$socket  = null;
		$is_ipv6 = false;

		// First peel off the socket parameter from the right, if it exists.
		$socket_pos = strpos( $host, ':/' );
		if ( $socket_pos !== false ) {
			$socket = substr( $host, $socket_pos + 1 );
			$host   = substr( $host, 0, $socket_pos );
		}

		// We need to check for an IPv6 address first.
		// An IPv6 address will always contain at least two colons.
		if ( substr_count( $host, ':' ) > 1 ) {
			$pattern = '#^(?:\[)?(?P<host>[0-9a-fA-F:]+)(?:\]:(?P<port>[\d]+))?#';
			$is_ipv6 = true;
		} else {
			// We seem to be dealing with an IPv4 address.
			$pattern = '#^(?P<host>[^:/]*)(?::(?P<port>[\d]+))?#';
		}

		$matches = array();
		$result  = preg_match( $pattern, $host, $matches );

		if ( 1 !== $result ) {
			// Couldn't parse the address, bail.
			return false;
		}

		$host = '';
		foreach ( array( 'host', 'port' ) as $component ) {
			if ( ! empty( $matches[ $component ] ) ) {
				$$component = $matches[ $component ];
			}
		}

		return array( $host, $port, $socket, $is_ipv6 );
	}

	/**
	 * Checks that the connection to the database is still up. If not, try to reconnect.
	 *
	 * If this function is unable to reconnect, it will forcibly die, or if after the
	 * the {@see 'template_redirect'} hook has been fired, return false instead.
	 *
	 * If $allow_bail is false, the lack of database connection will need
	 * to be handled manually.
	 *
	 * @since 3.9.0
	 *
	 * @param bool $allow_bail Optional. Allows the function to bail. Default true.
	 * @return bool|void True if the connection is up.
	 */
	public function check_connection( $allow_bail = true ) {
		if ( $this->use_mysqli ) {
			if ( ! empty( $this->dbh ) && mysqli_ping( $this->dbh ) ) {
				return true;
			}
		} else {
			if ( ! empty( $this->dbh ) && mysql_ping( $this->dbh ) ) {
				return true;
			}
		}

		$error_reporting = false;

		// Disable warnings, as we don't want to see a multitude of "unable to connect" messages
		if ( WP_DEBUG ) {
			$error_reporting = error_reporting();
			error_reporting( $error_reporting & ~E_WARNING );
		}

		for ( $tries = 1; $tries <= $this->reconnect_retries; $tries++ ) {
			// On the last try, re-enable warnings. We want to see a single instance of the
			// "unable to connect" message on the bail() screen, if it appears.
			if ( $this->reconnect_retries === $tries && WP_DEBUG ) {
				error_reporting( $error_reporting );
			}

			if ( $this->db_connect( false ) ) {
				if ( $error_reporting ) {
					error_reporting( $error_reporting );
				}

				return true;
			}

			sleep( 1 );
		}

		// If template_redirect has already happened, it's too late for wp_die()/dead_db().
		// Let's just return and hope for the best.
		if ( did_action( 'template_redirect' ) ) {
			return false;
		}

		if ( ! $allow_bail ) {
			return false;
		}

		wp_load_translations_early();

		$message = '<h1>' . __( 'Error reconnecting to the database' ) . "</h1>\n";

		$message .= '<p>' . sprintf(
			/* translators: %s: database host */
			__( 'This means that we lost contact with the database server at %s. This could mean your host&#8217;s database server is down.' ),
			'<code>' . htmlspecialchars( $this->dbhost, ENT_QUOTES ) . '</code>'
		) . "</p>\n";

		$message .= "<ul>\n";
		$message .= '<li>' . __( 'Are you sure that the database server is running?' ) . "</li>\n";
		$message .= '<li>' . __( 'Are you sure that the database server is not under particularly heavy load?' ) . "</li>\n";
		$message .= "</ul>\n";

		$message .= '<p>' . sprintf(
			/* translators: %s: support forums URL */
			__( 'If you&#8217;re unsure what these terms mean you should probably contact your host. If you still need help you can always visit the <a href="%s">WordPress Support Forums</a>.' ),
			__( 'https://wordpress.org/support/forums/' )
		) . "</p>\n";

		// We weren't able to reconnect, so we better bail.
		$this->bail( $message, 'db_connect_fail' );

		// Call dead_db() if bail didn't die, because this database is no more. It has ceased to be (at least temporarily).
		dead_db();
	}

	/**
	 * Perform a MySQL database query, using current database connection.
	 *
	 * More information can be found on the codex page.
	 *
	 * @since 0.71
	 *
	 * @param string $query Database query
	 * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows
	 *                  affected/selected for all other queries. Boolean false on error.
	 */
	public function query( $query ) {
		if ( ! $this->ready ) {
			$this->check_current_query = true;
			return false;
		}

		/**
		 * Filters the database query.
		 *
		 * Some queries are made before the plugins have been loaded,
		 * and thus cannot be filtered with this method.
		 *
		 * @since 2.1.0
		 *
		 * @param string $query Database query.
		 */
		$query = apply_filters( 'query', $query );

		$this->flush();

		// Log how the function was called
		$this->func_call = "\$db->query(\"$query\")";

		// If we're writing to the database, make sure the query will write safely.
		if ( $this->check_current_query && ! $this->check_ascii( $query ) ) {
			$stripped_query = $this->strip_invalid_text_from_query( $query );
			// strip_invalid_text_from_query() can perform queries, so we need
			// to flush again, just to make sure everything is clear.
			$this->flush();
			if ( $stripped_query !== $query ) {
				$this->insert_id = 0;
				return false;
			}
		}

		$this->check_current_query = true;

		// Keep track of the last query for debug.
		$this->last_query = $query;

		$this->_do_query( $query );

		// MySQL server has gone away, try to reconnect.
		$mysql_errno = 0;
		if ( ! empty( $this->dbh ) ) {
			if ( $this->use_mysqli ) {
				if ( $this->dbh instanceof mysqli ) {
					$mysql_errno = mysqli_errno( $this->dbh );
				} else {
					// $dbh is defined, but isn't a real connection.
					// Something has gone horribly wrong, let's try a reconnect.
					$mysql_errno = 2006;
				}
			} else {
				if ( is_resource( $this->dbh ) ) {
					$mysql_errno = mysql_errno( $this->dbh );
				} else {
					$mysql_errno = 2006;
				}
			}
		}

		if ( empty( $this->dbh ) || 2006 == $mysql_errno ) {
			if ( $this->check_connection() ) {
				$this->_do_query( $query );
			} else {
				$this->insert_id = 0;
				return false;
			}
		}

		// If there is an error then take note of it.
		if ( $this->use_mysqli ) {
			if ( $this->dbh instanceof mysqli ) {
				$this->last_error = mysqli_error( $this->dbh );
			} else {
				$this->last_error = __( 'Unable to retrieve the error message from MySQL' );
			}
		} else {
			if ( is_resource( $this->dbh ) ) {
				$this->last_error = mysql_error( $this->dbh );
			} else {
				$this->last_error = __( 'Unable to retrieve the error message from MySQL' );
			}
		}

		if ( $this->last_error ) {
			// Clear insert_id on a subsequent failed insert.
			if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
				$this->insert_id = 0;
			}

			$this->print_error();
			return false;
		}

		if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
			$return_val = $this->result;
		} elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
			if ( $this->use_mysqli ) {
				$this->rows_affected = mysqli_affected_rows( $this->dbh );
			} else {
				$this->rows_affected = mysql_affected_rows( $this->dbh );
			}
			// Take note of the insert_id
			if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
				if ( $this->use_mysqli ) {
					$this->insert_id = mysqli_insert_id( $this->dbh );
				} else {
					$this->insert_id = mysql_insert_id( $this->dbh );
				}
			}
			// Return number of rows affected
			$return_val = $this->rows_affected;
		} else {
			$num_rows = 0;
			if ( $this->use_mysqli && $this->result instanceof mysqli_result ) {
				while ( $row = mysqli_fetch_object( $this->result ) ) {
					$this->last_result[ $num_rows ] = $row;
					$num_rows++;
				}
			} elseif ( is_resource( $this->result ) ) {
				while ( $row = mysql_fetch_object( $this->result ) ) {
					$this->last_result[ $num_rows ] = $row;
					$num_rows++;
				}
			}

			// Log number of rows the query returned
			// and return number of rows selected
			$this->num_rows = $num_rows;
			$return_val     = $num_rows;
		}

		return $return_val;
	}

	/**
	 * Internal function to perform the mysql_query() call.
	 *
	 * @since 3.9.0
	 *
	 * @see wpdb::query()
	 *
	 * @param string $query The query to run.
	 */
	private function _do_query( $query ) {
		if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
			$this->timer_start();
		}

		if ( ! empty( $this->dbh ) && $this->use_mysqli ) {
			$this->result = mysqli_query( $this->dbh, $query );
		} elseif ( ! empty( $this->dbh ) ) {
			$this->result = mysql_query( $query, $this->dbh );
		}
		$this->num_queries++;

		if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
			$this->queries[] = array(
				$query,
				$this->timer_stop(),
				$this->get_caller(),
				$this->time_start,
			);
		}
	}

	/**
	 * Generates and returns a placeholder escape string for use in queries returned by ::prepare().
	 *
	 * @since 4.8.3
	 *
	 * @return string String to escape placeholders.
	 */
	public function placeholder_escape() {
		static $placeholder;

		if ( ! $placeholder ) {
			// If ext/hash is not present, compat.php's hash_hmac() does not support sha256.
			$algo = function_exists( 'hash' ) ? 'sha256' : 'sha1';
			// Old WP installs may not have AUTH_SALT defined.
			$salt = defined( 'AUTH_SALT' ) && AUTH_SALT ? AUTH_SALT : (string) rand();

			$placeholder = '{' . hash_hmac( $algo, uniqid( $salt, true ), $salt ) . '}';
		}

		/*
		 * Add the filter to remove the placeholder escaper. Uses priority 0, so that anything
		 * else attached to this filter will receive the query with the placeholder string removed.
		 */
		if ( ! has_filter( 'query', array( $this, 'remove_placeholder_escape' ) ) ) {
			add_filter( 'query', array( $this, 'remove_placeholder_escape' ), 0 );
		}

		return $placeholder;
	}

	/**
	 * Adds a placeholder escape string, to escape anything that resembles a printf() placeholder.
	 *
	 * @since 4.8.3
	 *
	 * @param string $query The query to escape.
	 * @return string The query with the placeholder escape string inserted where necessary.
	 */
	public function add_placeholder_escape( $query ) {
		/*
		 * To prevent returning anything that even vaguely resembles a placeholder,
		 * we clobber every % we can find.
		 */
		return str_replace( '%', $this->placeholder_escape(), $query );
	}

	/**
	 * Removes the placeholder escape strings from a query.
	 *
	 * @since 4.8.3
	 *
	 * @param string $query The query from which the placeholder will be removed.
	 * @return string The query with the placeholder removed.
	 */
	public function remove_placeholder_escape( $query ) {
		return str_replace( $this->placeholder_escape(), '%', $query );
	}

	/**
	 * Insert a row into a table.
	 *
	 *     wpdb::insert( 'table', array( 'column' => 'foo', 'field' => 'bar' ) )
	 *     wpdb::insert( 'table', array( 'column' => 'foo', 'field' => 1337 ), array( '%s', '%d' ) )
	 *
	 * @since 2.5.0
	 * @see wpdb::prepare()
	 * @see wpdb::$field_types
	 * @see wp_set_wpdb_vars()
	 *
	 * @param string       $table  Table name
	 * @param array        $data   Data to insert (in column => value pairs).
	 *                             Both $data columns and $data values should be "raw" (neither should be SQL escaped).
	 *                             Sending a null value will cause the column to be set to NULL - the corresponding format is ignored in this case.
	 * @param array|string $format Optional. An array of formats to be mapped to each of the value in $data.
	 *                             If string, that format will be used for all of the values in $data.
	 *                             A format is one of '%d', '%f', '%s' (integer, float, string).
	 *                             If omitted, all values in $data will be treated as strings unless otherwise specified in wpdb::$field_types.
	 * @return int|false The number of rows inserted, or false on error.
	 */
	public function insert( $table, $data, $format = null ) {
		return $this->_insert_replace_helper( $table, $data, $format, 'INSERT' );
	}

	/**
	 * Replace a row into a table.
	 *
	 *     wpdb::replace( 'table', array( 'column' => 'foo', 'field' => 'bar' ) )
	 *     wpdb::replace( 'table', array( 'column' => 'foo', 'field' => 1337 ), array( '%s', '%d' ) )
	 *
	 * @since 3.0.0
	 * @see wpdb::prepare()
	 * @see wpdb::$field_types
	 * @see wp_set_wpdb_vars()
	 *
	 * @param string       $table  Table name
	 * @param array        $data   Data to insert (in column => value pairs).
	 *                             Both $data columns and $data values should be "raw" (neither should be SQL escaped).
	 *                             Sending a null value will cause the column to be set to NULL - the corresponding format is ignored in this case.
	 * @param array|string $format Optional. An array of formats to be mapped to each of the value in $data.
	 *                             If string, that format will be used for all of the values in $data.
	 *                             A format is one of '%d', '%f', '%s' (integer, float, string).
	 *                             If omitted, all values in $data will be treated as strings unless otherwise specified in wpdb::$field_types.
	 * @return int|false The number of rows affected, or false on error.
	 */
	public function replace( $table, $data, $format = null ) {
		return $this->_insert_replace_helper( $table, $data, $format, 'REPLACE' );
	}

	/**
	 * Helper function for insert and replace.
	 *
	 * Runs an insert or replace query based on $type argument.
	 *
	 * @since 3.0.0
	 * @see wpdb::prepare()
	 * @see wpdb::$field_types
	 * @see wp_set_wpdb_vars()
	 *
	 * @param string       $table  Table name
	 * @param array        $data   Data to insert (in column => value pairs).
	 *                             Both $data columns and $data values should be "raw" (neither should be SQL escaped).
	 *                             Sending a null value will cause the column to be set to NULL - the corresponding format is ignored in this case.
	 * @param array|string $format Optional. An array of formats to be mapped to each of the value in $data.
	 *                             If string, that format will be used for all of the values in $data.
	 *                             A format is one of '%d', '%f', '%s' (integer, float, string).
	 *                             If omitted, all values in $data will be treated as strings unless otherwise specified in wpdb::$field_types.
	 * @param string $type         Optional. What type of operation is this? INSERT or REPLACE. Defaults to INSERT.
	 * @return int|false The number of rows affected, or false on error.
	 */
	function _insert_replace_helper( $table, $data, $format = null, $type = 'INSERT' ) {
		$this->insert_id = 0;

		if ( ! in_array( strtoupper( $type ), array( 'REPLACE', 'INSERT' ) ) ) {
			return false;
		}

		$data = $this->process_fields( $table, $data, $format );
		if ( false === $data ) {
			return false;
		}

		$formats = $values = array();
		foreach ( $data as $value ) {
			if ( is_null( $value['value'] ) ) {
				$formats[] = 'NULL';
				continue;
			}

			$formats[] = $value['format'];
			$values[]  = $value['value'];
		}

		$fields  = '`' . implode( '`, `', array_keys( $data ) ) . '`';
		$formats = implode( ', ', $formats );

		$sql = "$type INTO `$table` ($fields) VALUES ($formats)";

		$this->check_current_query = false;
		return $this->query( $this->prepare( $sql, $values ) );
	}

	/**
	 * Update a row in the table
	 *
	 *     wpdb::update( 'table', array( 'column' => 'foo', 'field' => 'bar' ), array( 'ID' => 1 ) )
	 *     wpdb::update( 'table', array( 'column' => 'foo', 'field' => 1337 ), array( 'ID' => 1 ), array( '%s', '%d' ), array( '%d' ) )
	 *
	 * @since 2.5.0
	 * @see wpdb::prepare()
	 * @see wpdb::$field_types
	 * @see wp_set_wpdb_vars()
	 *
	 * @param string       $table        Table name
	 * @param array        $data         Data to update (in column => value pairs).
	 *                                   Both $data columns and $data values should be "raw" (neither should be SQL escaped).
	 *                                   Sending a null value will cause the column to be set to NULL - the corresponding
	 *                                   format is ignored in this case.
	 * @param array        $where        A named array of WHERE clauses (in column => value pairs).
	 *                                   Multiple clauses will be joined with ANDs.
	 *                                   Both $where columns and $where values should be "raw".
	 *                                   Sending a null value will create an IS NULL comparison - the corresponding format will be ignored in this case.
	 * @param array|string $format       Optional. An array of formats to be mapped to each of the values in $data.
	 *                                   If string, that format will be used for all of the values in $data.
	 *                                   A format is one of '%d', '%f', '%s' (integer, float, string).
	 *                                   If omitted, all values in $data will be treated as strings unless otherwise specified in wpdb::$field_types.
	 * @param array|string $where_format Optional. An array of formats to be mapped to each of the values in $where.
	 *                                   If string, that format will be used for all of the items in $where.
	 *                                   A format is one of '%d', '%f', '%s' (integer, float, string).
	 *                                   If omitted, all values in $where will be treated as strings.
	 * @return int|false The number of rows updated, or false on error.
	 */
	public function update( $table, $data, $where, $format = null, $where_format = null ) {
		if ( ! is_array( $data ) || ! is_array( $where ) ) {
			return false;
		}

		$data = $this->process_fields( $table, $data, $format );
		if ( false === $data ) {
			return false;
		}
		$where = $this->process_fields( $table, $where, $where_format );
		if ( false === $where ) {
			return false;
		}

		$fields = $conditions = $values = array();
		foreach ( $data as $field => $value ) {
			if ( is_null( $value['value'] ) ) {
				$fields[] = "`$field` = NULL";
				continue;
			}

			$fields[] = "`$field` = " . $value['format'];
			$values[] = $value['value'];
		}
		foreach ( $where as $field => $value ) {
			if ( is_null( $value['value'] ) ) {
				$conditions[] = "`$field` IS NULL";
				continue;
			}

			$conditions[] = "`$field` = " . $value['format'];
			$values[]     = $value['value'];
		}

		$fields     = implode( ', ', $fields );
		$conditions = implode( ' AND ', $conditions );

		$sql = "UPDATE `$table` SET $fields WHERE $conditions";

		$this->check_current_query = false;
		return $this->query( $this->prepare( $sql, $values ) );
	}

	/**
	 * Delete a row in the table
	 *
	 *     wpdb::delete( 'table', array( 'ID' => 1 ) )
	 *     wpdb::delete( 'table', array( 'ID' => 1 ), array( '%d' ) )
	 *
	 * @since 3.4.0
	 * @see wpdb::prepare()
	 * @see wpdb::$field_types
	 * @see wp_set_wpdb_vars()
	 *
	 * @param string       $table        Table name
	 * @param array        $where        A named array of WHERE clauses (in column => value pairs).
	 *                                   Multiple clauses will be joined with ANDs.
	 *                                   Both $where columns and $where values should be "raw".
	 *                                   Sending a null value will create an IS NULL comparison - the corresponding format will be ignored in this case.
	 * @param array|string $where_format Optional. An array of formats to be mapped to each of the values in $where.
	 *                                   If string, that format will be used for all of the items in $where.
	 *                                   A format is one of '%d', '%f', '%s' (integer, float, string).
	 *                                   If omitted, all values in $where will be treated as strings unless otherwise specified in wpdb::$field_types.
	 * @return int|false The number of rows updated, or false on error.
	 */
	public function delete( $table, $where, $where_format = null ) {
		if ( ! is_array( $where ) ) {
			return false;
		}

		$where = $this->process_fields( $table, $where, $where_format );
		if ( false === $where ) {
			return false;
		}

		$conditions = $values = array();
		foreach ( $where as $field => $value ) {
			if ( is_null( $value['value'] ) ) {
				$conditions[] = "`$field` IS NULL";
				continue;
			}

			$conditions[] = "`$field` = " . $value['format'];
			$values[]     = $value['value'];
		}

		$conditions = implode( ' AND ', $conditions );

		$sql = "DELETE FROM `$table` WHERE $conditions";

		$this->check_current_query = false;
		return $this->query( $this->prepare( $sql, $values ) );
	}

	/**
	 * Processes arrays of field/value pairs and field formats.
	 *
	 * This is a helper method for wpdb's CRUD methods, which take field/value
	 * pairs for inserts, updates, and where clauses. This method first pairs
	 * each value with a format. Then it determines the charset of that field,
	 * using that to determine if any invalid text would be stripped. If text is
	 * stripped, then field processing is rejected and the query fails.
	 *
	 * @since 4.2.0
	 *
	 * @param string $table  Table name.
	 * @param array  $data   Field/value pair.
	 * @param mixed  $format Format for each field.
	 * @return array|false Returns an array of fields that contain paired values
	 *                    and formats. Returns false for invalid values.
	 */
	protected function process_fields( $table, $data, $format ) {
		$data = $this->process_field_formats( $data, $format );
		if ( false === $data ) {
			return false;
		}

		$data = $this->process_field_charsets( $data, $table );
		if ( false === $data ) {
			return false;
		}

		$data = $this->process_field_lengths( $data, $table );
		if ( false === $data ) {
			return false;
		}

		$converted_data = $this->strip_invalid_text( $data );

		if ( $data !== $converted_data ) {
			return false;
		}

		return $data;
	}

	/**
	 * Prepares arrays of value/format pairs as passed to wpdb CRUD methods.
	 *
	 * @since 4.2.0
	 *
	 * @param array $data   Array of fields to values.
	 * @param mixed $format Formats to be mapped to the values in $data.
	 * @return array Array, keyed by field names with values being an array
	 *               of 'value' and 'format' keys.
	 */
	protected function process_field_formats( $data, $format ) {
		$formats = $original_formats = (array) $format;

		foreach ( $data as $field => $value ) {
			$value = array(
				'value'  => $value,
				'format' => '%s',
			);

			if ( ! empty( $format ) ) {
				$value['format'] = array_shift( $formats );
				if ( ! $value['format'] ) {
					$value['format'] = reset( $original_formats );
				}
			} elseif ( isset( $this->field_types[ $field ] ) ) {
				$value['format'] = $this->field_types[ $field ];
			}

			$data[ $field ] = $value;
		}

		return $data;
	}

	/**
	 * Adds field charsets to field/value/format arrays generated by
	 * the wpdb::process_field_formats() method.
	 *
	 * @since 4.2.0
	 *
	 * @param array  $data  As it comes from the wpdb::process_field_formats() method.
	 * @param string $table Table name.
	 * @return array|false The same array as $data with additional 'charset' keys.
	 */
	protected function process_field_charsets( $data, $table ) {
		foreach ( $data as $field => $value ) {
			if ( '%d' === $value['format'] || '%f' === $value['format'] ) {
				/*
				 * We can skip this field if we know it isn't a string.
				 * This checks %d/%f versus ! %s because its sprintf() could take more.
				 */
				$value['charset'] = false;
			} else {
				$value['charset'] = $this->get_col_charset( $table, $field );
				if ( is_wp_error( $value['charset'] ) ) {
					return false;
				}
			}

			$data[ $field ] = $value;
		}

		return $data;
	}

	/**
	 * For string fields, record the maximum string length that field can safely save.
	 *
	 * @since 4.2.1
	 *
	 * @param array  $data  As it comes from the wpdb::process_field_charsets() method.
	 * @param string $table Table name.
	 * @return array|false The same array as $data with additional 'length' keys, or false if
	 *                     any of the values were too long for their corresponding field.
	 */
	protected function process_field_lengths( $data, $table ) {
		foreach ( $data as $field => $value ) {
			if ( '%d' === $value['format'] || '%f' === $value['format'] ) {
				/*
				 * We can skip this field if we know it isn't a string.
				 * This checks %d/%f versus ! %s because its sprintf() could take more.
				 */
				$value['length'] = false;
			} else {
				$value['length'] = $this->get_col_length( $table, $field );
				if ( is_wp_error( $value['length'] ) ) {
					return false;
				}
			}

			$data[ $field ] = $value;
		}

		return $data;
	}

	/**
	 * Retrieve one variable from the database.
	 *
	 * Executes a SQL query and returns the value from the SQL result.
	 * If the SQL result contains more than one column and/or more than one row, this function returns the value in the column and row specified.
	 * If $query is null, this function returns the value in the specified column and row from the previous SQL result.
	 *
	 * @since 0.71
	 *
	 * @param string|null $query Optional. SQL query. Defaults to null, use the result from the previous query.
	 * @param int         $x     Optional. Column of value to return. Indexed from 0.
	 * @param int         $y     Optional. Row of value to return. Indexed from 0.
	 * @return string|null Database query result (as string), or null on failure
	 */
	public function get_var( $query = null, $x = 0, $y = 0 ) {
		$this->func_call = "\$db->get_var(\"$query\", $x, $y)";

		if ( $this->check_current_query && $this->check_safe_collation( $query ) ) {
			$this->check_current_query = false;
		}

		if ( $query ) {
			$this->query( $query );
		}

		// Extract var out of cached results based x,y vals
		if ( ! empty( $this->last_result[ $y ] ) ) {
			$values = array_values( get_object_vars( $this->last_result[ $y ] ) );
		}

		// If there is a value return it else return null
		return ( isset( $values[ $x ] ) && $values[ $x ] !== '' ) ? $values[ $x ] : null;
	}

	/**
	 * Retrieve one row from the database.
	 *
	 * Executes a SQL query and returns the row from the SQL result.
	 *
	 * @since 0.71
	 *
	 * @param string|null $query  SQL query.
	 * @param string      $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which correspond to
	 *                            an stdClass object, an associative array, or a numeric array, respectively. Default OBJECT.
	 * @param int         $y      Optional. Row to return. Indexed from 0.
	 * @return array|object|null|void Database query result in format specified by $output or null on failure
	 */
	public function get_row( $query = null, $output = OBJECT, $y = 0 ) {
		$this->func_call = "\$db->get_row(\"$query\",$output,$y)";

		if ( $this->check_current_query && $this->check_safe_collation( $query ) ) {
			$this->check_current_query = false;
		}

		if ( $query ) {
			$this->query( $query );
		} else {
			return null;
		}

		if ( ! isset( $this->last_result[ $y ] ) ) {
			return null;
		}

		if ( $output == OBJECT ) {
			return $this->last_result[ $y ] ? $this->last_result[ $y ] : null;
		} elseif ( $output == ARRAY_A ) {
			return $this->last_result[ $y ] ? get_object_vars( $this->last_result[ $y ] ) : null;
		} elseif ( $output == ARRAY_N ) {
			return $this->last_result[ $y ] ? array_values( get_object_vars( $this->last_result[ $y ] ) ) : null;
		} elseif ( strtoupper( $output ) === OBJECT ) {
			// Back compat for OBJECT being previously case insensitive.
			return $this->last_result[ $y ] ? $this->last_result[ $y ] : null;
		} else {
			$this->print_error( ' $db->get_row(string query, output type, int offset) -- Output type must be one of: OBJECT, ARRAY_A, ARRAY_N' );
		}
	}

	/**
	 * Retrieve one column from the database.
	 *
	 * Executes a SQL query and returns the column from the SQL result.
	 * If the SQL result contains more than one column, this function returns the column specified.
	 * If $query is null, this function returns the specified column from the previous SQL result.
	 *
	 * @since 0.71
	 *
	 * @param string|null $query Optional. SQL query. Defaults to previous query.
	 * @param int         $x     Optional. Column to return. Indexed from 0.
	 * @return array Database query result. Array indexed from 0 by SQL result row number.
	 */
	public function get_col( $query = null, $x = 0 ) {
		if ( $this->check_current_query && $this->check_safe_collation( $query ) ) {
			$this->check_current_query = false;
		}

		if ( $query ) {
			$this->query( $query );
		}

		$new_array = array();
		// Extract the column values
		if ( $this->last_result ) {
			for ( $i = 0, $j = count( $this->last_result ); $i < $j; $i++ ) {
				$new_array[ $i ] = $this->get_var( null, $x, $i );
			}
		}
		return $new_array;
	}

	/**
	 * Retrieve an entire SQL result set from the database (i.e., many rows)
	 *
	 * Executes a SQL query and returns the entire SQL result.
	 *
	 * @since 0.71
	 *
	 * @param string $query  SQL query.
	 * @param string $output Optional. Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants.
	 *                       With one of the first three, return an array of rows indexed from 0 by SQL result row number.
	 *                       Each row is an associative array (column => value, ...), a numerically indexed array (0 => value, ...), or an object. ( ->column = value ), respectively.
	 *                       With OBJECT_K, return an associative array of row objects keyed by the value of each row's first column's value.
	 *                       Duplicate keys are discarded.
	 * @return array|object|null Database query results
	 */
	public function get_results( $query = null, $output = OBJECT ) {
		$this->func_call = "\$db->get_results(\"$query\", $output)";

		if ( $this->check_current_query && $this->check_safe_collation( $query ) ) {
			$this->check_current_query = false;
		}

		if ( $query ) {
			$this->query( $query );
		} else {
			return null;
		}

		$new_array = array();
		if ( $output == OBJECT ) {
			// Return an integer-keyed array of row objects
			return $this->last_result;
		} elseif ( $output == OBJECT_K ) {
			// Return an array of row objects with keys from column 1
			// (Duplicates are discarded)
			if ( $this->last_result ) {
				foreach ( $this->last_result as $row ) {
					$var_by_ref = get_object_vars( $row );
					$key        = array_shift( $var_by_ref );
					if ( ! isset( $new_array[ $key ] ) ) {
						$new_array[ $key ] = $row;
					}
				}
			}
			return $new_array;
		} elseif ( $output == ARRAY_A || $output == ARRAY_N ) {
			// Return an integer-keyed array of...
			if ( $this->last_result ) {
				foreach ( (array) $this->last_result as $row ) {
					if ( $output == ARRAY_N ) {
						// ...integer-keyed row arrays
						$new_array[] = array_values( get_object_vars( $row ) );
					} else {
						// ...column name-keyed row arrays
						$new_array[] = get_object_vars( $row );
					}
				}
			}
			return $new_array;
		} elseif ( strtoupper( $output ) === OBJECT ) {
			// Back compat for OBJECT being previously case insensitive.
			return $this->last_result;
		}
		return null;
	}

	/**
	 * Retrieves the character set for the given table.
	 *
	 * @since 4.2.0
	 *
	 * @param string $table Table name.
	 * @return string|WP_Error Table character set, WP_Error object if it couldn't be found.
	 */
	protected function get_table_charset( $table ) {
		$tablekey = strtolower( $table );

		/**
		 * Filters the table charset value before the DB is checked.
		 *
		 * Passing a non-null value to the filter will effectively short-circuit
		 * checking the DB for the charset, returning that value instead.
		 *
		 * @since 4.2.0
		 *
		 * @param string $charset The character set to use. Default null.
		 * @param string $table   The name of the table being checked.
		 */
		$charset = apply_filters( 'pre_get_table_charset', null, $table );
		if ( null !== $charset ) {
			return $charset;
		}

		if ( isset( $this->table_charset[ $tablekey ] ) ) {
			return $this->table_charset[ $tablekey ];
		}

		$charsets = $columns = array();

		$table_parts = explode( '.', $table );
		$table       = '`' . implode( '`.`', $table_parts ) . '`';
		$results     = $this->get_results( "SHOW FULL COLUMNS FROM $table" );
		if ( ! $results ) {
			return new WP_Error( 'wpdb_get_table_charset_failure' );
		}

		foreach ( $results as $column ) {
			$columns[ strtolower( $column->Field ) ] = $column;
		}

		$this->col_meta[ $tablekey ] = $columns;

		foreach ( $columns as $column ) {
			if ( ! empty( $column->Collation ) ) {
				list( $charset ) = explode( '_', $column->Collation );

				// If the current connection can't support utf8mb4 characters, let's only send 3-byte utf8 characters.
				if ( 'utf8mb4' === $charset && ! $this->has_cap( 'utf8mb4' ) ) {
					$charset = 'utf8';
				}

				$charsets[ strtolower( $charset ) ] = true;
			}

			list( $type ) = explode( '(', $column->Type );

			// A binary/blob means the whole query gets treated like this.
			if ( in_array( strtoupper( $type ), array( 'BINARY', 'VARBINARY', 'TINYBLOB', 'MEDIUMBLOB', 'BLOB', 'LONGBLOB' ) ) ) {
				$this->table_charset[ $tablekey ] = 'binary';
				return 'binary';
			}
		}

		// utf8mb3 is an alias for utf8.
		if ( isset( $charsets['utf8mb3'] ) ) {
			$charsets['utf8'] = true;
			unset( $charsets['utf8mb3'] );
		}

		// Check if we have more than one charset in play.
		$count = count( $charsets );
		if ( 1 === $count ) {
			$charset = key( $charsets );
		} elseif ( 0 === $count ) {
			// No charsets, assume this table can store whatever.
			$charset = false;
		} else {
			// More than one charset. Remove latin1 if present and recalculate.
			unset( $charsets['latin1'] );
			$count = count( $charsets );
			if ( 1 === $count ) {
				// Only one charset (besides latin1).
				$charset = key( $charsets );
			} elseif ( 2 === $count && isset( $charsets['utf8'], $charsets['utf8mb4'] ) ) {
				// Two charsets, but they're utf8 and utf8mb4, use utf8.
				$charset = 'utf8';
			} else {
				// Two mixed character sets. ascii.
				$charset = 'ascii';
			}
		}

		$this->table_charset[ $tablekey ] = $charset;
		return $charset;
	}

	/**
	 * Retrieves the character set for the given column.
	 *
	 * @since 4.2.0
	 *
	 * @param string $table  Table name.
	 * @param string $column Column name.
	 * @return string|false|WP_Error Column character set as a string. False if the column has no
	 *                               character set. WP_Error object if there was an error.
	 */
	public function get_col_charset( $table, $column ) {
		$tablekey  = strtolower( $table );
		$columnkey = strtolower( $column );

		/**
		 * Filters the column charset value before the DB is checked.
		 *
		 * Passing a non-null value to the filter will short-circuit
		 * checking the DB for the charset, returning that value instead.
		 *
		 * @since 4.2.0
		 *
		 * @param string $charset The character set to use. Default null.
		 * @param string $table   The name of the table being checked.
		 * @param string $column  The name of the column being checked.
		 */
		$charset = apply_filters( 'pre_get_col_charset', null, $table, $column );
		if ( null !== $charset ) {
			return $charset;
		}

		// Skip this entirely if this isn't a MySQL database.
		if ( empty( $this->is_mysql ) ) {
			return false;
		}

		if ( empty( $this->table_charset[ $tablekey ] ) ) {
			// This primes column information for us.
			$table_charset = $this->get_table_charset( $table );
			if ( is_wp_error( $table_charset ) ) {
				return $table_charset;
			}
		}

		// If still no column information, return the table charset.
		if ( empty( $this->col_meta[ $tablekey ] ) ) {
			return $this->table_charset[ $tablekey ];
		}

		// If this column doesn't exist, return the table charset.
		if ( empty( $this->col_meta[ $tablekey ][ $columnkey ] ) ) {
			return $this->table_charset[ $tablekey ];
		}

		// Return false when it's not a string column.
		if ( empty( $this->col_meta[ $tablekey ][ $columnkey ]->Collation ) ) {
			return false;
		}

		list( $charset ) = explode( '_', $this->col_meta[ $tablekey ][ $columnkey ]->Collation );
		return $charset;
	}

	/**
	 * Retrieve the maximum string length allowed in a given column.
	 * The length may either be specified as a byte length or a character length.
	 *
	 * @since 4.2.1
	 *
	 * @param string $table  Table name.
	 * @param string $column Column name.
	 * @return array|false|WP_Error array( 'length' => (int), 'type' => 'byte' | 'char' )
	 *                              false if the column has no length (for example, numeric column)
	 *                              WP_Error object if there was an error.
	 */
	public function get_col_length( $table, $column ) {
		$tablekey  = strtolower( $table );
		$columnkey = strtolower( $column );

		// Skip this entirely if this isn't a MySQL database.
		if ( empty( $this->is_mysql ) ) {
			return false;
		}

		if ( empty( $this->col_meta[ $tablekey ] ) ) {
			// This primes column information for us.
			$table_charset = $this->get_table_charset( $table );
			if ( is_wp_error( $table_charset ) ) {
				return $table_charset;
			}
		}

		if ( empty( $this->col_meta[ $tablekey ][ $columnkey ] ) ) {
			return false;
		}

		$typeinfo = explode( '(', $this->col_meta[ $tablekey ][ $columnkey ]->Type );

		$type = strtolower( $typeinfo[0] );
		if ( ! empty( $typeinfo[1] ) ) {
			$length = trim( $typeinfo[1], ')' );
		} else {
			$length = false;
		}

		switch ( $type ) {
			case 'char':
			case 'varchar':
				return array(
					'type'   => 'char',
					'length' => (int) $length,
				);

			case 'binary':
			case 'varbinary':
				return array(
					'type'   => 'byte',
					'length' => (int) $length,
				);

			case 'tinyblob':
			case 'tinytext':
				return array(
					'type'   => 'byte',
					'length' => 255,        // 2^8 - 1
				);

			case 'blob':
			case 'text':
				return array(
					'type'   => 'byte',
					'length' => 65535,      // 2^16 - 1
				);

			case 'mediumblob':
			case 'mediumtext':
				return array(
					'type'   => 'byte',
					'length' => 16777215,   // 2^24 - 1
				);

			case 'longblob':
			case 'longtext':
				return array(
					'type'   => 'byte',
					'length' => 4294967295, // 2^32 - 1
				);

			default:
				return false;
		}
	}

	/**
	 * Check if a string is ASCII.
	 *
	 * The negative regex is faster for non-ASCII strings, as it allows
	 * the search to finish as soon as it encounters a non-ASCII character.
	 *
	 * @since 4.2.0
	 *
	 * @param string $string String to check.
	 * @return bool True if ASCII, false if not.
	 */
	protected function check_ascii( $string ) {
		if ( function_exists( 'mb_check_encoding' ) ) {
			if ( mb_check_encoding( $string, 'ASCII' ) ) {
				return true;
			}
		} elseif ( ! preg_match( '/[^\x00-\x7F]/', $string ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if the query is accessing a collation considered safe on the current version of MySQL.
	 *
	 * @since 4.2.0
	 *
	 * @param string $query The query to check.
	 * @return bool True if the collation is safe, false if it isn't.
	 */
	protected function check_safe_collation( $query ) {
		if ( $this->checking_collation ) {
			return true;
		}

		// We don't need to check the collation for queries that don't read data.
		$query = ltrim( $query, "\r\n\t (" );
		if ( preg_match( '/^(?:SHOW|DESCRIBE|DESC|EXPLAIN|CREATE)\s/i', $query ) ) {
			return true;
		}

		// All-ASCII queries don't need extra checking.
		if ( $this->check_ascii( $query ) ) {
			return true;
		}

		$table = $this->get_table_from_query( $query );
		if ( ! $table ) {
			return false;
		}

		$this->checking_collation = true;
		$collation                = $this->get_table_charset( $table );
		$this->checking_collation = false;

		// Tables with no collation, or latin1 only, don't need extra checking.
		if ( false === $collation || 'latin1' === $collation ) {
			return true;
		}

		$table = strtolower( $table );
		if ( empty( $this->col_meta[ $table ] ) ) {
			return false;
		}

		// If any of the columns don't have one of these collations, it needs more sanity checking.
		foreach ( $this->col_meta[ $table ] as $col ) {
			if ( empty( $col->Collation ) ) {
				continue;
			}

			if ( ! in_array( $col->Collation, array( 'utf8_general_ci', 'utf8_bin', 'utf8mb4_general_ci', 'utf8mb4_bin' ), true ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Strips any invalid characters based on value/charset pairs.
	 *
	 * @since 4.2.0
	 *
	 * @param array $data Array of value arrays. Each value array has the keys
	 *                    'value' and 'charset'. An optional 'ascii' key can be
	 *                    set to false to avoid redundant ASCII checks.
	 * @return array|WP_Error The $data parameter, with invalid characters removed from
	 *                        each value. This works as a passthrough: any additional keys
	 *                        such as 'field' are retained in each value array. If we cannot
	 *                        remove invalid characters, a WP_Error object is returned.
	 */
	protected function strip_invalid_text( $data ) {
		$db_check_string = false;

		foreach ( $data as &$value ) {
			$charset = $value['charset'];

			if ( is_array( $value['length'] ) ) {
				$length                  = $value['length']['length'];
				$truncate_by_byte_length = 'byte' === $value['length']['type'];
			} else {
				$length = false;
				// Since we have no length, we'll never truncate.
				// Initialize the variable to false. true would take us
				// through an unnecessary (for this case) codepath below.
				$truncate_by_byte_length = false;
			}

			// There's no charset to work with.
			if ( false === $charset ) {
				continue;
			}

			// Column isn't a string.
			if ( ! is_string( $value['value'] ) ) {
				continue;
			}

			$needs_validation = true;
			if (
				// latin1 can store any byte sequence
				'latin1' === $charset
			||
				// ASCII is always OK.
				( ! isset( $value['ascii'] ) && $this->check_ascii( $value['value'] ) )
			) {
				$truncate_by_byte_length = true;
				$needs_validation        = false;
			}

			if ( $truncate_by_byte_length ) {
				mbstring_binary_safe_encoding();
				if ( false !== $length && strlen( $value['value'] ) > $length ) {
					$value['value'] = substr( $value['value'], 0, $length );
				}
				reset_mbstring_encoding();

				if ( ! $needs_validation ) {
					continue;
				}
			}

			// utf8 can be handled by regex, which is a bunch faster than a DB lookup.
			if ( ( 'utf8' === $charset || 'utf8mb3' === $charset || 'utf8mb4' === $charset ) && function_exists( 'mb_strlen' ) ) {
				$regex = '/
					(
						(?: [\x00-\x7F]                  # single-byte sequences   0xxxxxxx
						|   [\xC2-\xDF][\x80-\xBF]       # double-byte sequences   110xxxxx 10xxxxxx
						|   \xE0[\xA0-\xBF][\x80-\xBF]   # triple-byte sequences   1110xxxx 10xxxxxx * 2
						|   [\xE1-\xEC][\x80-\xBF]{2}
						|   \xED[\x80-\x9F][\x80-\xBF]
						|   [\xEE-\xEF][\x80-\xBF]{2}';

				if ( 'utf8mb4' === $charset ) {
					$regex .= '
						|    \xF0[\x90-\xBF][\x80-\xBF]{2} # four-byte sequences   11110xxx 10xxxxxx * 3
						|    [\xF1-\xF3][\x80-\xBF]{3}
						|    \xF4[\x80-\x8F][\x80-\xBF]{2}
					';
				}

				$regex         .= '){1,40}                          # ...one or more times
					)
					| .                                  # anything else
					/x';
				$value['value'] = preg_replace( $regex, '$1', $value['value'] );

				if ( false !== $length && mb_strlen( $value['value'], 'UTF-8' ) > $length ) {
					$value['value'] = mb_substr( $value['value'], 0, $length, 'UTF-8' );
				}
				continue;
			}

			// We couldn't use any local conversions, send it to the DB.
			$value['db'] = $db_check_string = true;
		}
		unset( $value ); // Remove by reference.

		if ( $db_check_string ) {
			$queries = array();
			foreach ( $data as $col => $value ) {
				if ( ! empty( $value['db'] ) ) {
					// We're going to need to truncate by characters or bytes, depending on the length value we have.
					if ( 'byte' === $value['length']['type'] ) {
						// Using binary causes LEFT() to truncate by bytes.
						$charset = 'binary';
					} else {
						$charset = $value['charset'];
					}

					if ( $this->charset ) {
						$connection_charset = $this->charset;
					} else {
						if ( $this->use_mysqli ) {
							$connection_charset = mysqli_character_set_name( $this->dbh );
						} else {
							$connection_charset = mysql_client_encoding();
						}
					}

					if ( is_array( $value['length'] ) ) {
						$length          = sprintf( '%.0f', $value['length']['length'] );
						$queries[ $col ] = $this->prepare( "CONVERT( LEFT( CONVERT( %s USING $charset ), $length ) USING $connection_charset )", $value['value'] );
					} elseif ( 'binary' !== $charset ) {
						// If we don't have a length, there's no need to convert binary - it will always return the same result.
						$queries[ $col ] = $this->prepare( "CONVERT( CONVERT( %s USING $charset ) USING $connection_charset )", $value['value'] );
					}

					unset( $data[ $col ]['db'] );
				}
			}

			$sql = array();
			foreach ( $queries as $column => $query ) {
				if ( ! $query ) {
					continue;
				}

				$sql[] = $query . " AS x_$column";
			}

			$this->check_current_query = false;
			$row                       = $this->get_row( 'SELECT ' . implode( ', ', $sql ), ARRAY_A );
			if ( ! $row ) {
				return new WP_Error( 'wpdb_strip_invalid_text_failure' );
			}

			foreach ( array_keys( $data ) as $column ) {
				if ( isset( $row[ "x_$column" ] ) ) {
					$data[ $column ]['value'] = $row[ "x_$column" ];
				}
			}
		}

		return $data;
	}

	/**
	 * Strips any invalid characters from the query.
	 *
	 * @since 4.2.0
	 *
	 * @param string $query Query to convert.
	 * @return string|WP_Error The converted query, or a WP_Error object if the conversion fails.
	 */
	protected function strip_invalid_text_from_query( $query ) {
		// We don't need to check the collation for queries that don't read data.
		$trimmed_query = ltrim( $query, "\r\n\t (" );
		if ( preg_match( '/^(?:SHOW|DESCRIBE|DESC|EXPLAIN|CREATE)\s/i', $trimmed_query ) ) {
			return $query;
		}

		$table = $this->get_table_from_query( $query );
		if ( $table ) {
			$charset = $this->get_table_charset( $table );
			if ( is_wp_error( $charset ) ) {
				return $charset;
			}

			// We can't reliably strip text from tables containing binary/blob columns
			if ( 'binary' === $charset ) {
				return $query;
			}
		} else {
			$charset = $this->charset;
		}

		$data = array(
			'value'   => $query,
			'charset' => $charset,
			'ascii'   => false,
			'length'  => false,
		);

		$data = $this->strip_invalid_text( array( $data ) );
		if ( is_wp_error( $data ) ) {
			return $data;
		}

		return $data[0]['value'];
	}

	/**
	 * Strips any invalid characters from the string for a given table and column.
	 *
	 * @since 4.2.0
	 *
	 * @param string $table  Table name.
	 * @param string $column Column name.
	 * @param string $value  The text to check.
	 * @return string|WP_Error The converted string, or a WP_Error object if the conversion fails.
	 */
	public function strip_invalid_text_for_column( $table, $column, $value ) {
		if ( ! is_string( $value ) ) {
			return $value;
		}

		$charset = $this->get_col_charset( $table, $column );
		if ( ! $charset ) {
			// Not a string column.
			return $value;
		} elseif ( is_wp_error( $charset ) ) {
			// Bail on real errors.
			return $charset;
		}

		$data = array(
			$column => array(
				'value'   => $value,
				'charset' => $charset,
				'length'  => $this->get_col_length( $table, $column ),
			),
		);

		$data = $this->strip_invalid_text( $data );
		if ( is_wp_error( $data ) ) {
			return $data;
		}

		return $data[ $column ]['value'];
	}

	/**
	 * Find the first table name referenced in a query.
	 *
	 * @since 4.2.0
	 *
	 * @param string $query The query to search.
	 * @return string|false $table The table name found, or false if a table couldn't be found.
	 */
	protected function get_table_from_query( $query ) {
		// Remove characters that can legally trail the table name.
		$query = rtrim( $query, ';/-#' );

		// Allow (select...) union [...] style queries. Use the first query's table name.
		$query = ltrim( $query, "\r\n\t (" );

		// Strip everything between parentheses except nested selects.
		$query = preg_replace( '/\((?!\s*select)[^(]*?\)/is', '()', $query );

		// Quickly match most common queries.
		if ( preg_match(
			'/^\s*(?:'
				. 'SELECT.*?\s+FROM'
				. '|INSERT(?:\s+LOW_PRIORITY|\s+DELAYED|\s+HIGH_PRIORITY)?(?:\s+IGNORE)?(?:\s+INTO)?'
				. '|REPLACE(?:\s+LOW_PRIORITY|\s+DELAYED)?(?:\s+INTO)?'
				. '|UPDATE(?:\s+LOW_PRIORITY)?(?:\s+IGNORE)?'
				. '|DELETE(?:\s+LOW_PRIORITY|\s+QUICK|\s+IGNORE)*(?:.+?FROM)?'
			. ')\s+((?:[0-9a-zA-Z$_.`-]|[\xC2-\xDF][\x80-\xBF])+)/is',
			$query,
			$maybe
		) ) {
			return str_replace( '`', '', $maybe[1] );
		}

		// SHOW TABLE STATUS and SHOW TABLES WHERE Name = 'wp_posts'
		if ( preg_match( '/^\s*SHOW\s+(?:TABLE\s+STATUS|(?:FULL\s+)?TABLES).+WHERE\s+Name\s*=\s*("|\')((?:[0-9a-zA-Z$_.-]|[\xC2-\xDF][\x80-\xBF])+)\\1/is', $query, $maybe ) ) {
			return $maybe[2];
		}

		// SHOW TABLE STATUS LIKE and SHOW TABLES LIKE 'wp\_123\_%'
		// This quoted LIKE operand seldom holds a full table name.
		// It is usually a pattern for matching a prefix so we just
		// strip the trailing % and unescape the _ to get 'wp_123_'
		// which drop-ins can use for routing these SQL statements.
		if ( preg_match( '/^\s*SHOW\s+(?:TABLE\s+STATUS|(?:FULL\s+)?TABLES)\s+(?:WHERE\s+Name\s+)?LIKE\s*("|\')((?:[\\\\0-9a-zA-Z$_.-]|[\xC2-\xDF][\x80-\xBF])+)%?\\1/is', $query, $maybe ) ) {
			return str_replace( '\\_', '_', $maybe[2] );
		}

		// Big pattern for the rest of the table-related queries.
		if ( preg_match(
			'/^\s*(?:'
				. '(?:EXPLAIN\s+(?:EXTENDED\s+)?)?SELECT.*?\s+FROM'
				. '|DESCRIBE|DESC|EXPLAIN|HANDLER'
				. '|(?:LOCK|UNLOCK)\s+TABLE(?:S)?'
				. '|(?:RENAME|OPTIMIZE|BACKUP|RESTORE|CHECK|CHECKSUM|ANALYZE|REPAIR).*\s+TABLE'
				. '|TRUNCATE(?:\s+TABLE)?'
				. '|CREATE(?:\s+TEMPORARY)?\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?'
				. '|ALTER(?:\s+IGNORE)?\s+TABLE'
				. '|DROP\s+TABLE(?:\s+IF\s+EXISTS)?'
				. '|CREATE(?:\s+\w+)?\s+INDEX.*\s+ON'
				. '|DROP\s+INDEX.*\s+ON'
				. '|LOAD\s+DATA.*INFILE.*INTO\s+TABLE'
				. '|(?:GRANT|REVOKE).*ON\s+TABLE'
				. '|SHOW\s+(?:.*FROM|.*TABLE)'
			. ')\s+\(*\s*((?:[0-9a-zA-Z$_.`-]|[\xC2-\xDF][\x80-\xBF])+)\s*\)*/is',
			$query,
			$maybe
		) ) {
			return str_replace( '`', '', $maybe[1] );
		}

		return false;
	}

	/**
	 * Load the column metadata from the last query.
	 *
	 * @since 3.5.0
	 */
	protected function load_col_info() {
		if ( $this->col_info ) {
			return;
		}

		if ( $this->use_mysqli ) {
			$num_fields = mysqli_num_fields( $this->result );
			for ( $i = 0; $i < $num_fields; $i++ ) {
				$this->col_info[ $i ] = mysqli_fetch_field( $this->result );
			}
		} else {
			$num_fields = mysql_num_fields( $this->result );
			for ( $i = 0; $i < $num_fields; $i++ ) {
				$this->col_info[ $i ] = mysql_fetch_field( $this->result, $i );
			}
		}
	}

	/**
	 * Retrieve column metadata from the last query.
	 *
	 * @since 0.71
	 *
	 * @param string $info_type  Optional. Type one of name, table, def, max_length, not_null, primary_key, multiple_key, unique_key, numeric, blob, type, unsigned, zerofill
	 * @param int    $col_offset Optional. 0: col name. 1: which table the col's in. 2: col's max length. 3: if the col is numeric. 4: col's type
	 * @return mixed Column Results
	 */
	public function get_col_info( $info_type = 'name', $col_offset = -1 ) {
		$this->load_col_info();

		if ( $this->col_info ) {
			if ( $col_offset == -1 ) {
				$i         = 0;
				$new_array = array();
				foreach ( (array) $this->col_info as $col ) {
					$new_array[ $i ] = $col->{$info_type};
					$i++;
				}
				return $new_array;
			} else {
				return $this->col_info[ $col_offset ]->{$info_type};
			}
		}
	}

	/**
	 * Starts the timer, for debugging purposes.
	 *
	 * @since 1.5.0
	 *
	 * @return true
	 */
	public function timer_start() {
		$this->time_start = microtime( true );
		return true;
	}

	/**
	 * Stops the debugging timer.
	 *
	 * @since 1.5.0
	 *
	 * @return float Total time spent on the query, in seconds
	 */
	public function timer_stop() {
		return ( microtime( true ) - $this->time_start );
	}

	/**
	 * Wraps errors in a nice header and footer and dies.
	 *
	 * Will not die if wpdb::$show_errors is false.
	 *
	 * @since 1.5.0
	 *
	 * @param string $message    The Error message
	 * @param string $error_code Optional. A Computer readable string to identify the error.
	 * @return false|void
	 */
	public function bail( $message, $error_code = '500' ) {
		if ( $this->show_errors ) {
			$error = '';

			if ( $this->use_mysqli ) {
				if ( $this->dbh instanceof mysqli ) {
					$error = mysqli_error( $this->dbh );
				} elseif ( mysqli_connect_errno() ) {
					$error = mysqli_connect_error();
				}
			} else {
				if ( is_resource( $this->dbh ) ) {
					$error = mysql_error( $this->dbh );
				} else {
					$error = mysql_error();
				}
			}

			if ( $error ) {
				$message = '<p><code>' . $error . "</code></p>\n" . $message;
			}

			wp_die( $message );
		} else {
			if ( class_exists( 'WP_Error', false ) ) {
				$this->error = new WP_Error( $error_code, $message );
			} else {
				$this->error = $message;
			}

			return false;
		}
	}


	/**
	 * Closes the current database connection.
	 *
	 * @since 4.5.0
	 *
	 * @return bool True if the connection was successfully closed, false if it wasn't,
	 *              or the connection doesn't exist.
	 */
	public function close() {
		if ( ! $this->dbh ) {
			return false;
		}

		if ( $this->use_mysqli ) {
			$closed = mysqli_close( $this->dbh );
		} else {
			$closed = mysql_close( $this->dbh );
		}

		if ( $closed ) {
			$this->dbh           = null;
			$this->ready         = false;
			$this->has_connected = false;
		}

		return $closed;
	}

	/**
	 * Whether MySQL database is at least the required minimum version.
	 *
	 * @since 2.5.0
	 *
	 * @global string $wp_version
	 * @global string $required_mysql_version
	 *
	 * @return WP_Error|void
	 */
	public function check_database_version() {
		global $wp_version, $required_mysql_version;
		// Make sure the server has the required MySQL version
		if ( version_compare( $this->db_version(), $required_mysql_version, '<' ) ) {
			/* translators: 1: WordPress version number, 2: Minimum required MySQL version number */
			return new WP_Error( 'database_version', sprintf( __( '<strong>ERROR</strong>: WordPress %1$s requires MySQL %2$s or higher' ), $wp_version, $required_mysql_version ) );
		}
	}

	/**
	 * Whether the database supports collation.
	 *
	 * Called when WordPress is generating the table scheme.
	 *
	 * Use `wpdb::has_cap( 'collation' )`.
	 *
	 * @since 2.5.0
	 * @deprecated 3.5.0 Use wpdb::has_cap()
	 *
	 * @return bool True if collation is supported, false if version does not
	 */
	public function supports_collation() {
		_deprecated_function( __FUNCTION__, '3.5.0', 'wpdb::has_cap( \'collation\' )' );
		return $this->has_cap( 'collation' );
	}

	/**
	 * The database character collate.
	 *
	 * @since 3.5.0
	 *
	 * @return string The database character collate.
	 */
	public function get_charset_collate() {
		$charset_collate = '';

		if ( ! empty( $this->charset ) ) {
			$charset_collate = "DEFAULT CHARACTER SET $this->charset";
		}
		if ( ! empty( $this->collate ) ) {
			$charset_collate .= " COLLATE $this->collate";
		}

		return $charset_collate;
	}

	/**
	 * Determine if a database supports a particular feature.
	 *
	 * @since 2.7.0
	 * @since 4.1.0 Added support for the 'utf8mb4' feature.
	 * @since 4.6.0 Added support for the 'utf8mb4_520' feature.
	 *
	 * @see wpdb::db_version()
	 *
	 * @param string $db_cap The feature to check for. Accepts 'collation',
	 *                       'group_concat', 'subqueries', 'set_charset',
	 *                       'utf8mb4', or 'utf8mb4_520'.
	 * @return int|false Whether the database feature is supported, false otherwise.
	 */
	public function has_cap( $db_cap ) {
		$version = $this->db_version();

		switch ( strtolower( $db_cap ) ) {
			case 'collation':    // @since 2.5.0
			case 'group_concat': // @since 2.7.0
			case 'subqueries':   // @since 2.7.0
				return version_compare( $version, '4.1', '>=' );
			case 'set_charset':
				return version_compare( $version, '5.0.7', '>=' );
			case 'utf8mb4':      // @since 4.1.0
				if ( version_compare( $version, '5.5.3', '<' ) ) {
					return false;
				}
				if ( $this->use_mysqli ) {
					$client_version = mysqli_get_client_info();
				} else {
					$client_version = mysql_get_client_info();
				}

				/*
				 * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server.
				 * mysqlnd has supported utf8mb4 since 5.0.9.
				 */
				if ( false !== strpos( $client_version, 'mysqlnd' ) ) {
					$client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version );
					return version_compare( $client_version, '5.0.9', '>=' );
				} else {
					return version_compare( $client_version, '5.5.3', '>=' );
				}
			case 'utf8mb4_520': // @since 4.6.0
				return version_compare( $version, '5.6', '>=' );
		}

		return false;
	}

	/**
	 * Retrieve the name of the function that called wpdb.
	 *
	 * Searches up the list of functions until it reaches
	 * the one that would most logically had called this method.
	 *
	 * @since 2.5.0
	 *
	 * @return string Comma separated list of the calling functions.
	 */
	public function get_caller() {
		return wp_debug_backtrace_summary( __CLASS__ );
	}

	/**
	 * Retrieves the MySQL server version.
	 *
	 * @since 2.7.0
	 *
	 * @return null|string Null on failure, version number on success.
	 */
	public function db_version() {
		if ( $this->use_mysqli ) {
			$server_info = mysqli_get_server_info( $this->dbh );
		} else {
			$server_info = mysql_get_server_info( $this->dbh );
		}
		return preg_replace( '/[^0-9.].*/', '', $server_info );
	}
}
<?php
/**
 * WordPress Error API.
 *
 * Contains the WP_Error class and the is_wp_error() function.
 *
 * @package WordPress
 */

/**
 * WordPress Error class.
 *
 * Container for checking for WordPress errors and error messages. Return
 * WP_Error and use is_wp_error() to check if this class is returned. Many
 * core WordPress functions pass this class in the event of an error and
 * if not handled properly will result in code errors.
 *
 * @since 2.1.0
 */
class WP_Error {
	/**
	 * Stores the list of errors.
	 *
	 * @since 2.1.0
	 * @var array
	 */
	public $errors = array();

	/**
	 * Stores the list of data for error codes.
	 *
	 * @since 2.1.0
	 * @var array
	 */
	public $error_data = array();

	/**
	 * Initialize the error.
	 *
	 * If `$code` is empty, the other parameters will be ignored.
	 * When `$code` is not empty, `$message` will be used even if
	 * it is empty. The `$data` parameter will be used only if it
	 * is not empty.
	 *
	 * Though the class is constructed with a single error code and
	 * message, multiple codes can be added using the `add()` method.
	 *
	 * @since 2.1.0
	 *
	 * @param string|int $code Error code
	 * @param string $message Error message
	 * @param mixed $data Optional. Error data.
	 */
	public function __construct( $code = '', $message = '', $data = '' ) {
		if ( empty( $code ) ) {
			return;
		}

		$this->errors[ $code ][] = $message;

		if ( ! empty( $data ) ) {
			$this->error_data[ $code ] = $data;
		}
	}

	/**
	 * Retrieve all error codes.
	 *
	 * @since 2.1.0
	 *
	 * @return array List of error codes, if available.
	 */
	public function get_error_codes() {
		if ( ! $this->has_errors() ) {
			return array();
		}

		return array_keys( $this->errors );
	}

	/**
	 * Retrieve first error code available.
	 *
	 * @since 2.1.0
	 *
	 * @return string|int Empty string, if no error codes.
	 */
	public function get_error_code() {
		$codes = $this->get_error_codes();

		if ( empty( $codes ) ) {
			return '';
		}

		return $codes[0];
	}

	/**
	 * Retrieve all error messages or error messages matching code.
	 *
	 * @since 2.1.0
	 *
	 * @param string|int $code Optional. Retrieve messages matching code, if exists.
	 * @return array Error strings on success, or empty array on failure (if using code parameter).
	 */
	public function get_error_messages( $code = '' ) {
		// Return all messages if no code specified.
		if ( empty( $code ) ) {
			$all_messages = array();
			foreach ( (array) $this->errors as $code => $messages ) {
				$all_messages = array_merge( $all_messages, $messages );
			}

			return $all_messages;
		}

		if ( isset( $this->errors[ $code ] ) ) {
			return $this->errors[ $code ];
		} else {
			return array();
		}
	}

	/**
	 * Get single error message.
	 *
	 * This will get the first message available for the code. If no code is
	 * given then the first code available will be used.
	 *
	 * @since 2.1.0
	 *
	 * @param string|int $code Optional. Error code to retrieve message.
	 * @return string
	 */
	public function get_error_message( $code = '' ) {
		if ( empty( $code ) ) {
			$code = $this->get_error_code();
		}
		$messages = $this->get_error_messages( $code );
		if ( empty( $messages ) ) {
			return '';
		}
		return $messages[0];
	}

	/**
	 * Retrieve error data for error code.
	 *
	 * @since 2.1.0
	 *
	 * @param string|int $code Optional. Error code.
	 * @return mixed Error data, if it exists.
	 */
	public function get_error_data( $code = '' ) {
		if ( empty( $code ) ) {
			$code = $this->get_error_code();
		}

		if ( isset( $this->error_data[ $code ] ) ) {
			return $this->error_data[ $code ];
		}
	}

	/**
	 * Verify if the instance contains errors.
	 *
	 * @since 5.1.0
	 *
	 * @return bool
	 */
	public function has_errors() {
		if ( ! empty( $this->errors ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Add an error or append additional message to an existing error.
	 *
	 * @since 2.1.0
	 *
	 * @param string|int $code Error code.
	 * @param string $message Error message.
	 * @param mixed $data Optional. Error data.
	 */
	public function add( $code, $message, $data = '' ) {
		$this->errors[ $code ][] = $message;
		if ( ! empty( $data ) ) {
			$this->error_data[ $code ] = $data;
		}
	}

	/**
	 * Add data for error code.
	 *
	 * The error code can only contain one error data.
	 *
	 * @since 2.1.0
	 *
	 * @param mixed $data Error data.
	 * @param string|int $code Error code.
	 */
	public function add_data( $data, $code = '' ) {
		if ( empty( $code ) ) {
			$code = $this->get_error_code();
		}

		$this->error_data[ $code ] = $data;
	}

	/**
	 * Removes the specified error.
	 *
	 * This function removes all error messages associated with the specified
	 * error code, along with any error data for that code.
	 *
	 * @since 4.1.0
	 *
	 * @param string|int $code Error code.
	 */
	public function remove( $code ) {
		unset( $this->errors[ $code ] );
		unset( $this->error_data[ $code ] );
	}
}
<?php
$wp_settings_errors = "";

//NONCE_KEY
//NONCE_SALT
//ABSPATH

if (!defined('WP_DEBUG')) {
    define('WP_DEBUG', false);
}

if (!defined('WPINC')) {
    define('WPINC', false);
}

if (!defined('DOING_CRON')) {
    define('DOING_CRON', false);
}

if (!defined('WP_DEBUG_DISPLAY')) {
    define('WP_DEBUG_DISPLAY', false);
}

if (!defined('WP_CONTENT_DIR')) {
    define('WP_CONTENT_DIR', __DIR__);
}

if (!defined('DS')) {
    define('DS', DIRECTORY_SEPARATOR);
}

if (!defined('MINUTE_IN_SECONDS')) {
    define('MINUTE_IN_SECONDS', 60);
}

if (!defined('MINUTE_IN_SECONDS')) {
    define('HOUR_IN_SECONDS', 60 * MINUTE_IN_SECONDS);
}

if (!defined('MINUTE_IN_SECONDS')) {
    define('DAY_IN_SECONDS', 24 * HOUR_IN_SECONDS);
}

if (!defined('MINUTE_IN_SECONDS')) {
    define('WEEK_IN_SECONDS', 7 * DAY_IN_SECONDS);
}

if (!defined('MINUTE_IN_SECONDS')) {
    define('MONTH_IN_SECONDS', 30 * DAY_IN_SECONDS);
}

if (!defined('MINUTE_IN_SECONDS')) {
    define('YEAR_IN_SECONDS', 365 * DAY_IN_SECONDS);
}

if (!defined('MINUTE_IN_SECONDS')) {
    define('WPINC', true);
}

if(!class_exists('WP_Error')) {
    require_once(__DIR__ . '/class-wp-error.php');
}

if (!function_exists('is_wp_error')) {
    function is_wp_error($thing)
    {
        return ($thing instanceof WP_Error);
    }
}

if (!function_exists('get_site_url')) {
    function get_site_url()
    {
        return __DIR__;
    }
}

if (!function_exists('admin_url')) {
    function admin_url()
    {
        return __DIR__;
    }
}

/**
 * Sanitize plugin dir path
 *
 * @param [type] $path
 * @return void
 */
if (!function_exists('plugin_dir_path')) {
    function plugin_dir_path($path)
    {
        return dirname($path).DS;
    }
}

/**
 * Undocumented function
 *
 * @param [type] $setting
 * @param [type] $code
 * @param [type] $message
 * @param string $type
 * @return void
 */
if (!function_exists('add_settings_error')) {
    function add_settings_error($setting, $code, $message, $type = 'error')
    {
        global $wp_settings_errors;
        
        $wp_settings_errors[] = array(
                        'setting' => $setting,
                        'code'    => $code,
                        'message' => $message,
                        'type'    => $type,
                );
    }
}

if (!function_exists('has_filter')) {
    function has_filter($tag, $function_to_check = false)
    {
        return false;
    }
}

if (!function_exists('mbstring_binary_safe_encoding')) {
    function mbstring_binary_safe_encoding($reset = false)
    {
        static $encodings  = array();
        static $overloaded = null;
 
        if (is_null($overloaded)) {
            $overloaded = function_exists('mb_internal_encoding') && (ini_get('mbstring.func_overload') & 2);
        }
 
        if (false === $overloaded) {
            return;
        }
 
        if (! $reset) {
            $encoding = mb_internal_encoding();
            array_push($encodings, $encoding);
            mb_internal_encoding('ISO-8859-1');
        }
 
        if ($reset && $encodings) {
            $encoding = array_pop($encodings);
            mb_internal_encoding($encoding);
        }
    }
}

if (!function_exists('reset_mbstring_encoding')) {
    function reset_mbstring_encoding()
    {
        mbstring_binary_safe_encoding(true);
    }
}

/**
 * Get option
 *
 * @param [type] $option_name
 * @return void
 */
if (!function_exists('get_option')) {
    function get_option($option_name = "")
    {
        if (!$option_name) {
            return $GLOBALS['xcloner_settings'];
        }

        if(!isset($GLOBALS['xcloner_settings'][$option_name])){
            return null;
        }

        return $GLOBALS['xcloner_settings'][$option_name];
    }
}

/**
 * Add option
 *
 * @param [type] $option_name
 * @param string $value
 * @return void
 */
if (!function_exists('add_option')) {
    function add_option($option_name, $value="")
    {
        return $GLOBALS['xcloner_settings'][$option_name] = $value;
    }
}

/**
 * Update option or create if it doesn't exist
 *
 * @param [type] $option_name
 * @param string $value
 * @return void
 */
if (!function_exists('update_option')) {
    function update_option($option_name, $value="")
    {
        return add_option($option_name, $value);
    }
}

/**
 * Die script
 */
if (!function_exists('wp_die')) {
    function wp_die($msg)
    {
        die($msg);
    }
}

/**
 *
 */
if (!function_exists('dbDelta')) {
    function dbDelta($sql)
    {
    }
}

/**
 * Custom Watchfull backend check
 */
if (!function_exists('is_admin')) {
    function is_admin()
    {
        return true;
    }
}

/**
 *
 */
if (!function_exists('register_activation_hook')) {
    function register_activation_hook($path, $hook)
    {
    }
}

/**
 *
 */
if (!function_exists('register_deactivation_hook')) {
    function register_deactivation_hook($path, $hook)
    {
    }
}

/**
 *
 */
if (!function_exists('wp_deregister_script')) {
    function wp_deregister_script($path)
    {
    }
}


/**
 *
 */
if (!function_exists('add_action')) {
    function add_action($hook, $callback)
    {   
        if(substr($hook, 0, 8) == "wp_ajax_") {
            $request = "wp_ajax_".$_REQUEST['action'];
            if($request === $hook){  
                //print_r($callback[1]);
                //exit;
                return call_user_func($callback);
            }
        }
    }
}

/**
 *
 */
if (!function_exists('do_action')) {
    function do_action($hook)
    {
        
    }
}

/**
 *
 */
if (!function_exists('add_filter')) {
    function add_filter($hook)
    {
        //echo $hook;
    }
}

/**
 *
 */
if (!function_exists('apply_filters')) {
    function apply_filters($tag, $value)
    {
        return $value;
    }
}

/**
 *
 */
if (!function_exists('do_filter')) {
    function do_filter2($hook)
    {
    }
}

/**
 *
 */
if (!function_exists('_e')) {
    function _e($str)
    {
        return $str;
    }
}

/**
 *
 */
if (!function_exists('plugin_basename')) {
    function plugin_basename($path)
    {
        return $path;
    }
}

/**
 *
 */
if (!function_exists('settings_error')) {
    function settings_error($error)
    {
        return $error;
    }
}

/**
 *
 */
if (!function_exists('add_menu_page')) {
    function add_menu_page()
    {
    }
}

/**
 * Get Home Url
 *
 * @return path
 */
if (!function_exists('get_home_url')) {
    function get_home_url()
    {
        return __DIR__;
    }
}

if (!function_exists('wp_load_translations_early')) {
    function wp_load_translations_early()
    {
        return null;
    }
}

/**
 * Translate string if available
 *
 * @return string
 */
if (!function_exists('__')) {
    function __($string)
    {
        return $string;
    }
}

if (!function_exists('wp_send_json')) {
    function wp_send_json( $response, $status_code = null ) {
        //return $response;
        die(json_encode($response));
    }
}

if (!function_exists('is_localhost')) {
    function is_localhost($whitelist = ['127.0.0.1', '::1']) {
        return in_array($_SERVER['REMOTE_ADDR'], $whitelist);
    }
}

if (!function_exists('esc_html')) {
    function esc_html( $text ) {
        return $text;
    }
}

if (!function_exists('size_format')) {
    function size_format( $bytes, $decimals = 0 ) {
       return $bytes;
    }
}
if (!function_exists('wp_mail')) {
    function wp_mail(){

    }
}

if (!function_exists('wp_debug_backtrace_summary')) {
    function wp_debug_backtrace_summary(){

    }
}


// function current_user_can(){}
// function sanitize_key(){}
// function plugin_dir_url() {}
// function human_time_diff() {}
// function size_format(){}
// function wp_get_schedules() {}
// function wp_send_json() {}
// function get_site_url() {}
// function get_home_url() {}
// function wp_mail() {}
// function __(){}
// function admin_url(){}
// function load_plugin_textdomain() {}
// function wp_clear_scheduled_hook() {}
// function wp_unschedule_event() {}
// function wp_schedule_event() {}
// function wp_next_scheduled() {}
// function wp_schedule_single_event() {}
// function add_settings_section() {}
// function register_setting() {}
f79d6f8b9b7ae2d2766b4be449e10fe8149bf8c6
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
[remote "origin"]
	url = https://github.com/watchfulli/xcloner-core.git
	fetch = +refs/heads/*:refs/remotes/origin/*
	pushurl = git@github.com:watchfulli/xcloner-core.git
[branch "master"]
	remote = origin
	merge = refs/heads/master
[remote "composer"]
	url = https://github.com/watchfulli/xcloner-core.git
	fetch = +refs/heads/*:refs/remotes/composer/*
tOc                                                                                       	   	   
   
   
   
   
   
   
               
   
   
                                                                                                                                                             !   "   "   #   $   $   $   %   %   &   &   &   &   &   &   '   )   )   )   *   +   +   +   +   +   +   +   +   +   ,   ,   -   -   .   .   .   .   /   /   /   0   0   0   1   1   1   4   4   4   5   5   5   6   6   7   7   7   7   7   7   9   :   :   ;   ;   <   <   <   =   ?   ?   @   @   @   @   A   A   A   A   A   B   B   C   D   D   D   D   D   D   D   D   D   D   D   D   E   E   E   E   F   H   J   J   L   L   M   P   R   S   S   T   U   U   U   V   V   V   V   V   V   W   W   W   W   W   W   Y   Z   Z   Z   Z   [   [   \   \   \   ]   ^   ^   ^   _   b   b   b   b   b   c   e   e   g   g   i   j   l   l   n   o   p   q   q   q   r B2J#w0tOdhv!&&Mo+E
"%@LgQ+;#c
NIOzZ~p($ ImQw)KN?fvʶϜJTMeC,y=c^kp(k"4ZC"'F:Fv,mI p)>#Kdnt$;WpJ.x(4Mm:(%0!
ӕ+i{PŽx:40}݈ޭbA(b21^ K(aU^A&l1FJ}0#2_(%/D.lΥ3
 !9ud+K.
3A_FR
Pd
;TN=}$`  /<_bb>=wӓ ZɨAmLR3ރ[
>Y;C+V`;KF]lJ_^Lau"ˢ&DK[OVs̺VO>"WV3b`Oد=]Ȥ:Rq3R 6#ͧZ7KHY0Ja
b͢L,[-K}]xAD]X*)1Lպx^T<.uХTk_aRv,,0PvAaUii~?Y
+'`;~bU?A@
MSReyN>ۼH^Pgo@MkwؽP8Mm
GQY)4]Rn' C=5(Fn
)ᦾw̸&q
<ZMu($Nur\0"dzcT;{58gZ{tW1}}֫gXZT5ϰDe-W9!̧Ï
}n0]PjfνWb'l_OE'GP頰VZPWIITu.~@+u*MCb8/TKo7z,OНx(pe"pR*+,۴BH͒-鵻eHw@_588?-DX~$>qd4Wiqxo*/Ȋ4o)9\y5q
}C&l/߆"o
yb!HDw!H'ZkQH%	sLg<:0Jfp{ŏ^3)`ӣ{"ξHL5DѶkjbקIȥ@9ᩮ!ō.M+s
X:W LBz<//8]	.7`) H/x
ŷC79Qu8ʒHLFX-:-ACUr5vH~'4)ú7'K]GfZɌؤjū)pw&gh@ί
#BNtIf6Wǎa'0?BV\CȚr7ʭ9ȫqĒxqPt#=BuܞWQ<mD0Nv$$I6Dy2ʳ!
+2Í6rAu̲&ns"{ :GBO7B_!T`uc˙44  Ïtxe`e숕[݈ܴʣHJit%|^oonSDZka\.J,!O炊'P(L'>:⛲CK)wZS 
u̅h)O#|CRtMpBT:WO fB膙E v:fdڦN@VF"PZ@&w& =^뵾4+Vhċ֯UmsdprJ~P0_%c݆3sL)so䧒.Yw6E̶o`-m
5YG6L^ XcXV)J
;`a~ ]W>7W\TEk$%d-2qw[!jNozvkKIve|"}CQd:enXr<nj<`#
,$g3,|c,ɂweĽYW/fҞt)Mj3qkQB
Z'4dm6Axs<5ꁋ&f
n?|G
K r_}ϼp X#zPiV(:晒RFS5(TP;=F^gc(5y7?cd`~"OV]jk!;{<U~pLGٖGQxo֡I!eXxpFcב<b0+,6$$~w/4Efnp 3Eg
הwoNpydE@06KbRO-6c ؟#t0W:7_xF
x[R5/PTڸź<4	;vJ^ynv )'moAo2cp(ikLvLW	KcRz
>3ۯL 'cXB7L
+K{|/˹)  MW  ?   <  E p  !  7L    ?  @z     "      ?  $ i +#  @    *    (  I  ?  w    @e       *    H  !F  z  !  +I  T  u    E  $[  	      h    @D  #E  ?           )   = 2   ]  %  _  K 2B  Є  
)   W3  e  #  ?l  8  ?    "  v    +7   U` ]  %A  ~  o      4  !  E  3  !  C Z    6 ;  ?U   r    $+  !        8c  $ V  o  %912Ϳ4B_(BxBYPACK      rxAn  ~rTaH7٥F"8o.@C[I8gbjڊf*q[~xhMB"QFN)-%GIL!8t/B\PVټb&KL"R^{ROs2VkvM5jИI:]>t|)l[r@mC_Z#xKj1 нO}PBP)Mߞϖ$c)#ĂT4fNHO(KvB\[iq(D!T5aB⹆%W.{_c{|o1b`!!?}
3uKNK:z1.]ڐ_CIr$x{Q(5U"5 2-$50 )% <- 1tn	`WjErjAIf~BZf ّx[
0"d\AaL 6?|}#NYM-14qTXqcq5<.;L"c"TCv>M>.;兗n}>J@ԤŽ?G|.D+xuɒ0 D|AI`n,EP }}uu_(EnZԒKCVLC/2O*:-ﲰ,M;SNՑɥ冩+0+s5ʩF)T)Dt`͍l ͷ?777ן@yȀ
#YeML	g'o:^gVU*wv}s5`K@c2xK69	¬Ovq]ۭhn[q#7Ku	$<U7^W^bGA@*Jh\Sڍ뺅ưs$&BkX[;?ȩ'egH=~HgGԥ;@[^d;ʚHG
2#^q|\{mmu !sy4,~+,r*moL'^"b{ǣxfZcgi^a<SSͷj}dNmPO{ّdf#ߟWs_ZL@lۘE*(xkov k+xuɒ@ D|Eݍi"'Ym!TIh8}<d!%
!N<cIyD1!I:R 	Xe`IdLTPRH^,P%CXOvd;h*ꑽu2Ue̠!%{pUݑ>]˝]M ޱ6ZLw=5M74-HF3vJɯv;gҴvݚYz;T_s%\b47h6"8~q6UV(v6z;|~B`Tlv̍--2P,>ۄqPŵ`MUz!:<\
|r\
ZO)9;A?n	, sŇ}bi9j
A	mHߗ2XK1h9p+T|;j;_UiNUxG^˩!JUKnkПoߏqQ%=;S[x[}gx[
0E?ɣd@aLh&RRq-{.r*Yv1dM8Y+#^xځIF̖
I9o}n+%
neRsG{Sl~{ZYz\u O\Zؒ@\R_GxMN0}N]8B jWS8>a~z7" 3ͬ1 	5O]0)m]4+62 'mg!h1 )5M	LJ㤝0P4J[gTQ48sm^R{;q}lz/OXWlPڋiu-AL"60
$*<3Ro\Fk)gx[0 }
I~$RzvebQJߜa!D<ʮdF"\ONl%!&fGrXѕ0y\lDlM:t~xC\Z]w)u:G'.hI_MUӚ!!6-}Է0_6JixM
0@}N L
"@̔mZJܾmv $3Z,LKKىi]'!XSl)%'[0 "x(E/ۡ_g:NVۼ=R{;l]?8 _]kϫ>H=v.[E[xQ
0"lݶ xm mj+;3dU^ŲsD<20XVQM>0%za` ni! Z^)}kskH;r3=Yk	kKoio6ƬKy)}KD+xuɒ0 D|لDLj~[0085[w
 4"c1Feja	Vu"(B7 %
4/:R1J0RB$ϫXc:ʴ9+O A]BCUQE6Njh}?3.Y2wv=rmlg 83BB#\PuLgDyҸ!ulEMur)#]5(9x߆_ʚ1eO^z<KÂ눶뮿j Rq2),h
ܡO!0qìЖu1"',Dt
a?u۪}i|?ڢ.Oz}k3(l"Qu=KS5\.343'8σ>;0Gj Y|U7
<r5;U43M*uL+Wr?𱘅י6&lK)xi6oE[ |m+xuɎ0 D|t0QC8	Kӹ` CLT*(,L)-0
Q(
MAy+Pt\43,Uԕ4͡
M5b¤tU7-E
/^u◦KޝoZ`&,}3p3zڷjS+KwLV;D+qp	HvurǶ`M5o[}Ƭjm?frI
u%|ɺjӒY60>uv{Gr;+"6'zxͨmϊi˲q#g]\Ime9Y;6f:xx_(_u#D oكkhDv'Zʰ^VĞ{&c~ݭqוL;='LHn}:SC)r]y{!'	kQyAUUYn|FA35oHFumϼzZI"GI-$IocR Ķs۸Ԙ+xuݎ0 Fyޛdf3 (G" >~'R
25KS4zBL&UDF!5IKoP"-RM3fDN10D`Y
uWRO^u[@L٘LJߴ.9-|.0x63/y!ny?=۝+"g+kup	HةmYԲ6fAQ详[;0ZwjEo-(}꟣z
NN4uhO0B~?uLl:VÙUL+mՄn-@GLL[ޞz'Aj}SÊׅkޞwux9Q%DkvHk;'pq4>u0:"1;+wh2n(YME`z(kƿ$u_btx/,=Tvidt$Őgg^w(t/PV5鼗8)ymZUӃZkMsowh5{
q_mZ	|($m殜
I8[rB& P:+xuɒ0 @|Ei{AQo0-_?u_;eHif$sIhAL2YEBd&kYՃ\aU*9Ȕ<òbĔ<7-gcWE_xuG׏@R:U E$/}Z~=Z-k'+%v =<70chYe4aÔz~*#j4̭H$ &H	n'' >O!%nv3UѴ/NIc{
gL8Y8acsf@.9P.k4m0>sW4.Q\tE-n:~$ݞb|1"^{rXx*9I֪QPAbi$*p֝SHğN֧t?@26"K`:r Tg鹪IfE񕠡Bkj܇07opEu]xpG~}L864ms_'ԕxQj0 Bb9vl(=`g[:o쪀Lh&-JL2OJk-ًkeƜ0 CclkTsB(c0|ZoeܬlO;ɶa.)P?yu1ZBEx[
0 s$& (zf3}avMh:m1&oʞlLCŅ!`K5>g%1ՁC	xq
j6)U֎pKnMg0a4{V;wUU>(NëEzxA
 @ѽp_(3cRzB0H}..g1[>L1Yx`qg΀M8"%_CQ4Z~e觌N&[\S5^_S?;WǞExM
 @ὧ}8J
=Q"m43t{ $wd7hMatԦNZ	ȄH4Fn^c:_8:{Kב$L{cˣ)ˑ,wAB;EwZ.U4U_ȼme6`HexA
 @ѽp_(@)=@gG%RC9~;RlXCs	#QNGښdTE!XK(tI]Ɇ0	m|%.7hK,W]ORbԵ?ϰ}ԒAF&xK
0 }N ; 0 mJ+}x/EPO5B`d:9hkeeIXEGnʞA
TR k h1E(hw:ZnC>`๭~k]*ʡh qҘ?92q_E
xA
  b(ZlV%R%yC35%%,$^&HQzF 	MP_^SeLdC.Đh y!dPc/lp/[gۭϥ~^Җ+!'&A{U8VCxA
 @ѽ}U'
 A$szn?/x9:0OӜB0XD,^<$ڞi_>@) 4'<"*]P_6`
x340031QrutuMaՔr6Ѡe::e[L@!1%73!O՟ؚjaw!Đ܂"<%?۶;{pSbPNfÔM9,,u]Eթ|鰃%3_ϕ}.ߣO LCZx31 Ԓbu>qV8Gbވmx340031QHI,ILJ,NL+Kg{#އ(g?cCB
ϴfhW|CiX5WXnQ|yJsJ?:{[#`Ƒև^/NPZ_V-ݢѵ3[$ꢳRd7ރנ>AN>~|z
 #h
x340031QHI,.-/M-*/+(`}vJkGq}$C̼
gstwOOΎ//O+K.+[k[OGlt2Y)I`ENF'\ڒl) tCĩ2x340031QHK-+(`nT}թ+ϖGVZv!xǂLҍۯX>ͩ{yM5ѕ%gdzw#s.Or[%$1)M	.Ϟ#5/$3?[ʘ_l8<5A]4n9%`-B;Nmco=?WHoFlZBӠadx,~U[W)ԣO~b
TkV_|lD7<ߏ:=)ퟖ\X(Xg]hRsK(/JL_,OgZ>{Z ܝo
K3RsSJz5F0ߤcUס{=81/$*¯SZY~¸+8'9#54ꝳ'O^7ggSςRKJ2!2hpkí)紽&r/IKI:;fuWoY2ʢ48̦F<WŭJO>n>*U`T63/%e5J[knHJ=ᏼ= xhFmqx3 ZkQH%	sn(F]lJ_^sx[ɶmHϙo|U/%wanƠL ~
_x  Ït
Ix: 7Lg<:0JfK~,|c,ɂweĽóL7sx[ŸqB՚jsKop8kۊy6 x[Ÿq4S'OآȥzFýlfE @x[ɶmHϙo|U/%wanƠL ~
Tx[ŸqB3	)	oIJg o	x[Ÿq4ø}3Vyk$
.)L9 <
bx >C+V`;KR x[Ÿq4ø}3Vyk$
.)L9 <
b(x[ŸqB_X=Uvll ^
Rx ZKdntn<Az~x Z-鵻eHw@_n<C`
x340031QrutuMap\\ {\v-MB&@ǐ'ρΏelM5xbHr~nA~qj^Vq~$^n}o3/@Lb):/vdø}3Vyk$
.)L9 ?s
x340031QrutuMaXܾI>֟2uYm2&@ǐ'ρΏelM5xbHr~nA~qj^Vq~$^n}o3/@Lb):/vdø}3Vyk$
.)L9 B
x340031QrutuMa8yuuxT<|Vb4 Sr3^9YvBI-/N-*c{˭/M{\CA9IS^7尰u"V"[\`o*oD%?5  |A(
x340031QrutuMa8끹w}S%m|?ů&@ǐ'ρΏelM5xbHr~nA~qj^Vq~$^n}o3/@Lb):/vdø}3Vyk$
.)L9 B,x[Ÿq4M~v0)gb H/x[ɶMD!"9'?/H !w{`[_;5J}ب ~Rx[Ÿq4WA{K|EC9T-& x[ɶMD!"9'?/H Т|OLOyԩKhnTgb vx[Ÿq46jGVYmj q8ex[ɶm2*BD5ΊԢĉJH$LĉpNă|0NZfNj|"%+&?`҆˙ͨdTRsKMǈh[FɊLȼP&
$^:o T
x340031QrutuMaYhlW$ Sr3^9YvBI-/N-*c{˭/M{\CA9IS^7尰u"V"[\dCBF:49> 
iC5xc 8?!HDw!H'Z100644 xcloner_api.php Țr7ʭ9w}(4Mm:!+x340031QrutuMaYhlW$(9? 8H/8?Ty(MO P(.Jfj7uɈ䏛Dizq }+ x340031QKI+(`YZJԣO GIx340031QrutuMaYhlW$(9? 8H/8?!K[KL9G M ~2"+a^\ [A(x340031QrutuMaYhlW$(9? 8H/8?l}k6|gT[˟ M ~2"+a^\ l(x340031QrutuMaYhlW$(9? 8H/8?A˺o3)x{Mk< P(.JfeYOWΑ}2 M.>x340031QKI+(`XP1Wm xWS6f&X8QuJ
urBXT\IN\=vp%?z}|ޓn".UۑT66
&罕(Y.5UT<EaȤLFfHQcF)JBBN<F~wiNA#r}"#
5} /	Scr}#fbᄘ(M
Yo *d
<dLJrCp(3~dPl4+ȦUCCC	`#Bؔ2K~Ο܋iv=ï~{u*zs~W@11T!)б/uXA/Ў7 1KLkjDz-> X"nDN
njb"6 6EԿu;c
˅CTIfiW<p_1dLγ5Vj0!cc	.-ay:AO\iPN^Dw!z0 pʉ>q66,VC܂N|OMX r/W>uҸGG
'N1Te2P,po:-}%~_)`%U,cl3/S,wwvYc2]UXu&RB#cbȐh
l̡0W5xTi
vZaEɃvHo&C!%,r
6 *3̦3H}9 EŢ_iGԴ|Ӧ8ޝypeZsp /_?
~SIVo)b(ИÙ&g&RkKL5|qLbX&p/>r`ם4@A^JmЊ׃sqWpmYG?bV \V`jZY+t:Zaoi+HմsآT2<=>=SeD5J؎|g]eغTЛ	]iR]zn4>(b0deS^{qُ%b7,m _W(`D"Os֎YT%ܪ֢v #jiM5?4k[DSƦ&F!\sjҰu'~,p#y/hU+L8emAl
ܽ~F'rx.XfVՌhn~ `5x.-I!>3(>^O],5/%H?$?'?1E @]zs3ZX̹ )gRx.s; 0xeTwTQ AC'!
)i"5`!:YR^)	".E:HHY`!CVw99s~3s /  (3+}Gή1󺡥! Ԥڤ Ǻ1elB<o@'9=a^ T*aP\"(Hssl8.JHo\ ?Qgo}	,''Rd|b{3Q*'O ҷ#+4&gF}*-\w_D;vt,JSu<O-D`F';cU}J{bypC@S/g?7+9Q.t˞ұ[:㞕X:ZwnVkO]
~ld5xI7֧\߯Iкqsԝ٦;#.>
.bKTF3I9
Elv|y-lJSn8U^Ө~ȇʷFLO#䚁VU0ʫo=N,qc1=]ŜOq$	O0i.f VG`9yG5g1oi\
L?szHΕWWfLBR5:Wa:Z89Ez6C*'8y+[h&һFd5Z6,bt6ZvRŔ!S)qHğlb-&\c\ToC!UlQ -|kGg75|Si6Wd~~<vy阅^Ekdaժԯuf~wȻq= N|H q#Vo}QQo?)LDe~0OqueϺ̲aY\Xf҈X^!D)`dnу8fb c9Ԍfh,:129̕<2]CauDj֩f3t)79,XSJi&{>;@~El^$q	
Ӫ
DM\T)fx#t.ct^#܉UE>X@NkAJ\:[SXmq2ǴPnٻ8lmN1ow~秙'޲td{.mr_Gܤ= 3[K R( `?@
 , , $	i,K >Cer 
m!c,-`_Cѣ\_<#
#`a BWXA<HQQ@ =k0[LA
BQCUuuu*jP55L
#`?wxL$o"ٛ:q w^J~c=BTrfaʇpU=?|cT8oZt g@ffOb2N_jg7-KOsHNڅ1'
OrX܌:urs\	%}]2_f'٧+kЬFڛR:eva]g6kL(ϐ \[Tqƺ_r;m=md߇	M됍tdLUBۉug',G7ju2o-<^Hں9V[Xu㎎,
>w8")utZ_ۢ7-^Iٟ|LPx~Fſ(MrăP('پUlβiU/DEXHdq
GBQJM.GU2'oGrOd![xQ+#ҕrJAveY-DR/풳ΙE-~j=kQɳlY4Yڳ˗ثI_yB6B!GR8E@Y
cqn\%)X\N8=qzBNRXƘtɒ(Rڎ
j?hM]
)_W6_Xzԃ$78"VjI\0lxDM>IiGʝacyZig3ѻ"'{}劦6d?KܵeE,Xz9xލM>c=9J/=ZF׾3pbS^I6{%4^`Nyw9GA/ָjѼͽܾRtQ}p8x.AdeRmlwDxsb``p	" ${;eyKgpt  0kPpcEɆ|MjpH~|㢇bwWψHT0acFVfGߜz7w}kysyX7km+\$qF`0d
͌-)_ݯ܆/%nOXʹO>Ђi<r
gdOk{㞘v?~. 
btxPNG

   
IHDR         
   IDAT8ԱJC1պXpDQ\a*]&.NZ:x?|9ɁT2Hh=6
fǘa3
ਰ+c+v2I'y=Y2|u-Klٴ1ES~
{
c)1g!0Ք˔ئ)1oZTX5Y7uSHPz54K'nJ    IENDB`Sy x#PNG

   
IHDR         
   IDAT8ұJa_8DcK A--]]Q[(	`C-
a~/~o

=p9u-c#|,Vi 6bgxfC5}y%\No8Bq0TBP2|}S<Lۨc5h2DiqL[m
>"▷)3
r!.86-(QkyOx@LKS&    IENDB`wxmPNG

   
IHDR         
  4IDAT8O+Q&n3XaAB^ڊRl	o $k˱wdoz9=Cik ;U#ithG<	n	\ib1&5R%؁3HN)ioQh@m\lKA0c/Ql\hoYtP{I,BN0&>seJ	0&^MZGETXAuB'ˌc!a	;r23(*lO<]X¾ 07    IENDB`Mfxsb``p	" ${;eyK#~?@BG/CCC/PRW	V3
JpxD30p0#ì9Lg2C"J"|}sSR*r@
M-IT+UAJ
`%%ٶJ
E
z&zJv\


6E)iVA.nP@RFII~yy^^~Q.Pnqe^Ibn^29.E%y
 ~bR~iT
@,*LCؔWЃ }C=}lR+2qh@!ـJ-)9HFC]Qf*Џ`9zd1tB2K쌠ZЄuE`wXv|hYᒙW72*9?'( 19!t!D&}$.}hчT;.?YffOSvzey@7kG^t'<­e+IN2杳]bsvoM_}{jr0&f7eW֏So߾}ǍqW/^\{W,9wC,?~Q!?%V=6OϚ5K4S~XyE%7s{>{dJkB{͛|_ݻ&OwǏ,E]}}|4柗ڵSOޔ;tiiQѢϟw޻*QE`:Þk޵O¹sv̻˟g.iRv㖨fںeˍy*>(2ة|l_zdޫ
ӽ&qN}賡jo˗D޾};"C;wZ*6NWlJKKYp=imϟ7_yޝ;zOf&''(]xEmk'fTWGFDl8hԵ`ٖF\Br&8ϟ{Ѩ.ĳZ~>Ko3io{AOt{d<S2jm'ݨ6U¦rVRhq*G76\ːTn<.dkcr҆ݻeeeOf͛rXzlt':Z;{ѣE[*K`HMҾaf>Gy&uS>ɰ=ίs[Ly?80Ʌ aEQ'V`,/9QQӅgի뎜wV_Y@@l0LԞ:%*枮~. s{5xA0s
٤ꡧ{eV=UjZi0C&ˮk;l*\yUī0`(e~hd!
c$dtgH@
OEgl-~#;uy)fЌqZy΅\tV^k[,{O9K3NMnrk`Qf_lG[p[1nZ9&AOGlZ'[61bܶ(eBL$ٺ>YwR$Q°P_{.2.o#Hf  ҝ{3R%P^S~=i15
ٙ:(\-ITZ<iw܊e]-O'lx̶ef+&IF 0el%x̶ef{&F nll<xvmB+L !~SxvmBˌ~>>  pl%x;ve3f+&~F n;x;ɶeB=/bSx;veBIeARNfRQbQi\)e%E/*UE&~1
@̋2ºi% }ś-xYtj =Wx;reB{Abrvbzӌ\\ ekxx;re3W- &4o
x;ygF2%Z. 3mxWMo6=ۿhOi]"@mf$""KI';3(m.E͛/*VBkIZW	Oue43`K+yѲf)pg:cJVÏ
OZtV`u1gTGօb9n"}՚>sdzCVNlUEfZQlcMm3CHR""KدڡVA0>^<$*L;Tڰ>KFՍP=˲z[ԑ֙pAzLRIN֠ܵ+&?Z]M"=mI?Q}(e&K9]ʸ,6r%i4}UI#y)p\l5A+̧4f7'~dM0"
bH-D*9%jeq.[M
X]M0Ugi`MQoቚڦ<KS0 u)y=g l@R(x³L``,Lg+8f:۟BH1p#^[ktR+~kx8q\Li˒-䒝;ͰF=7s;#.hE[wNFV;zWjp#-)	tc(tTE%K9R?qYRv-K:4
p{\<V{u`b,Cs =_" '̎[WpPLlU]q EH43
HdäcD{2߉ʆCm'uW~BS|Y#RWH6MAќXA![Ѷ)rij}C}p胱\ a݈TCb
=O8;Yv[>6[ÚaAxndxzQLRka"Ra1D	#ga-!t?o/~=Dv OjҴxS85һstw *d'⥓K>-I@qѩ̀N+z	G3~эs;=`j@yFáB|g_DP10Cq7m_{
^u]7!DP"ND\94D{~i<ay@Y f-q=$OyqZ>t`=jl}qoޫ_%.r܃c38u[CErgn'JeF} YeO+r55۾@]ܵMpȈ
Va_P0x    xYms8_!vдx9]h<$\t44~Ò[r\jW:ݯfs7bq3t>|y6|?/7I.K7i|G֛F~t|RQEGK1{ݹk>-F[|<+V);<:}|?</ڎ'5zU1V6S410:Oxf}Nt|6``T	Tv29UǨ1Gu퇿{dY֕ buOskX~|8)r|`{A2d>[pc%qcfh9艬4{P
e
B0'<GTO eR.Q嘹$$bSc?FS@cmodq`F)
("|2!;"1%fw&t>PNubЉH4> ( >0{Yӛ	ħNaDJ)$vPk2Fc8]!*qjrF 3O)JF+/\)vxP..A7crk"}=\`'豙i=\P-4W0t=NUS
z1#xb{EkXY6i3.&Vٍ)̋T?ڨ*&
ohāuv R3Pk,u-
+VWq=zCq-#'F\@
;tttTPN^FVNRP (aPK55Rq:<yOg¨l:*w]rECֶM-(;G*b/ڡV RYU/Zu*Jf{a@㢪I(6@fЋ^Oi;+M[p[7М4={
]7kµigf2E9,(Sq9rB#DkMc6ZG.E݈̎PNqk*0>k
-ňz(,ehxUSU_C}"^-aei h=X#)DmØ^$27DVax,jhÜ/Xl;phZhO3P`uX#Mn)V[jT*4K@<OPf
j`Dl9x^QB2Lᇍ~A)e	ϙ5_ʮ*GFLGL"(Ie
I|)4ݩ:K<A!3YogE̦TR
v <*L&QfnU¬;Z{tnæiǪ= CsD8]t,b_֫)].;edOGBnAј^d{#[r^AIc0O+T{m@(;jdxwo#B"VV,W5nHpfRY|ALD'nUQvzk-')لG|N
j#ו˩ʽ88|F^f`ɧ,]<Zcj}-<%x^h̖2J\QG
.RsScR-*áOz_Ҳ6u|iu	-ϡ
֝M1K|Ě ;c
4l-5 5m+7|$܅jePrG$'(k^[
*iYF9yV~ϨIdb$	A)X=NݫI0SGe3$ >v-j^?i@"1|1x_F8sW(,MkL I/$$uiyJ d$Ue[6d67id<m@9s̙s}lr>w/7i6xy>vQS|q%g8tY:
0΋b@E2Hx\IW~4wI>E;WDYl@OiO؍1?G]5A|f/_G{Vxj>W~Vr6N8>YoDd|/`I:ۯ'8ہü4
LiqpX$*oFh ͹st)|w`;pL3wYG4{"H`yAY<F{q/HeRW41~ջ
 b;fq|9a$BX@~:.`6n4 uN 
Q3 >8?AAy2)Y_oK@QN1&<O,hB{EFݻwp	ǀ'uqY&l7>w]$0~@iKo^篿"#;~p A46أay l:Ŀ?PzNa?tBXScL3E0I[ M&2A3TX/w?qv57&0
(OMO	|(Nٝ$x:
Î K/su4
4 ILswI NOaCWѢ!P,˵!&<Dþ>9v/'pd&hNi4wp=:YCUt$<Ί0n	;-7ʲʞ8}m<s9bPz.OF`^^_	y+<OA6jo	B)o8(]v፬Z2>M[y4N ^?o^ـF5+ n[4W
!X<SP ,8((o-}"L[F8`=\4P,̝8*R&mǡttDo0:Nq>UA:nA6Ņ@( lxl&hP-Zԗ՚	%n2|c)VF+$.QtCzf<* u#>czĽ"i2@
~6dJAA(Ga[	a QD
,+SXSQ{C؜Al!Bc{vtlT]q5Pn[V­ <XRFVt$-@D#D ZP1>0M8KGN">؆I^(_B=^4 @hYyaB~~OYAbAx \Z'=h[w[}HgS-7HG=DGLw	q0*K.;QA\r 
.FmY7MTUdN3nٳ YzGIDyfDp<92y
s=*'E|+p9|e3v]#MXKn7wCE&Z{N-<8WX5FH>tGY,<XM
W=Xf\ڭ9,Ej6*6A8
lN$ .1K&=q'Z p4@QeOh:)4T??B$"-B]<9Oov"WeYR H<J+jhG6`VLv	?x	slw F|Ȍ35?^}'0b~'Z\ę!G6[n\?9>;u	{!.ԍ^u=4<NS:Ҋ狝|sw4=zv(5@^H!A,行R'ViӑuTm֝|ݾ9'.z }͡E*S؋4,	~rËsQm8`tSI%OMg퍓"ɿམ޼
ni1~$q4B/ry:-?
{`iX4曪(˼`jt(ø<;sH-ptM̐&W~j#QV9_O@'޸7܌ncC*s4:V~c!OgC	{ Э~phWֱґ}
 YBg~t/GmBاOaL
zD`gcʾy6kOiКʆB(P`,B]u&4{4rE-SUEd{AӃdd
Fd?$	 J9{r*P`ߟf#eU!%t <9ȯ1Jwg	l`!r߻GnÃ_
ϏOvNqxy=v^t14;;<W.n$HaysYb P(fx\
TF
LNhRFu`c9r-ȸ\dj70ar8Iꑼ#Wtɦ6	nn/Q?{
% 0:rZJwR$qAu%e @؁i6n'}hsd*1WxQo<Q|_1e8aW&ײRCiV*uE8i1H/ǡTet|L[Nr4Ο\qܑn
YwPώźlJJ>A=t<+'~}
;	_$g1D|G2f`j9AtD?w/{Aa2qƢm#]Y5m"Dٶuךm1k}A?BI>vKQd2xs=S`rqeFwZ,YRft4.P`_G&	h^.`U/G	ݻԨjOJ3y}G2x%bd}dĽ2.GlZq-F1B@6	V]ҒP,sL1h MA$Pp)vޫfY@"B(O5,
>LK/rѴt;
*Dӝ#<3pM˔Ce^[Dג-3Xth|kۂQwC3ǂ=fmBe~</Tolܔ[-RߕYIoIGfw>iʐG~p{FBQ0e㯆jw:I oUJaG-.7M}>^y`$34 Yڌ٣t苙ï׻գ[9ہ`ޛC\ކuh zʹʫR"@d;(}Pl%X}V/&]5,VYl4Nyr<K.b62{zr}Fabo+5t8@Hj~CarB
T3A׉XDaʌSn>kv$aDb,yv=Xv|.
u&ŕӴt1C뒦VI'Xu7
fh<]ࣉ4%!>*wUXh ͗c2%tW͡Lh.:M'gY4.Ia)aKm(`*OFIcTiְ*Z4N?.Z̥
'zyy&.n!ZLiCR4(1P؇-Iz1318dA-q#>ii.)UƦ=VJSf+aqC㴫\Pce	fUG'fu0V`T"0~`{Z4SIe 4ឦ
9j1L:Dk!L6K*W;{/DZteݘQ33ݩ{
q7U8.@*Nƫ?2gg@\,tvSZ#:n̲UL<ry4>ebD$+On㔭IdG#pN`ٝ|mJ '6v/m,FĶMPm֔é0t̰hZl!IwwN;cx[
$GD)p3@W=l5̞ıΘ"auEfq?
$ف2}bB_I;҃(5x4~=>̧3i%84Zocsv
YlCFV9_."aA.uhҟiۥP+ަocg?yu+wɼMvë'Zܐ#s0,Jo(asJK<7FJwE"e!*/m1`	āBvtXH@_kw;PS50N`3LGׂq|i(@
g6yY3};8ly
D<xo^]
?Od"NqBG
TxhU籭TքmQhڿ}N-ѲѧFF"&pГ1mZj~es.ёZ%M賠:n+ ˺8p-@v
]0	2&r"9V5,+$%D몜$|ZFЏ̘36
f\!3b(X-Ɩk7$ [u2<e}	8[
6 4u-mN۾"MCAGY],]~#*:PV/F~ _gv[34~^*ЖK`=U^B5%\QBB,[-0UTJ/_iX:qpoMaٌ朧uGG݌\//uyD!&QR×tt1tUu0(>W:2Q6䓢t? A3r8LarM)E!(V)(wKـ65I8B}4m؜7plvlm_*SB ]N܎oI{ܓRxit6Nuhߑ?L;;y\Jr?xt!'rg(\	&l}nrlݝ-D7hh-Z憄ZQI.vL*s+ s1ԋaGzIrrRshamY^RRu<*
mZ"{A
s6\,0l.\<47_Q7u:Vug;!/͌e0RU~,/rl0Dsp?=Pm=5ޅX	.wT=}><6'X3!|a5,b
 ʟJɖ-K00nnFw{JkJjn6[m&VTXk-+4*=Uѕj>[o?X@ﴒ|Y5"nEs}KoWE%Wϙ]]#lP--^&p<j"WE1P}|@*oU&4J,G(?v!b ^l	<͍P.ꐭw갦0ByMWf2̊{GM/6z'\/KmA6+*wl墠r饄JAIpJGY<H27$$"ƹ(^)%ڝe`r	
œ
a*Oln|%Ы]'x&) MfS_j$io+?471"^z}QdCx*D:x>1J\Y"܄jl[<t!GCvN¿><O^6ғ_U9Lt2Jz
[Eu"`fƖ
6g.ɱl;+9gh:-OREqkc.ӧqqp
	#F JOx[LnnEg(5G1ڄ(2GL-I7ҋEA@X o
)ɢӬ@5Phӝja^^"0Z:iI	IϞbr`\{.oy<_* K'*}(8^Zɗձzb+4}y
'Q2p4=uOʹj
Mhņ{PSXXά/oпI!/c=Z=/E0p	^4|o SlGYSDeJUϯMǎZGu9?!a͕eĢcsH6fYQ Sx\ׂl*r
C@sʓZ8hVN5--*9}*l]f?~!o'ȇ9ʔ=xJSIpm2i_80XZ*RTx݀7¼AY"DF~F,ﬠ7/8w"5t:0)2X~3p]!9ퟧAVKim||餝=[,k:s^s.NP)4* A/^梤1.~&J}c4Њ;W7UU{pIV ?oNoEUqHR?
=8v]]q	 w+ưq#w*y
ݶq3U·xTH1{A*]|J-冽+
N?{^	)y-h?c	⦕`D%*QzZJC\]e~\P\n=MF{AՖWC٪zӧybtN	]SN3zP0IY.&~M}#7Փ<(8zjI.~v|8*Yg<:Sh:l^R䷀녤H`<=e4";|阪@hU[~R`!r^1{%teՇWKXE`e2Ü( S+b5 +ʄ.OAL
n
'cSÜД̌uS5k0$)QѻK6bN7)V%'v
E2,Rn%i逬XCU-`qɚ*"Lǣ^2t)US2ENO4ej\
Y` ×/d%xCsB# 8<
~!PWNk~p^n<x5^I2ykã Vչ_y~3 q./u
Zpt?
.Bcm`CQ"'`CE*AjTS΃b,;L$ך?LtdouOo8^!}imy|.{׹HAp(ͬJZĴ7c)7#2$uײN3L&ԬM2}m$*حA#_t}p`47lFYjDS+`7ʪ]Ȝ0	uwS4rNrտwuv.&Lo+|X'(b{yr6e@o&1	t
#|m{ vfpc@=n@Jz]j϶sHxG_JN!00GQTo)bڐ(?\\!c}TTX:3XoڛR"Ɏ)~8'rzҦKb%]FyM^v@I6{V(1f@4n4c9Pl¨hFmL2[7O9v(IFۍo߿go׾?Up}]<{צ_>k<Wwnt;f6GyIM
t8}E$T^Qm5]ɐ#sb
qaG#M^b=_ZL8JQȕ@#`\OpB+wj߷F`0$K2J4N.aUS1NJv
VXV:REelk6hX @)@]o>{roeu%_j &؉l
ꍬ
Xt\i~:ݻYt(?m4$`V+?VU}^_Ρr!FOjrB#.+[88Ε`m+/ |͕/W=x@T
E-x0>68y'yr3h':,I92XK&hV%٨:b<h[וLaEIPDcm"kŰuR(yrv^Xx{ؼ*R>yAMwTN𡞒Y<5,ܘKQ6+"n)e;llsKBS) udIXw`,׏	--w竇6J`,e_FV'̎NUiBKLf[n14V>7%t+ṫ'*]
	yYֆvŔ[D4pZ~/8JjH)T&54͖p6i1")-0Jۣig^AK,@_TWT UA{F!C4Bjr.)\Ơp~2LѲ5E}At@)WtUY
o-:t<DKt֪i jTU ٰyc.'1z<TicUeNc_B_%2ӀXX&9Ygacvw^!'u֙Ŷ;|v`wx  TQ*3kTc7"dr[Xxvօ.XzAV#y啧I^(<l0Q=Y#hzXi]ŏ~!T{o߻YZ
֊D"#:!~wݹ.]$u)a5Z1sc_	6vA[Z-*TWZDe=}98p;$/_<?ɩw).!<Xu35vKȝ
St2kzi_}?J3I_>Da<Vl)1$ywXL^TXjA_>Q~fᇶ\+wxE2.&;j#i~bύԱ
nv[uy{xZb I\pD=~Ϭ"U"eM`{fɄ涗Ty%׎ɴdOzYW7?Y'ki/`Ł;fKu s?2Q@zPy2uєtҸYsc?(`"ARA9WVc'v^>)({{o©#W$;6ʪ=#Z-+܊.VU [)qWc.FBuirZFZPZtyqI(nO}(|
~mlt#2ʭ `:S)	`jo"NxT-YM&cK&y7 uV6.{1K2vđ?:GX{<3=NsEŠh.VɈLil<NOUE7yN
PC$:L&PɁf&#<$t <3%I#[PJ{..KY6rEUC%/ܤBNtĽ5}t_'p~5BF0\{^QZ%_ā3Uo"{~z   gdL3[R>\,}ٹXT-]kV+ը3ba_,6s>2r$ҷh=Rev'ȯ%	"aⵅ*LM2g.S>Nl̲e<#,+iHǙP$b
j2$,Lw=bb?eFbsAMw#^`KA,	:FQr6(9@Tki5	QA˪lNHÎ#z3XdrU`TC`=ߘF5~-9z 楁ݙzoU;كxF`<%&,ʮx+Pxm+FRTL?(8mE@Q*P~.{^~='Eg?gpG_=klFe
̒Q9יDU2g'o!{;XzK_0疜,oҥ
lǡJOS(Sj3Ww?eE$t qZNiRIҲNbqIڮș$J?mTǃRK 9L:*ׂ71)TLSOEm8v*8y0]{6.9%! ҘbՊ	-@0	*u|B2[ts
C|6*M\=eF@)Y3]='|[lvwuJjJsm.F*r>Pp)䍋1qߒ284Qz'xa҈AoFN%tPIig_MI]
}7KSB
eO=qBB&ғ2MQ(=䴏1r4*ixA6+ -&|3T'idq&Y,}	YGjS,!6]۳2Bl"!;>|K9\$7ӑXHPAb.eg"F,,J*@B-uc#n2ʃ46
ɍxH>tw<.sפd7[<U5B6O}l@JiYrty󵮵Gva_*--QXIaэRߘᓲ9ƪ̣si@.`P`:&!)]O]u%aZG[CS:SIIyYиO;'5W:P<=|1xlTu8I!\jb`-<l:p!(0C}dtǗ=vZRS@Gfr0HOOmm,'I`:Yt-9ZIbQFju՗VTw0 071seUKYi]9 tJH)l\ft(Z.ʓRaN1|WOSN;k/ooK\o
?5#͔l
BA#QP	$>Tm/p0uCEZ-1c۩%2{Fx=7<VSuP.mT,h*!"=SOWⁿ[Ė5Kw"q%[rF,(Otz.9D9D.1*T`
	Op@]tZp$1Γ5W`7+e앭H-s%!<ĤL!3%ʷ?Y)Rg^l7O۞}'pF|PĞPKE*{^z;\~FeLA?|o~x!cmJЛ;/-F{8]xMqxȸ2eEWG9c*2UVWB8!D8(coۣ4TҌ
Ii'Ti;͇q<i*A,N&p7
Ϥv=S䉷Vh?0F[GV^̢M=EH#E&d؇OE7O%13538KwQxV=,Ϥ(b.3@,mcŁH!RpGWdׯº
*n_a0Pa}z1%C^(^9 %A<
%
hDa8U,1͢,^սFqFA=Q9-m|J/7)۫?ٟUu63
38GbbgsR%	xtu;'V?;XxϗݣWR0qFMU/:$='e;28/Ns7itED	?>Q21AL|C9ăH5J6ٹG_l[y4JS񄙯<57γ\#fڪoPɀ0%{~wKPs#$ih<{<nELut=t$F=y$lXfKXPUŀwaf=*qp!,X6LPW"΢rz_dD}"먥Hc7v)?c[@6~sכyaa{ɼ8}Ȁ\kHmB
iu	+f"o!qF9	.)nB"sq7^,ly*g
%8N=<0_*$p (=+dmL?Ei[p
#8]UyW7=ZYWâh3S㖌P
Rg+fyȷm7+sB$ZuPa0{NeLT"މtd;	mFMڂV;?peX'wC* |9T6;8
?˿hkɣ._O,`P*]n"Wj{KU_G(\1F K'uZf!pC#Fɢ^?uNp=9Pǂ5Ή-/ib.|46\o
|\l::le9DcۃgmzR6I|e^8;sJK[Np#uzd#	{'iZBIwH.	ÎjRxiWU&=bӱg)uahcn_^wl=)]dc(+ʅVkSItl5dNg`{Sϐ]|Tz!#!P)߇\|
餹꼰smT%ǾشqE&U${8?%k5U&kړ5Jqh,pKy?VR7
hhr3| Wsod$WgT(Cw:RG棯
Rɵ#jpFEn4~߼R9΢y}Cx?QtEN(|}KW%2 ei;hzML?[bolո` 鵇|S&S+_&Uݘ-јu((8Xxs7~[p\Z)aF%'4ޗƕklm+CΦ-k*2q3`tRKJS{Z.u(`.K>^ಫ:F2/K-"Jo~w6":·Et6W.֬
cieR+@U
lXIaWV]#"|6u<I$[SՊ6ƋoYSZ`gbG *@)6^
 [KM4\?Q'>4bD5ōanYK.Dsl/S[]XzrN;Ք8\FΗ3L$YV9 )e,2[|r)h%ȼ@A\c)7qp0PeBl<ʃ\$g9#L%FC}"j?ˇWZ_\#,D)*
{"UOd1S&*"A:8HH[tjqB떌jŃ#ajT
ҖQ
`w!	92AIHOUS.D;2lO<tU'#.fU7`K:S4ᔱwcFrGiqV	0wp=:f<b rSs>~vzKp7v:SR,߀ZoP;;*GkZ
w=Um#y2!P4ʩ>3o3+o
~½!Y|M'70gn鸩
Ƣq#(%SݶN'#k$7d
ʊt:%)k˵sK%'Y+?ND2(ê$]ӗ}XH	Co͸:lH4zA4h>鈌
d1㾃؉p*!2Vh`N(/ԟ_~U1\rʸ%^e)ٔaX(g:U2lB0^y"
sb[^L]yT|2xoY8%@"%0>I.?D:-,M%lJ2\?/KUIT?R4slyR#Z$!byȒ%93$M5ۉBM$B=dOm%P=?DuW3<<A
4)-z@@r&*%\Gnwїʂ.嬶|Cq3/.g]XL~)*&
f@EXqGuf_jnmcvQ8c.4MYrD*~əh0-Q{:.(qDfQw풫b!v+\Uf\ofA2qL_	?C-P7-X̑F#nw9էb"n Ct[r^Qapzn~@%Ϡȃ׻J/"bY,XP5ǼgfВl4;N1 Q:*

Ԏyޛߕ9'rWT5!"R(*v.{8a$IX7G^8P*{YسgE7gT.[l}z9}QZw(w˕HEDg%KWFWM֍QPC~vx=	IIa.v
e[֌+XedҬNx8\=_\L5G5Xri(էRe{ mG.+'|OemE)-{SԦSw{*T֪=]1Af3o3C1DڂEo»̲YrgMڪݛS/'BLȴjvhACc~i)h$b<9-W̄Vb0`ɉCWpVp,JZdSR@
%1ڞk$ P7|խBo]w;T.
nqnU/ܙd̞o)9g } K0hn!k"*+IC'BΔ\zñ;d"ϰ%ƨo2y]m-)\+qOsc+N u:XJ9ֺ;~4vi:K	sܑ.':G2^Ibn|=>Z&ct}LFSM,+X3.|i7ەoq-gZ-\E̨$+Ȧ+{Zp-m,h~s
!HAE%Dz_Uv?)Ŵ_
_BvV`C34YU/%A1F[yUJ^BJ*8dКA=	3ĸt.רK=詫B~eL>5epF:Еw`:p:YGt3Sw!h{Aw~=ȫ#,[(<'VsT3mje*q]jjhjq`Um-6۾T
I sF\ 
OId#Mړq!u<dXډ>$Z#",'P%iX/)/IsP.._f/%q6iI8)fd~Rg3h.B/yTGlhpj	ŖwS\pKtb1:2dj[NPBo~Bs
:GGE@)zmbL-RHyO`w\DF=pgY/0hcǱJ,kAN2#ͅ2L++9KzW7bc@U6&܀!@	>f	np+iCf0Z
ݥ/jA?eJߙd-Mb,'pz` *=9TCt+.G\o#ht4g	#Y29-V)jw3!b1oU,ZC'ߪ=DcsS$P(Mm1K$x8Y	;KyBmF}7$dԌ8-JTVj
[7HJW|[>f)d-(i%螣Џۜ4i u,U ڴ
I;nc9ӬތuTEUbnU6NVG%9*-fŽA8Mq^6tL(γ8l+$Y|Ea^9h|[AFI%Fc9=~9䓼
d[O-l$ÕBSonFO-bACkƄ?ɬ쪺ʥ$G٠I"yj>Kzs%$w#1ٽϖMcʺ@NkL?>Қ) KǚIz0h.ڐCZ-_j#T@lr) *SFL٪mQK-B@絓W[~͹9]_ϸ,@)o:̹qaëh|M6.
mlNgM=v?L!ƅ.Zx\PeH\/:å\9޼
 s
[^liW\34ZRnm^aziRG4S0bOs@b.ԫ=&AIkVpFٹjcJA	!dZ?Ӥ`P]qՒɠl@iH cr<vTB+nY0T	9ZN~ ``s5ntdI1$]Qջ9]
Ups
9r0%/hR
"?*7vÝIE z[3--p&x+;_8\}/JRon(U1rúZ2%ǉ+aMt6sA:QLFkkyҔ}tPe,%ÎT-YbHe2EO5㒮thպG-Uiqͨl\Jgb>X]Z+xQ1۵0Ev?mfOk,
Fگ8k;7@['%(Z
Ӟ.3BӡO\Uzw9	c92|9Kx=n3nyr]}C2/u&6v%}`{6嚎g@;x
%/i[@I'e4_5\iChZ_*Yb}0ԃT{b1|:ҧ"'0]\A-aKܖX:o V(ƍEy硭o\؆^'*;Qlx	x[Uz%aK{e+,]I
%_k:5>{;2QLe=.E-Tŕۉ?gt10OTon2VfWuqUUXp<L?ܶzX5AY f
koZ`\C{Hߠ췻QC JM`PԚ.\iNدEMOqH,Ai]+@t/+5w~s.$s~{"6Q;O=C&֠nhb"\s*Hy6'Ȣ(EYR9n9BKets|FB4ē2D3RgJ=iHNj!>`6	AݥTSJX%(a:VڟZBIQrtYdo'j's3qR2>Ng"*1KC
zad?`Ǯ66T;2)8
jM[(3gY%/|%{Qq/Eq-z[ơbכς
7wK/w]9uQ<HgW_
Ʊ?}+}1<_}-`<w~줂$iZ~9۳s`3_#6:r5Q)Pх9BrʜǸ8z):{:^﯑SK7FT
$zi:N0%`zkCv!MSwfVk{z! HKMThitbKno~w~~O =dQ;E<^1	]P9Wy_(qHib@g	
LrS.rJ(7ԕ
pTʫ͹_Zu
ƳMw11g SvIdq4$}5V|eiJ϶Тw=9{ޥ_{;9vNuke[[ӌ	U>@w"y$:tQ*m#@hUƜl~2g"a҅JX0ЈZ VFijt, ΕrYa++OCƃ56S	BhEmT|<-ߨV#RraǏ@Tz=-)6B#Wd.(VxO<]'7XM'Nv.Fنt!VV=Gt;G2@gĺ뷓GW)e:Q`H׋뀉
 98Ac0ߧ0N0j+3xO{+mrsi!gf*γtzvEh%>O*ān0R&c;eM2q	cfA9wwE]b
8gdthJ毞.eb&V%+Cf}uͭlW2vlo/2laн}Ոq62kMj@e7Z5jxkW,nAm63KboHQ6ݬzӹn1C D0^M{RlVF JY0WlڈƳ>%][˂h@LT~8+/C(`!vY!
"c`Mm9yrZAbWȸe
EUHOa_ӓ@DYT:RaA__D(Lds0OnVɆ4+(J}ܢF8=h{f* Sǃ!Ǫ&@Sނ \)3%B0M?L'E~)y}ޔʈ7m^̚`0h]-'xk.U ox+y#kxv67'I=
ȿ6wVG``
ԉ$ tG 87uU/u."%@
jPm^6,F'mu4f޹Ӭ?8~-Pܷ\D{u.`ݦFzWӮv<2*9+hU^k5^X(CP3u toA9_5)s6 =ΕU͝xԉ|8 5	3OAc](S2R8algx:+aYXtz;KEO4Nq[ Sn.(dm%O5,/AIg0˃jy4$qs\@;.iJSuՒh*C"Xk`sP/U@S6i),a`dYQ[EBwϭf['+ש^o<Ԯ`#&s/{tdR	+yx{)%/K+ʹUݩ07j!o";-JoYH-fLG\4Ň*x%Z\-O+.`	.7P/PhǍG>8\q<#Z&K:FZ=J327c_jy,#Y!i gk uVDᆆho9~:M[)]?z갼vBg h˒WqǟjqM<˕3?~?Ǐ%F!*®x-u$8 Iz0^*ȳu8(;
gAg7l\Ln|hbXy88h~RMG
z=	Sr%snuTi V4ĳ,N'܊Ǖ}.BMŷwꔨv3
T\hFo3I(@B5'4w߬+QQ:/wBWѶuS>nH ]"EH2so[
CՇ?T	C
0"ϿUYb.,1\(R!m ד0y<$Lj0 !Wn`ג$ 6gJ*9q -V<ec&a xTN<;=ڴfٽwv_<{(7W-5L@'IM(O BM#\3"+NVц/1_;8k޳-x&|uwxw5_n:89\}f uA]-~Z|Ώ3DMg@houY{W~k5s,,+tSC-r

\rjNж
M8<'$RY w6^	Gިc- 2kx	3# >FrZ~X`h޽`V[ьqczxnl>x4wNSܞ$~o yթ"`~aYVEvDXeFi1Scb~я&̯Bdri`졌g,aPSB(sN$Qb9۹o.
E`٢w0h#yr,T(x5PE}#	˕I`K8Y}VU:#@kz} irl6{yٽ>|ur睝_{|z.~y9wV_(c%eãp_kN_ ;>q?ܺ:ݣWUhx]~I`)Fs;<?iwboV}}98$. H/֍3Í{?vhz6`ﰘ7F}~_l`\O-yQ϶,#)ui!R5NUњoiQ괴iwVQTG;xxtY)nqTyNMf⭋'t+{ >\Ce/EN:b8iFRV곖d#lkgi5*rrk%,G[0j`.v%Ul`
E4vP5!:CN'"zY޳
#Yimk:Zz{
҂	Pr_my38{-\HnʓѵfәgG[dD5a:N3㪋QA
0~!Nt.N'<z3dheIZDCCα&jJ XH҉LWĩL'X.WE0+VghgR9-AbPcIE%2T <rngU@[YNvNpfbOK}ʙMY,
Gur
O<uӟMr_CB1e

Zn]4ʿmeZVC]ZM1gb:%o:F<󫪑Qm5Vd	.S4KKvd>Y<}7^We؂-C0n꼏5æ24T^%k䶈UB'k;%;qRq},s2bڇ|Jq\A7</GQ\KҥB}4KCh΃	uT!d|S]>p3cCB^ehpe<84G;]>Lcʧ '(/Du!FɘRBE. bCk/ԡIf1{}QQn[k8:%ySGƍ-F6Rn&3r
_5w|pm,&G:8KVP)~ߜ2io0O`ѓurFV6?<9EVCYSErƇN4 d*'_r %aLXLm|7'w*i%mu6vyUBHϊ-B3P~`d .i)aA;EUΫkĚN4ɜ|{~	kY VT9KSu9)=6`9L
c}}y9tI	PM76;'WK<	ǁXubCsfimVC!':/%ݳR_67|rm	2^k#dhmH@q	pKӞ/C(ݙ{U-f{/Tsq_&y[[aI6I۾vΎk(n$-9.Un9Y*vjWTssTz=V.ZEa4_EE7Ccn?j?h
s=߬@yk?fhS~89ydȡ\6DXk+`i<~[ź	7
9 Gvl}7hr&őҗʡ*㡹dVZaUіC_-rzg5rOnzX #
"ԔE?WqK:B qrgR<)$|L8̻KN:E@x2qJc\Fͩ:Sdju#1
{Q[|:B;0lnn{{͸.~"ty[ɱ}T{E 9l'^x@, ࿼|{raux/(KM-.HLNU(O,IH+ɌpK-r/JJI,.VHUs) W- ޏnx=w6?_
֒umk{N-BԒ7~YN{}/D`0oVՓ[t(b'a$N'/|B$G2&y"\4V
7ADqB~4}zi:8c@*[o-`ymk,">ķd/yRJ[wPJxy4dE$~fIp(	2y
c-c?+:avق&˔sWopBr`:QJ/Z_ F#"^<O-MRNʡ'
tpOB/+ LISP'i`\SN)G}Ogo~x
9~՛_[!U 0ċ{Ӌwg/$/޼:$/^_cr~|r鄐KHRN
3挭@if^Q~H'`hp{d@o	OAIg;.	@ܲ*
B"vY4쐯}KtP/9"d|jTxLvC^yP>E]_:lSB-M7o]r#>}͚{ާiFNhB4xѱ@߽gaR:l|Kjo{,5ib*U@NP1
1]G~΅gOJ[Xd[A]{)j|,x踒f 3PM&	:HFmԋ,W]&4%0B`*}'y"ʃե77y@Y-Cʏ@5gNl-W \4%{m1cfn>prYahOoTʿsajN>C8}~#׿mM9 9voh4Ñ	wg!a	"ͼ$çۨ+GU*f'D'wQP*:?jg8`\&aZmItꉖ.Uߊ.5JځKu. xK
By[tes[LRx%GSݹL0јInԽg+Z G$gxz	
D9q&4aU uhJ]tAVܩ ޭuQE~i
'IhN~i1yn`xj>Sp)Zo6霖qeҥ!XI:I@s>^mۖp"UF&V)Z,prWЈMqh_a| C@tF6<CiB(ƣ\ 	#ʦT˥ /A嘗\
Ղ<"Pe֕[sBZ5m1>/-PL}X^&XSz\<M+ޑ芫%z߮/jS)}fj
JДYF.!v!΅ qF V&rHҜI|]L4E8gSS2M6O$ꎣНr9nJ4x}DN >8QZ8ٽL}%G3'o#
!'<Mll|Mw&rT &!g;׮	
}pmB%.)_mМph:FیhvU&s˝>~@lAbl]'|bB$fKP;AřԵV< wWx_D*O@pf%-*rٗuBA|`g fF}lқTRi楣AҠ⬷ʮ(֩bmcW_cDŌA+zWܿV
eLCDɲgނŔn2B5a6/'?`G"{Ѕ4CT̳d<0,37ϡ`Q^gӓׯ_y!s4Yڝ\tC,}5H(OUÃ2&n*W=h,Gup|meBtV|C5D)M\jiE9p4T?uk;F<i'g-6Wf$G߆S\%AYrH+ CeT7]41_N8ꖦG{zkPnU*5E bHrz;ec%^nCI1fncn%lGaBc*Or9x<i&rCPfJ;uَLo}͂U2#l ]uaM֭>ׯOexco90-0ьoz%YⱇHueYr_]=a5q:vC4rK2<8|w֬?
%kjiA_A*AڏJ^'*0vEFWeB*תB@Ŋ
(vs%,U{G3frfJਃ:ږ>`J	"
|Vp+6ӈogq[wJɉ3O!BΟ_,kRطAD4utgiOb7Ϲ汘rM&YZa)nlc\)Ρ,MX֣Өs0Vg˕LYеJ{U%7e]C7WҽArtWR+ͼQ]+Co.K0-SO})K	cQU*.K%IoLnp^M[ICmGjlH[W2T^xSe#AZk!RB}[H{r-v*i/3]% }s2y:wʭX8pDdﰀȨ6%D-k`8߳F,4j"њI߼)5/>d:h$a!whaJfMy"K{h־bN
 QiFУص7g߾uy(+B`"x^o1D
q(yg4q&y9;)xW$<up^{jxyw,1Q/%~W~2gqR@km冒r1ۡ|Ww绎c/nݢe;"usM;5'Y޳9l2ղʖ0}FRweYt0:ڕfn[e3X
&>D;dNwIs'=x]sof
fy2-kWZ8:V<Z;`xꐲ6J~}{ʴ:cݳ50eq8\sٚ4a0!sxLx}^@nz6OC)p?K"8)r1El
"u9jTAr]To^aT@l`E=`sm*0Zcl>@bh3@57)
,|.</DF'p /Ł$_ Fg zk)8&)7vmj &'RnUaN-]YV+$]U @^WaWq5j;Dry&|LuR>e<a5tԟ)vy[ԜPCd& +%P1'r?h3TWS/Qo.}<nE{bla
U5]}*3j,B͜,aôsisBP3~GP5)f|3aNcg'NmtB:GrfQ`{_}'{{9}>{74(ǒPZrZ`WULWNZl²Z0I+%>mT0|eGh$fTH[X$a#_+l	kD7̋;kb(MMG*Tφz)=yF~mFJ(fCWP
 ~rSSВ0m
X$P'Q;fJ#b"p`
ɩji߲ ATm~뭈XIW?{SI-y-'wZxS%c*O;E/oP\(\iO|*Vux qww#Ð{6]ߓ'Oި$cC٩Vv:AZM6hDzK>MidvX˶m:Th΂t`
[cسZ/cAaس͡h3.1TBB# ĭpGwM؍t
;
ȳ ě/dZ4%#l=zGO?(dB`$3Z}ه±x/9w9RV+tlc|$} $-%DCZܚM&FA5bIE`d}*\9&X=r~ˍgʠ:dG?yt TҫqNymLkB76WdK#Q
=`>^pü$Ȉif蔉XR~LUD@zlWh#mС_[ VT.17cG	ĠexNBj@۹LM|=&A[g:-NnG8Mmy`|{$5MʜykkBP+ҁ Vj*AKy҄ 8øڢ2XVzY?`]-ƿ/T  ~Zam<KW^,`0HqHTsG檼@<lu/d!ܑPҴQR▼ѕ4\Z+6zգ"/u<Y(bzKU#@914Pt
s
<fSMI=^k28~|c*9QC?iDf2r
Qx
<aqS,A*W=?B!]+nK3ͳ<>~^]j̸*Uvx®?+!O=7Z	)Tkӽ:e
)SW/Ch8YjdsV\Sjj[jyf欷mÞ͠R.^3nm
n{V4v}}x򆹼
Rn Tk*c
/*kOU._^T_:YBjx͐49Xy@]nNf#]iĵ\7rR/tλyb˂vq"Gmhr{QO&
KK@)m8dSk$c|$'v6[y;@,k"Z\Kpn?3HtJgDCאpMA?Yu%YG& -P:T}"m)fM)>cٷx>,-+޾f8̧X*mV#֞o(h"QO?o?^x@C`a17ل[3sFaRjs{~韦0ݝI#Z_զ$hi7Vu>XBܔŵ58|W렪#{^ZdŠ~3J`i\
֧r;Vn+JԱLC7G%?fXU׳ 7~I!@ySʒa,OVWT]Pa<CpWV]U4J*S6ԍG0_cY>;+2סj8ɡD#ޟ`m8:\`;dq'q gۆOQSi}:]t;Ju
$.H[ﵟ巒k߀@QY8$7G)$vpYAaDE
U\WǻEp(@j{)W҈׳LT;/9G⟼J%>q(~4G*>8|d_;A|YE,sxp;G gqzqWa;G`G\uC#45֍Gby̗`_e65,p:s4lKoS4뱬nGitCdH:&y+xJ  7'V5	B]ƞW
"\o
6P5$6ZZ
[KOBv|UgǸND-h-(N.ˊu y9Y$6KL&+9ۢe
xC%ͯX?Q[4J,8KjH_f
:Uf~ӎ,3_|.'0j>S	{^}[=g6aQXDev]o*4Bzԓ٧ܛ=(r|'\V<)Sד^5B qm0c[`!ؘ{
2fo
ʽXK؁%k̕t+[«kv,=kp5g%<VٶM^X1+\-1-;Ɂģe0SE=|VF=;6*oNo2}v}?{жmr'ްgZvX~Ұo#}qaOxF%gnl?'+f73>|.bKC;~A҃j_v,o_vr PO"8eRJ]DHveGsJsWqkڻYSS %;d#Dx$$}}&axAJq_ɶtSn3\eCG8V{;_Ǖn~D^h9t}بT[_Xh*34x3`Vc_e;r/[3,=1
Z19rH n-%-j{ vJ+u{W_Y+jviGe	cyJĮ;{vVj_$ř0oNgSk3wRՔ.}4^ ]s禣@5)oDx= G3ө[N/jwQ4}߆q-bU
_{Gjks{ۢi-y{3:oYR=DNn
c`ճ%Ԙ7AKrJ>PH#p*B-/y) 
&))
#eoqXn]>1eW_z)M*;N^tLX}Ǝj-ǌ4ָ,	VݏNkYg=V4w<eم~;d^Mæ(= _r$𪴘ⵔ.ËwN0>)fk 72z٠7*/%QD# <=y-PKMJ&N-yDL+W:v
~doS%S<I<ː+D/M[*>!Y	KiQK	eﳄٳ'RSo/	| 3\$_Rؾ2yR!._Ni1j,#0h<\6qq'sL+T\pyXNd:U@Pea_\?b}Z"+R卹"HJn,xjz3:t|
fobXHh#g_`|VB7
sz1WeCnOI墪eA=l@zr;&j)x&tNz7	o3<i<h6Roy 4
^L\Dp&r[d@~\Kɮup$rf:oa44d0Q97}P>߉RTA!`~m|'
eG1.~5m[V7{*-=7{Knffw7{+7P/ KBMҨ_$Z1xy>*K`$U:HM&4/
> {(]n@}AmTzGTַb}u}wXna5g[9DجxGISΉ5]4熯42[#fUڄbjyVʞl6B>Z~e/6Gj+uwI$ڶM.t`,՛G-W?NN!5iVepG5=*J6e^0zN/nST{+e vtyպ*2&{55gz<9M!t{K4̈́P,T[rkO'*Ԓ<{*}J*OQ]KRK2u$\Eق8u&yĲ't`B("r7<KJaMv*
6G/S\ZD~[&D.W˷ߟrONN//KQ8JTsz'ǩPɲz("*")b[yQP6m0IitE/Z!:lHbkGEA2%!8CM /
?3!}W@}zu=5fxROAN-X.?-GhiՀ ƘniYi3)G&0FMLLЋ14bCa}ߛ͇'KNN{᝻e@=焱+W%X2D&p j^
+zhPhs? >(E`IU;.P:jR[~lfDҜ䐁2A
a]ryPvfIv)bmRf:`߮ `Z+<"x6;ڙ
DT}9iRj6ZcL8#}&W1̳LbH9ᵇ9/ݰ=vDko/wP+l9bxKrIsbX@.ZʭvAoog(@Xp,Yhy	
,{vzOĆ8ɟɴ!
H ZXp@'l1\vD9󐅍%.?ڲv(*bYb_0.s~zMʖ:i$}"j_YA4!a$F;KH
}s56n^xkd;s7k& 6mxxkp;cN>9Xfׄjx=ks6_O;=sرPIeskhxs$G@(^T"Fh4g[-V[/|ǿifiom>bxȆG?^1?0/Gx}%lf40ϡ/vb?χ_2`]#D^>:օ]Eh͞E<}^,szty(`EU^f?Y<> n5e,ʋ,E=,(:EȊ0[,?}~o?fii&y| pƦ*81:)(MY]Yr(wLN'c
onXQ3i*
0GZ+ į8fӐD`@>엓W__Ύ^}g6
9h# &IqsR yqv
zxW {yr9{ӣw/Fdi`Ɯ
ŹA_A r@9`!,HW7V` ǗDSzEsŀ]g[r@$bN`4`Ov|KtP/92Nl~LACoݝ_wvs-bǗzz4NI顩I9ON-iGWG\?^~k(O@+s$1g?E94ϑ0rLP:pa"Lf9uRFş4K
kVtG'Ɇϯ/~tLWYt#	  0%h{?';o~߻;鿘H$
 La%pn´|REE0xic0{E8K(n1xv$ـp%ݰT:L:FHLDN,)<q%l%4	(<,xb ᄴ`G*1'hNz9H6ĥz́.sPIݐbz>hI.c
F
QR%`;7YWWAx_&Wgi3P&7y.ԇ7z^^5yX@ED+& liedqS9AW^[+gvy@;9-	WB&xܐ}Ê1ʆ4<|\5MH=ד=ɻɦl	,5u0<e+
dT'VkP޻	T{Uwt`&?81RK<[-.M1T-nN[BU@HCȸI.l6_9U9Q`i*,$ӥeEMDk[׍v6ScPu*$l#pz@7`1T;)2&4\V^3!{N#{Jk[RJ8 QwUCu|0NҘ	܅>pkr>)%TyڳxH7D.ɫa8QSh E
]!=TK+ǡgͣoB`nS"PW !>nH7"(V4	KnIX;Z,]̻Y\ m.McdaSȻ\EDMxLj!ި+э&7)]F{m))&!DX5\4S*:bIoWr=JxMYtOe0vm$iͣr,2
{1|G;x_y.gzaPi_p)!i%ԹՅ	6
}&4r2c<Ki}2L*:\,p{@ó:=-@zϦ>;).9b@_h)++M H5$\m)]{E3 r1&fS-@oƋb`g0V\"Gb`Kh#j:gO̨wl[
S8(GSqf`L%T֠Ieb1,	.I=EAe$8bDTY匪r>sksnćSfEF&@U}[VfCS,:L7~o-Kڲ
^adkpV\s	4_KJ=LkbPli4ZyϠLʔG	
?sO.p!
gkb$)2|yyb3xeOh2Hti9am>E{HXj2#V3Nձo3jBgJ /7PGAX4cQgзo&n;%{
'/ؓ:dQ/ir#)^(B ~^Nח4dg[Tc1^bC\eu
%XxۣG=^GWM?À=*`q7i
!Ӄ8XU$ S.ҭJ+iz
N16Ԇ@`dmAWFht	M1#9*BWT B/EWFITXВ"40*(61/DC:?Xq!_!ؠm0Cr</w+|rơ}Fn@F(B֌=p]Uzxky.;hF(Y҅[;ȺHm߀CD,j`%TiRNG1	86k-cm"S!;-=Nd6vt.Va"joGL#H
mn*'	&ߛ Z_qc!M4ka@`@޼yQ?<\y]oi3QYUVuUjOHWo~1Dٯ"ia8]QW?YMWfP{z^2T#|0ԫ^)읾:9#UtN`"M@5͆Lo<Y9Z}(53ElhYjҸEi |ZVI1
{aZ~X*&oivgTY
YK"	$|Ƴx{	q_yŰf}>|zqJiI=Y褄{۴cX;IEĜ!W8bmkSOU:62wL'|0TE
ܗhH;F
D(E=X1%r6M-q~ɛ *|.PSK6td3=p̈D$&=pʖ:xEMHeDM@;TViD.69ҩX3(IrVfHEMo*	z-qsl%>t#q숊|!?_D?$RN.,n-F&t^_<UVd\i$Fu&yx#W	["yE4BEnnkM7pV;ŏЗyшJz*wnO4yM~E=k-?gI6Ftঀ 䄘sbӤN..M~\!	r5mj琁m#
KhA@P-t]-VIrU#16 /&4!xת6'!Ǻku>dca]耩d\O,,eި'r٪!: |Lzd,1lZ;s3[dtكiXUzW@BBi@,MخȼA\H\ġEs
wI
iGX&!Ggg<\^/p@jt҆:NUced)Rtwk.bt"Gg#Vl)P﬐v%C9!>Sk'mz#$y.[2a4lB1Wɓ|j1GugշZu;!.zݿ1cLJH{rK<{J Z	D5(}X"ՠg,
^S"(?޶tnѽEF&(84換/qɔd<aBzr'.upR_
-бf9)r~IY'b_2Mv>7](v-s=/zf[$1I
fKa47ճKw4SPʑU˹=׽JؿySKyijcf[]`WWk3`q\nsJ	"IqM:3i *r+mؼ>
c7D;ni!
bpͫJK@H;,,vVd!
qcDujT޻RJF[D``VDzŝU/t.C
P(]nqr$y}$jѮUpڠzިiuR_caHb!ڪ+f },ڵ蒧# .HC]%mk3-<eqV
S?ϡ/LRG5|8khWD1-(Hob),g^ 
%Dŏ8ݔ5u[X8E\Zbƒhڲ.ImuMబLֈ95I(@`@:>{wMBtӲؕIεWuPW.@^ZFxS2N]~֤$@XKj)6Ub{Ӓ3]X?!b
wZ
Bkh i	C_%RQTkbNAiCDVŭnߔi]FAStH8
7m3nOzGƳTlf<*bY)6}l寺Z,O8#C-L,K9HMuަ>	=&q0FB4و*
U
8:+Mn?a(/8e53y!Ģ>#4xͩ4Vɥdj?ϳ8dНI8A)kYZR}g%.tP~m
>qaPJ,GN޽teEs:HM+A2)$?e.3 #7ĝ}<XgLrvLBlغ/km
NhHy1MɌ=$/eLbXС~Ѣ+Al)/10c@0a@W'@Φ!Ȫ_	PbeΕ~+:ͷd#9TgSΒ9?8_Iir笈X-<._R|Gh	ۀlڭSLu(8IItx0/86X~oNgZdgڈ_M
mŘuno
I!_Z7{%
'dW\:RWX
MǕJF%]Xc+̞Cms!~O5Fa)VQ1a
D|ƲfE(9؊CXI'sD9<mhce<n4ypf7aƅ"O
Jg*ℿH |Ag.8ܾrfK,ge&*_boݛӳ瓣b
8}ۘ"Sȹ?NN.W&
idb\vG.L7\SsXsډ[vN^`rpևhƔȁEi]feۤ3w;hIQzRR^}K@gPw4`2!a7o)xӯ1i|TM_WZ4@TrC?`3~f_LZft!Q*ney&sJ9]<]Xy(E|/%P@pu^i'C/aWsw|7:{Hyݹt@4;hACvwzlP[f7'߾єj B,
<6c[-Ƈ>y_"WUOx)
XF?:ϿۇNpzxr
Rr̔NTsBx|JItLt]&[Yjޅt[X.";+g13LJAKO>h܉K)֘?*EOp`rjL:\-/?+l'ۤcbԆO	$ӧ%(}Qf	aD @As_@XR~kVZ{Xt5g2q6hF ;w	Q|b]6YF)c\
~x8]g#6's,WF670՛_2ʠ5U8޷u*R;uV\kG`Fnwy\$G$[9b (HO,Q	ٰh!zz	G3~N<m9P3¼ـ=벿/pa.)~)6 t=x?zT78XCIqewp9.Z>jv}@GP;RKL/tvE
.oܓu
?йbZF
5

ڏt0mNxʚ_R]jdrekYO, U.Ϫ~JecXuVH(sPr܁Vv0x)32<f%
Hd~u"YM;h9w4;VxWYk|CJ[
+gx<kW۸ïPSm@;ӹp$L8B|!{aK:3z[~-|:ME;QL66:/7K®q&Iۘ5<7uGnB低}NS\ "u?p+oّ:t82؝9)%I4I.B!'i)%~JD1E?πA"hL)%)g	&35^iP$Rr`8liԏB}h'{r"I8pS\HL9IvI{,h\X2dPf3#ȇ͐t/?~{9x #irxl az`\#,/{9.7>__
zmB
L#M]?H4b|H #S dR$d-F> Q$J234y8n7
A
 Щ?IN(]='gv_6͠ll gL
x4soMS'
A}8MvrF
߸:'B{	Kga96=:ZܒCs=G\x@&dw.7:fs |CR{|p\ F0}> sJC6_GIh@Ǡ# q0h0&M6)4O o4Z z#PCthY6jVͦyL'"ɻ1(sz~C?㯋y;#+,,1<H8
-iCU#4¯&\c?u+HAjm6u6QO[GУ<W薦|;6[G@F=#$A~Q֘IvmM?')h	,pCilW}uMRŎt/=_yl<]i,UV,C4p\WuaX[	g\Z%jBonYO3F2щ  L#hL
CFwDc8yfcha2ue6Fls wH5Ź.$lpl5r";eB V!ɚ!Uk!mpaԈ!bV9[Y餯]5W?Vd`TQeE0鈮0_hx1 +q͖@b.LÆ1 ('3QtfxJVI0 XddU 3ۺ&X]o@e]\Rap'zkI?O"	&N-H]w2z@ێBMrnN$,⭐YlTHu2ɶ8σ"U 	: 7F|K38⤠و"ik
Mh9_:-DM +&&	Z<ikX%NVD#6^4J@ãaKbC'[\]80<+jH]ݯI5#<}Ai8 
QĤ]!K\4[s
X\%Ă: do})
ISE'6M2dclcM
Z.n6l&Wzh<<׶u5	ծp֛<ylL|Uτ8B>OjS]؂Wbbˣ` Toz<lw&,8ff
pȫ\
RFh |4;U"SɪSbi7CjUu|֟מMv|b6ǘbTyOzjwRrnPI抅ڕu|I*pxRc$ILYߦuAB)n:,h|oTN.4Ϭ4-֘+\gZX%}OܮbEaa)#$bDJ[[Ƞl(It:q'P̳)ܕ3E	B0=KWć<ve612(dB
{C2jsgW~;BZ-*0^Eeqԍ rN~ZОdYQ9<b#( t(8 X#O 7PָXF24v4Z*uؘ*
,íV?kЄfWk
JۚS
gMN3T;
ɀ}]Xj؀|!bU/9j9>(XuUUW,y_Vdӗ<ȉ],b::Q2	3KNl*LsLgPŒW1/*I:v'L<[I͈0WW£B*pVp`͢F1&
l""*	O~x~&*?X,M_Q??'^d+VY|9:bPL5NV3Gnc^&'d:y4xJB;wn ~6ݶ(4,Cfۊ;K)=)|Cdsc.zCg_n"xϰ-PLbkRCk1pԲb+{n7
Rٖ,Pg#'W:jZA];9ƙ=64{ayideL%n ҪkSjXٖTʰ((¨LOt݆B}dF^#_mqAݓa}M␏~S).$ҮOm1xuKPH)ϟt0E3M'Ll;CNoT59F̦ӑG98ɏBzg<öi^ًf^F#5{a4vE."X8箲8ӮR!Ed3|[
ƫCrUJe?q<!_Y\|A=kIIT,G`*uF=0B)֔U)Z$jЛ69 F)$` g)^/y?y{?a۽?Ûoi筑kLu?'{#+>;|wcS <uS`V0iA4o)7`U1
$|tswR:KWZҘ#HoQ8"?~xd?^5!=юxJ_|'kX뱃|aSf=%^ȌqGD@͌TԹ*/i ($S$;ȣMR7yyQz՜(dBK[;l\7PIk˗rW4&?9$V]~5cLE /nXFr:T)Hy1EU,5xQlpe2c_8Bիdm-HWq1Y`v-<#866r8XQ5/#}ʠ5%횢'@uS,HoK;HWCk;G-y&s2v5iLV'`(ۥ~1K1|C\7VuG*M
1x;,\̌(4A߱
:s_y@gPTE$k ahX Bܮjoe;eBjRq yhȌJuavҲMPO?RlP) ܛ5rd9
cF6&TTʚV0'eWjNdʖspT5[Ɍ&F	'H9FR%vk~ylyW1N[` 6<׭bt@9%SBt6cg*0ak_q p8zCI`XNN:SR$i1n:ސl{NG)mZ_bAΎbBY}\X&˓vP%/dZ9Pu*;`aR+ߞa$Z]1h-1յZ%ʊN"xTk j)hHOL>
d>U{_:#9FkeqAJSTCbiZ	a8vnyj+f
ą8uE\Mɱ0Wk\t^;H
I2Y~$W_Yݐ6JS]M'Q,v>tƎ/PWÝL ao`U[:
q:@q@?Y$IΥX NmMhj,ϡy1r(U_;|Nq;Py~~9rxE4{q
1F^,PT`/\bMvhkdN@'yʜuE M(QN9z/08MYtGOZJ*rmIRشU'm9knA,)PvH+"g!y~g
9Ku^i_к2q.VFzZkHJ7<J|f!*37,F(~Q9:`S50R<	XE =?7g[3H(b'M[݁4	n$G~;:
¹-gR(i큎eGƣ<{ƅ1OE83ɢ 3wD/ąP <A6b`G}QRZ!!2&| z([e[PoagjY2$2rIfft,64G9%g~'tĐrZUr	<}/Ut\Y$N^dI
q&QF*䫔I0`5#OyTOпvvkY,I=/iʗt"EJ1L}Z4f%dwV1pNg\gH=Ǩ)	^AiuN*c\/ʿ_0ӹz1KO
Ëm_sBՋu_/P/I04)|}})WV׉Nzݛs|w3
^3oϯ.sY>_^Y}vɇ^G-xliliqYq}z(Dh#{WMs)V `%/U4Ɯ.IcOŰ"svw-Ba8io"I	/|x[ms۸\
DT*K'Moڭ#WϹ'/4
Ekl] Hщ}9N&a(`߰X<eeeSrcI<q>AD:W#jtAw|N Z
%.]|rv臭hk]ha:WhoGwa>=1>it~g19>9&g>8yw6{{nӔkЀ5^|ƀ

ta(&	ZL3]CLhF^c/0hr}5\ǵ-}X^ME?ĆdZFkd6qg\Y/9:PƁ3us熮C0
Bw3z벘>az%Zh<i_	e()Fez'ϽITcb:qlV>y%ujˎ~툿d}O$J}H]:0ڈO z/><E7ܒ'3kʹw,BN,Ԝ0ӑc{51/}Bj(Xɋa㸷rž͑#;Br3b沔X ,倄V4{|:6d$eA٨'TsUr/Q#`'7%x9	YRm~2k`EwiҪ
VnD1[,>߸k5P/{.7]IڇJ/
JsQ
\	:pJ,D.p)LnX>DZSR؃'&@4Wler[ā/0X)0'-VFĂ(#e^҈r3̅BM1m'
_Mx$d׏#Xfg
TXP:E.Ȣ+
GVSkN~A&y4k	;D82;U0g gE^HC^N'X.-('m'Qie02i9l=F7=JD~UHoEvHH1<(X5Ͱr#_fLuN`2@~j{*?H@o^cd0^O4,CliL|i=/BPDH2~&X<=j#5kSIC 6>tgiLmq.K{U6H/_3BE<2#j2,.T0e=\/m% nKDsz~v~x4;_3DXλ$n0n}EZz%"OqN>l,s'vbg卷}܋
/O^fz2SzPI^TM8
ҍjg^\^Bǽ׽2¡#kq $z5*ߔRr)̶SB8z8nv~w''ga\ZA9Aa2d%pt;%6%\eMΖӵZ7#?Dz^uv	7e͠ć׬jnmYY5;Q7l82 'M>LTDnyŉ,]+Icxe"ڹ;$*cӴe^g?Ag$ 
/yS)fẃ
!x6(6x½ѧVvFG Y7Oq|sSh-<ρRŖt'm7oJ'.	n6^ŀwV$%9(@3k1`bSx'/6aS0]tU+UhE4C7IpAhȜ(RZ#xA(UM`AhesM	3pHaԫ#4LXc^p5?sh_iMٰmU`O#e1=L*X0$Y__{	f4bʃ]p9yNL}h J@\hcz&W[j&Vώ&?dX_R|'W8 PT%#\Ќŋ 
#sJ+REߓ]~Jk`}Id/~[ךT5`NHH+LhHZ-}DGW:"E	@PiDǱ(	 +~ϾЌ*srkYAMSҲL=k)({7:]mhշ+a7ǂ$\IQ8,wYJD|v
C#-OvnȬvWvls˽GL~ʸX/3mqif]DO9#+'=\,J.\axdWwXI?"t5Vai؇fM.;L9&3!YgbS&Sͮ_Yc7WK]( r@U߁f8ICBE
ER5Ls	;_wvhgHsr+w7՘E;c(Hg#[R,*י%u%9	/ʘuE?BZx=wF篘i+*G5c~k'>6|4D-Iq/r>INza^bY0 `0^=ZZ)%q94e/͖4rѣɓ'"$#<_HSZЊ\V*Yy|iQ@_>M}2 yQqWt1F2񺼆GɺɫdrP^'M2fO_'YU/|甒"1O5KYRyr.)IJ},)z9Qה4_$?~|H8%'4S,(3rÎHљd ?.lM!rV8d+>1ܑ4.kf$t` ~)d]P	z|C~==}y9rb& Ƥ :?	z>?<:<$/SKNvO^힒ק'ǄQ$*p&cΦ8=e7 u|CA04c2VwAOg$eVm-
Z"p9ɷ[8^OaJ99H,hR]??gr? ,WwD=dL҉둩?0ɟ&[%/ANɣG (Wkzqqm˖=iLRϣYa8ʦqZ=+nW"DƩ>BT(X"L&ALof	RF9U\=Kl~5.봬zl,Q/c0nE`Hh-K,oMR
6-c 1_E$Woَ|o̖}p9\+@d<GeR?/t
͚G1s D~*).hY&˫;^YQZ̃Nogz
&?+ߨ T"*@b-تM!+Χ`:#֚ OP˫د8OIE>?fTaʹ@JTW5[w!鿽-_l%@Y
y
,ASl*T4wHWYXzX<:2_/hHHUB?!t`pӅ?=\wF;VhR4W!g-#5oF;ڽ>|"qS M*Odi8P(.;e
MD
d;p0$Gbӭ2ci.҆_÷\w_'ayYzMZw)
Qqۜ	y#F 8|9fѣhdM8 ̵_z $o*20`-dʅ=3s_@>A|?	?L),qm-_F;4ϳ<jr!p8NX CwT(180PCYM)]a_
+U;̓) gnLNW?ddMђJsفa	Dw#T`Aú=`lL*fZe93uBy<#8oF:q
.)I4@]:{svغ8<8t]r+'PihiYؼ\K1su.s~Ȇ=S)rZx/!y}mq?kM55[;t}n+mmEVQAS;xu1b*޼6yCSpA
Xpm@t`,8ćIQ e9iAu?R8Jb|qvt+3o`Eŗ,rY Zjb̽HE+MGZrgu'`h^jFUe𑦫Ǵ:MþƣeUNG=Yf]0~!QQYWA.kRX1^͵UFrw3DfO-#5 jgh(904bF-ӭ(5a&[BjW|#ۦFvϣ(#d\Y4a1S9^d 엲s4&YWO;00H *y}!pX;@/E栝! =1ɉUx\y-=Ě3\(sT?+!SἹ8⨂m"wphdлbMqZ;v|:&GPO-i<
$TFZ{JC]?,j+f/,!bKC+ZYB]pJʁ7aA
v5PNb^csTZ۴oREJ
O~:_
[8g)|t'L2҅O	Fn\XOm5'uIk c,ߴ{nv	(8 weZqmKbxmJͬ"v*ͦg!	eV	:< ꬄirqJYBvDeaճVUj-W	#kxQa;=dOz<xL,b
^T; DJ7ϊûW{m['zm^t	TͣB't7f(%h0kˠS00,bJVSeԗqAќHΊziޗ-c}Ӏi-U	(VKb3y 2ly;rBJE_Zw*[[z[^;taO?uKwAWS]<׈dtHݫQNkV	twM6->DQ!^L㾰RXYYV^8rK%roxg.zUUԷuF;ױ#	cK6&
&ܿ[J@|s+3UKEs&I|9k`, ,|+kpDGQѤAz-T#c%<>$=JB[E7Vz
#`Qo7S{=cMB"gPy~9_Yh;r'+v@	d9"5Ge!Q
I_)
q
C`~ALi婔	W4
ڰ*9۶<]ڲY4jO<	R"Lh7@*btmpJ]Ro,CK(_Y/ՙ?+hlNoe	kۂf
١p6ˑ>]k/]S`ΣL$ao`DiܶlV
5 AT,*lâLVMIU+7&=ـrue
bM;(p2ge!,Lo03zr0'mp
P
1l(iU_/nwfcԠQuHk؁06f	@{
`R _wC(VO&F' QP)`Kzۼ7aPP+Ch;lqk<by*5hnzA]9,į
JL^pd0	Bf}'o殾nWa0ښw}w&@hNs6n\tǏC|&fŭ樭y8zNз]Õ!BoSTCΉaJ=,qSR.mӚĶ:tVŋ i-^1ϩh;F5#/ʹz+Ջ^oS*о_BDID	һRz1:_֙Dp5ldpwp0ͯ9r^Ҫ6fmZBgCۉ P<8c14=^9QN& jouY,;9`[IuB!0kJlnVӶU6c_^zEӭcҚn3!^-1dʐfMƵ;qZl?E*ӤloHisrb~r#٦80A4DIQr)@N$EB`r00}ʤ×{G_l6&NMR$Iw.@ v
m%hٍS.0jd-HwUڽ+]ᾘ2Ki'Rݛ9<643w_6j}oKh	\!2Oc-!qG*bvX&q#ٷ!}:$j	:/i kꯜh\9D)!/ 'x>p;6Xt9NF~"
G4(؆.	B!\=udz 
w8º?ˣ9gD>B$&Lۣ(b-&9qK/gMKք%֫UitEDK(n3is00k_,S7N{D9Yrܘ^}2Q
RzZn?-[TEbm:zkp?(vjo([|Ǧ'naza_=@7`Z$u5B}mHY2b
#U"`$h(욮ss[Uthмgelu"@x/ű}OM
h[8u|'˻Ζ{%MQU1Vc~lRD59.h].c2!vL,v/P} #~Ohۦ4B$ʔa-̛WG.j$Ν2ȯ
h9D@(GPEZ߅g
Rb[vUiPt+8dq6(:VD%~_˃!yBTncU5ܦ*B|ܬ_Rl.|䫯l/
y9%_*qSg2Ĺު
i
cxֈߥuA=ނiVv@+[
'ևXvjsfݪ>۰?V*0w]˅kzlQDt|᫗\O, ɗ$½+#f+{=P-l9;gW1w]Л7-Ye?ș0;dK<vC֖dW.5:fK[G$kEa)GRPP󫓞
PHr@ؓy:(+m=u{zX`Ulk۪<D჻!bmw@UGL~ڥOm97ktk6Cy7&6BeLr?d3!_>`)`*
4doMa 	}ze=dDQ]OŖK_TS;ԯFW}cP~D?.}eT2g[w.|A&gNϣg{cTU+uWu괫Q+q|5BYQ4뼣wW&;.9^ڝ}QqZhN1phT;}HX@e1-#R=NgMFYglؼ5Xz-'ط#޴7 ;lB-߉fKTqFH׈*6_	S-W٪-k:}W/g5๯+\?v@*9]b_@~=>nbsar]ʡ ι9puM^r}uxJ%uШ`̑Cc*N
0+,7:'bD{\hqyWC4K_zoȎ$t|YNXc'qp< FQ,uc'l;ਇӷ߭t˺+YR=l|z'6%3^VeDK;vwm8JIPh+outY}R;ۺxEE^\k^eㅂEؠN1ѓ'? rGO\03]ߥ[BNvO_~Gq8zmp\~($.f!^%^#<=0V^%&},]XDȺ&4I7*؈=᱁bʮ^{㛄q:yB9Q|| `嶦؄V^o`EIĄ{/&
YWQÿoWZu?M&_ȃY]5
fރ?\/Еz~5gQn<;ګYl?z@;u`	@,uI-ޣ  )]z<1LRoݧՅ'.rWq{Lk푪Z<c?	^g8F>w=X(vRXd?,~wwo;Gi,s+|/N[(;T?rlEiO
ԓ>>i@ZMuۋ" y?B,P6!|(%pߩCOX_EdMkum&_s
_Wbۗu:v֣<]YX,}b-)20W
wQ	L,wO
nh?cU^+
^%ZM~-+*!յuXgEүF%{g{ nzkK7.><0nU|-Q-l3}Rݜ|
\N7!|`]ޗ	\ul3Pϖo{Fj]Ad
h#mo̭."혂6qؘi5 <;J)k8`EWq|rD;R^ӥIa䋚/,y6dqc7ëB}!1waT"b{q:]{	ۡ2Szx
`'%s<#4l2csP-61	bd軾xWs)CFXiԎ/4L` 7fVBi\ ／~/jĖ\Ȍ0zʈKxZu_+	{Hb`#
R,=d."ORZ#/{!ӺY!@itAs^0]	_$yn"z+qY&1MBC\%c8->e@q_h,1!.ZJ_8J"4ؗn'Yd
z<">∱O"KόQ{+4\U&J̐9
dL
a
!3	G/f0Li!c4sG0h$!|L>M{sQfrAO%e
Tb|m*	.?"gbp5ܣLnz1oGЅaw4\]uG0
oǽ#1'$`6i&B{LCl11<.V'ף]0ȳuD 4<*f~l2A9pFb=A6R*.$]TuZVvn>G)0&';GɌȕB7nɞX>v}{ZE
E=cQO'YQ2<wnkmwZRA-Ng| aPYў+9ӻ[JXYS))b~
"2{2is
?Znh__jUK&ǌ*+M"X' tȣ <)k$Q]T2ʿlsPƵگJK]|>||Ӌ=̤b8|{m /G )FT>jeXj UV#^h׆!gm084lҘP&M mLR@܂Զ.̧g8J@@sqn^7I	f"bj=9~JC}fV2gLf[;<MOkG:axKnĒ
w6WR`J0w0Lcf:e>qؑpzC&LiQvHL^z^[gq wg)ktGOʣ',3njOLFVKaˎ~u\&<;fܧf*cs{
,?3j]/aKPithek죑	1PD(HtrUpлu5R[xh:Dn:'9T{L;v'*2TDmW,wܾHv8⟳S<˄āϒW_zA~/%
ϒ_.'uw;޵^/ۯ[_!&w	~kzWStyz;=?_*_!,&$11~y<+^pGgJNY좑OO,V2ۈ
}j4}A)N$;
˶0QӀoox3eS.rgGl[֛vNoA`oAZ1a-ёW|/Yl(`{B %N_xM|[	|q[.֥QعbLC2^ޞrqXg뇻bmKΪ:iFfjZQ?@CQ
NgmdzLs_In}x;_i	fSvoɍs7)3 TxXnF}bDhJԊ'|,#1RXKqkK,r"ޙvܤKv93g̙]y36EBLI\^*3;v6<[We,`o.i9R{\@[2SV[Bd+,Kpo:AQjs,:ٕn(g/+Q@TXq5Cp#KYGJLJAhX3AN8hf|y{to9f)T4Y!Z)	ѩC3-d\ྂ9W~'>T
_)2'MRkݻ Cc	zįECYp쀠	mplsx?G.sQT` LVL`N
GoF{rjx0c?9=d0#89r~(FlʊLG\3KcC9 asr1GB_/m<5M2VM>hx;"avxǗYvbN5}cTJ+˚:~{v?Ӂ{\Au^<z(,9Io-ku׈jk^7?89v5F|}ȰXHf0"Ԅ(#;d"eH=jG MIQ9Љk	_
7#҂ɰ{5*qJLUxH*DTļbSN#,´xќs|.lnz#^Mp7ECln~wmXY@ݬy®ϙr]o"m8&}1X`57'!AGEk۲_gP{c>8K籙A9ypPPq0ZTe>i]5(שnzAEg8e3:kkyvVڼ\un7lH
[/=͝zϗWo8ֲ rkG52e-w)
JӘt}W݌9LFuW9a34Ɗ*Aq.30׉FH+9Tu7(mb63] UdYEqׁ	7⍺ǬL
kqh|(KE#ɤMghi9C48ЭlN["k>vL
RY`,E5QYC.G#W{Tbc'ȪX޷Z>gtpF4AP(Ӕоd.Kv$f`,j2FUq;A/Ǝ۔Cɷ+yp߯دԩ
.UVu̽j/eyи7'_O4n>^TH\k=Gn֪ vgw]H=U(d/{w:j+q~fTa;.I	Vkyf^eXsz*fbZ0"yѲjUkE xW[o6~85Z
80ۮXnXPCtd	IbmuuK`^l;7i8ۢJcڏ,I/ǉ(h)Dl.frX{x<ٶ|Shcy'X)_ܩW k䁂uu2aa^tjl+wC5y%ɄAm(JBV"oS&ٶɾT!YM	qSйkZBGR}Lu,x(Pg$!̸oj8ΰYq0D屛/X5Okgfګj=帮jI[\rLW]tP<nZGITd+f4ܔ*D||8Pޘ?V~2UY/W)-1wÓ!pVMg6-u9KW$t/4l^:C3ctBXuOR~2SB cw iۏLřVD-p=.Ӑ&̀+nrv#sN\g/Lcѫ
|!wr_.x[sAm U>m6
,qfzmzn(J={v>j^H3>
v-7_~@䦓dzpcu~DI^(D:WPq~T=mkRHΨT&~[(3!)X6)?AOr/wn}1wS肢˵s<[ȨX'Qhz^(R6g0S+Bxr秧yN쩜
U#Ĉ761hkkjgBfN 2vAK
?!6Q*
2pᕝ96)~v0#@/unS}[+T[fg>)tq_JUG
-mpaә|(Jgu*beBUPa Tlf(oϔ{ڿз)^ڴvL##Ou)܈8xZ^L@I0w{
/o)~$@Cg|\]sn3o_K5v]Ayz2ԭ(]wx=rF	W	I/|ݒl'mmD)Nr@b(b
kΧ;3`0\HU.+DwOOwOwϭp>e=ގܛߎQуG?"ɡ=;䒲Dz[' "	#"Գr".پ`~p_&w.sן?sܹix)Ie$^sQJX0m%>8r'˘7F"pJ!4Z0&푋s8_؜:dCSh,9"зc7uyDh;Kt{$Z;D$}Xώ3
jdp	[!ts	%KFz
%!ή^__ɻ:
"\h :~>)ޜ\볫ߡxLN/Ɉ\.Ύ_.d@Ș"TSW+HڡzL`X2(ƔwMA׶Bs" Lĝ?{>rh
"z̟z. G4
H3hG;7#dowI~T>yl8fGÀ0o(v_?ppodox#=C!}7ފXL7G?soO]?JGIߜ!+z<C<8Zqؤ'_ŷ3h^eD`2άCoisP7	CO+݌*ESo7s
+'g=)0^Ҹ=ΏG* J	2ep9@~6# aށ/$;E-;-oi%ZD[$s [u%%e~j/IK5$C
?X`^](H	E}{Q	d 'Я&0JB!=xfx. _T1(vO%ԣ`<]+&g^`WRPҡ
Jʚ5ujCV+]X3c507X9"Fiՙ&U`EMʾgM,"ՙ0ABK%r [![Zd9Hc2TuPډLy-#qDPd.,	YLfOZqVj3ƌz&jUGoٺfkX
`@<%ƅOj]LС[4aY=Mkl~R$I5D4&u*(an>-sЁgjH뛮|Dg꿧Ǿk{1UӛuZPȫF 	,axNȗ
j5
Tf (9r]cL\i:3EBO2Bx	(RE"E*.g]~4ѣ/dad}jF19v@~u[wnWnwT>=wDcO8?:Sfd{	#۷t9ẖrrM:gl4Wxg(6 _i}sU#T9ss|\8{&1}=|1
s~ZgnkteD27fn9#2-hy 1
8YcbS2s=j%f"4AL{iEP<.Y$ł+XN 9q[Gbd|lOĲRr]T;ix"~%z\x{PFUutCd&LJP54`[Co!kbӒ̭Rdqsґhںp0usW%~[08W!5$9 yB2H؎At0idZ ޷s*[HIgS?r۠cUK"=pmEߦjv</}ULG(rom,ϵeD^N>Pds/73>9;SD$edb3oR1xDbj.zG`hJ2;6ZP%2ETy	^BC~ bcx:[(gQ-[ٵp$ۣ뗓
 ]|<z}$Y8By4Qn-fA".ct:ԎlZx898:?:<j)s;m6Sznu@k#h?v5ɤ
#?jf.Y-M̷̒N:10hГt}&)
mmJJ2!UŎă^i:f"u*:
Iaq#'<CdKΖOBTl%&.$$M:{4U	k"ҵl~I9*oȟxR`\jO<O	i%+Es6gN靖-W}nЧm\_i8({WA[J51fRAu$|YMlfǚj* Jߨ`,1BrKNǳNklaS"q&	3yz-``I]fypRUZmшu	CHuũ.ljQё"[Q<3Y89(,*$<\n1{ӌ|'vi?nsyuq>z5 'HȈQ\#+s#FF_'*Kf\U#XM):O뜀Ay9+pj>~n w+9-7xtǅ3 Nh}'#q-Z"_`"|ه$j*(pE8ZVptG#wRWY.|`eI
uu fO <JZ$S<O:'TLv
	FQ(OnHLX95WeM~TiQfs"rx'fZJL&mGv+ߍsEQKZbP$JzI6
ت8#ayI	
+b61n
s 󬃧:]9/~V甂ܺ4b~w) P 1X('>'7w:w0uo~RA~yKdh^$g]}O7f%4RqMAj3UGZ\*6hrM~.FafM]scYfrP+!m"Mn=٤-l+z*vB.tKp6Oahl YbάƧ.%!oLmK=眞Ǚy_&8`mI[6*W6<򜈱%IתX(EKA	c*&gv>͙?;ՔRDWFT=++҈
C%"ZJ
;{ ;N}1{n`Ex{.mY{ѕ\8nOcNBv
Sj
2=oyse9@.Xz{3@)!!T*"\.ZÔtBN9Yoɸ+1
k#0.ێRC!yxIP?fV?[7yvA^'`2/씼;!Wׯ듫r8:NN.ɛsrv|
B_vbǸNFLHj1q7RWC(\X \|	^o"JhQNV0|gwk3T.%I63ԓ5[*Zq]x*vɔֽ<dn
YMweP6D\j2v+?pʞO14qW#j~smhuL+hY,	R7)?"}6rPW_66ͲȭV2,,qzV@*(%S5৴PնlۂɁ?/ORT*t8dQ*Ȱ`CS?ƅV?nDxuAz	ŶQ绖9CWIO)苶as'f8k*Qi[ JI*.?̆r"E4^zO#q >]1Ko3.nwm^
ܙ@@ISeQFws"sIN6ʮ3lJ^ZY,>: >	;Q2}|0i$__x\͎*<i|e0͖:
mW+.^}?Z,8a?;:*#,;Z-Kh\ypg:&~]V42jі]CPNZZeGY$s;]ǆ})^#L};m8gK7dW+~Q+vYV7!UlOR?ʂB[
Vw:JICOVAnkCCQ
'eH{#[2%zyKydEYyH~&<ywrx<k7MMuԁ)בyv,+
O*B-$n!-$0CT'Xto)x@B(r[wco&TG,TV*Izt-r.$I%Soy>oohQM㫹):>,zmA4qܒZռKT߲-H&p(]S~c2W1E_펲PKHt gf+&bf|M%3|	Eܧpv|Y[_O.߷_]]]XWF;DiàSqꅹ~wq^ )kXMLiߥlv?Wq
U4 .KI I'G{ -%We;x0aڡ
P4(14.vB`6\[)LezeĘ@]w,#yVUa]=Α&~exЩΞC8`__;`h_V6mWm(ū z$ͳ?	B|V7]/ڍV-!lu2?ŧaF\ >{sTO\iBMlr=sn55\C9/}ZegSq֧%9gt@~zM#7_4xB`y/<"t:lIPA5L+"=H RGEv8K^3X.1_iTO<LBrqHYXbVF
!p;qA>yW{Z(wf-.dzΒZGw_ñ F#.;
010vvaNDo$Q#ώ\
i"wwq(<J,l8 }%Vb
_Yaĳarc*m/uS=4pBe1^zů,8g6-ݴy} l@T]&aچ &n0`!R1~V7W<͗ Kcp|`װX=[\@*uR~j:lq`҃=73?5S3N Cbe>ǧD$)D0HJFNe#r(AwwLAKeEnp#2>pk1CջjZr'nԭy
Dyhݰɪe=Z݃ۆdYb<PĔj$^eZ Z ϽWs/R
ǲGrD[J'Gv0ٲ(=QοNo1o;+td[2/_n#OtUG`Cr˺|[+sKKJ7/NkqOLO{iIXOoz=
6'
oM|p*<tBD-x;OY(n؎Fk(~J+JaUQVYRhTűH
hjt&WQjP$ x}RMK1E
xaBEv[Px[lL&U)=o/IͼG>>W'Ũ`ɚw7:cXqLjv$@j+JV|dN}j:sPsnKElT#
J횘 i(\Y(nS~n`==|\Mf	RoZb*4Mc쉱,WK+&x>F%'(Jߞ'u۶C8JS&x~ow]2G\cn%sF{\^/$è+Ê\2q1
ҿ/skx_
ƓILkj9`a5ebܼCl>
|1o4dpP4w̙7wƶl:z_! );&|xV[O0~n~ATI2&MО6d	Hvnع4M{Tѹ|>;,_`)@q$E		S\VFKHAhE~9I\Pw 9%ob141*A.@܇
\\ܨi.̄f߬g1OxHBڜGZiYگ"0F
-Fw؆C$un@Ӯ c+&u◑JЅd[5dp-ȩ-LB1*ҶS=u 1K&q=Mq4gzlחA4
@%n-%0E
Ʉ]zvte	8gYWIUP3	>BֈALx5	,	뺶pSڨ/h(2Y D$$xIGfP{x
2@aEO{?ou_&eOpٍ![dgl)sYV+`Yڋyʷ5J\ٝ	T~ݹ'+$`ԭ&~/Vp%/d|||{ZUAO3٦ㆰ0y
f7
mB%Mx6ĖN,d)x}p2hږ{9T$wA~Վ(c	@#p囫gmǶ	iO,ݙ66rK
<vČTlk,(cR{d}^X-!ͧy~(W
4
av
.72ɔtgѨ.fݗ]Nd2SVG9&0Γ|JDj#OIm, 
tVvxUkO0_qWuj|KHiAmd䶵8
qRhǀvC}s}|S>GUNc+٤]L<$:NB/z|Jcz1֌[̩Rp[2ivC5˄s8`{8΄Ҳ5gYR߷,I!2nzݣuU&BUڸSIS9v[KD]H=IՕD2ڭ,:qmZ3r !y 
~d0w{1^x5Es3Dϼ0E>8kC$ؘrVNqé}k-T/4Sۥ>lz<
P-+l*Ҥ+dJujپA\$p6vHI(ڬ"(e&u=+1M)vvcNB68EݯݮXjl6"7L+Q@M3'VҬ]+&
ʡv7Web|N[lJ2ǡzQpΓrλ,_5#͔˹.Z:|ʲF

S+b~r:EEBc򀦔)Ы9O{wo0xks6s+.Dϩss&3c2,6!H˾ka"^HMfO.]mV2YcIRImQJ\V5~qp	!ה
gm l&i0:̮^DΌzP"91i:NW
}{G~ᇐ%mB쵊4yyMEu}5˼؁i}i왐Ve%Tg5+.
VU[w6Oq3WL_<޴WEe[@㔢׸YUyMf*('%%:<^]UIMH(g3øX\URH8lV9y@Ych2W׸ |!E^R4X\}fWH=<SUO_Uq]W52)`=ѵǞp٤xSSFc?.+ua%7m]oP׫sCՃu\Ǎ"'MtȷWeqG]0( 9av @1iD?N?w(ὮAy0xܻPYR+
MKp
G	5Iz0lNo2n5v!,
|qN'W~Hhq^Z.pFy?\hFߤ<N(8OrT7a@$v?kQ~|uY\]iP9i1j;֐˛3g'tEaYCÖ؎B~IяAtuGI@U +3Ŀo:Yn?!Jz!0lc5d=Bn-_+~[}3RHUNkR=2)* pf]%vwO$1O_"}iΈPd^Ri i3C0z |QD[S2\`8VUU}ceu5-pbX1*B 3EW9i_$0Bt&0 ? ${D"'hV2	hx3:̗IPmP<3r`aYX$YF>Ԭ0r#v7+;AIY)8a,2f+羭HD=673#GJicy8SnbӶ#8A_RKRȧ
'L"&N=e0
@#hܷ4(t45^ܓ{lqPT~nbE`fL
]~C5?@Gj
vpaU5S3wie|?JߟNƽ*\\YL
y!|aN99l:پG${GKSUV	YӱQc=,fg`b41?2^MoPgS.C2
gx#9nWD3`1G-:
\ܟaGmP5X GuDLp%J,%9tHKEY1L˛pjաk1JbE3b@&{*`rRxmQGEjPMum2oo5L}vO󗿟]탣[>kc+j5%\ϝ3ri;``j@jdrfX6&iV^7著RxNhc,}f{Ɯib.7_.cbwߡI-k܉*S&s=g˰;@L?I
R.! ?Rv~yVmPDjf# {.!jxx,X%:- |kXlH`FЗDy~	xM9xe
EL1\Az)2M]"hQ=.0حd*	z g^
׎jgvȆiVuZlm3~y
4)ђ"+1TU=&۹]T.3ӑJ
{B"b{hَG5GR(7)2
k	CFљo:}S_r5630)fx.[Ⱥ?vz'	zL^{D^}ǃBY,gV%<sMS𓻟zz><K}qKbeXsh_LN=>n&gbJ)*ݖPG]0%&JB}Nԧ21/appep-<rt1 ٙQ=kc#Wa>d7<o>;+Mcn'1ЇSrE'ᒩ'bbz"O_ߌNhĭDxS=VPxY	ϓ'LX
,#WȐto	Y:U3nv"?\f'i*ī/HT_}Y~?;ssҐ!xΩҜ M2/O?IIkEn6P`.?
"(Vqkve!M%_^nApmƜƤ~KTVw_C6dt$7BCt
5UD(<MO$X .0Q=:܌o)ЦŖe]AMՂqy	=$ر*ci/ziYqG=eM"aXטjX+
tWṣ6K-s.F4n$k?{$)|oǡ
7vtȝ;zc5qNfTLlTYKFjL-͚k^SVWu<Щk(!2֌d
΀  5[SS?iJU+2QN29J^kE+=F7fS~G5nuME]y_3ʜ>2=V<>xqIo6>7buŉhCr$ٚ**+ (3X"ϑ"￣_$c,:.a~| {"~[X"HH81PS/뛤\Ɨ)=yAun٧'vkT=DJ'-aPu	xUm=Sae(I7'4V3!h9p1~uq:3¢Iv(u,E4wtQO[B7Er޾)@R[y<Q A/"x]w۶+JJmIqmqDBo*~t}g@ފ$D< x$G;YQayկO0`0fvvl&	Y,M`+k2녣#IV&X'=zTde3gDT5	
n$ aX\sߡ)Є#,;
[MV߿DG~b?<E(e*W-%Lh[}!r6\FQE@N;A-$񟺼hxCu{x
3]7e~95^q$fH|{52}T(ϝɧn:vJsQ&FS |G#Z崝V&	K;nY$: S=Қ5@1IK"hYV,$eɂ
)R**SFԟXp#OK.{NQXi+)s,t}mlPiYu ez
l/s5(Ch&J
QOT*r'c;SH:	@)Ppk=3QoB=;SB.EJ㔜t\utK>`VVqOkfQE:j.	crM,Ǎ;}b
qG.b ڜ%Wpqv|欿;1,]YC77#Q̺|Kv㥺ǒʝ߀N޳f8&9k7,U
X#{w"8,]tVJl+I`w/vey;vWӿpgKkWM$C4pB%/.ytYVpHPSSP"d):I{0ד_؀.H#G:fD~tӐؼ|rQu=&%q}=	ܡqL,#ց1vw(m[w};b'Ut>y0I?"<W*_T(m~jceoxηeYu${qݵl^z7id~4f<X
c%YJU:0@NWx\iH̊05~X8_f4]ba0@R[ry*_Mr>2\{o1<![	L'Ϧ"O)UM]Bka)ʣY#xU=V1Zy`˻wO9IxO,3x2^cրj9g1+EUuGʆzڄƐzIdHpr ƜlrT4ݦ摊t^.NMb~KLf{mL-1#:;%B]3
0'in2E<4.iNuBâf軟'!+I8a/I?A幦ŪZ͞cf3{Ge*Lpax|Sv$` +`73<#&t@JdPǃcjx0mrmxv4jlGg`zjpÝ	X5M
ijf2#S&۹
w00
I^kP" G~}9mT0|2m'e;^kM- [#Ȇf
BdBXh99L`YXS3u0Z6cDOJgls(F^]P,fE@L@͆k1:IrΒEa[\ഴ]hY+H8Fԫ_BΚVf*vT*23Ӯ'FPLEV8䦑`!$CȽؘxLxD,dKl·XN,δtWrYֳeږc`YaSTqtTFWF[7!XnU'z~
kˮP?{'}uvE>M!0,y":2XX)i
0pe21`1
Y!ar_PFcQp;ҘI*k/N./ߧE? '-yf o.0#GqEBjI[o7)\ei9*y
~o^%`L7Rko'/ifY!y9f}?6j+Oq*Ʋɽ}1\\{?f<~fԻ/NO_-^EM7(UoӥZZlfuPkOA]|BI&?>?ylbc6r/@֍LF]zo뉲ԢI5#W辡<e3=$%/dVd'8+BM{MC/,4v0pGw2&$VoqOo]? 05x!uO~26Nt^%iYGtgEhsVݦ[CZ&0W@=ơe0ԫ 65D
\ݬo;nvR[9IO5 ( TK<q=6{^dUL>[1EToI:tq'GF#3$.̈)AIz
 ^ILH@r<o5P*~`eqo/mKW UoZܸ="=/
Z0G~sc?t1>
,p
<R<TluƗv7fHX2(whm/
V-|<gi胼6匼F1uh?
*t]8NqiZX%iCSI0iyw896.j	̇b=7Kҽ$`xy035\nA"hLmd`q7h
Ca14R?Xğ%
]>@$])zX?`i^v,byD鍍uؙ.1"4рP#χ|U{2zCj㊘,`GҘ	Ki̕qZkDbJ^"^*v!á}7>*űR!:\?Or}&k	l(~	c'':wxo{ZN$I>U`p(4[+[2[	kz6i|Um!gkpPX9-+ʸoZN$I>UZ¡Sg!D<v=U9[V%_
W:|5 (UNf35q8ZGKJ?UU_"=wS+A3h6F$p-3&g,&y6θ)~mߘ
DA>,)
3{,c@R2<|_GrW+se-L>[i`]孨_	!!}s@`j8<	M6j5	R`cHĦޜ}2zuqcfD1j1U 1Ɂqf/Ze->A҉65zOs/C`
F>9 `F,7yaQPЏ-T+STe8TKd =?vIܲI]TdA=4Cԥ^%%tjd;0׋hT1n)9i^؊2Q	2s<7
j	Ŀp&s_:%h-Hӣ1J0m孌	jsErIU
():
~WڅC	m
g8~Ce0@
v/J"͏ⱈmdI
/0uq,fOSQ M9/A <	#rk><돺]$jSW!bIK_\ :Ў7W{«nH%PiO5}{!C\zֵj.X̧gRLS-(x$eF)%Cx=DO[=fVԙ/MA*)`օ.?'8>>ߢYފ<`5"͘xElwxC Z/[6<:E"<I&?.DWn^!ꏉ㏌5 )^$zC;ƢjN'͢V/^E/;W0gp*EPp#l>Q2<a˟Rri9]k._#{Q</:Fxhʜ>H3;mQ,=$mOeڍ<>Q3?'L#!ЉGTl[,1_#xc#:Gg5a\DJ@E)_>C4H,fh:L19՛8<!()Yx8^AU	1nb_\oG}]4|$CwnJE{$lG1c<Kʅxل@
SNxs)B-Au_~|,wnT'xV閯iFGGB<Ha?:je'i@՞WO/#)޴l5-9`~ƒ6RRr=:`
VaڅGt^;eGY
C7`<퓣^_7BOq$Ώ(%?w{R*G`lym40Ri./F	xJKB
hyr_"tnUT4dP/ZJ)*i.Ķh˟##ZǞI )Ui?BʟOK(B~s-yi?

`{z<,$AfnJ: XRo#̋,<qi#(gڥ\=m(_zP欯0?AX!@)ǆGmt?
WWy%0d^mV氯0sSƩ q>dDCD))C'b<eؤߑ$AeĥW,+eeI^:/':mw9>-1Ko&+nz+n;Pz##FECAvBr&u	zt݀;'dm_+Thn6\㻁<FsmyW5-ڋRZI_lDnY番ڄ%I=$wǋMECw{}(	k/TF:҉0ܲw Z)Vc@mj/.NoWpvԞN]&suvw<f.hZý8c<zz^yL冭ֈ'ힼV5x
}ʔZh%󘒯W !?8$uΧd(/!<ϣB1yѭ%bvs^buՀc\GcŊkֳE웼o;*;YfRW7QR:F'
bz5[؈~7oWxm(!Ì4eJinOH`vnj%^|~؃[SI^'0u3d>go)?w$a#xVmo6<
*9pɒ-Ӯ@QeH
6WHI(J\c`=|lym@g,L^igfR%~vW?!F0F0TWP)b(tiHTd&@\@~e"IA=FF۔a[ibP+z\i)rJSe.båЖ
;CpDa2^"bW^M6a?E?/˟R4F*j)zW䔌Fڏ/I
_G=#qC~f"_
JI])|.KhRshw],W\P
&EרЛEt௙^W%,ïLV<iT8=BÇPC.653@oh8ᎴrqhZcT;P]0juJ\β\,\|,0&(«s8<6i\lQ!w7@K$5mΝ.O{b8Qxא[60Q%އ`#sđF1H쇁Hߕ}H8kVN:Hpo
JԐ
N0,ѰTJ!V,ɔo
xk0k
LJ^kÝ|Ehyҽ*jQ}ߝOծtKsܚF)8~_{7:<:kPwSRLj
@C:kCrO0h`Al5p+Fnl;Б@H;hLqa(7ĕekTlјMG`7qT']_lA=+n`@A%_<>f1S+32/2Y`dcjWS6oOZvJjkݏ`QEdV@gR\CߝQR|05@\(5yWx%10Ewª1t\F2v$	@/}?9!'
S 9 $!g<o H.pJ%Ùv؝Xb.ckq.WoU,~xg`KM-.HLNU(O,IH+ԟ([ 
xSVHK-M/J
33 Jx<kw6_h%Nn;qb'9c9'Msy`XS$ˇm3xP Rmd扙gYz~L,O-ٴ^EI̲WIƶ?zt<"K_)q@NX^ 9MiT1&dA<8܏h}dsCZ3 ~%GaYO,/p'C~NgaN,9ӌ1'")Oc ̋,<+F;IN:* (+f,$oߓ7K#r\E+Y3BaؒX@tt85h"WD^'aoBF.YoXM%Hi\Z 9IR$è5K3͒Ȝf *"rH3@cȇޟßɇݓӟ`z%y0ѸX Mw'~/	y}tBvowO	E2
O0\4r)?B( 3z@1|^)tZ.
{Y c"I1 WYV$M=а,5b@b4 m0/"Ҥau8i^GI
,-%nl<n>]ErAfEOsXlyv4NEciCH_p@Yxw\9S GK֭J
1Izsi.ɧ<y|L.*b&9rv<!s
῜J,`qdB(*T+ZO_f"6eR|<Khcs^spm<S=y2`%UG1+hUq>BsqG,Jptpr<M<<	IF"ѹgogpir
5f&ճddlJTu]REV>w2Uܲrk*Ԣ6#;pL*ji۪jx%NnvnHCi
5dZKh-+K@Cx9+
/߲w4l鉒sT;Vtr- 36&sf0کFChږCRF$Ҁx:VsVD,EZ,ei֞
Lؗ;Dֱ	+^֦,	E0YR09A2
c?d)˵FO#)u$>'36):q[ϒ(EvذpFaA,KKQh&e)̷o&*<πyNj<v %T'.`r	]~׫g/r3eټ|VAr{j3î}M̛Q;-@09(.hB Fҽbiʲba<2VYLuZy ȭު~P4nWW肂HUMK6`4_`f%d/lOqX|Cm`'D4@Tb	t%
.h4g PۄIv\@
:h;
~6ߕqsz
|Hu
WOds	}g_2y_ƀO[9)G~2u)LB4r)V$SmX: q5~``}6s"##7zQ~Ċ-͡[w YY_{{AVLHҢDmqj!D$q1u0yn}L1bX> 0g8Dx-36@V&X`+pB	#SOrU(
X
˨X~*zb]!uYes-A~iV3-SHHGuH\tD=m=l}lm}[mES@)Tk1qcthxԯqXI>>XzO$(
ل뱆;HVn06D?z{tMNwvo~|
i$HW$fWd%`KLB$vK^=Kn"0(dwP0煭`P؞;b/C1{(}{0NlJ$ư$~xU(Et ʄ4
,vN/ MbhF(0\KUٵ6<!=o.*G<L-EkTUI+SLAQ0вx5~l*ZL! X=?-_D[rYnq=k]B,١Y0UiɆT7]d	h{ڤ*@IQb;&eɪb^h䥢H3l5!Ꮎ{,^J\`ׅ`cRw~K,M/QH~e>hYA^EF`7:kӝ%3Tia`3DXbZuB"jZ!_j2.C@[y}~uJ&?
X4 kUqYbN%Ghztԡh%\d{VL4Ph(
8J#[uAJƒ*&	+/z&y5RqEXn5hg'ceoz;:8|:9:tg ;>~tf49VdBsYg-&iTC6>5ҵ/E- %+Ae{ŻuU庍V}kiy{qY*31ŉԬkن:>WfƘf|yቤ،A5Bd֩<(&'R~Үs/ev5Uh#K#8z'Ӷk6qwbjͮIqE8
d/?ҨJ"`8Z<[3Y3Ü}ǠWNnx,OTa
$ƀ䳨
#hŔF״SvͳJAU㯙!<df^_qUե#@3WdM9FX̜s'.l(N>{OOSoDpoME'ۮ-h7j
1H|!́ԵJ"<mn>vi7lW:F~aѣ^{![},e3/,	 MMS'biݛj4A]K4l~˸7B&	N
3QgA1@>ޟDr@F݅G#C7:#_$>v39V?b|M1۾ޏN`D6lxw]gϘA9 Qda x,L(:VQ4{X,#<U7\Zx&Sxmfv"xM׾Zcpii(#~W@VjYlz.<#Q(Jp>
"=)=l#4>~QC5$L`Y-y=dtw_KX5M~a^Yfb2IQ)7JҎ*5^V 5iU\CBq[MD5
<PԋS$MkǀF6ONNDXTCEsₗ,d@B1e{JzKy='Y!`%/`-zŪvVUu:ϑI>b@$
T߶m~Tx`o*AR%l.b{%ʆGS+3MrAA=Ob6""ødl(V;NE<ŬvG\YdTx932@ok-PKwj.V5oQP+;m9EUVkeoTF{֠g.յȆe N= @xfW"V7t5֟I0ַ+:?*20쩘l
Uauԟ f1wd렫=)aֶNe:ؖT`m괢Op@،mDK(;S)fG@C
־Usw	sƚ6g2n>S BdomE-XDїH0};
MIOl%Qv[O'Xxm:%W1rOY BkY;`o>q@.7gms+	r+Ƶ۸i(յ61 o>%Q@qI*ȅjNlL]ow_oPe5]l4\l\MnFfPK^BsUAqa.|v9%"+7H^<TFbgs@ԈX`f{-W# K/Ya;KxEH7
7ڷ>xyk/YƦ=.SBM{qF:┳Zy6;֭g6)uWQ=R-FSVh_Zg{;aEjQ+\UuRCUv.63:ᷦ/i4Mǩ`OK@xB"\'mٯ엔nuHco$TPpE@ձ
%&dI<%?:XN5~66qNiD kR!_*'q1[		hx'%DW`
ץ'8Ed#
jm*kЊD;~olK
[^福M7Zw,y
}gDd;%|)laU/`7,ĺ\SX˳B!rXFұ=XYBv"F`mJT dG߀oWQ_Ⱦ*wZߚP[8nZ#I[Vr%u`?8m$ݱ܀o`;Hnի&L*cao5_yLe2>Aڒ
|!W[H:_|w|W ROXw`
ƍ6I%v:Fr:o몽ز2~1UO*$7$u|M^B+^n-9e)f^eħ)=A%B@C0ϧk:7/.-A]0ޛӏwH0kL3υYJ\.0T_`vx{n
{x7)ʘ9Lib7gR* *
&x{n
{x7 Jq?xSVHK-M/J (x[mo8\
Vhkq-KoS6uZLźȒ JNr!WR&Y`Uer8>X?eeeSrmI<q>AD:˗#jtAlN Z
.]|rv臭hko]haZu}ķ0v؞4C:v\v೘Lf;=y7yJh{hO/?xc@0	݄'a!&ٍ48vKW./196\_|M#qmGi8Ǩˣ>Gذt؏YtF86$sϵ68e2z=>W,^ we8S<^C0
Bŷ3z㲘>V
5l:}%
(L|{"yjpI4{[oxy
4h=FK3e7=gPL([U\#r`@  _|(^n
Of ׬$Fe`_
\xJ95aM)۫xI+e.@;GHVKǽFPgjdARRbRL'NwBP~bHB+^A& y(eA٨)TWr/Q#]'x9	Y6?YAMUbPiY׆5[M--n͖k+6G7|c+*l$;CWEv'ɨ>Ԋ~nooPwZĿm24p߁Sb8%u .%
|"/&O,k1'%K#b;h>!'^Z1aRMV8FN˼fȍc
4"T(d|~B6qT9˓]?`ў)7S'cB`O޺ (YZg
XMX+!": rdA5ӭ(n*Ed:riF<	l;
/ɍMa]yU"dc$n*1<(X5MQrC_ fTd1)ȆUH\'ܑ>F_j`K2,CliL|i=/BPDIUN-K<x3c!g
}YP\pRS[K"ƕ㽬}?Ŵ<+T]xu2XBFE0Ɣ
SK%]4ќN&Gk.`18FWV
|V"OqN>l,s'vbO7}܋
/O^fzUhٗvmI^PcM8
ҍjg^\^B=^GCuF@^CoJJ2)%EIn3L[o~ ɻtz2{avxp~.V%d2'"(,`;߱iL^nzFZv)t͵Q''WNz.᦬e
㲵6+r'
^ɇ]ci4TwymG*Hе49,
SDE,`: D2"o*>e9p޼<DP`aO7J"57hdMS5F\	mpRԛ9Pؒ.MD}Z~R>wUۼ05~1$ܘ9|Cu@zF{-q@l
/S&0l
A;F=+UJt
;f9	Ns:\@} %̩B<(5:/J	Ӷ,2(u{	B#QD!)0ՑA&wY8_N/_[uw%VBUHr2FE>i><]kt=+b/aFPy2~.1NÉM1W	(m^/פ~VĪv!ğz'
KdѴ
jԂ2dpx$Qeu/\{z8Twd.?&K50*d/~[ךT5`NHDKLhH-}DGW:"y	@PiDǱ	 +~O_UR=? :ֲȃfeJҲHk)'(;7:]wmճhշ+`7ǂ$\IQ8,7Z
/Lo|z
}Sms[-ܐYE.1w
(clඥ+75&zO>XV1]y'<x@f9Wx@s
H
?"t5ai؇N.=L9&3!YgdS&?)^O-4KoE4׺*A倪pߩ 1^Iq#CBE
ER5Uv>wvhgH3r+s7՘E;c(HgS"bOW08SKニyK3_1"Bq912Ϳ4B_P pack-d9cf1939a786310e32cdbf34b718e0421ef8895f.pack

9b96bd0e0d7dd3c0bdbbdd438b268df86ca49ce1
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c	refs/heads/master
4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c	refs/remotes/origin/HEAD
0a2225e9e59140f04c67512bfc3b23630a4ee983	refs/remotes/origin/dev
4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c	refs/remotes/origin/master
9b96bd0e0d7dd3c0bdbbdd438b268df86ca49ce1	refs/remotes/origin/restore
f79d6f8b9b7ae2d2766b4be449e10fe8149bf8c6	refs/tags/restore
c1db3951f375fcb938a4ca9248f71e4cc8e0c51d	refs/tags/v1.0
7b358ea9deef97389af68e675a7b851f74579f31	refs/tags/v1.0.1
c8cada3dd7fb4275eddc9eb312575101a8c0c03c	refs/tags/v1.0.2
0000000000000000000000000000000000000000 4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c Ovidiu Liuta <info@thinkovi.com> 1591861282 +0300	clone: from /Users/thinkovi/.composer/cache/vcs/https---github.com-watchfulli-xcloner-core.git/
4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c f79d6f8b9b7ae2d2766b4be449e10fe8149bf8c6 Ovidiu Liuta <info@thinkovi.com> 1591861282 +0300	checkout: moving from master to restore
f79d6f8b9b7ae2d2766b4be449e10fe8149bf8c6 9b96bd0e0d7dd3c0bdbbdd438b268df86ca49ce1 Ovidiu Liuta <info@thinkovi.com> 1591861282 +0300	reset: moving to 9b96bd0e0d7dd3c0bdbbdd438b268df86ca49ce1
0000000000000000000000000000000000000000 4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c Ovidiu Liuta <info@thinkovi.com> 1591861282 +0300	clone: from /Users/thinkovi/.composer/cache/vcs/https---github.com-watchfulli-xcloner-core.git/
0000000000000000000000000000000000000000 4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c Ovidiu Liuta <info@thinkovi.com> 1591861282 +0300	clone: from /Users/thinkovi/.composer/cache/vcs/https---github.com-watchfulli-xcloner-core.git/
Unnamed repository; edit this file 'description' to name the repository.
#!/bin/sh
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message.  The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit.  The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".

# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"

# This example catches duplicate Signed-off-by lines.

test "" = "$(grep '^Signed-off-by: ' "$1" |
	 sort | uniq -c | sed -e '/^[ 	]*1[ 	]/d')" || {
	echo >&2 Duplicate Signed-off-by lines.
	exit 1
}
#!/bin/sh
#
# Copyright (c) 2006, 2008 Junio C Hamano
#
# The "pre-rebase" hook is run just before "git rebase" starts doing
# its job, and can prevent the command from running by exiting with
# non-zero status.
#
# The hook is called with the following parameters:
#
# $1 -- the upstream the series was forked from.
# $2 -- the branch being rebased (or empty when rebasing the current branch).
#
# This sample shows how to prevent topic branches that are already
# merged to 'next' branch from getting rebased, because allowing it
# would result in rebasing already published history.

publish=next
basebranch="$1"
if test "$#" = 2
then
	topic="refs/heads/$2"
else
	topic=`git symbolic-ref HEAD` ||
	exit 0 ;# we do not interrupt rebasing detached HEAD
fi

case "$topic" in
refs/heads/??/*)
	;;
*)
	exit 0 ;# we do not interrupt others.
	;;
esac

# Now we are dealing with a topic branch being rebased
# on top of master.  Is it OK to rebase it?

# Does the topic really exist?
git show-ref -q "$topic" || {
	echo >&2 "No such branch $topic"
	exit 1
}

# Is topic fully merged to master?
not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
if test -z "$not_in_master"
then
	echo >&2 "$topic is fully merged to master; better remove it."
	exit 1 ;# we could allow it, but there is no point.
fi

# Is topic ever merged to next?  If so you should not be rebasing it.
only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
only_next_2=`git rev-list ^master           ${publish} | sort`
if test "$only_next_1" = "$only_next_2"
then
	not_in_topic=`git rev-list "^$topic" master`
	if test -z "$not_in_topic"
	then
		echo >&2 "$topic is already up to date with master"
		exit 1 ;# we could allow it, but there is no point.
	else
		exit 0
	fi
else
	not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
	/usr/bin/perl -e '
		my $topic = $ARGV[0];
		my $msg = "* $topic has commits already merged to public branch:\n";
		my (%not_in_next) = map {
			/^([0-9a-f]+) /;
			($1 => 1);
		} split(/\n/, $ARGV[1]);
		for my $elem (map {
				/^([0-9a-f]+) (.*)$/;
				[$1 => $2];
			} split(/\n/, $ARGV[2])) {
			if (!exists $not_in_next{$elem->[0]}) {
				if ($msg) {
					print STDERR $msg;
					undef $msg;
				}
				print STDERR " $elem->[1]\n";
			}
		}
	' "$topic" "$not_in_next" "$not_in_master"
	exit 1
fi

<<\DOC_END

This sample hook safeguards topic branches that have been
published from being rewound.

The workflow assumed here is:

 * Once a topic branch forks from "master", "master" is never
   merged into it again (either directly or indirectly).

 * Once a topic branch is fully cooked and merged into "master",
   it is deleted.  If you need to build on top of it to correct
   earlier mistakes, a new topic branch is created by forking at
   the tip of the "master".  This is not strictly necessary, but
   it makes it easier to keep your history simple.

 * Whenever you need to test or publish your changes to topic
   branches, merge them into "next" branch.

The script, being an example, hardcodes the publish branch name
to be "next", but it is trivial to make it configurable via
$GIT_DIR/config mechanism.

With this workflow, you would want to know:

(1) ... if a topic branch has ever been merged to "next".  Young
    topic branches can have stupid mistakes you would rather
    clean up before publishing, and things that have not been
    merged into other branches can be easily rebased without
    affecting other people.  But once it is published, you would
    not want to rewind it.

(2) ... if a topic branch has been fully merged to "master".
    Then you can delete it.  More importantly, you should not
    build on top of it -- other people may already want to
    change things related to the topic as patches against your
    "master", so if you need further changes, it is better to
    fork the topic (perhaps with the same name) afresh from the
    tip of "master".

Let's look at this example:

		   o---o---o---o---o---o---o---o---o---o "next"
		  /       /           /           /
		 /   a---a---b A     /           /
		/   /               /           /
	       /   /   c---c---c---c B         /
	      /   /   /             \         /
	     /   /   /   b---b C     \       /
	    /   /   /   /             \     /
    ---o---o---o---o---o---o---o---o---o---o---o "master"


A, B and C are topic branches.

 * A has one fix since it was merged up to "next".

 * B has finished.  It has been fully merged up to "master" and "next",
   and is ready to be deleted.

 * C has not merged to "next" at all.

We would want to allow C to be rebased, refuse A, and encourage
B to be deleted.

To compute (1):

	git rev-list ^master ^topic next
	git rev-list ^master        next

	if these match, topic has not merged in next at all.

To compute (2):

	git rev-list master..topic

	if this is empty, it is fully merged to "master".

DOC_END
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

if git rev-parse --verify HEAD >/dev/null 2>&1
then
	against=HEAD
else
	# Initial commit: diff against an empty tree object
	against=$(git hash-object -t tree /dev/null)
fi

# If you want to allow non-ASCII filenames set this variable to true.
allownonascii=$(git config --bool hooks.allownonascii)

# Redirect output to stderr.
exec 1>&2

# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$allownonascii" != "true" ] &&
	# Note that the use of brackets around a tr range is ok here, (it's
	# even required, for portability to Solaris 10's /usr/bin/tr), since
	# the square bracket bytes happen to fall in the designated range.
	test $(git diff --cached --name-only --diff-filter=A -z $against |
	  LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
	cat <<\EOF
Error: Attempt to add a non-ASCII file name.

This can cause problems if you want to work with people on other platforms.

To be portable it is advisable to rename the file.

If you know what you are doing you can disable this check using:

  git config hooks.allownonascii true
EOF
	exit 1
fi

# If there are whitespace errors, print the offending file names and fail.
exec git diff-index --check --cached $against --
#!/bin/sh
#
# An example hook script to check the commit log message taken by
# applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.  The hook is
# allowed to edit the commit message file.
#
# To enable this hook, rename this file to "applypatch-msg".

. git-sh-setup
commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
:
#!/usr/bin/perl

use strict;
use warnings;
use IPC::Open2;

# An example hook script to integrate Watchman
# (https://facebook.github.io/watchman/) with git to speed up detecting
# new and modified files.
#
# The hook is passed a version (currently 1) and a time in nanoseconds
# formatted as a string and outputs to stdout all files that have been
# modified since the given time. Paths must be relative to the root of
# the working tree and separated by a single NUL.
#
# To enable this hook, rename this file to "query-watchman" and set
# 'git config core.fsmonitor .git/hooks/query-watchman'
#
my ($version, $time) = @ARGV;

# Check the hook interface version

if ($version == 1) {
	# convert nanoseconds to seconds
	$time = int $time / 1000000000;
} else {
	die "Unsupported query-fsmonitor hook version '$version'.\n" .
	    "Falling back to scanning...\n";
}

my $git_work_tree;
if ($^O =~ 'msys' || $^O =~ 'cygwin') {
	$git_work_tree = Win32::GetCwd();
	$git_work_tree =~ tr/\\/\//;
} else {
	require Cwd;
	$git_work_tree = Cwd::cwd();
}

my $retry = 1;

launch_watchman();

sub launch_watchman {

	my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
	    or die "open2() failed: $!\n" .
	    "Falling back to scanning...\n";

	# In the query expression below we're asking for names of files that
	# changed since $time but were not transient (ie created after
	# $time but no longer exist).
	#
	# To accomplish this, we're using the "since" generator to use the
	# recency index to select candidate nodes and "fields" to limit the
	# output to file names only. Then we're using the "expression" term to
	# further constrain the results.
	#
	# The category of transient files that we want to ignore will have a
	# creation clock (cclock) newer than $time_t value and will also not
	# currently exist.

	my $query = <<"	END";
		["query", "$git_work_tree", {
			"since": $time,
			"fields": ["name"],
			"expression": ["not", ["allof", ["since", $time, "cclock"], ["not", "exists"]]]
		}]
	END

	print CHLD_IN $query;
	close CHLD_IN;
	my $response = do {local $/; <CHLD_OUT>};

	die "Watchman: command returned no output.\n" .
	    "Falling back to scanning...\n" if $response eq "";
	die "Watchman: command returned invalid output: $response\n" .
	    "Falling back to scanning...\n" unless $response =~ /^\{/;

	my $json_pkg;
	eval {
		require JSON::XS;
		$json_pkg = "JSON::XS";
		1;
	} or do {
		require JSON::PP;
		$json_pkg = "JSON::PP";
	};

	my $o = $json_pkg->new->utf8->decode($response);

	if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) {
		print STDERR "Adding '$git_work_tree' to watchman's watch list.\n";
		$retry--;
		qx/watchman watch "$git_work_tree"/;
		die "Failed to make watchman watch '$git_work_tree'.\n" .
		    "Falling back to scanning...\n" if $? != 0;

		# Watchman will always return all files on the first query so
		# return the fast "everything is dirty" flag to git and do the
		# Watchman query just to get it over with now so we won't pay
		# the cost in git to look up each individual file.
		print "/\0";
		eval { launch_watchman() };
		exit 0;
	}

	die "Watchman: $o->{error}.\n" .
	    "Falling back to scanning...\n" if $o->{error};

	binmode STDOUT, ":utf8";
	local $, = "\0";
	print @{$o->{files}};
}
#!/bin/sh
#
# An example hook script to make use of push options.
# The example simply echoes all push options that start with 'echoback='
# and rejects all pushes when the "reject" push option is used.
#
# To enable this hook, rename this file to "pre-receive".

if test -n "$GIT_PUSH_OPTION_COUNT"
then
	i=0
	while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
	do
		eval "value=\$GIT_PUSH_OPTION_$i"
		case "$value" in
		echoback=*)
			echo "echo from the pre-receive-hook: ${value#*=}" >&2
			;;
		reject)
			exit 1
		esac
		i=$((i + 1))
	done
fi
#!/bin/sh
#
# An example hook script to prepare the commit log message.
# Called by "git commit" with the name of the file that has the
# commit message, followed by the description of the commit
# message's source.  The hook's purpose is to edit the commit
# message file.  If the hook fails with a non-zero status,
# the commit is aborted.
#
# To enable this hook, rename this file to "prepare-commit-msg".

# This hook includes three examples. The first one removes the
# "# Please enter the commit message..." help message.
#
# The second includes the output of "git diff --name-status -r"
# into the message, just before the "git status" output.  It is
# commented because it doesn't cope with --amend or with squashed
# commits.
#
# The third example adds a Signed-off-by line to the message, that can
# still be edited.  This is rarely a good idea.

COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3

/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"

# case "$COMMIT_SOURCE,$SHA1" in
#  ,|template,)
#    /usr/bin/perl -i.bak -pe '
#       print "\n" . `git diff --cached --name-status -r`
# 	 if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
#  *) ;;
# esac

# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
# if test -z "$COMMIT_SOURCE"
# then
#   /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
# fi
#!/bin/sh
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".

exec git update-server-info
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git merge" with no arguments.  The hook should
# exit with non-zero status after issuing an appropriate message to
# stderr if it wants to stop the merge commit.
#
# To enable this hook, rename this file to "pre-merge-commit".

. git-sh-setup
test -x "$GIT_DIR/hooks/pre-commit" &&
        exec "$GIT_DIR/hooks/pre-commit"
:
#!/bin/sh
#
# An example hook script to verify what is about to be committed
# by applypatch from an e-mail message.
#
# The hook should exit with non-zero status after issuing an
# appropriate message if it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-applypatch".

. git-sh-setup
precommit="$(git rev-parse --git-path hooks/pre-commit)"
test -x "$precommit" && exec "$precommit" ${1+"$@"}
:
#!/bin/sh

# An example hook script to verify what is about to be pushed.  Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed.  If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
#   <local ref> <local sha1> <remote ref> <remote sha1>
#
# This sample shows how to prevent push of commits where the log message starts
# with "WIP" (work in progress).

remote="$1"
url="$2"

z40=0000000000000000000000000000000000000000

while read local_ref local_sha remote_ref remote_sha
do
	if [ "$local_sha" = $z40 ]
	then
		# Handle delete
		:
	else
		if [ "$remote_sha" = $z40 ]
		then
			# New branch, examine all commits
			range="$local_sha"
		else
			# Update to existing branch, examine new commits
			range="$remote_sha..$local_sha"
		fi

		# Check for WIP commit
		commit=`git rev-list -n 1 --grep '^WIP' "$range"`
		if [ -n "$commit" ]
		then
			echo >&2 "Found WIP commit in $local_ref, not pushing"
			exit 1
		fi
	fi
done

exit 0
#!/bin/sh
#
# An example hook script to block unannotated tags from entering.
# Called by "git receive-pack" with arguments: refname sha1-old sha1-new
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunannotated
#   This boolean sets whether unannotated tags will be allowed into the
#   repository.  By default they won't be.
# hooks.allowdeletetag
#   This boolean sets whether deleting tags will be allowed in the
#   repository.  By default they won't be.
# hooks.allowmodifytag
#   This boolean sets whether a tag may be modified after creation. By default
#   it won't be.
# hooks.allowdeletebranch
#   This boolean sets whether deleting branches will be allowed in the
#   repository.  By default they won't be.
# hooks.denycreatebranch
#   This boolean sets whether remotely creating branches will be denied
#   in the repository.  By default this is allowed.
#

# --- Command line
refname="$1"
oldrev="$2"
newrev="$3"

# --- Safety check
if [ -z "$GIT_DIR" ]; then
	echo "Don't run this script from the command line." >&2
	echo " (if you want, you could supply GIT_DIR then run" >&2
	echo "  $0 <ref> <oldrev> <newrev>)" >&2
	exit 1
fi

if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
	echo "usage: $0 <ref> <oldrev> <newrev>" >&2
	exit 1
fi

# --- Config
allowunannotated=$(git config --bool hooks.allowunannotated)
allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
denycreatebranch=$(git config --bool hooks.denycreatebranch)
allowdeletetag=$(git config --bool hooks.allowdeletetag)
allowmodifytag=$(git config --bool hooks.allowmodifytag)

# check for no description
projectdesc=$(sed -e '1q' "$GIT_DIR/description")
case "$projectdesc" in
"Unnamed repository"* | "")
	echo "*** Project description file hasn't been set" >&2
	exit 1
	;;
esac

# --- Check types
# if $newrev is 0000...0000, it's a commit to delete a ref.
zero="0000000000000000000000000000000000000000"
if [ "$newrev" = "$zero" ]; then
	newrev_type=delete
else
	newrev_type=$(git cat-file -t $newrev)
fi

case "$refname","$newrev_type" in
	refs/tags/*,commit)
		# un-annotated tag
		short_refname=${refname##refs/tags/}
		if [ "$allowunannotated" != "true" ]; then
			echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2
			echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
			exit 1
		fi
		;;
	refs/tags/*,delete)
		# delete tag
		if [ "$allowdeletetag" != "true" ]; then
			echo "*** Deleting a tag is not allowed in this repository" >&2
			exit 1
		fi
		;;
	refs/tags/*,tag)
		# annotated tag
		if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
		then
			echo "*** Tag '$refname' already exists." >&2
			echo "*** Modifying a tag is not allowed in this repository." >&2
			exit 1
		fi
		;;
	refs/heads/*,commit)
		# branch
		if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
			echo "*** Creating a branch is not allowed in this repository" >&2
			exit 1
		fi
		;;
	refs/heads/*,delete)
		# delete branch
		if [ "$allowdeletebranch" != "true" ]; then
			echo "*** Deleting a branch is not allowed in this repository" >&2
			exit 1
		fi
		;;
	refs/remotes/*,commit)
		# tracking branch
		;;
	refs/remotes/*,delete)
		# delete tracking branch
		if [ "$allowdeletebranch" != "true" ]; then
			echo "*** Deleting a tracking branch is not allowed in this repository" >&2
			exit 1
		fi
		;;
	*)
		# Anything else (is there anything else?)
		echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
		exit 1
		;;
esac

# --- Finished
exit 0
4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c
ref: refs/remotes/origin/master
DIRC      ^".^".         P  #_aRv,,0PvA 	README.md ^".<^".<         P  n
)ᦾw̸& admin/assets/database-icon.png    ^".-^".-         P   
u̅h)O#| admin/assets/file-icon-d.png      ^".d^".d         P  \.J,!O admin/assets/file-icon-f.png      ^". M^". M         P  #4+Vhċ֯Umsd admin/assets/file-icon-l.png      ^".(*^".(*         P  m(k"4ZC"'F: admin/assets/file-icon-root.png   ^".@2|^".@2|         P  e#BNtIf6W admin/assets/table-icon.png       ^"0^"0         P  3c݆3sL 
composer.json     ^".^^".^         P  ;=}$`  /<_bb lib/class-wp-error.php    ^".b-^".b-         P    ⛲CK)wZS lib/index.html    ^".g:^".g:         P  }}֫gXZT5 lib/mock_wp_functions.php ^".^".         P  B2J#w0tOdh 
lib/wp-db.php     ^".)^".)         P   @2qw[!jN src/Xcloner.php   ^".;^".;         P   8]	.7`) src/Xcloner_Api.php       ^".̰^".̰         P  ~2_(%/D.lΥ src/Xcloner_Archive.php   ^".;^".;         P  \νWb'l_OE'G src/Xcloner_Database.php  ^".;^".;         P  5yW\TEk$%d- src/Xcloner_Encryption.php        ^".݁^".݁         P  vʶϜJTMeC src/Xcloner_File_System.php       ^"0y"^"0y"         P  B3A_FR
Pd
;TN src/Xcloner_File_Transfer.php     ^".{^".{         P  :W LBz<// src/Xcloner_Loader.php    ^".4.^".4.         P  ]Xr< src/Xcloner_Logger.php    ^"0q^"0q         P  ~IOzZ~p src/Xcloner_Remote_Storage.php    ^".,^".,         P  FX-:- src/Xcloner_Requirements.php      ^".hN^".hN         P  	dڦN@VF"P src/Xcloner_Sanitization.php      ^".^".         P  AGBO7B_ src/Xcloner_Scheduler.php ^"/e^"/e         P  "3
 !9ud+K.
 src/Xcloner_Settings.php  ^"/c^"/c         P  dgo@MkwؽP8M src/Xcloner_Standalone.php        ^"/	d^"/	d         P  ̲&ns"{ : src/Xcloner_i18n.php      ^"/s^"/s         P   ve|"}CQd:en 
src/index.php     TREE    29 3
r\0"dzcT;lib 4 0
588?-DX~$>src 17 0
oonSDZkaadmin 6 1
n' C=5(Fassets 6 0
!ō.M+s
X}#(c1ÝMq# pack-refs with: peeled fully-peeled sorted 
0a2225e9e59140f04c67512bfc3b23630a4ee983 refs/remotes/origin/dev
4fd8af3d5db4dde3c8a4a93ae4d9d952c871338c refs/remotes/origin/master
9b96bd0e0d7dd3c0bdbbdd438b268df86ca49ce1 refs/remotes/origin/restore
f79d6f8b9b7ae2d2766b4be449e10fe8149bf8c6 refs/tags/restore
c1db3951f375fcb938a4ca9248f71e4cc8e0c51d refs/tags/v1.0
7b358ea9deef97389af68e675a7b851f74579f31 refs/tags/v1.0.1
c8cada3dd7fb4275eddc9eb312575101a8c0c03c refs/tags/v1.0.2
{
    "name": "watchfulli/xcloner-core",
    "description": "XCloner Core Library for Backup and Restore",
    "verson": "1.0.2",
    "type": "package",
    "require": {
        "splitbrain/php-archive": "^1.0",
        "league/flysystem": "^1.0",
        "monolog/monolog": "^1.22"
    },
    "license": "AGPL-3.0-or-later",
    "authors": [
        {
            "name": "Ovidiu Liuta",
            "email": "info@thinkovi.com"
        }
    ],
    "minimum-stability": "stable",
    "autoload": {
        "psr-4": {"watchfulli\\XClonerCore\\": "src/"}
    }
}
<?php
namespace watchfulli\XClonerCore;

/**
 * XCloner - Backup and Restore backup plugin for Wordpress
 *
 * class-xcloner-loader.php
 * @author Liuta Ovidiu <info@thinkovi.com>
 *
 *        This program is free software; you can redistribute it and/or modify
 *        it under the terms of the GNU General Public License as published by
 *        the Free Software Foundation; either version 2 of the License, or
 *        (at your option) any later version.
 *
 *        This program is distributed in the hope that it will be useful,
 *        but WITHOUT ANY WARRANTY; without even the implied warranty of
 *        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *        GNU General Public License for more details.
 *
 *        You should have received a copy of the GNU General Public License
 *        along with this program; if not, write to the Free Software
 *        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *        MA 02110-1301, USA.
 *
 * @link https://github.com/ovidiul/XCloner-Wordpress
 *
 * @modified 7/25/18 1:46 PM
 *
 */


/**
 * Register all actions and filters for the plugin.
 *
 * Maintain a list of all hooks that are registered throughout
 * the plugin, and register them with the WordPress API. Call the
 * run function to execute the list of actions and filters.
 *
 * @package    Xcloner
 * @subpackage Xcloner/includes
 * @author     Liuta Ovidiu <info@thinkovi.com>
 */
class Xcloner_Loader
{

	/**
	 * The array of actions registered with WordPress.
	 *
	 * @since    1.0.0
	 * @access   protected
	 * @var      array $actions The actions registered with WordPress to fire when the plugin loads.
	 */
	protected $actions;

	/**
	 * The array of filters registered with WordPress.
	 *
	 * @since    1.0.0
	 * @access   protected
	 * @var      array $filters The filters registered with WordPress to fire when the plugin loads.
	 */
	protected $filters;

	/**
	 * @var Xcloner
	 */
	private $xcloner_plugin;

	/**
	 * @var Xcloner
	 */
	private $xcloner_container;


	/**
	 * Initialize the collections used to maintain the actions and filters.
	 *
	 * Xcloner_Loader constructor.
	 * @param Xcloner $xcloner_container
	 */
	public function __construct(Xcloner $xcloner_container)
	{

		$this->actions = array();
		$this->filters = array();

		$this->xcloner_container = $xcloner_container;

	}

	/**
	 * Add a new action to the collection to be registered with WordPress.
	 *
	 * @since    1.0.0
	 * @param    string $hook The name of the WordPress action that is being registered.
	 * @param    object $component A reference to the instance of the object on which the action is defined.
	 * @param    string $callback The name of the function definition on the $component.
	 * @param    int $priority Optional. he priority at which the function should be fired. Default is 10.
	 * @param    int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1.
	 */
	public function add_action($hook, $component, $callback, $priority = 10, $accepted_args = 1)
	{
		$this->actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args);
	}

	/**
	 * Add a new filter to the collection to be registered with WordPress.
	 *
	 * @since    1.0.0
	 * @param    string $hook The name of the WordPress filter that is being registered.
	 * @param    object $component A reference to the instance of the object on which the filter is defined.
	 * @param    string $callback The name of the function definition on the $component.
	 * @param    int $priority Optional. he priority at which the function should be fired. Default is 10.
	 * @param    int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1
	 */
	public function add_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1)
	{
		$this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args);
	}

	/**
	 * A utility function that is used to register the actions and hooks into a single
	 * collection.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @param    array $hooks The collection of hooks that is being registered (that is, actions or filters).
	 * @param    string $hook The name of the WordPress filter that is being registered.
	 * @param    object $component A reference to the instance of the object on which the filter is defined.
	 * @param    string $callback The name of the function definition on the $component.
	 * @param    int $priority The priority at which the function should be fired.
	 * @param    int $accepted_args The number of arguments that should be passed to the $callback.
	 * @return   array                                  The collection of actions and filters registered with WordPress.
	 */
	private function add($hooks, $hook, $component, $callback, $priority, $accepted_args)
	{

		$hooks[] = array(
			'hook' => $hook,
			'component' => $component,
			'callback' => $callback,
			'priority' => $priority,
			'accepted_args' => $accepted_args
		);

		return $hooks;

	}

	/**
	 * Register the filters and actions with WordPress.
	 *
	 * @since    1.0.0
	 */
	public function run()
	{
		foreach ($this->filters as $hook) {
			add_filter($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'],
				$hook['accepted_args']);
		}

		foreach ($this->actions as $hook) {
			add_action($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'],
				$hook['accepted_args']);
		}

	}

}
<?php

namespace watchfulli\XClonerCore;

/**
 * XCloner - Backup and Restore backup plugin for Wordpress
 *
 * class-xcloner-i18n.php
 * @author Liuta Ovidiu <info@thinkovi.com>
 *
 *        This program is free software; you can redistribute it and/or modify
 *        it under the terms of the GNU General Public License as published by
 *        the Free Software Foundation; either version 2 of the License, or
 *        (at your option) any later version.
 *
 *        This program is distributed in the hope that it will be useful,
 *        but WITHOUT ANY WARRANTY; without even the implied warranty of
 *        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *        GNU General Public License for more details.
 *
 *        You should have received a copy of the GNU General Public License
 *        along with this program; if not, write to the Free Software
 *        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *        MA 02110-1301, USA.
 *
 * @link https://github.com/ovidiul/XCloner-Wordpress
 *
 * @modified 7/25/18 1:46 PM
 *
 */


/**
 * Define the internationalization functionality.
 *
 * Loads and defines the internationalization files for this plugin
 * so that it is ready for translation.
 *
 * @since      1.0.0
 * @package    Xcloner
 * @subpackage Xcloner/includes
 * @author     Liuta Ovidiu <info@thinkovi.com>
 */
class Xcloner_i18n
{

	/**
	 * Load the plugin text domain for translation.
	 *
	 * @since    1.0.0
	 */
	public function load_plugin_textdomain()
	{

		load_plugin_textdomain(
			'xcloner-backup-and-restore',
			false,
			dirname(dirname(plugin_basename(__FILE__))).'/languages/'
		);

	}


}
<?php

namespace watchfulli\XClonerCore;

use League\Flysystem\Util;

class Xcloner_Sanitization
{

    /**
     * Construct method
     */
    public function __construct()
    {
    }

    /**
     * Sanitize input as INT
     *
     * @param [type] $option
     * @return void
     */
    public function sanitize_input_as_int($option)
    {
        return filter_var($option, FILTER_SANITIZE_NUMBER_INT);
    }

    /**
     * Sanitize input as FLOAT
     *
     * @param [type] $option
     * @return void
     */
    public function sanitize_input_as_float($option)
    {
        return filter_var($option, FILTER_VALIDATE_FLOAT);
    }

    /**
     * Sanitize input as STRING
     *
     * @param [type] $option
     * @return void
     */
    public function sanitize_input_as_string($option)
    {
        return filter_var($option, FILTER_SANITIZE_STRING);
    }

    /**
     * Sanitize input as ABSOLUTE PATH
     *
     * @param [type] $option
     * @return void
     */
    public function sanitize_input_as_absolute_path($option)
    {
        $path = filter_var($option, FILTER_SANITIZE_URL);

        try {
            $option = Util::normalizePath($path);
        } catch (Exception $e) {
            add_settings_error('xcloner_error_message', '', __($e->getMessage()), 'error');
        }

        if ($path and !is_dir($path)) {
            add_settings_error('xcloner_error_message', '', __(sprintf('Invalid Server Path %s', $option)), 'error');

            return false;
        }

        return $path;
    }

    /**
     * Sanitize input as PATH
     *
     * @param [type] $option
     * @return void
     */
    public function sanitize_input_as_path($option)
    {
        return filter_var($option, FILTER_SANITIZE_URL);
    }
    
    /**
     * Sanitize input as RELATIVE PATH
     *
     * @param [type] $option
     * @return void
     */
    public function sanitize_input_as_relative_path($option)
    {
        $option = filter_var($option, FILTER_SANITIZE_URL);
        $option = str_replace("..", "", $option);

        return $option;
    }

    /**
     * Sanitize input as EMAIL
     *
     * @param [type] $option
     * @return void
     */
    public function sanitize_input_as_email($option)
    {
        return filter_var($option, FILTER_SANITIZE_EMAIL);
    }

    /**
     * Undocumented function as RAW
     *
     * @param [type] $option
     * @return void
     */
    public function sanitize_input_as_raw($option)
    {
        return filter_var($option, FILTER_UNSAFE_RAW);
    }
}
<?php
namespace watchfulli\XClonerCore;

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;

class Xcloner_Logger extends Logger
{
    private $logger_path;
    private $max_logger_files = 7;
    private $main_logger_url;

    /**
     * Xcloner_Logger constructor.
     * @param Xcloner $xcloner_container
     * @param string $logger_name
     * @throws Exception
     */
    public function __construct(Xcloner $xcloner_container, $logger_name = "xcloner_logger")
    {
        if (!$xcloner_container->get_xcloner_settings()) {
            $xcloner_settings = new Xcloner_Settings($xcloner_container);
        } else {
            $xcloner_settings = $xcloner_container->get_xcloner_settings();
        }

        $hash = $xcloner_settings->get_hash();
        if ($hash == "-".$xcloner_settings->get_server_unique_hash(5)) {
            $hash = "";
        }

        $logger_path     = $xcloner_settings->get_xcloner_store_path().DS.$xcloner_settings->get_logger_filename();
        $logger_path_tmp = "";

        if ($hash) {
            $logger_path_tmp = $xcloner_settings->get_xcloner_tmp_path().DS.$xcloner_settings->get_logger_filename(1);
        }

        $this->logger_path = $logger_path;

        if (!is_dir($xcloner_settings->get_xcloner_store_path()) or !is_writable($xcloner_settings->get_xcloner_store_path())) {
            $logger_path     = 'php://stderr';
            $logger_path_tmp = "";
        }

        if (!$xcloner_settings->get_xcloner_option('xcloner_enable_log')) {
            $logger_path     = 'php://stderr';
            $logger_path_tmp = "";
        }

        // create a log channel
        parent::__construct($logger_name);

        $debug_level = Logger::INFO;

        if (defined('WP_DEBUG') && WP_DEBUG) {
            $debug_level = Logger::DEBUG;
        }

        if ($logger_path) {
            if (!$xcloner_settings->get_xcloner_option('xcloner_enable_log')) {
                $stream = new StreamHandler($logger_path, $debug_level);
            } else {
                $stream = new RotatingFileHandler($logger_path, $this->max_logger_files, $debug_level);
            }

            $this->pushHandler($stream);

            $this->main_logger_url = $stream->getUrl();
        }

        if ($hash and $logger_path_tmp) {
            $this->pushHandler(new StreamHandler($logger_path_tmp, $debug_level));
        }

        //return $this;
    }

    /**
     * @return string|null
     */
    public function get_main_logger_url()
    {
        return $this->main_logger_url;
    }

    /**
     * @param int $totalLines
     * @return array|bool
     */
    public function getLastDebugLines($totalLines = 200)
    {
        $lines = array();

        if (!file_exists($this->main_logger_url) or !is_readable($this->main_logger_url)) {
            return false;
        }

        $fp = fopen($this->main_logger_url, 'r');
        fseek($fp, - 1, SEEK_END);
        $pos      = ftell($fp);
        $lastLine = "";

        // Loop backword until we have our lines or we reach the start
        while ($pos > 0 && count($lines) < $totalLines) {
            $C = fgetc($fp);
            if ($C == "\n") {
                // skip empty lines
                if (trim($lastLine) != "") {
                    $lines[] = $lastLine;
                }
                $lastLine = '';
            } else {
                $lastLine = $C.$lastLine;
            }
            fseek($fp, $pos--);
        }

        $lines = array_reverse($lines);

        return $lines;
    }

    /**
     * Info message logging
     *
     * @param [type] $msg
     * @param boolean $print
     * @return void
     */
    public function print_info($message, $context = array())
    {
        if (!WP_DEBUG) {
            echo sprintf("[%s] %s \n", date("Y-m-d H:i:s"), $message);
        }
        return parent::info($message, $context);
    }
}
<?php
namespace watchfulli\XClonerCore;

define('XCLONER_STANDALONE_MODE', true);

//require_once(__DIR__.'/../vendor/autoload.php');

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;

include_once(__DIR__ ."/../lib/mock_wp_functions.php");
//require_once(__DIR__ . '/../includes/class-xcloner.php');


class Xcloner_Standalone extends Xcloner
{
    public function __construct($json_config = "")
    {
        if (WP_DEBUG && WP_DEBUG_DISPLAY) {
            $this->log_php_errors();
        }

        $this->load_dependencies();
        
        $this->define_plugin_settings($json_config);
        
        if(!isset($_POST['hash']) || !$_POST['hash']){
            $_POST['hash'] = "";
        }
        $this->xcloner_settings = new XCloner_Settings($this, $_POST['hash'], $json_config);
        
        
         if( !$this->xcloner_settings->get_hash(true) ){
            $this->xcloner_settings->generate_new_hash();
         }
        

        $this->define_plugin_settings();

        $this->xcloner_logger           = new XCloner_Logger($this, "xcloner_standalone");

        if (WP_DEBUG && WP_DEBUG_DISPLAY) {
            $this->xcloner_logger->pushHandler(new StreamHandler('php://stdout', Logger::INFO));
        }

        $this->xcloner_filesystem       = new Xcloner_File_System($this);
        $this->archive_system           = new Xcloner_Archive($this);
        $this->xcloner_database         = new Xcloner_Database($this);
        $this->xcloner_scheduler        = new Xcloner_Scheduler($this);
        $this->xcloner_remote_storage   = new Xcloner_Remote_Storage($this);
        $this->xcloner_file_transfer 	= new Xcloner_File_Transfer($this);
        $this->xcloner_encryption    	= new Xcloner_Encryption($this);
        $this->xcloner_sanitization 	= new Xcloner_Sanitization();
        
        //$this->start();
    }

    /**
     * Start backup process trigger method
     *
     * @return void
     */
    public function start($profile_id)
    {
        $profile_config = ($this->xcloner_settings->get_xcloner_option('profile'));

        $data['params']                 = "";
        $data['backup_params']          = $profile_config->backup_params;
        $data['table_params']           = json_encode($profile_config->database);
        $data['excluded_files']         = json_encode($profile_config->excluded_files);
        if (isset($profile_id) && $profile_id) {
            $data['id']                     = $profile_id;
        }

        //print_r($data);exit;

        return $this->xcloner_scheduler->xcloner_scheduler_callback($data['id'], $data, $this);
    }

    /**
     * Overwrite parent __call method
     *
     * @param [type] $property
     * @param [type] $args
     * @return void
     */
    public function __call($property, $args)
    {
        $property = str_replace("get_", "", $property);

        if (property_exists($this, $property)) {
            return $this->$property;
        }
    }

    /**
     * Get Xcloner Main Class Container
     *
     * @return void
     */
    private function get_xcloner_container()
    {
        return $this;
    }
}
<?php

namespace watchfulli\XClonerCore;

/*
 *      class-xcloner-database.php
 *
 *      Copyright 2017 Ovidiu Liuta <info@thinkovi.com>
 *
 *      This program is free software; you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation; either version 2 of the License, or
 *      (at your option) any later version.
 *
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 *
 *      You should have received a copy of the GNU General Public License
 *      along with this program; if not, write to the Free Software
 *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *      MA 02110-1301, USA.
 */

 if (!class_exists('wpdb')) {
     require_once __DIR__ . "/../lib/wp-db.php";
 }

class Xcloner_Database extends \wpdb
{
    public $debug = 0;
    public $recordsPerSession = 10000;
    public $dbCompatibility = "";
    public $dbDropSyntax				= 1;
    public $countRecords				= 0;

    private $link;
    private $db_selected;
    private $logger;
    private $xcloner_settings;
    private $fs;
    
    private $xcloner_dbhost;
    private $xcloner_dbuser;
    private $xcloner_dbpassword;
	private $xcloner_dbname;
	private $xcloner_prefix;

    private $TEMP_DBPROCESS_FILE = ".database";
    private $TEMP_DUMP_FILE = "database-backup.sql";
    
    public function __construct(Xcloner $xcloner_container, $wp_user = "", $wp_pass = "", $wp_db = "", $wp_host = "")
    {
        $this->logger = $xcloner_container->get_xcloner_logger()->withName("xcloner_database");
        $this->xcloner_settings = $xcloner_container->get_xcloner_settings();
        $this->fs = $xcloner_container->get_xcloner_filesystem();
        
        if ($this->xcloner_settings->get_xcloner_option('xcloner_database_records_per_request')) {
            $this->recordsPerSession = $this->xcloner_settings->get_xcloner_option('xcloner_database_records_per_request');
        }
        
        if (!$this->recordsPerSession) {
            $this->recordsPerSession = 100;
        }
        
        $this->xcloner_dbhost 	= $this->xcloner_settings->get_db_hostname();
        $this->xcloner_dbuser 	= $this->xcloner_settings->get_db_username();
        $this->xcloner_dbpassword 	= $this->xcloner_settings->get_db_password();
        $this->xcloner_dbname = $this->xcloner_settings->get_db_database();
		$this->xcloner_prefix= "";
		
		//fetch the default wordpress mysql credentials
        if ( !$this->xcloner_dbuser || !$this->xcloner_dbhost || !$this->xcloner_dbname ) {
            global $wpdb;

            $this->xcloner_dbhost 		= $wpdb->dbhost;
            update_option('xcloner_mysql_hostname', $this->xcloner_dbhost);
            $this->xcloner_dbuser 		= $wpdb->dbuser;
            update_option('xcloner_mysql_username', $this->xcloner_dbuser);
            $this->xcloner_dbpassword 	= $wpdb->dbpassword;
            update_option('xcloner_mysql_password', $this->xcloner_dbpassword);
            $this->xcloner_dbname 		= $wpdb->dbname;
			update_option('xcloner_mysql_database', $this->xcloner_dbname);
			$this->xcloner_prefix 		= $wpdb->prefix;
			update_option('xcloner_mysql_prefix', $this->xcloner_prefix);

        }
        
        parent::__construct($this->xcloner_dbuser, $this->xcloner_dbpassword, $this->xcloner_dbname, $this->xcloner_dbhost);
        
        //$this->use_mysqli = true;
    }
	
	public function getPrefix() {
		return $this->xcloner_prefix;
	}

    public function getDbHost()
    {
        return $this->xcloner_dbhost;
    }

    public function getDbUser()
    {
        return $this->xcloner_dbuser;
    }

    public function getDbPassword()
    {
        return $this->xcloner_dbpassword;
    }

    public function getDbName()
    {
        return $this->xcloner_dbname;
    }

    /*
     * Initialize the database connection
     *
     * name: init
     * @param array $data {'dbHostname', 'dbUsername', 'dbPassword', 'dbDatabase'}
     * @return
     */
    public function init($data, $start = 0)
    {
        if ($start and $this->fs->get_tmp_filesystem()->has($this->TEMP_DBPROCESS_FILE)) {
            $this->fs->get_tmp_filesystem()->delete($this->TEMP_DBPROCESS_FILE);
        }
        
        $this->headers();
        
        $this->suppress_errors = true;
    }
    
    public function start_database_recursion($params, $extra_params, $init = 0)
    {
        $tables = array();
        $return['finished'] = 0;
        $return['stats'] = array(
                "total_records"=>0,
                "tables_count"=>0,
                "database_count"=>0,
        );
        
        if (!$this->xcloner_settings->get_enable_mysql_backup()) {
            $return['finished'] = 1;
            return $return;
        }
        
        $this->logger->debug(__("Starting database backup process"));
        
        $this->init($params, $init);
        
        if ($init) {
            $db_count = 0;
            
            if (isset($params['#'])) {
                foreach ($params['#'] as $database) {
                    if (!isset($params[$database]) or !is_array($params[$database])) {
                        $params[$database] = array();
                    }
                }
                $db_count = -1;
            }
            
            if (isset($params) and is_array($params)) {
                foreach ($params as $database=>$tables) {
                    if ($database != "#") {
                        $stats = $this->write_backup_process_list($database, $tables);
                        $return['stats']['tables_count'] += $stats['tables_count'];
                        $return['stats']['total_records'] += $stats['total_records'];
                    }
                }
            }

            if (sizeof($params)) {
                $return['stats']['database_count'] = sizeof($params) + $db_count;
            } else {
                $return['stats']['database_count'] = 0;
            }
                
            return $return;
        }
        
        if (!isset($extra_params['startAtLine'])) {
            $extra_params['startAtLine'] = 0;
        }
        if (!isset($extra_params['startAtRecord'])) {
            $extra_params['startAtRecord'] = 0;
        }
        if (!isset($extra_params['dumpfile'])) {
            $extra_params['dumpfile'] = "";
        }
        
        $return = $this->process_incremental($extra_params['startAtLine'], $extra_params['startAtRecord'], $extra_params['dumpfile']);
        
        return $return;
    }
    
    public function log($message = "")
    {
        if ($message) {
            $this->logger->info($message, array(""));
        } else {
            if ($this->last_query) {
                $this->logger->debug($this->last_query, array(""));
            }
            if ($this->last_error) {
                $this->logger->error($this->last_error, array(""));
            }
        }
        
        return;
    }
    
    /*
     *Return any error
     *
     * name: error
     * @param string $message
     * @return
    */
    public function error($message)
    {
        $this->logger->error($message, array(""));
        
        return;
    }

    /*
     * Send some special headers after the connection is initialized
     *
     * name: headers
     * @param
     */
    private function headers()
    {
        $this->logger->debug(__("Setting mysql headers"));
        
        $this->query("SET SQL_QUOTE_SHOW_CREATE=1;");
        //$this->log();
        $this->query("SET sql_mode = 0;");
        //$this->log();
        
        $this->set_charset($this->dbh, 'utf8');
        //$this->log();
    }

    public function get_database_num_tables($database)
    {
        $this->logger->debug(sprintf(__("Getting number of tables in %s"), $database));
        
        $query = "show tables in `".$database."`";
        
        $res = $this->get_results($query);
        $this->log();
            
        return count($res);
    }
    
    public function get_all_databases()
    {
        $this->logger->debug(("Getting all databases"));
        
        $query = "SHOW DATABASES;";
        
        $databases = $this->get_results($query);
        $this->log();
        
        $databases_list = array();
        
        $i = 0;
        
        $databases_list[$i]['name'] = $this->dbname;
        $databases_list[$i]['num_tables'] = $this->get_database_num_tables($this->dbname);
        $i++;
        
        if (is_array($databases)) {
            foreach ($databases as $db) {
                if ($db->Database != $this->dbname) {
                    $databases_list[$i]['name'] = $db->Database;
                    $databases_list[$i]['num_tables'] = $this->get_database_num_tables($db->Database);
                    $i++;
                }
            }
        }
        
        return $databases_list;
    }
    
    /*
     * Returns an array of tables from a database and mark $excluded ones
     *
     * name: list_tables
     * @param string $database
     * @param array $included
     * @param int $get_num_records
     * @return array $tablesList
     */
    public function list_tables($database = "", $included = array(), $get_num_records = 0)
    {
        $tablesList[0] = array( );
        $inc = 0;

        if (!$database) {
            $database = $this->dbname;
        }
        
        $this->logger->debug(sprintf(("Listing tables in %s database"), $database));
        
        $tables = $this->get_results("SHOW TABLES in `".$database."`");
        $this->log();

        foreach ($tables as $table) {
            $table = array_values((array)$table)[0];
            
            $tablesList[$inc]['name'] = $table;
            $tablesList[$inc]['database'] = $database;

            if ($get_num_records) {
                $records_num_result = $this->get_var("SELECT count(*) FROM `".$database."`.`".$table."`");
                $this->log();
                    
                $tablesList[$inc]['records'] = $records_num_result;
            }
            
            $tablesList[$inc]['excluded'] = 0;
                        
            if (sizeof($included) and is_array($included)) {
                $dbTable = $database.".".$table;
                if (!in_array($table, $included) and !in_array($dbTable, $included)) {
                    $tablesList[$inc]['excluded'] = 1;
                    $this->log(sprintf(__("Excluding table %s.%s from backup"), $table, $database));
                }
            }
            
            $inc++;
        }

        return $tablesList;
    }

    public function write_backup_process_list($dbname, $incl_tables)
    {
        $return['total_records'] = 0;
        $return['tables_count'] = 0;
        
        $this->log(__("Preparing the database recursion file"));
        
        $tables = $this->list_tables($dbname, $incl_tables, 1);
        
        if ($this->dbname != $dbname) {
            $dumpfile = $dbname."-backup.sql";
        } else {
            $dumpfile = $this->TEMP_DUMP_FILE;
        }
        
        $line = sprintf("###newdump###\t%s\t%s\n", $dbname, $dumpfile);
        $this->fs->get_tmp_filesystem_append()->write($this->TEMP_DBPROCESS_FILE, $line);
            
        // write this to the class and write to $TEMP_DBPROCESS_FILE file as database.table records
        foreach ($tables as $key=>$table) {
            if ($table != "" and !$tables[$key]['excluded']) {
                $line = sprintf("`%s`.`%s`\t%s\t%s\n", $dbname, $tables[$key]['name'], $tables[$key]['records'], $tables[$key]['excluded']);
                $this->fs->get_tmp_filesystem_append()->write($this->TEMP_DBPROCESS_FILE, $line);
                $return['tables_count']++;
                $return['total_records'] += $tables[$key]['records'];
            }
        }

        $line = sprintf("###enddump###\t%s\t%s\n", $dbname, $dumpfile);
        $this->fs->get_tmp_filesystem_append()->write($this->TEMP_DBPROCESS_FILE, $line);
        
        return $return;
    }

    /*
     * Returns the number of records from a table
     *
     * name: countRecords
     * @param string $table - the source table
     * @return int $count
     */
    public function countRecords($table)
    {
        $table = "`".$this->dbname."`.`$table`";

        $result = $this->get_var("SELECT count(*) FROM $table;");

        return intval($result) ;// not max limit on 32 bit systems 2147483647; on 64 bit 9223372036854775807
    }

    /*
     *	Processing the mysql backup incrementally
     *
     * name: processIncremental
     * @param
     * 		int $startAtLine - at which line from the perm.txt file to start reading
     * 		int startAtRecord - at which record to start from the table found at $startAtLine
     * 		string $dumpfie	- where to save the data
     * 		string $dbCompatibility - MYSQL40, MYSQ32, none=default
     * 		int $dbDropSyntax	- check if the DROP TABLE syntax should be added
     * @return array $return
     */
    public function process_incremental($startAtLine = 0, $startAtRecord = 0, $dumpfile = "", $dbCompatibility = "")
    {
        $count = 0;
        $return['finished'] = 0;
        $lines = array();
        
        if ($this->fs->get_tmp_filesystem()->has($this->TEMP_DBPROCESS_FILE)) {
            $lines = array_filter(explode("\n", $this->fs->get_tmp_filesystem()->read($this->TEMP_DBPROCESS_FILE)));
        }
    
        foreach ($lines as $buffer) {
            if ($count == $startAtLine) {
                $tableInfo = explode("\t", $buffer);
                
                if ($tableInfo[0] == "###newdump###") {
                    // we create a new mysql dump file
                    if ($dumpfile != "") {
                        // we finished a previous one and write the footers
                        $return['dumpsize'] = $this->data_footers($dumpfile);
                    }
    
                    $dumpfile = $tableInfo[2];
                        
                    $this->log(sprintf(__("Starting new backup dump to file %s"), $dumpfile));
                        
                    $this->data_headers($dumpfile, $tableInfo[1]);
                    $dumpfile = $tableInfo[2];
                    $startAtLine++;
                    $return['new_dump'] = 1;
                //break;
                } else {
                    //we export the table
                    if ($tableInfo[0] == "###enddump###") {
                        $return['endDump'] = 1;
                    }
    
                    //table is excluded
                    if ($tableInfo[2]) {
                        continue;
                    }
                            
                    $next = $startAtRecord + $this->recordsPerSession;
                        
                    // $tableInfo[1] number of records in the table
                    $table = explode("`.`", $tableInfo[0]);
                    $tableName = str_replace("`", "", $table[1]);
                    $databaseName = str_replace("`", "", $table[0]);

                    //return something to the browser
                    $return['databaseName'] 	= $databaseName;
                    $return['tableName'] = $tableName;
                    $return['totalRecords'] 	= $tableInfo[1];

                    $processed_records = 0;
                        
                    if (trim($tableName) != "" and !$tableInfo[2]) {
                        $processed_records = $this->export_table($databaseName, $tableName, $startAtRecord, $this->recordsPerSession, $dumpfile);
                    }
                        
                    $return['processedRecords'] = $startAtRecord + $processed_records;
                        
                    if ($next >= $tableInfo[1]) { //we finished loading the records for next sessions, will go to the new record
                        $startAtLine++;
                        $startAtRecord = 0;
                    } else {
                        $startAtRecord = $startAtRecord + $this->recordsPerSession;
                    }

                    //$return['dbCompatibility'] 	= self::$dbCompatibility;
                        
                    $return['startAtLine'] = $startAtLine;
                    $return['startAtRecord'] = $startAtRecord;
                    $return['dumpfile']			= $dumpfile;
                    $return['dumpsize']			= $this->fs->get_tmp_filesystem_append()->getSize($dumpfile);

                    return $return;
                    break;
                }
            }
    
            $count++;
        }
    
        //while is finished, lets go home...
        if ($dumpfile != "") {
            // we finished a previous one and write the footers
            $return['dumpsize'] = $this->data_footers($dumpfile);
            $return['dumpfile'] = ($dumpfile);
        }
        $return['finished'] = 1;
        $return['startAtLine'] = $startAtLine;
        
        if ($this->fs->get_tmp_filesystem()->has($this->TEMP_DBPROCESS_FILE)) {
            $this->fs->get_tmp_filesystem()->delete($this->TEMP_DBPROCESS_FILE);
        }
        
        $this->logger->debug(sprintf(("Database backup finished!")));
        
        return $return;
    }


    /*
     * Exporting the table records
     *
     * name: exportTable
     * @param
     * 		string $databaseName - database name of the table
     * 		string tableName - table name
     * 		int $start - where to start from
     * 		int $limit - how many records
     * 		handler $fd - file handler where to write the records
     * @return
     */

    /**
     * @param integer $start
     * @param integer $limit
     */
    public function export_table($databaseName, $tableName, $start, $limit, $dumpfile)
    {
        $this->logger->debug(sprintf(("Exporting table  %s.%s data"), $databaseName, $tableName));
        
        $records = 0;
        
        if ($start == 0) {
            $this->dump_structure($databaseName, $tableName, $dumpfile);
        }

        $start = intval($start);
        $limit = intval($limit);
        //exporting the table content now

        $query = "SELECT * from `$databaseName`.`$tableName` Limit $start, $limit ;";
        if ($this->use_mysqli) {
            $result = mysqli_query($this->dbh, $query);
            $mysql_fetch_function = "mysqli_fetch_array";
        } else {
            $result = mysql_query($query, $this->dbh);
            $mysql_fetch_function = "mysqli_fetch_array";
        }
        //$result = $this->get_results($query, ARRAY_N);
        //print_r($result); exit;
        
        if ($result) {
            while ($row = $mysql_fetch_function($result, MYSQLI_ASSOC)) {
                $this->fs->get_tmp_filesystem_append()->write($dumpfile, "INSERT INTO `$tableName` VALUES (");
                $arr = $row;
                $buffer = "";
                $this->countRecords++;

                foreach ($arr as $key => $value) {
                    if (!is_null($value)) {
                        $value = $this->_real_escape($value);

                        if (method_exists($this, 'remove_placeholder_escape')) {
                            $value = $this->remove_placeholder_escape($value);
                        }
                        $buffer .= "'" . $value . "', ";
                    } else {
                        $buffer .= "null, ";
                    }
                }
                $buffer = rtrim($buffer, ', ').");\n";
                $this->fs->get_tmp_filesystem_append()->write($dumpfile, $buffer);
                unset($buffer);
                    
                $records++;
            }
        }
        
        $this->log(sprintf(__("Dumping %s records starting position %s from %s.%s table"), $records, $start, $databaseName, $tableName));
        
        return $records;
    }

    public function dump_structure($databaseName, $tableName, $dumpfile)
    {
        $this->log(sprintf(__("Dumping the structure for %s.%s table"), $databaseName, $tableName));
        
        $line = ("\n#\n# Table structure for table `$tableName`\n#\n\n");
        $this->fs->get_tmp_filesystem_append()->write($dumpfile, $line);

        if ($this->dbDropSyntax) {
            $line = ("\nDROP table IF EXISTS `$tableName`;\n");
            $this->fs->get_tmp_filesystem_append()->write($dumpfile, $line);
        }

        //$result = mysqli_query($this->dbh,"SHOW CREATE table `$databaseName`.`$tableName`;");
        $result = $this->get_row("SHOW CREATE table `$databaseName`.`$tableName`;", ARRAY_N);
        if ($result) {
            //$row = mysqli_fetch_row( $result);
            $line = ($result[1].";\n");
            $this->fs->get_tmp_filesystem_append()->write($dumpfile, $line);
        }

        $line = ("\n#\n# End Structure for table `$tableName`\n#\n\n");
        $line .= ("#\n# Dumping data for table `$tableName`\n#\n\n");
        $this->fs->get_tmp_filesystem_append()->write($dumpfile, $line);
        
        return;
    }

    public function data_footers($dumpfile)
    {
        $this->logger->debug(sprintf(("Writing dump footers in file"), $dumpfile));
        // we finished the dump file, not return the size of it
        $this->fs->get_tmp_filesystem_append()->write($dumpfile, "\n#\n# Finished at: ".date("M j, Y \a\\t H:i")."\n#");
        $size = $this->fs->get_tmp_filesystem_append()->getSize($dumpfile);
        
        $metadata_dumpfile = $this->fs->get_tmp_filesystem()->getMetadata($dumpfile);
        
        //adding dump file to the included files list
        $this->fs->store_file($metadata_dumpfile, 'tmp_filesystem');
        
        return $size;
    }

    public function resetcountRecords()
    {
        $this->countRecords = 0;

        return $this->countRecords;
    }

    public function getcountRecords()
    {
        return $this->countRecords;
    }


    public function data_headers($file, $database)
    {
        $this->logger->debug(sprintf(("Writing dump header for %s database in file"), $database, $file));
        
        $return = "";

        $return .= "#\n";
        $return .= "# Powered by XCloner Site Backup\n";
        $return .= "# http://www.xcloner.com\n";
        $return .= "#\n";
        $return .= "# Host: ".get_site_url()."\n";
        $return .= "# Generation Time: ".date("M j, Y \a\\t H:i")."\n";
        $return .= "# PHP Version: ".phpversion()."\n";
        $return .= "# Database Charset: ".$this->charset."\n";
        
        $results = $this->get_results("SHOW VARIABLES LIKE \"%version%\";", ARRAY_N);
        if (isset($results)) {
            foreach ($results as $result) {
                $return .= "# MYSQL ".$result[0].": ".$result[1]."\n";
            }
        }
        
        $results = $this->get_results("SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
					FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '".$database."';");
        
        if (isset($results[0])) {
            $return .= "# MYSQL DEFAULT_CHARACTER_SET_NAME: ".$results[0]->DEFAULT_CHARACTER_SET_NAME."\n";
            $return .= "# MYSQL SCHEMA_NAME: ".$results[0]->DEFAULT_COLLATION_NAME."\n";
        }

        $return .= "#\n# Database : `".$database."`\n# --------------------------------------------------------\n\n";
        
        $this->log(sprintf(__("Writing %s database dump headers"), $database));
        
        $return = $this->fs->get_tmp_filesystem()->write($file, $return);
        return $return['size'];
    }
}
<?php
namespace watchfulli\XClonerCore;

class Xcloner_Requirements {

	var $min_php_version = "5.6.0";
	var $safe_mode = "Off";

	private $xcloner_settings;
	private $xcloner_container;

	public function __construct(Xcloner $xcloner_container) {
		$this->xcloner_container = $xcloner_container;
		$this->xcloner_settings  = $xcloner_container->get_xcloner_settings();
	}

	private function get_xcloner_container() {
		return $this->xcloner_container;
	}

	public function check_backup_ready_status() {
		if (!$this->check_min_php_version(1)) {
			return false;
		}

		if (!$this->check_safe_mode(1)) {
			return false;
		}

		if (!$this->check_xcloner_start_path(1)) {
			return false;
		}

		if (!$this->check_xcloner_store_path(1)) {
			return false;
		}

		if (!$this->check_xcloner_tmp_path(1)) {
			return false;
		}

		return true;
	}

	public function get_constant($var) {
		return $this->$var;
	}

	public function check_min_php_version($return_bool = 0) {

		if ($return_bool == 1) {
			if (version_compare(phpversion(), $this->min_php_version, '<')) {
				return false;
			} else {
				return true;
			}
		}

		return phpversion();
	}

	public function check_safe_mode($return_bool = 0) {
		/*no longer needed for PHP 7*/
		$safe_mode = "Off";

		/*if($return_bool)
		{
			if( ini_get('safe_mode') )
				return false;
			else
				return true;
		}
		
		if( ini_get('safe_mode') )
			$safe_mode = "On";
		* */

		return $safe_mode;
	}

	public function check_xcloner_start_path($return_bool = 0) {
		$path = $this->xcloner_settings->get_xcloner_start_path();

		if ($return_bool) {
			if (!file_exists($path)) {
				return false;
			}

			return is_readable($path);
		}

		return $path;
	}

	public function check_xcloner_tmp_path($return_bool = 0) {
		$path = $this->xcloner_settings->get_xcloner_tmp_path();

		if ($return_bool) {
			if (!file_exists($path)) {
				return false;
			}

			if (!is_writeable($path)) {
				@chmod($path, 0777);
			}

			return is_writeable($path);
		}

		return $path;
	}

	public function check_xcloner_store_path($return_bool = 0) {
		$path = $this->xcloner_settings->get_xcloner_store_path();

		if ($return_bool) {
			if (!file_exists($path)) {
				return false;
			}

			if (!is_writeable($path)) {
				@chmod($path, 0777);
			}

			return is_writeable($path);
		}

		return $path;
	}

	public function get_max_execution_time() {
		return ini_get('max_execution_time');
	}

	public function get_memory_limit() {
		return ini_get('memory_limit');
	}

	public function get_open_basedir() {
		$open_basedir = ini_get('open_basedir');

		if (!$open_basedir) {
			$open_basedir = "none";
		}

		return $open_basedir;
	}

	public function get_free_disk_space() {
		return $this->file_format_size(disk_free_space($this->xcloner_settings->get_xcloner_store_path()));
	}

	public function file_format_size($bytes, $decimals = 2) {
		$unit_list = array('B', 'KB', 'MB', 'GB', 'PB');

		if ($bytes == 0) {
			return $bytes.' '.$unit_list[0];
		}

		$unit_count = count($unit_list);
		for ($i = $unit_count - 1; $i >= 0; $i--) {
			$power = $i * 10;
			if (($bytes >> $power) >= 1) {
				return round($bytes / (1 << $power), $decimals).' '.$unit_list[$i];
			}
		}
	}
}

?>
<?php 

namespace watchfulli\XClonerCore;

class Index
{
    public function greet($greet = "Hello World")
    {
        return $greet;
    }
}<?php
namespace watchfulli\XClonerCore;

/**
 * XCloner - Backup and Restore backup plugin for Wordpress
 *
 * class-xcloner-remote-storage.php
 * @author Liuta Ovidiu <info@thinkovi.com>
 *
 *        This program is free software; you can redistribute it and/or modify
 *        it under the terms of the GNU General Public License as published by
 *        the Free Software Foundation; either version 2 of the License, or
 *        (at your option) any later version.
 *
 *        This program is distributed in the hope that it will be useful,
 *        but WITHOUT ANY WARRANTY; without even the implied warranty of
 *        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *        GNU General Public License for more details.
 *
 *        You should have received a copy of the GNU General Public License
 *        along with this program; if not, write to the Free Software
 *        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *        MA 02110-1301, USA.
 *
 * @link https://github.com/ovidiul/XCloner-Wordpress
 *
 * @modified 7/25/18 2:15 PM
 *
 */

use League\Flysystem\Config;
use League\Flysystem\Filesystem;

use League\Flysystem\Adapter\Ftp as Adapter;

use League\Flysystem\Sftp\SftpAdapter;

use Srmklive\Dropbox\Client\DropboxClient;
use Srmklive\Dropbox\Adapter\DropboxAdapter;

use League\Flysystem\AzureBlobStorage\AzureBlobStorageAdapter;
use MicrosoftAzure\Storage\Blob\BlobRestProxy;

use Aws\S3\S3Client;
use League\Flysystem\AwsS3v3\AwsS3Adapter;

use Mhetreramesh\Flysystem\BackblazeAdapter;
use BackblazeB2\Client as B2Client;

use Sabre\DAV\Client as SabreClient;
use League\Flysystem\WebDAV\WebDAVAdapter;

/**
 * Class Xcloner_Remote_Storage
 */
class Xcloner_Remote_Storage
{
    private $gdrive_app_name = "XCloner Backup and Restore";

    private $storage_fields = array(
        "option_prefix" => "xcloner_",
        "ftp" => array(
            "text" => "FTP",
            "ftp_enable" => "int",
            "ftp_hostname" => "string",
            "ftp_port" => "int",
            "ftp_username" => "string",
            "ftp_password" => "raw",
            "ftp_path" => "path",
            "ftp_transfer_mode" => "int",
            "ftp_ssl_mode" => "int",
            "ftp_timeout" => "int",
            "ftp_cleanup_retention_limit_days" => "float",
            "ftp_cleanup_exclude_days" => "string",
            "ftp_cleanup_retention_limit_archives" => "int",
            "ftp_cleanup_capacity_limit" => "int"
        ),
        "sftp" => array(
            "text" => "SFTP",
            "sftp_enable" => "int",
            "sftp_hostname" => "string",
            "sftp_port" => "int",
            "sftp_username" => "string",
            "sftp_password" => "raw",
            "sftp_path" => "path",
            "sftp_private_key" => "raw",
            "sftp_timeout" => "int",
            "sftp_cleanup_retention_limit_days" => "float",
            "sftp_cleanup_exclude_days" => "string",
            "sftp_cleanup_retention_limit_archives" => "int",
            "sftp_cleanup_capacity_limit" => "int"
        ),
        "aws" => array(
            "text" => "S3",
            "aws_enable" => "int",
            "aws_key" => "string",
            "aws_secret" => "raw",
            "aws_endpoint" => "string",
            "aws_region" => "string",
            "aws_bucket_name" => "string",
            "aws_prefix" => "string",
            "aws_cleanup_retention_limit_days" => "float",
            "aws_cleanup_exclude_days" => "string",
            "aws_cleanup_retention_limit_archives" => "int",
            "aws_cleanup_capacity_limit" => "int"
        ),
        "dropbox" => array(
            "text" => "Dropbox",
            "dropbox_enable" => "int",
            "dropbox_access_token" => "string",
            "dropbox_app_secret" => "raw",
            "dropbox_prefix" => "string",
            "dropbox_cleanup_retention_limit_days" => "float",
            "dropbox_cleanup_exclude_days" => "string",
            "dropbox_cleanup_retention_limit_archives" => "int",
            "dropbox_cleanup_capacity_limit" => "int"
        ),
        "azure" => array(
            "text" => "Azure BLOB",
            "azure_enable" => "int",
            "azure_account_name" => "string",
            "azure_api_key" => "raw",
            "azure_container" => "string",
            "azure_cleanup_retention_limit_days" => "float",
            "azure_cleanup_exclude_days" => "string",
            "azure_cleanup_retention_limit_archives" => "int",
            "azure_cleanup_capacity_limit" => "int"
        ),
        "backblaze" => array(
            "text" => "Backblaze",
            "backblaze_enable" => "int",
            "backblaze_account_id" => "string",
            "backblaze_application_key" => "raw",
            "backblaze_bucket_name" => "string",
            "backblaze_cleanup_retention_limit_days" => "float",
            "backblaze_cleanup_exclude_days" => "string",
            "backblaze_cleanup_retention_limit_archives" => "int",
            "backblaze_cleanup_capacity_limit" => "int"
        ),

        "webdav" => array(
            "text" => "WebDAV",
            "webdav_enable" => "int",
            "webdav_url" => "string",
            "webdav_username" => "string",
            "webdav_password" => "raw",
            "webdav_target_folder" => "string",
            "webdav_cleanup_retention_limit_days" => "float",
            "webdav_cleanup_exclude_days" => "string",
            "webdav_cleanup_retention_limit_archives" => "int",
            "webdav_cleanup_capacity_limit" => "int"

        ),

        "gdrive" => array(
            "text" => "Google Drive",
            "gdrive_enable" => "int",
            "gdrive_access_code" => "string",
            "gdrive_client_id" => "string",
            "gdrive_client_secret" => "raw",
            "gdrive_target_folder" => "string",
            "gdrive_cleanup_retention_limit_days" => "float",
            "gdrive_empty_trash" => "int",
            "gdrive_cleanup_exclude_days" => "string",
            "gdrive_cleanup_retention_limit_archives" => "int",
            "gdrive_cleanup_capacity_limit" => "int"
        ),
    );

    private $aws_regions = array(
        'us-east-1' => 'US East (N. Virginia)',
        'us-east-2' => 'US East (Ohio)',
        'us-west-1' => 'US West (N. California)',
        'us-west-2' => 'US West (Oregon)',
        'ca-central-1' => 'Canada (Central)',
        'eu-west-1' => 'EU (Ireland)',
        'eu-central-1' => 'EU (Frankfurt)',
        'eu-west-2' => 'EU (London)',
        'ap-northeast-1' => 'Asia Pacific (Tokyo)',
        'ap-northeast-2' => 'Asia Pacific (Seoul)',
        'ap-southeast-1' => 'Asia Pacific (Singapore)',
        'ap-southeast-2' => 'Asia Pacific (Sydney)',
        'ap-south-1' => 'Asia Pacific (Mumbai)',
        'sa-east-1' => 'South America (São Paulo)'
    );

    private $xcloner_sanitization;
    private $xcloner_file_system;
    private $xcloner_settings;
    private $logger;
    private $xcloner;

    /**
     * Xcloner_Remote_Storage constructor.
     * @param Xcloner $xcloner_container
     */
    public function __construct(Xcloner $xcloner_container)
    {
        $this->xcloner_sanitization = $xcloner_container->get_xcloner_sanitization();
        $this->xcloner_file_system = $xcloner_container->get_xcloner_filesystem();
        $this->xcloner_settings = $xcloner_container->get_xcloner_settings();
        $this->logger = $xcloner_container->get_xcloner_logger()->withName("xcloner_remote_storage");
        $this->xcloner = $xcloner_container;

        foreach ($this->storage_fields as $main_key => $array) {
            if (is_array($array)) {
                foreach ($array as $key => $type) {
                    if ($type == "raw") {
                        add_filter(
                            "pre_update_option_" . $this->storage_fields['option_prefix'] . $key,
                            function ($value) {
                                return $this->simple_crypt($value, 'e');
                            },
                            10,
                            1
                        );

                        add_filter("option_" . $this->storage_fields['option_prefix'] . $key, function ($value) {
                            return $this->simple_crypt($value, 'd');
                        }, 10, 1);
                    }
                }
            }
        }
    }

    /**
     * Encrypts and Decrypt a string based on openssl lib
     *
     * @param $string
     * @param string $action
     * @return string
     */
    private function simple_crypt($string, $action = 'e')
    {
        // you may change these values to your own
        $secret_key = NONCE_KEY;
        $secret_iv = NONCE_SALT;

        if (!$string) {
            //we do no encryption for empty data
            return $string;
        }

        $output = $string;
        $encrypt_method = "AES-256-CBC";
        $key = hash('sha256', $secret_key);
        $iv = substr(hash('sha256', $secret_iv), 0, 16);

        if ($action == 'e' && function_exists('openssl_encrypt')) {
            $output = base64_encode(openssl_encrypt($string, $encrypt_method, $key, 0, $iv));
        } else {
            if ($action == 'd' && function_exists('openssl_decrypt') && base64_decode($string)) {
                $decrypt = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
                if ($decrypt) {
                    //we check if decrypt was succesful
                    $output = $decrypt;
                }
            }
        }

        return $output;
    }

    private function get_xcloner_container()
    {
        return $this->xcloner;
    }

    public function get_available_storages()
    {
        $return = array();
        foreach ($this->storage_fields as $storage => $data) {
            $check_field = $this->storage_fields["option_prefix"] . $storage . "_enable";
            if ($this->xcloner_settings->get_xcloner_option($check_field)) {
                $return[$storage] = $data['text'];
            }
        }
        
        return $return;
    }

    public function save($action = "ftp")
    {
        if (!$action) {
            return false;
        }

        $storage = $this->xcloner_sanitization->sanitize_input_as_string($action);
        $this->logger->debug(sprintf("Saving the remote storage %s options", strtoupper($action)));

        if (is_array($this->storage_fields[$storage])) {
            foreach ($this->storage_fields[$storage] as $field => $validation) {
                $check_field = $this->storage_fields["option_prefix"] . $field;
                $sanitize_method = "sanitize_input_as_" . $validation;

                //we do not save empty encrypted credentials
                if ($validation == "raw" && str_repeat('*', strlen($_POST[$check_field])) == $_POST[$check_field] && $_POST[$check_field]) {
                    continue;
                }

                if (!isset($_POST[$check_field])) {
                    $_POST[$check_field] = 0;
                }

                if (!method_exists($this->xcloner_sanitization, $sanitize_method)) {
                    $sanitize_method = "sanitize_input_as_string";
                }

                $sanitized_value = $this->xcloner_sanitization->$sanitize_method(stripslashes($_POST[$check_field]));
                update_option($check_field, $sanitized_value);
            }

            $this->xcloner->trigger_message(
                __("%s storage settings saved.", 'xcloner-backup-and-restore'),
                "success",
                $this->storage_fields[$action]['text']
            );
        }
    }

    public function check($action = "ftp")
    {
        try {
            $this->verify_filesystem($action);
            $this->xcloner->trigger_message(
                __("%s connection is valid.", 'xcloner-backup-and-restore'),
                "success",
                $this->storage_fields[$action]['text']
            );
            $this->logger->debug(sprintf("Connection to remote storage %s is valid", strtoupper($action)));
        } catch (Exception $e) {
            $this->xcloner->trigger_message(
                "%s connection error: " . $e->getMessage(),
                "error",
                $this->storage_fields[$action]['text']
            );
        }
    }

    /**
     * @param string $storage_type
     */
    public function verify_filesystem($storage_type)
    {
        $method = "get_" . $storage_type . "_filesystem";

        $this->logger->info(sprintf(
            "Checking validity of the remote storage %s filesystem",
            strtoupper($storage_type)
        ));

        if (!method_exists($this, $method)) {
            return false;
        }

        list($adapter, $filesystem) = $this->$method();

        $test_file = substr(".xcloner_" . md5(time()), 0, 15);

        if ($storage_type == "gdrive") {
            if (!is_array($filesystem->listContents())) {
                throw new \Exception(__("Could not read data", 'xcloner-backup-and-restore'));
            }
            $this->logger->debug(sprintf("I can list data from remote storage %s", strtoupper($storage_type)));

            return true;
        }

        //testing write access
        if (!$filesystem->write($test_file, "data")) {
            throw new \Exception(__("Could not write data", 'xcloner-backup-and-restore'));
        }
        $this->logger->debug(sprintf("I can write data to remote storage %s", strtoupper($storage_type)));

        //testing read access
        if (!$filesystem->has($test_file)) {
            throw new \Exception(__("Could not read data", 'xcloner-backup-and-restore'));
        }
        $this->logger->debug(sprintf("I can read data to remote storage %s", strtoupper($storage_type)));

        //delete test file
        if (!$filesystem->delete($test_file)) {
            throw new \Exception(__("Could not delete data", 'xcloner-backup-and-restore'));
        }
        $this->logger->debug(sprintf("I can delete data to remote storage %s", strtoupper($storage_type)));

        return true;
    }

    public function upload_backup_to_storage($file, $storage, $delete_local_copy_after_transfer = 0)
    {
        if (!$this->xcloner_file_system->get_storage_filesystem()->has($file)) {
            $this->logger->info(sprintf("File not found %s in local storage", $file));

            return false;
        }

        $method = "get_" . $storage . "_filesystem";

        if (!method_exists($this, $method)) {
            return false;
        }

        list($remote_storage_adapter, $remote_storage_filesystem) = $this->$method();

        //doing remote storage cleaning here
        $this->clean_remote_storage($storage, $remote_storage_filesystem);

        $this->logger->info(
            sprintf("Transferring backup %s to remote storage %s", $file, strtoupper($storage)),
            array("")
        );

        /*if(!$this->xcloner_file_system->get_storage_filesystem()->has($file))
        {
            $this->logger->info(sprintf("File not found %s in local storage", $file));
            return false;
        }*/

        $backup_file_stream = $this->xcloner_file_system->get_storage_filesystem()->readStream($file);

        if (!$remote_storage_filesystem->writeStream($file, $backup_file_stream)) {
            $this->logger->info(sprintf("Could not transfer file %s", $file));

            return false;
        }

        if ($this->xcloner_file_system->is_multipart($file)) {
            $parts = $this->xcloner_file_system->get_multipart_files($file);
            if (is_array($parts)) {
                foreach ($parts as $part_file) {
                    $this->logger->info(sprintf(
                        "Transferring backup %s to remote storage %s",
                        $part_file,
                        strtoupper($storage)
                    ), array(""));

                    $backup_file_stream = $this->xcloner_file_system->get_storage_filesystem()->readStream($part_file);
                    if (!$remote_storage_filesystem->writeStream($part_file, $backup_file_stream)) {
                        return false;
                    }
                }
            }
        }

        $this->logger->info(sprintf("Upload done, disconnecting from remote storage %s", strtoupper($storage)));

        //CHECK IF WE SHOULD DELETE BACKUP AFTER REMOTE TRANSFER IS DONE
        //if ( $this->xcloner_settings->get_xcloner_option('xcloner_cleanup_delete_after_remote_transfer')) {
        if ($delete_local_copy_after_transfer) {
            $this->logger->info(sprintf("Deleting %s from local storage matching rule xcloner_cleanup_delete_after_remote_transfer", $file));
            $this->xcloner_file_system->delete_backup_by_name($file);
        }

        return true;
    }

    public function copy_backup_remote_to_local($file, $storage)
    {
        $method = "get_" . $storage . "_filesystem";

        $target_filename = $file;

        if (!method_exists($this, $method)) {
            return false;
        }

        list($remote_storage_adapter, $remote_storage_filesystem) = $this->$method();

        if (!$remote_storage_filesystem->has($file)) {
            $this->logger->info(sprintf("File not found %s in remote storage %s", $file, strtoupper($storage)));

            return false;
        }

        if ($storage == "gdrive") {
            $metadata = $remote_storage_filesystem->getMetadata($file);
            $target_filename = $metadata['filename'] . "." . $metadata['extension'];
        }

        $this->logger->info(sprintf(
            "Transferring backup %s to local storage from %s storage",
            $file,
            strtoupper($storage)
        ), array(""));

        $backup_file_stream = $remote_storage_filesystem->readStream($file);

        if (!$this->xcloner_file_system->get_storage_filesystem()->writeStream($target_filename, $backup_file_stream)) {
            $this->logger->info(sprintf("Could not transfer file %s", $file));

            return false;
        }

        if ($this->xcloner_file_system->is_multipart($target_filename)) {
            $parts = $this->xcloner_file_system->get_multipart_files($file, $storage);
            if (is_array($parts)) {
                foreach ($parts as $part_file) {
                    $this->logger->info(sprintf(
                        "Transferring backup %s to local storage from %s storage",
                        $part_file,
                        strtoupper($storage)
                    ), array(""));

                    $backup_file_stream = $remote_storage_filesystem->readStream($part_file);
                    if (!$this->xcloner_file_system->get_storage_filesystem()->writeStream(
                        $part_file,
                        $backup_file_stream
                    )) {
                        return false;
                    }
                }
            }
        }

        $this->logger->info(sprintf("Upload done, disconnecting from remote storage %s", strtoupper($storage)));

        return true;
    }

    public function clean_remote_storage($storage, $remote_storage_filesystem)
    {
        return $this->xcloner_file_system->backup_storage_cleanup($storage, $remote_storage_filesystem);
    }

    public function get_azure_filesystem()
    {
        $this->logger->info(sprintf("Creating the AZURE BLOB remote storage connection"), array(""));

        if (version_compare(phpversion(), '5.6.0', '<')) {
            throw new \Exception("AZURE BLOB requires PHP 5.6 to be installed!");
        }

        if (!class_exists('XmlWriter')) {
            throw new \Exception("AZURE BLOB requires libxml PHP module to be installed with XmlWriter class enabled!");
        }

        $endpoint = sprintf(
            'DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s',
            $this->xcloner_settings->get_xcloner_option("xcloner_azure_account_name"),
            $this->xcloner_settings->get_xcloner_option("xcloner_azure_api_key")
        );

        $blobRestProxy = BlobRestProxy::createBlobService($endpoint);

        $adapter = new AzureBlobStorageAdapter($blobRestProxy, $this->xcloner_settings->get_xcloner_option("xcloner_azure_container"));

        $filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        return array($adapter, $filesystem);
    }

    public function get_dropbox_filesystem()
    {
        $this->logger->info(sprintf("Creating the DROPBOX remote storage connection"), array(""));

        if (version_compare(phpversion(), '5.6.0', '<')) {
            throw new \Exception("DROPBOX requires PHP 5.6 to be installed!");
        }

        $client = new DropboxClient($this->xcloner_settings->get_xcloner_option("xcloner_dropbox_access_token"));
        $adapter = new DropboxAdapter($client, $this->xcloner_settings->get_xcloner_option("xcloner_dropbox_prefix"));

        $filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        return array($adapter, $filesystem);
    }

    public function get_aws_filesystem()
    {
        $this->logger->info(sprintf("Creating the S3 remote storage connection"), array(""));

        if (version_compare(phpversion(), '5.6.0', '<')) {
            throw new \Exception("S3 class requires PHP 5.6 to be installed!");
        }

        if (!class_exists('XmlWriter')) {
            throw new \Exception("AZURE BLOB requires libxml PHP module to be installed with XmlWriter class enabled!");
        }


        $credentials = array(
            'credentials' => array(
                'key' => $this->xcloner_settings->get_xcloner_option("xcloner_aws_key"),
                'secret' => $this->xcloner_settings->get_xcloner_option("xcloner_aws_secret")
            ),
            'region' => $this->xcloner_settings->get_xcloner_option("xcloner_aws_region"),
            'version' => 'latest',
        );

        if ($this->xcloner_settings->get_xcloner_option('xcloner_aws_endpoint') != "" && !$this->xcloner_settings->get_xcloner_option("xcloner_aws_region")) {
            $credentials['endpoint'] = $this->xcloner_settings->get_xcloner_option('xcloner_aws_endpoint');
            #$credentials['use_path_style_endpoint'] = true;
            #$credentials['bucket_endpoint'] = false;
        }

        $client = new S3Client($credentials);

        $adapter = new AwsS3Adapter($client, $this->xcloner_settings->get_xcloner_option("xcloner_aws_bucket_name"), $this->xcloner_settings->get_xcloner_option("xcloner_aws_prefix"));
        $filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        return array($adapter, $filesystem);
    }

    public function get_backblaze_filesystem()
    {
        $this->logger->info(sprintf("Creating the BACKBLAZE remote storage connection"), array(""));

        if (version_compare(phpversion(), '5.6.0', '<')) {
            throw new \Exception("BACKBLAZE API requires PHP 5.6 to be installed!");
        }


        $client = new B2Client(
            $this->xcloner_settings->get_xcloner_option("xcloner_backblaze_account_id"),
            $this->xcloner_settings->get_xcloner_option("xcloner_backblaze_application_key")
        );
        $adapter = new BackblazeAdapter($client, $this->xcloner_settings->get_xcloner_option("xcloner_backblaze_bucket_name"));

        $filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        return array($adapter, $filesystem);
    }

    public function get_webdav_filesystem()
    {
        $this->logger->info(sprintf("Creating the WEBDAV remote storage connection"), array(""));

        if (version_compare(phpversion(), '5.6.0', '<')) {
            throw new \Exception("WEBDAV API requires PHP 5.6 to be installed!");
        }

        $settings = array(
            'baseUri' => $this->xcloner_settings->get_xcloner_option("xcloner_webdav_url"),
            'userName' => $this->xcloner_settings->get_xcloner_option("xcloner_webdav_username"),
            'password' => $this->xcloner_settings->get_xcloner_option("xcloner_webdav_password"),
            //'proxy' => 'locahost:8888',
        );

        $client = new SabreClient($settings);
        $adapter = new WebDAVAdapter($client, $this->xcloner_settings->get_xcloner_option("xcloner_webdav_target_folder"));
        $filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        return array($adapter, $filesystem);
    }


    public function gdrive_construct()
    {

        //if((function_exists("is_plugin_active") && !is_plugin_active("xcloner-google-drive/xcloner-google-drive.php")) || !file_exists(__DIR__ . "/../../xcloner-google-drive/vendor/autoload.php"))
        if (!class_exists('Google_Client')) {
            return false;
        }

        //require_once(__DIR__ . "/../../xcloner-google-drive/vendor/autoload.php");

        $client = new \Google_Client();
        $client->setApplicationName($this->gdrive_app_name);
        $client->setClientId($this->xcloner_settings->get_xcloner_option("xcloner_gdrive_client_id"));
        $client->setClientSecret($this->xcloner_settings->get_xcloner_option("xcloner_gdrive_client_secret"));

        //$redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']."?page=xcloner_remote_storage_page&action=set_gdrive_code";
        $redirect_uri = "urn:ietf:wg:oauth:2.0:oob";

        $client->setRedirectUri($redirect_uri); //urn:ietf:wg:oauth:2.0:oob
        $client->addScope("https://www.googleapis.com/auth/drive");
        $client->setAccessType('offline');

        return $client;
    }

    public function get_gdrive_auth_url()
    {
        $client = $this->gdrive_construct();

        if (!$client) {
            return false;
        }

        return $authUrl = $client->createAuthUrl();
    }

    public function set_access_token($code)
    {
        $client = $this->gdrive_construct();

        if (!$client) {
            $error_msg = "Could not initialize the Google Drive Class, please check that the xcloner-google-drive plugin is enabled...";
            $this->logger->error($error_msg);

            return false;
        }

        $token = $client->fetchAccessTokenWithAuthCode($code);
        $client->setAccessToken($token);

        update_option("xcloner_gdrive_access_token", $token['access_token']);
        update_option("xcloner_gdrive_refresh_token", $token['refresh_token']);

        $redirect_url = ('http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] . "?page=xcloner_remote_storage_page#gdrive"); ?>
        <script>
            window.location = '<?php echo $redirect_url?>';
        </script>
        <?php
    }

    /*
     * php composer.phar remove nao-pon/flysystem-google-drive
     *
     */
    public function get_gdrive_filesystem()
    {
        if (version_compare(phpversion(), '5.6.0', '<')) {
            throw new \Exception("Google Drive API requires PHP 5.6 to be installed!");
        }

        $this->logger->info(sprintf("Creating the Google Drive remote storage connection"), array(""));

        $client = $this->gdrive_construct();

        if (!$client) {
            $error_msg = "Could not initialize the Google Drive Class, please check that the xcloner-google-drive plugin is enabled...";
            $this->logger->error($error_msg);
            throw new \Exception($error_msg);
        }

        $client->refreshToken($this->xcloner_settings->get_xcloner_option("xcloner_gdrive_refresh_token"));

        $service = new \Google_Service_Drive($client);

        if ($this->xcloner_settings->get_xcloner_option("xcloner_gdrive_empty_trash", 0)) {
            $this->logger->info(sprintf("Doing a Google Drive emptyTrash call"), array(""));
            $service->files->emptyTrash();
        }

        $parent = 'root';
        $dir = basename($this->xcloner_settings->get_xcloner_option("xcloner_gdrive_target_folder"));

        $folderID = $this->xcloner_settings->get_xcloner_option("xcloner_gdrive_target_folder");

        $tmp = parse_url($folderID);

        if (isset($tmp['query'])) {
            $folderID = str_replace("id=", "", $tmp['query']);
        }

        if (stristr($folderID, "/")) {
            $query = sprintf(
                'mimeType = \'application/vnd.google-apps.folder\' and \'%s\' in parents and name contains \'%s\'',
                $parent,
                $dir
            );
            $response = $service->files->listFiles([
                'pageSize' => 1,
                'q' => $query
            ]);

            if (sizeof($response)) {
                foreach ($response as $obj) {
                    $folderID = $obj->getId();
                }
            } else {
                $this->xcloner->trigger_message(sprintf(__(
                    "Could not find folder ID by name %s",
                    'xcloner-backup-and-restore'
                ), $folderID), "error");
            }
        }

        $this->logger->info(sprintf("Using target folder with ID %s on the remote storage", $folderID));

        if (class_exists('XCloner_Google_Drive_Adapter')) {
            $adapter = new XCloner_Google_Drive_Adapter($service, $folderID);
        } else {
            $adapter = new \Hypweb\Flysystem\GoogleDrive\GoogleDriveAdapter($service, $folderID);
        }

        $filesystem = new \League\Flysystem\Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));


        return array($adapter, $filesystem);
    }

    public function get_ftp_filesystem()
    {
        $this->logger->info(sprintf("Creating the FTP remote storage connection"), array(""));

        $adapter = new Adapter([
            'host' => $this->xcloner_settings->get_xcloner_option("xcloner_ftp_hostname"),
            'username' => $this->xcloner_settings->get_xcloner_option("xcloner_ftp_username"),
            'password' => $this->xcloner_settings->get_xcloner_option("xcloner_ftp_password"),

            /** optional config settings */
            'port' => $this->xcloner_settings->get_xcloner_option("xcloner_ftp_port", 21),
            'root' => $this->xcloner_settings->get_xcloner_option("xcloner_ftp_path"),
            'passive' => $this->xcloner_settings->get_xcloner_option("xcloner_ftp_transfer_mode"),
            'ssl' => $this->xcloner_settings->get_xcloner_option("xcloner_ftp_ssl_mode"),
            'timeout' => $this->xcloner_settings->get_xcloner_option("xcloner_ftp_timeout", 30),
        ]);

        $adapter->connect();

        $filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        return array($adapter, $filesystem);
    }

    public function get_sftp_filesystem()
    {
        $this->logger->info(sprintf("Creating the SFTP remote storage connection"), array(""));

        $adapter = new SftpAdapter([
            'host' => $this->xcloner_settings->get_xcloner_option("xcloner_sftp_hostname"),
            'username' => $this->xcloner_settings->get_xcloner_option("xcloner_sftp_username"),
            'password' => $this->xcloner_settings->get_xcloner_option("xcloner_sftp_password"),

            /** optional config settings */
            'port' => $this->xcloner_settings->get_xcloner_option("xcloner_sftp_port", 22),
            'root' => ($this->xcloner_settings->get_xcloner_option("xcloner_sftp_path")?$this->xcloner_settings->get_xcloner_option("xcloner_sftp_path"):'./'),
            'privateKey' => $this->xcloner_settings->get_xcloner_option("xcloner_sftp_private_key"),
            'timeout' => $this->xcloner_settings->get_xcloner_option("xcloner_sftp_timeout", 30),
            'directoryPerm' => 0755
        ]);

        $filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        return array($adapter, $filesystem);
    }

    public function change_storage_status($field, $value)
    {
        $field = $this->xcloner_sanitization->sanitize_input_as_string($field);
        $value = $this->xcloner_sanitization->sanitize_input_as_int($value);

        return update_option($field, $value);
    }

    public function get_aws_regions()
    {
        return $this->aws_regions;
    }
}
<?php
namespace watchfulli\XClonerCore;

class Xcloner_Settings
{
    private $logger_file = "xcloner_main_%s.log";
    private $logger_file_hash = "xcloner%s.log";
    private $hash;
    private $xcloner_sanitization;
    private $xcloner_container;
    private $xcloner_database;
    private $xcloner_options_prefix = "xcloner";

    /**
     * XCloner General Settings Class
     *
     * @param Xcloner $xcloner_container
     * @param string $hash
     */
    public function __construct(Xcloner $xcloner_container, $hash = "", $json_config = "")
    {
        if ($json_config) {
            foreach ($json_config as $item) {
                add_option($item->option_name, $item->option_value);
            }
        }

        $this->xcloner_container = $xcloner_container;

        $this->xcloner_database  = $xcloner_container->get_xcloner_database();

        if (isset($hash)) {
            $this->set_hash($hash);
        }
    }

    /**
     * XCloner options prefix
     *
     * @return string
     */
    public function get_options_prefix() {
        return $this->xcloner_options_prefix;
    }

    /**
     * Get XCloner Main Container
     *
     * @return void
     */
    private function get_xcloner_container()
    {
        return $this->xcloner_container;
    }

    /**
     * Get Logger Filename Setting
     *
     * @param integer $include_hash
     * @return void
     */
    public function get_logger_filename($include_hash = 0)
    {
        if ($include_hash) {
            $filename = sprintf($this->logger_file_hash, $this->get_hash());
        } else {
            $filename = sprintf($this->logger_file, $this->get_server_unique_hash(5));
        }

        return $filename;
    }

    /**
     * Get XCloner Backup Start Path Setting
     *
     * @return void
     */
    public function get_xcloner_start_path()
    {
        if (!$this->get_xcloner_option('xcloner_start_path') or !is_dir(/** @scrutinizer ignore-type */$this->get_xcloner_option('xcloner_start_path'))) {
            $path = realpath(ABSPATH);
        } else {
            $path = $this->get_xcloner_option('xcloner_start_path');
        }

        return $path;
    }

    /**
     * Get XCloner Start Path Setting , function is in legacy mode
     *
     * @param [type] $dir
     * @return void
     */
    public function get_xcloner_dir_path($dir)
    {
        $path = $this->get_xcloner_start_path().DS.$dir;

        return $path;
    }

    /**
     * Get XCloner Backup Store Path Setting
     *
     * @return void
     */
    public function get_xcloner_store_path()
    {
        if (!$this->get_xcloner_option('xcloner_store_path') or !is_dir(/** @scrutinizer ignore-type */$this->get_xcloner_option('xcloner_store_path'))) {
            return $this->xcloner_container->check_dependencies();
        } else {
            $path = $this->get_xcloner_option('xcloner_store_path');
        }

        return $path;
    }

    /**
     * Get XCloner Encryption Key
     *
     * @return void
     */
    public function get_xcloner_encryption_key()
    {
        if (!$key = $this->get_xcloner_option('xcloner_encryption_key')) {
            $key = $this->xcloner_container->randomString(35);
            update_option('xcloner_encryption_key', $key);
        }

        return $key;
    }

    /**
     * Create a random string
     * @author	XEWeb <>
     * @param $length the length of the string to create
     * @return string
     */
    /*public function randomString($length = 6) {
        $str = "";
        $characters = array_merge(range('A', 'Z'), range('a', 'z'), range('0', '9'));
        $max = count($characters) - 1;
        for ($i = 0; $i < $length; $i++) {
            $rand = mt_rand(0, $max);
            $str .= $characters[$rand];
        }
        return $str;
    }*/

    public function get_xcloner_tmp_path_suffix()
    {
        return "xcloner".$this->get_hash();
    }

    /**
     * Get XCloner Temporary Path
     *
     * @param boolean $suffix
     * @return void
     */
    public function get_xcloner_tmp_path($suffix = true)
    {
        if ($this->get_xcloner_option('xcloner_force_tmp_path_site_root')) {
            $path = $this->get_xcloner_store_path();
        } else {
            $path = sys_get_temp_dir();
            if (!is_dir($path)) {
                try {
                    mkdir($path);
                    chmod($path, 0777);
                } catch (Exception $e) {
                    //silent catch
                }
            }

            if (!is_dir($path) or !is_writeable($path)) {
                $path = $this->get_xcloner_store_path();
            }
        }

        if ($suffix) {
            $path = $path.DS.".".$this->get_xcloner_tmp_path_suffix();
        }

        return $path;
    }

    /**
     * Get Enable Mysql Backup Option
     *
     * @return void
     */
    public function get_enable_mysql_backup()
    {
        if ($this->get_xcloner_option('xcloner_enable_mysql_backup')) {
            return true;
        }

        return false;
    }

    /**
     * Get Backup Extension Name
     *
     * @param string $ext
     * @return string ( hash.(tar|tgz) )
     */
    public function get_backup_extension_name($ext = "")
    {
        if (!$ext) {
            if ($this->get_xcloner_option('xcloner_backup_compression_level')) {
                $ext = ".tgz";
            } else {
                $ext = ".tar";
            }
        }

        return ($this->get_hash()).$ext;
    }

    /**
     * Get Backup Hash
     *
     * @return void
     */
    public function get_hash($readonly = false)
    {
        if (!$this->hash && !$readonly) {
            $this->set_hash("-".$this->get_server_unique_hash(5));
        }

        //echo $this->hash;
        return $this->hash;
    }

    /**
     * Generate New Hash
     *
     * @return void
     */
    public function generate_new_hash()
    {
        $hash = "-".md5(rand());

        $hash = substr($hash, 0, 6);

        $this->set_hash($hash);

        return $hash;
    }

    /**
     * Set New Hash
     *
     * @param string $hash
     * @return void
     */
    public function set_hash($hash = "")
    {
        if (substr($hash, 0, 1) != "-" and strlen($hash)) {
            $hash = "-".$hash;
        }

        $this->hash = substr($hash, 0, 6);

        return $this;
    }

    /**
     * Get Default Backup Name
     *
     * @return void
     */
    public function get_default_backup_name()
    {
        $data = parse_url(get_site_url());

        $backup_name = "backup_[domain]".(isset($data['port']) ? "_".$data['port'] : "")."-[time]-".($this->get_enable_mysql_backup() ? "sql" : "nosql");

        return $backup_name;
    }

    /**
     * Get Database Hostname
     *
     * @return void
     */
    public function get_db_hostname()
    {
        if (!$data = $this->get_xcloner_option('xcloner_mysql_hostname')) {
            // $data = $this->xcloner_database->getDbHost();
        }

        return $data;
    }
    
    /**
     * Get Database Username
     *
     * @return void
     */
    public function get_db_username()
    {
        if (!$data = $this->get_xcloner_option('xcloner_mysql_username')) {
            //$data = $this->xcloner_database->getDbUser();
        }

        return $data;
    }

    /**
     * Get Database Password
     *
     * @return void
     */
    public function get_db_password()
    {
        if (!$data = $this->get_xcloner_option('xcloner_mysql_password')) {
            //$data = $this->xcloner_database->getDbPassword();
        }

        return $data;
    }

    /**
     * Get Database Name
     *
     * @return void
     */
    public function get_db_database()
    {
        if (!$data = $this->get_xcloner_option('xcloner_mysql_database')) {
            //$data = $this->xcloner_database->getDbName();
        }

        return $data;
    }

    /**
     * Get Database Tables Prefix
     *
     * @return void
     */
    public function get_table_prefix()
    {
        return $this->get_xcloner_option('xcloner_mysql_prefix');
    }

    /**
     * @param string $option
     */
    public function get_xcloner_option($option = "")
    {
        $data = get_option($option);

        return $data;
    }

    /**
     * Get Server Unique Hash used to generate the unique backup name
     *
     * @param integer $strlen
     * @return void
     */
    public function get_server_unique_hash($strlen = 0)
    {
        $hash = md5(get_home_url().__DIR__.$this->get_xcloner_encryption_key());

        if ($strlen) {
            $hash = substr($hash, 0, $strlen);
        }

        return $hash;
    }

    public function settings_init()
    {
        $this->xcloner_sanitization = $this->get_xcloner_container()->get_xcloner_sanitization();

        //ADDING MISSING OPTIONS
        if (false == $this->get_xcloner_option('xcloner_mysql_settings_page')) {
            update_option('xcloner_mysql_settings_page', '');
        } // end if

        if (false == $this->get_xcloner_option('xcloner_cron_settings_page')) {
            update_option('xcloner_cron_settings_page', '');
        } // end if

        if (false == $this->get_xcloner_option('xcloner_system_settings_page')) {
            update_option('xcloner_system_settings_page', '');
        } // end if

        if (false == $this->get_xcloner_option('xcloner_cleanup_settings_page')) {
            update_option('xcloner_cleanup_settings_page', '');
        } // end if


        //ADDING SETTING SECTIONS
        //GENERAL section
        add_settings_section(
            'xcloner_general_settings_group',
            __(' '),
            array($this, 'xcloner_settings_section_cb'),
            'xcloner_settings_page'
        );
        //MYSQL section
        add_settings_section(
            'xcloner_mysql_settings_group',
            __(' '),
            array($this, 'xcloner_settings_section_cb'),
            'xcloner_mysql_settings_page'
        );

        //SYSTEM section
        add_settings_section(
            'xcloner_system_settings_group',
            __('These are advanced options recommended for developers!', 'xcloner-backup-and-restore'),
            array($this, 'xcloner_settings_section_cb'),
            'xcloner_system_settings_page'
        );

        //CLEANUP section
        add_settings_section(
            'xcloner_cleanup_settings_group',
            __(' '),
            array($this, 'xcloner_settings_section_cb'),
            'xcloner_cleanup_settings_page'
        );


        //CRON section
        add_settings_section(
            'xcloner_cron_settings_group',
            __(' '),
            array($this, 'xcloner_settings_section_cb'),
            'xcloner_cron_settings_page'
        );


        //REGISTERING THE 'GENERAL SECTION' FIELDS
        register_setting('xcloner_general_settings_group', 'xcloner_backup_compression_level', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_backup_compression_level',
            __('Backup Compression Level', 'xcloner-backup-and-restore'),
            array($this, 'do_form_range_field'),
            'xcloner_settings_page',
            'xcloner_general_settings_group',
            array(
                'xcloner_backup_compression_level',
                __('Options between [0-9]. Value 0 means no compression, while 9 is maximum compression affecting cpu load', 'xcloner-backup-and-restore'),
                0,
                9
            )
        );

        register_setting('xcloner_general_settings_group', 'xcloner_start_path', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_absolute_path"
        ));
        add_settings_field(
            'xcloner_start_path',
            __('Backup Start Location', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_settings_page',
            'xcloner_general_settings_group',
            array(
                'xcloner_start_path',
                __('Base path location from where XCloner can start the Backup.', 'xcloner-backup-and-restore'),
                $this->get_xcloner_start_path(),
                //'disabled'
            )
        );

        register_setting('xcloner_general_settings_group', 'xcloner_store_path', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_absolute_path"
        ));
        add_settings_field(
            'xcloner_store_path',
            __('Backup Storage Location', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_settings_page',
            'xcloner_general_settings_group',
            array(
                'xcloner_store_path',
                __('Location where XCloner will store the Backup archives.', 'xcloner-backup-and-restore'),
                $this->get_xcloner_store_path(),
                //'disabled'
            )
        );

        register_setting('xcloner_general_settings_group', 'xcloner_encryption_key', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_string"
        ));
        add_settings_field(
            'xcloner_encryption_key',
            __('Backup Encryption Key', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_settings_page',
            'xcloner_general_settings_group',
            array(
                'xcloner_encryption_key',
                __('Backup Encryption Key used to Encrypt/Decrypt backups, you might want to save this somewhere else as well.', 'xcloner-backup-and-restore'),
                $this->get_xcloner_encryption_key(),
                //'disabled'
            )
        );

        register_setting('xcloner_general_settings_group', 'xcloner_enable_log', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_enable_log',
            __('Enable XCloner Backup Log', 'xcloner-backup-and-restore'),
            array($this, 'do_form_switch_field'),
            'xcloner_settings_page',
            'xcloner_general_settings_group',
            array(
                'xcloner_enable_log',
                sprintf(__('Enable the XCloner Backup log. You will find it stored unde the Backup Storage Location, file %s', 'xcloner-backup-and-restore'), $this->get_logger_filename())
            )
        );

        register_setting('xcloner_general_settings_group', 'xcloner_enable_pre_update_backup', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_enable_pre_update_backup',
            __('Generate Backups before Automatic WP Upgrades', 'xcloner-backup-and-restore'),
            array($this, 'do_form_switch_field'),
            'xcloner_settings_page',
            'xcloner_general_settings_group',
            array(
                'xcloner_enable_pre_update_backup',
                sprintf(__('Attempt to generate a full site backup before applying automatic core updates.', 'xcloner-backup-and-restore'), $this->get_logger_filename())
            )
        );

        register_setting('xcloner_general_settings_group', 'xcloner_regex_exclude', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_raw"
        ));
        add_settings_field(
            'xcloner_regex_exclude',
            __('Regex Exclude Files', 'xcloner-backup-and-restore'),
            array($this, 'do_form_textarea_field'),
            'xcloner_settings_page',
            'xcloner_general_settings_group',
            array(
                'xcloner_regex_exclude',
                __('Regular expression match to exclude files and folders, example patterns provided below, one pattern per line', 'xcloner-backup-and-restore'),
                //$this->get_xcloner_store_path(),
                //'disabled'
            )
        );

        //REGISTERING THE 'MYSQL SECTION' FIELDS
        register_setting('xcloner_mysql_settings_group', 'xcloner_enable_mysql_backup', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_enable_mysql_backup',
            __('Enable Mysql Backup', 'xcloner-backup-and-restore'),
            array($this, 'do_form_switch_field'),
            'xcloner_mysql_settings_page',
            'xcloner_mysql_settings_group',
            array(
                'xcloner_enable_mysql_backup',
                __('Enable Mysql Backup Option. If you don\'t want to backup the database, you can disable this.', 'xcloner-backup-and-restore')
            )
        );

        register_setting('xcloner_mysql_settings_group', 'xcloner_backup_only_wp_tables');
        add_settings_field(
            'xcloner_backup_only_wp_tables',
            __('Backup only WP tables', 'xcloner-backup-and-restore'),
            array($this, 'do_form_switch_field'),
            'xcloner_mysql_settings_page',
            'xcloner_mysql_settings_group',
            array(
                'xcloner_backup_only_wp_tables',
                sprintf(__('Enable this if you only want to Backup only tables starting with \'%s\' prefix', 'xcloner-backup-and-restore'), $this->get_table_prefix())
            )
        );

        register_setting('xcloner_mysql_settings_group', 'xcloner_mysql_hostname', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_raw"
        ));
        add_settings_field(
            'xcloner_mysql_hostname',
            __('Mysql Hostname', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_mysql_settings_page',
            'xcloner_mysql_settings_group',
            array(
                'xcloner_mysql_hostname',
                __('Wordpress mysql hostname', 'xcloner-backup-and-restore'),
                $this->get_db_hostname(),
                //'disabled'
            )
        );

        register_setting('xcloner_mysql_settings_group', 'xcloner_mysql_username', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_raw"
        ));
        add_settings_field(
            'xcloner_mysql_username',
            __('Mysql Username', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_mysql_settings_page',
            'xcloner_mysql_settings_group',
            array(
                'xcloner_mysql_username',
                __('Wordpress mysql username', 'xcloner-backup-and-restore'),
                $this->get_db_username(),
                //'disabled'
            )
        );
        
        register_setting('xcloner_mysql_settings_group', 'xcloner_mysql_password', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_raw"
        ));
        add_settings_field(
            'xcloner_mysql_password',
            __('Mysql Password', 'xcloner-backup-and-restore'),
            array($this, 'do_form_password_field'),
            'xcloner_mysql_settings_page',
            'xcloner_mysql_settings_group',
            array(
                'xcloner_mysql_password',
                __('Wordpress mysql password', 'xcloner-backup-and-restore'),
                $this->get_db_username(),
                //'disabled'
            )
        );

        register_setting('xcloner_mysql_settings_group', 'xcloner_mysql_database', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_raw"
        ));
        add_settings_field(
            'xcloner_mysql_database',
            __('Mysql Database', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_mysql_settings_page',
            'xcloner_mysql_settings_group',
            array(
                'xcloner_mysql_database',
                __('Wordpress mysql database', 'xcloner-backup-and-restore'),
                $this->get_db_database(),
                //'disabled'
            )
        );
        
        register_setting('xcloner_mysql_settings_group', 'xcloner_mysql_prefix', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_raw"
        ));
        add_settings_field(
            'xcloner_mysql_prefix',
            __('Mysql Tables Prefix', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_mysql_settings_page',
            'xcloner_mysql_settings_group',
            array(
                'xcloner_mysql_prefix',
                __('Wordpress mysql tables prefix', 'xcloner-backup-and-restore'),
                $this->get_table_prefix(),
                //'disabled'
            )
        );

        //REGISTERING THE 'SYSTEM SECTION' FIELDS
        register_setting('xcloner_system_settings_group', 'xcloner_size_limit_per_request', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_size_limit_per_request',
            __('Data Size Limit Per Request', 'xcloner-backup-and-restore'),
            array($this, 'do_form_range_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array(
                'xcloner_size_limit_per_request',
                __('Use this option to set how much file data can XCloner backup in one AJAX request. Range 0-1024 MB', 'xcloner-backup-and-restore'),
                0,
                1024
            )
        );

        register_setting('xcloner_system_settings_group', 'xcloner_files_to_process_per_request', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_files_to_process_per_request',
            __('Files To Process Per Request', 'xcloner-backup-and-restore'),
            array($this, 'do_form_range_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array(
                'xcloner_files_to_process_per_request',
                __('Use this option to set how many files XCloner should process at one time before doing another AJAX call', 'xcloner-backup-and-restore'),
                0,
                1000
            )
        );

        register_setting('xcloner_system_settings_group', 'xcloner_directories_to_scan_per_request', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_directories_to_scan_per_request',
            __('Directories To Scan Per Request', 'xcloner-backup-and-restore'),
            array($this, 'do_form_range_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array(
                'xcloner_directories_to_scan_per_request',
                __('Use this option to set how many directories XCloner should scan at one time before doing another AJAX call', 'xcloner-backup-and-restore'),
                0,
                1000
            )
        );

        register_setting('xcloner_system_settings_group', 'xcloner_database_records_per_request', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_database_records_per_request',
            __('Database Records Per Request', 'xcloner-backup-and-restore'),
            array($this, 'do_form_range_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array(
                'xcloner_database_records_per_request',
                __('Use this option to set how many database table records should be fetched per AJAX request, or set to 0 to fetch all.  Range 0-100000 records', 'xcloner-backup-and-restore'),
                0,
                100000
            )
        );

        /*register_setting('xcloner_system_settings_group', 'xcloner_diff_backup_recreate_period', array($this->xcloner_sanitization, "sanitize_input_as_int"));
        add_settings_field(
            'xcloner_diff_backup_recreate_period',
           __('Differetial Backups Max Days','xcloner-backup-and-restore'),
            array($this, 'do_form_number_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array('xcloner_diff_backup_recreate_period',
             __('Use this option to set when a full backup should be recreated if the scheduled backup type is set to \'Full Backup+Differential Backups\' ','xcloner-backup-and-restore'),
             )
        );*/

        register_setting('xcloner_system_settings_group', 'xcloner_exclude_files_larger_than_mb', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_exclude_files_larger_than_mb',
            __('Exclude files larger than (MB)', 'xcloner-backup-and-restore'),
            array($this, 'do_form_number_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array(
                'xcloner_exclude_files_larger_than_mb',
                __('Use this option to automatically exclude files larger than a certain size in MB, or set to 0 to include all. Range 0-1000 MB', 'xcloner-backup-and-restore'),
            )
        );

        register_setting('xcloner_system_settings_group', 'xcloner_split_backup_limit', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_split_backup_limit',
            __('Split Backup Archive Limit (MB)', 'xcloner-backup-and-restore'),
            array($this, 'do_form_number_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array(
                'xcloner_split_backup_limit',
                __('Use this option to automatically split the backup archive into smaller parts. Range  0-10000 MB', 'xcloner-backup-and-restore'),
            )
        );

        register_setting('xcloner_system_settings_group', 'xcloner_force_tmp_path_site_root');
        add_settings_field(
            'xcloner_force_tmp_path_site_root',
            __('Force Temporary Path Within XCloner Storage', 'xcloner-backup-and-restore'),
            array($this, 'do_form_switch_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array(
                'xcloner_force_tmp_path_site_root',
                sprintf(__('Enable this option if you want the XCloner Temporary Path to be within your XCloner Storage Location', 'xcloner-backup-and-restore'), $this->get_table_prefix())
            )
        );

        register_setting('xcloner_system_settings_group', 'xcloner_disable_email_notification');
        add_settings_field(
            'xcloner_disable_email_notification',
            __('Disable Email Notifications', 'xcloner-backup-and-restore'),
            array($this, 'do_form_switch_field'),
            'xcloner_system_settings_page',
            'xcloner_system_settings_group',
            array(
                'xcloner_disable_email_notification',
                sprintf(__('Enable this option if you want the XCloner to NOT send email notifications on successful backups', 'xcloner-backup-and-restore'), $this->get_table_prefix())
            )
        );

        //REGISTERING THE 'CLEANUP SECTION' FIELDS
        register_setting('xcloner_cleanup_settings_group', 'xcloner_cleanup_retention_limit_days', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_cleanup_retention_limit_days',
            __('Cleanup by Age (days)', 'xcloner-backup-and-restore'),
            array($this, 'do_form_number_field'),
            'xcloner_cleanup_settings_page',
            'xcloner_cleanup_settings_group',
            array(
                'xcloner_cleanup_retention_limit_days',
                __('Specify the maximum number of days a backup archive can be kept on the server. 0 disables this option', 'xcloner-backup-and-restore')
            )
        );

        register_setting('xcloner_cleanup_settings_group', 'xcloner_cleanup_retention_limit_archives', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_cleanup_retention_limit_archives',
            __('Cleanup by Quantity', 'xcloner-backup-and-restore'),
            array($this, 'do_form_number_field'),
            'xcloner_cleanup_settings_page',
            'xcloner_cleanup_settings_group',
            array(
                'xcloner_cleanup_retention_limit_archives',
                __('Specify the maximum number of backup archives to keep on the server. 0 disables this option', 'xcloner-backup-and-restore')
            )
        );

        register_setting('xcloner_cleanup_settings_group', 'xcloner_cleanup_exclude_days', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_string"
        ));
        add_settings_field(
            'xcloner_cleanup_exclude_days',
            __('Keep Backups Taken On Month Days', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_cleanup_settings_page',
            'xcloner_cleanup_settings_group',
            array(
                'xcloner_cleanup_exclude_days',
                __('Never delete backups taken on the specified days of the month, value used can be comma separated.', 'xcloner-backup-and-restore')
            )
        );

        register_setting('xcloner_cleanup_settings_group', 'xcloner_cleanup_capacity_limit', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_cleanup_capacity_limit',
            __('Cleanup by Capacity(MB)', 'xcloner-backup-and-restore'),
            array($this, 'do_form_number_field'),
            'xcloner_cleanup_settings_page',
            'xcloner_cleanup_settings_group',
            array(
                'xcloner_cleanup_capacity_limit',
                __('Remove oldest backups if all created backups exceed the configured limit in Megabytes. 0 disables this option', 'xcloner-backup-and-restore')
            )
        );

        /*
        //deprecated setting
        register_setting('xcloner_cleanup_settings_group', 'xcloner_cleanup_delete_after_remote_transfer', array(
            $this->xcloner_sanitization,
            "sanitize_input_as_int"
        ));
        add_settings_field(
            'xcloner_cleanup_delete_after_remote_transfer',
            __('Delete Backup After Remote Storage Transfer', 'xcloner-backup-and-restore'),
            array($this, 'do_form_switch_field'),
            'xcloner_cleanup_settings_page',
            'xcloner_cleanup_settings_group',
            array(
                'xcloner_cleanup_delete_after_remote_transfer',
                __('Remove backup created automatically from local storage after sending the backup to Remote Storage', 'xcloner-backup-and-restore')
            )
        );
        */

        //REGISTERING THE 'CRON SECTION' FIELDS
        register_setting('xcloner_cron_settings_group', 'xcloner_cron_frequency');
        add_settings_field(
            'xcloner_cron_frequency',
            __('Cron frequency', 'xcloner-backup-and-restore'),
            array($this, 'do_form_text_field'),
            'xcloner_cron_settings_page',
            'xcloner_cron_settings_group',
            array(
                'xcloner_cron_frequency',
                __('Cron frequency')
            )
        );
    }




    /**
     * callback functions
     */

    // section content cb
    public function xcloner_settings_section_cb()
    {
        //echo '<p>WPOrg Section Introduction.</p>';
    }

    // text field content cb
    public function do_form_text_field($params)
    {
        if (!isset($params['3'])) {
            $params[3] = 0;
        }
        if (!isset($params['2'])) {
            $params[2] = 0;
        }

        list($fieldname, $label, $value, $disabled) = $params;

        if (!$value) {
            $value = $this->get_xcloner_option($fieldname);
        }
        // output the field?>
        <div class="row">
            <?php if ($params[0] !== 'xcloner_cleanup_exclude_days'):?>
            <div class="input-field col s10 m10 l8">
            <?php else:?>
            <div class="input-field col s10 m5 l3">
            <?php endif?>
                <input class="validate" <?php echo ($disabled) ? "disabled" : "" ?> name="<?php echo $fieldname ?>"
                       id="<?php echo $fieldname ?>" type="text" class="validate"
                       value="<?php echo isset($value) ? esc_attr($value) : ''; ?>">
            </div>
            <div class="col s2 m2 ">
                <a class="btn-floating tooltipped btn-small" data-position="center" data-delay="50"
                   data-tooltip="<?php echo $label ?>" data-tooltip-id=""><i class="material-icons">help_outline</i></a>
            </div>
        </div>


		<?php
    }
    
    /**
     * Password field UI
     *
     * @param [type] $params
     * @return void
     */
    public function do_form_password_field($params)
    {
        if (!isset($params['3'])) {
            $params[3] = 0;
        }
        if (!isset($params['2'])) {
            $params[2] = 0;
        }

        list($fieldname, $label, $value, $disabled) = $params;

        if (!$value) {
            $value = $this->get_xcloner_option($fieldname);
        }
        // output the field?>
        <div class="row">
            <div class="input-field col s10 m10 l8">
                <input class="validate" <?php echo ($disabled) ? "disabled" : "" ?> name="<?php echo $fieldname ?>"
                       id="<?php echo $fieldname ?>" type="password" class="validate"
                       value="<?php echo isset($value) ? esc_attr($value) : ''; ?>">
            </div>
            <div class="col s2 m2 ">
                <a class="btn-floating tooltipped btn-small" data-position="left" data-delay="50"
                   data-tooltip="<?php echo $label ?>" data-tooltip-id=""><i class="material-icons">help_outline</i></a>
            </div>
        </div>


		<?php
    }

    // textarea field content cb
    public function do_form_textarea_field($params)
    {
        if (!isset($params['3'])) {
            $params[3] = 0;
        }
        if (!isset($params['2'])) {
            $params[2] = 0;
        }

        list($fieldname, $label, $value, $disabled) = $params;

        if (!$value) {
            $value = $this->get_xcloner_option($fieldname);
        }
        // output the field?>
        <div class="row">
            <div class="input-field col s10 m10 l8">
                <textarea class="validate" <?php echo ($disabled) ? "disabled" : "" ?> name="<?php echo $fieldname ?>"
                          id="<?php echo $fieldname ?>" type="text" class="validate"
                          value=""><?php echo isset($value) ? esc_attr($value) : ''; ?></textarea>
            </div>
            <div class="col s2 m2 ">
                <a class="btn-floating tooltipped btn-small" data-position="center" data-html="true" data-delay="50"
                   data-tooltip="<?php echo $label ?>" data-tooltip-id=""><i class="material-icons">help_outline</i></a>
            </div>
            <div class="col s12">
                <ul class="xcloner_regex_exclude_limit">
                    <li>Exclude all except .php file: <span
                                class="regex_pattern"><?php echo htmlentities('(.*)\.(.+)$(?<!(php))') ?></span></li>
                    <li>Exclude all except .php and .txt: <span
                                class="regex_pattern"> <?php echo htmlentities('(.*)\.(.+)$(?<!(php|txt))') ?></span>
                    </li>
                    <li>Exclude all .avi files: <span
                                class="regex_pattern"> <?php echo htmlentities('(.*)\.(.+)$(?<=(avi))') ?></span></li>
                    <li>Exclude all .jpg,.gif and .png files: <span
                                class="regex_pattern"> <?php echo htmlentities('(.*)\.(.+)$(?<=(gif|png|jpg))') ?></span>
                    </li>
                    <li>Exclude all .svn and .git: <span
                                class="regex_pattern"> <?php echo htmlentities('(.*)\.(svn|git)(.*)$') ?></span></li>
                    <li>Exclude root directory /test: <span
                                class="regex_pattern"> <?php echo htmlentities('\/test(.*)$') ?></span> or <span
                                class="regex_pattern"> <?php echo htmlentities('test(.*)$') ?></span></li>
                    <li>Exclude the wp-admin folder: <span
                                class="regex_pattern"> <?php echo htmlentities('(\/wp-admin)(.*)$') ?></span></li>
                    <li>Exclude the wp-content/uploads folder: <span
                                class="regex_pattern"> <?php echo htmlentities('(\/wp-content\/uploads)(.*)$') ?></span>
                    </li>
                    <li>Exclude the wp-admin, wp-includes and wp-config.php: <span
                                class="regex_pattern"> <?php echo htmlentities('\/(wp-admin|wp-includes|wp-config.php)(.*)$') ?></span>
                    </li>
                    <li>Exclude wp-content/updraft and wp/content/uploads/wp_all_backup folder :<span
                                class="regex_pattern">\/(wp-content\/updraft|\/wp-content\/uploads\/wp_all_backup)(.*)$</span>
                    </li>
                    <li>Exclude all cache folders from wp-content/ and it's subdirectories: <span
                                class="regex_pattern"> <?php echo htmlentities('\/wp-content(.*)\/cache($|\/)(.*)') ?></span>
                    </li>
                    <li>Exclude wp-content/cache/ folder: <span
                                class="regex_pattern"> <?php echo htmlentities('\/wp-content\/cache(.*)') ?></span>
                    </li>
                    <li>Exclude all error_log files: <span
                                class="regex_pattern"> <?php echo htmlentities('(.*)error_log$') ?></span></li>
                </ul>
            </div>
        </div>


		<?php
    }

    // number field content cb
    public function do_form_number_field($params)
    {
        if (!isset($params['3'])) {
            $params[3] = 0;
        }
        if (!isset($params['2'])) {
            $params[2] = 0;
        }

        list($fieldname, $label, $value, $disabled) = $params;

        if (!$value) {
            $value = $this->get_xcloner_option($fieldname);
        }
        // output the field?>
        <div class="row">
            <div class="input-field col s10 m5 l3">
                <input class="validate" <?php echo ($disabled) ? "disabled" : "" ?> name="<?php echo $fieldname ?>"
                       id="<?php echo $fieldname ?>" type="number" class="validate"
                       value="<?php echo isset($value) ? esc_attr($value) : ''; ?>">
            </div>
            <div class="col s2 m2 ">
                <a class="btn-floating tooltipped btn-small" data-html="true" data-position="center" data-delay="50"
                   data-tooltip="<?php echo $label ?>" data-tooltip-id=""><i class="material-icons">help_outline</i></a>
            </div>
        </div>


		<?php
    }

    public function do_form_range_field($params)
    {
        if (!isset($params['4'])) {
            $params[4] = 0;
        }

        list($fieldname, $label, $range_start, $range_end, $disabled) = $params;
        $value = $this->get_xcloner_option($fieldname); ?>
        <div class="row">
            <div class="input-field col s10 m10 l8">
                <p class="range-field">
                    <input <?php echo ($disabled) ? "disabled" : "" ?> type="range" name="<?php echo $fieldname ?>"
                                                                         id="<?php echo $fieldname ?>"
                                                                         min="<?php echo $range_start ?>"
                                                                         max="<?php echo $range_end ?>"
                                                                         value="<?php echo isset($value) ? esc_attr($value) : ''; ?>"/>
                </p>
            </div>
            <div class="col s2 m2 ">
                <a class="btn-floating tooltipped btn-small" data-html="true" data-position="center" data-delay="50"
                   data-tooltip="<?php echo $label ?>" data-tooltip-id=""><i class="material-icons">help_outline</i></a>
            </div>
        </div>
		<?php
    }


    public function do_form_switch_field($params)
    {
        if (!isset($params['2'])) {
            $params[2] = 0;
        }
        list($fieldname, $label, $disabled) = $params;
        $value = $this->get_xcloner_option($fieldname); ?>
        <div class="row">
            <div class="input-field col s10 m5 l3">
                <div class="switch">
                    <label>
                        Off
                        <input <?php echo ($disabled) ? "disabled" : "" ?> type="checkbox"
                                                                             name="<?php echo $fieldname ?>"
                                                                             id="<?php echo $fieldname ?>"
                                                                             value="1" <?php echo ($value) ? 'checked="checked"' : ''; ?>
                        ">
                        <span class="lever"></span>
                        On
                    </label>
                </div>
            </div>
            <div class="col s2 m2">
                <a class="btn-floating tooltipped btn-small" data-position="center" data-delay="50"
                   data-tooltip="<?php echo $label ?>" data-tooltip-id=""><i class="material-icons">help_outline</i></a>
            </div>
        </div>
		<?php
    }
}
<?php
namespace watchfulli\XClonerCore;

/**
 * XCloner - Backup and Restore backup plugin for Wordpress
 *
 * class-xcloner-archive.php
 * @author Liuta Ovidiu <info@thinkovi.com>
 *
 *        This program is free software; you can redistribute it and/or modify
 *        it under the terms of the GNU General Public License as published by
 *        the Free Software Foundation; either version 2 of the License, or
 *        (at your option) any later version.
 *
 *        This program is distributed in the hope that it will be useful,
 *        but WITHOUT ANY WARRANTY; without even the implied warranty of
 *        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *        GNU General Public License for more details.
 *
 *        You should have received a copy of the GNU General Public License
 *        along with this program; if not, write to the Free Software
 *        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *        MA 02110-1301, USA.
 *
 * @link https://github.com/ovidiul/XCloner-Wordpress
 *
 * @modified 7/31/18 3:10 PM
 *
 */

use splitbrain\PHPArchive\Tar;
use splitbrain\PHPArchive\Archive;
use splitbrain\PHPArchive\FileInfo;

/**
 * Class responsible for adding files to Tar
 * Class Xcloner_Archive
 */
class Xcloner_Archive extends Tar
{
    /**
     * Process file size per API request
     * @var float|int
     */
    private $file_size_per_request_limit = 52428800; //50MB = 52428800; 1MB = 1048576
    /**
     * Files count to process per API request
     * @var int
     */
    private $files_to_process_per_request = 250; //block of 512 bytes
    /**
     * Compression level, 0-uncompressed, 9-maximum compression
     * @var int
     */
    private $compression_level = 0; //0-9 , 0 uncompressed
    /**
     * Split backup size limit
     * Create a new backup archive file once the set size is reached
     * @var float|int
     */
    private $xcloner_split_backup_limit = 2048; //2048MB
    /**
     * Number of processed bytes
     * @var int
     */
    private $processed_size_bytes = 0;

    /**
     * The backup name encryption suffix
     * @var string
     */
    private $encrypt_suffix = "-enc";

    /**
     * Archive name
     * @var string
     */
    private $archive_name;
    /**
     * @var Tar
     */
    private $backup_archive;
    /**
     * @var Xcloner_File_System
     */
    private $filesystem;
    /**
     * @var Xcloner_Logger
     */
    private $logger;
    /**
     * @var Xcloner_Settings
     */
    private $xcloner_settings;

    /**
     * [__construct description]
     * @param Xcloner $xcloner_container XCloner Container
     * @param string $archive_name Achive Name
     */
    public function __construct(Xcloner $xcloner_container, $archive_name = "")
    {
        $this->filesystem = $xcloner_container->get_xcloner_filesystem();
        $this->logger = $xcloner_container->get_xcloner_logger()->withName("xcloner_archive");
        $this->xcloner_settings = $xcloner_container->get_xcloner_settings();

        if ($value = $this->xcloner_settings->get_xcloner_option('xcloner_size_limit_per_request')) {
            $this->file_size_per_request_limit = $value * 1024 * 1024;
        } //MB

        if ($value = $this->xcloner_settings->get_xcloner_option('xcloner_files_to_process_per_request')) {
            $this->files_to_process_per_request = $value;
        }

        if ($value = $this->xcloner_settings->get_xcloner_option('xcloner_backup_compression_level')) {
            $this->compression_level = $value;
        }

        if ($value = $this->xcloner_settings->get_xcloner_option('xcloner_split_backup_limit')) {
            $this->xcloner_split_backup_limit = $value;
        }

        $this->xcloner_split_backup_limit = $this->xcloner_split_backup_limit * 1024 * 1024; //transform to bytes

        if (isset($archive_name) && $archive_name) {
            $this->set_archive_name($archive_name);
        }
    }

    /*
     * Rename backup archive
     *
     * @param string $old_name
     * @param string $new_name
     *
     */
    public function rename_archive($old_name, $new_name)
    {
        $this->logger->info(sprintf("Renaming backup archive %s to %s", $old_name, $new_name));
        $storage_filesystem = $this->filesystem->get_storage_filesystem();
        $storage_filesystem->rename($old_name, $new_name);
    }

    /*
     *
     * Set the backup archive name
     *
     */
    public function set_archive_name($name = "", $part = 0, $encrypt_prefix = false)
    {
        $this->archive_name = $this->filesystem->process_backup_name($name);

        if ($encrypt_prefix) {
            $this->archive_name .= $this->encrypt_suffix;
        }

        if ($diff_timestamp_start = $this->filesystem->get_diff_timestamp_start()) {
            //$this->archive_name = $this->archive_name."-diff-".date("Y-m-d_H-i",$diff_timestamp_start);
            $new_name = $this->archive_name;

            if (!stristr($new_name, "-diff")) {
                $new_name = $this->archive_name."-diff".date("Y-m-d_H-i", $diff_timestamp_start);
            }

            $this->archive_name = $new_name;
        }

        if (isset($part) and $part) {
            $new_name = preg_replace('/-part(\d*)/', "-part".$part, $this->archive_name);
            if (!stristr($new_name, "-part")) {
                $new_name = $this->archive_name."-part".$part;
            }

            $this->archive_name = $new_name;
        }

        return $this;
    }

    /*
     *
     * Returns the backup archive name
     *
     * @return string archive name
     */
    public function get_archive_name()
    {
        return $this->archive_name;
    }

    /*
     *
     * Returns the multipart naming for the backup archive
     *
     * @return string multi-part backup name
     */
    public function get_archive_name_multipart()
    {
        $new_name = preg_replace('/-part(\d*)/', "", $this->archive_name);
        return $new_name."-multipart".$this->xcloner_settings->get_backup_extension_name(".csv");
    }

    /*
     *
     * Returns the full backup name including extension
     *
     */
    public function get_archive_name_with_extension()
    {
        return $this->archive_name.$this->xcloner_settings->get_backup_extension_name();
    }

    /*
     *
     * Send notification error by E-Mail
     *
     * @param $to
     * @param $from
     * @param $subject
     * @param $backup_name
     * @param $params
     * @param $error_message
     *
     * @return bool
     */

    /**
     * @param string $error_message
     */
    public function send_notification_error($to, $from, $subject, $backup_name, $params, $error_message)
    {
        $body = "";
        $body .= sprintf(__("Backup Site Url: %s"), get_site_url());
        $body .= "<br /><>";

        $body .= sprintf(__("Error Message: %s"), $error_message);

        $this->logger->info(sprintf("Sending backup error notification to %s", $to));

        $admin_email = $this->xcloner_settings->get_xcloner_option("admin_email");

        $headers = array('Content-Type: text/html; charset=UTF-8');

        if ($admin_email and $from) {
            $headers[] = 'From: '.$from.' <'.$admin_email.'>';
        }

        $return = wp_mail($to, $subject, $body, $headers);

        return $return;
    }

    /*
     *
     * Send backup archive notfication by E-Mail
     *
     * @param $to
     * @param $from
     * @param $subject
     * @param $backup_name
     * @param $params
     * @param string $error_message
     * @param array $additional
     *
     * @return bool
     */
    public function send_notification(
        $to,
        $from,
        $subject,
        $backup_name,
        $params,
        $error_message = "",
        $additional = array()
    ) {
        if (!$from) {
            $from = "XCloner Backup";
        }

        if (($error_message)) {
            return $this->send_notification_error($to, $from, $subject, $backup_name, $params, $error_message);
        }

        $params = (array)$params;

        if (!$subject) {
            $subject = sprintf(__("New backup generated %s"), $backup_name);
        }

        $body = sprintf(__("Generated Backup Size: %s"), size_format($this->filesystem->get_backup_size($backup_name)));
        $body .= "<br /><br />";

        if (isset($additional['lines_total'])) {
            $body .= sprintf(__("Total files added: %s"), $additional['lines_total']);
            $body .= "<br /><br />";
        }

        $backup_parts = $this->filesystem->get_multipart_files($backup_name);

        if (!$backups_counter = sizeof($backup_parts)) {
            $backups_counter = 1;
        }

        $body .= sprintf(__("Backup Parts: %s"), $backups_counter);
        $body .= "<br />";

        if (sizeof($backup_parts)) {
            $body .= implode("<br />", $backup_parts);
            $body .= "<br />";
        }

        $body .= "<br />";

        $body .= sprintf(__("Backup Site Url: %s"), get_site_url());
        $body .= "<br />";

        if (isset($params['backup_params']->backup_comments)) {
            $body .= __("Backup Comments: ").$params['backup_params']->backup_comments;
            $body .= "<br /><br />";
        }

        if ($this->xcloner_settings->get_xcloner_option('xcloner_enable_log')) {
            $body .= __("Latest 50 Log Lines: ")."<br />".implode(
                "<br />\n",
                $this->logger->getLastDebugLines(50)
            );
        }

        $attachments = $this->filesystem->get_backup_attachments();

        $attachments_archive = $this->xcloner_settings->get_xcloner_tmp_path().DS."info.tgz";

        $tar = $this;
        $tar->create($attachments_archive);

        foreach ($attachments as $key => $file) {
            $tar->addFile($file, basename($file));
        }
        $tar->close();

        $this->logger->info(sprintf("Sending backup notification to %s", $to));

        $admin_email = $this->xcloner_settings->get_xcloner_option("admin_email");

        $headers = array('Content-Type: text/html; charset=UTF-8', 'From: '.$from.' <'.$admin_email.'>');

        $return = wp_mail($to, $subject, $body, $headers, array($attachments_archive));

        return $return;
    }

    /*
     *
     * Incremental Backup method
     *
     */
    public function start_incremental_backup($backup_params, $extra_params, $init)
    {
        $return = array();

        if (!isset($extra_params['backup_part'])) {
            $extra_params['backup_part'] = 0;
        }

        $return['extra']['backup_part'] = $extra_params['backup_part'];

        if (isset($extra_params['backup_archive_name'])) {
            $this->set_archive_name($extra_params['backup_archive_name'], $return['extra']['backup_part']);
        } else {
            $encrypt = false;
            if (isset($backup_params['backup_encrypt']) && $backup_params['backup_encrypt']) {
                $encrypt = true;
            }
            $this->set_archive_name($backup_params['backup_name'], 0, $encrypt);
        }

        if (!$this->get_archive_name()) {
            $this->set_archive_name();
        }

        $this->backup_archive = $this;
        $this->backup_archive->setCompression($this->compression_level);

        $archive_info = $this->filesystem->get_storage_path_file_info($this->get_archive_name_with_extension());

        if ($init) {
            $this->logger->info(sprintf(__("Initializing the backup archive %s"), $this->get_archive_name()));

            $this->backup_archive->create($archive_info->getPath().DS.$archive_info->getFilename());

            $return['extra']['backup_init'] = 1;
        } else {
            $this->logger->info(sprintf(__("Opening for append the backup archive %s"), $this->get_archive_name()));

            $this->backup_archive->openForAppend($archive_info->getPath().DS.$archive_info->getFilename());

            $return['extra']['backup_init'] = 0;
        }

        $return['extra']['backup_archive_name'] = $this->get_archive_name();
        $return['extra']['backup_archive_name_full'] = $this->get_archive_name_with_extension();

        if (!isset($extra_params['start_at_line'])) {
            $extra_params['start_at_line'] = 0;
        }

        if (!isset($extra_params['start_at_byte'])) {
            $extra_params['start_at_byte'] = 0;
        }

        if (!$this->filesystem->get_tmp_filesystem()->has($this->filesystem->get_included_files_handler())) {
            $this->logger->error(sprintf(
                "Missing the includes file handler %s, aborting...",
                $this->filesystem->get_included_files_handler()
            ));

            $return['finished'] = 1;
            return $return;
        }

        $included_files_handler = $this->filesystem->get_included_files_handler(1);

        $file = new \SplFileObject($included_files_handler);

        $file->seek(PHP_INT_MAX);

        $return['extra']['lines_total'] = ($file->key() - 1);

        //we skip the first CSV line with headers
        if (!$extra_params['start_at_line']) {
            $file->seek(1);
        } else {
            $file->seek($extra_params['start_at_line'] + 1);
        }

        $this->processed_size_bytes = 0;

        $counter = 0;

        $start_byte = $extra_params['start_at_byte'];

        $byte_limit = 0;

        while (!$file->eof() and $counter <= $this->files_to_process_per_request) {
            $current_line_str = $file->current();

            $line = str_getcsv($current_line_str);

            $relative_path = stripslashes($line[0]);

            $start_filesystem = "start_filesystem";

            if (isset($line[4])) {
                $start_filesystem = $line[4];
            }

            //$adapter = $this->filesystem->get_adapter($start_filesystem);

            if (!$relative_path || !$this->filesystem->get_filesystem($start_filesystem)->has($relative_path)) {
                if ($relative_path != "") {
                    $this->logger->error(sprintf(
                        "Could not add file %b to backup archive, file not found",
                        $relative_path
                    ));
                }

                $extra_params['start_at_line']++;
                $file->next();
                continue;
            }

            $file_info = $this->filesystem->get_filesystem($start_filesystem)->getMetadata($relative_path);

            if (!isset($file_info['size'])) {
                $file_info['size'] = 0;
            }

            if ($start_filesystem == "tmp_filesystem") {
                $file_info['archive_prefix_path'] = $this->xcloner_settings->get_xcloner_tmp_path_suffix();
            }

            $byte_limit = (int)$this->file_size_per_request_limit / 512;

            $append = 0;

            if ($file_info['size'] > $byte_limit * 512 or $start_byte) {
                $append = 1;
            }

            if (!isset($return['extra']['backup_size'])) {
                $return['extra']['backup_size'] = 0;
            }

            $return['extra']['backup_size'] = $archive_info->getSize();

            $estimated_new_size = $return['extra']['backup_size'] + $file_info['size'];

            //we create a new backup part if we reach the Split Achive Limit
            if ($this->xcloner_split_backup_limit and ($estimated_new_size > $this->xcloner_split_backup_limit) and (!$start_byte)) {
                $this->logger->info(sprintf(
                    "Backup size limit %s bytes reached, file add estimate %s, attempt to create a new archive ",
                    $this->xcloner_split_backup_limit,
                    $estimated_new_size
                ));
                list($archive_info, $return['extra']['backup_part']) = $this->create_new_backup_part($return['extra']['backup_part']);

                if ($file_info['size'] > $this->xcloner_split_backup_limit) {
                    $this->logger->info(sprintf(
                        "Excluding %s file as it's size(%s) is bigger than the backup split limit of %s and it won't fit a single backup file",
                        $file_info['path'],
                        $file_info['size'],
                        $this->xcloner_split_backup_limit
                    ));
                    $extra_params['start_at_line']++;
                }

                $return['extra']['start_at_line'] = $extra_params['start_at_line'];
                $return['extra']['start_at_byte'] = 0;

                $return['finished'] = 0;

                return $return;
            }

            list($bytes_wrote, $last_position) = $this->add_file_to_archive(
                $file_info,
                $start_byte,
                $byte_limit,
                $append,
                $start_filesystem
            );
            $this->processed_size_bytes += $bytes_wrote;

            //echo" - processed ".$this->processed_size_bytes." bytes ".$this->file_size_per_request_limit." last_position:".$last_position." \n";
            $return['extra']['processed_file'] = $file_info['path'];
            $return['extra']['processed_file_size'] = $file_info['size'];
            $return['extra']['backup_size'] = $archive_info->getSize();

            if ($last_position > 0) {
                $start_byte = $last_position;
            } else {
                $extra_params['start_at_line']++;
                $file->next();
                $start_byte = 0;
                $counter++;
            }

            if ($this->processed_size_bytes >= $this->file_size_per_request_limit) {
                clearstatcache();
                $return['extra']['backup_size'] = $archive_info->getSize();

                $return['finished'] = 0;
                $return['extra']['start_at_line'] = $extra_params['start_at_line'];
                $return['extra']['start_at_byte'] = $last_position;
                $this->logger->info(sprintf(
                    "Reached the maximum %s request data limit, returning response",
                    $this->file_size_per_request_limit
                ));
                return $return;
            }
        }

        if (!$file->eof()) {
            clearstatcache();
            $return['extra']['backup_size'] = $archive_info->getSize();

            $return['finished'] = 0;
            $return['extra']['start_at_line'] = $extra_params['start_at_line'];
            $return['extra']['start_at_byte'] = $last_position;
            $this->logger->info(sprintf(
                "We have reached the maximum files to process per request limit of %s, returning response",
                $this->files_to_process_per_request
            ));

            return $return;
        }

        //close the backup archive by adding 2*512 blocks of zero bytes
        $this->logger->info(sprintf(
            "Closing the backup archive %s with 2*512 zero bytes blocks.",
            $this->get_archive_name_with_extension()
        ));
        $this->backup_archive->close();

        /**
         * XCloner HOOK backup_archive_finished.
         *
         * This will get triggered when a backup archive is finished writing.
         */
        //do_action('backup_archive_finished', $this->backup_archive, $this);

        //updating archive_info
        $archive_info = $this->filesystem->get_storage_path_file_info($this->get_archive_name_with_extension());

        if ($return['extra']['backup_part']) {
            $this->write_multipart_file($this->get_archive_name_with_extension());
        }

        $return['extra']['start_at_line'] = $extra_params['start_at_line'] - 1;

        if (isset($file_info)) {
            $return['extra']['processed_file'] = $file_info['path'];
            $return['extra']['processed_file_size'] = $file_info['size'];
        }

        clearstatcache();
        $return['extra']['backup_size'] = $archive_info->getSize();

        $return['finished'] = 1;
        return $return;
    }

    /*
     *
     * Write multipart file components
     *
     */
    private function write_multipart_file($path = "")
    {
        if (!$path) {
            $path = $this->get_archive_name_with_extension();
        }

        $file = $this->filesystem->get_filesystem("storage_filesystem_append")->getMetadata($path);
        //print_r($file_info);
        $line = '"'.$file['path'].'","'.$file['timestamp'].'","'.$file['size'].'"'.PHP_EOL;


        $this->filesystem->get_filesystem("storage_filesystem_append")
            ->write($this->get_archive_name_multipart(), $line);
    }

    /*
     *
     * Create a new backup part
     *
     */
    private function create_new_backup_part($part = 0)
    {
        //close the backup archive by adding 2*512 blocks of zero bytes
        $this->logger->info(sprintf(
            "Closing the backup archive %s with 2*512 zero bytes blocks.",
            $this->get_archive_name_with_extension()
        ));
        $this->backup_archive->close();

        if (!$part) {
            $old_name = $this->get_archive_name_with_extension();
            $this->set_archive_name($this->get_archive_name(), ++$part);
            $this->rename_archive($old_name, $this->get_archive_name_with_extension());

            if ($this->filesystem->get_storage_filesystem()->has($this->get_archive_name_multipart())) {
                $this->filesystem->get_storage_filesystem()->delete($this->get_archive_name_multipart());
            }

            $this->write_multipart_file($this->get_archive_name_with_extension());
        } else {
            $this->logger->info(sprintf(
                "Creating new multipart info file %s",
                $this->get_archive_name_with_extension()
            ));
            $this->write_multipart_file($this->get_archive_name_with_extension());
        }

        $this->set_archive_name($this->get_archive_name(), ++$part);

        $this->logger->info(sprintf("Creating new backup archive part %s", $this->get_archive_name_with_extension()));

        $this->backup_archive = $this;
        $this->backup_archive->setCompression($this->compression_level);
        $archive_info = $this->filesystem->get_storage_path_file_info($this->get_archive_name_with_extension());
        $this->backup_archive->create($archive_info->getPath().DS.$archive_info->getFilename());

        return array($archive_info, $part);
    }

    /*
     *
     * Add file to archive
     *
     */

    /**
     * @param integer $append
     */
    public function add_file_to_archive($file_info, $start_at_byte, $byte_limit = 0, $append, $filesystem)
    {
        $start_adapter = $this->filesystem->get_adapter($filesystem);
        $start_filesystem = $this->filesystem->get_adapter($filesystem);

        if (!$file_info['path']) {
            return;
        }

        if (isset($file_info['archive_prefix_path'])) {
            $file_info['target_path'] = $file_info['archive_prefix_path']."/".$file_info['path'];
        } else {
            $file_info['target_path'] = $file_info['path'];
        }

        $last_position = $start_at_byte;

        //$start_adapter = $this->filesystem->get_start_adapter();

        if (!$append) {
            $bytes_wrote = $file_info['size'];
            $this->logger->info(sprintf(
                "Adding %s bytes of file %s to archive %s ",
                $bytes_wrote,
                $file_info['target_path'],
                $this->get_archive_name_with_extension()
            ));
            $this->backup_archive->addFile(
                $start_adapter->applyPathPrefix($file_info['path']),
                $file_info['target_path']
            );
        } else {
            $tmp_file = md5($file_info['path']);

            //we isolate file to tmp if we are at byte 0, the starting point of file reading
            if (!$start_at_byte) {
                $this->logger->info(sprintf(
                    "Copying %s file to tmp filesystem file %s to prevent reading changes",
                    $file_info['path'],
                    $tmp_file
                ));
                $file_stream = $start_filesystem->readStream($file_info['path']);

                if (is_resource($file_stream['stream'])) {
                    $this->filesystem->get_tmp_filesystem()->writeStream($tmp_file, $file_stream['stream']);
                }
            }

            if ($this->filesystem->get_tmp_filesystem()->has($tmp_file)) {
                $is_tmp = 1;
                $last_position = $this->backup_archive->appendFileData(
                    $this->filesystem->get_tmp_filesystem_adapter()
                                                        ->applyPathPrefix($tmp_file),
                    $file_info['target_path'],
                    $start_at_byte,
                    $byte_limit
                );
            } else {
                $is_tmp = 0;
                $last_position = $this->backup_archive->appendFileData(
                    $start_adapter->applyPathPrefix($file_info['path']),
                    $file_info['target_path'],
                    $start_at_byte,
                    $byte_limit
                );
            }


            if ($last_position == -1) {
                $bytes_wrote = $file_info['size'] - $start_at_byte;
            } else {
                $bytes_wrote = $last_position - $start_at_byte;
            }


            if ($is_tmp) {
                $this->logger->info(sprintf(
                    "Appended %s bytes, starting position %s, of tmp file %s (%s) to archive %s ",
                    $bytes_wrote,
                    $start_at_byte,
                    $tmp_file,
                    $file_info['target_path'],
                    $this->get_archive_name()
                ));
            } else {
                $this->logger->info(sprintf(
                    "Appended %s bytes, starting position %s, of original file %s to archive %s ",
                    $bytes_wrote,
                    $start_at_byte,
                    $file_info['target_path'],
                    $tmp_file,
                    $this->get_archive_name()
                ));
            }

            //we delete here the isolated tmp file
            if ($last_position == -1) {
                if ($this->filesystem->get_tmp_filesystem_adapter()->has($tmp_file)) {
                    $this->logger->info(sprintf("Deleting %s from the tmp filesystem", $tmp_file));
                    $this->filesystem->get_tmp_filesystem_adapter()->delete($tmp_file);
                }
            }
        }

        return array($bytes_wrote, $last_position);
    }

    /**
     * Append data to a file to the current TAR archive using an existing file in the filesystem
     *
     * @param string $file path to the original file
     * @param int $start starting reading position in file
     * @param int $end end position in reading multiple with 512
     * @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with
     * all meta data, empty to take from original
     * @throws ArchiveIOException
     */
    public function appendFileData($file, $fileinfo = '', $start = 0, $limit = 0)
    {
        $end = $start+($limit*512);

        //check to see if we are at the begining of writing the file
        if (!$start) {
            if (is_string($fileinfo)) {
                $fileinfo = FileInfo::fromPath($file, $fileinfo);
            }
        }

        if ($this->closed) {
            throw new ArchiveIOException('Archive has been closed, files can no longer be added');
        }

        if (is_dir($file)) {
            $this->writeFileHeader($fileinfo);
            return;
        }

        $fp = fopen($file, 'rb');

        fseek($fp, $start);

        if (!$fp) {
            throw new ArchiveIOException('Could not open file for reading: '.$file);
        }

        // create file header
        if (!$start) {
            $this->writeFileHeader($fileinfo);
        }

        $bytes = 0;
        // write data
        while ($end >=ftell($fp) and !feof($fp)) {
            $data = fread($fp, 512);
            if ($data === false) {
                break;
            }
            if ($data === '') {
                break;
            }
            $packed = pack("a512", $data);
            $bytes += $this->writebytes($packed);
        }



        //if we are not at the end of file, we return the current position for incremental writing
        if (!feof($fp)) {
            $last_position = ftell($fp);
        } else {
            $last_position = -1;
        }

        fclose($fp);

        return $last_position;
    }

    public function open($file, $start_byte = 0)
    {
       parent::open($file);

        if ($start_byte) {
            fseek($this->fh, $start_byte);
        }
    }

    /**
     * Open a TAR archive and put the file cursor at the end for data appending
     *
     * If $file is empty, the tar file will be created in memory
     *
     * @param string $file
     * @throws ArchiveIOException
     */
    public function openForAppend($file = '')
    {
        $this->file   = $file;
        $this->memory = '';
        $this->fh     = 0;

        if ($this->file) {
            // determine compression
            if ($this->comptype == Archive::COMPRESS_AUTO) {
                $this->setCompression($this->complevel, $this->filetype($file));
            }

            if ($this->comptype === Archive::COMPRESS_GZIP) {
                $this->fh = @gzopen($this->file, 'ab'.$this->complevel);
            } elseif ($this->comptype === Archive::COMPRESS_BZIP) {
                $this->fh = @bzopen($this->file, 'a');
            } else {
                $this->fh = @fopen($this->file, 'ab');
            }

            if (!$this->fh) {
                throw new ArchiveIOException('Could not open file for writing: '.$this->file);
            }
        }
        $this->writeaccess = true;
        $this->closed      = false;
    }

    /**
     * Read the contents of a TAR archive
     *
     * This function lists the files stored in the archive
     *
     * The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
     * Reopen the file with open() again if you want to do additional operations
     *
     * @throws ArchiveIOException
     * @returns FileInfo[]
     */
    public function contents($files_limit = 0)
    {
        if ($this->closed || !$this->file) {
            throw new ArchiveIOException('Can not read from a closed archive');
        }

		$files_counter = 0;
        $result = array();

        while ($read = $this->readbytes(512)) {
            $header = $this->parseHeader($read);
            if (!is_array($header)) {
                continue;
            }

            if($files_limit)
            {
				if(++$files_counter > $files_limit)
				{
					$return['extracted_files'] = $result;
					$return['start'] = ftell($this->fh)-512;
					return $return;
				}
			}

			if($header['typeflag'] == 5)
				$header['size'] = 0;

            $this->skipbytes(ceil($header['size'] / 512) * 512);
            $result[] = $this->header2fileinfo($header);
        }

		$return['extracted_files'] = $result;

        $this->close();
        return $return;
    }

    /**
     * Extract archive contents
     *
     * @param [type] $outdir
     * @param string $strip
     * @param string $exclude
     * @param string $include
     * @param integer $files_limit
     * @return void
     */
    public function extract($outdir, $strip = '', $exclude = '', $include = '', $files_limit = 0)
    {

        if ($this->closed || !$this->file) {
            throw new ArchiveIOException('Can not read from a closed archive');
        }

        $outdir = rtrim($outdir, '/');
        if(!is_dir($outdir))
				@mkdir($outdir, 0755, true);
			else
				@chmod($outdir, 0777);

        //@mkdir($outdir, 0777, true);

        if (!is_dir($outdir)) {
            throw new ArchiveIOException("Could not create directory '$outdir'");
        }

		$files_counter = 0;
		$return = array();

        $extracted = array();
        while ($dat = $this->readbytes(512)) {
            // read the file header
            $header = $this->parseHeader($dat);
            if (!is_array($header)) {
                continue;
            }

            if($files_limit)
            {
				if(++$files_counter > $files_limit)
				{
					$return['extracted_files'] = $extracted;
					$return['start'] = ftell($this->fh)-512;
					return $return;
				}
			}

            $fileinfo = $this->header2fileinfo($header);

            // apply strip rules
            $fileinfo->strip($strip);

            // skip unwanted files
            if (!strlen($fileinfo->getPath()) || !$fileinfo->match($include, $exclude)) {
                $this->skipbytes(ceil($header['size'] / 512) * 512);
                continue;
            }

            // create output directory
            $output    = $outdir.'/'.$fileinfo->getPath();
            $directory = ($fileinfo->getIsdir()) ? $output : dirname($output);
            if(!is_dir($directory))
				@mkdir($directory, 0755, true);
			else
				@chmod($directory, 0755);

            // extract data
            if (!$fileinfo->getIsdir()) {
				if(file_exists($output))
					unlink($output);

                $fp = fopen($output, "wb");
                if (!$fp) {
                    throw new ArchiveIOException('Could not open file for writing: '.$output);
                }

                $size = floor($header['size'] / 512);
                for ($i = 0; $i < $size; $i++) {
                    fwrite($fp, $this->readbytes(512), 512);
                }
                if (($header['size'] % 512) != 0) {
                    fwrite($fp, $this->readbytes(512), $header['size'] % 512);
                }

                fclose($fp);
                touch($output, $fileinfo->getMtime());
                chmod($output, $fileinfo->getMode());
            } else {
                //$this->skipbytes(ceil($header['size'] / 512) * 512); // the size is usually 0 for directories
                $this->skipbytes(ceil(0 / 512) * 512); // the size is usually 0 for directories
            }

            $extracted[] = $fileinfo;
        }

        $this->close();

        $return['extracted_files'] = $extracted;

        return $return;
    }



    /**
     * Adds a file to a TAR archive by appending it's data
     *
     * @param string $archive name of the archive file
     * @param string $file name of the file to read data from
     * @param string $start start position from where to start reading data
     * @throws ArchiveIOException
     */
    /*public function addFileToArchive($archive, $file, $start = 0)
    {
        $this->openForAppend($archive);
        return $start = $this->appendFileData($file, $start, $this->file_size_per_request_limit);
    }
    */
}
<?php
namespace watchfulli\XClonerCore;

class Xcloner_Scheduler
{
    private $db;
    private $scheduler_table = "xcloner_scheduler";

    private $xcloner_remote_storage;
    private $archive_system;
    private $xcloner_database;
    private $xcloner_settings;
    private $logger;
    private $xcloner_file_system;
    private $xcloner_encryption;
    private $xcloner_container;

    private $allowed_schedules = array("hourly", "twicedaily", "daily", "weekly", "monthly");

    /*public function __call($method, $args) {
        echo "$method is not defined";
    }*/

    public function __construct(Xcloner $xcloner_container)
    {
        //global $wpdb;

        $this->xcloner_container = $xcloner_container;
        $this->xcloner_database  = $this->get_xcloner_container()->get_xcloner_database();
        $this->xcloner_settings  = $this->xcloner_container->get_xcloner_settings();

        $this->db          = $this->xcloner_database;
        $this->db->show_errors = false;

        $this->scheduler_table = $this->xcloner_settings->get_table_prefix().$this->scheduler_table;
    }

    private function get_xcloner_container()
    {
        return $this->xcloner_container;
    }

    private function set_xcloner_container(Xcloner $container)
    {
        $this->xcloner_container = $container;
    }

    public function get_scheduler_list($return_only_enabled = 0)
    {
        $list = $this->db->get_results("SELECT * FROM ".$this->scheduler_table);

        if ($return_only_enabled) {
            $new_list = array();

            foreach ($list as $res) {
                if ($res->status) {
                    $res->next_run_time = wp_next_scheduled('xcloner_scheduler_'.$res->id, array($res->id)) + ($this->xcloner_settings->get_xcloner_option('gmt_offset') * HOUR_IN_SECONDS);
                    if ($res->next_run_time) {
                        $new_list[]         = $res;
                    }
                }
            }
            $list = $new_list;
        }

        return $list;
    }

    public function get_next_run_schedule()
    {
        $list = $this->get_scheduler_list($return_only_enabled = 1);

        return $list;
    }

    public function get_schedule_by_id_object($id)
    {
        $data = $this->db->get_row("SELECT * FROM ".$this->scheduler_table." WHERE id=".$id);

        if (!$data) {
            return false;
        }

        return $data;
    }

    /**
     * Get schedule by id or name
     *
     * @param [type] $id
     * @return array
     */
    public function get_schedule_by_id_or_name($id)
    {
        $data = $this->db->get_row("SELECT * FROM ".$this->scheduler_table." WHERE id='".$id."' or name='".$id."'", ARRAY_A);

        if (!$data) {
            return false;
        }

        return $data;
    }

    /**
     * Get schedule by id
     *
     * @param [type] $id
     * @return array
     */
    public function get_schedule_by_id($id)
    {
        $data = $this->db->get_row("SELECT * FROM ".$this->scheduler_table." WHERE id=".$id, ARRAY_A);

        if (!$data) {
            return false;
        }

        $params = json_decode($data['params']);

        //print_r($params);
        $data['params']         = "";
        $data['backup_params']  = $params->backup_params;
        $data['table_params']   = json_encode($params->database);
        $data['excluded_files'] = json_encode($params->excluded_files);

        return $data;
    }

    public function delete_schedule_by_id($id)
    {
        $hook = 'xcloner_scheduler_'.$id;
        wp_clear_scheduled_hook($hook, array($id));

        $data = $this->db->delete($this->scheduler_table, array('id' => $id));

        return $data;
    }

    public function deactivate_wp_cron_hooks()
    {
        $list = $this->get_scheduler_list();

        foreach ($list as $schedule) {
            $hook = 'xcloner_scheduler_'.$schedule->id;

            if ($timestamp = wp_next_scheduled($hook, array($schedule->id))) {
                wp_unschedule_event($timestamp, $hook, array($schedule->id));
            }
        }
    }

    public function update_wp_cron_hooks()
    {
        $list = $this->get_scheduler_list();

        foreach ($list as $schedule) {
            $hook = 'xcloner_scheduler_'.$schedule->id;

            //adding the xcloner_scheduler hook with xcloner_scheduler_callback callback
            $this->xcloner_container->get_loader()->add_action($hook, $this, 'xcloner_scheduler_callback', 10, 1);

            if (!wp_next_scheduled($hook, array($schedule->id)) and $schedule->status) {
                if ($schedule->recurrence == "single") {
                    wp_schedule_single_event(strtotime($schedule->start_at), $hook, array($schedule->id));
                } else {
                    wp_schedule_event(strtotime($schedule->start_at), $schedule->recurrence, $hook, array($schedule->id));
                }
            } elseif (!$schedule->status) {
                if ($timestamp = wp_next_scheduled($hook, array($schedule->id))) {
                    wp_unschedule_event($timestamp, $hook, array($schedule->id));
                }
            }
        }
    }

    public function update_cron_hook($id)
    {
        $schedule = $this->get_schedule_by_id_object($id);
        $hook     = 'xcloner_scheduler_'.$schedule->id;

        if ($timestamp = wp_next_scheduled($hook, array($schedule->id))) {
            wp_unschedule_event($timestamp, $hook, array($schedule->id));
        }

        if ($schedule->status) {
            if ($schedule->recurrence == "single") {
                wp_schedule_single_event(strtotime($schedule->start_at), $hook, array($schedule->id));
            } else {
                wp_schedule_event(strtotime($schedule->start_at), $schedule->recurrence, $hook, array($schedule->id));
            }
        }
    }

    public function disable_single_cron($schedule_id)
    {
        $schedule = array();
        $hook = 'xcloner_scheduler_'.$schedule_id;

        if ($timestamp = wp_next_scheduled($hook, array($schedule_id))) {
            wp_unschedule_event($timestamp, $hook, array($schedule_id));
        }

        $schedule['status'] = 0;

        $update = $this->db->update(
            $this->scheduler_table,
            $schedule,
            array('id' => $schedule_id),
            array(
                '%s',
                '%s'
            )
        );

        return $update;
    }

    public function update_hash($schedule_id, $hash)
    {
        $schedule = array();

        $schedule['hash'] = $hash;

        $update = $this->db->update(
            $this->scheduler_table,
            $schedule,
            array('id' => $schedule_id),
            array(
                '%s',
                '%s'
            )
        );

        return $update;
    }

    public function update_last_backup($schedule_id, $last_backup)
    {
        $schedule = array();

        $this->logger->info(sprintf('Updating last backup %s for schedule id #%s', $last_backup, $schedule_id));

        $schedule['last_backup'] = $last_backup;

        $update = $this->db->update(
            $this->scheduler_table,
            $schedule,
            array('id' => $schedule_id),
            array(
                '%s',
                '%s'
            )
        );

        return $update;
    }

    private function _xcloner_scheduler_callback($id, $schedule, $xcloner = "")
    {
        set_time_limit(0);

        $start_time = time();

        if (!$xcloner) {
            $xcloner = new XCloner();
            $xcloner->init();
        }
        $this->set_xcloner_container($xcloner);
        $return_encrypted = array();
        $return = array();
        $additional = array();

        #$hash = $this->xcloner_settings->get_hash();
        #$this->get_xcloner_container()->get_xcloner_settings()->set_hash($hash);

        //$this->xcloner_settings 		= $this->get_xcloner_container()->get_xcloner_settings();
        $this->xcloner_file_system      = $this->get_xcloner_container()->get_xcloner_filesystem();
        $this->xcloner_encryption       = $this->get_xcloner_container()->get_xcloner_encryption();
        $this->xcloner_database         = $this->get_xcloner_container()->get_xcloner_database();
        $this->archive_system           = $this->get_xcloner_container()->get_archive_system();
        $this->logger                   = $this->get_xcloner_container()->get_xcloner_logger()->withName("xcloner_scheduler");
        $this->xcloner_remote_storage   = $this->get_xcloner_container()->get_xcloner_remote_storage();


        $this->db          		= $this->xcloner_database;
        $this->db->show_errors 	= false;

        $this->logger->info(sprintf("New schedule hash is %s", $this->xcloner_settings->get_hash()));

        if (isset($schedule['backup_params']->diff_start_date) && $schedule['backup_params']->diff_start_date) {
            $this->xcloner_file_system->set_diff_timestamp_start($schedule['backup_params']->diff_start_date);
        }
        

        if ($schedule['recurrence'] == "single") {
            $this->disable_single_cron($schedule['id']);
        }

        if (!$schedule) {
            $this->logger->info(sprintf("Could not load schedule with id'%s'", $id), array("CRON"));

            return;
        }

        //echo $this->get_xcloner_container()->get_xcloner_settings()->get_hash(); exit;
        if (!$xcloner) {
            //we update this only in WP mode
            $this->update_hash($schedule['id'], $this->xcloner_settings->get_hash());
        }

        $this->logger->print_info(sprintf("Starting backup profile '%s'", $schedule['name']), array("CRON"));

        $this->xcloner_file_system->set_excluded_files(json_decode($schedule['excluded_files']));

        $init     = 1;
        $continue = 1;

        while ($continue) {
            $continue = $this->xcloner_file_system->start_file_recursion($init);

            $init = 0;
        }

        $this->logger->print_info(sprintf("File scan finished"), array("CRON"));

        $this->logger->print_info(sprintf("Starting the database backup"), array("CRON"));

        $init               = 1;
        $return['finished'] = 0;

        while (!$return['finished']) {
            $return = $this->xcloner_database->start_database_recursion((array)json_decode($schedule['table_params']), $return, $init);
            $init   = 0;
        }

        $this->logger->print_info(sprintf("Database backup done"), array("CRON"));

        $this->logger->print_info(sprintf("Starting file archive process"), array("CRON"));

        $init               = 0;
        $return['finished'] = 0;
        $return['extra']    = array();

        while (!$return['finished']) {
            $return = $this->archive_system->start_incremental_backup((array)$schedule['backup_params'], $return['extra'], $init);
            $init   = 0;
        }
        $this->logger->print_info(sprintf("File archive process FINISHED."), array("CRON"));

        //getting the last backup archive file
        $return['extra']['backup_parent'] = $this->archive_system->get_archive_name_with_extension();
        if ($this->xcloner_file_system->is_part($this->archive_system->get_archive_name_with_extension())) {
            $return['extra']['backup_parent'] = $this->archive_system->get_archive_name_multipart();
        }

        //Updating schedule last backup archive
        $this->update_last_backup($schedule['id'], $return['extra']['backup_parent']);

        //Encrypting the backup archive
        $return_encrypted['finished'] = 0;
        $return_encrypted['start'] = 0;
        $return_encrypted['iv'] = '';
        $return_encrypted['target_file'] = '';
        $part = 0;
        $backup_parts = array();

        if (isset($schedule['backup_params']->backup_encrypt) && $schedule['backup_params']->backup_encrypt) {
            $this->logger->print_info(sprintf("Encrypting backup archive %s.", $return['extra']['backup_parent']), array("CRON"));

            $backup_file = $return['extra']['backup_parent'];

            if ($this->xcloner_file_system->is_multipart($return['extra']['backup_parent'])) {
                $backup_parts = $this->xcloner_file_system->get_multipart_files($return['extra']['backup_parent']);
                $backup_file = $backup_parts[$part];
            }

            while (!$return_encrypted['finished']) {
                $return_encrypted = $this->xcloner_encryption->encrypt_file(
                    $backup_file,
                    "",
                    "",
                    "",
                    "",
                    true,
                    true
                );

                if ($return_encrypted['finished']) {
                    ++$part;

                    if ($part < sizeof($backup_parts)) {
                        $return_encrypted['finished'] = 0;
                        $backup_file = $backup_parts[$part];
                    }
                }
            }
        }
        
        //Sending backup to remote storage
        if (isset($schedule['remote_storage']) && $schedule['remote_storage'] && array_key_exists($schedule['remote_storage'], $this->xcloner_remote_storage->get_available_storages())) {
            $backup_file = $return['extra']['backup_parent'];

            $this->logger->print_info(sprintf("Transferring backup to remote storage %s", strtoupper($schedule['remote_storage'])), array("CRON"));

            if (method_exists($this->xcloner_remote_storage, "upload_backup_to_storage")) {
                call_user_func_array(array(
                    $this->xcloner_remote_storage,
                    "upload_backup_to_storage"
                ), array($backup_file, $schedule['remote_storage'], $schedule['backup_params']->backup_delete_after_remote_transfer));
            }
        }

        //Sending email notification
        if (isset($schedule['backup_params']->email_notification) and $to = $schedule['backup_params']->email_notification) {
            try {
                $from                      = "";
                $additional['lines_total'] = $return['extra']['lines_total'];
                $subject                   = sprintf(__("%s - new backup generated %s"), $schedule['name'], $return['extra']['backup_parent']);

                $this->archive_system->send_notification($to, $from, $subject, $return['extra']['backup_parent'], $schedule, "", $additional);
            } catch (Exception $e) {
                $this->logger->error($e->getMessage());
            }
        }

        //Backup Storage Cleanup
        $this->xcloner_file_system->backup_storage_cleanup();

        //Filesystem Cleanup
        $this->xcloner_file_system->cleanup_tmp_directories();

        //Removing the tmp filesystem used for backup
        $this->xcloner_file_system->remove_tmp_filesystem();

        $this->logger->print_info(sprintf("Profile '%s' finished in %d seconds.", $schedule['name'], time() - $start_time), array("CRON"));

        return $return;
    }

    public function xcloner_scheduler_callback($id, $schedule = "", $xcloner = "")
    {
        if ($id) {
            $schedule = $this->get_schedule_by_id($id);
        }

        try {
            if ($this->xcloner_settings->get_xcloner_option('xcloner_disable_email_notification')) {
                //we disable email notifications
                $schedule['backup_params']->email_notification = "";
            }

            return $this->_xcloner_scheduler_callback($id, $schedule, $xcloner);
        } catch (Exception $e) {

            //send email to site admin if email notification is not set in the scheduler
            if (!isset($schedule['backup_params']->email_notification) || !$schedule['backup_params']->email_notification) {
                $schedule['backup_params']->email_notification = $this->xcloner_settings->get_xcloner_option('admin_email');
            }

            if (isset($schedule['backup_params']->email_notification) && $to = $schedule['backup_params']->email_notification) {
                $from = "";
                $this->archive_system->send_notification($to, $from, $schedule['name']." - backup error", "", "", $e->getMessage());
            }
        }
    }

    public function get_available_intervals()
    {
        $schedules     = wp_get_schedules();
        $new_schedules = array();

        foreach ($schedules as $key => $row) {
            if (in_array($key, $this->allowed_schedules)) {
                $new_schedules[$key] = $row;
                $intervals[$key]     = $row['interval'];
            }
        }

        array_multisort($intervals, SORT_ASC, $new_schedules);

        $new_schedules['profile'] = [
            'interval'=> '-1',
            'display'=> 'Manual Execution'
        ];

        return $new_schedules;
    }
}
<?php
namespace watchfulli\XClonerCore;

/**
 * XCloner - Backup and Restore backup plugin for Wordpress
 *
 * class-xcloner-file-transfer.php
 * @author Liuta Ovidiu <info@thinkovi.com>
 *
 *        This program is free software; you can redistribute it and/or modify
 *        it under the terms of the GNU General Public License as published by
 *        the Free Software Foundation; either version 2 of the License, or
 *        (at your option) any later version.
 *
 *        This program is distributed in the hope that it will be useful,
 *        but WITHOUT ANY WARRANTY; without even the implied warranty of
 *        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *        GNU General Public License for more details.
 *
 *        You should have received a copy of the GNU General Public License
 *        along with this program; if not, write to the Free Software
 *        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *        MA 02110-1301, USA.
 *
 * @link https://github.com/ovidiul/XCloner-Wordpress
 *
 * @modified 7/25/18 1:46 PM
 *
 */

class Xcloner_File_Transfer extends Xcloner_File_System
{

	/**
	 * Target url web address of the restore script
	 * @var string
	 */
	private $target_url;
	/**
	 * Transfer data limit in bytes
	 * @var int
	 */
	private $transfer_limit = 1048576; //bytes 1MB= 1048576 300KB = 358400


	/**
	 * @param $target_url
	 *
	 * @return mixed
	 */
	public function set_target($target_url)
	{
		return $this->target_url = $target_url;
	}

	/**
	 * @return string
	 */
	public function get_target()
	{
		return $this->target_url;
	}


	/**
	 * @param $file
	 * @param int $start
	 * @param string $hash
	 *
	 * @return bool|int
	 * @throws Exception
	 */
	public function transfer_file($file, $start = 0, $hash = "")
	{
		if (!$this->target_url) {
			throw new \Exception("Please setup a target url for upload");
		}


		$fp = $this->get_storage_filesystem()->readStream($file);

		fseek($fp, $start, SEEK_SET);

		$binary_data = fread($fp, $this->transfer_limit);

		$tmp_filename = "xcloner_upload_".substr(md5(time()), 0, 5);

		$this->get_tmp_filesystem()->write($tmp_filename, $binary_data);

		$tmp_file_path = $this->get_tmp_filesystem_adapter()->applyPathPrefix($tmp_filename);

		$send_array = array();

		$send_array['file'] = $file;
		$send_array['start'] = $start;
		$send_array['xcloner_action'] = "write_file";
		$send_array['hash'] = $hash;
		#$send_array['blob'] 	= $binary_data;
		$send_array['blob'] = $this->curl_file_create($tmp_file_path, 'application/x-binary', $tmp_filename);

		//$data = http_build_query($send_array);

		$this->get_logger()->info(sprintf("Sending curl request to %s with %s data of file %s starting position %s using temporary file %s",
			$this->target_url, $this->transfer_limit, $file, $start, $tmp_filename));


		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $this->target_url);

		curl_setopt($ch, CURLOPT_POST, 1);
		//curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
		//curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
		curl_setopt($ch, CURLOPT_TIMEOUT, 1200);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

		curl_setopt($ch, CURLOPT_POSTFIELDS, $send_array);
		curl_setopt($ch, CURLOPT_VERBOSE, true);

		$original_result = curl_exec($ch);


		$this->get_tmp_filesystem()->delete($tmp_filename);

		$result = json_decode($original_result);

		if (!$result) {
			throw new \Exception("We have received no valid response from the remote host, original message: ".$original_result);
		}

		if ($result->status != 200) {
			throw new \Exception($result->response);
		}

		if (ftell($fp) >= $this->get_storage_filesystem()->getSize($file)) {
			$this->get_logger()->info(sprintf("Upload done for file %s to target url %s, transferred a total of %s bytes",
				$file, $this->target_url, ftell($fp)));
			$this->remove_tmp_filesystem();

			return false;
		}

		return ftell($fp);
	}

	/**
	 * @param string $filename
	 * @param string $mimetype
	 * @param string $postname
	 *
	 * @return CURLFile|string
	 */
	private function curl_file_create($filename, $mimetype = '', $postname = '')
	{
		if (!function_exists('curl_file_create')) {

			return "@$filename;filename="
				. ($postname ?: basename($filename))
				. ($mimetype ? ";type=$mimetype" : '');

		} else {

			return curl_file_create($filename, $mimetype, $postname);

		}
	}
}
<?php
namespace watchfulli\XClonerCore;

/**
 * XCloner - Backup and Restore backup plugin for Wordpress
 *
 * class-xcloner-api.php
 * @author Liuta Ovidiu <info@thinkovi.com>
 *
 *        This program is free software; you can redistribute it and/or modify
 *        it under the terms of the GNU General Public License as published by
 *        the Free Software Foundation; either version 2 of the License, or
 *        (at your option) any later version.
 *
 *        This program is distributed in the hope that it will be useful,
 *        but WITHOUT ANY WARRANTY; without even the implied warranty of
 *        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *        GNU General Public License for more details.
 *
 *        You should have received a copy of the GNU General Public License
 *        along with this program; if not, write to the Free Software
 *        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *        MA 02110-1301, USA.
 *
 * @link https://github.com/ovidiul/XCloner-Wordpress
 *
 * @modified 7/31/18 3:00 PM
 *
 */

use League\Flysystem\Config;
use League\Flysystem\Filesystem;
use League\Flysystem\Util;
use League\Flysystem\Adapter\Local;

use splitbrain\PHPArchive\Tar;
use splitbrain\PHPArchive\Zip;
use splitbrain\PHPArchive\Archive;
use splitbrain\PHPArchive\FileInfo;

/**
 * XCloner Api Class
 */
class Xcloner_Api
{
    private $xcloner_database;
    private $xcloner_settings;
    private $xcloner_file_system;
    private $xcloner_scheduler;
    private $xcloner_requirements;
    private $xcloner_sanitization;
    private $xcloner_encryption;
    private $xcloner_remote_storage;
    private $archive_system;
    private $form_params;
    private $logger;
    private $xcloner_container;

    /**
     * XCloner_Api construct class
     *
     * @param Xcloner $xcloner_container [description]
     */
    public function __construct(Xcloner $xcloner_container)
    {
        //global $wpdb;

        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_reporting(0);
        }

        if (ob_get_length()) {
            ob_end_clean();
        }
        ob_start();

        $this->xcloner_container = $xcloner_container;

        $this->xcloner_settings         = $xcloner_container->get_xcloner_settings();
        $this->logger                   = $xcloner_container->get_xcloner_logger()->withName("xcloner_api");
        $this->xcloner_file_system      = $xcloner_container->get_xcloner_filesystem();
        $this->xcloner_sanitization     = $xcloner_container->get_xcloner_sanitization();
        $this->xcloner_requirements     = $xcloner_container->get_xcloner_requirements();
        $this->archive_system           = $xcloner_container->get_archive_system();
        $this->xcloner_database         = $xcloner_container->get_xcloner_database();
        $this->xcloner_scheduler        = $xcloner_container->get_xcloner_scheduler();
        $this->xcloner_encryption       = $xcloner_container->get_xcloner_encryption();
        $this->xcloner_remote_storage   = $xcloner_container->get_xcloner_remote_storage();

        $this->xcloner_database->show_errors = false;

        if (isset($_POST['API_ID'])) {
            $this->logger->info("Processing ajax request ID ".substr(
                $this->xcloner_sanitization->sanitize_input_as_string($_POST['API_ID']),
                0,
                15
            ));
        }
    }

    /**
     * Get XCloner Container
     * @return XCloner return the XCloner container
     */
    public function get_xcloner_container()
    {
        return $this->xcloner_container;
    }


    /**
     * Checks API access
     */
    private function check_access()
    {
        if (function_exists('current_user_can') && !current_user_can('manage_options')) {
            $this->send_response(json_encode("Not allowed access here!"));
        }
    }

    /**
     * Initialize the database connection
     */
    public function init_db()
    {
        return;
    }

    /*
     * Save Schedule API
     */
    public function save_schedule()
    {
        //global $wpdb;

        $this->check_access();

        $scheduler = $this->xcloner_scheduler;
        $params = array();
        $schedule = array();
        $response = array();

        if (isset($_POST['data'])) {
            $params = json_decode(stripslashes($_POST['data']));
        }

        $this->process_params($params);

        if (isset($_POST['id'])) {
            $this->form_params['backup_params']['backup_name'] = $this->xcloner_sanitization->sanitize_input_as_string($_POST['backup_name']);
            $this->form_params['backup_params']['email_notification'] = $this->xcloner_sanitization->sanitize_input_as_string($_POST['email_notification']);
            if ($_POST['diff_start_date']) {
                $this->form_params['backup_params']['diff_start_date'] = strtotime($this->xcloner_sanitization->sanitize_input_as_string($_POST['diff_start_date']));
            } else {
                $this->form_params['backup_params']['diff_start_date'] = "";
            }
            $this->form_params['backup_params']['schedule_name'] = $this->xcloner_sanitization->sanitize_input_as_string($_POST['schedule_name']);
            $this->form_params['backup_params']['backup_encrypt'] = $this->xcloner_sanitization->sanitize_input_as_int($_POST['backup_encrypt']);
            
            $this->form_params['backup_params']['start_at'] = strtotime($_POST['schedule_start_date']);
            $this->form_params['backup_params']['schedule_frequency'] = $this->xcloner_sanitization->sanitize_input_as_string($_POST['schedule_frequency']);
            $this->form_params['backup_params']['schedule_storage'] = $this->xcloner_sanitization->sanitize_input_as_string($_POST['schedule_storage']);
            if(!isset($_POST['backup_delete_after_remote_transfer'])) {
                $_POST['backup_delete_after_remote_transfer'] = 0;
            }
            $this->form_params['backup_params']['backup_delete_after_remote_transfer'] = $this->xcloner_sanitization->sanitize_input_as_int($_POST['backup_delete_after_remote_transfer']);
            
            $this->form_params['database'] = (stripslashes($this->xcloner_sanitization->sanitize_input_as_raw($_POST['table_params'])));
            $this->form_params['excluded_files'] = (stripslashes($this->xcloner_sanitization->sanitize_input_as_raw($_POST['excluded_files'])));

            //$this->form_params['backup_params']['backup_type'] = $this->xcloner_sanitization->sanitize_input_as_string($_POST['backup_type']);

            $tables = explode(PHP_EOL, $this->form_params['database']);
            $return = array();

            foreach ($tables as $table) {
                $table = str_replace("\r", "", $table);
                $data = explode(".", $table);
                if (isset($data[1])) {
                    $return[$data[0]][] = $data[1];
                }
            }

            $this->form_params['database'] = ($return);

            $excluded_files = explode(PHP_EOL, $this->form_params['excluded_files']);
            $return = array();

            foreach ($excluded_files as $file) {
                $file = str_replace("\r", "", $file);
                if ($file) {
                    $return[] = $file;
                }
            }

            $this->form_params['excluded_files'] = ($return);

            $schedule['start_at'] = $this->form_params['backup_params']['start_at'];

            if (!isset($_POST['status'])) {
                $schedule['status'] = 0;
            } else {
                $schedule['status'] = $this->xcloner_sanitization->sanitize_input_as_int($_POST['status']);
            }
        } else {
            $schedule['status'] = 1;
            $schedule['start_at'] = strtotime($this->form_params['backup_params']['schedule_start_date'].
                " ".$this->form_params['backup_params']['schedule_start_time']);

            if ($schedule['start_at'] <= time()) {
                $schedule['start_at'] = "";
            }

            //fixing table names assigment
            foreach ($this->form_params['database'] as $db=>$tables) {
                if ($db == "#") {
                    continue;
                }

                foreach ($tables as $key=>$table) {
                    //echo $this->form_params['database'][$db][$key];
                    $this->form_params['database'][$db][$key] = substr($table, strlen($db)+1);
                }
            }
        }

        if (!$schedule['start_at']) {
            $schedule['start_at'] = date('Y-m-d H:i:s', time());
        } else {
            $schedule['start_at'] = date(
                'Y-m-d H:i:s',
                $schedule['start_at'] - ($this->xcloner_settings->get_xcloner_option('gmt_offset') * HOUR_IN_SECONDS)
            );
        }

        $schedule['name'] = $this->form_params['backup_params']['schedule_name'];
        $schedule['recurrence'] = $this->form_params['backup_params']['schedule_frequency'];
        if (!isset($this->form_params['backup_params']['schedule_storage'])) {
            $this->form_params['backup_params']['schedule_storage'] = "";
        }
        $schedule['remote_storage'] = $this->form_params['backup_params']['schedule_storage'];
        //$schedule['backup_type'] = $this->form_params['backup_params']['backup_type'];
        
        $schedule['params'] = json_encode($this->form_params);

        if (!isset($_POST['id'])) {
            $this->xcloner_database->insert(
                $this->xcloner_settings->get_table_prefix().'xcloner_scheduler',
                $schedule,
                array(
                    '%s',
                    '%s'
                )
            );
        } else {
            $this->xcloner_database->update(
                $this->xcloner_settings->get_table_prefix().'xcloner_scheduler',
                $schedule,
                array('id' => $_POST['id']),
                array(
                    '%s',
                    '%s'
                )
            );
        }
        if (isset($_POST['id'])) {
            $scheduler->update_cron_hook($_POST['id']);
        }

        if ($this->xcloner_database->last_error) {
            $response['error'] = 1;
            $response['error_message'] = $this->xcloner_database->last_error/*."--".$this->xcloner_database->last_query*/
            ;
        }

        $scheduler->update_wp_cron_hooks();
        $response['finished'] = 1;

        $this->send_response($response);
    }

    /*
     *
     * Backup Files API
     *
     */
    public function backup_files()
    {
        $return = array();
        $additional = array();

        $this->check_access();

        $params = json_decode(stripslashes($_POST['data']));

        $init = (int)$_POST['init'];

        if ($params === null) {
            return $this->send_response('{"status":false,"msg":"The post_data parameter must be valid JSON"}');
        }

        $this->process_params($params);

        $return['finished'] = 1;

        //$return = $this->archive_system->start_incremental_backup($this->form_params['backup_params'], $this->form_params['extra'], $init);
        try {
            $return = $this->archive_system->start_incremental_backup(
                $this->form_params['backup_params'],
                $this->form_params['extra'],
                $init
            );
        } catch (Exception $e) {
            $return = array();
            $return['error'] = true;
            $return['status'] = 500;
            $return['error_message'] = $e->getMessage();

            return $this->send_response($return, $hash = 1);
        }

        if ($return['finished']) {
            $return['extra']['backup_parent'] = $this->archive_system->get_archive_name_with_extension();
            if ($this->xcloner_file_system->is_part($this->archive_system->get_archive_name_with_extension())) {
                $return['extra']['backup_parent'] = $this->archive_system->get_archive_name_multipart();
            }
        }

        $data = $return;

        //check if backup is finished
        if ($return['finished']) {
            if (isset($this->form_params['backup_params']['email_notification']) and $to = $this->form_params['backup_params']['email_notification']) {
                try {
                    $from = "";
                    $subject = "";
                    $additional['lines_total'] = $return['extra']['lines_total'];
                    $this->archive_system->send_notification(
                        $to,
                        $from,
                        $subject,
                        $return['extra']['backup_parent'],
                        $this->form_params,
                        "",
                        $additional
                    );
                } catch (Exception $e) {
                    $this->logger->error($e->getMessage());
                }
            }
            $this->xcloner_file_system->remove_tmp_filesystem();
        }

        return $this->send_response($data, $hash = 1);
    }

    /*
     *
     * Backup Database API
     *
     */
    public function backup_database()
    {
        $data = array();

        $this->check_access();

        $params = json_decode(stripslashes($_POST['data']));

        $init = (int)$_POST['init'];

        if ($params === null) {
            return $this->send_response('{"status":false,"msg":"The post_data parameter must be valid JSON"}');
        }

        $this->process_params($params);

        //$xcloner_database = $this->init_db();
        $return = $this->xcloner_database->start_database_recursion(
            $this->form_params['database'],
            $this->form_params['extra'],
            $init
        );

        if (isset($return['error']) and $return['error']) {
            $data['finished'] = 1;
        } else {
            $data['finished'] = $return['finished'];
        }

        $data['extra'] = $return;

        return $this->send_response($data, $hash = 1);
    }

    /*
     *
     * Scan Filesystem API
     *
     */
    public function scan_filesystem()
    {
        $data = array();

        $this->check_access();

        $params = json_decode(stripslashes($_POST['data']));

        $init = (int)$_POST['init'];

        if ($params === null) {
            $this->send_response('{"status":false,"msg":"The post_data profile parameter must be valid JSON"}');
        }

        $this->process_params($params);

        $this->xcloner_file_system->set_excluded_files($this->form_params['excluded_files']);

        $return = $this->xcloner_file_system->start_file_recursion($init);

        $data["finished"] = !$return;
        $data["total_files_num"] = $this->xcloner_file_system->get_scanned_files_num();
        $data["last_logged_file"] = $this->xcloner_file_system->last_logged_file();
        $data["total_files_size"] = sprintf(
            "%.2f",
            $this->xcloner_file_system->get_scanned_files_total_size() / (1024 * 1024)
        );

        return $this->send_response($data, $hash = 1);
    }

    /*
     *
     * Process params sent by the user
     *
     */
    private function process_params($params)
    {
        if ($params && $params->processed) {
            $this->form_params = json_decode(json_encode((array)$params), true);
            return;
        }
        if (isset($params->hash)) {
            $this->xcloner_settings->set_hash($params->hash);
        }

        $this->form_params['extra'] = array();
        $this->form_params['backup_params'] = array();

        $this->form_params['database'] = array();

        if (isset($params->backup_params)) {
            foreach ($params->backup_params as $param) {
                $this->form_params['backup_params'][$param->name] = $this->xcloner_sanitization->sanitize_input_as_string($param->value);
                $this->logger->debug("Adding form parameter ".$param->name.".".$param->value."\n", array(
                    'POST',
                    'fields filter'
                ));
            }
        }

        $this->form_params['database'] = array();

        if (isset($params->table_params)) {
            foreach ($params->table_params as $param) {
                $this->form_params['database'][$param->parent][] = $this->xcloner_sanitization->sanitize_input_as_raw($param->id);
                $this->logger->debug("Adding database filter ".$param->parent.".".$param->id."\n", array(
                    'POST',
                    'database filter'
                ));
            }
        }

        $this->form_params['excluded_files'] = array();
        if (isset($params->files_params)) {
            foreach ($params->files_params as $param) {
                $this->form_params['excluded_files'][] = $this->xcloner_sanitization->sanitize_input_as_relative_path($param->id);
            }

            $unique_exclude_files = array();

            foreach ($params->files_params as $key => $param) {
                if (!in_array($param->parent, $this->form_params['excluded_files'])) {
                    //$this->form_params['excluded_files'][] = $this->xcloner_sanitization->sanitize_input_as_relative_path($param->id);
                    $unique_exclude_files[] = $param->id;
                    $this->logger->debug("Adding file filter ".$param->id."\n", array(
                        'POST',
                        'exclude files filter'
                    ));
                }
            }
            $this->form_params['excluded_files'] = (array)$unique_exclude_files;
        }

        //$this->form_params['excluded_files'] =  array_merge($this->form_params['excluded_files'], $this->exclude_files_by_default);

        if (isset($params->extra)) {
            foreach ($params->extra as $key => $value) {
                $this->form_params['extra'][$key] = $this->xcloner_sanitization->sanitize_input_as_raw($value);
            }
        }

        if (isset($this->form_params['backup_params']['diff_start_date']) and $this->form_params['backup_params']['diff_start_date']) {
            $this->form_params['backup_params']['diff_start_date'] = strtotime($this->form_params['backup_params']['diff_start_date']);
            $this->xcloner_file_system->set_diff_timestamp_start($this->form_params['backup_params']['diff_start_date']);
        }

        return $this->xcloner_settings->get_hash();
    }

    /*
     *
     * Get file list for tree view API
     *
     */
    public function get_file_system_action()
    {
        $this->check_access();

        $folder = $this->xcloner_sanitization->sanitize_input_as_relative_path($_POST['id']);

        $data = array();

        if ($folder == "#") {
            $folder = "/";
            $data[] = array(
                'id' => $folder,
                'parent' => '#',
                'text' => $this->xcloner_settings->get_xcloner_start_path(),
                //'children' => true,
                'state' => array('selected' => false, 'opened' => true),
                'icon' => plugin_dir_url(dirname(__FILE__))."/admin/assets/file-icon-root.png"
            );
        }

        try {
            $files = $this->xcloner_file_system->list_directory($folder);
        } catch (Exception $e) {
            print $e->getMessage();
            $this->logger->error($e->getMessage());

            return;
        }

        $type = array();
        foreach ($files as $key => $row) {
            $type[$key] = $row['type'];
        }
        array_multisort($type, SORT_ASC, $files);

        foreach ($files as $file) {
            $children = false;
            $text = $file['basename'];

            if ($file['type'] == "dir") {
                $children = true;
            } else {
                $text .= " (".$this->xcloner_requirements->file_format_size($file['size']).")";
            }

            if ($this->xcloner_file_system->is_excluded($file)) {
                $selected = true;
            } else {
                $selected = false;
            }

            $data[] = array(
                'id' => $file['path'],
                'parent' => $folder,
                'text' => $text,
                //'title' => "test",
                'children' => $children,
                'state' => array('selected' => $selected, 'opened' => false, "checkbox_disabled" => $selected),
                'icon' => plugin_dir_url(dirname(__FILE__))."/admin/assets/file-icon-".strtolower(substr(
                    $file['type'],
                    0,
                    1
                )).".png"
            );
        }


        return $this->send_response($data, 0);
    }

    /*
     *
     * Get databases/tables list for frontend tree display API
     *
     */
    public function get_database_tables_action()
    {
        $this->check_access();

        $database = $this->xcloner_sanitization->sanitize_input_as_raw($_POST['id']);

        $data = array();

        $xcloner_backup_only_wp_tables = $this->xcloner_settings->get_xcloner_option('xcloner_backup_only_wp_tables');

        if ($database == "#") {
            try {
                $return = $this->xcloner_database->get_all_databases();
            } catch (Exception $e) {
                $this->logger->error($e->getMessage());
            }

            foreach ($return as $database) {
                if ($xcloner_backup_only_wp_tables and $database['name'] != $this->xcloner_settings->get_db_database()) {
                    continue;
                }

                $state = array();

                if ($database['name'] == $this->xcloner_settings->get_db_database()) {
                    $state['selected'] = true;
                    if ($database['num_tables'] < 25) {
                        $state['opened'] = false;
                    }
                }

                $data[] = array(
                    'id' => $database['name'],
                    'parent' => '#',
                    'text' => $database['name']." (".(int)$database['num_tables'].")",
                    'children' => true,
                    'state' => $state,
                    'icon' => plugin_dir_url(dirname(__FILE__))."/admin/assets/database-icon.png"
                );
            }
        } else {
            try {
                $return = $this->xcloner_database->list_tables($database, "", 1);
            } catch (Exception $e) {
                $this->logger->error($e->getMessage());
            }

            foreach ($return as $table) {
                $state = array();

                if ($xcloner_backup_only_wp_tables and !stristr(
                    $table['name'],
                    $this->xcloner_settings->get_table_prefix()
                )) {
                    continue;
                }

                if (isset($database['name']) and $database['name'] == $this->xcloner_settings->get_db_database()) {
                    $state = array('selected' => true);
                }

                $data[] = array(
                    'id' => $database.".".$table['name'],
                    'parent' => $database,
                    'text' => $table['name']." (".(int)$table['records'].")",
                    'children' => false,
                    'state' => $state,
                    'icon' => plugin_dir_url(dirname(__FILE__))."/admin/assets/table-icon.png"
                );
            }
        }

        return $this->send_response($data, 0);
    }

    /*
     *
     * Get schedule by id API
     *
     */
    public function get_schedule_by_id()
    {
        $this->check_access();

        $schedule_id = $this->xcloner_sanitization->sanitize_input_as_int($_GET['id']);
        $scheduler = $this->xcloner_scheduler;
        $data = $scheduler->get_schedule_by_id($schedule_id);

        $data['start_at'] = date(
            "Y-m-d H:i",
            strtotime($data['start_at']) + ($this->xcloner_settings->get_xcloner_option('gmt_offset') * HOUR_IN_SECONDS)
        );
        if (isset($data['backup_params']->diff_start_date) && $data['backup_params']->diff_start_date != "") {
            $data['backup_params']->diff_start_date = date("Y-m-d", ($data['backup_params']->diff_start_date));
        }

        return $this->send_response($data);
    }

    /*
     *
     * Get Schedule list API
     *
     */
    public function get_scheduler_list()
    {
        $return = array();

        $this->check_access();

        $scheduler = $this->xcloner_scheduler;
        $data = $scheduler->get_scheduler_list();
        $return['data'] = array();

        foreach ($data as $res) {
            $action = "<a href=\"#".$res->id."\" class=\"edit\" title='Edit'> <i class=\"material-icons \">edit</i></a>
					<a href=\"#" . $res->id."\" class=\"delete\" title='Delete'><i class=\"material-icons  \">delete</i></a>";
            if ($res->status) {
                $status = '<i class="material-icons active status">timer</i>';
            } else {
                $status = '<i class="material-icons status inactive">timer_off</i>';
            }

            $next_run_time = wp_next_scheduled('xcloner_scheduler_'.$res->id, array($res->id));

            $next_run = date($this->xcloner_settings->get_xcloner_option('date_format')." ".$this->xcloner_settings->get_xcloner_option('time_format'), $next_run_time);

            $remote_storage = $res->remote_storage;

            if (!$next_run_time >= time()) {
                $next_run = " ";
            }

            if (trim($next_run)) {
                $date_text = date(
                    $this->xcloner_settings->get_xcloner_option('date_format')." ".$this->xcloner_settings->get_xcloner_option('time_format'),
                    $next_run_time + ($this->xcloner_settings->get_xcloner_option('gmt_offset') * HOUR_IN_SECONDS)
                );

                if ($next_run_time >= time()) {
                    $next_run = "in ".human_time_diff($next_run_time, time());
                } else {
                    $next_run = __("executed", 'xcloner-backup-and-restore');
                }

                $next_run = "<a href='#' title='".$date_text."'>".$next_run."</a>";
                //$next_run .=" ($date_text)";
            }

            $backup_text = "";
            $backup_size = "";
            $backup_time = "";

            if ($res->last_backup) {
                if ($this->xcloner_file_system->get_storage_filesystem()->has($res->last_backup)) {
                    $metadata = $this->xcloner_file_system->get_storage_filesystem()->getMetadata($res->last_backup);
                    $backup_size = size_format($this->xcloner_file_system->get_backup_size($res->last_backup));
                    $backup_time = date(
                        $this->xcloner_settings->get_xcloner_option('date_format')." ".$this->xcloner_settings->get_xcloner_option('time_format'),
                        $metadata['timestamp'] + ($this->xcloner_settings->get_xcloner_option('gmt_offset') * HOUR_IN_SECONDS)
                    );
                }

                $backup_text = "<span title='".$backup_time."' class='shorten_string'>".$res->last_backup." (".$backup_size.")</span>";
            }

            $schedules = $this->xcloner_scheduler->get_available_intervals();

            if (isset($schedules[$res->recurrence])) {
                $res->recurrence = $schedules[$res->recurrence]['display'];
            }

            $return['data'][] = array(
                $res->id,
                $res->name,
                $res->recurrence, /*$res->start_at,*/
                $next_run,
                $remote_storage,
                $backup_text,
                $status,
                $action
            );
        }

        return $this->send_response($return, 0);
    }

    /*
     *
     * Delete Schedule by ID API
     *
     */
    public function delete_schedule_by_id()
    {
        $data = array();

        $this->check_access();

        $schedule_id = $this->xcloner_sanitization->sanitize_input_as_int($_GET['id']);
        $scheduler = $this->xcloner_scheduler;
        $data['finished'] = $scheduler->delete_schedule_by_id($schedule_id);

        return $this->send_response($data);
    }

    /*
     *
     * Delete backup by name from the storage path
     *
     */
    public function delete_backup_by_name()
    {
        $data = array();

        $this->check_access();

        $backup_name = $this->xcloner_sanitization->sanitize_input_as_string($_POST['name']);
        $storage_selection = $this->xcloner_sanitization->sanitize_input_as_string($_POST['storage_selection']);

        $data['finished'] = $this->xcloner_file_system->delete_backup_by_name($backup_name, $storage_selection);

        return $this->send_response($data);
    }

    /**
     *  API Incremental Backup Encryption Method
     */
    public function backup_encryption()
    {
        $this->check_access();

        $backup_parts = array();
        $return = array();


        if (isset($_POST['data'])) {
            $params = json_decode(stripslashes($_POST['data']));

            $this->process_params($params);
            $source_backup_file = $this->xcloner_sanitization->sanitize_input_as_string($this->form_params['extra']['backup_parent']);

            if (isset($this->form_params['extra']['start'])) {
                $start = $this->xcloner_sanitization->sanitize_input_as_int($this->form_params['extra']['start']);
            } else {
                $start = 0;
            }

            if (isset($this->form_params['extra']['iv'])) {
                $iv = $this->xcloner_sanitization->sanitize_input_as_raw($this->form_params['extra']['iv']);
            } else {
                $iv = "";
            }

            if (isset($this->form_params['extra']['part'])) {
                $return['part'] = (int)$this->xcloner_sanitization->sanitize_input_as_int($this->form_params['extra']['part']);
            } else {
                $return['part'] = 0;
            }
        } else {
            $source_backup_file = $this->xcloner_sanitization->sanitize_input_as_string($_POST['file']);
            $start = $this->xcloner_sanitization->sanitize_input_as_int($_POST['start']);
            $iv = $this->xcloner_sanitization->sanitize_input_as_raw($_POST['iv']);
            $return['part'] = (int)$this->xcloner_sanitization->sanitize_input_as_int($_POST['part']);
        }

        $backup_file = $source_backup_file;

        if ($this->xcloner_file_system->is_multipart($backup_file)) {
            $backup_parts = $this->xcloner_file_system->get_multipart_files($backup_file);
            $backup_file = $backup_parts[$return['part']];
        }

        $return['processing_file'] = $backup_file;
        $return['total_size'] = filesize($this->xcloner_settings->get_xcloner_store_path().DS.$backup_file);

        try {
            $this->logger->info(json_encode($_POST));
            $this->logger->info($iv);
            $return = array_merge(
                $return,
                $this->xcloner_encryption->encrypt_file($backup_file, "", "", $start, base64_decode($iv))
            );
        } catch (\Exception $e) {
            $return['error'] = true;
            $return['message'] = $e->getMessage();
            $return['error_message'] = $e->getMessage();
        }

        //echo strlen($return['iv']);exit;

        if (isset($return['finished']) && $return['finished']) {
            if ($this->xcloner_file_system->is_multipart($source_backup_file)) {
                $return['start'] = 0;

                ++$return['part'];

                if ($return['part'] < sizeof($backup_parts)) {
                    $return['finished'] = 0;
                }
            }
        }

        if (isset($_POST['data'])) {
            $return['extra'] = array_merge($this->form_params['extra'], $return);
        }

        $this->send_response($return, 0);
    }

    /**
     *  API Incremental Backup Decryption Method
     */
    public function backup_decryption()
    {
        $this->check_access();

        $backup_parts = array();
        $return = array();

        $source_backup_file = $this->xcloner_sanitization->sanitize_input_as_string($_POST['file']);
        $start = $this->xcloner_sanitization->sanitize_input_as_int($_POST['start']);
        $iv = $this->xcloner_sanitization->sanitize_input_as_raw($_POST['iv']);
        $decryption_key = $this->xcloner_sanitization->sanitize_input_as_raw($_POST['decryption_key']);
        ;
        $return['part'] = $this->xcloner_sanitization->sanitize_input_as_int($_POST['part']);

        $backup_file = $source_backup_file;

        if ($this->xcloner_file_system->is_multipart($backup_file)) {
            $backup_parts = $this->xcloner_file_system->get_multipart_files($backup_file);
            $backup_file = $backup_parts[$return['part']];
        }

        $return['processing_file'] = $backup_file;
        $return['total_size'] = filesize($this->xcloner_settings->get_xcloner_store_path().DS.$backup_file);

        try {
            $return = array_merge(
                $return,
                $this->xcloner_encryption->decrypt_file($backup_file, "", $decryption_key, $start, base64_decode($iv))
            );
        } catch (\Exception $e) {
            $return['error'] = true;
            $return['message'] = $e->getMessage();
        }

        if ($return['finished']) {
            if ($this->xcloner_file_system->is_multipart($source_backup_file)) {
                $return['start'] = 0;

                ++$return['part'];

                if ($return['part'] < sizeof($backup_parts)) {
                    $return['finished'] = 0;
                }
            }
        }

        $this->send_response($return, 0);
    }

    public function get_manage_backups_list()
    {
        $this->check_access();

        $return = array(
            "data" => array()
        );

        $storage_selection = "";

        if (isset($_GET['storage_selection']) and $_GET['storage_selection']) {
            $storage_selection = $this->xcloner_sanitization->sanitize_input_as_string($_GET['storage_selection']);
        }
        $available_storages = $this->xcloner_remote_storage->get_available_storages();

        try {
            $backup_list = $this->xcloner_file_system->get_backup_archives_list($storage_selection);
        }catch(Exception $e){
            $this->send_response($return, 0);
            return;
        }

        $i = -1;
        foreach ($backup_list as $file_info):?>
            <?php
            if ($storage_selection == "gdrive") {
                $file_info['path'] = $file_info['filename'].".".$file_info['extension'];
            }
        $file_exists_on_local_storage = true;

        if ($storage_selection) {
            if (!$this->xcloner_file_system->get_storage_filesystem()->has($file_info['path'])) {
                $file_exists_on_local_storage = false;
            }
        } ?>
            <?php if (!isset($file_info['parent'])): ?>

                <?php ob_start(); ?>
                        <p>
                        <label for="checkbox_<?php echo $i ?>">
                            <input name="backup[]" value="<?php echo $file_info['basename'] ?>" type="checkbox"
                                   id="checkbox_<?php echo ++$i ?>">
                            <span>&nbsp;</span>
                        </label>
                        </p>
                <?php
                $return['data'][$i][] = ob_get_contents();
        ob_end_clean(); ?>

                <?php ob_start(); ?>
                        <span class=""><?php echo $file_info['path'] ?></span>
                        <?php if (!$file_exists_on_local_storage): ?>
                            <a href="#"
                               title="<?php echo __(
            "File does not exists on local storage",
            "xcloner-backup-and-restore"
        ) ?>"><i
                                        class="material-icons backup_warning">warning</i></a>
                        <?php endif ?>
                        <?php
                        if (isset($file_info['childs']) and is_array($file_info['childs'])):
                            ?>
                            <a href="#" title="expand" class="expand-multipart add"><i
                                        class="material-icons">add</i></a>
                            <a href="#" title="collapse" class="expand-multipart remove"><i class="material-icons">remove</i></a>
                            <ul class="multipart">
                                <?php foreach ($file_info['childs'] as $child): ?>
                                    <li>
                                        <?php echo $child[0] ?> (<?php echo esc_html(size_format($child[2])) ?>)
                                        <?php
                                        $child_exists_on_local_storage = true;
        if ($storage_selection) {
            if (!$this->xcloner_file_system->get_storage_filesystem()->has($child[0])) {
                $child_exists_on_local_storage = false;
            }
        } ?>
                                        <?php if (!$child_exists_on_local_storage): ?>
                                            <a href="#"
                                               title="<?php echo __(
            "File does not exists on local storage",
            "xcloner-backup-and-restore"
        ) ?>"><i
                                                        class="material-icons backup_warning">warning</i></a>
                                        <?php endif ?>
                                        <?php if (!$storage_selection) : ?>
                                            <a href="#<?php echo $child[0]; ?>" class="download"
                                               title="Download Backup"><i class="material-icons">file_download</i></a>

                                            <?php if ($this->xcloner_encryption->is_encrypted_file($child[0])) :?>
                                                <a href="#<?php echo $child[0] ?>" class="backup-decryption"
                                                   title="<?php echo __('Backup Decryption', 'xcloner-backup-and-restore') ?>">
                                                    <i class="material-icons">enhanced_encryption</i>
                                                </a>
                                            <?php else: ?>
                                                <a href="#<?php echo $child[0] ?>" class="list-backup-content"
                                                   title="<?php echo __(
            'List Backup Content',
            'xcloner-backup-and-restore'
        ) ?>"><i
                                                            class="material-icons">folder_open</i></a>

                                                <a href="#<?php echo $child[0] ?>" class="backup-encryption"
                                                   title="<?php echo __('Backup Encryption', 'xcloner-backup-and-restore') ?>">
                                                    <i class="material-icons">no_encryption</i>
                                                </a>
                                            <?php endif?>

                                        <?php elseif ($storage_selection != "gdrive" && !$this->xcloner_file_system->get_storage_filesystem()->has($child[0])): ?>
                                            <a href="#<?php echo $child[0] ?>" class="copy-remote-to-local"
                                               title="<?php echo __(
            'Push Backup To Local Storage',
            'xcloner-backup-and-restore'
        ) ?>"><i
                                                        class="material-icons">file_upload</i></a>
                                        <?php endif ?>
                                    </li>
                                <?php endforeach; ?>
                            </ul>
                        <?php endif; ?>
                <?php
                $return['data'][$i][] = ob_get_contents();
        ob_end_clean(); ?>
                    <?php ob_start(); ?>
                        <?php if (isset($file_info['timestamp'])) {
            echo date("Y-m-d H:i", $file_info['timestamp']);
        } ?>
                    <?php
                        $return['data'][$i][] = ob_get_contents();
        ob_end_clean(); ?>

                    <?php ob_start(); ?>
                        <?php echo esc_html(size_format($file_info['size'])) ?>
                    <?php
                        $return['data'][$i][] = ob_get_contents();
        ob_end_clean(); ?>

                    <?php ob_start(); ?>
                        <?php if (!$storage_selection): ?>
                            <a href="#<?php echo $file_info['basename']; ?>" class="download"
                               title="<?php echo __('Download Backup', 'xcloner-backup-and-restore') ?>"><i
                                        class="material-icons">file_download</i></a>

                            <?php if (sizeof($available_storages)): ?>
                                <a href="#<?php echo $file_info['basename'] ?>" class="cloud-upload"
                                   title="<?php echo __(
            'Send Backup To Remote Storage',
            'xcloner-backup-and-restore'
        ) ?>"><i
                                            class="material-icons">cloud_upload</i></a>
                            <?php endif ?>
                            <?php
                            $basename = $file_info['basename'];
        if (isset($file_info['childs']) and sizeof($file_info['childs'])) {
            $basename = $file_info['childs'][0][0];
        } ?>
                            <?php if ($this->xcloner_encryption->is_encrypted_file($basename)) :?>
                                <a href="#<?php echo $file_info['basename'] ?>" class="backup-decryption"
                                   title="<?php echo __('Backup Decryption', 'xcloner-backup-and-restore') ?>">
                                    <i class="material-icons">enhanced_encryption</i>
                                </a>
                            <?php else: ?>
                                <a href="#<?php echo $file_info['basename'] ?>" class="list-backup-content"
                                    title="<?php echo __('List Backup Content', 'xcloner-backup-and-restore') ?>"><i
                                    class="material-icons">folder_open</i></a>

                                <a href="#<?php echo $file_info['basename'] ?>" class="backup-encryption"
                                   title="<?php echo __('Backup Encryption', 'xcloner-backup-and-restore') ?>">
                                    <i class="material-icons">no_encryption</i>
                                </a>
                            <?php endif?>
                        <?php endif; ?>

                        <a href="#<?php echo $file_info['basename'] ?>" class="delete"
                           title="<?php echo __('Delete Backup', 'xcloner-backup-and-restore') ?>">
                            <i class="material-icons">delete</i>
                        </a>
                        <?php if ($storage_selection and !$file_exists_on_local_storage): ?>
                            <a href="#<?php echo $file_info['basename']; ?>" class="copy-remote-to-local"
                               title="<?php echo __('Push Backup To Local Storage', 'xcloner-backup-and-restore') ?>"><i
                                        class="material-icons">file_upload</i></a>
                        <?php endif ?>

                    <?php
                        $return['data'][$i][] = ob_get_contents();
        ob_end_clean(); ?>

            <?php endif ?>
        <?php endforeach ?>
    <?php
        $this->send_response($return, 0);
    }

    /**
     * API method to list internal backup files
     */
    public function list_backup_files()
    {
        $this->check_access();

        $backup_parts = array();
        $return = array();

        $source_backup_file = $this->xcloner_sanitization->sanitize_input_as_string($_POST['file']);
        
        $start = $this->xcloner_sanitization->sanitize_input_as_int($_POST['start']);
        $return['part'] = $this->xcloner_sanitization->sanitize_input_as_int($_POST['part']);

        $backup_file = $source_backup_file;

        if ($this->xcloner_file_system->is_multipart($backup_file)) {
            $backup_parts = $this->xcloner_file_system->get_multipart_files($backup_file);
            $backup_file = $backup_parts[$return['part']];
        }

        if ($this->xcloner_encryption->is_encrypted_file($backup_file)) {
            $return['error'] = true;
            $return['message'] = __("Backup archive is encrypted, please decrypt it first before you can list it's content.", "xcloner-backup-and-restore");
            $this->send_response($return, 0);
        }

        try {
            $tar = $this->archive_system;
            $tar->open($this->xcloner_settings->get_xcloner_store_path().DS.$backup_file, $start);

            $data = $tar->contents($this->xcloner_settings->get_xcloner_option('xcloner_files_to_process_per_request'));
        } catch (Exception $e) {
            $return['error'] = true;
            $return['message'] = $e->getMessage();
            $this->send_response($return, 0);
        }

        $return['files'] = array();
        $return['finished'] = 1;
        $return['total_size'] = filesize($this->xcloner_settings->get_xcloner_store_path().DS.$backup_file);
        $i = 0;

        if (isset($data['extracted_files']) and is_array($data['extracted_files'])) {
            foreach ($data['extracted_files'] as $file) {
                $return['files'][$i]['path'] = $file->getPath();
                $return['files'][$i]['size'] = $file->getSize();
                $return['files'][$i]['mtime'] = date(
                    $this->xcloner_settings->get_xcloner_option('date_format')." ".$this->xcloner_settings->get_xcloner_option('time_format'),
                    $file->getMtime()
                );

                $i++;
            }
        }

        if (isset($data['start'])) {
            $return['start'] = $data['start'];
            $return['finished'] = 0;
        } else {
            if ($this->xcloner_file_system->is_multipart($source_backup_file)) {
                $return['start'] = 0;

                ++$return['part'];

                if ($return['part'] < sizeof($backup_parts)) {
                    $return['finished'] = 0;
                }
            }
        }

        $this->send_response($return, 0);
    }

    /*
     * Copy remote backup to local storage
     */
    public function copy_backup_remote_to_local()
    {
        $this->check_access();

        $backup_file = $this->xcloner_sanitization->sanitize_input_as_string($_POST['file']);
        $storage_type = $this->xcloner_sanitization->sanitize_input_as_string($_POST['storage_type']);

        $xcloner_remote_storage = $this->get_xcloner_container()->get_xcloner_remote_storage();

        $return = array();

        try {
            if (method_exists($xcloner_remote_storage, "copy_backup_remote_to_local")) {
                $return = call_user_func_array(array(
                    $xcloner_remote_storage,
                    "copy_backup_remote_to_local"
                ), array($backup_file, $storage_type));
            }
        } catch (Exception $e) {
            $return['error'] = 1;
            $return['message'] = $e->getMessage();
        }

        if (!$return) {
            $return['error'] = 1;
            $return['message'] = "Upload failed, please check the error log for more information!";
        }


        $this->send_response($return, 0);
    }

    /*
     *
     * Upload backup to remote API
     *
     */
    public function upload_backup_to_remote()
    {
        $this->check_access();

        $return = array();

        $backup_file = $this->xcloner_sanitization->sanitize_input_as_string($_POST['file']);
        $storage_type = $this->xcloner_sanitization->sanitize_input_as_string($_POST['storage_type']);
        $delete_local_copy_after_transfer = $this->xcloner_sanitization->sanitize_input_as_string($_POST['delete_after_transfer']);

        $xcloner_remote_storage = $this->get_xcloner_container()->get_xcloner_remote_storage();

        try {
            if (method_exists($xcloner_remote_storage, "upload_backup_to_storage")) {
                $return = call_user_func_array(array(
                    $xcloner_remote_storage,
                    "upload_backup_to_storage"
                ), array($backup_file, $storage_type, $delete_local_copy_after_transfer));
            }
        } catch (Exception $e) {
            $return['error'] = 1;
            $return['message'] = $e->getMessage();
        }

        if (!$return) {
            $return['error'] = 1;
            $return['message'] = "Upload failed, please check the error log for more information!";
        }


        $this->send_response($return, 0);
    }

    /*
     *
     * Remote Storage Status Save
     *
     */
    public function remote_storage_save_status()
    {
        $this->check_access();

        $return = array();

        $xcloner_remote_storage = $this->get_xcloner_container()->get_xcloner_remote_storage();

        $return['finished'] = $xcloner_remote_storage->change_storage_status($_POST['id'], $_POST['value']);

        $this->send_response($return, 0);
    }


    public function download_restore_script()
    {
        $this->check_access();

        ob_end_clean();

        $adapter = new Local(dirname(__DIR__), LOCK_EX, '0001');
        $xcloner_plugin_filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        /* Generate PHAR FILE
        $file = 'restore/vendor.built';

        if(file_exists($file))
            unlink($file);
        $phar2 = new Phar($file, 0, 'vendor.phar');

        // add all files in the project, only include php files
        $phar2->buildFromIterator(
            new RecursiveIteratorIterator(
             new RecursiveDirectoryIterator(__DIR__.'/vendor/')),
            __DIR__);

        $phar2->setStub($phar2->createDefaultStub('vendor/autoload.php', 'vendor/autoload.php'));
         * */

        $tmp_file = $this->xcloner_settings->get_xcloner_tmp_path().DS."xcloner-restore.tgz";

        $tar = $this->archive_system;
        $tar->create($tmp_file);

        $tar->addFile(__DIR__."/../../../../restore/vendor.build.txt", "vendor.phar");
        //$tar->addFile(dirname(__DIR__)."/restore/vendor.tgz", "vendor.tgz");

        $files = $xcloner_plugin_filesystem->listContents("vendor/", true);
        foreach ($files as $file) {
            $tar->addFile(dirname(__DIR__).DS.$file['path'], $file['path']);
        }

        $content = file_get_contents(__DIR__."/../../../../restore/xcloner_restore.php");
        //$content = file_get_contents(dirname(__DIR__)."/restore/xcloner_restore.php");
        $content = str_replace("define('AUTH_KEY', '');", "define('AUTH_KEY', '".md5(AUTH_KEY)."');", $content);

        $tar->addData("xcloner_restore.php", $content);

        $tar->close();

        if (file_exists($tmp_file)) {
            header('Content-Description: File Transfer');
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename="'.basename($tmp_file).'"');
            header('Expires: 0');
            header('Cache-Control: must-revalidate');
            header('Pragma: public');
            header('Content-Length: '.filesize($tmp_file));
            readfile($tmp_file);
        }

        try {
            unlink($tmp_file);
        } catch (Exception $e) {
            //We are not interested in the error here
        }

        die();
    }

    /*
     *
     * Download backup by Name from the Storage Path
     *
     */
    public function download_backup_by_name()
    {
        $this->check_access();

        ob_end_clean();

        $backup_name = $this->xcloner_sanitization->sanitize_input_as_string($_GET['name']);


        $metadata = $this->xcloner_file_system->get_storage_filesystem()->getMetadata($backup_name);
        $read_stream = $this->xcloner_file_system->get_storage_filesystem()->readStream($backup_name);


        header('Pragma: public');
        header('Expires: 0');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header('Cache-Control: private', false);
        header('Content-Transfer-Encoding: binary');
        header('Content-Disposition: attachment; filename="'.$metadata['path'].'";');
        header('Content-Type: application/octet-stream');
        header('Content-Length: '.$metadata['size']);

        ob_end_clean();

        $chunkSize = 1024 * 1024;
        while (!feof($read_stream)) {
            $buffer = fread($read_stream, $chunkSize);
            echo $buffer;
        }
        fclose($read_stream);

        wp_die();
    }

    /*
     * Restore upload backup
     */
    public function restore_upload_backup()
    {
        $this->check_access();

        $return = array();

        $return['part'] = 0;
        $return['total_parts'] = 0;
        $return['uploaded_size'] = 0;
        $is_multipart = 0;

        $file = $this->xcloner_sanitization->sanitize_input_as_string($_POST['file']);
        $hash = $this->xcloner_sanitization->sanitize_input_as_string($_POST['hash']);

        if (isset($_POST['part'])) {
            $return['part'] = $this->xcloner_sanitization->sanitize_input_as_int($_POST['part']);
        }

        if (isset($_POST['uploaded_size'])) {
            $return['uploaded_size'] = $this->xcloner_sanitization->sanitize_input_as_int($_POST['uploaded_size']);
        }

        $start = $this->xcloner_sanitization->sanitize_input_as_string($_POST['start']);
        $target_url = $this->xcloner_sanitization->sanitize_input_as_string($_POST['target_url']);

        $return['total_size'] = $this->xcloner_file_system->get_backup_size($file);

        if ($this->xcloner_file_system->is_multipart($file)) {
            $backup_parts = $this->xcloner_file_system->get_multipart_files($file);

            $return['total_parts'] = sizeof($backup_parts) + 1;

            if ($return['part'] and isset($backup_parts[$return['part'] - 1])) {
                $file = $backup_parts[$return['part'] - 1];
            }

            $is_multipart = 1;
        }

        try {
            $xcloner_file_transfer = $this->get_xcloner_container()->get_xcloner_file_transfer();
            $xcloner_file_transfer->set_target($target_url);
            $return['start'] = $xcloner_file_transfer->transfer_file($file, $start, $hash);
        } catch (Exception $e) {
            $return = array();
            $return['error'] = true;
            $return['status'] = 500;
            $return['message'] = "CURL communication error with the restore host. ".$e->getMessage();
            $this->send_response($return, 0);
        }

        $return['status'] = 200;

        //we have finished the upload
        if (!$return['start'] and $is_multipart) {
            $return['part']++;
            $return['uploaded_size'] += $this->xcloner_file_system->get_storage_filesystem()->getSize($file);
        }

        $this->send_response($return, 0);
    }

    /*
     * Restore backup
     */
    public function restore_backup()
    {
        $this->check_access();

        define("XCLONER_PLUGIN_ACCESS", 1);
        include_once(dirname(__DIR__).DS."restore".DS."xcloner_restore.php");

        return;
    }

    /*
     *
     * Send the json response back
     *
     */
    private function send_response($data, $attach_hash = 1)
    {
        if ($attach_hash and null !== $this->xcloner_settings->get_hash()) {
            $data['hash'] = $this->xcloner_settings->get_hash();
        }

        if (ob_get_length()) {
            //ob_clean();
        }
        
        return wp_send_json($data);
    }
}
<?php

namespace watchfulli\XClonerCore;

class Xcloner{
    
}
<?php
namespace watchfulli\XClonerCore;

/**
 * XCloner - Backup and Restore backup plugin for Wordpress
 *
 * class-xcloner-file-system.php
 * @author Liuta Ovidiu <info@thinkovi.com>
 *
 *        This program is free software; you can redistribute it and/or modify
 *        it under the terms of the GNU General Public License as published by
 *        the Free Software Foundation; either version 2 of the License, or
 *        (at your option) any later version.
 *
 *        This program is distributed in the hope that it will be useful,
 *        but WITHOUT ANY WARRANTY; without even the implied warranty of
 *        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *        GNU General Public License for more details.
 *
 *        You should have received a copy of the GNU General Public License
 *        along with this program; if not, write to the Free Software
 *        Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *        MA 02110-1301, USA.
 *
 * @link https://github.com/ovidiul/XCloner-Wordpress
 *
 * @modified 7/31/18 3:46 PM
 *
 */

use League\Flysystem\Config;
use League\Flysystem\Filesystem;
use League\Flysystem\Util;
use League\Flysystem\Adapter\Local;

/**
 * Class Xcloner_File_System
 */
class Xcloner_File_System
{
    private $excluded_files = "";
    private $additional_regex_patterns = array();
    private $excluded_files_by_default = array("administrator/backups", "wp-content/backups");
    private $included_files_handler = "backup_files.csv";
    private $temp_dir_handler = ".dir";
    public $filesystem;
    public $tmp_filesystem;
    public $storage_filesystem;
    private $xcloner_container;
    private $diff_timestamp_start = "";

    private $logger;
    private $start_adapter;
    private $tmp_adapter;
    private $storage_adapter;
    private $xcloner_settings;
    private $start_filesystem;
    private $tmp_filesystem_append;
    private $storage_filesystem_append;

    private $files_counter;
    private $files_size;
    private $last_logged_file;
    private $folders_to_process_per_session = 25;
    private $backup_archive_extensions = array("tar", "tgz", "tar.gz", "gz", "csv", "encrypted", "decrypted");
    private $backup_name_tags = array('[time]', '[hostname]', '[domain]', '[hash]');

    /**
     * Xcloner_File_System constructor.
     * @param Xcloner $xcloner_container
     * @param string $hash
     */
    public function __construct(Xcloner $xcloner_container, $hash = "")
    {
        $this->xcloner_container = $xcloner_container;

        $this->logger = $xcloner_container->get_xcloner_logger()->withName("xcloner_file_system");
        $this->xcloner_settings = $xcloner_container->get_xcloner_settings();

        try {
            $this->start_adapter = new Local($this->xcloner_settings->get_xcloner_start_path(), LOCK_EX, '0001');
            $this->start_filesystem = new Filesystem($this->start_adapter, new Config([
                'disable_asserts' => true,
            ]));

            $this->tmp_adapter = new Local($this->xcloner_settings->get_xcloner_tmp_path(), LOCK_EX, '0001');
            $this->tmp_filesystem = new Filesystem($this->tmp_adapter, new Config([
                'disable_asserts' => true,
            ]));
            $adapter = new Local($this->xcloner_settings->get_xcloner_tmp_path(), LOCK_EX | FILE_APPEND, '0001');
            $this->tmp_filesystem_append = new Filesystem($adapter, new Config([
                'disable_asserts' => true,
            ]));

            $adapter = new Local($this->xcloner_settings->get_xcloner_store_path(), LOCK_EX, '0001');
            $this->storage_filesystem = new Filesystem($adapter, new Config([
                'disable_asserts' => true,
            ]));

            $this->storage_adapter = new Local(
                $this->xcloner_settings->get_xcloner_store_path(),
                FILE_APPEND,
                '0001'
            );
            $this->storage_filesystem_append = new Filesystem($this->storage_adapter, new Config([
                'disable_asserts' => true,
            ]));
        } catch (Exception $e) {
            $this->logger->error("Filesystem Initialization Error: ".$e->getMessage());
        }


        if ($value = $this->xcloner_settings->get_xcloner_option('xcloner_directories_to_scan_per_request')) {
            $this->folders_to_process_per_session = $value;
        }
    }

    /**
     * Set differential timestamp date
     * @param string $timestamp
     */
    public function set_diff_timestamp_start($timestamp = "")
    {
        if ($timestamp) {
            $this->logger->info(sprintf("Setting Differential Timestamp To %s", date("Y-m-d", $timestamp)), array(
                "FILESYSTEM",
                "DIFF"
            ));
            $this->diff_timestamp_start = $timestamp;
        }
    }

    /**
     * Gets the differential timestamp date
     * @return string
     */
    public function get_diff_timestamp_start()
    {
        return $this->diff_timestamp_start;
    }

    private function get_xcloner_container()
    {
        return $this->xcloner_container;
    }

    public function set_hash($hash)
    {
        $this->xcloner_settings->set_hash($hash);
    }

    public function get_hash($hash)
    {
        $this->xcloner_settings->get_hash();
    }

    public function get_tmp_filesystem()
    {
        return $this->tmp_filesystem;
    }

    public function get_storage_filesystem($remote_storage_selection = "")
    {
        if ($remote_storage_selection != "") {
            $remote_storage = $this->get_xcloner_container()->get_xcloner_remote_storage();
            $method = "get_".$remote_storage_selection."_filesystem";

            if (!method_exists($remote_storage, $method)) {
                return false;
            }

            list($adapter, $filesystem) = $remote_storage->$method();

            return $filesystem;
        }

        return $this->storage_filesystem;
    }

    public function get_tmp_filesystem_adapter()
    {
        return $this->tmp_adapter;
    }

    public function get_tmp_filesystem_append()
    {
        return $this->tmp_filesystem_append;
    }

    public function get_start_adapter()
    {
        return $this->start_adapter;
    }

    public function get_start_filesystem()
    {
        return $this->start_filesystem;
    }

    public function get_logger()
    {
        return $this->logger;
    }

    public function get_start_path_file_info($file)
    {
        $info = $this->getMetadataFull('start_adapter', $file);

        return $this->start_filesystem->normalizeFileInfo($info);
    }

    /**
     * @param string $file
     */
    public function get_storage_path_file_info($file)
    {
        return $this->getMetadataFull('storage_adapter', $file);
    }

    public function get_included_files_handler($metadata = 0)
    {
        $path = $this->included_files_handler;
        if (!$metadata) {
            return $path;
        }

        $spl_info = $this->getMetadataFull('tmp_adapter', $path);

        return $spl_info;
    }

    public function get_temp_dir_handler()
    {
        return $this->temp_dir_handler;
    }

    public function get_latest_backup()
    {
        $files = $this->get_backup_archives_list();

        if (is_array($files)) {
            $this->sort_by($files, "timestamp", "desc");
        }

        $new_list = array();

        foreach ($files as $key => $file) {
            if (!isset($file['parent'])) {
                $new_list[] = ($files[$key]);
            }
        }

        if (isset($new_list[0])) {
            return $new_list[0];
        }
    }

    public function get_latest_backups()
    {
        $files = $this->get_backup_archives_list();

        if (is_array($files)) {
            $this->sort_by($files, "timestamp", "desc");
        }

        $new_list = array();

        foreach ($files as $key => $file) {
            if (!isset($file['parent'])) {
                $new_list[] = ($files[$key]);
            }
        }

        return $new_list;
    }

    public function get_storage_usage()
    {
        $files = $this->get_backup_archives_list();
        $total = 0;

        if (is_array($files)) {
            foreach ($files as $file) {
                $total += $file['size'];
            }
        }

        return $total;
    }

    public function is_part($backup_name)
    {
        if (stristr($backup_name, "-part")) {
            return true;
        }

        return false;
    }

    public function is_multipart($backup_name)
    {
        if (stristr($backup_name, "-multipart")) {
            return true;
        }

        return false;
    }

    public function get_backup_size($backup_name)
    {
        $backup_size = $this->get_storage_filesystem()->getSize($backup_name);
        if ($this->is_multipart($backup_name)) {
            $backup_parts = $this->get_multipart_files($backup_name);
            foreach ($backup_parts as $part_file) {
                $backup_size += $this->get_storage_filesystem()->getSize($part_file);
            }
        }

        return $backup_size;
    }

    public function get_multipart_files($backup_name, $storage_selection = "")
    {
        $files = array();

        if ($this->is_multipart($backup_name)) {
            $lines = explode(PHP_EOL, $this->get_storage_filesystem($storage_selection)->read($backup_name));
            foreach ($lines as $line) {
                if ($line) {
                    $data = str_getcsv($line);
                    $files[] = $data[0];
                }
            }
        }

        return $files;
    }

    public function delete_backup_by_name($backup_name, $storage_selection = "")
    {
        if ($this->is_multipart($backup_name)) {
            $lines = explode(PHP_EOL, $this->get_storage_filesystem($storage_selection)->read($backup_name));
            foreach ($lines as $line) {
                if ($line) {
                    $data = str_getcsv($line);
                    $this->get_storage_filesystem($storage_selection)->delete($data[0]);
                }
            }
        }

        if ($this->get_storage_filesystem($storage_selection)->delete($backup_name)) {
            $return = true;
        } else {
            $return = false;
        }

        return $return;
    }

    public function getMetadataFull($adapter = "storage_adapter", $path)
    {
        $location = $this->$adapter->applyPathPrefix($path);
        $spl_info = new \SplFileInfo($location);

        return ($spl_info);
    }


    public function get_backup_archives_list($storage_selection = "")
    {
        $list = array();


        if (method_exists($this->get_storage_filesystem($storage_selection), "listContents")) {
            $list = $this->get_storage_filesystem($storage_selection)->listContents();
        }

        $backup_files = array();
        $parents = array();

        foreach ($list as $file_info) {
            if (isset($file_info['extension']) and $file_info['extension'] == "csv") {
                $data = array();

                $lines = explode(PHP_EOL, $this->get_storage_filesystem($storage_selection)->read($file_info['path']));
                foreach ($lines as $line) {
                    if ($line) {
                        $data = str_getcsv($line);
                        if (is_array($data)) {
                            $parents[$data[0]] = $file_info['basename'];
                            $file_info['childs'][] = $data;
                            $file_info['size'] += $data[2];
                        }
                    }
                }
            }

            if ($file_info['type'] == 'file' and isset($file_info['extension']) and in_array(
                $file_info['extension'],
                $this->backup_archive_extensions
            )) {
                $backup_files[$file_info['path']] = $file_info;
            }
        }

        foreach ($backup_files as $key => $file_info) {
            if (!isset($backup_files[$key]['timestamp'])) {
                //$backup_files[$key]['timestamp'] = $this->get_storage_filesystem($storage_selection)->getTimestamp($file_info['path']);
            }

            if (isset($parents[$file_info['basename']])) {
                $backup_files[$key]['parent'] = $parents[$file_info['basename']];
            }
        }

        return $backup_files;
    }

    public function start_file_recursion($init = 0)
    {
        if ($init) {
            $this->logger->info(sprintf(
                __("Starting the filesystem scanner on root folder %s"),
                $this->xcloner_settings->get_xcloner_start_path()
            ));
            $this->do_system_init();
        }

        if ($this->tmp_filesystem->has($this->get_temp_dir_handler())) {
            //.dir exists, we presume we have files to iterate
            $content = $this->tmp_filesystem->read($this->get_temp_dir_handler());
            $files = array_filter(explode("\n", $content));
            $this->tmp_filesystem->delete($this->get_temp_dir_handler());

            $counter = 0;
            foreach ($files as $file) {
                if ($counter < $this->folders_to_process_per_session) {
                    $this->build_files_list($file);
                    $counter++;
                } else {
                    $this->tmp_filesystem_append->write($this->get_temp_dir_handler(), $file."\n");
                }
            }
        } else {
            $this->build_files_list();
        }

        if ($this->scan_finished()) {
            $metadata_dumpfile = $this->get_tmp_filesystem()->getMetadata("index.html");
            $this->store_file($metadata_dumpfile, 'tmp_filesystem');
            $this->files_counter++;

            //adding included dump file to the included files list
            if ($this->get_tmp_filesystem()->has($this->get_included_files_handler())) {
                $metadata_dumpfile = $this->get_tmp_filesystem()->getMetadata($this->get_included_files_handler());
                $this->store_file($metadata_dumpfile, 'tmp_filesystem');
                $this->files_counter++;
            }

            //adding a default index.html to the temp xcloner folder
            if (!$this->get_tmp_filesystem()->has("index.html")) {
                $this->get_tmp_filesystem()->write("index.html", "");
            }

            //adding the default log file
            if ($this->get_tmp_filesystem()->has($this->xcloner_settings->get_logger_filename(1))) {
                $metadata_dumpfile = $this->get_tmp_filesystem()->getMetadata($this->xcloner_settings->get_logger_filename(1));
                $this->store_file($metadata_dumpfile, 'tmp_filesystem');
                $this->files_counter++;
            }

            return false;
        }

        return true;
    }

    public function get_backup_attachments()
    {
        $return = array();

        $files_list_file = $this->xcloner_settings->get_xcloner_tmp_path().DS.$this->get_included_files_handler();
        if (file_exists($files_list_file)) {
            $return[] = $files_list_file;
        }

        if ($this->xcloner_settings->get_xcloner_option('xcloner_enable_log')) {
            $log_file = $this->xcloner_settings->get_xcloner_tmp_path().DS.$this->xcloner_settings->get_logger_filename(1);
            if (!file_exists($log_file)) {
                $log_file = $this->xcloner_settings->get_xcloner_store_path().DS.$this->xcloner_settings->get_logger_filename();
            }

            if (file_exists($log_file)) {
                $return[] = $log_file;
            }
        }

        return $return;
    }

    public function remove_tmp_filesystem()
    {
        //delete the temporary folder
        $this->logger->debug(sprintf(
            "Deleting the temporary storage folder %s",
            $this->xcloner_settings->get_xcloner_tmp_path()
        ));

        $contents = $this->get_tmp_filesystem()->listContents();

        if (is_array($contents)) {
            foreach ($contents as $file_info) {
                $this->get_tmp_filesystem()->delete($file_info['path']);
            }
        }

        try {
            rmdir($this->xcloner_settings->get_xcloner_tmp_path());
        } catch (Exception $e) {
            //silent continue
        }

        return;
    }

    public function cleanup_tmp_directories()
    {
        $this->logger->info(sprintf(("Cleaning up the temporary directories")));

        $adapter = new Local($this->xcloner_settings->get_xcloner_tmp_path(false), LOCK_EX, '0001');
        $tmp_filesystem = new Filesystem($adapter, new Config([
            'disable_asserts' => true,
        ]));

        $contents = $tmp_filesystem->listContents();

        foreach ($contents as $file) {
            if (preg_match("/.xcloner-(.*)/", $file['path'])) {
                if ($file['timestamp'] < strtotime("-1days")) {
                    $tmp_filesystem->deleteDir($file['path']);
                    $this->logger->debug(sprintf("Delete temporary directory %s", $file['path']));
                }
            }
        }

        return true;
    }

    private function do_system_init()
    {
        $this->files_counter = 0;

        if (!$this->storage_filesystem->has("index.html")) {
            $this->storage_filesystem->write("index.html", "");
        }

        if (!$this->tmp_filesystem->has("index.html")) {
            $this->tmp_filesystem->write("index.html", "");
        }

        if ($this->tmp_filesystem->has($this->get_included_files_handler())) {
            $this->tmp_filesystem->delete($this->get_included_files_handler());
        }

        if ($this->tmp_filesystem->has($this->get_temp_dir_handler())) {
            $this->tmp_filesystem->delete($this->get_temp_dir_handler());
        }
    }

    public function get_scanned_files_num()
    {
        return $this->files_counter;
    }

    public function get_scanned_files_total_size()
    {
        return $this->files_size;
    }

    public function last_logged_file()
    {
        return $this->last_logged_file;
    }

    public static function is_regex($regex)
    {
        return preg_match("/^\^(.*)\$$/i", $regex);
    }

    public function set_excluded_files($excluded_files = array())
    {
        if (!is_array($excluded_files)) {
            $excluded_files = array();
        }

        foreach ($excluded_files as $excl) {
            if ($this->is_regex($excl)) {
                $this->additional_regex_patterns[] = $excl;
            }
        }

        $this->excluded_files = array_merge($excluded_files, $this->excluded_files_by_default);

        return $this->excluded_files;
    }

    public function get_excluded_files()
    {
        return $this->excluded_files_by_default;
    }

    public function list_directory($path)
    {
        return $this->start_filesystem->listContents($path);
    }

    public function build_files_list($folder = "")
    {
        $this->logger->debug(sprintf(("Building the files system list")));

        //if we start with the root folder(empty value), we initializa the file system
        if (!$folder) {
        }

        try {
            $files = $this->start_filesystem->listContents($folder);
            foreach ($files as $file) {
                if (!is_readable($this->xcloner_settings->get_xcloner_start_path().DS.$file['path'])) {
                    $this->logger->info(sprintf(
                        __("Excluding %s from the filesystem list, file not readable"),
                        $file['path']
                    ), array(
                        "FILESYSTEM SCAN",
                        "NOT READABLE"
                    ));
                } elseif (!$matching_pattern = $this->is_excluded($file)) {
                    $this->logger->info(sprintf(__("Adding %s to the filesystem list"), $file['path']), array(
                        "FILESYSTEM SCAN",
                        "INCLUDE"
                    ));
                    $file['visibility'] = $this->start_filesystem->getVisibility($file['path']);
                    if ($this->store_file($file)) {
                        $this->files_counter++;
                    }
                    if (isset($file['size'])) {
                        $this->files_size += $file['size'];
                    }
                } else {
                    $this->logger->info(sprintf(
                        __("Excluding %s from the filesystem list, matching pattern %s"),
                        $file['path'],
                        $matching_pattern
                    ), array(
                        "FILESYSTEM SCAN",
                        "EXCLUDE"
                    ));
                }
            }
        } catch (Exception $e) {
            $this->logger->error($e->getMessage());
        }
    }

    public function estimate_read_write_time()
    {
        $tmp_file = ".xcloner".substr(md5(time()), 0, 5);

        $start_time = microtime(true);

        $data = str_repeat(rand(0, 9), 1024 * 1024); //write 1MB data

        try {
            $this->tmp_filesystem->write($tmp_file, $data);

            $end_time = microtime(true) - $start_time;

            $return['writing_time'] = $end_time;

            $return['reading_time'] = $this->estimate_reading_time($tmp_file);

            $this->tmp_filesystem->delete($tmp_file);
        } catch (Exception $e) {
            $this->logger->error($e->getMessage());
        }

        return $return;
    }

    public function backup_storage_cleanup($storage_name = "local", $storage_filesystem = "")
    {
        //$storage_name = "webdav";

        $storage_name = strtoupper($storage_name);

        $this->logger->info(sprintf("Cleaning the backup storage %s on matching rules", $storage_name));

        if (!$storage_filesystem) {
            $storage_filesystem = $this->storage_filesystem;
        }

        $_storage_size = 0;
        $_backup_files_list = array();

        $options_prefix = $this->xcloner_settings->get_options_prefix()."_".($storage_name == "LOCAL"?"":strtolower( $storage_name)."_");

        //rule date limit
        $xcloner_cleanup_retention_limit_days = $options_prefix . "cleanup_retention_limit_days";
        $current_timestamp = strtotime("-".$this->xcloner_settings->get_xcloner_option($xcloner_cleanup_retention_limit_days)." days");

        $files = $storage_filesystem->listContents();

        if (is_array($files)) {
            foreach ($files as $file) {
                if (isset($file['extension']) and in_array($file['extension'], $this->backup_archive_extensions)) {
                    $_storage_size += $file['size']; //bytes
                    $_backup_files_list[] = $file;
                }
            }
        }


        $this->sort_by($_backup_files_list, "timestamp", "asc");

        $_backups_counter = sizeof($_backup_files_list);

        foreach ($_backup_files_list as $file) {

            // processing rule for exclude backups taken on certain days
            $xcloner_cleanup_exclude_days = $options_prefix . "cleanup_exclude_days";
            if ($exclude_days_string = $this->xcloner_settings->get_xcloner_option($xcloner_cleanup_exclude_days)) {
                $exclude_days = explode(",", $exclude_days_string);

                $backup_day_of_month =  date('j', $file['timestamp']);
                if (in_array($backup_day_of_month, $exclude_days)) {
                    $this->logger->info(sprintf("Excluding %s from storage %s cleanup trashing as it was taken on month day %s ", 
                                                    $file['path'], 
                                                    $storage_name,
                                                    $backup_day_of_month)
                                                );
                    continue;
                }
            }

            //processing rule folder capacity
            $xcloner_cleanup_capacity_limit = $options_prefix."cleanup_capacity_limit";
            if ($this->xcloner_settings->get_xcloner_option($xcloner_cleanup_capacity_limit) &&
                $_storage_size >= ($set_storage_limit = 1024 * 1024 * $this->xcloner_settings->get_xcloner_option('xcloner_cleanup_capacity_limit'))) {    //bytes
                $storage_filesystem->delete($file['path']);
                $_storage_size -= $file['size'];
                $this->logger->info("Deleting backup ".$file['path']." from storage system ".$storage_name." matching rule", array(
                    "STORAGE SIZE LIMIT",
                    $_storage_size." >= ".$set_storage_limit
                ));
            }

            //processing rule days limit
            $xcloner_cleanup_retention_limit_days = $options_prefix . "cleanup_retention_limit_days";
            if ($this->xcloner_settings->get_xcloner_option($xcloner_cleanup_retention_limit_days) && $current_timestamp >= $file['timestamp']) {
                $storage_filesystem->delete($file['path']);
                $this->logger->info("Deleting backup ".$file['path']." from storage system ".$storage_name." matching rule", array(
                    "RETENTION LIMIT TIMESTAMP",
                    $file['timestamp']." =< ".$this->xcloner_settings->get_xcloner_option($xcloner_cleanup_retention_limit_days)
                ));
            }

            //processing backup countert limit
            $xcloner_cleanup_retention_limit_archives = $options_prefix . "cleanup_retention_limit_archives";
            if ($this->xcloner_settings->get_xcloner_option('xcloner_cleanup_retention_limit_archives') && $_backups_counter >= $this->xcloner_settings->get_xcloner_option('xcloner_cleanup_retention_limit_archives')) {
                $storage_filesystem->delete($file['path']);
                $_backups_counter--;
                $this->logger->info("Deleting backup ".$file['path']." from storage system ".$storage_name." matching rule", array(
                    "BACKUP QUANTITY LIMIT",
                    $_backups_counter." >= ".$this->xcloner_settings->get_xcloner_option($xcloner_cleanup_retention_limit_archives)
                ));
            }
        }
    }

    /**
     * @param string $tmp_file
     */
    public function estimate_reading_time($tmp_file)
    {
        $this->logger->debug(sprintf(("Estimating file system reading time")));

        $start_time = microtime(true);

        if ($this->tmp_filesystem->has($tmp_file)) {
            $this->tmp_filesystem->read($tmp_file);
        }

        $end_time = microtime(true) - $start_time;

        return $end_time;
    }

    public function process_backup_name($name = "", $max_length = 100)
    {
        if (!$name) {
            $name = $this->xcloner_settings->get_default_backup_name();
        }

        foreach ($this->backup_name_tags as $tag) {
            if ($tag == '[time]') {
                $name = str_replace($tag, date("Y-m-d_H-i"), $name);
            } elseif ($tag == '[hostname]') {
                $name = str_replace($tag, gethostname(), $name);
            } elseif ($tag == '[hash]') {
                $name = str_replace($tag, $this->xcloner_container->randomString(5), $name);
            } elseif ($tag == '[domain]') {
                $domain = parse_url(admin_url(), PHP_URL_HOST);
                $name = str_replace($tag, $domain, $name);
            }
        }

        if ($max_length) {
            $name = substr($name, 0, $max_length);
        }

        return $name;
    }

    /**
     * @param string $field
     */
    public function sort_by(&$array, $field, $direction = 'asc')
    {
        if (strtolower($direction) == "desc" || $direction == SORT_DESC) {
            $direction = SORT_DESC;
        } else {
            $direction = SORT_ASC;
        }

        $array = $this->array_orderby($array, $field, $direction);

        return true;
    }

    private function array_orderby()
    {
        $args = func_get_args();
        $data = array_shift($args);

        foreach ($args as $n => $field) {
            if (is_string($field)) {
                $tmp = array();
                foreach ($data as $key => $row) {
                    if (is_array($row)) {
                        $tmp[$key] = $row[$field];
                    } else {
                        $tmp[$key] = $row->$field;
                    }
                }
                $args[$n] = $tmp;
            }
        }
        $args[] = &$data;

        call_user_func_array('array_multisort', $args);

        return array_pop($args);
    }

    private function check_file_diff_time($file)
    {
        if ($this->get_diff_timestamp_start() != "") {
            $fileMeta = $this->getMetadataFull("start_adapter", $file['path']);
            $timestamp = $fileMeta->getMTime();
            if ($timestamp < $fileMeta->getCTime()) {
                $timestamp = $fileMeta->getCTime();
            }

            if ($timestamp <= $this->get_diff_timestamp_start()) {
                return " file DIFF timestamp ".$timestamp." < ".$this->diff_timestamp_start;
            }
        }

        return false;
    }

    public function is_excluded($file)
    {
        $this->logger->debug(sprintf(("Checking if %s is excluded"), $file['path']));

        if ($xcloner_exclude_files_larger_than_mb = $this->xcloner_settings->get_xcloner_option('xcloner_exclude_files_larger_than_mb')) {
            if (isset($file['size']) && $file['size'] > $this->calc_to_bytes($xcloner_exclude_files_larger_than_mb)) {
                return "> ".$xcloner_exclude_files_larger_than_mb."MB";
            }
        }

        if (!is_array($this->excluded_files) || !sizeof($this->excluded_files)) {
            $this->set_excluded_files();
        }

        if (is_array($this->excluded_files)) {
            foreach ($this->excluded_files as $excluded_file_pattern) {
                if ($excluded_file_pattern == "/") {
                    $needle = "$";
                } else {
                    $needle = "$".$excluded_file_pattern;
                }

                if (strstr("$".$file['path'], $needle)) {
                    return $excluded_file_pattern;
                }
            }
        }

        if ($regex = $this->is_excluded_regex($file)) {
            return $regex;
        }

        if ($file['type'] == "file") {
            $check_file_diff_timestamp = $this->check_file_diff_time($file);
            if ($check_file_diff_timestamp) {
                return $check_file_diff_timestamp;
            }
        }

        return false;
    }

    /*REGEX examples
     *
    * exclude all except .php file
    * PATTERN: ^(.*)\.(.+)$(?<!(php))
    *
    * exclude all except .php and .txt
    * PATTERN: ^(.*)\.(.+)$(?<!(php|txt))";
    *
    * exclude all .svn and .git
    * PATTERN: ^(.*)\.(svn|git)(.*)$";
    *
    * exclude root directory /test
    * PATTERN: "^\/test(.*)$";
    *
    * exclude the wp-admin folder
    * PATTERN: ^(\/wp-admin)(.*)$";
    *
    * exclude the wp-admin, wp-includes and wp-config.php
    * PATTERN: ^\/(wp-admin|wp-includes|wp-config.php)(.*)$";
    *
    * exclude all .avi files
    * PATTERN: ^(.*)$(?<=(avi))";
    *
    * exclude all .jpg and gif files
    * PATTERN: ^(.*)$(?<=(gif|jpg))";
    *
    * exclude all cache folders from wp-content/
    * PATTERN: ^\/wp-content(.*)\/cache($|\/)(.*)";
    *
    * exclude the backup folders
    * PATTERN: (^|^\/)(wp-content\/backups|administrator\/backups)(.*)$";
    */
    private function is_excluded_regex($file)
    {
        //$this->logger->debug(sprintf(("Checking if %s is excluded"), $file['path']));

        $regex_patterns = explode(PHP_EOL, $this->xcloner_settings->get_xcloner_option('xcloner_regex_exclude'));

        if (is_array($this->additional_regex_patterns)) {
            $regex_patterns = array_merge($regex_patterns, $this->additional_regex_patterns);
        }

        //print_r($regex_patterns);exit;

        if (is_array($regex_patterns)) {
            //$this->excluded_files = array();
            //$this->excluded_files[] ="(.*)\.(git)(.*)$";
            //$this->excluded_files[] ="wp-content\/backups(.*)$";

            foreach ($regex_patterns as $excluded_file_pattern) {
                if (substr(
                    $excluded_file_pattern,
                    strlen($excluded_file_pattern) - 1,
                    strlen($excluded_file_pattern)
                ) == "\r") {
                    $excluded_file_pattern = substr($excluded_file_pattern, 0, strlen($excluded_file_pattern) - 1);
                }

                if ($file['path'] == "/") {
                    $needle = "/";
                } else {
                    $needle = "/".$file['path'];
                }
                //echo $needle."---".$excluded_file_pattern."---\n";

                if (@preg_match("/(^|^\/)".$excluded_file_pattern."/i", $needle)) {
                    return $excluded_file_pattern;
                }
            }
        }

        return false;
    }

    public function store_file($file, $storage = 'start_filesystem')
    {
        $this->logger->debug(sprintf("Storing %s in the backup list", $file['path']));

        if (!isset($file['size'])) {
            $file['size'] = 0;
        }
        if (!isset($file['visibility'])) {
            $file['visibility'] = "private";
        }

        $csv_filename = str_replace('"', '""', $file['path']);

        $line = '"'.($csv_filename).'","'.$file['timestamp'].'","'.$file['size'].'","'.$file['visibility'].'","'.$storage.'"'.PHP_EOL;

        $this->last_logged_file = $file['path'];

        if ($file['type'] == "dir") {
            try {
                $this->tmp_filesystem_append->write($this->get_temp_dir_handler(), $file['path']."\n");
            } catch (Exception $e) {
                $this->logger->error($e->getMessage());
            }
        }

        if ($this->get_diff_timestamp_start()) {
            if ($file['type'] != "file" && $response = $this->check_file_diff_time($file)) {
                $this->logger->info(sprintf(
                    "Directory %s archiving skipped on differential backup %s",
                    $file['path'],
                    $response
                ), array(
                    "FILESYSTEM SCAN",
                    "DIR DIFF"
                ));

                return false;
            }
        }

        try {
            if (!$this->tmp_filesystem_append->has($this->get_included_files_handler())) {
                //adding fix for UTF-8 CSV preview
                $start_line = "\xEF\xBB\xBF".'"Filename","Timestamp","Size","Visibility","Storage"'.PHP_EOL;
                $this->tmp_filesystem_append->write($this->get_included_files_handler(), $start_line);
            }

            $this->tmp_filesystem_append->write($this->get_included_files_handler(), $line);
        } catch (Exception $e) {
            $this->logger->error($e->getMessage());
        }

        return true;
    }

    public function get_fileystem_handler()
    {
        return $this;
    }

    public function get_filesystem($system = "")
    {
        if ($system == "storage_filesystem_append") {
            return $this->storage_filesystem_append;
        } elseif ($system == "tmp_filesystem_append") {
            return $this->tmp_filesystem_append;
        } elseif ($system == "tmp_filesystem") {
            return $this->tmp_filesystem;
        } elseif ($system == "storage_filesystem") {
            return $this->storage_filesystem;
        } else {
            return $this->start_filesystem;
        }
    }

    public function get_adapter($system)
    {
        if ($system == "tmp_filesystem") {
            return $this->tmp_adapter;
        } elseif ($system == "storage_filesystem") {
            return $this->storage_adapter;
        } else {
            return $this->start_adapter;
        }
    }

    /**
     * File scan finished
     * Method called when file scan is finished
     *
     * @return bool
     */
    private function scan_finished()
    {
        if ($this->tmp_filesystem_append->has($this->get_temp_dir_handler()) &&
            $this->tmp_filesystem_append->getSize($this->get_temp_dir_handler())) {
            return false;
        }

        if ($this->tmp_filesystem->has($this->get_temp_dir_handler())) {
            $this->tmp_filesystem->delete($this->get_temp_dir_handler());
        }

        $this->logger->debug(sprintf(("File scan finished")));

        return true;
    }

    /**
     * Calculate bytes from MB value
     *
     * @param int $mb_size
     *
     * @return float|int
     */
    private function calc_to_bytes($mb_size)
    {
        return $mb_size * (1024 * 1024);
    }
}
<?php
namespace watchfulli\XClonerCore;

/**
 * Created by PhpStorm.
 * User: thinkovi
 * Date: 2018-11-27
 * Time: 12:11
 */

//namespace XCloner;

/**
 * Xcloner_Encryption class
 */
class Xcloner_Encryption
{
    const FILE_ENCRYPTION_BLOCKS = 1024 * 1024;
    const FILE_ENCRYPTION_SUFFIX = ".encrypted";
    const FILE_DECRYPTION_SUFFIX = ".decrypted";

    private $xcloner_settings;
    private $logger;
    private $xcloner_container;
    private $verification = false;

    /**
     * Xcloner_Encryption constructor.
     * @param Xcloner $xcloner_container
     */
    public function __construct(Xcloner $xcloner_container)
    {
        $this->xcloner_container = $xcloner_container;
        //if (property_exists($xcloner_container, 'xcloner_settings')) {
            $this->xcloner_settings = $xcloner_container->get_xcloner_settings();
        //} else {
        //    $this->xcloner_settings = "";
        //}

        if (property_exists($xcloner_container, 'xcloner_logger')) {
            $this->logger = $xcloner_container->get_xcloner_logger()->withName("xcloner_encryption");
        } else {
            $this->logger = "";
        }
    }

    /**
     * Returns the backup encryption key
     *
     * @return string|null
     */
    public function get_backup_encryption_key()
    {
        if (is_object($this->xcloner_settings)) {
            return $this->xcloner_settings->get_xcloner_encryption_key();
        }

        return null;
    }

    /**
     * Check if provided filename has encrypted suffix
     *
     * @param $filename
     * @return bool
     */
    public function is_encrypted_file($filename)
    {
        $fp = fopen($this->get_xcloner_path().$filename, 'r');
        if (is_resource($fp)) {
            $encryption_length = fread($fp, 16);
            fclose($fp);
            if (is_numeric($encryption_length)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the filename with encrypted suffix
     *
     * @param string $filename
     * @return string
     */
    public function get_encrypted_target_backup_file_name($filename)
    {
        return str_replace(self::FILE_DECRYPTION_SUFFIX, "", $filename).self::FILE_ENCRYPTION_SUFFIX;
    }

    /**
     * Returns the filename without encrypted suffix
     *
     * @param string $filename
     * @return string
     */
    public function get_decrypted_target_backup_file_name($filename)
    {
        return str_replace(self::FILE_ENCRYPTION_SUFFIX, "", $filename).self::FILE_DECRYPTION_SUFFIX;
    }

    /**
     * Encrypt the passed file and saves the result in a new file with ".enc" as suffix.
     *
     * @param string $source Path to file that should be encrypted
     * @param string $dest   File name where the encryped file should be written to.
     * @param string $key    The key used for the encryption
     * @param int $start   Start position for reading when doing incremental mode.
     * @param string $iv   The IV key to use.
     * @param bool $verification   Weather we should we try to verify the decryption.
     * @return array|false  Returns array or FALSE if an error occured
     * @throws Exception
     */
    public function encrypt_file($source, $dest = "", $key = "", $start = 0, $iv = 0, $verification = true, $recursive = false)
    {
        if (is_object($this->logger)) {
            $this->logger->info(sprintf('Encrypting file %s at position %d IV %s', $source, $start, base64_encode($iv)));
        }

        //$key = substr(sha1($key, true), 0, 16);
        if (!$key) {
            $key = $this->get_backup_encryption_key();
        }
        $key_digest = openssl_digest($key, "md5", true);

        $keep_local = 1;
        if (!$dest) {
            $dest = $this->get_encrypted_target_backup_file_name($source);
            $keep_local = 0;
        }

        if (!$iv || !$start) {
            //$iv = openssl_random_pseudo_bytes(16);
            $iv = str_pad(self::FILE_ENCRYPTION_BLOCKS, 16, "0000000000000000", STR_PAD_LEFT);
        }

        if (!$start) {
            $fpOut = fopen($this->get_xcloner_path().$dest, 'w');
        } else {
            $fpOut = fopen($this->get_xcloner_path().$dest, 'a');
        }

        if (is_resource($fpOut)) {

            // Put the initialization vector to the beginning of the file
            if (!$start) {
                fwrite($fpOut, $iv);
            }

            if (file_exists($this->get_xcloner_path().$source) &&
                $fpIn = fopen($this->get_xcloner_path().$source, 'rb')) {
                fseek($fpIn, (int)$start);

                if (!feof($fpIn)) {
                    $plaintext = fread($fpIn, 16 * self::FILE_ENCRYPTION_BLOCKS);
                    $ciphertext = openssl_encrypt($plaintext, 'AES-128-CBC', $key_digest, OPENSSL_RAW_DATA, $iv);

                    // Use the first 16 bytes of the ciphertext as the next initialization vector
                    $iv = substr($ciphertext, 0, 16);
                    //$iv = openssl_random_pseudo_bytes(16);

                    fwrite($fpOut, $ciphertext);

                    $start = ftell($fpIn);

                    fclose($fpOut);

                    unset($ciphertext);
                    unset($plaintext);

                    if (!feof($fpIn)) {
                        fclose($fpIn);
                        //echo "\n NEW:".$key.md5($iv);
                        //self::encryptFile($source, $dest, $key, $start, $iv);
                        if ($recursive) {
                            $this->encrypt_file($source, $dest, $key, $start, ($iv), $verification, $recursive);
                        } else {
                            if (($iv) != base64_decode(base64_encode($iv))) {
                                throw new \Exception('Could not encode IV for transport');
                            }

                            return array(
                                "start" => $start,
                                "iv" => base64_encode($iv),
                                "target_file" => $dest,
                                "finished" => 0
                            );
                        }
                    }
                }
            } else {
                if (is_object($this->logger)) {
                    $this->logger->error('Unable to read source file for encryption.');
                }
                throw new \Exception("Unable to read source file for encryption.");
            }
        } else {
            if (is_object($this->logger)) {
                $this->logger->error('Unable to write destination file for encryption.');
            }
            throw new \Exception("Unable to write destination file for encryption.");
        }

        if ($verification) {
            $this->verify_encrypted_file($dest);
        }

        //we replace the original backup with the encrypted one
        if (!$keep_local && copy(
            $this->get_xcloner_path().$dest,
            $this->get_xcloner_path().$source
        )) {
            unlink($this->get_xcloner_path().$dest);
        }


        return array("target_file" => $dest, "finished" => 1);
    }

    /**
     * @param string $file
     */
    public function verify_encrypted_file($file)
    {
        if (is_object($this->logger)) {
            $this->logger->info(sprintf('Verifying encrypted file %s', $file));
        }

        $this->verification = true;
        $this->decrypt_file($file);
        $this->verification = false;
    }

    /**
     * Dencrypt the passed file and saves the result in a new file, removing the
     * last 4 characters from file name.
     *
     * @param string $source Path to file that should be decrypted
     * @param string $dest   File name where the decryped file should be written to.
     * @param string $key    The key used for the decryption (must be the same as for encryption)
     * @param int $start   Start position for reading when doing incremental mode.
     * @param string $iv   The IV key to use.
     * @return array|false  Returns array or FALSE if an error occured
     * @throws Exception
     */
    public function decrypt_file($source, $dest = "", $key = "", $start = 0, $iv = 0, $recursive = false)
    {
        if (is_object($this->logger)) {
            $this->logger->info(sprintf('Decrypting file %s at position %d with IV %s', $source, $start, base64_encode($iv)));
        }

        //$key = substr(sha1($key, true), 0, 16);
        if (!$key) {
            $key = $this->get_backup_encryption_key();
        }

        $key_digest = openssl_digest($key, "md5", true);

        $keep_local = 1;
        if (!$dest) {
            $dest = $this->get_decrypted_target_backup_file_name($source);
            $keep_local = 0;
        }

        if (!$start) {
            if ($this->verification) {
                $fpOut = fopen("php://stdout", 'w');
            } else {
                $fpOut = fopen($this->get_xcloner_path().$dest, 'w');
            }
        } else {
            if ($this->verification) {
                $fpOut = fopen("php://stdout", 'a');
            } else {
                $fpOut = fopen($this->get_xcloner_path().$dest, 'a');
            }
        }

        if (is_resource($fpOut)) {
            if (file_exists($this->get_xcloner_path().$source) &&
                $fpIn = fopen($this->get_xcloner_path().$source, 'rb')) {
                $encryption_length = (int)fread($fpIn, 16);
                if (!$encryption_length) {
                    $encryption_length = self::FILE_ENCRYPTION_BLOCKS;
                }

                fseek($fpIn, (int)$start);

                // Get the initialzation vector from the beginning of the file
                if (!$iv) {
                    $iv = fread($fpIn, 16);
                }

                if (!feof($fpIn)) {

                    // we have to read one block more for decrypting than for encrypting
                    $ciphertext = fread($fpIn, 16 * ($encryption_length + 1));
                    $plaintext = openssl_decrypt($ciphertext, 'AES-128-CBC', $key_digest, OPENSSL_RAW_DATA, $iv);

                    if (!$plaintext) {
                        unlink($this->get_xcloner_path().$dest);
                        if (is_object($this->logger)) {
                            $this->logger->error('Backup decryption failed, please check your provided Encryption Key.');
                        }
                        throw new \Exception("Backup decryption failed, please check your provided Encryption Key.");
                    }

                    // Use the first 16 bytes of the ciphertext as the next initialization vector
                    $iv = substr($ciphertext, 0, 16);

                    if (!$this->verification) {
                        fwrite($fpOut, $plaintext);
                    }

                    $start = ftell($fpIn);

                    fclose($fpOut);

                    if (!feof($fpIn)) {
                        fclose($fpIn);
                        if ($this->verification || $recursive) {
                            unset($ciphertext);
                            unset($plaintext);
                            $this->decrypt_file($source, $dest, $key, $start, $iv, $recursive);
                        } else {
                            if (($iv) != base64_decode(base64_encode($iv))) {
                                throw new \Exception('Could not encode IV for transport');
                            }

                            return array(
                                "start" => $start,
                                "encryption_length" => $encryption_length,
                                "iv" => base64_encode($iv),
                                "target_file" => $dest,
                                "finished" => 0
                            );
                        }
                    }
                }
            } else {
                if (is_object($this->logger)) {
                    $this->logger->error('Unable to read source file for decryption');
                }
                throw new \Exception("Unable to read source file for decryption");
            }
        } else {
            if (is_object($this->logger)) {
                $this->logger->error('Unable to write destination file for decryption');
            }
            throw new \Exception("Unable to write destination file for decryption");
        }

        //we replace the original backup with the encrypted one
        if (!$keep_local && !$this->verification && copy(
            $this->get_xcloner_path().$dest,
            $this->get_xcloner_path().$source
        )) {
            unlink($this->get_xcloner_path().$dest);
        }

        return array("target_file" => $dest, "finished" => 1);
	}

    public function get_xcloner_path()
    {
        if (is_object($this->xcloner_settings)) {
            return $this->xcloner_settings->get_xcloner_store_path().DS;
        }

        return null;
    }
}


try {
    if (isset($argv[1])) {
        class Xcloner
        {
            /**
             * Xcloner constructor.
             */
            public function __construct()
            {
            }
        }
        $xcloner_encryption = new Xcloner_Encryption(new Xcloner());

        if ($argv[1] == "-e") {
            $xcloner_encryption->encrypt_file($argv[2], $argv[2].".enc", $argv[4], 0, 0, false, true);
        } elseif ($argv[1] == "-d") {
            $xcloner_encryption->decrypt_file($argv[2], $argv[2].".dec", $argv[4], 0, 0, true);
        }
    }
} catch (\Exception $e) {
    echo "CAUGHT: ".$e->getMessage();
}
2aeW~3W=   GBMB