From 70233a6e3476091def1259b8f83fa90d88226598 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Tue, 5 Apr 2016 18:40:44 -0400 Subject: [PATCH] lbry log upload --- controller/Controller.class.php | 2 + controller/action/OpsActions.class.php | 27 + dev.sh | 3 + lib/{ => tools}/Config.class.php | 0 lib/tools/Debug.class.php | 75 + lib/{ => tools}/shellExec.class.php | 0 lib/vendor/S3.class.php | 2389 ++++++++++++++++++++++++ web/index.php | 19 +- web/osx.dmg | Bin 92778 -> 0 bytes 9 files changed, 2512 insertions(+), 3 deletions(-) create mode 100755 dev.sh rename lib/{ => tools}/Config.class.php (100%) create mode 100644 lib/tools/Debug.class.php rename lib/{ => tools}/shellExec.class.php (100%) create mode 100644 lib/vendor/S3.class.php delete mode 100644 web/osx.dmg diff --git a/controller/Controller.class.php b/controller/Controller.class.php index 27f68243..dacbcf42 100644 --- a/controller/Controller.class.php +++ b/controller/Controller.class.php @@ -45,6 +45,8 @@ class Controller return ContentActions::executeGet(); case '/postcommit': return OpsActions::executePostCommit(); + case '/log-upload': + return OpsActions::executeLogUpload(); case '/list-subscribe': return MailActions::executeListSubscribe(); case '/LBRY-deck.pdf': diff --git a/controller/action/OpsActions.class.php b/controller/action/OpsActions.class.php index 23d09bfa..3854d139 100644 --- a/controller/action/OpsActions.class.php +++ b/controller/action/OpsActions.class.php @@ -26,4 +26,31 @@ class OpsActions extends Actions return [null, []]; } + + public static function executeLogUpload() + { + $log = isset($_POST['log']) ? urldecode($_POST['log']) : null; + $name = isset($_POST['name']) ? + preg_replace('/[^a-z0-9_-]+/', '', substr(strtolower(trim(urldecode($_POST['name']))),0,50)) : + null; + + Actions::returnErrorIf(!$log || !$name, "Required params: log, name"); + + $awsKey = Config::get('aws_log_access_key'); + $awsSecret = Config::get('aws_log_secret_key'); + + Actions::returnErrorIf(!$awsKey || !$awsSecret, "Missing AWS credentials"); + + $tmpFile = tempnam(sys_get_temp_dir(), 'lbryinstalllog'); + file_put_contents($tmpFile, $log); + + Actions::returnErrorIf(filesize($tmpFile) > 1024*1024*2, "File is too large"); + + S3::$useExceptions = true; + S3::setAuth($awsKey, $awsSecret); + S3::putObject(S3::inputFile($tmpFile, false), 'lbry-install-logs', $name); + unlink($tmpFile); + + return [null, []]; + } } diff --git a/dev.sh b/dev.sh new file mode 100755 index 00000000..022eb5a2 --- /dev/null +++ b/dev.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +php --server localhost:8000 --docroot web/ \ No newline at end of file diff --git a/lib/Config.class.php b/lib/tools/Config.class.php similarity index 100% rename from lib/Config.class.php rename to lib/tools/Config.class.php diff --git a/lib/tools/Debug.class.php b/lib/tools/Debug.class.php new file mode 100644 index 00000000..3caa1d61 --- /dev/null +++ b/lib/tools/Debug.class.php @@ -0,0 +1,75 @@ +getMessage() . '\' in ' . $e->getFile() . ':' . $e->getLine(); + } + + /** + * Same as the normal getTraceAsString(), but does not truncate long lines. + * @param Exception $exception + * @return string + * @see http://stackoverflow.com/questions/1949345/how-can-i-get-the-full-string-of-phps-gettraceasstring/6076667#6076667 + * @see https://gist.github.com/1437966 + */ + public static function getFullTrace(Exception $exception) + { + $rtn = ''; + foreach ($exception->getTrace() as $count => $frame) + { + $args = isset($frame['args']) ? static::exceptionFrameArgsToString($frame['args']) : ''; + + $rtn .= sprintf("#%s %s(%s): %s(%s)\n", + $count, + isset($frame['file']) ? $frame['file'] : 'unknown file', + isset($frame['line']) ? $frame['line'] : 'unknown line', + isset($frame['class']) ? $frame['class'].$frame['type'].$frame['function'] : $frame['function'], + $args); + } + return $rtn; + } + + public static function exceptionFrameArgsToString($args) + { + $ret = []; + foreach ($args as $arg) + { + if (is_string($arg)) + { + $ret[] = "'" . $arg . "'"; + } + elseif (is_array($arg)) + { + $ret[] = 'Array(' . count($arg) . ')'; + } + elseif (is_null($arg)) + { + $ret[] = 'NULL'; + } + elseif (is_bool($arg)) + { + $ret[] = ($arg) ? 'true' : 'false'; + } + elseif (is_object($arg)) + { + $ret[] = get_class($arg) . (!($arg instanceof Closure) && isset($arg->id) ? "({$arg->id})" : ''); + } + elseif (is_resource($arg)) + { + $ret[] = get_resource_type($arg); + } + else + { + $ret[] = $arg; + } + } + return join(', ', $ret); + } +} \ No newline at end of file diff --git a/lib/shellExec.class.php b/lib/tools/shellExec.class.php similarity index 100% rename from lib/shellExec.class.php rename to lib/tools/shellExec.class.php diff --git a/lib/vendor/S3.class.php b/lib/vendor/S3.class.php new file mode 100644 index 00000000..660844c4 --- /dev/null +++ b/lib/vendor/S3.class.php @@ -0,0 +1,2389 @@ + $host, 'type' => $type, 'user' => $user, 'pass' => $pass); + } + + + /** + * Set the error mode to exceptions + * + * @param boolean $enabled Enable exceptions + * @return void + */ + public static function setExceptions($enabled = true) + { + self::$useExceptions = $enabled; + } + + + /** + * Set AWS time correction offset (use carefully) + * + * This can be used when an inaccurate system time is generating + * invalid request signatures. It should only be used as a last + * resort when the system time cannot be changed. + * + * @param string $offset Time offset (set to zero to use AWS server time) + * @return void + */ + public static function setTimeCorrectionOffset($offset = 0) + { + if ($offset == 0) + { + $rest = new S3Request('HEAD'); + $rest = $rest->getResponse(); + $awstime = $rest->headers['date']; + $systime = time(); + $offset = $systime > $awstime ? -($systime - $awstime) : ($awstime - $systime); + } + self::$__timeOffset = $offset; + } + + + /** + * Set signing key + * + * @param string $keyPairId AWS Key Pair ID + * @param string $signingKey Private Key + * @param boolean $isFile Load private key from file, set to false to load string + * @return boolean + */ + public static function setSigningKey($keyPairId, $signingKey, $isFile = true) + { + self::$__signingKeyPairId = $keyPairId; + if ((self::$__signingKeyResource = openssl_pkey_get_private($isFile ? + file_get_contents($signingKey) : $signingKey)) !== false) return true; + self::__triggerError('S3::setSigningKey(): Unable to open load private key: '.$signingKey, __FILE__, __LINE__); + return false; + } + + + /** + * Free signing key from memory, MUST be called if you are using setSigningKey() + * + * @return void + */ + public static function freeSigningKey() + { + if (self::$__signingKeyResource !== false) + openssl_free_key(self::$__signingKeyResource); + } + + + /** + * Internal error handler + * + * @internal Internal error handler + * @param string $message Error message + * @param string $file Filename + * @param integer $line Line number + * @param integer $code Error code + * @return void + */ + private static function __triggerError($message, $file, $line, $code = 0) + { + if (self::$useExceptions) + throw new S3Exception($message, $file, $line, $code); + else + trigger_error($message, E_USER_WARNING); + } + + + /** + * Get a list of buckets + * + * @param boolean $detailed Returns detailed bucket list when true + * @return array | false + */ + public static function listBuckets($detailed = false) + { + $rest = new S3Request('GET', '', '', self::$endpoint); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::listBuckets(): [%s] %s", $rest->error['code'], + $rest->error['message']), __FILE__, __LINE__); + return false; + } + $results = array(); + if (!isset($rest->body->Buckets)) return $results; + + if ($detailed) + { + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) + $results['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName + ); + $results['buckets'] = array(); + foreach ($rest->body->Buckets->Bucket as $b) + $results['buckets'][] = array( + 'name' => (string)$b->Name, 'time' => strtotime((string)$b->CreationDate) + ); + } else + foreach ($rest->body->Buckets->Bucket as $b) $results[] = (string)$b->Name; + + return $results; + } + + + /** + * Get contents for a bucket + * + * If maxKeys is null this method will loop through truncated result sets + * + * @param string $bucket Bucket name + * @param string $prefix Prefix + * @param string $marker Marker (last file listed) + * @param string $maxKeys Max keys (maximum number of keys to return) + * @param string $delimiter Delimiter + * @param boolean $returnCommonPrefixes Set to true to return CommonPrefixes + * @return array | false + */ + public static function getBucket($bucket, $prefix = null, $marker = null, $maxKeys = null, $delimiter = null, $returnCommonPrefixes = false) + { + $rest = new S3Request('GET', $bucket, '', self::$endpoint); + if ($maxKeys == 0) $maxKeys = null; + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + if ($marker !== null && $marker !== '') $rest->setParameter('marker', $marker); + if ($maxKeys !== null && $maxKeys !== '') $rest->setParameter('max-keys', $maxKeys); + if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + else if (!empty(self::$defDelimiter)) $rest->setParameter('delimiter', self::$defDelimiter); + $response = $rest->getResponse(); + if ($response->error === false && $response->code !== 200) + $response->error = array('code' => $response->code, 'message' => 'Unexpected HTTP status'); + if ($response->error !== false) + { + self::__triggerError(sprintf("S3::getBucket(): [%s] %s", + $response->error['code'], $response->error['message']), __FILE__, __LINE__); + return false; + } + + $results = array(); + + $nextMarker = null; + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) + { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strtotime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $nextMarker = (string)$c->Key; + } + + if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) + foreach ($response->body->CommonPrefixes as $c) + $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); + + if (isset($response->body, $response->body->IsTruncated) && + (string)$response->body->IsTruncated == 'false') return $results; + + if (isset($response->body, $response->body->NextMarker)) + $nextMarker = (string)$response->body->NextMarker; + + // Loop through truncated results if maxKeys isn't specified + if ($maxKeys == null && $nextMarker !== null && (string)$response->body->IsTruncated == 'true') + do + { + $rest = new S3Request('GET', $bucket, '', self::$endpoint); + if ($prefix !== null && $prefix !== '') $rest->setParameter('prefix', $prefix); + $rest->setParameter('marker', $nextMarker); + if ($delimiter !== null && $delimiter !== '') $rest->setParameter('delimiter', $delimiter); + + if (($response = $rest->getResponse()) == false || $response->code !== 200) break; + + if (isset($response->body, $response->body->Contents)) + foreach ($response->body->Contents as $c) + { + $results[(string)$c->Key] = array( + 'name' => (string)$c->Key, + 'time' => strtotime((string)$c->LastModified), + 'size' => (int)$c->Size, + 'hash' => substr((string)$c->ETag, 1, -1) + ); + $nextMarker = (string)$c->Key; + } + + if ($returnCommonPrefixes && isset($response->body, $response->body->CommonPrefixes)) + foreach ($response->body->CommonPrefixes as $c) + $results[(string)$c->Prefix] = array('prefix' => (string)$c->Prefix); + + if (isset($response->body, $response->body->NextMarker)) + $nextMarker = (string)$response->body->NextMarker; + + } while ($response !== false && (string)$response->body->IsTruncated == 'true'); + + return $results; + } + + + /** + * Put a bucket + * + * @param string $bucket Bucket name + * @param constant $acl ACL flag + * @param string $location Set as "EU" to create buckets hosted in Europe + * @return boolean + */ + public static function putBucket($bucket, $acl = self::ACL_PRIVATE, $location = false) + { + $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + $rest->setAmzHeader('x-amz-acl', $acl); + + if ($location !== false) + { + $dom = new DOMDocument; + $createBucketConfiguration = $dom->createElement('CreateBucketConfiguration'); + $locationConstraint = $dom->createElement('LocationConstraint', $location); + $createBucketConfiguration->appendChild($locationConstraint); + $dom->appendChild($createBucketConfiguration); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + } + $rest = $rest->getResponse(); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::putBucket({$bucket}, {$acl}, {$location}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Delete an empty bucket + * + * @param string $bucket Bucket name + * @return boolean + */ + public static function deleteBucket($bucket) + { + $rest = new S3Request('DELETE', $bucket, '', self::$endpoint); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::deleteBucket({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Create input info array for putObject() + * + * @param string $file Input file + * @param mixed $md5sum Use MD5 hash (supply a string if you want to use your own) + * @return array | false + */ + public static function inputFile($file, $md5sum = true) + { + if (!file_exists($file) || !is_file($file) || !is_readable($file)) + { + self::__triggerError('S3::inputFile(): Unable to open input file: '.$file, __FILE__, __LINE__); + return false; + } + clearstatcache(false, $file); + return array('file' => $file, 'size' => filesize($file), 'md5sum' => $md5sum !== false ? + (is_string($md5sum) ? $md5sum : base64_encode(md5_file($file, true))) : ''); + } + + + /** + * Create input array info for putObject() with a resource + * + * @param string $resource Input resource to read from + * @param integer $bufferSize Input byte size + * @param string $md5sum MD5 hash to send (optional) + * @return array | false + */ + public static function inputResource(&$resource, $bufferSize = false, $md5sum = '') + { + if (!is_resource($resource) || (int)$bufferSize < 0) + { + self::__triggerError('S3::inputResource(): Invalid resource or buffer size', __FILE__, __LINE__); + return false; + } + + // Try to figure out the bytesize + if ($bufferSize === false) + { + if (fseek($resource, 0, SEEK_END) < 0 || ($bufferSize = ftell($resource)) === false) + { + self::__triggerError('S3::inputResource(): Unable to obtain resource size', __FILE__, __LINE__); + return false; + } + fseek($resource, 0); + } + + $input = array('size' => $bufferSize, 'md5sum' => $md5sum); + $input['fp'] =& $resource; + return $input; + } + + + /** + * Put an object + * + * @param mixed $input Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param array $requestHeaders Array of request headers or content type as a string + * @param constant $storageClass Storage class constant + * @param constant $serverSideEncryption Server-side encryption + * @return boolean + */ + public static function putObject($input, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD, $serverSideEncryption = self::SSE_NONE) + { + if ($input === false) return false; + $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + + if (!is_array($input)) $input = array( + 'data' => $input, 'size' => strlen($input), + 'md5sum' => base64_encode(md5($input, true)) + ); + + // Data + if (isset($input['fp'])) + $rest->fp =& $input['fp']; + elseif (isset($input['file'])) + $rest->fp = @fopen($input['file'], 'rb'); + elseif (isset($input['data'])) + $rest->data = $input['data']; + + // Content-Length (required) + if (isset($input['size']) && $input['size'] >= 0) + $rest->size = $input['size']; + else { + if (isset($input['file'])) { + clearstatcache(false, $input['file']); + $rest->size = filesize($input['file']); + } + elseif (isset($input['data'])) + $rest->size = strlen($input['data']); + } + + // Custom request headers (Content-Type, Content-Disposition, Content-Encoding) + if (is_array($requestHeaders)) + foreach ($requestHeaders as $h => $v) + strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); + elseif (is_string($requestHeaders)) // Support for legacy contentType parameter + $input['type'] = $requestHeaders; + + // Content-Type + if (!isset($input['type'])) + { + if (isset($requestHeaders['Content-Type'])) + $input['type'] =& $requestHeaders['Content-Type']; + elseif (isset($input['file'])) + $input['type'] = self::__getMIMEType($input['file']); + else + $input['type'] = 'application/octet-stream'; + } + + if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class + $rest->setAmzHeader('x-amz-storage-class', $storageClass); + + if ($serverSideEncryption !== self::SSE_NONE) // Server-side encryption + $rest->setAmzHeader('x-amz-server-side-encryption', $serverSideEncryption); + + // We need to post with Content-Length and Content-Type, MD5 is optional + if ($rest->size >= 0 && ($rest->fp !== false || $rest->data !== false)) + { + $rest->setHeader('Content-Type', $input['type']); + if (isset($input['md5sum'])) $rest->setHeader('Content-MD5', $input['md5sum']); + + $rest->setAmzHeader('x-amz-acl', $acl); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + $rest->getResponse(); + } else + $rest->response->error = array('code' => 0, 'message' => 'Missing input parameters'); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) + { + self::__triggerError(sprintf("S3::putObject(): [%s] %s", + $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Put an object from a file (legacy function) + * + * @param string $file Input file path + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectFile($file, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = null) + { + return self::putObject(self::inputFile($file), $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Put an object from a string (legacy function) + * + * @param string $string Input data + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Array of x-amz-meta-* headers + * @param string $contentType Content type + * @return boolean + */ + public static function putObjectString($string, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $contentType = 'text/plain') + { + return self::putObject($string, $bucket, $uri, $acl, $metaHeaders, $contentType); + } + + + /** + * Get an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param mixed $saveTo Filename or resource to write to + * @return mixed + */ + public static function getObject($bucket, $uri, $saveTo = false) + { + $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); + if ($saveTo !== false) + { + if (is_resource($saveTo)) + $rest->fp =& $saveTo; + else + if (($rest->fp = @fopen($saveTo, 'wb')) !== false) + $rest->file = realpath($saveTo); + else + $rest->response->error = array('code' => 0, 'message' => 'Unable to open save file for writing: '.$saveTo); + } + if ($rest->response->error === false) $rest->getResponse(); + + if ($rest->response->error === false && $rest->response->code !== 200) + $rest->response->error = array('code' => $rest->response->code, 'message' => 'Unexpected HTTP status'); + if ($rest->response->error !== false) + { + self::__triggerError(sprintf("S3::getObject({$bucket}, {$uri}): [%s] %s", + $rest->response->error['code'], $rest->response->error['message']), __FILE__, __LINE__); + return false; + } + return $rest->response; + } + + + /** + * Get object information + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param boolean $returnInfo Return response information + * @return mixed | false + */ + public static function getObjectInfo($bucket, $uri, $returnInfo = true) + { + $rest = new S3Request('HEAD', $bucket, $uri, self::$endpoint); + $rest = $rest->getResponse(); + if ($rest->error === false && ($rest->code !== 200 && $rest->code !== 404)) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getObjectInfo({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return $rest->code == 200 ? $returnInfo ? $rest->headers : true : false; + } + + + /** + * Copy an object + * + * @param string $srcBucket Source bucket name + * @param string $srcUri Source object URI + * @param string $bucket Destination bucket name + * @param string $uri Destination object URI + * @param constant $acl ACL constant + * @param array $metaHeaders Optional array of x-amz-meta-* headers + * @param array $requestHeaders Optional array of request headers (content type, disposition, etc.) + * @param constant $storageClass Storage class constant + * @return mixed | false + */ + public static function copyObject($srcBucket, $srcUri, $bucket, $uri, $acl = self::ACL_PRIVATE, $metaHeaders = array(), $requestHeaders = array(), $storageClass = self::STORAGE_CLASS_STANDARD) + { + $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + $rest->setHeader('Content-Length', 0); + foreach ($requestHeaders as $h => $v) + strpos($h, 'x-amz-') === 0 ? $rest->setAmzHeader($h, $v) : $rest->setHeader($h, $v); + foreach ($metaHeaders as $h => $v) $rest->setAmzHeader('x-amz-meta-'.$h, $v); + if ($storageClass !== self::STORAGE_CLASS_STANDARD) // Storage class + $rest->setAmzHeader('x-amz-storage-class', $storageClass); + $rest->setAmzHeader('x-amz-acl', $acl); + $rest->setAmzHeader('x-amz-copy-source', sprintf('/%s/%s', $srcBucket, rawurlencode($srcUri))); + if (sizeof($requestHeaders) > 0 || sizeof($metaHeaders) > 0) + $rest->setAmzHeader('x-amz-metadata-directive', 'REPLACE'); + + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::copyObject({$srcBucket}, {$srcUri}, {$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return isset($rest->body->LastModified, $rest->body->ETag) ? array( + 'time' => strtotime((string)$rest->body->LastModified), + 'hash' => substr((string)$rest->body->ETag, 1, -1) + ) : false; + } + + + /** + * Set up a bucket redirection + * + * @param string $bucket Bucket name + * @param string $location Target host name + * @return boolean + */ + public static function setBucketRedirect($bucket = NULL, $location = NULL) + { + $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + + if( empty($bucket) || empty($location) ) { + self::__triggerError("S3::setBucketRedirect({$bucket}, {$location}): Empty parameter.", __FILE__, __LINE__); + return false; + } + + $dom = new DOMDocument; + $websiteConfiguration = $dom->createElement('WebsiteConfiguration'); + $redirectAllRequestsTo = $dom->createElement('RedirectAllRequestsTo'); + $hostName = $dom->createElement('HostName', $location); + $redirectAllRequestsTo->appendChild($hostName); + $websiteConfiguration->appendChild($redirectAllRequestsTo); + $dom->appendChild($websiteConfiguration); + $rest->setParameter('website', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::setBucketRedirect({$bucket}, {$location}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Set logging for a bucket + * + * @param string $bucket Bucket name + * @param string $targetBucket Target bucket (where logs are stored) + * @param string $targetPrefix Log prefix (e,g; domain.com-) + * @return boolean + */ + public static function setBucketLogging($bucket, $targetBucket, $targetPrefix = null) + { + // The S3 log delivery group has to be added to the target bucket's ACP + if ($targetBucket !== null && ($acp = self::getAccessControlPolicy($targetBucket, '')) !== false) + { + // Only add permissions to the target bucket when they do not exist + $aclWriteSet = false; + $aclReadSet = false; + foreach ($acp['acl'] as $acl) + if ($acl['type'] == 'Group' && $acl['uri'] == 'http://acs.amazonaws.com/groups/s3/LogDelivery') + { + if ($acl['permission'] == 'WRITE') $aclWriteSet = true; + elseif ($acl['permission'] == 'READ_ACP') $aclReadSet = true; + } + if (!$aclWriteSet) $acp['acl'][] = array( + 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'WRITE' + ); + if (!$aclReadSet) $acp['acl'][] = array( + 'type' => 'Group', 'uri' => 'http://acs.amazonaws.com/groups/s3/LogDelivery', 'permission' => 'READ_ACP' + ); + if (!$aclReadSet || !$aclWriteSet) self::setAccessControlPolicy($targetBucket, '', $acp); + } + + $dom = new DOMDocument; + $bucketLoggingStatus = $dom->createElement('BucketLoggingStatus'); + $bucketLoggingStatus->setAttribute('xmlns', 'http://s3.amazonaws.com/doc/2006-03-01/'); + if ($targetBucket !== null) + { + if ($targetPrefix == null) $targetPrefix = $bucket . '-'; + $loggingEnabled = $dom->createElement('LoggingEnabled'); + $loggingEnabled->appendChild($dom->createElement('TargetBucket', $targetBucket)); + $loggingEnabled->appendChild($dom->createElement('TargetPrefix', $targetPrefix)); + // TODO: Add TargetGrants? + $bucketLoggingStatus->appendChild($loggingEnabled); + } + $dom->appendChild($bucketLoggingStatus); + + $rest = new S3Request('PUT', $bucket, '', self::$endpoint); + $rest->setParameter('logging', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::setBucketLogging({$bucket}, {$targetBucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Get logging status for a bucket + * + * This will return false if logging is not enabled. + * Note: To enable logging, you also need to grant write access to the log group + * + * @param string $bucket Bucket name + * @return array | false + */ + public static function getBucketLogging($bucket) + { + $rest = new S3Request('GET', $bucket, '', self::$endpoint); + $rest->setParameter('logging', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getBucketLogging({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + if (!isset($rest->body->LoggingEnabled)) return false; // No logging + return array( + 'targetBucket' => (string)$rest->body->LoggingEnabled->TargetBucket, + 'targetPrefix' => (string)$rest->body->LoggingEnabled->TargetPrefix, + ); + } + + + /** + * Disable bucket logging + * + * @param string $bucket Bucket name + * @return boolean + */ + public static function disableBucketLogging($bucket) + { + return self::setBucketLogging($bucket, null); + } + + + /** + * Get a bucket's location + * + * @param string $bucket Bucket name + * @return string | false + */ + public static function getBucketLocation($bucket) + { + $rest = new S3Request('GET', $bucket, '', self::$endpoint); + $rest->setParameter('location', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getBucketLocation({$bucket}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return (isset($rest->body[0]) && (string)$rest->body[0] !== '') ? (string)$rest->body[0] : 'US'; + } + + + /** + * Set object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param array $acp Access Control Policy Data (same as the data returned from getAccessControlPolicy) + * @return boolean + */ + public static function setAccessControlPolicy($bucket, $uri = '', $acp = array()) + { + $dom = new DOMDocument; + $dom->formatOutput = true; + $accessControlPolicy = $dom->createElement('AccessControlPolicy'); + $accessControlList = $dom->createElement('AccessControlList'); + + // It seems the owner has to be passed along too + $owner = $dom->createElement('Owner'); + $owner->appendChild($dom->createElement('ID', $acp['owner']['id'])); + $owner->appendChild($dom->createElement('DisplayName', $acp['owner']['name'])); + $accessControlPolicy->appendChild($owner); + + foreach ($acp['acl'] as $g) + { + $grant = $dom->createElement('Grant'); + $grantee = $dom->createElement('Grantee'); + $grantee->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + if (isset($g['id'])) + { // CanonicalUser (DisplayName is omitted) + $grantee->setAttribute('xsi:type', 'CanonicalUser'); + $grantee->appendChild($dom->createElement('ID', $g['id'])); + } + elseif (isset($g['email'])) + { // AmazonCustomerByEmail + $grantee->setAttribute('xsi:type', 'AmazonCustomerByEmail'); + $grantee->appendChild($dom->createElement('EmailAddress', $g['email'])); + } + elseif ($g['type'] == 'Group') + { // Group + $grantee->setAttribute('xsi:type', 'Group'); + $grantee->appendChild($dom->createElement('URI', $g['uri'])); + } + $grant->appendChild($grantee); + $grant->appendChild($dom->createElement('Permission', $g['permission'])); + $accessControlList->appendChild($grant); + } + + $accessControlPolicy->appendChild($accessControlList); + $dom->appendChild($accessControlPolicy); + + $rest = new S3Request('PUT', $bucket, $uri, self::$endpoint); + $rest->setParameter('acl', null); + $rest->data = $dom->saveXML(); + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::setAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Get object or bucket Access Control Policy + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return mixed | false + */ + public static function getAccessControlPolicy($bucket, $uri = '') + { + $rest = new S3Request('GET', $bucket, $uri, self::$endpoint); + $rest->setParameter('acl', null); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getAccessControlPolicy({$bucket}, {$uri}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + + $acp = array(); + if (isset($rest->body->Owner, $rest->body->Owner->ID, $rest->body->Owner->DisplayName)) + $acp['owner'] = array( + 'id' => (string)$rest->body->Owner->ID, 'name' => (string)$rest->body->Owner->DisplayName + ); + + if (isset($rest->body->AccessControlList)) + { + $acp['acl'] = array(); + foreach ($rest->body->AccessControlList->Grant as $grant) + { + foreach ($grant->Grantee as $grantee) + { + if (isset($grantee->ID, $grantee->DisplayName)) // CanonicalUser + $acp['acl'][] = array( + 'type' => 'CanonicalUser', + 'id' => (string)$grantee->ID, + 'name' => (string)$grantee->DisplayName, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->EmailAddress)) // AmazonCustomerByEmail + $acp['acl'][] = array( + 'type' => 'AmazonCustomerByEmail', + 'email' => (string)$grantee->EmailAddress, + 'permission' => (string)$grant->Permission + ); + elseif (isset($grantee->URI)) // Group + $acp['acl'][] = array( + 'type' => 'Group', + 'uri' => (string)$grantee->URI, + 'permission' => (string)$grant->Permission + ); + else continue; + } + } + } + return $acp; + } + + + /** + * Delete an object + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @return boolean + */ + public static function deleteObject($bucket, $uri) + { + $rest = new S3Request('DELETE', $bucket, $uri, self::$endpoint); + $rest = $rest->getResponse(); + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::deleteObject(): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Get a query string authenticated URL + * + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param integer $lifetime Lifetime in seconds + * @param boolean $hostBucket Use the bucket name as the hostname + * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) + * @return string + */ + public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) + { + $expires = self::__getTime() + $lifetime; + $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); + return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', + // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, + $hostBucket ? $bucket : self::$endpoint.'/'.$bucket, $uri, self::$__accessKey, $expires, + urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); + } + + + /** + * Get a CloudFront signed policy URL + * + * @param array $policy Policy + * @return string + */ + public static function getSignedPolicyURL($policy) + { + $data = json_encode($policy); + $signature = ''; + if (!openssl_sign($data, $signature, self::$__signingKeyResource)) return false; + + $encoded = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($data)); + $signature = str_replace(array('+', '='), array('-', '_', '~'), base64_encode($signature)); + + $url = $policy['Statement'][0]['Resource'] . '?'; + foreach (array('Policy' => $encoded, 'Signature' => $signature, 'Key-Pair-Id' => self::$__signingKeyPairId) as $k => $v) + $url .= $k.'='.str_replace('%2F', '/', rawurlencode($v)).'&'; + return substr($url, 0, -1); + } + + + /** + * Get a CloudFront canned policy URL + * + * @param string $url URL to sign + * @param integer $lifetime URL lifetime + * @return string + */ + public static function getSignedCannedURL($url, $lifetime) + { + return self::getSignedPolicyURL(array( + 'Statement' => array( + array('Resource' => $url, 'Condition' => array( + 'DateLessThan' => array('AWS:EpochTime' => self::__getTime() + $lifetime) + )) + ) + )); + } + + + /** + * Get upload POST parameters for form uploads + * + * @param string $bucket Bucket name + * @param string $uriPrefix Object URI prefix + * @param constant $acl ACL constant + * @param integer $lifetime Lifetime in seconds + * @param integer $maxFileSize Maximum filesize in bytes (default 5MB) + * @param string $successRedirect Redirect URL or 200 / 201 status code + * @param array $amzHeaders Array of x-amz-meta-* headers + * @param array $headers Array of request headers or content type as a string + * @param boolean $flashVars Includes additional "Filename" variable posted by Flash + * @return object + */ + public static function getHttpUploadPostParams($bucket, $uriPrefix = '', $acl = self::ACL_PRIVATE, $lifetime = 3600, + $maxFileSize = 5242880, $successRedirect = "201", $amzHeaders = array(), $headers = array(), $flashVars = false) + { + // Create policy object + $policy = new stdClass; + $policy->expiration = gmdate('Y-m-d\TH:i:s\Z', (self::__getTime() + $lifetime)); + $policy->conditions = array(); + $obj = new stdClass; $obj->bucket = $bucket; array_push($policy->conditions, $obj); + $obj = new stdClass; $obj->acl = $acl; array_push($policy->conditions, $obj); + + $obj = new stdClass; // 200 for non-redirect uploads + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $obj->success_action_status = (string)$successRedirect; + else // URL + $obj->success_action_redirect = $successRedirect; + array_push($policy->conditions, $obj); + + if ($acl !== self::ACL_PUBLIC_READ) + array_push($policy->conditions, array('eq', '$acl', $acl)); + + array_push($policy->conditions, array('starts-with', '$key', $uriPrefix)); + if ($flashVars) array_push($policy->conditions, array('starts-with', '$Filename', '')); + foreach (array_keys($headers) as $headerKey) + array_push($policy->conditions, array('starts-with', '$'.$headerKey, '')); + foreach ($amzHeaders as $headerKey => $headerVal) + { + $obj = new stdClass; + $obj->{$headerKey} = (string)$headerVal; + array_push($policy->conditions, $obj); + } + array_push($policy->conditions, array('content-length-range', 0, $maxFileSize)); + $policy = base64_encode(str_replace('\/', '/', json_encode($policy))); + + // Create parameters + $params = new stdClass; + $params->AWSAccessKeyId = self::$__accessKey; + $params->key = $uriPrefix.'${filename}'; + $params->acl = $acl; + $params->policy = $policy; unset($policy); + $params->signature = self::__getHash($params->policy); + if (is_numeric($successRedirect) && in_array((int)$successRedirect, array(200, 201))) + $params->success_action_status = (string)$successRedirect; + else + $params->success_action_redirect = $successRedirect; + foreach ($headers as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; + foreach ($amzHeaders as $headerKey => $headerVal) $params->{$headerKey} = (string)$headerVal; + return $params; + } + + + /** + * Create a CloudFront distribution + * + * @param string $bucket Bucket name + * @param boolean $enabled Enabled (true/false) + * @param array $cnames Array containing CNAME aliases + * @param string $comment Use the bucket name as the hostname + * @param string $defaultRootObject Default root object + * @param string $originAccessIdentity Origin access identity + * @param array $trustedSigners Array of trusted signers + * @return array | false + */ + public static function createDistribution($bucket, $enabled = true, $cnames = array(), $comment = null, $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + $useSSL = self::$useSSL; + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('POST', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontDistributionConfigXML( + $bucket.'.s3.amazonaws.com', + $enabled, + (string)$comment, + (string)microtime(true), + $cnames, + $defaultRootObject, + $originAccessIdentity, + $trustedSigners + ); + + $rest->size = strlen($rest->data); + $rest->setHeader('Content-Type', 'application/xml'); + $rest = self::__getCloudFrontResponse($rest); + + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 201) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::createDistribution({$bucket}, ".(int)$enabled.", [], '$comment'): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } elseif ($rest->body instanceof SimpleXMLElement) + return self::__parseCloudFrontDistributionConfig($rest->body); + return false; + } + + + /** + * Get CloudFront distribution info + * + * @param string $distributionId Distribution ID from listDistributions() + * @return array | false + */ + public static function getDistribution($distributionId) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::getDistribution($distributionId): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + $useSSL = self::$useSSL; + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId, 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::getDistribution($distributionId): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + elseif ($rest->body instanceof SimpleXMLElement) + { + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + $dist['id'] = $distributionId; + return $dist; + } + return false; + } + + + /** + * Update a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @return array | false + */ + public static function updateDistribution($dist) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('PUT', '', '2010-11-01/distribution/'.$dist['id'].'/config', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontDistributionConfigXML( + $dist['origin'], + $dist['enabled'], + $dist['comment'], + $dist['callerReference'], + $dist['cnames'], + $dist['defaultRootObject'], + $dist['originAccessIdentity'], + $dist['trustedSigners'] + ); + + $rest->size = strlen($rest->data); + $rest->setHeader('If-Match', $dist['hash']); + $rest = self::__getCloudFrontResponse($rest); + + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::updateDistribution({$dist['id']}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } else { + $dist = self::__parseCloudFrontDistributionConfig($rest->body); + $dist['hash'] = $rest->headers['hash']; + return $dist; + } + return false; + } + + + /** + * Delete a CloudFront distribution + * + * @param array $dist Distribution array info identical to output of getDistribution() + * @return boolean + */ + public static function deleteDistribution($dist) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('DELETE', '', '2008-06-30/distribution/'.$dist['id'], 'cloudfront.amazonaws.com'); + $rest->setHeader('If-Match', $dist['hash']); + $rest = self::__getCloudFrontResponse($rest); + + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 204) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::deleteDistribution({$dist['id']}): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + return true; + } + + + /** + * Get a list of CloudFront distributions + * + * @return array + */ + public static function listDistributions() + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/distribution', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + self::__triggerError(sprintf("S3::listDistributions(): [%s] %s", + $rest->error['code'], $rest->error['message']), __FILE__, __LINE__); + return false; + } + elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->DistributionSummary)) + { + $list = array(); + if (isset($rest->body->Marker, $rest->body->MaxItems, $rest->body->IsTruncated)) + { + //$info['marker'] = (string)$rest->body->Marker; + //$info['maxItems'] = (int)$rest->body->MaxItems; + //$info['isTruncated'] = (string)$rest->body->IsTruncated == 'true' ? true : false; + } + foreach ($rest->body->DistributionSummary as $summary) + $list[(string)$summary->Id] = self::__parseCloudFrontDistributionConfig($summary); + + return $list; + } + return array(); + } + + /** + * List CloudFront Origin Access Identities + * + * @return array + */ + public static function listOriginAccessIdentities() + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::listOriginAccessIdentities(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/origin-access-identity/cloudfront', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + $useSSL = self::$useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + trigger_error(sprintf("S3::listOriginAccessIdentities(): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + + if (isset($rest->body->CloudFrontOriginAccessIdentitySummary)) + { + $identities = array(); + foreach ($rest->body->CloudFrontOriginAccessIdentitySummary as $identity) + if (isset($identity->S3CanonicalUserId)) + $identities[(string)$identity->Id] = array('id' => (string)$identity->Id, 's3CanonicalUserId' => (string)$identity->S3CanonicalUserId); + return $identities; + } + return false; + } + + + /** + * Invalidate objects in a CloudFront distribution + * + * Thanks to Martin Lindkvist for S3::invalidateDistribution() + * + * @param string $distributionId Distribution ID from listDistributions() + * @param array $paths Array of object paths to invalidate + * @return boolean + */ + public static function invalidateDistribution($distributionId, $paths) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::invalidateDistribution(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('POST', '', '2010-08-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); + $rest->data = self::__getCloudFrontInvalidationBatchXML($paths, (string)microtime(true)); + $rest->size = strlen($rest->data); + $rest = self::__getCloudFrontResponse($rest); + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 201) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + trigger_error(sprintf("S3::invalidate('{$distributionId}',{$paths}): [%s] %s", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + return true; + } + + + /** + * Get a InvalidationBatch DOMDocument + * + * @internal Used to create XML in invalidateDistribution() + * @param array $paths Paths to objects to invalidateDistribution + * @param int $callerReference + * @return string + */ + private static function __getCloudFrontInvalidationBatchXML($paths, $callerReference = '0') + { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $invalidationBatch = $dom->createElement('InvalidationBatch'); + foreach ($paths as $path) + $invalidationBatch->appendChild($dom->createElement('Path', $path)); + + $invalidationBatch->appendChild($dom->createElement('CallerReference', $callerReference)); + $dom->appendChild($invalidationBatch); + return $dom->saveXML(); + } + + + /** + * List your invalidation batches for invalidateDistribution() in a CloudFront distribution + * + * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/ListInvalidation.html + * returned array looks like this: + * Array + * ( + * [I31TWB0CN9V6XD] => InProgress + * [IT3TFE31M0IHZ] => Completed + * [I12HK7MPO1UQDA] => Completed + * [I1IA7R6JKTC3L2] => Completed + * ) + * + * @param string $distributionId Distribution ID from listDistributions() + * @return array + */ + public static function getDistributionInvalidationList($distributionId) + { + if (!extension_loaded('openssl')) + { + self::__triggerError(sprintf("S3::getDistributionInvalidationList(): [%s] %s", + "CloudFront functionality requires SSL"), __FILE__, __LINE__); + return false; + } + + $useSSL = self::$useSSL; + self::$useSSL = true; // CloudFront requires SSL + $rest = new S3Request('GET', '', '2010-11-01/distribution/'.$distributionId.'/invalidation', 'cloudfront.amazonaws.com'); + $rest = self::__getCloudFrontResponse($rest); + self::$useSSL = $useSSL; + + if ($rest->error === false && $rest->code !== 200) + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + if ($rest->error !== false) + { + trigger_error(sprintf("S3::getDistributionInvalidationList('{$distributionId}'): [%s]", + $rest->error['code'], $rest->error['message']), E_USER_WARNING); + return false; + } + elseif ($rest->body instanceof SimpleXMLElement && isset($rest->body->InvalidationSummary)) + { + $list = array(); + foreach ($rest->body->InvalidationSummary as $summary) + $list[(string)$summary->Id] = (string)$summary->Status; + + return $list; + } + return array(); + } + + + /** + * Get a DistributionConfig DOMDocument + * + * http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?PutConfig.html + * + * @internal Used to create XML in createDistribution() and updateDistribution() + * @param string $bucket S3 Origin bucket + * @param boolean $enabled Enabled (true/false) + * @param string $comment Comment to append + * @param string $callerReference Caller reference + * @param array $cnames Array of CNAME aliases + * @param string $defaultRootObject Default root object + * @param string $originAccessIdentity Origin access identity + * @param array $trustedSigners Array of trusted signers + * @return string + */ + private static function __getCloudFrontDistributionConfigXML($bucket, $enabled, $comment, $callerReference = '0', $cnames = array(), $defaultRootObject = null, $originAccessIdentity = null, $trustedSigners = array()) + { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $distributionConfig = $dom->createElement('DistributionConfig'); + $distributionConfig->setAttribute('xmlns', 'http://cloudfront.amazonaws.com/doc/2010-11-01/'); + + $origin = $dom->createElement('S3Origin'); + $origin->appendChild($dom->createElement('DNSName', $bucket)); + if ($originAccessIdentity !== null) $origin->appendChild($dom->createElement('OriginAccessIdentity', $originAccessIdentity)); + $distributionConfig->appendChild($origin); + + if ($defaultRootObject !== null) $distributionConfig->appendChild($dom->createElement('DefaultRootObject', $defaultRootObject)); + + $distributionConfig->appendChild($dom->createElement('CallerReference', $callerReference)); + foreach ($cnames as $cname) + $distributionConfig->appendChild($dom->createElement('CNAME', $cname)); + if ($comment !== '') $distributionConfig->appendChild($dom->createElement('Comment', $comment)); + $distributionConfig->appendChild($dom->createElement('Enabled', $enabled ? 'true' : 'false')); + + $trusted = $dom->createElement('TrustedSigners'); + foreach ($trustedSigners as $id => $type) + $trusted->appendChild($id !== '' ? $dom->createElement($type, $id) : $dom->createElement($type)); + $distributionConfig->appendChild($trusted); + + $dom->appendChild($distributionConfig); + //var_dump($dom->saveXML()); + return $dom->saveXML(); + } + + + /** + * Parse a CloudFront distribution config + * + * See http://docs.amazonwebservices.com/AmazonCloudFront/latest/APIReference/index.html?GetDistribution.html + * + * @internal Used to parse the CloudFront DistributionConfig node to an array + * @param object &$node DOMNode + * @return array + */ + private static function __parseCloudFrontDistributionConfig(&$node) + { + if (isset($node->DistributionConfig)) + return self::__parseCloudFrontDistributionConfig($node->DistributionConfig); + + $dist = array(); + if (isset($node->Id, $node->Status, $node->LastModifiedTime, $node->DomainName)) + { + $dist['id'] = (string)$node->Id; + $dist['status'] = (string)$node->Status; + $dist['time'] = strtotime((string)$node->LastModifiedTime); + $dist['domain'] = (string)$node->DomainName; + } + + if (isset($node->CallerReference)) + $dist['callerReference'] = (string)$node->CallerReference; + + if (isset($node->Enabled)) + $dist['enabled'] = (string)$node->Enabled == 'true' ? true : false; + + if (isset($node->S3Origin)) + { + if (isset($node->S3Origin->DNSName)) + $dist['origin'] = (string)$node->S3Origin->DNSName; + + $dist['originAccessIdentity'] = isset($node->S3Origin->OriginAccessIdentity) ? + (string)$node->S3Origin->OriginAccessIdentity : null; + } + + $dist['defaultRootObject'] = isset($node->DefaultRootObject) ? (string)$node->DefaultRootObject : null; + + $dist['cnames'] = array(); + if (isset($node->CNAME)) + foreach ($node->CNAME as $cname) + $dist['cnames'][(string)$cname] = (string)$cname; + + $dist['trustedSigners'] = array(); + if (isset($node->TrustedSigners)) + foreach ($node->TrustedSigners as $signer) + { + if (isset($signer->Self)) + $dist['trustedSigners'][''] = 'Self'; + elseif (isset($signer->KeyPairId)) + $dist['trustedSigners'][(string)$signer->KeyPairId] = 'KeyPairId'; + elseif (isset($signer->AwsAccountNumber)) + $dist['trustedSigners'][(string)$signer->AwsAccountNumber] = 'AwsAccountNumber'; + } + + $dist['comment'] = isset($node->Comment) ? (string)$node->Comment : null; + return $dist; + } + + + /** + * Grab CloudFront response + * + * @internal Used to parse the CloudFront S3Request::getResponse() output + * @param object &$rest S3Request instance + * @return object + */ + private static function __getCloudFrontResponse(&$rest) + { + $rest->getResponse(); + if ($rest->response->error === false && isset($rest->response->body) && + is_string($rest->response->body) && substr($rest->response->body, 0, 5) == 'response->body = simplexml_load_string($rest->response->body); + // Grab CloudFront errors + if (isset($rest->response->body->Error, $rest->response->body->Error->Code, + $rest->response->body->Error->Message)) + { + $rest->response->error = array( + 'code' => (string)$rest->response->body->Error->Code, + 'message' => (string)$rest->response->body->Error->Message + ); + unset($rest->response->body); + } + } + return $rest->response; + } + + + /** + * Get MIME type for file + * + * To override the putObject() Content-Type, add it to $requestHeaders + * + * To use fileinfo, ensure the MAGIC environment variable is set + * + * @internal Used to get mime types + * @param string &$file File path + * @return string + */ + private static function __getMIMEType(&$file) + { + static $exts = array( + 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', + 'png' => 'image/png', 'ico' => 'image/x-icon', 'pdf' => 'application/pdf', + 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', 'swf' => 'application/x-shockwave-flash', + 'zip' => 'application/zip', 'gz' => 'application/x-gzip', + 'tar' => 'application/x-tar', 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', 'txt' => 'text/plain', + 'asc' => 'text/plain', 'htm' => 'text/html', 'html' => 'text/html', + 'css' => 'text/css', 'js' => 'text/javascript', + 'xml' => 'text/xml', 'xsl' => 'application/xsl+xml', + 'ogg' => 'application/ogg', 'mp3' => 'audio/mpeg', 'wav' => 'audio/x-wav', + 'avi' => 'video/x-msvideo', 'mpg' => 'video/mpeg', 'mpeg' => 'video/mpeg', + 'mov' => 'video/quicktime', 'flv' => 'video/x-flv', 'php' => 'text/x-php' + ); + + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + if (isset($exts[$ext])) return $exts[$ext]; + + // Use fileinfo if available + if (extension_loaded('fileinfo') && isset($_ENV['MAGIC']) && + ($finfo = finfo_open(FILEINFO_MIME, $_ENV['MAGIC'])) !== false) + { + if (($type = finfo_file($finfo, $file)) !== false) + { + // Remove the charset and grab the last content-type + $type = explode(' ', str_replace('; charset=', ';charset=', $type)); + $type = array_pop($type); + $type = explode(';', $type); + $type = trim(array_shift($type)); + } + finfo_close($finfo); + if ($type !== false && strlen($type) > 0) return $type; + } + + return 'application/octet-stream'; + } + + + /** + * Get the current time + * + * @internal Used to apply offsets to sytem time + * @return integer + */ + public static function __getTime() + { + return time() + self::$__timeOffset; + } + + + /** + * Generate the auth string: "AWS AccessKey:Signature" + * + * @internal Used by S3Request::getResponse() + * @param string $string String to sign + * @return string + */ + public static function __getSignature($string) + { + return 'AWS '.self::$__accessKey.':'.self::__getHash($string); + } + + + /** + * Creates a HMAC-SHA1 hash + * + * This uses the hash extension if loaded + * + * @internal Used by __getSignature() + * @param string $string String to sign + * @return string + */ + private static function __getHash($string) + { + return base64_encode(extension_loaded('hash') ? + hash_hmac('sha1', $string, self::$__secretKey, true) : pack('H*', sha1( + (str_pad(self::$__secretKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . + pack('H*', sha1((str_pad(self::$__secretKey, 64, chr(0x00)) ^ + (str_repeat(chr(0x36), 64))) . $string))))); + } + +} + +/** + * S3 Request class + * + * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class + * @version 0.5.0-dev + */ +final class S3Request +{ + /** + * AWS URI + * + * @var string + * @access pricate + */ + private $endpoint; + + /** + * Verb + * + * @var string + * @access private + */ + private $verb; + + /** + * S3 bucket name + * + * @var string + * @access private + */ + private $bucket; + + /** + * Object URI + * + * @var string + * @access private + */ + private $uri; + + /** + * Final object URI + * + * @var string + * @access private + */ + private $resource = ''; + + /** + * Additional request parameters + * + * @var array + * @access private + */ + private $parameters = array(); + + /** + * Amazon specific request headers + * + * @var array + * @access private + */ + private $amzHeaders = array(); + + /** + * HTTP request headers + * + * @var array + * @access private + */ + private $headers = array( + 'Host' => '', 'Date' => '', 'Content-MD5' => '', 'Content-Type' => '' + ); + + /** + * Use HTTP PUT? + * + * @var bool + * @access public + */ + public $fp = false; + + /** + * PUT file size + * + * @var int + * @access public + */ + public $size = 0; + + /** + * PUT post fields + * + * @var array + * @access public + */ + public $data = false; + + /** + * S3 request respone + * + * @var object + * @access public + */ + public $response; + + + /** + * Constructor + * + * @param string $verb Verb + * @param string $bucket Bucket name + * @param string $uri Object URI + * @param string $endpoint AWS endpoint URI + * @return mixed + */ + function __construct($verb, $bucket = '', $uri = '', $endpoint = 's3.amazonaws.com') + { + + $this->endpoint = $endpoint; + $this->verb = $verb; + $this->bucket = $bucket; + $this->uri = $uri !== '' ? '/'.str_replace('%2F', '/', rawurlencode($uri)) : '/'; + + //if ($this->bucket !== '') + // $this->resource = '/'.$this->bucket.$this->uri; + //else + // $this->resource = $this->uri; + + if ($this->bucket !== '') + { + if ($this->__dnsBucketName($this->bucket)) + { + $this->headers['Host'] = $this->bucket.'.'.$this->endpoint; + $this->resource = '/'.$this->bucket.$this->uri; + } + else + { + $this->headers['Host'] = $this->endpoint; + $this->uri = $this->uri; + if ($this->bucket !== '') $this->uri = '/'.$this->bucket.$this->uri; + $this->bucket = ''; + $this->resource = $this->uri; + } + } + else + { + $this->headers['Host'] = $this->endpoint; + $this->resource = $this->uri; + } + + + $this->headers['Date'] = gmdate('D, d M Y H:i:s T'); + $this->response = new STDClass; + $this->response->error = false; + $this->response->body = null; + $this->response->headers = array(); + } + + + /** + * Set request parameter + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setParameter($key, $value) + { + $this->parameters[$key] = $value; + } + + + /** + * Set request header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setHeader($key, $value) + { + $this->headers[$key] = $value; + } + + + /** + * Set x-amz-meta-* header + * + * @param string $key Key + * @param string $value Value + * @return void + */ + public function setAmzHeader($key, $value) + { + $this->amzHeaders[$key] = $value; + } + + + /** + * Get the S3 response + * + * @return object | false + */ + public function getResponse() + { + $query = ''; + if (sizeof($this->parameters) > 0) + { + $query = substr($this->uri, -1) !== '?' ? '?' : '&'; + foreach ($this->parameters as $var => $value) + if ($value == null || $value == '') $query .= $var.'&'; + else $query .= $var.'='.rawurlencode($value).'&'; + $query = substr($query, 0, -1); + $this->uri .= $query; + + if (array_key_exists('acl', $this->parameters) || + array_key_exists('location', $this->parameters) || + array_key_exists('torrent', $this->parameters) || + array_key_exists('website', $this->parameters) || + array_key_exists('logging', $this->parameters)) + $this->resource .= $query; + } + $url = (S3::$useSSL ? 'https://' : 'http://') . ($this->headers['Host'] !== '' ? $this->headers['Host'] : $this->endpoint) . $this->uri; + + //var_dump('bucket: ' . $this->bucket, 'uri: ' . $this->uri, 'resource: ' . $this->resource, 'url: ' . $url); + + // Basic setup + $curl = curl_init(); + curl_setopt($curl, CURLOPT_USERAGENT, 'S3/php'); + + if (S3::$useSSL) + { + // Set protocol version + curl_setopt($curl, CURLOPT_SSLVERSION, S3::$useSSLVersion); + + // SSL Validation can now be optional for those with broken OpenSSL installations + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, S3::$useSSLValidation ? 2 : 0); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, S3::$useSSLValidation ? 1 : 0); + + if (S3::$sslKey !== null) curl_setopt($curl, CURLOPT_SSLKEY, S3::$sslKey); + if (S3::$sslCert !== null) curl_setopt($curl, CURLOPT_SSLCERT, S3::$sslCert); + if (S3::$sslCACert !== null) curl_setopt($curl, CURLOPT_CAINFO, S3::$sslCACert); + } + + curl_setopt($curl, CURLOPT_URL, $url); + + if (S3::$proxy != null && isset(S3::$proxy['host'])) + { + curl_setopt($curl, CURLOPT_PROXY, S3::$proxy['host']); + curl_setopt($curl, CURLOPT_PROXYTYPE, S3::$proxy['type']); + if (isset(S3::$proxy['user'], S3::$proxy['pass']) && S3::$proxy['user'] != null && S3::$proxy['pass'] != null) + curl_setopt($curl, CURLOPT_PROXYUSERPWD, sprintf('%s:%s', S3::$proxy['user'], S3::$proxy['pass'])); + } + + // Headers + $headers = array(); $amz = array(); + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + foreach ($this->headers as $header => $value) + if (strlen($value) > 0) $headers[] = $header.': '.$value; + + // Collect AMZ headers for signature + foreach ($this->amzHeaders as $header => $value) + if (strlen($value) > 0) $amz[] = strtolower($header).':'.$value; + + // AMZ headers must be sorted + if (sizeof($amz) > 0) + { + //sort($amz); + usort($amz, array(&$this, '__sortMetaHeadersCmp')); + $amz = "\n".implode("\n", $amz); + } else $amz = ''; + + if (S3::hasAuth()) + { + // Authorization string (CloudFront stringToSign should only contain a date) + if ($this->headers['Host'] == 'cloudfront.amazonaws.com') + $headers[] = 'Authorization: ' . S3::__getSignature($this->headers['Date']); + else + { + $headers[] = 'Authorization: ' . S3::__getSignature( + $this->verb."\n". + $this->headers['Content-MD5']."\n". + $this->headers['Content-Type']."\n". + $this->headers['Date'].$amz."\n". + $this->resource + ); + } + } + + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); + curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); + curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this, '__responseHeaderCallback')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + + // Request types + switch ($this->verb) + { + case 'GET': break; + case 'PUT': case 'POST': // POST only used for CloudFront + if ($this->fp !== false) + { + curl_setopt($curl, CURLOPT_PUT, true); + curl_setopt($curl, CURLOPT_INFILE, $this->fp); + if ($this->size >= 0) + curl_setopt($curl, CURLOPT_INFILESIZE, $this->size); + } + elseif ($this->data !== false) + { + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + curl_setopt($curl, CURLOPT_POSTFIELDS, $this->data); + } + else + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + break; + case 'HEAD': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'HEAD'); + curl_setopt($curl, CURLOPT_NOBODY, true); + break; + case 'DELETE': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + default: break; + } + + // Execute, grab errors + if (curl_exec($curl)) + $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + else + $this->response->error = array( + 'code' => curl_errno($curl), + 'message' => curl_error($curl), + 'resource' => $this->resource + ); + + @curl_close($curl); + + // Parse body into XML + if ($this->response->error === false && isset($this->response->headers['type']) && + $this->response->headers['type'] == 'application/xml' && isset($this->response->body)) + { + $this->response->body = simplexml_load_string($this->response->body); + + // Grab S3 errors + if (!in_array($this->response->code, array(200, 204, 206)) && + isset($this->response->body->Code, $this->response->body->Message)) + { + $this->response->error = array( + 'code' => (string)$this->response->body->Code, + 'message' => (string)$this->response->body->Message + ); + if (isset($this->response->body->Resource)) + $this->response->error['resource'] = (string)$this->response->body->Resource; + unset($this->response->body); + } + } + + // Clean up file resources + if ($this->fp !== false && is_resource($this->fp)) fclose($this->fp); + + return $this->response; + } + + /** + * Sort compare for meta headers + * + * @internal Used to sort x-amz meta headers + * @param string $a String A + * @param string $b String B + * @return integer + */ + private function __sortMetaHeadersCmp($a, $b) + { + $lenA = strpos($a, ':'); + $lenB = strpos($b, ':'); + $minLen = min($lenA, $lenB); + $ncmp = strncmp($a, $b, $minLen); + if ($lenA == $lenB) return $ncmp; + if (0 == $ncmp) return $lenA < $lenB ? -1 : 1; + return $ncmp; + } + + /** + * CURL write callback + * + * @param resource &$curl CURL resource + * @param string &$data Data + * @return integer + */ + private function __responseWriteCallback(&$curl, &$data) + { + if (in_array($this->response->code, array(200, 206)) && $this->fp !== false) + return fwrite($this->fp, $data); + else + $this->response->body .= $data; + return strlen($data); + } + + + /** + * Check DNS conformity + * + * @param string $bucket Bucket name + * @return boolean + */ + private function __dnsBucketName($bucket) + { + if (strlen($bucket) > 63 || preg_match("/[^a-z0-9\.-]/", $bucket) > 0) return false; + if (S3::$useSSL && strstr($bucket, '.') !== false) return false; + if (strstr($bucket, '-.') !== false) return false; + if (strstr($bucket, '..') !== false) return false; + if (!preg_match("/^[0-9a-z]/", $bucket)) return false; + if (!preg_match("/[0-9a-z]$/", $bucket)) return false; + return true; + } + + + /** + * CURL header callback + * + * @param resource $curl CURL resource + * @param string $data Data + * @return integer + */ + private function __responseHeaderCallback($curl, $data) + { + if (($strlen = strlen($data)) <= 2) return $strlen; + if (substr($data, 0, 4) == 'HTTP') + $this->response->code = (int)substr($data, 9, 3); + else + { + $data = trim($data); + if (strpos($data, ': ') === false) return $strlen; + list($header, $value) = explode(': ', $data, 2); + if ($header == 'Last-Modified') + $this->response->headers['time'] = strtotime($value); + elseif ($header == 'Date') + $this->response->headers['date'] = strtotime($value); + elseif ($header == 'Content-Length') + $this->response->headers['size'] = (int)$value; + elseif ($header == 'Content-Type') + $this->response->headers['type'] = $value; + elseif ($header == 'ETag') + $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; + elseif (preg_match('/^x-amz-meta-.*$/', $header)) + $this->response->headers[$header] = $value; + } + return $strlen; + } + +} + +/** + * S3 exception class + * + * @link http://undesigned.org.za/2007/10/22/amazon-s3-php-class + * @version 0.5.0-dev + */ + +class S3Exception extends Exception { + /** + * Class constructor + * + * @param string $message Exception message + * @param string $file File in which exception was created + * @param string $line Line number on which exception was created + * @param int $code Exception code + */ + function __construct($message, $file, $line, $code = 0) + { + parent::__construct($message, $code); + $this->file = $file; + $this->line = $line; + } +} diff --git a/web/index.php b/web/index.php index 83f35c81..b7a8fe36 100644 --- a/web/index.php +++ b/web/index.php @@ -5,6 +5,19 @@ include __DIR__ . '/../bootstrap.php'; define('IS_PRODUCTION', $_SERVER['SERVER_NAME'] == 'lbry.io'); -i18n::register(); -Session::init(); -Controller::dispatch(strtok($_SERVER['REQUEST_URI'], '?')); +try +{ + i18n::register(); + Session::init(); + Controller::dispatch(strtok($_SERVER['REQUEST_URI'], '?')); +} +catch(Exception $e) +{ + if (IS_PRODUCTION) + { + throw $e; + } + + http_response_code(500); + echo '
'.Debug::exceptionToString($e).'
'; +} \ No newline at end of file diff --git a/web/osx.dmg b/web/osx.dmg deleted file mode 100644 index c5865bc0902fd95430b97adaee6f39545b853703..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92778 zcmeFa2T)T{+b(LaG(kX`s0c_`TIi^N^xg@*6M79Du>gXAbSa_t7Ac_w0xBI09YTNv zksb&jkWfRe|2g+N=l{NY&dhh_%$+;uj(a9+_GIt1R%Wf|efRr5&)O@27xANYH@{!F zdu5c){6WiWNJTf>y9Xw>Hm<&niFpsPYkB|h)CB(e4abwuy~7u=p4;r^g!)PbRbZo% zBl2tvQ~K682*KZW`bAEj+c0y7O_?;&$JLfoKXQ~3UEQF$FFksL=E3vV7cO49^1pKu z*?AS4c7+BK_~++FUifo#4EMIoj2nSI=?$h9TzE}4d!K)HKQLjgSvF6=shn~ zUS6zMSDuQTd~$F3;e`t<9yHLvi+0xct`d9 zJNkid>E&k*t4TY_x{#uWuW2t`5aOYC#MwMl$0f$K5||JOqd1)i^Pks`{(Pl$^7dxv zbYC)_`PscujeS_e9nnkL_H4--V5WTgQe2T-}-r5tENJUS81>=X4rt z3%n8~)*anyVs_;eAi zm*d8!6)a#3m^S1XKlvxM5BEdM{B4uSoHxJ`h_o=k(%4

VGQ*PJabq zQhj9iR>qmM@=1=lW3r8i!!Nz^Y}>Wya?gtQkUh@zN;bKtVNykjwin6}D(?NOV54nlra3crW2w}4*-zl5{<^8Ho{c*jGMQ1@!_i{3#PL_lq z7(?FTe&{$IX1dm!aaaXxh{@sV458VN+9uJY-$Wxs0ImfUo+V=CsPOW_#Va(=|4Z>R zBI@<==b*AytV~}HWDEqiXwwa~l6kT>ekw?Qs0UudksnP}5a&~tqMlZ?g-uzu!r!9I zU$zTI<7&^|XoU!_kLw5iqp^Q9E0^`!Uxy-A^U)6fNQ8QL5wycFwY6eL-Jz_5`A3JI z998WLty)%6@dE<-^-1;fl7pvWVzp#4>DUvO^1;A#_Ei+aqp7+yys7;_S{O;|7a3en?m9#Tg$axuhg@(|)T{w3q91$M~SE*$rUVGe+ z(>qY_<80GkpucTH_iNk=bq@EPKM?Ye^Clnm!MME%*TH$SVjd^N<1kP1*k#+92mcxT z|BA+cSOsJLbKdhVE}3mZz##No^He@?M~dn2cg(HJ_dbwyIesv z$v`0?t$;G#`|196RXN-f?-=BV?~xl;zXB#YMVN0t)9rl1d)KrcpU|eHP>;AX_2`{y zs6b`~&4kvRSr{lh*!57pMqIXj``x#en)qwbkJX>3w347A7@sDTcK=qQnC@eM!IP1C zpnVnR@1$u3T=Ei61ihGkJ9*MSMGwPG9AU;hB-|E+8KyPN1`yYuJ)uL^8zy+wlNe#s zob~E#%^$=RNcXpBkJX=cnp+_rJ(x3oHn5OdGG0RV!C6J*OPT5$xZf;wOk|axBUOfd zP7FDFRWC<I@$S{)Am(>}H>sh`z>#+A^iW7)q^3CfY zcP1k8wvxB4&Va{KnQ}crMVgv@_5&OqW}G7W%I9Y)PDURF;4CS^cE%}8bWkpT#9P)srB>roX0> zd`8YLn*+Hl=i*jiVQDhgHr)IA+})?o5BU}}0wQ9-wEjem3=7=6^r>Xwv_2jPaYy*o z=x!oYEkF@+u|>f`**u3}Sb37gEV!D8=E9vn1m($uR}_QtnB^ltd3y~5f-7e>jIhx3 zt?`aB6dj>fRK0^ye9#pF;;GcgMfwk%NOJ{e-W5>w$6;2s3eQ(nUakVNhQq;6U55JA z)C_th@|+4Y253tZN8Ci<(I5W{D$UO>8Jl!wG;<22hde0r-Ah65H$+(Tz*v7Y`I+TT zvrpNb2$-gs>I+At9Q&-|RLPFv&+LqQ(?6b2G`rNfM2VNnmWPv5#FIUPaU>H`Uv*Ro z$I@&IQGU|ta@OV}7jlvDHpR{V<|F>ASx3fMJiwgy?Plts(8Tc7-HHZ*ar;+Jz<{I? zdD$O7A1zn}Q2%q4pO+Z!9iLlKC3}1Fj`K@;&}E&FqHzWX?I-6}ob+Ab7iF@Hej7Y8 z|Blu1L}qe~kTpX3iR4M}xx;|VRB|TGas3p83w=u)-6GyMnCz}LHPDKzzgpO0t`58P zG?}0JdN#|Z0i7@8e6qSrUY>9$G^m^Mw5aOVutAdLZf50yK zo)P@W`=fJZwV^08i2*NQ;*ZS7-Is+aUa)v&LX^YgJbU&k=c+tRd2-A&(-$YB{MNeW z8gr)%#i~}S%^C_^JD~So)dVHivh5wyMrz*tT|41l=#=61498bky3}gXN>wvq%TPlK zJ{~(-?Imd5v6309$C@5{jFi+fl{3LD8-~m(r;Ko$!aaoO(1Pd1k~ArC7_Gz(iGxot zRA{+iZp`~4N+lzw6;<$$BIDp1qv+KGVkdqdrU-~CB6N6(&JxVZn#U zIT=hEeHCBS$#-eD%Okla9x(`|${a#A2ca@YA#wA2&`yO%o)B{;NyW**_Yv`%6-Gv> z`8_WoC0!uywiGYNIaUK4sVwAW)KS!Fvn9$)JtZb4F0S-m)7L0BkiTlObSUY(rzu?4 z$!xfQ9$>IwU>MZ36%A*5*MhgvXgTv1R~9O4tK}7W^`~!OoVtuG0rg zC#NNCB{f5YqO5^dE%)bEr=OcmSnHa*?T9LYh_GqwtiS|ZBjPlL{G&s_^TTP<_Z@$N z?3tVLhcA>(;S)J{cjPqLd_f#|KM%V#z=nI`J^>_z?7K>#GO0hq1?!-*tlC$Y@qFJm z?apT$+oV@LDcsy*F>K93uCZ5*uG+GE_m2!T{nEHz9;W$#ff}H+M7*`;aJlKk`S^Kr zDJkU`f_AhK1O*Q~0h&$g-nS%&H`9zbQ9vXnb&S{IGLU>0yC#t>g`K?)Rbq}ns{TTa z>HtrF`gzXTkXd-hTmlntN3@wS!;PGb?I`S?Z!!+qKyl$2@viCzUDKF!!TsOBxey~v zQz_o$_w?z<8UaaQ)j?Uc1salxzr7|Ch*%;(SKW|jF2>(3hnW3PE`xzR4xN=@v7YlX z?G6!-=1!JK?h3?oY*F;A6=*~tBW)Ye`@rUW__z$%E)4WT<+KYY#ZS3pVIy>cl__w`<5Poc;{ZnJU@_sbJIh*bcc?B z`LGIaxxuEPm_>@_yRLS5yINtN*oY`xXhnVAgcp?$oq?wE17SFeJM+e3#UF^3ht0;6 zIN$k9S$F7NOsjFVFQcC$m6hdWb7^2a(&AU4%8$JJ=Aqin;#2Gi zVkd6|#^6V0M?!I>nU;m_CeIc59CrkXavoYsKYjDEQS=K3hyTtHZ)hMiY+vGdw8OYaX z-uKPbZJ%cML;55NWVrw@SUOZXe)IO_S5KS>U*T<)nq&COV`bM1M<^8|Bxpz*ucarj zVi-uxj+==r=GU(JYh>WO5*RcN9>(;TCvEHu>SI5bF87T?I^X|hR| zfWnwH5^S_fol4yg^GF6w)F%h2vf@GR)60A!ej{Di8@bEmpsrDTpqxgg?ds*U<4n)( zjon&gNNDtEeAYneBm4lH5N`H|9WiUZW#ZIh*ThS$Lbp=(c>2CYNU3LH547!#L&U^k zq1U#|F{UB}ghb`Fk0BR@fRhkVs3%MJs-zz!~^*+uDxjQCh16|`a76~FK8 zQz^zjL^!H8biu`}{0@%9kL-Lz5WjxwdC#NAT!Tw@O3XZ6Mq|=*8`~E}Px3MeJ~b#N z{#YsP@B$Iula4kut&q1kI1ZoDwmMpiAsB29iTSz}1VNgz)hQ8Ot%aSoKZO%}7JW5g zrqq^}C!TOe5{g!Kd0Zia>E=xg(d76dP#ekrFspL{ZVcE=K=-Y@ud_?>P1l!Bsv8T$ zp3F{`Z7)>05on3;;V@(IApyz09>`nZpI!*Z7Ead1e|S424?Iw9tPHjZ>?wjLI>w< zqp`M))G$Of;$VU4e$B$fxsWn4w<*I7^N#eE(EJ(=s`jjv#BOxn zM(UY<(=}w=VRP8x7&O1S9Tc)XUzNe1Cp^-yBX(M4h@d8JM4yP&L2)gkLsYN@z&bkI z%5yRN)KkVCdJnwK)-hkhcIX)FG3VS`7t%xAXn0>zW;?aJl5r!<0pZZsVG-cH{8Is4 zurcLnBm}RS=&-Unf;5Lah9PYi>?6+b?FGYp8in~#5q)PAJ;q=iQsL_9dN}{6Q8UVM z*?vR1yJQ-J4%RFa4+OPn11Ru1O1Z!DT}*2ck|P^9Bt-|4KZW|2fDm7;8cVur%BbaM z&QFjcTe+NUC7jb(U)1R$mYwZKr^Yb#VDtfAi_D=LMVq$>7_27IF8bk5gSn zpevYZXRp=SWsA=s7Jb;dUuj!wuYDCFJji%0!~M-kk1d*cKSQy^?Y6|j4AH?lt~%YO zZ}*ZEU`Eda(*AT3SGKfG>i$kZCV9y$ddUpUyr0F^EA1gYvYreB@UFOtmaj?SFXv~h(zWxd(G5*h@q`y%&cwAJhU41N{GZ`PS#+pdAM&|7GH#_Y0(vg(wiIb>=W z&ehi!rA@fDb+QahIR(6W;Q5BT7+EMI388%=QS{YVF-|mbKrqX#M~wH-zwn$u6>WD8 zyA8Fp>dfEHg1Z_Yzdbi;`LQq`S`%`9=E@++U@+`IY}MrE5u`LF=F68Zl|GD^t*Z_# z2pKC>Te}p*q4J+H-hU*ie-hxxxK|IjefLoHKmpGL7+FX@%aDDza*e~~5%)2FOW+>e zV$9hVipwabWDo6Um9DNipdH37cT)=y_3oc|lK%+G*NR>%|Ij`s=bbHB^pdsH-k*h$ z$z|sw#t+ZAjx`S5IYt5^?A;E>wI^$JM!;kpHQBELeoY|Vr)dWLW+&*kYv)&9%CgNo z&Z9EtS{gOFnYTqNysEAg5iiSjq;22&odXg~Ji}DP7Y_xHCw$Br;){Rlx;aHidFjH} z>r+e9284@+Nn5A2_AaIBcM?Co26EOW=b-{iR7~9NeVA-et@0W#_fT|gFz2T4TVrh9 zo5=QiykJmKl7Mpt*c@sMF|YVlQuW_~dJS~?7LN=#aG^cyGh5W}7crQ=Dd*Q!9Me>!0=+p`g>eKxsdHeb-6XE zC-I@SVZzh(TD>+2JS%01{h8n*L*#j&TG4fxAknF@d$N_h4I`rQQ(05>y+;nBWwARN z^75ASHP3^~_k$bN(*R-NLZq~u`xtg(kWIb(_SyaHkoT;Hc_8cq*R@Q!eGT%ts`~Wf zw4F6Bxe1k}ssE;t8g1 z-(L;xSR7r{T7D-*o!I+0eCN2b_B&4P6MP!7@x#z{IT5&2H0?lu^VF*=b%wVi455ZQ znj3#u6h6pcm9@DgZdSA|y+UlVPK70B1M;e-2;>4+?Cjhwhskq8-wiEG*4hSM9tekP zRk_n|M{KV23<|iS#MYvs(^r9hK8neA=03 zN>+-N(UVM1iCSiUW}4Kc_1bwZ72xs7_75cQ66UIn0Yff#CR6=f)za*hqxDRwXV**r zLFz2sGV!Uu)^IM%_t`Ml!Twx)f8d9;qp9O?F;r7HlN}lJc^V`Xw-a|=3Hya>Ay_BgZBPmCCdtcR7zrR z2SYw`YQ8$C`m86ZE%pnfdNs1w=VejxbopuIUdskXh?OuZQHFefMMu&s3>R zqY&1N%Bf-?0sf0dS)pY;dV@z z7;d|z9r=Hc*MD*e|D3`8$Ay*~SM`3gH~IEN^v_VEeGG*iIhe?cvI=K?{cpoZu<3Na z*#+q(3Vz8AR~=EC=Gxy)bR$pj_Q{)_<_X)|H?$_>><^?w+YM1{~`qbkX~8n&lbjRU%)~-~H?n z^t%h+TPY@ZFwN}((CfH9?onJl@c19x+fQoVPj<&m;fePL2P=R0*{^B_hw9lUBj?@2 zhtDV*`klovwV2N!vIs)=_~Gl5oo6KZz=8f2#~UedcH=x?d5ABPvwPF0e7jp{eKO=l zM(+22A%h?6j$NsKYpdgyJVKVx$4LS2i$rTf+>?%QySt_t7W21Xy=N<3d^={2g zG6$d=WLVuis)v%7{^&yU|EvqWj(k{e#*GXvfjv+LPMF~X*%qYb{0`?GdyenSgY{;1~#@(i0wxi0(~n;-;HPsEWl!QfGx{-GE0oqavr|n;j-yNDtGj$ z)n^$+$*l@7C~pb;{wzyt>6r&>4j5__oLD$tmT&MJ0{#jw-VLz`qn4l3edPAGWQkU`^o?zKG9NYa|cEw>{-^Z|LWz^TJ^0e@jkl9SEsSNC!gh;(Ax>eESFib zZNYu`p)A`-!^oK^FN+734EzF8AS=3B}1WS)5KiE<{B4iqjrLvovZrr zG%tzA)i}V(@$|bSwdGDAsY((k5;1GI`!$zgf6N6BnUkH>t@*_Qp!P)P^*5adM$COe zlpL-X5Hn_dha5^mpm?)75|!`JPr9gZgx7tI&ru73FTtxs*!jky6&uFVzZy~B-9BPY z%eZM)cHa{0bu?`^R~77@SB2dmZnZkP8t%M6V9b4vOl>SoU4m?pa&kZ)IG(C}@f$SN_NWC4QlY~e?s!6(C)Zj;LS`*_;J#3k zHNo{CtR3y;o|?azv)j5wWG8(eOUcOa`&7nF*FXg=n@eY#wtI2OIHBQoj>FH;u?X;2 zWA>OMz;#6rZT&Fy$DjNkAA%PZXV`Bp(8V0%&XQE-pB-*a)Myf-?czJE)f)xntR)@s z5dNwL?2fppyAin6!=oJ8W-wA0umWdT3+!~BY4j-Cykqfs<-Nn#*|arbV>n_P#->z| z$u6}_$M>tT*{WZS&^T$s{h z1rbo(Ackr|Yw6_s>pO#{vR}4qLHd5D&kr^t*L^RKu~R8VNc|jc1Z#qwHl}o0M-MszAWZK({Hv(``Nvpgud|TpptLcQzAo0ts+JlV?1v3 z)#r$GbOf8D1i_&NsL@goD6)jYbjKr@OrbXs=awdEI7)AMIZd18=xcwO$ zz=_?r;|`PjhdKTczrTGJx?iqtVkN+kw^OVB&i0YcOh!aMqbo@DU6JjO&$FQ&Gv@7^ z-L_*~Ne;^afRoLa!{bYxamSl>ad_f;;}Je96FDnOeQjy?UY6;TcT4`w4DMkwDI1wC zb0jBu`iz*evaPxS*9I=D%z}{na{027gdhtMbKRw4q?Z#X^v6i3uXZ%p9lIp`&SO$T zoZdxyJ5U$pc)tO)+zY{i-}b%HSBuxf4>S|kXGOw7_`@;u{HNI+jPHHd26nVC`kVe& z5W0Q&`XOa~Yq#HpzPHK+f+P@qZ)-}Z%UMNoW<%01yWS73^w*B>`wV^nqSz2q&tjXd zIW8>fYyQTblD$Enm=U`)oM1U25+*bitp4^@<3aHXW!V_{M|FlPV%S=DGHCBMM0e~E zW9T!>Vt_LA?3@>i4#3xznOW6l+TBKOHe$>{>ukgHB>Kx4LHtyCu9e8(-M#UM39-%2 z^!0}s_fLjYRMwmN2B~L)L5?@a&LGnWxGDIDoS@K0w(OjSoY@Z_<9qj~QKfSJWJ`+% zur$28h+y>1uG&&C{geC>)1BN|n0-$> zRta7jT{s-ci48)ag{kM-3Bv$;`QV?8OW=`jZSTqueJKPxWz^k~=~KTEB12JFIo<(Z zLLT0ACgl<%Btd^P+~SZ~h7aj$_;~}t!i*#U$o^Xp&!Bw``%jyl#@DMj1W(QZy(M@EV+dA zwD|7qu6W>E0297*uZH3=d`;SU0{pV{RZ&8nitBuv0@6f$eH_Pi_CYF5Xm~^Lm^*|> zI{CiSP7v92WjN(JUGiL`^tZI3y&49|eA!mJUixxJ!y*LOw#HE4E-cEyt-kP35Vk!$ z?J5QPrr#9?w{ec<-TQc5bI1pe4AL_8blgRj+nKz9&1L(72wB$dPah|~P;kk$Ipp7X zB2Y)nZNpL@p08VJf z+@XGVC-r*Uq@_cWHi4TpfxGc4y^v_{LMKUR8)Z0Cu90<%&u}1>`6IEc1jfKLS_aV<_|ot{7=@PdX3#! zw%0m03#eK_*6ommvMlVB|JOs@zcv1u!TmRa`MYIl*h$VoFx?c)>dMs-x z4fto~U^Y7hUh(Iju>0gtrvN}U&E27}+|!Wrot3Dq;T*{o{|?{PoXnn03GC3CL`85$ zA^RytiikzcsH#q!A`T`@yuN4iwW z_qDB>!=_%(?Ma_tsmnQ$!#B08`qmU-n$6d9!m!6Dt#g_b4w-h z2*bRHD$%y=q_ax8)BGj9ll8FpK5q71d1A+VHPH5?|A|D;u!`hIpTT9%9odMs>kVXS zX(`lR`1zLzykguIRYDp_5{M?yhW!M;l2Cxqrde!b*U-wRi0~?ACe6k$_L6(~2LXz8 z<7E;l{L%<6~Mo+}jLTM{BTn35bZ31_8VPo1eH#SsVHT^26U+Mv!7?RK zEO3MjZU-6hq{Kkv(~4AwDvMMzSP(e!P5M5t*2k}}Qfv|A9VSgGLw6#|M6RFh#kIjg zMJMOhb3wz#BSNcr5k7xr#+D$rD|8Q7nSeiab=#?ui^*VPAE<78i$yb>{2(3k)jkd& zvgWhz%+C=dZ8yx$)!>1XCh*IIC72VK6>k=hKRY)fsU+{cFt|QcUU_P)n0GP~VtRl+ zCFib-bW)H4Np%fzczKmSci_BjjZ5D839y#d)pp0Dl43dH7SzU!Xx;IonJEWU;pJBp z0>o69o$q{$##u@ZMD8pnKoVr&$S_6BAzF}uEnb&-CH_nm9qq?(Yn3I%#{TSbJg zOlh3zeCM!R_lUSF-g&}o+d^6Dx>qxx zCPxu!=18uBDi{XG@;ZaRf~1ztVjs43>IJdanodnA!cM)8v0*LPJ}_RkG4yLp;DmrE zDCcw)zA}f9-yyg>Idd&gL=%$>=fqvAi*>a^i0Y`d4s-F>MW_P*>Kv4+sx4; z{p|gJ6a0SO5-kr=fZX29v1ki;A`1vhGy_aAoAGhr=NdyMN=qaw#!@FjF_7yA)#B&o z*y#sNf+sbE&Ss-JChI-$mCtc^{Qp%xJ!`(@=PUKNhD-*!sv`bAq5sc+LjTRz{-XFV zivQ+o{~D(LwTi!1@z*N;TE*Y^;IAkA>k0pQ!oQyIuP6Nfktd8KTVAxXp3EmEOqhsO z7qOp0jG(Ky+3-{JMjGv0a_xc~Ao1d`4r#zE0enfi#PRZ_8)wZoXIkfG{)1oTfcS79 zpW&Bsa7QqQcOt*4$2gR8#2OFyODqDWV+uGqhJ1bH_ezROr_`VpWgfDp6Xn|rr@0nV zaL?n{d%7H#FAtuthVwERgc@yy?Oa@O>RUME6JW~b?sq86*1Hx>xB-q_u{$k`P) zluRW{T25YktEPNiO_`Eibnjzt$e|#goK-~#^*zf_y@CGZ2DPa4R4bjbT9Rau_kNt< z;O3{aF}Y?ErzsO7o4!$#LJGF=j-9XLHXQSBL+u1c7GxYvT%L$J8yr9H3vyCkGuuvT^Zt>4^la{B z9v=IQV_2(p&NyJyGu=0Qp?*<8J(^l~wk3|OZk~!+ar3izsz0nk; znVqwlo$|vuHmQS*wkxAMqV54!!Je)+rMWnW((*)U9BKMKA{1M2f@j0U2|NqMF3#rO zQO{sMWg~=#L`%yx+po~^p&5Y%NAu$_*)GlPqdmmJS^R}dhFA3viShUob}Zqf6ZazY z<%LMH{-usaoduoenz0r@Fet~uRph|8Giv;Cb{Jq zBPG%!v~sgltvX`(`g^S#L}?gUqAP4QW326oSE$X?BEtYJEz}*3D2BD=cPHqpnq zs6i#R?ekRz%E0Ss6#P4SP>(g=IH7ggk5>*ZcsYBc3KZ0p5BZt&W!8_zBQHm#n@i-I z3*19X_R;dV2cgS<<>0Qrj~x-ZZzg4H@_Tnru16tNE-BPLDAR17QzkSf*CiJt$xUoE zD_DHib6Gi!TEf-Yn{XLJOxwY=e2*;Y;^6E&Eq_tzH1IR|S3Ry|;2GB}*xGa;W^Xq? zd2d`eV&DOJv|&(CqR9TJfA69~^sKj7bCK84&+$(>oKN$&-lVi+p_ctg=kpe93$9gwAlB1 z$;JuJRdiqEh}xcX=sU!l3b)(Ol-q;}-7~e6;Wu*p`KH$4@`xah(0vomJQM5SB+1ov zjuDBy-9Q8`AYrU|gutF@-jK8GEZ5Oaqd`1)ptMo`R-fYWxk0Fdg^<# zC1IysiQ73#9W}-%X|K`k^!$Cdz~kQ~3JJ}0wZaR@iz__}u|sgb^&GkgmXY8VG%tbEU#o@YK=ovmCz#`zcMHPAG zlXmZaXLHH&3}2Ko1m2!UXgr_z*Y)#9@O)ZdRK)_~sN~LzI9VFGQGvbXtkp0XwJYwu|nGq1N^GR}`A|)+t$$rh@ zc)w55zp*B)RKCr>&pBWcEy5=)^5?^+E_u{Y?_1vxS7;j2NR-wJ2uSaY?|iG~EcQ)x zgy`{S`|nDO~}&RDi{Bz^MLl&HN$M3Cnop&b5umjrIaog)K+SSKfQP3 z`eh>)#O-T@QmrTVyj~uEtF4R|qWySH@9nqm31ikv<#d!Tu21h#T~jztvVOwsrgxWW zmGKoTcd?$-RfVgO*3Rwv(#%v*QUR3yu#@t$M^L#W6~Gj?9!<>MyM7HPijOKsW@2!6 z<6jwMeph)+L&a#~p!M&Qnacm&I7rasFu8E@sf4uvyTJUPpr74Hyv94jm#Hph^yBSG0Ait?uYNP*A zAGdhh26DmW**^*aSvQKvM_Q6Jzl+-p+RT?AWUye3)f=j~3Cg7=Qt z7KsjGObjN!+_AD`JNPAVy<>OMDFg-iZD|^tBD{##qTK$o)NW#-wZfa!x?_2=`f+kX z{+>x&#}dog_7s7|66=x2NiDipuPS*6slri_TeM|>q|2$dAApLYpq-m_?iJu3Bst9}^_)3YUiJmRt1? zdOa6GNx2n%P11b6Z@TI0&g;^c5)FG9koZ1cYW!e4SBB`cw{Q`<5^?Jg9AhK|z)g-l zOmHwQh8S+rAhm`jRg>rlIAJl%o6=8)2E6%%F)mvp#6jb$Y|WP@at@jXzr0P6)RJsj zag;VqVN(`xtxRYhKl4b^0&)zndBSvV9~g$hBryV_L+t)J^L6M7(}bdUDWi}gG{N9Y zQKKZ&zVH%VCB9;yCN-d+gc~-``4D*IrBN4VN*)iRu z^JS?L7n;H0ykCkXS_Gr4AJ8;j97Kslq%R$OyauVg#Z9RaX+1Io zse8d4QMlwcb^ZyLT>6{0c&uwkMf48mlg|%M>X;XQp?Q}@C9Wzg)@}dZQxkNk)*p#_ z-Z$1yPf?LlnO?Cz=Aq1Cukz5?G~6OSk=cNa(t~%Qrf<^Z_=J_h<&2djXM55(*8^<< z8%W44?PQ5&;<3_Vg9*TNLF3lzaZ| zBwsqFYkNh*_m7#|FP~g@o-Mbr1m?V?+*WOsv zT)aqdEVT7U#JX51cB!aj&~mqGxp?rE-cSTtIuyeDpBO%0bd0B}r0U3pOIZ}C$5zrj z@w&@!QAtqiU*K za#aS1m)!Y$zLAxc>fl(ZhxfyNXKmzry#Od#!Hpa4PajqSv!*mDwJ59ZZN4#?x*Kje zmE$oM!&U@(l1Wgg2Dx}lHCCJ|5)4f)4Z!qXznfa@*nYJ(^$MG%`;I<*OhxU5iI`4V z#T{dbrAHgQ+LXR_LWK&US3P!JK(^lilMs(${HkBqu~3j$$kCF%et>C4YQ15V{LF!a zw;7Vh2fMIm?^`w$mmSew9Oa10ZnH6^KKx2d^f+>{MSy}1fK48jzsi5S-!!shI$c}$ zfRl1(4;<%~IjG=v$iW~@Tj01L$zeruAOs&aG&gn;jAp*q|@*Y*rQed^S;c{CR$l-`=v7oojA(N?PVg zN5Fq~lw!0e&z8UW^OcE>rpuNJEbPww+?ht|0z_M#x-u-GF|^Sqe1l^=+${z%-RxarjZ0_Z#uIy^JgK`y~_I?egD4OibP`+hvGikzC?(E`KE zC3Vx&ISH^A4dR-e^=Co*kXPak$U%>#_ja#IR9+)+?uD z-&%jriV3A~Uq}d|6$UbqpD1lMyW-^XkPRs=q1h?hjyBUdf_?R1obu3YA{Yrw-$Jk4 ziEMV%mFy6g5=TNEo~PIPZ9kR{sZ7C|!TJZcC5A1a$_@JUZoRTE=sX&J|1SWYKE3RDQE9<@l06Cfgd+v@@fn};2wFnu3zC+5<{J`3IvmQJlTP8B=4+xR4z|s^!8vNwo5QE-Qj&r(N zKYUzO7kXP-8A;dRqw~33yh*c}z1l@<*k2F|D!VX2y23fcQ~#;KA!#<oTdE=rl7BoeaO>}K?gaTk9ykyCwi>Eq;P%D>ccsH zr%VvaSIw4Ma7@V(;cc35Pju{|W7DL(oHg|R)><#uJAKUQtwBjqAbIL4km?Hk9Gt^O zqe@+Py<1xn)QQDi zxp}Rv61w&Q8+$`({?)70r}y;U&GAdk__ymrot~D*pU~Vuj`=y5KNZ8hn{0|oPa?j< zoLvHaG0cDdkn@K@N#YZD4KeHPh*$5>=teQypqwyPb?l?nV~eTZ?<>O|l(*#49Jpu| zvAo;>eL6?^Cp!-FWFa?7qLQln`}m)(}t$Srn%-<$a59gD2Shv z=7uYSn^MWuOX5OztLm#9Y_mQN={#F$n)Hw=$$pEgihb{Q-^Hc~%rHA0NYVzsX`KE7 zmeNQY#eUV8dS-}5hgH{RRm@V|t{zPjY?m`}1A4!ROOC0@eUlt|%k9aHHJ3#u{RscXX z3_!c_XVnPOm*bXeAA~BPK@MeZo&o}VasOp$9f@%b?bZ!*McXWn8rZyZuPgXML|-v0 zGF0pgUA)gQVYzB%!RTU?t*4X!*@zdZb9;5&sEDhGXym?4FQ78!cEGF@>}=j$mmeSk z%WsM`)nQDCb5=2m=P#_0vU%@!xjAM#ix232cYLs2%Y>&oICdlT({>xryaAkLtRl$F zUU7;s>jR&z$?q-w{z?Yk`Z7l>?~G5@(xlPF;Hs4(60K~-m>L`>shV96PTPNPU@1E# zs%_Y~%4dKPvo@)73p%GnDwQ{``l|JBGT(M|A7{Vq2(*&KI7#tr^G?>a-emDc6#0+@Z#uVk!H4>+;?QtMmG#7PWE|$vEnHjgRwM0JD=+A zL^0N&;TsvhL$+`+xkJq!!ElE?>@^2@PxRi<72wOQgIVzJUID&S!v*tBx0O|aO{Xo> zu(rA3Y(~1DIdOBg1}U4K>3H?ZM&Gq1J#F$~DP@h@>3O2(Jlt0rEcfd(Tj!H7T+n8dXHK*F zuWp<8HP88uruQlP=c~xP9ox)ol_~(j$j!ISag#`3JX4dli*5|InV8N?Xr`WZyTP_r?u6Qz*-d4(IRcx(*-*5D-nUDgm@_H#?i2jG^3brv;UFvQmIhF7%&gK=OkY!D#W_7 z?VCfDxH;hq^_<_c=*Ns7T|-;w_*)NHRtx9G=LxhXdd}3@k?smL=tp4Tzbh`=1sR_2 zwJ8TK#Tc6fe<~A>X81?Z+I1)5FW9eECy@9V|N08-3l8yhu*(^O${6QMFAc_m>=X00ud zGSR6JUA{mjiQvj*wp<57^=u9N&e*KHu1jOTi`~?hK?A(gq2+uaXY2e>E$rTWay3ao&o_D+W9~B09LS|5pCJ@Z7+vLmFA8O?t8QB%JUm^BWeM^wt*+>jA1Poh+tse`xcWT#uCy_N}+x5~-iCSGALUFcwWAi5^nQ8-AOUloBjz!aGw}hGM5? zo8x=3geNGz25SmbrKRy*D%xLI(J;J-!?F&ESt(UK2Vl8c?DVGHr{iNI`8m}F8gw+q zz?y{#9qqrAAO+|z$!!m?7j~A_33HVC1O#K~1RK4(gA4!I(_-4W*07Qt-gT{(x#!Ya zk`BjW#rNV2`9^{@z`=b%-hm~=^4Ux{`d70VsMt?Cak4rg0348WY1|{h5hPRRXL^De z9vbj5NG!+vUjR}-t-oWl$16Pb>{CUxze)p~JE|tmRku9%7OVQv^zC|dl^4T`SY>RX zAy^Ib))BhL?6VuqUeXkOi^K*q6Uo#N;0}#Ss}CwiLEbUTOKO*vj;g1!veUt@!1Ajo zB{T*lF4Ch;>r+KpN>;}lrL^mjQVPqx(&Wz6`Rty0SGtAkK1yulw??`7TL1epy_bhh$#vSwIc=O@kqOXXQ9m8#h`#+B$wU0tWD{?c)i$uL97 z+FPM{Zq1dQ8&h@)Siq$CEwJsP?qp8WC$9svC0{_XlfY@CbnXeDdRj*AQn8jRmyY{@M)5`{N#bH|;in5mK;OW-G zUaK^@Z9iQ-8JmE((_cH>`pm6~rpRqAp#4R5C9*QweC};Z^Q$K`Ksz?iwvezZtK-+~ zyUlDR%lpBuj5_6=oE7eAr&E8G*S}7?wU)hH{2QIL47)hm9nfF@GtXTM=pjY^M#tCH zeKOE_t=;CeCmXBvtRpa0-)35&``v-d?Wut+2SQiagQ#WBT-%^p7KA40VMcfsH*y6yXG+m7bbZl2G4#JkQ ztEOBrJI(bi+t5F41k`U`Q#?!Ck#M+MPh{NY=|E3d&2Xz62;A-+8PKknxx3G0(Usn1 zu^DHq8@vL2A$!uCZe?Q%l%14&4V|ZPFh8Uc zQUdlJMz1inyrv$t8e8D#QtCu?guXqfb3?~g>10jmBQjEj9m(tOYF#ep!q7w|&=Rqe zHBBZ}gFrdtO;7?|kE|05RY3!?xMj;3dhSx4iQx`WO?XoPF<4w%lq= zcB8=RpoS!ckiLNs3QSTKy8d>Kb$Yd!itPz#D7U0KTE^9*OwUc+ouMLK@`JHHv%tIC z3pig_tAO%Q85VM0?A@>WSj4FUp~;SvdP>2Ky@lG72strm*)YkNz0i9^rKy(oATt=5 zW=zIw2L`lo2zlqBZP!+}V~U<|QuN@_&PHL4*HcGdP{WrO2>F_p=UZVGa+BP>(O_yv z#|ynDETAR^y)!k?bAr2HPa(Z<)1h6b1XII_=o;tl*OPEJpu6g5i#taTSarnIU`kO? znd#<2EVOjITR}Z|O6e4ILn^9~Vti2QYK9JXG4+_MmcG~@w3u>F|o5hRs5)6avPm zg@S$8)XT?FsuahIFf+Q>gY$)=GgNB}F#9fn1QzL57SaptFN*qO1SkoRW)W)zptErxor$mc% zh&`Ls*B-_@m#d_!bbu4k$=p@vY|y+y5i8xIBhXpjBngC~v5tBKKP%|#XjP4O%K?c z@RdrY7_oGbu4;^sQv#N&+EL^Mx0VT=^=$+D-$Xa!Ns>5B^bs)GA9V1>Jl8g@zq3PT1Om?a41udg*o%)Yn~4e1&lQrN2@8Yd;!*&u>; zLA7qokB9$sb9c}1o~p8PP}`h*#X-SL^=&NrZHL9a2F`ptD?^3qCSSe{rs^JF=h!*m z4J8)-nhzBz=X~qU0_%4R4Lil%lfXR@Qv*5$#s}RYS3#7o>ZRm|EW3yskM5ZeRL)VQ z(pC#=U0s5@diQiYPTP7~1cxkvzCz$72nV!Ia0{|aghhyc#wx{?hGNkg&~!IH9`XuC zg_Gjl;;+y;KuNj=G_T034EfMZ^^(T4P~<9^n6j>VZf~nuPFMjG(W$B~+d1M>)N!+Q z(s?oOjvQST9d5|0?y+dsp=wQKxCcUZ;jm?cMZCJx#^}nRR|WMnUf*Jgg}oED09998 z)N8P`*b61cy{tn~yC5Y(UqVu{(AP4;O&<^b)IeBq6Ejr!y91oQ2H~ZON4~yj6Do4@ zW1;U2f|_}`IaP~#oq{?qY#ou>J|%RP0PGf*Saw&eC^t$kxduN zuIkdtfVJW4KGU+(UB@IxsndCC@FCl0hgONpfSoiwAJ&Pi9!Z3u+gG8-yjpu<=b~w4 z5s_~o@DI^gKnI_3PZ&3_ysPmG+T$jXTAu2pMTHFmj>5*3%74wyM62v<_cFiKh8_DT zZIz*xy<_ts+oBUm@#RFcuE=-!)Q#>HDx`IV4MFO1TUZ!zC(XnyTaEN?JtCU0cMi=_ zM=#U0FJM#o{2%mHs+r1ng_DvW&_Nm_R;fW2NCj(kLIuW2ZKE0#4LOCszMinX-kL&R z57C;!3G16bJ(Nj7!9;yeLraS3`X-Q;E}@zFt%5tw);B#0muWaH48j=N}>>Mj|k z>Kim+_kn_PcM9e@mn+%JcGx5eqBEtapunnDuvp&#ni{!77o>35HrQ%GU%|K4f*H)wj)?NI#e!S5=2I?;J9F0; zf$Two4%DSBW@J02NAwgyndw4pU^bx^$L;C^xBbe)1+?v6!*yj>CU!46M(SnnE3jt> zs;JXV$9Trt(|wxqc9#YP#$jbAsq5i`TCDYe(@h8VQkKb`pu_W3CmR~N$BwSvD_Sb( zOFdQ{TL7pX1yPIQ|ZH0%}nk&&Fl;HRU=LPonDC+CwHYg^;ZH-{Z$Et zkd2i|eXEJT_PT+gQZ)15xvSlozY^){pQjqw>L9u6m2;j|VY26E&p4`uf{l{a1f6|7 zlz}bw)W)RHupLt)`rECd^-a}C(K+fW*_GQUm4k)O-La7Ix&Hc>8(!^V@pHdQQD}x# zw*pQ0BR1i8BWR&^TpHq9@%N=MoZU;D-?>Qwb! zPpDN2Hwzj~?X@E{67fREruNopy4J=cX0rCjVn)STm2^;QFBN0dQDr&aL_^x)PVKE^ z`vzN7yWEh`@1pS$U91#1?iT`kJgQ5Y>aUIrs7iSgB5C>h%9(0n?Q(-hE}Gc8jnvfM zYON#@&2A#Px$BnfjuYP0Ub$*&uX8)(<)j%)#AhL-4zh-Jzt_h8h@+XkuaiP3Ex*8K zH9E|)RaB`voWi?e5m{jKn4k@7`33g9K`)fXi_*=k@HS;;{jXF#dsx3mjV~5aMrEB5 zUJ$Fi#qg9!N|C-!sebf=%4YT5tjO3%U{k&B)-l~hrF53Ndw!_S-E3NK(%m$3%*%JX zb)1~EMe};ct-04Z{cY+6iL;;wd$@*uNzj~;vQT<=Aa{BzCHYHK(@%oF1Cdha)zl zkNAp|MyIE?7T9FoI@O)ccUN8oHk*%3aAxWn(6K=2>{{VyHm?KJE_XKX9$~Y2^`vU* z&E^X=Y-=j-hQ7)yw4*eWS4>#=scf~-38{jLQgJ9G5e#2$ z<>tqOd%9i0R6j*UPmSO~uLDd#tlg{`Zg6XtMYhe`1(u?J&87nLCn+mmkAS5Xw1iR$ zENB%xMfM@w-R4aN_61v;-^SwG=`P5HB*%cnXLe(rKKcZ=i#t}D66ySDsbrjy? ztq==rm0I9U+NG!cs;{HS9v`s_hDIxWSyWSYx5_gu1Qe#@BQ{%CAo~jK`dnx`-B2#@ z_Dj)%z?}$mi@1~ZSRgxjv)m9?Gu;|SbbZoSWD_B*4)NLdNXOZ!5DVWF{d;?xqr2mj z0@(Uq{cZ!0;@9RHZs=cfH*yx!xqltan*+Y!FEbPG7)gMq!(fduN^s^g`0T962@> zkJu@h9EaW|?LP0)<1TgF$g}IDO~h4CSGF?J49e!=E>R-3tf4jySB(@$PETvr5(SFx z?7Kl14nE_mtEq*?7}E5+`*cNxJ4sM8ZT3A~fuM{EZRRZ~-C7PRpYT#Ih}^g|H+SOg z4jDG_o~miLCKk%mn{}&TO-qBeLDxxT*GrRDeT=hztvSG4CDL@eSAj0iVNJJFdW9`$ z3s%@Es3zDg{D@7E3*7m(LNPYqR^hud-!6B*_8*ss`OYkC(pn+Bf2FRfB(a_@Ai3J_`~>zJ9pAzwu<^V@1Ua zf&X+3QU?DLFK<<8id$Oeh4C%={X)v z@t=mP6q2=IYkW^d#XJ2|tL*%%KJ@%|yo`;}+SM~{H9P5?_ihrn=T?uzORspU zZB4ngoV_8DqT;L4x;3Tw!`o_=u8U{>eg9qM(k!|cX>aa@%9Bx ze&z6{(Vyc9D=L;p|DsD%lj{=ko%ruq?y3?EO24bD!J64Ec9m}TxBL1pkLS2<^ln6U z?ZMx~(^piio4w!emsDfQ>=zTcEj*yc6gzE9@BeY)n2OB{TJMx#_czpwZjNX5dExFK zmQs`mn2L%m3u-)Hb|<&vzl@i@Wp;zzIUtcHrRS^hLv?9dn3ve`JKpSDd8v8s5Bw{} z&Pyt5Q1QgkzoTF`X`FUW=E$W5M3Px9*Ab?UOG}FZA5)W;Cw6 z)O`HsF6!9}5*MwCinn(2(p250bt!8fh0+Y-;MqT;>&X}_6G>&Fu57iRos z>fK0iVDhEui_%}e?VLV(W4!ofyS&uwMxj-bG>azYO%)a2kFHl`XO$26AI3}HI=f+& zJ%8@QUdflHpLVrwYjJzL^oo~r{#=1O9o^`n#DzrbwwkUaDd((;7yt9w-t2OxT>i|4 z`en=F`B&5shDo!_Tblowa&~=e?UFBJHQ1^AhO-hU>YqI2cU`>p)H=hPMpiGs@3(Q9 zjhF5;yZqH0fA7va-GZu?f6$_M=@pM3@_RGhH?!_a?42 zDn5@*D*xE`;-zn&`=7m^(%EjAxUU_F^7ovr|NMpbdh2&zq<>fYlS+E~nAnlN{)rnI zyEVJ~T?JN2oK({K#6|7U%H{LImQ|Af)gSyx^VkF9nNLbw8NcWr-}WE*-C#yyf%-*{ z$`axozh%;|T#+i1|KWFi6H9{46O$K)vGSk&&)&Of^>DnM)t9dB_7wwmds_BK0Mai` ztc!m*y;@0b`8WTOTzC7Jz(?`MZ@+q{^=P&7`~B&;UiE6VxHVpS#T!Rf-|0Bj%Kzct zf9-99l>YXg4su^L4=(PmO8=D=72gg|j?bNI{Cl=kE+H!J+_%O9tCjCw8@?~AUt`+e zyRp$#iTT7Qktzr8*{;jUBWB(I%IA+(D?c_G{WSXbtu?D;9{+G^;y%R2s5jwmc*=+? z?tkTrxNa2|9a;o&?+i^8Q-+>(BJj0+h_51Y@1!h zu_)>8R7J&4b*q=}ICX|5ZZg`FjX$#R-S3`w=@tK~encLH8=)5Pku zZhGazj4H==tDH1eY`g7OW=XxiiZ^fTjGYJX<0|h)-gkiWgr;zSER z6VKS5vH7diF7Y7=_r;x)G`l%5?tX5w+`SYgAA83p1=)rxYQ97|U6M|k9rv|Q6qi0)7-O4`s6#s+wb1Rsov^3ojLK>i#_X4NqqdaUzObK zL(*-;Ri&vr?cQla=f~qYS5$0Uu!|ZTd_!WC(K#DGV9S(xJ7rh!X?*XFn|F@Nos!#z zj+I^-Y}(bB#yRln#MCBUz_U9)8`$se_|A&=kE=POnzkl~`Syh!YD(fb&8H+9ZiUqF zY#^;PakF)O4Hu%Cv-P&-Jy?0FZ@*{fvCK`?Vn$-X*?wJ%Xg<>MtY@xW95- z(!;_Zl1AHKIpSA#9h5xS?A8}MdM4%o+ZX)Nr9*OsGLpYi@S9)k*uQex|K?wH8}Y!| z8x#Hc>t8LYRk}8wa<@j?$> z)%XOlY326=YMA!_b`igkqFM5Q`_@ihuklshWT|P|{~MX_*6zKNYw&i}8st{apB5gp zTerHF!7n9JgoUzUr30^kLyHYLIw3XU}m{ zoz{!v=Ul~eSv4N=7jN7%cG+`_{a|ilCi3H-9&LBGq76su9$~fDjs<(|?%{tyx`r1f zJw>&e{N~*DFDSH177a!xuJIKWtB38z+_uW$f5C_PBjevsxP70$AiF=1MZFPrr|zTp zI_MAN@MjOHGwjog|Fh3&|7;C89svXpKmY**5U4?*WMHoD70fzAyBeI+J0qiEy~%bf zbFabW7mPS(M28GJB=-9R{XSbCo0XhBsLXNfX10FPYI}2_MgvQSmf1Gl7t&9wfrE+% zJKo(XpOYF-?WE|BqtALNyMYxaRGjFzw;5yE*$($r?Q>dfpD*#hib2l7`VTOa6%Qyc zEjuQo`+$+Xi_0=jDlQvYQaU`Ny}x5_uD^XnAIF}<_?%j4X^yYrbf;;4ez$JDlmA!o zcV{nOjayS{+?u-c)+-$M&hN~YD&ndy$JJ^4`iFVxhn_Mm*l@z2GwyG*Pwv6{G)?WX zs&vXdOGbY-=Jh)37hH4dL#sx}%_e<8_c3G7? z8ak~uuKx7K)}wBpJE^emHyzGg|5)EqUFMXIxO-OHYhLZq{LxomIOCNr`+Z&3@b#DL zELf2HV)qF}1y}d`a-36H59RUncel-JJ+kScdzIbs(vVgUU%BGV9%YkSb`0g-@o>k> zTK_om%nhf0GVto)(f6M>?9)@bWuAOV>8D?g>z16wzxGNSJZ$l!FE`$h@l)Ma*PnFY z`KkAuvwyqg=l5vsKd`9d$ghSJ+?&?sp6hR%+-SeF=%ytvG@d&3!H++?;yxYz9<8zA#Hm-Zp4Gmh& z$h_vz2IoHX(7=m2j(oMStnbxBrC-;;-;m2 z4{0^;u$NvNGPl?2e+_^0t6ta4Z#nXtZ68l>d(hY8$6h$H`QXC`oL=YHqkD8c?fqlt zUDEQ!$&D6Y)x5>xjYqz9?}zvIowcbb^5wKmX{|QDdO+y*oCBV^_o$^q#{YER`J4N0 zS$yxv_h;UB(ZmlnPWo`uYtiRkIrrXE*L}Y}{jmYJyxH7;jI^?6E&AZN)HAJePo8(t zrglf4w)F4!962d;Ro>Exo&R>@-aX%cD79{b>wbRg#aC9`U*7K3yuXE2yDvXo(_!B& zWhWomAp42PDVN>zUSMj+r~kF!@vN7QUUXyqw)g%0i?g>Jnf>wfWh?J`^uRu^O*ygN ztfcxK-P9j_=cfI)zOuaOxmUD*VN8cFCJo8l=bxwFyS=#9Pao$@dU5!R>23D8bl68@ z&sh4*&)eP_^4&}q@6I2-+_LJ~F$KZkURz$cfB7D_UEcNLy=Ip=BPRP!S+oDKgC4x( z;pUg#I=}6m&m8*F==VPOX^xIestT(N8h;l)w{=(*Ec$4=G_}^$$e*V>6LfC-~ZJ%-7Z=+L>o<<0wK zagM)n+kfAhb>^}A{qoqz0~g-d`T1AxpL52-g_nHO=&g4~Uoiib1z%Kjc%f|XXyu`44sZVPGjEPMV)*P+ zq9wELeei>ihM(|8{_520=IuFm(!b{~|LBwh_MJF=%qJzQP8h!GszApV54d__tF3|I zmye$Dz|nm(yNwuLdrgA_^7dTw`MQCRzVTk~!omNz;OqHqU!C;zq7DySdhs1|Mopf4 ze4D4Q%OA2wvmS#_n}7AbSNEFm^oplm7D8KIYIz_r2?`GM`y&iQubwx&(@o-z5h=8aRrPY)jb)S=UlTY7HF ze$L?=Z>o1{qka4JncnuO5fckPDP7#A*?n7P&pG75S3=>oIeWc*Kbb>KO&u zuP*)g@%+Lc=iYd0pM{^)`TD&pmsDIf=g51ucR97uD>tpVvE`y2-`zCji9-i}6Z&cW zyZu-Ha!&f8!_I16zwc**hfJLl9=p#48{S`Y|J2h47q8CVw|LU~&pmU}sPb>GYuusr z2``+OK6^;VJ`X&;?3JgsAD7kk>!vM2i(g(+KCtzEE$`j<{M(n+ef;w=r@#2a;aOdu zI=3)-RkMmC$G-t}Y1De0K>av$V`ToFRO&dKj+3Sw3YrZU6l9_({`B&)w zW4nE5!?2OJ99Q!6rY?Iu^U{IG9NoU-1N$Ac_^~dhwp;YjZCBm)+&ABMeRKYh!nc-= zI`QPb(>|Q4Q19K~()r=zS04D}6$jPnKk1X(-b)>SS*_z!Momp`cg|7H>gR8MwBh1l zkGpQ)r@@*b1)Cqa;^{-zZCdy3q-A^04PX4_NiPlGx7H82^Y7Tw?|`&R_o&EwC{E8~y1%v!=BBW&5e`Jb7rxA6}jF^p+1-e79rxfLqt!^w#yupKcdf z(sSb4WkJ2fdj=ZWya9qU{@;GEtqn_OM`{Y@)!E?$50A;Cxf zmDQ{CsSEah<%($SI=%Kj^`t(FKDlK4Gasz}`lLSZoV)zp=L?Q|wKV?0bLzFY4{x?~$qR&Rx1J zdhe?(W?a8z=Hkn*IO5-je%2tkb^N_!ep$Kw{`q^XYxCV4e*M&|fkRKd^wXC6ZLa_K(1KCUw5%hVef9R-)FavqKlJeS z`@OpD!$FU&p4jh<%}t&vdcAYe$%i&jwy5!vupQ^H9B(g zNe>_TTHBTzQNb%m;+a&l>ZpmCv!^HsE2gj&r|XTwcGyoo`*`U%7sU z{%c%yPpN2gTBG^n-blT>-RFz@9q`5>i>~f>sdLQOcONLYDDUyVU2y4&`A;9drQY?W zS1*3>zPmToK7Zo4U$z#V_e92x&#h~A=H$P3_~5#!lRnOQ?b*8r9d}N4&qs^)3|%;J zWqAAMpT~@T{N-#%&pQ*=jsC3f zlg~6cdDO@~jyoVac+i;Bu03t|$@dL!yztC>T6~)O$+ChAzAt_Lr?=}|a@wrg@86a2 zdb36k^;vwv#b>?vd~RuU<0*NoR<>Wcs!sXz(mEGj7oHa=xnUSi;ie`|KOv#wK#Lc>p#5{nAiJWtaCFbxXN?^cR;c zZaV*#12Uh^{^Frl-5>7b+&^~R>mA3mADr22$+9Lb4jC}(>D#XVX8A!W>Di0U>|1u` z_Y;qLv){|J-nk^d)3V!p-J9|L-V^%2d&bhTUox-#YV!S$Er0Ee)AxVop!LUGS{~@% zEpp}Lv-}?{{af)bCj|X>YlTuO(@VeYtNYsN_u`}A_yICe1yGAbta}WPgg}sgX`1F z9KP-;=Tg)3pFBi>xgw=0iCd^*`S1 zMcOuhAB+Ipn94rRyxMd)F$QgXNa%i(E^t)_CLMWM7R?1xlg>{g_**0Qee|jG z(3ABS1i7ph+DXnYrcp2~4c9awB91IclFIf>iZYTb!bMW=Ce*AQ4M^j|2(O8l>-@Bw zp-)Yho|yT7Uq-hE!q^g8|G0;Mq|ADUFTHz4o0K?#D)?1PS8>EXXB%)3f;E7bR^^>H zqcX@C5(25TbL-|WcG~s2rt5GB=e9}9FXe$Kk0yAcgo8yk1olQLK2|3F66rbquYr#U z`zHjf#qZ_^DD32?EN=;QZ4OUf+3fdpJ{6@LWL=7c(AHQ8+e^vZ!BPp7qENpJXu`MM zhsP|Souw7d)g=R$f7*)9+%`DfaP80lbP5wy-jP$)9@7y3QSDn&kO?r@TMD!@%sgRh z65qX4{{)tEsF zG6iBB_mughxT*QmI^q}5ZLgqQMPFMJ3@)dT)=AgNEz%vYL;iy|4yWU!zqTL}!k9!_{ z+D_kWR`ddfn@pS@*bX1A&&xBXBPrYmiO0TQotd7?u0VzUXo=&j*=ntuL=B5P2Ghbz zZ)E9}C^e)$XDsxi^}=vrIp8@CN#ankd3kR;CEuK=2{DsaaEZJ55F>HKmwVm#GK{E^ zuqc24$JAODpl!w)rRh9j;a$#$;?4Ar^0n;|PmT^Q+R`^d?CU0{2lbi}5bQ@suQO!P zxh_R#++G*{&`aF0e+}z7d8j2Ij6Y9Bw~2X*mDTZwHV;MQS>EVV{;_yZ{8}~XkyWv8 zTz#h{H#%u$J#e(|A*K$z#$DgPHcVl9Cr!cAI;bQ2Ecy)>0uk4ln83|+WvCh(%`(U$ z(4|hi%g1`u{fO)G7t9TxxcWg@uE`pc@zOp5>6Vy|KPD2*tIxVSG*vxqz?UIvww^(p za-l9(ZaSoo3*ANaoKCORm1^S-P7>3DGOp$QM}_&%mQnaBZGitng?|jVlGJy35k{XY zS~tfrjGm^GieWFx??!we+vXV?_yY=r2SZQTQqwYgM>XlE!joj4szDW~FVqH%50pSb znWs^Z5YI?J2=An$4Rn?OTLYSG3gv-DIn{eN{fQk8`uuq^hBs1{ltkmTBpz%eyH;Rk zBh^{$E}UbM-3H#wY3sv^0GpfO@{JedoMv3~#`5^f4HTVlJJ05pGLcd@1n?L>f1asP zJ2VTs+SusX`1Ip66SzU~Nt)h_C>h7d95X&i`vBn6YdenvUyt?%@|3w0W8gWl@^0KP zg<}#-;xF`6It4_|`~3JbqKMz~?LY2>VB_O$pBQU@Zi|FwkTBL%E)pS+Q%SnJOV1|( znt=u4pap9T0aqS8+@iGoM2G%PnVMa_0x7hZN}W7e?!_+RG`JP@V^m%JM#A3yra(Mz$mTHi)`r4!y3P zQ5CHF-B9Hv79}+rfb}gp?9IU;dmss3Pv@GZVDUB*z-PD|GZizQ;kTS0t3M?IM&5No zy+Jg!_|t#42IXS)COAwpXwdqj)C&{eu2?R>@}@Vbmzo%aIzYS zO4v+~t2mBlyU)ZOJp@PD62N=purFZfzZ~s%Glx>Cl46YOn!v zHK&nG_4Ua8(+xrEERQT(JF0nlP`q0cmX9$&cygM%mkqKNz{#>*7=r)be4yRXVF9YS zOe2w$tD`ZTv;?D}TcO=xgx1)39XO#DWPj9LAlt0&;t3hnca)VIhIT~jCn9P`yF@hEt>y2yn4{eeDBoC&2O1{olao?+ITpS9%=f8H3t7VQY7PL3$(OlUyA=ZPBX03%S67El35 z9(_VkEhIi3#9SQb32wUmt`TR1cxsQP--)bjrjBQE4fGTrjxFHQZ-a`V54$&qQ_7!r(eU-s?V3mKsvJ7+_H5wJJV%J z)w=8GIy2K_!!$oBnV!}gn>`&3nGWenx#FZhy)ZMzLX!M9!^RhmZg-o!U=^6FxL@ zTHp%CnkG6KtFA;Bu za;BIz8G72J=Hl6RR#X331_^t!w_`gt)9cW_s%XtnwwC)k6)bUrkyrgo?;?Dw ztyEn&Xg1~uZJMd8isia6KE`7kb1^;-k)tT@s7U;iZ~cu#H^r8^wgVxM2T}pC3Tpd7 zKB40}TaXOnL+B)c#A0N~-46U*h2WN8clAY990NOYFB)}05#U~oJ0in?aRG!ANcB^+ zj-X|DQ1e^_dEvE}E$6^-Woem%quzO?mip0{ zPl_RMt6VcjQ-c&9BE)m5_E|}H1=weZkA)Q9#gc|;X2lu1QuFJffUd4f;tnwn!cQE2 z(5>rXW;Njzx*?a%Rs<83^<4b!Rznjg1+meaxN3;+)L8tmQe6y=pFX0CD-cGtl?mm^zC6VQpGy$CygG>hcQuT0%`EzPc zIoCv69f5GSI+dRfUT4C@!Y_gZkgk|@={({cBQ=PeMRzS3iXDeuOE-d4CSb|rbaG}; zk;sZejIQ9x*F0iVUaObxsp-%>%7%+zJ~cYxrcBV0T;`;@qE~S-2GwK9hn^dLLN(SU zOp?G;8q3u-pBF^J+MP6LbCiChoaAx_L2zasD4w4UvZZH-JW|G8&}_!D_`j{+4Hy4C zHc;?$k_eKsWn)7m&`X97Fsi~l+V-vZtUujzA4P&WH;Q*#W5vdFfY!!jZUtHi;zJgH z{tFGT)Cc2>u*29U$?wl%XFah!DRIp(Z9jQ!(8vTFc@zQv0^{Hp%w> zqqsmVw%ja`AqUd57|6gp1aLW`i%BC*bXq)cQ!`IE!1+7Ze`TvQd`GA?QtCWPk?v(I zqv=+I#UnGT5z*`p`)Ug9fo$t>ghY~1*e=2bZ)`ioc@|t@l*$MnM1J;(4{KkLG%e!R07U*-#WF&b7-=SI*b}6oJ@ZqGv)|Wv#fK%FJ z6GI&v)&d?P0--Y5VAI|#Q_m2{zDD%+wUYY!?v-v)5aiQRL1Aqt)6-i7-n%81i*ooN7f%fFlmvK)Ef)5`5{8-K}Vd ze}tAv@0`hrkF$YQ*OcXBCSLKR@E5yf*>_;Y?Epg9^Uom0X~ZfgTC&jFb5KIPf_L50 z(fvECMXdL%h`(?CMU%po`a{ma#_X+MI1_C!CkIQi`Fg)fZt3;>)25vwhCdX#tdJ`qNz(q3GCo5HbSQPr;VK~F8R1Q?z~)i|NLL{7@}|x5Z1p)ID2>S&zlTx1QN**D z5QKKl>T+WFCm&Bp} zxJ=eEY@{`16@7Z8^ScERa2db2g(u_1>1RC4AFo|{{z*MA+)@vg%T-NN3d{3m2YcpQ z2lRh;bVsE0eybsaU6Weod4GyFvH+||q3j8JrGwMTq%y=^T7$PO;O~x8ce+2G_KKBY z?oDHF^ixRf@W@)dg>BeL_IwGZsY)e!m<*!(S9zAHC&4f&j;=>3kir77Knr%_^(sk{4m_2d z&00TffX|hIO{n?SgOmB_)23`C)+E&fFCLhk=3N&+Bogx3L>t&DIlJcPW=Kmjn#OYS z9G>hP`Ms|Mqyg&|tQ@gC8{h*(Pj0IKK}1kGn5f&)bKHf>e? zX0RcO-TvW((jfx{k!XOX6FD-Y9yh1pizU=jTiracg9CK!AE-W{KWaVkob~aTL z6N+AxP9VV!ba~mNOsG;gkM)^|cr%CBM|GV^WB8%t7-qLdiM>8EILoV78CP-O zB6nc>v45|oH3v*|_a*}I!2Bn(9!m*10+7tS-r*gymh<=KItjgtS}ytA0f(|ZfG=1I zbT6}@R?+BNTzop%FE{q%P$mbFe9@0T6J{)k1P>Q(SFZ4q(>Ns*x#6ga>JHwNL^C&o zZ?SD_*5I`0D`ZbX0-?GiY!dFFpZ>C-&JR_CD`?UAqD;-#Uc5^bl-({3fP_I2W!tnZ zqf!SfWNyo_jyvVn)SI>Gb;tdwMzP}ohEEWVWrJ1H4v#*4mtvF5ePqs&U;qt^;56`T zaF>y|U($RUU3W{Qqj9Ptf!6P7oP~Op{aGEjD#R+AAeMIg^?%sY*?j((gnEA>xFaH3 zeF)CR9V`Ch)Y7>Vm`F!92tqSCk>sT_;dNbEF?5A{e*Z>4uoulI`w}Q|qVny;)5Oy| z_sD2sY#U@t<N(8=k@+JV7HO8B7eH6b>LH!C=0 zGGp9b$Qeia8YO~ek!`#b3q_C1_(^trHHSVI!NemEt_`8|i{y-<= zQo=e*443&n9*kghNn48DyS)cH?3UHHSY)BE4_A33$Z`{@-GAC{bRs3D2_R!`)qg{_ z;H)ldrjK3pNM)0F9?aBQZm~w5BX@{)jS>?I8U1mm+C`f#DtX;u9s2?i8FS>vez!Og zA-dwPG}KPH_I9A$tfD7~b`G&KLqG(WY!E3vSFg4)_c3!iO%yt7bz%AsVDuy8)ocd9 zzZvN!ZWHIWI^Tx^$v0B_9!CH8>v@RL^HxF0YvdQ|%}?B%41vmX&|1f1E?_dx8zL)& zq>d~|d}mQ&F96)XyuR(}jrgw%w8^VtF)reCP1Du`lj&+dQg+QeSD*=uPGSVwOvf@` zFWzJ32@vKvS|Rf-!g$Nilrg~1C4QZ_*W*mv<2&)Hw= zC1|4o+@V*Xmz>UH#&tf1x`ZdEtBIvPQvK$Wz0j#Jz+XqUR`3LZOH-LYF zzvg18OJ+s1l)QHA`1^X<+gdF!hoMdaKITuS)v|42XX?|?{V}iS_)hJ51lIs*^6kad zM#`R!wlJDoSWJH^DBia^Pm+CA0{6*wmypNFGFxXLNIb}kV8-@6wzMYj;95jVLarWf zJyUp?eo#zT?BA}n_UBYTe4Rex5_KAA$toUs5YTLVh?rjeL|9Ux3JIhH-I_(_=Y=jo z@BP+?_7Sp3DCk=evuaiFCbDpc+d{ava9?MnQlQeW6|JjvkYMwlU!Nx18Kz&?K7C$N z>KyJ1nezGS#Bns!R7etSX^@fC{kN>U#Mx0%d%;+3lVY-Uukd(~Pk0!Qpo+^&gsA;% z&hHF)kO+++u$IU_ywzVO7>NiKBQb%kRLD~uKsoSnaA!7qx$h)Co{oc zdbX{tv7Xd|kkhf<>Qng4nl7~W3;Mv=B-2(PZ4oqswHnN0{K$p;F5eQ_Z==RPd8b6( zVMTV-l|DA=Wf;)<&uLg>z>^i&n$Qv6y!N8#D$ag<=XHGXP9*rpc}tGHkkZ2nBZkw^Poe1{X};L&{c3lEv4*RL633JKZBabwl-w=Y8MG#U;F=) znjm!@q3ba)Y(M66$kML5=yM4w_6!Bn2KrzHEm`^|0KrEwViA1EOj(#Lr(?(%zugJ zQ=%x(@G`uuUD*v;&Ipr|TG_eH8)Q1}q_lxrl)`~jrKsTpw^n7LT??CcIal3@-eQD^ zBO*z4M==t=aj|xa*!$pt_x9Xk$*&qj4kC?$*8HEgc;^F=wu*J4iFiY2-{Om+@al=w zp40M&oAB(eI+~@qUi&Rfm`C)S@(;ClF@mRBmU-VU5q3DT5_)pCvMDB%_lf%-D}YUD z9N7L6ZWEkfu>%uCg~M?n=lTUvC733Mv7E^S<{VwtlE?`2rfZUq6q~~cvOU*!Fy_DR ztQC<+t=?xYE(+1|Bw2l*YO_JbZ?a`!=ww&zQI59~o%EHQ#`a`3zX4NBe}LOhf%4q# zI>RG>T*b)YS!AZ#P@|o%V_EX1%whT5``k@2<^xWI%y8v~2L|n=mN7DF zB){~}1}oOs>8XbjW(BrA#8vcSVqd3{y9?Z=q8VJ3IA>lWc%&}K@48eF5CjykMKHW$ zf-Kn&XjyVzDROQJW`RewemY;fwSO`UO)Y({%Z)Pclo->fREUxnN?BJ9PAzf_wYW)# z{>tlT(s7u%TmIG&w_aXcocyGze$xB2)bWTJcV5G4GbVc(>f-LViR4>yMiiegnb*bZ z>eFsBp62kUZ=UgxrH(>fa=Lg9Ue>y)(*prV)a?8d*A%yzCQ@N)!l#!Rg<%KyD1`O7 z$#K8*RB0`{e$|l*YrsO_w=Oe0m4t(!5ejS@iXL6avxw{?QtZwa5DJT{h^#^7l3?UY z!@iXKZgkJ^H>X2~|1?8;4@V4qGO@}DznhDOGW%IIdL}NfdLj0_}zl#L-%D#)RSms4y&dnMqWFbHY_y$EKWMClO$XO5W zl7FJM?K;cvA1@;@l`J;SldRLmv}&TQqzt?Ab%6*iZKH)@df7T zNiKA??yMS45@AEziJlYrxE+Ih4My0FJqZvTzMOW>SHWx*qhAMqSUF~bhVUHmUKHC3 zi9es3KDwQLXXH>=*f`oOVOgOO{e1c^DTjtm}2*0j3i~ z6_lg4i3`U#c#sRTh$wCDJV?|p&*){S4L1qtM@R9WmmYh4d-uELP;xM#cOJq-#R!VV zPdNCwL_UU`HIY?^zAG(q^g&}L`++CH1|S;2^ZWq#CSWC_9;vB*o+t6MM6d+5#e$MN zU$b0`g9$(u8EUIF(0dx<%xP@Cn+#a!R@SAWGER<(K!E(nQUx>`y-9|Wk+I@L4Vh1n zIJ%Ho*C_0*pfd`R(9OoUj3Wwen+_A(#ClO6cBg_&{Tw)4AAZCFuXnjNUd{QwtJu;l zz7(#2JvQLmi}C` z26x|oR`R&AN|VP>LbY~2t5b2szx#XUzfG;SKB-?VpP%&Gi$Ru2F0?fR%JMN_=)t`(#|AZemQ@q?DX{fk0UXoHF02uQTh=q z3KZn&sA4g?hg1^CJo6Nm#jd~6i(gz-&NS3^$Pqd3Z@f?oe}*51-iAWu5&5@(-!bjF4f&B30$% zBr47{@TvC>zgTxQ2!O}eyVRk_A z@7R#R&PlzcT2Iw^3&urc{~(#lwcEN&hQF?Xlrv4AUdN9hGAPB%rr`{s z;wKjzckZ7$J$%q1WmNNrXzP;6>1om7Tt;+5q$yq2K&*P8!J%L?AosNHsrDtVjtytT zVw1Cmc`5X%txJYx3Vb^38v)}=#>Y2FvDC)I0~~&6jTEH7TSF*J!?+`$d30IS)F}-VFgn20X$_W*dd#K(u=v;nA8}^ z*iLW31LnA9u5nHrlQxhCsxkZ?Qf$Ifjm)_dz@axfJ) z`BlAookor(gmIIHr2%tbw|oVydR1GG{%#x#T*Cu_jweju)AC21>wGh+34bs~@{#Oe z{f$p^j{_p6<=mUwQRETGh%Pmog`gE%?4XmV`2fA>XZ+kSbrE_?HJi(gxsC-f^wwgVi)++JC#tV>*72zqs1AvRg%5UdNoIq zD#z_Hm+`7{|N7Ssc8VV|70aBN@9mY9!;^@nH6)kDL>42sLHhdA7AfMi2I-Yrw078V zJw4e@ISKLR5+LBl2jkMkkWo7EH+K(rL*0>}G1o-kI5B5gk0NUp2?{vb!=)C|Mz&?( zph3W?Z3=9ZX)k?=7A_#DS9_BR`6txVW3g{_-gY1ppK=>nd;tP5jTImh?zg)m4$SHSlEwo^;ZR3S zoloB2ee;y7@UfCP$kNhL@9BApodOKQ-#4dP9AMQYL?;_P>OC9P^lrUn7JX+jjPi^G z`;e9V9Rj131^>pyyxFv2njs6K#EAezU?#KskVs|_WN6lrmK1d8web*e*ivner0n2X zhklVL?8``yZ0qc@R6%3vYQ)tZv%~)3?!L$WBdeFmTT9$_9^C)Avr&R_%QA@szw`p- zd5|v7WIDh_jW63VYn^S}Jo07*-EGXAL=S{&piW8#y7-*tQ00T>K1=|9!^wv#0*o$( z^=U92y5ArXXXw3v%zE{4c=M2QqaAG6>6Czu_y?=?Peg5+y;;_t7#B)J0sP3Se5laO z_R~R0s1}lPse_9#NhoWs{prirNo%;hTl8x&=B3r$sEf?^7uM``DGdQNA)7OK7xK5q zcwBjfy8MS?4vp}dUL|%TsL2Ul0;kI^q?l>0d=-lF4(Yr2M%N7m8xB>cmCu|hmSFyX zlGI+dan+MV=jA(1CD5aq2T6F4=R$vxQKesYN;@C(cq@`$J-luV2ukDE_|l}{5|IbL zr(>pOO}Fa+{XTB7N(QQql5l#`P&~u2Alq$yh~-|E@2UY#OGpDITSwnY%@04zg9k<0jqXHs`Q`tL7FMbsOTo!G#-_=f?kx&=gB5ytRjw zy=wazin;Dj)MFC0vX#wS8Q8l%6;>f0EZZhbQ|yH!wt!6Zz^hO`^?ZQT2zS4iJoXz$ zFVX7u|E+j3D->`V+dAt<@e#1kgHN~(59@fPa3CtweRB}5JJq!rbeW)C%r+N(7jPP7 zVid+@`=f90{f(sM1}K6VtCcRo^Q5U@CEGcmaLmfj2u7I}_mF}(S_tJNZdH5X9Ew=n zp4PC~@*BFSU#oMyw8r3O;~=#~^#?FqI?kIs_qii4Gb^b(ToKYIiV6%Gy@sKu7(6Ky z*VMfKe#thBeb3@dUo%9YypJ4u4$sbcz~=r03OJnjTsVzIz_>2`Cgl}!%eH+EiU_Uh zX8s!oHtrAJ$|ZzI<5}5|{S-o&u2d|9sA%mn3P{9LTgI)9;|A%7|rnIqlCfQ%S`to!$#RQ&O^V8WohvK=-FsM6}_Y zo_`O`SD}Jp$NfP!^$Wl9c+raJGNU@zy;`&QFaIMpM1lT!aDXtO=vy=CUioRUtY90Z zXx38nrzNgv!k>9TBF(v5F(z+Rl1Xpq(yq(uUalwQv#sNS@1Id>{6z9+tdlq$C40cC z{)$H^x)=gBi2h%Ax5PN5w?Gw(Awr zT8y-xt_B_Wie$Q@e8aUg$*a>mqw+9`AAZluB*)-x5KfZnbOe>m;1< zCL=D1!A~YDLKPSOtN`oi&PI)j`P`f)ZcOIOo~7*Zzr7z#S+VE%B$}s#wv(0Ci83+< z+M%5H#1?C+%7{a0w%G5jeI8L%{&s0?+(j^jP8M=$n;nRx{nVnvR0~=h=Wk0DndCBl zAdXetfv&o`bDnjJD}@thExX){w!-Aia|>yg2Pav1ZNA3xgBeNlk80hiAen0(*Plfn zK7Zm{4Vj`w+_z-j`F;0h0#9Kd0*LHq*!!Ow{;T=O%)y3&^jry^{lK?bNg33D-WPZO zfzWW^ILE0h|18d}N{DeMIDUT@vDknx_@co(aiR@-9_?=|u#y#-O_v$yI4A@zk(*}l zRA-9hLX>Qe0s5Z>GL55^l0$tjOGg48vrcvcQmX{1(mDlUXj;DG2aPK*sm}z?D(B7D zSH@3Semhx!X{b+wr|J?u%i=mP23P9j1V|qV=-pzta4POUR>z15Bu(7wGK7FnEoL=t#- zF0K5^TFhLlPx)DM6kkApdj+UDUXcigZICDKM#7nw@)cl@Z@qN(>5%dN2C66qqjDNy zu7Q}j{B>zrYq;#~atP;?=7Q#GYH&HZTfv3G{Yq#pqibU9EDT_5Xl+{P_aaf!0y9Ec!={mlg0QC5nByGgtxJifl>&00Q{dZN>ugf zdv{8U{}?hY%Tlpv+u)mD^sl{Hu%p}IM&HRvh&=5sq+sr7q^h0BS^1kEDM}^tb04RAP@3@>owu(NjrSl=w2CZ_A9)`Fuozv zMU2~W=E%VjdA{7_8+NM-2Rk!%Ko>#2Oa9oFmd2Dj>R%4$FJc(z!nlGbso9NT1x9`A zZw*3FB!WKe2KdWo4j2$?zmv1iT?TiY$HbgtHjN!%jMMeZF;oo!5;p;8KA{?CuOQo- zv_z&ySQL>H0cdsv4&|-@&zvtkTO#gM1k=Zkl2!=G4H>>S7&c`0^728xc@c{4pV|KN zzF??zYJsl|ZZF`ho?VUj)?c;807YIJq$+m&Ie+aA@n#@d5)c(+;4_xjYd5-+D|GMd zZUCj#f5^19a3PiU(-mqBVY#G>&Win&OR4_T+wY{euIp$-&^Ri8%}I5?vGD%o|_GE~h6ET`gf;rdLEe5gI8G zCB|tNlFt?~E=5+)gm&@}OC(%v#v3@Xi9v;(-E)S6QDqkF|Ni^O)r><+Z0Ivs$Dv$+8>>4 zV;b1!Ii`d?j{k(3y3k3LBekIZ%)}bd!jW}NTO|7mmQ~~|On$thd5SCPvRev_qfC6sJq{fC%C?7qK4rwzT`-k%43-el&I9BF1o8&tM+^r1 z@1SDm698>_-p^0ypuM49(N$n~{xyAKgHkwon^kW{7`@A_prQ>V!NT>&qGD^w(a^J^ zGj+ZBdadHllDGys3S^{*Ck9|Sgz5IzIUKL2vv$z98JWgQ!29(|jmIB~=9y=3rt*a_ z0KRlAMSv~-RKvG!kb;7Al$MJal~{yj^tacw*^m$8pBqi1r1tIBS?X9=I@O^k0-SJy zlTv*RJ+DI_!*NnnnLaX=E~;<_;Mea=`~uvbG7uLmmaxTlGAkM_?2=SFHC95dwQm8? zAvRti}@2yC>)y|MpwIuV1hK)l^H z9>N>BM!B}Gn7wUK6;GT$o_ucxx&nAf!O>VzEyDpLA}~Sh4wY#2WQJg{e?jF4(x{az zvM^JZqgU`c2-=hTNmKnHcFDxx0o@YMjzxhx)1$qa)#4SazuY|=GqOwmC6~E)k30J2 z%^}^+lH8HD8XK1fk%e9|XO4zDqHs%n-S$FjbxwEVT8|!Yl>?*r2uH5{cD4-;C622PkxB^D# zYs<`WN_h^>P*QYjfdoAVW-slg}|!1)sdt{pJGEhx`+{-!wy8hr@bbpoXiFv zU^;)A6SVA(&Regw>^~`(!e>@=?d)SF@LHDsQ-qjz5*U zIx`_Ssi&aO-F2CCn{f~Rfz_r!c+YLyyw}!!j$G|zWr~O`E*Q1DUhlXdP?=vv6Ey=# zJDrxKJ6Ze+iY1x-Gu2dfhoohV7_3M;m88e?lOzZ1GNdK7hY&)hd%MRSUr|6BK>S_{ z8;WJaVx$X$j(2R*%SnFM?4#oA_6vI05yil~EA_B+ou_I)&rTi_&8>967G6A1_32Dn zU>%o~s6r7YnNVqyJ_`RE#!_m00o5YOym{FWeq})q9aBUCrI(_0tm65H0$D(kC<k*q&p+Fygcc_@HXr=zK#vw86BGBS< z{QKHWy4FDIf$AqoZ4gOyE54FUw2BeO$Ud@mK)3l|w z*jd|Mw6e^ZL^Ccyh_Iy?mnm&H=3sXwrAajW2BZ$}ff+dy@KgW?o`>v6th zc71lZ6r%M2-pr_U_e0NB<@gA(3qkK>v7C}7U9~Q#2`B?a=cg9`MH}jn4Y?_{CpP^+ z2Dbs(#b~zmGbJTadFO~MLH!Vg>ZhM>%!ro7()m*u?)7V}jG8O{EAqDCU64a;HQpxN zySu=2H2u@x@aVb3dByyv-*nVt;`y_8{ne~Z!Uo#fCd`d%L08f|V<(*ixTp?gU zkwmEyC`ShKfKzh*bIWLu@SK7iF$hxr;Zv40BBkd3Qz+|Au1V8(9=){lqM{%k+f%leqRY3gH;JtB4|Jp5){jWi&ie9}3_bNn8b?S$9C4W)0^&4Ih_~L0eU54z6zV2| zI(!NgB8|I}9}CM4ay9!Eq^Ma|cKwC}qc5r)UGLb_RiEUMEX$lDat5nVyZoCeUs@Nt z-D91p-j#32;4~15ex(2P*uLT^Cc}Zc1fKjRYPn4jVzjPzByqyIG^Q|Ie-1VIYu*9x zF$n9i1>=V;b-yJxAL(+N7!~}XVUNs`E}kL?tkCehMRSu#5K3ua`IfA%>@O6A3L#bk zaf?ZY#g!(bj~^I{!S4OPbud2Fn4Ntqo{1acD~#tUPIymrq!JA+}>zD5|cjCh|ajBp1SV-keoQ4C({01n$J3r}I`(x<&m+n_Y(!X431yThztrA6W zhrK9ydZ`BQH@7F~qZF8b0T={dgZ@xAZM5jRd+xD7+)y$YFw&#qV6I>6@1kK@Ygr$OMD9a{ zx=v#57H9|Pe*r(7L1P(qn!zd{=gUA{Q0Yl0n2Rh*=c~~~8J8KKwhr}JO-MNL^2nGx zTfC%17!j=J#Fi%^jX} z9y9?jdL4~56+z;m3k~nb9_Qw8KT94mKn5BtBUhvwEcQjaBrH9LRo{W(!E?^8xa2#1 zxfPX$==Q(8P%v8^!3m`d(8{va$OO9wcYwBYRf#=>Y1ntJqn=;LRR?45cn!#c%$^OS%+9 z+>-K&{WOVIur1#l2R=>|Mlbae=(r`Q*W+}kr5Fkh@tv$<>~a||9x=Acuvr>cSSTDi zHeDO;dV?*#3A&W7$Bg#qJp-IX3c<+0bsXg12|GKQ#Bi|zMioi{psR&@fIzD^_rz2) zE}BTi??C5(Z|5%VMEEiLGT%4&JLpp}1tS>>PnXU1jc|xa>j?R-NtHrq;4pI=JYqXf zWIp1hKKAT6R^?nE50K<~WYfEI2cOO6FP8C_oBS*7>wZ;FFBa*SYw#+ftXJVg9=spP_=Dc&tg~`na4as7dzDV-=j}M*XL8Or{F;nP8;oxRFI_mhW9OspfFeIZF#8?6FvCg~S3uNNq*& zBY6y0B$8V3{_){SfR%VxA!DXmuIALwG0>8uR#2>3TOsBLii)Y@MiZsWuXpohQVC?N zDNg#i{i)Lwq$q^Hzl04L7jUd9f=4%k6Wz$Hm09HV8}_&#G`hboVmg6==jA4)5L|B) zE?m*mW1)R^{xjWvb0k&EY4ANz5z;(~2s_evO97c4kQd)H2kMfK1R5t#j#y52Vy{Fw zZN~B4Rz5bFz$Fv;^>nd5a;MN-84l#PpEc$miKRFjw-XFNDBK%GAw@P#d04;45#)tu zHFU5nUhLB`JA3<1FG9VkO4dqxY7wa}>-!WMf_RqWwq$SD+IR&+eDO zV@`Sl+Ziv?Gk2g}I9Qk8%@`H+^TgZQrhP+}m(2K$;IjGmd+LIbA1)CGof7fpQ zjaH{%PU}IkK4tj`654U%PrP@C>B>UwwCbW|#H*x1*~mTpFHK* z*)djQ?Ww4fCCHma{(|>N$aPD!znA_`^fZ|nl#Yj-UU@?vtKLGvGn+||LPgXl_x(ZE zcG$Q&vo%k4U-nVD8GRe;(2Di8=N_9X(~Lo40ol~zPC`67JmGd=0SHR|IrcU>s~OMf zL_1vptbj{(64Gch?i^Or_Bf`4!L3-sGOhS?y&hk4^OUA?x*g}%HB?RUQ4Z|%CSj*Q zBN;BmEOsa9L@xP4W2@w}sd8emS%9b}hnv{a{UXiQlqgXJ8ttGYF2NAY^=8SGgDxrN z5k(a|{RECAIY{VtMM)Jme^SXm!q295j5a^L&vQm+laWDByh z2D)$r0V@_mX+*%j2ki{>@T5ZxhXJbVPd-w$S&@J>Zy0{qWH@mY2mem}dW~tr*H_nM z=>y(HkugK_Fht<@_bv9wsqC9fL%7^6CJ1q|g?TF4p2x5hLuv)orl`v0Um%@d^dC84 z1vZSxzn+MPfUpAG(TY+(tD9sGii;iQc%dt!>)~ZLi04Ybi~u=#i3rCovp;y4sa0k% z+}3pZ%eUBvMzg6!kkTp#F|RrgiJkYx7`L-4cP#5znqMZlmVFm+%&A9GIU9OYh6nEzJ&jp){`m2IjEKpdCSQXbH<3&nVn&huUM*-RXy0fLorI)!VE=#c$=p1PxYdhX z|E||gD?;>0yY5fM$U+8y4l68A5eT!Rhhr9@8X3aEv4cT4m~s8>#FDTHx(Uu7*I1aL zpL)~W7-_TPB%v{eVC#t|qu4z&J}O9%Q3Y4dJBaJCjof)pt6sVVX&~&IW2hR8I+x@Q z&5^foE$~C5wCN;3@M|b4o|?Yb;(W7pAZzTiR`9#p%C*#|$?0(SG1IB5iE3V4x-^7* z9~9E%vLldlImto4h$D6L5R;lpiXVU?2P#mWQ*R_M=vz|-pE5BV6VBK!*`FluTIi7%GP<`{dMmM?8dxIO!}+? z?9W-Z3?=%?v~7yp`7CGB5PIfz@kxv7g(Cd_ts2RT>?;&#Fl6Aj#AtQ_8${P9!M;5Bz{O%f8#Q8(zN@{G7gDIsdM60N@>0P6z97@g6Ur9 zNU-FT&E$9IDBEp2|GqUab_sFhuzY)5(Hreu9ytC0N46?~U-cSEpb<#X_ut*MYKG&o zj>&S+IG71icSqEMiDd9t*57$cosr?tQMjjqIqLjM$}sW*?Dc(Uw)rxh7eBytibYUO zGyv!AfdtrmSmfO?;+FXjcUl?hy~(hqdPX{XRTgh`piCe9j-Fpb`wuy<5=z|oC~R+G z`u*#*?u~3kMOEgBRotqKxLsk7bC#;{5Q&y(Y#?K~I4+8g)H~OM{P#Q<4bH;b!#k{( zobTIL=iyU57gCLj-epc$69GZSUgG3fb0$ocC`t!Icj!P_Lyr`?h7_S6U;xw4M6g=V zfzp$nQdCLkL=x>U4Yv<7&6_-9Tqo|v?LY5pW8#__X{#Sm`*hgdbT`@EhRM?kmWoFv zwhw(pb_I)h!wTdG;u`e*jF~7Wff$z-VNQ@w64jZycGa~k{ig)^mex&BzK6mjFZ%fW zIEj-hM-AfSbhZ8id%xHK00##yuUQN&BWY(S-yP?IAurs8_?}=qTMJ4BZDH#UVDwyk zti_!)pr0V`>vmPxwC@1_sI6tTab}NK$hyiZ5r$;IEU;I7xN(U7! zSvYx1iNRa*v67|vlSO&F;iu=nz6(1Q*Cb?>;Eend5l~5!Q$o9i@8l*_@QBW-DJVhi z1qAda@I$O^Y*O>FYL6=fL}Bgj)?dc&blhwQv9iZ)FOM|wk@7)H6~!63mJel+te}=n ztydzA!(H2%6F=P8qCCRQQmlf|T9U*6Veg$|bc>ol-?nYrw)?bg+qP}ncK2!9K5d`2 zZQIuE_cu2;GxtvJ{5i?wJjt$AtM>C`XMM8sB>P)?RgHcn#6pqc=8&pKR+5I+i++ly zq{59m-L(uA=`3c%#^1d4`2OPdBTm7;;akc6CQad^nz7VxlGe``Us@LPfl?-ezQawI;qX7v8o=_4Ohew=i-KuSRwh* zpw!{l&b&qh2iW@OPR%|6=gHiM<4iOiD+f8ayXG3G-l^2?m>%RTA(mJZU=|XHA`m*3qi<4W@2{~_Mp-YT2iBE~#LH~)kd@dB4O!i{MRBL(d_ZvL)*}UK>U1=BbLpOVpg}kxyOPee z1__A07G{!(7rP&m->9)Y#5`$Sbqm%fETEXnLB}?4HK$4ewytAs9w^I_-NOJ!<6Yrd zSZ-=!rroB%Zv>Rv<*=pa3Z$H0!78q}ziN1A!q$3*TXC*{=P*B1czgH)&NbT)KC7gB zSE;ToOJdemD0Tr42ZC1x)Y0+$MQ|#9o%Fb^>&S|bTI0^>R>**g78G^Zvp$a9FzXE- zFMj=MCcmr(0~fd%G;Pv|IrS!^GIok%K+LiI)CQ38eo1R`u7^IXM+@~4&?5W$VgK*| z-7cptR^nMgL@7Ev7H3c4Oic3AN7#7YW*oih8Z!BV4#T#d&s7RMFd1pHLrbsc8LC_D zl4l+n=iD{K55%MoVU0R?RTz(x&KT-!K`IIjqD7?`nUw;v{Njn}5?5a=#{php(>$!c z3C?+(nznph=f)w;9v1m27c;-={Ivt@?0AI{TG(-n9FISA?s!R1p6hOQb3vkKt}ced zb>(tFBs6Nb#Sl-|L*E!(!J`TVxU%PKiB@A`4EpFx9{#KbKP zHUQtZ2mdUxtFLJIKv^;kPuLsaLy*4Rt*D;7F8;-2aXh`<%W2-TC9KJn?L3nxcQ(2=QfeK*G+rA z15UlH{f=_5ti1Gv{s;NA(PC#c_)v_CF$*b%!X6n>rPWk)Es|B1()u8ZdRqX|AOds8z&4#^9bN8GlXNb3v0W*SC(T)l2zFCrV zL!&eu0@f*ka2?;^3MPAL~JMxh?&GYcIs1c6)cd> z)5mb54Vka~Qao`0tuw$Q=Dcxtb-!1wL~8$Drmg}&>Y^uDQXM8L z9-l)h&rww56di?t0FCVV>dNk*46l)6$(p+6q$|{?TMs&sG(-GeFeA9Oar)1zsLu@R z2MdF$<$g)ROjr*T5RC>$Dxpv2lDX(!(mkCwr@R;k8MV*0GHf=_`X83^i$z1ZhnO zMDK(dyd;y9h6>;#KJkBEc2$I-K}g_g^jASdjO)Fu^A<2`FeZedm-wj^pLr8fe4M#L zf&`@XgVV)d;CF96du9W=OTc?97HdB6dSyPK-y0AH6h6(Pa&Tz823`&IOMYk>mZ!Vu z1F_O;5kJD`6l|lB_C<;g>@L$00rxw+Aen>;yfINlPhcrPY7-{tP;J|*xE7M@7iGCx z(EQ3u0uy@|&gqD{h+Qp>z`tld`HIqLsszI*VnRsKbl5r1U4%9Ouq`WS8M$C*c{}bA z4TL4!%7NwD&d%;HnGtZXG*}64D@1Ri(@{3!H{-h_wocdP!xpsN zvV=^k5QPBpNttIha;?;l?4cMPO@Zd?@;HnIN1|Dh&8ZL_{&t?Iv%{Ki^_Q5m$d3Bp zh?%cvx!=yQXpLUYkN#At7ZI0_d8iblLI5@zjeikXJdxAEmJ;u!^O+xC2N`Cqh}%#e zS^N#Ho;$$YXf2+=9mQecAF&nlSq<4|T8^x0*c4W7N$$1FH0o_Y;yQiG!9@ zlZ9C^C3ANAfpKxw+to+=i&ueuZxw_qw;kXbKCS9;&0U^cDk$nI9Avg!xrD?1x4W88 zl|hR71*u;ctz)hj1-Kb2PKyu#hW;-$2lu{*EFhPs2 ziqs%9$OBgyqKAkwmQ8viHp#I{jV;EGmCiM@jk(1j8f#%A-mvPHzA9#jotGTPG<#iO zM7p{)q_k_*!`9OvAhC&toQCqO;E+qBMK;NEWVQ3o`KejY5-|`~L5_41BVbw0K-z=L z?0v_GE+^n&O%g0J)$2fqsMR zQkkWA)%VV)m5n1YGzSHExqP6QDz2`PnVJM!_9Y^QGrI0>uty||i$cfT^qfvv(X;#{#`eX5E zriw7!h%NE}yvF&}taDEC53#fy>U1bfM@=@9`~q1D+KD7nAQFyFQsa4h#^fPc!~+E> zMKC2}P%yH}*5~93had;xdwzSLG_jL67JZd4d;OXV#JIoFPK?MJm)%ns2$CogZ z=hqjjd@@kt6DrIJ4ZC&d%;2t}26gP8z|}lFTC$*9Ov*cH(z)e-Umn^TcXYSDvVSLX zCM!D-&$d>?2htl+({r-M#!LK*9|3XkyLgQ|p@ZKc_Dd+GJ!u+5oH}Tn>~mCHx#jBxfGD(FhPY;d zRYARAG-hpFITz^oSMQ~CdVFFi!{UUzs_s6?^+Z$t!bs1tsv`9rl1-vvwNR+AL0=Ln zLFl9GtoYhx!!bpxf<@*>2%s(;8*miu%-ff@wLW9lg6|zDe#OgMc+#7vTYAlo3pPLW z9r%fnLA<%T2wWh~7uwi)gJQj%#%Xm-G_qBWK@e7Uvq!#egLa@;{i8rXI!)?&hdWctRI<6B;;@Zg$Z?PUbPlssnpq|1FuN?_^Z@ zhL8|~Lw@CX*#)2t7DpAn_L9xjF5{d)Q-`pcJb|zk%eRPat>bu6Ew@P9cy5S-hc znmIgaCZt{!n{VZpiB#%6uW<3A^xk_21uZoG-o^8!u#Rx7h0M)opxxCBEq^#5Crr8p zSI~`{hK%uXPDOXCd=Y3PtB0H;X>^Vyp)+(=LDIXt#yMiP^!H{mkMlS@Z~h1_hyv#!eBJ?B4bj6NYssj<)2YFP|2*a>}Zs?MF-zP$|sdEzTlH50(L-*1clJi-Be3ZP$hh&Y z7GxV)jpD-)QB6%cv$FfP{tE;~5kK+BX`z%6xkmu~1EE>CeA~yFX(%h4;G>;!Gn!icvJAcKz95-l(1q z<(*m~LBgQ`|Kx5gido%fOd3mhwa160V~@(#`GK_|30XJzS1?^qsc}cX4))Rx4XMI~ z2<)1`*dX>glNFWWfaFk>&yLX$8<8XU$u4Ym8ng?$>S`t6ku*0oBUkup|5JCiTQZL& zHU`=1r>jAScAp!<^GpG#Py+b5TGUVHYtX7@NtQ; zSEbZvr5n~!I!nR)x_#Tt*b(`>^3eSk_$!r1i@v$68sq>;fiPN32Ln$PV9S03$D3QZ z%(A`R`DFQnL;n6RXzz*|Fawy>9UNz-pap*On?N5b-Wg~7YcGWubB}izOW-l}>O*xA z7+|-+4>x=!)<(ML$TKt`2z3y`t!HV`GZnP;7xFW}OeOj$?Q~5>iUI~n7OpgJCgvolf&bP{g zN?-v2 zjSOZ3qgTYeYcGuy@P6Y_Og2uQ;1X=2L>}X$s5}1+O?*Xg6 zlh09?1uqz$ybm(KgJCs0M4u-yqaE&22)^{{v=pM*8(^m)f#HP)Ki!U1+&4zAs_gqk zm@b6B@f%vFf@|GeaCS1m6oHytY! z9q%a|eufUj2C@)sWcIh9m_i9Q@d$qnuy5SzX4_-c%WlKt!rFv(N)G zz0TwN!4pzV>fFY}L$8}wF#TL11<7xeGvnc6++zoHyfzCH!vr0eQUiG^!Mp`Gmf9~tkrArxTbg*01Z~tXoJw4DO-w^JQFG5zFMyrJ z(TLY2>h@7O8QmrZt!Ny>ieeO|U|Q^LRO2z`yMFM}u$;=Mej))w;GmaCuQf zej-M6|IVbJwMK8LWkI%VZU7TfB~m3FIg(Kv)a!6qO6ih(p4@2p zD`$*}?0@LxeivU*>URD{_~_E$G+yUo&MV7BB9dL=b>`j`*h7g^g2oZDJhg4cViNW9 zsA2#DMrDl-k)5aCq56XQv$+^OA5^AActfx9& zE+?EY|Ax6>e0x&pH@pkc;5(ZmD0PtP9`or;$KA71Sk{QkHiKzR{zaEEyjaMzF92+c z2txo6Y@(l+wIQ&XuxDzFXk{{$a8x;?_Xy9#y&t@~$J~=?zwXd)sEsneRDL@haRQbF zQoD%HKbJT=5~Z63v#0y~{LGp$6^dFhtCtcEA1--V$>!+_Y$QtKBD}b-gI=Qr{6o|p zr?F<~eY+OqR^n%%2y(U{J;^Xm48gi-?qodJaP>ni0uwD#7;&yu2L|AYkQki_^V3V_78kGT{fL@HDb@g5g%=bCdIme5G3cQZbWk;QXswownA zX=dbSF#HMSt*GFu1LznKpbdzeLWniqMb)4lP6S4-II@1f6h%5tuRqeL#*IYfq4V{% z1Z`G^*d7w-KjAW!-wjl?$R@1gKSaa4MeZWueA6W6?VT2?&DnvryLh}U zOgaJOW|m08Za_Z1(R9R*8g6UKQfPevsji7~yUSfz4iG-h@S2>7$-06%bq>8rT4{?) zl{Xy@q7BcXZrjYHy#u=WOGxp3Go4>Jb+qP}nwr$(CZQJ(j`S0Gl@$SYO z_si`M8ClU)U0o5?nb}oY&qK8NVGZ}H%8dB3*M~Ab25C82ge394W+uXam>3p;PG7IZ{^cGVq$~EZ^WO_DK(UFmauGrb6sR{HNa9ks*?4Ipx>mg5df~hZp|J zI3GT9Xt9!8chUsYQHfd#GM&$CFP<5*17hhpiQzMHaDAStNB(6Iy>{d~#hKxaH+ z>0(V>uhGy|TCLuq zNX^*HMTj=b$KP*sg}M<_X-CDl*F&;nl#JAIsrvO`3;xWkhUmWO@{wuDhPuuB1~NNu&k|=L!57Wn=FOmW>*fJ2f|bA7`nj z|0Zc!{Pk@^Q1=3(6+rmhRBx$0qJx=ubi~^ba4_vWRZ#nV#2pA8W_q~dSk=@kWOB8x zMJ4U9*t#h5cF}KK!>8GW9oai|h|_2CtJ(vOm{jA@7GFd9 zP}m?nzM-fljfb^(KHF4)oWFtth>Mc7>)pS}tKu;x&l~b|G>)vjG?+Dxc(uJG{!qTQ zw_D1Erf=A(6nM4+AOM_@#a7M;>Yy>MZ)5+doMm4u=}XX9GUFtV@XSL{{|E-o#^Psa zD<;8T`XgsmOpA84%;B#7g}t2>l`Snoqw=-Pq2me2`4?pZ^KcVUC&3%roN78w*tCKs%3pf9M8?^3a;cJzu@apYE_wLRlnBvzp zM>IEf^HxSNUN3&F9eqc6kyzD8!D4oT>E|cJj7nvD{)c<%`Li{>xR-7NEwZ<-5Jy5Q zz5a0Svwz)gK=a{Gr`;Z%QUQHJUhPHi;rvih;eM$qdeJvnCU!7zmiD$x`$7@2)~BS zEgw@xr$+FVDV5>J@n4<`lWch-l*rlNoKJ|8m7orYSWm({nhj&ST+iLisae37qfICtpG)w%iYt=q{zp%VBsR+rsY|sP@jNiS>ab6y2%<|n zfAgD=JYaA^04iZe`v-`~+=9QmjFLAC%m$@X_5(Ab=cW|3ZY3!!4$8oo2O4ty<6PXa zy#e}*);;$~=c38zhEMUcvPuz(6!Xl}#^{$gn`cLP_4C-i{9Q+)LHaDF6P#vSR4TrH ze5Ohm(#aplMe&7ye$VV*Axe&F zsaPebI@^!_r~_`2q+eTzPkemZeG08OL^!=j7;ZQm#YK_j#rzq(%Km6q?)3F6@KyjZ zi+5UpdG0_pXfZ7On6H!Q@Hbw*V-d!UWC)A7((0p&0Y~%Koj3X z8eK})!FdYrr{0d2ipkKZYc-t>4m+M>`0l&6=wOO=Dpm9(N@|GrLH>#@By1vxkK%%w z@Q`F*6rK{(=YX#G!`j;r!FlMs=uR%Ur4rQ#+nQq8s=hN zIvjh!=NI3he)h2Hkoj@RM{w&|Sw-L}CT-aY*!UrPrS&Me%e~lUaLhtc9`j$yVKlvC zX@+h>0#_bBxO};#neO{ftf>cvOg{Swo45 zKuLPYPdbsPQ!HUC9B=#ztG0M1s@a%|{FsF<5Big6Ld7c~YpI{^PtO>dgewm{q$P~6 z9UyArzRZh-7H-DAp_Fhp!Px)1*SKf`A zfQt%B|9PmQ6{!V*-UCkHcR0fmpkLek<X3wuf-P&Qz^A1^~?s zp-CJN55ENJCDr*IYz-WFD7{3|r1*r97+EyB=SB?!P;QC=1K#3 zHJZ!PVwivCrm?BdF#xdn?+?%SRn&*^Z*2}}!K9+27C*(DEb_xoHaucQh|xi27;tVW zf}|GkE^vhzg$zffR8tA3qz=8R1x$6u5M=B}t!0RnL zm$RFbY*DUY$crX@yR1)1zg-MTUu}9KG`T;3-1#JZoR zb7ARL&leW9R|b+)zW=Fvo%zG5v7CvjZCB`Zeh7p~Z(VB4oriz3(zSJ{WI2c#YBk!o zbN3t47ET{wNKjGw1fKE3)MadSf#TxLX!uy#+&^l$vWd_NVyztX83Y`{x5K8X*RsDk z?qInGdRPB&!VhD?5j_Exd_**Qce(1m`Yd=(xEAjz|bs3=F#Q^Cl z)m72)#4XwaXcLcQvAlLLH&dfN&YOe^L1q+tCo+FUP6v}FqV-HVg5XfnIp!bW0e-tm zBzo)D)@lV8E4T{jz)}134W5^X-?aaxlN?uAP3@85%Lk!M?vW*Qz}f=K$)@6I=zX`s z<o%iR|;Y+tpVbKJ~H%CaGIC`!}y#WTiY$=8!UX zvUa4#6-Hpw3QlC&;>Y{`?bm)cPL}qmRi^0G@N_8mzsR{!=v}B);Uxtz>@xN)D@cZS zD0i6{;3P(fqx5i2)~bXXQw3HJKwf|@*DR=Q9fgw;X8JCJ3wg~=u6Nl8f)_Jo1V~w+ z!*58weVP#{iS(KyJIXR;ww9Wfhh_=Ke3yQHuq&E(RFkB_&P9}!mHV?%ETm?D4!b15 zPQS)b!OC&{dY<)bt=T^)Rx;J>6}vMe^%YKmyRYl9J-e^gQ)vV*Am8KNT{#krLa;M& zH0;^*yriSP%?1i8MBaU-(Fj;5+-b>JKUX3|p;`QE-Gb1hj?9-=t?t`gH(-32En%M% zu+#asNp|Miln$NMxAh-?O*3irRdt~f*vh+;aF>Oii`QR_sU#mR5`+@HBt(&4WiYd3 zF>l0h1lD{RPC`Du=G3-JpYhQ=jRB2R94}E zduS>#5F?oCo&p-Pp1VE{*FT_pnfw9See9c@bsFE|L3FaO<2^$HgZ*eri^S?kHYU7H zyCfH7B*&6Bv_~O(Kw_q9EAp-HTUqnkJKi}*F4$+ZvY}U9tSa<7)5h46cgIl9_FV2i zJinsT?>B77i*WsckU%HW0yoi^v}bM|Me)DB4K>sCCLiKS@Mu~s^$FF(DsOi4hhuwz zUN&CCE>7_;aq2*`AtBtJ%iQ%Ri?Uoc4j^I>^MCOxGhASas&yt!mO-eV4VfEq4<$OF zCs)&2vdW6NbUmStk4vP8i>^5qc6;=>c%d#5>Fi*r!~%c6nGV+Icl=|H7e@G0lUdg&g3`Q! znM(dP$H}agt%-jaUyOqz`MIZ|zwqeAbcG#xwXxt2kZT z!wW+&VFm6pxx8O-j0gM~a!Z*@n}D4-3e z30?l~zc)_d3>DRxSA?w7( zI;9JNy^Z_x-vi<5@p1v<-{_6$^3Q6Lks4zhM^(6xyD? z9Ng%qKk{t^UXvtD!?W=!e`Fl_QDeW9#AJ!L_)ZKpfhUSVsegxtx6t$GW68tAShsvz z7JK{{?u51SMsLU1cZkQ|7s;(8xvj0i`BaO@<-8yPA{C6+udew|SsoJDI~$r7#WzVO z{}#TiIxjA6jsw50p&^CZK}1>UIb#URYi)bmgn6mH%)hIyQn}l?)hcov#p!rn=knkD1H#OkwqdN)h8Bd% z;BKsM8`G&mf%Bb!e$_InIejzp#J;AiyIiM`vnO%N2LQE0RaGD%I3tZrGejvALDQIF zYB~hej?V#C>wI&i*sWVcinOVLAp{kodZcxY9JE))eOE7?+z9XXGL{HH;VP^us-+;Y zZtEaHbXChy`=sBTVK0hY>qhiV&uUd8@r=sE$m){4YyC3X`BYhpx+$e0EK~66Hw%zA zi|~!|7-_VQ)1n25PqT5?o;mYnUy#8t1wtDEyiFj%Gd2zLcM2VLtVpt%&8K$#eadi1 z*rMR$LFc~SYTg`|_STMV$bi=F08N|4x|R2VjFnAH4|HLyjS@oLvVPR;3q{AMVhZGl zkF)G9SgKdB{unaK4*H>jLGFgq6|N@b=@`yJp%j|X=NyNplz`y^nLjjGZ-*^MClEKK zx{@vHf7<}*POaWOwgEKo3^U?$PB+^Zb;!~Zu!W<{WtJ6K(l$GPrg1@oj76;;5i6(! zld-Y14^N6AFEj7#EwEiLfL||wzCfnDjvm!-yC1YHS8g1gF@3uK%cB;Kxtdr}HoS=_ z7p!Otg0||5#*|7McQmn+(v-o@iuPKh*YC$n2T*|3M9CLKS|r0C$OJoN9aE|@+i4Lv z2g{6VIy)ehPRi3!%aY-_H|wuGM7sExqIGKMSp0C704^=3L&*emw`p-d zr@Y`z*IJLxp&`VG^CWb8h2G*QlZxAElUTytCOD=a=40VMMg;?pCfV&WaY}jCa16=7 zgFJZchBLOuOc{`IXqJes3FC5hh?#Ky^`;p`n^0kd%(c|=he$?sX&mA9&3t=4ckQp@se!^_D zE@FHyu>$-%6`xtK;SFu=wT|FOB;ok!^NxhaiX!ME0mgzG=AphtST)Ri7ufMpF1p!`O^L&q_6? zqF70D|A7HyelAA>%K*_+ty48=U17*HXT)x$28mY?Qt@jgRA!#MEX3Kz-?SO)Qv><< zhCt*(Rr9?et)HS`aZ^vS?B&4B*a}uMLumUfR)cfV2bC|d2G_id?2-(!D^i`R76ZH- z516KFwhV>)3rMfQ%b0K_ieOXj;Hgkwk8o!db@-j6kt(@PMC7qbkvs?p`J z{)o{znM~#>rrPn6g%l{tin)dGmp_Rw zg^^caBg*qoBf<~OH{*%!W4hQi_lb@e1vhcfIC!R0l+BDt%(sGSy<5j9WoJ104HTjW z>f;cT+sJ{d{MzskMmO`PlcpponJ%UQh*bM{MQRVj6jPaec#gMCw;`+Ce-NCUkz-l! zc5|na?aW+~_l1C*9}Cv!cP1$7{Uee&`zJkM?mS_D6!XjlU8$K>0c)o ziYqHe-Y%`>N?Ila(zSD879@p+qv_LAv_*g6wEBj5CHY$yS6T?(GN1!0-{*(tZTZLT zO!qki06$F5Le~Cj{4s5yw3r!R2f8t2*Dii#mPWi?c`bMUV(SGR?g6t8+oA59ZR!a-u)-e`5id zlZ2Fc()sir)2DXxME0+t3U+1MXdUN-cv$0*k+eqK_*){1E&JNcP@KZy5xXeAUOCWz z5}GPS$2wGV)q2k@DAW|QI_Lrtwuhfye-~4Hz+<8H3PdS-^rp%!4oR1Ao~&L$u$w%r zkD3@|W4P>=ttP2(C}Emurr$9pmH{$01uXe1&#?H$!NMz04S2_3E-lLzta)pd`$ceH4mHz@VzpRQkO>Y5guvF9%qz>F(qeyt?3<79&$ zv}dzxF=dj za;{(}e4cqqw@-O8A{zXxTZ?KY18$|{Zu@Uu+$Zj*rM5y2LeOoBE#2iE^{@nu2Jl*9 ze312O(^XO6j-;|u+JK)QEZdJge6oSUG>?#Y;xo*_dImvjmt$iu{3$ypudKdVoC4>$ z61T})L7Ye;x`E8OCgUU0hhGT0j4Rj+k@GkpM^)uo8JVGqZ4xjSyt?9>C*Dc|# zLox^g$fqnVcYg=}CYEuIe!S}$O0ADn@=)2LQ#lx--K76aXlJxdn$P+>*(l79NQX(5 z2%ZBnCi8_Bv%zrtVoZQtc`L!loRG` zd+WJ|#0gaZ#5Oi*)xfCsGwfoEmk~nc_y^%TkQd(tA`6#rj1rNShQ^ z8e$osQ`un?TpT*7A)U6Bvmo!Pf5$LjxkQF3L46O%F04@W>+V7VH_Mid#a8W`sgl{Yd|FfWKSbklVijpGoj)ge zqzSHnqbb;W0DV&oMGt2J?6)69)rhEq)j;C)^){T7pw=(rK+ke|P(nA3=a3PV?%_)uTLo5v=` zcQP!BXq%6gIOxpA97!;j(twdwW4o$N)9$+t&{@Spgo&s&XK|7$Uw zFAS79f^at6U#5^#{%K6S8g4ScGL1Z8Hkm@?$(SBc6B>B zVWlFjut=R}T!O*i{z?OM5C2T2X|x6{4&^{6brNSMWB65z74g7I2ktM;`S-br}Mz`ZAjrWLFb((LYq`uLn2Sy$}vR&5ScOM|;9<(C((VrS(&=qq}cJhu@*F`z3VFfz|NNx&%v1(f!;9`oB zb?VMVSLeuncZZ63zY0*;QoUBV%=dC_DhoO+9P&*PkBVxj zuaU%sLrGu3oBsSct#!#nD|zw7qJ=iy!H5Ggia)wFp7u^V#F3MYurlKeaa(Rm%~ue1 z;&^7%<7IMXBaEB2@-iYCA2sLqqTx|+(h|Kq<;-EA#h6J-f@1`*{$>k!G(T)f79Cy{ zIq|YzB?eyN4UwPPSM5Mrk!eGy(wmjWw{5Tl_j}=?&(aaaN#H0(MIZG-y^9JI4mo4P_C&nU5{Ia0bE$V!wJnx9F| zgGfz!7b!VOfiJBRMFk>10h=0|LV@C_0lvb4Q%k+6lsj*O-H`7FBPv&7io6vk5ikw8 zol1;J)aTi&m|80h>5{ja9F(}6IfEYV`6&m>B}WP_(e(YU>@n!rc=sAmkSCOXyXwCH zPr;T1#_b@KGfAN6<1Z+dw*Z>`y=HE!-kp&=quV3GMI8vrJlPmwLJ}bR20YzDXe%EV zA6oxFja&25zg2}g5WZF|+=#anJRR2mO#honbZw5yv7ufgLUfY!t8onn>I@N&iaCop zr?ZHgA0t7LgERYqH(p6Gqe+y~z}f8g3Qto`)~{!irr!qL2RpTmhbT!a*|Jn)CVi#? z&{@JDzi9Lmn&ayqcgY}f3F4aFf}in9D~hGJ0wopb-wdSO_QhM;82vmYS?CUk00E|!f{9X&#_R}Pt@a>>Ug4Bl_wPWYuMqA7^J2ho(3U)|3!a2* z&8?p*uuV%x6-9d+j-(BFFcn^+_a6~IW{NDM71q(7P2`7=(r->! z)Vrc{B)=y`&UxO<$MUYs()UNl+>U^#QOcCQCj6ws~@bLlQurHwi5e3jx94J ztojodQbqwUs7JR!9`U^BpzT4)kw$WqpVz_GS5WD=?@eI}b_6^U=M8%f!4j_!)<|WZ zfP%IV3FhHM-t0QaD+ybq`|Hdcbjv$#jC4N@>0#gs@0*nl+9p8ecmZOUcb;Z`r@KJN zk_XV&sx0XF0-uEf5-TGVfy(2ZU!PMgw%E*7M^%Ti~AMD^? zNlUSdn~kVJ5R#W$>me%+J?0dnzLm=F#z;}DIHeN-hzoI%s=LyfX7l2vtR%Oc?(6*` zdEj(PH{#h1__S$^1rxa1S;d!1Q;?CaYPMB>ia1Ze5Byq~O9RK(n`8)EH60h$`RJ!c5fAy>NahjHl>$t7 zGWVdev{ggb0fwbdLJ$1b&w}I;ti4*85NM{Jz7YS)1KOcFc^$V`b%UduRY_;4uw>Zr zs*D1|j;6j8d0I|1)BkX)76#4y9MI)^j}g|)i3HgrGFtyhmL0pIsKrrERoIi%xY1&F zh8Hz9#Xbw$U4I)isZk`><0nuM248*cswiiXv3a|3;5f?8GJ3NE$JL;~;%ubMZoU0S z6PT`mw2+<$mJ;p5AIasG1bfD@@1{n1BM8Ufri1R2^Q7MWG!F{EFwIu-;gT}b&PEIO ztB|&)nlSrEy04M&6xI#Z>9vs z14mw|w&5avB{J#Jk%br=dLA(i0Vwe1}o$cQ8!5IaB0Kft*vq z3+wzLe587!jz!(UD<>p7W>4eJ%@!n3zVC=QMVIu-wXMN|r-7(bu~LOZt3-b0j9McL zAf5&6x_WJv|%xLm9lxoui zhm6irZeU=5_B&kz<`pZ0_=2|b?@C+;@lxJU_dTguT0BVP`9ol9qyJNzhDlki9n7e@ zI31foP$}avK)wY}Dlk7jjQ#9zf3gQiC-qh}?qi+*)2LxY9Wv ze+~C#qz%Nx9#K86^Rb)yqno+k7vuojt~mxNykh`nOurzs%Q?Dl0!XeFHfHF_u*Qy7 zu}PuL>Ly=Aca}V7y;@%~$n>sk93<*uJ(QtT{;Nj?_}lCI_p8V*o}Ulc|E>HqKm&bK z{exZ_`8+}V;_;~ZSy`g*!=HprxC13!~#7`dH|QdgE)S#VWe-kL-l791mZIT{;tgQ;OjN*j}N zgM-gJ(Rob~V>6>yphpiq7EV9aAO9a+|Ib?l_~yHYm4vy1y`m$dw>hBKq!1uK34IV? zKp;R6pE49!&m)k{-)SH~xuCwDK3+ckAih9AV_zpHJildu=vMJSO`jM+fq6lJAVGkB z-Wb4u{$9oaC%@kD0pa)Xbno=+|k+Ot{rxBbWs3NLCkbqtBaJm|0O2Sb+pYpp94TsJ!U7&om* zXVVF4u2e$7D z4ug`ohaYfZYnO8Y#mP-9uQhs4J}`)m+jneD`W7+ju+oPplWpIkDqx1gcCSR1L!2YF zejwczY*HI+5<5>bT((lQ1)F_$BL7bC!q}L5By#>+K(?EJz$08jGzxx0`VTaiHGMJE z{yoZ6xPqvffipq)@qj5t=_P~_nsC?U8CGO3nY_P%bANrnFjlF9h<$Vca(4~eIRf!h z3p}-yDg8DJ_WjRA8;5SMqov9PGgEn_XpY$$c+F|Fp}WU)YDT%F^NTi7?R>%v(N~-O zHchH@i5f-L-Uk*#;`Rrf*RB0^stT*`@FcxgBMZE1vw??mG|h0BuA~wwiEHxg%>v|2 z&FvO%z%yAQ3q|Cvu@#V(i#YK}v*Zj7>LyJl*+oD3i-G8a{Cfq^7_vCHcB=s0KYJ8L zw^ZU)Sb3hN0v-`8M(gf6reYCgC}U|Tlg0}ZY54^DM*#k`;S+3ckij~fXq5lk|1VAc z(k=|B6cW@%eUV|tk>PQ+tB~=(Oyb-EA3yl$TFWJVQsFlkySGS*dU7n$JgH@ru?G~v zHM;UFewS!RUp*wCdem6vtKDMZgWzY9q7oLCc+@vRs+#d|s)=|)JMRzKdUr(vaph|1e&*(ZvRYh{o-r z9BRK%zey0iR&vAy{P@CzNO)c@ID9Wc-c8sdKF@ogaJR@IUIE_Ycm&>KK2InH6Lv%e zW862IzFBA>#X=NgS6~BObZ{cJ|9tO2zy@aMt3$m!dgsr{uE1Bben9`Of!sF*yu%<_ zyyf_WKH~5->kAhoIrflUuWO}p`k%p2CYE^ zAFA!X{zPP{!_&aqHrIHXPxa-fVj7J?##5hjTg3q?Ze3IzAGO>(q)KTwI3iyQBP1P} z^)s&tA(ahM4;-^li1@s|K)@hi|9=#}9`NlXFvKjIFS>&76-pcutRpNLo`-g-EX>c> z-jkRvwq`1YsC$a_Q&zTzB|fO&A67peAZr)2n|}k`f^$?n%6h#!kq$nT7G*#a%^(AY zz_Kch#lS!Hl_!aXEtseKrC&a3m^w*UkRZm+xHq)qV-T=RmD|{1g7xxvvoAr(WzoT9 z^+T;y6|pbg+*C2~{Aeq^wbd$x|HtydAF&|*=V8(?-j~+(p%_#?7)@1Zd?{kAOU1?DB}9c zk!`^2Vc~h;YviT9fx4gz z-t$~DVlX8nL}R)w790xS7YO9PircYYp1n8Q*k|(NHXuA_k680LlDi4q$(DHk^FjYx z?9LF?x(Q81WvnPlqHc(ALyW>Ijn;^>5Lmx||09z9D|l;YV2;`)A)>3w2vesR%2V0) zq`HjJag;LRu`(E!#-Qo(Tu9upF(Os-wx27b9N`#GGzOs6@aBO2-+lbAcmMB;-RbzypRuLQB*uBke#D(0cGelHFXNLoPQupMj`chg%1juM zV6Z6AT*qrzC2vF(eM**bDbO-)-!OCcpfcxep~MIcTqPB(4D-6I8 zO(hS}g^}Mp+>R9>BNLb%B3~F?nJ12%S+^Cle^W@YU(AWpCVZhXKkp?9BeBB@{| z8KY?!hU98KcS$i9^AVZMn-;?z>JPO0K@n=TQmawNNzq_b#tBc$d2E9U>4 zUcSAM$hGj{e06GnG7sKt%4{B&TEZCsPhMr0iz)$aIY}`A8~CIu(cbd0-cZH#c5fg@9u7`{QiLL%Bx??C<(SxX52rA)KoI-)1IJ>Ut{qb{h`PxU^%H42l z0pg&U1+$Z(o}YwOsC(LRqD|XJ-Por(OusFU&@Fk3P=i`5vJn?aG^*rFX;E^_Phmwy z)CF&i$HXyYk+yYFS!vpQqpe^hSCeT>nT@dQS!Yo_WsJsaQ@RG@{6#lqOvnH2==~gp zZ>>9A?7NsMi{__A;pZE4X)Hb8T!M_tM?Z;TAx2Ih zj%hVZnv}$OgCWY&BBr^p)*O%2k}A&eW`6tpkwW`1C>mv%liLX#S~Mb*Qy`&!hs;8vEmL~@+2!0QOhb5_>p;fVitTG%o3Q4p;*fsSl7rsb zf)SxpR8f;rng;3Qc zL$^WGjC1>SPX6=QL5g(wbJ-nC$ul`m#^wCob$vDk{hcTDjjX2|xhh9MEa#?cz z_|Y1+=+X}T9A%-YS-Zjw%?augKq!HsXU_4uBLG@ML|HzcVfYOMxr;xUjN08fV~)!Z z>_4V*cOI>E{JqP+3jFRfzzqjEXEZD215_gINVd9Y;6Cx&iK}`iULlE*?Zah_*0?77 zmh$02t)rpYc*&$qDnfFhiSLT<=_4mH&T~QGX5^g5n3)O-V`aGMCyyIzRx?HP1Fy^8 zp|LH+A&q30jd4gw>*=L$w7=vZ9EaFHQ*;nc%JipWQAW~kh>plL9VfjxJw7TBW zmmmAsa1Vtz<1)L9*~J?k#ghD!rI#OyGW6~U_N%T^37Ng$|Bnaz@4dkP@^7Lrljv^W zCc^^FKPBA{v@%i^$&-Ppe>Jwu602?#V=NphIhTqRd8e)7sS$|})ytK9;|$$c7Z(v_ zr6!$T7aH8Xz0dJ-QVK})oKGCbg27?q78vx7C8&ek*3*+QRis2zk-G*?-Y%}+SMmTd zUe8t*b6^8jTnrxmhzO%}U{<|G&bUu+ok#h2sQrfWfXA|cWjz;(hwZ_K_6cJ<%%w>SE zSD4|0yUEhVR?38NZt;ctm9<5hvPx0S1Zo7rM}MEG0v!!nM;5M^+$(`a&6_QLT%?FT zR(REPdBSz8NX@-^75voMmCwS7MYL7;;JH_vYN=e8X^hAVD<1pa;`?KDMvF&#IfaS} z{F9LLIz+dFejC9vN`?KhP1tgUJv>)2%t=>CyJxqm<1!JBlGNC%hmXsH;TCH3yo`D? z4TWV*_`65LRuFF2tlCrZcY57KDLb~QrKxEobjEQd`<Ob26(HjYkjCCFe>;JSHb?W1S}AI9Qfd6~X0^uRCgI zm(3l-%|-Mc4n3V^bcE_(!vJBBK0&wA%GXjaDZ1=XreUOK@e>r#=6BU0-jw36h_@;= zV?7#c&pt98^=<9nYCqN~h%?4;S*)D|h_}S!C&^Pjjk20cIv4dz7!6dAMsm_M&Nhyy z+r;bb8XfK$F&G=yOf5-?*RTHD&H0O)?J^#FEhJZHRu&RZxEHt*C2y})R$CR;8Vl}~ zh8teP&BvWqOYaHG)0@$k&EO4qnwPFm6K)&zKI>|)z1zkQb7fXI9GvTNRrjTGev(}Q(9G+mBgnm&bQY@)AA zxtEod%(rc5q(Ysj72KC`FuR{tqtCTBN-Uh)xIAjMl(#%YZ5UIYJw;Sil+|!`8gwps z(pQ{q%C?iKm~ARps&D^DA-8|{RcCRy1Xp2E$9OI=cp9xt?@>9FB`mj%k3mt z{`xEUD$B5bcvFT+2Gi~UKeM&*9`Aj^Z4iACy)XSRu@ta>blxIl>hly!vR=NkCC61d z<80#6(yi!u!d*A5wHn9eX=JyP9?#6;Qf8vw`XMU$^_Q%s*mdhYJ>JQ}-15S@lDz8e zGm_&z<_+`H1=8!Ouhd6E_Rs~e3=j!m6S@CE6chfO8|CV2rQMAYMvi9F$uhR8ya@xm z@=w+A>i61Du5-vKn{@41)vs4x|Jv%ztKPNenpsb~QBOSi>eO3kkDL~9)JsN72DUKq zo6tOZZeM9e3o+@uiOtalc6WD{$$HXJ*M7$4nlbVo^72}lH~#^v;P#T?G;2b4a&x<0 z%j0(m+Gsv0H%&HPL{VwQ>DC(F&(Hlmt-)1$U5%+=Il_Bc=C{fST89Te=*_-6-RvUm zaB|+kQkiXRcfw&Ma89A9pniG9%;r-`se&vtlKCHd;$|I9n+vswVop0^qv${Z%}F9YmuJ7rb^Ek9>UbaPyiMo+e->|_y zE%nMHJ(*^cV5=FOFU4kr_r=357B{QqL&^rnnE}Wq(qhgEi(pFQ%tnms%*CX)vzjZb zmuhNP%aYls^{`_7tFWw@i}gr@=k?FPg%9UbRa5&kxni!nVQnpscAJ`^nhyRWpPiRZ z+iUi>D{_yE&gfXcF}kCCLm%~BgAGq)+TM;@++A@&+Z zMQn%cPfZcYM=y`0%;(@mr=<;Am#I9hmXEPyGz*iBeg_UqKnjL?hl7yMQ7;N^#K1|q zegkfM{iRB~@z;wy*X8_x4;CEE*0bplpN7koBtd%jQoRh|B3f!J$}zBd%%8 zb@_cfk@RM##NwGoq~!IT=SN@!j*=ocrvdBA<FVYV?aC$eLiv01 zTZYod%BQ=i*5qw`d=`huEW_b035iTGgJYe1#F>0sM(#wvpf;9RvzZ3$lXsjN}G%(#l_Qlne6OA`C@>|_XY^}e4G-pS~AC3Tgp z5!k#o8`+8NRnofIPO4IKH47mF_i`4Thf_^Ohs#9$Z-)ehRjX?)Z^a;54|>_yX1rp- zLcl4nmJcAri3%3HL?XWqO^}FryXEf^+hpw8J*~7c5W(9ojcPgB;Zg zEbE7H6Kv%jYk|e{p_T7hD6Hp%i*lTf{$VLnrw9&-nywd+J)N-MchOH^)LiMYsi4OK2OXNjb)UoB%9{h4cjb5>~ zLcVC`aH)E@;Zp4KCja^h@u~`SP8^pTQq)SDaA72|kOMm_?;{7eFc+Jjj1MOi7K(&o zelaEXhm?4Q?362`@W}m2r^pZayD7=F>{&@}12@(*OYzYN z^}n;|8|aawOLTHHH|dvoqD-l=4@X-={_ndWC;n~^AP+TKt=`_b`I=K&@;j~Ms8)|F zwA)CMuaRjd(`EFiQG@=@Ui(#LUsy!3fWDF<>U=`xEL1Yiiry5#2Bw$^nq>1KW|EgD z$FCvy5QA+em})g09v)k(-FBt#SRP#R7XY)?NjBhSJt+E3$szM0nL0y3PR;8o^WS;m zB8O@_stVOz?bh0HHl1y#`2Tl}+kC7uDu{4N{Jv%AfX6K&C*1qOGgL-2_1q-XrV0@5G>dLDZy0~qy<4jFA7##L?Dz9 zSQSLNfYb>2LTG`F?R7ui=iP_=aqrBTb7syx^P4j>XW-rkUE@cho~z4k0A86;KC~Z9 z#sxE5CHhE`SlW;2b?LR%-25d0qNKPqY!R?Qm-;5qyHgbH$$siT>?cs8r|A?*4W%P_ ztN%5hc=E^<+kCKCmUkqdgHcjPelh4S=Ls3<<2e2sTKl2GLp_hEUq-qWyV>cg_OHN* zlXl5xwB6NMhwk1kej2;^lYj~K`H_TJIe!l{cVq^May^FN#BJ}hgP41BvssKlp3q%; zSklH$4e)r8ph1-VvB%upe;EcRw|7WD1h#M6qslex^lJZAvGo`1oUe4>c{}!r%fb(R zEx0Y33sJO*edT znb>qI?_*meH9nz~FUWB|P$@XM&=x39{Ai4ExN_bg6SEt#%RWmeP%YNh7Cm+hpSxL& z`=;^D5ZuP+hblzxszq`mMo4zL55O_6-v6x4uIYB*aA*?sV-%X!I2&Aws2^)(;u{%p ze5jF%;B}6!0*>BS?UgaD>i*mbs-w@sAUC=~Ws0A@gYc-i z^!Q+65?Lc(+W{ROw_Pn14F;o&l9KP|ZNHP|W-EiN+kNe6>VQIb2ATwIyc=ghi=iZq z##cCktHQL}4GsZ8ZqCw$;fWHp>Cbn?^kNRD#yQx>EBhDp-NS)})6SH)Nnx!#Pbd1t zYb84~vwOaMqLK(SPt#7*<1$ZZL-(t{?YfS((Bd;F;cC3A?wKW=+&aem!rj*()yOCo zF@C=2Ucu%>@e@6m%&`km97k<$O4YpTzticJKsAL!N_>K0^LNP|?5ooCxGCs81>J=NUr@>|z1vtR;W*mZ{oI&0S0m_mH~ z^V1zJo-Z6$Yr1&7KOl3DfTP%aBz5fC`ZklI#MZbVBqAw;y>Sf_NAyD=n zq%>kS7jY6J|6=V0V|T?e=l|lrsuZ|)NpPA`*0p+r&mSHBucqEU^MxchNGQ0m*pNe$ z%=Udt|1tLci*@{l0K^K*im_Kfvoe^zFjK4;XT>-x)5J1P6Jx)virQ*o>m9jQ85?84 zpD&P6W0t{pO+YTklPt^vuNIJ6bnZ=ET5uXal;&Ui#76f+fW^ZQ#*0}-E07i1KWHnK zGk@?ZhYOu!yCaRXY39u|lfp`yS0Z1;YBY8}dO~dV9@vSw%KZ%{7uhdjt+O$t@-{%+ z+L)a!gA!rT@Z3&9ny-EsE zPCC1 zH<=Ilz7XlEU_$lY$46{*(F)1NV3|ny3F`)Ubcs_HeLg`1OVRV;euZ!Btkg>-|IWo4`dD(u zaf|^U>0~IR9cRiORYRo27=Nu1EbV$NxaAtHoJQ>)5%=F z0m3{yYLmY~4t?uEm++*7b%d_lp_dIWU+!X(tTb_ZQyn7~4OE?AX`C1?Nc0LaYSuyO zxz%lYz|n>HZQ!bWs$q6#G^War)D3iR95icL718o`u&7T=+o`_bc2!E}vtf_&kt4?= zx>a#ZLzt#Nibizd!H2BiT913HpQFhYzHY}8+@p%f*C-W*sV9QTS8n&ajmUk%j2yGv z<1`*6RZqN{Ou-fThRezq<}wlMbepi*??<(gdW~)6?YasoiX7CrSe@>tol6NNzdhd5 zms7kqCsEYTqogQBl*5eq0{p@+r-BD{Oa^qxQ72@#*olAm5&+aBvkV=7 zP*GymJa$UAD6^oDD`ScT+#6fDTl%D@65ucO>v-X4iruZ8q+$XrS=`2CbJ^_}NN>w~ zp#t?xw`}*}EDrH3lD&(d0BK-Np|eV2uG`I#61Zj)sEH8XB}b48*BTl)PRuhRWyKF0 zzsMqAaIW+sCJpnYVshCVoj2Hka~cM^B8IYHl*|Vo0tzeN#pw~H-90noOxrD5WDACW z7ptFmyE;{A{fS2(Q+M-O_Qq19oujD&Cf3LZbo|-;(8qy6K`IcI1A%0$ zXF4>_>e0qIBr(>%d7|sIE+zcW=lw1z@Jnlvm)aeBI(K| z<5nj4hqF=E0gwM^zk&0(ol=D^CPHy0B<4IXCG$t^=vbV1D~=ye_9~5cW5tBNFA)L> zttM2hipVYNbxU&wZN_`H?gbaF`uMRYR=T@5v!F)`9b5h>ggk-0TQ{}N(03x%^Ob{W z*Wh6D1EsTl>$9El2Gv59_XM%7GCuhio~S5r$7G{Vkjy3G8!iPj=&eq-U26qqJ2#1X zh;iC!DPJ#lDT1s_VVxQ!c$BTtf1I#M4vhP=kHdZQ5%~O0qKqb|tS5 z{8U+e$2zYlz6Ryts$LqHlje5~%n{Xb{?~)bh(3_ij!~Xd0e6Pmw+}>Oe-~q8d33u- zEy#D^={x8?-Yg)C_$2o3mA(_Q9F9Ar4*MCM25)r_XQwDNTX0%Hvq3pFE>BK{cnwCO zpW2JJ+^^zeOUuIf7N#1C3;^|zVDvNlev+Q0prL+eNO!na->m3Otuj8ahaAW z1i5=s8}E}|)xDAVSqvYzejAf-D}*PZaIc&B@JP8i(yqSOFQffQQ)AYL=FP-PSYS(^ zS+#|f>)B^-KIFzXqy%Aw$YEF$N}>Li{Er`650@%t%bA{hNidhuI~Nbf_0&}6Srblr zSvs}$#B|nbL7l~7aYbV7FUX^vQHABz0!34J%FC0bAtA3jdJdD>!LhEB#Ppu2yj1Td z96q?kt~P(u%%!-`(>dyeQs@7RprV2IIzL-#Aq`+YP=tbfHH@*8E# zQ}=Ajlam6wqqIhx^0h~M>?E54O)Y96WBo#*gnoN+cASlN_lCR#w_A4dw>q$>E;(~& zviBzyGW(mnFmN`drp$tP6E4tWQZy)$|4gFht)xdK$-6_o5H;SG8#|fSV1e!Py{`>! zP79u_dv+G$m(yFMfWJgWpWocz-(m0jA_v*}A;G|EIBZ( zK;yk&x3J7;f7W^KAda$4+R0vpHm{x8M^;aqf)@^mcMI+$+mNl{J2$$S!M30EWp7l> zFB*&I>fdPYt(V`i^Om#Z0dA0Ls{k;&??s)XOy(oKQr|pFyCA_07kHO;eI~CUh7P@Q{)L=jLsd8z?OB!_#JO( z-#^j#(I}|AUe!v~R)Xjqd+toDZ zrJEVy;nXdjgHq@Xe7QU2(SvF=Ev;qwsnvf~;1csiyb`$e8cq7&?QSEP9cyW=E7#h@ z5}f~pML8@-L2FIBIV7xLb@M)}!Im^poS_cZB+T?6{}9`k&xA^j8i?KfHEvP>f4(j; z7{#g%KE?`XvNSRDQe$#yH1bHXZH@?m+mwx*u^D77FMR>kQ-suV1w3tddO|iaFc{%g z58a}2`_Y*gP(N^I?XN6{SSq4fZus|>3=gelZg=RrI`LF9-7c=!Zy@|ciEV|fht|b( zyL~$|yP4pc{XP5vAsOs9v(AOoLQVhtkh02Rd*v%xVEzqR;I>Avo_~7j>l+@VQ;vjs z{1&r4IWA$~wouiwNlnfA+>Xk8_gmI}b!;64u=02M`i2%V35Z1N&KO6w^8Gt=Sb!;r zs)&Q9J`+03f&Ma>!RN=5{bX>WVU!Y4flcTB2Uor;RWbUL$`#tuztH}Mz0V5E%3xX< zI4c8ZX@u)vF)BW!ubBcnh-UH1f<2v`0PZjZ(jM+2$0Z6B;R3*1tl?057h5?l1O3Co zhq%D<>t**UYb)uS=%@f(o$Qe)fR2HphME$9OBe)FaCLQpfk4Xo$^ab=H9dU*wGI$S zMT-l-Wrsq!N`pWi9v(o;X)d5O+Ur*V5>$?{8D;WfW~iYt*_^#{3UK`*{r}@+O(qXbo)p;CFYYf zYt5^qrFme!nsn@89C0+;G7OM--M*)#kh_o z0%eb)&W(U+C%qW!`j%EsFse@%`v;&t?Yd@u3n^-T7ACVkeNoXI|Kj^1s2@FKtOxyy z&<_D|aoeBC{ZI=FP*-I*1Ja(3VG9UGK3!=>M}`7)>VJ<<{|=Wv4fX#3P|JsS06%Rt zXK@$-n+CS{j{{q60e-4<23`Ve5Beg;>Dexu@!^bF7E{00$BVhCx#bm@LE^M^7z3MT ztp%0T=@uspeYgqjc@_lz_INbH_W)b$+t(^z#?A8K7iy1m1W*?vwy-&kjvUsb0{Os? z16h5}po&@x(rD1*efl?{r}djtN%64q{L&mP!C=@cO2KK*Gbf;M^efh(aWAw$$1OpZp5vn4r>WEG&a|N9 zI9H_MJeN{{&FDbQYiWzjQk9`J0XR)(TIadW@vF@7Pzj3jjVig%Kf??oZNwj*POHx? zKNYo9XUe9pD)UyF&gji6rl6!jW16mcL0$c0R88nxn8Ti;is!tE6%>~Eig^JQk$FXF zl{2_}kFOB_c3<(0V|4kP#+<8|o%AGr*jMP&Ey|D38+O+Ev%X>u1oRwB(z62p!bxud z$n=j)v(_9&6;;r5CRzgI8L1gV&k4}iM4$d1ARqX4Ab%M*%ZFb^t4c*nYezR%+Kzl~ zmm>M&xP~q`>_y&VF|HRC(f7kbdJ(SwRl5|%xTcw84yKAKqVw~M!gB3WB)%Qj{~4Z^ z7w>bSCpw(ZlB59#e;nve3qYTnR%zf}4DT6o z0t?%5#z0>jc~a2QoY!05eR(2JlHUjP?*P&Q!ev48hYGYyO4 z+;2!MFHXKep~b7$6@kxLV5Q?2TZS4Aq+1@YSc$Qf7E@QOwAdbubN>rgqUlJTk>*c| zKY!@>9cM9%=1xZQ-5hP