<commit_before><?hh

require_once ('aws.hh');
require_once ('api/reference.hh');
require_once ('mimes.hh');

require_once ('api/validation.hh');
require_once ('api/router.hh');

class KeygenException extends Exception {
  protected $t;

  public function __construct(int $tries) {
    parent::__construct();
    $this->t = $tries;
  }

  public function __toString() {
    return 'Couldn\'t generate a key in '.$this->t.'tries';
  }
}

class jump_api {

  // basic URL scanning
  public static function filter_url(string $url): bool {

    $banned_domains = ['lovlytime\.club'];

    $banned_terms_regex = ['jenniferhot', 'fooBar'];

    $input = parse_url($url);

    return
      (preg_match(
         '/$'.implode('|', $banned_domains).'^/',
         $input['host'],
       ) ===
       1) ||
      (!empty($banned_terms_regex) &&
       preg_match('/'.implode('|', $banned_terms_regex).'/', $url) === 1);
  }

  public static function jump_key_exists(string $key): bool {
    $dyclient = awsHelper::dydb_client();

    $res = $dyclient->query(
      [
        'TableName' => aws_config::LINK_TABLE,
        'KeyConditions' => [
          'Object ID' => [
            'AttributeValueList' => [['S' => $key]],
            'ComparisonOperator' => 'EQ',
          ],
        ],
      ],
    );

    return $res['Count'] > 0;
  }

  private static function gen_uniq_key(int $maxtries): string {
    $tries = 0;
    $new_key = '';

    do {
      $tries++;
      if ($tries > $maxtries) {
        throw new KeygenException($maxtries);
      }
      $new_key = key_config::generate_key();
    } while (self::jump_key_exists($new_key));

    return $new_key;
  }

  public static function get_mime(string $extension): string {
    return
      mimes::$mime_types[strtolower(substr($extension, 1))] ?: "application/octet-stream";
  }

  public static $doc;
  public static $cache;

  public function init(): void {
    self::$doc = api_config::api_methods();
    self::$cache = new Memcached();
    if (!self::$cache->addServer('localhost', 11211)) {
      self::$cache = NULL;
    }

  }

  public static function error(string $message, int $code = 500): array {
    return ['success' => false, 'message' => $message, 'code' => $code];
  }

  public static function success(array $data, int $code = 400): array {
    return array_merge(['success' => true, 'code' => $code], $data);
  }

  public static function genUploadURL(array $input): array {

    try {
      $input = api_validator::validate($input);
    } catch (ValidationException $ve) {
      return self::error((string) $ve, 400);
    }

    $limit = jump_config::MAX_FILE_SIZE;
    if (isset($input['promo-code'])) {
      $balance = self::getBalance(
        ['action' => 'getBalance', 'promo-code' => $input['promo-code']],
      );
      if (!$balance['success']) {
        return self::error('Invalid promo code', 400);
      } else {
        if ($balance['large-files'] > 0) {
          $limit = jump_config::PROMO_MAX_FILE_SIZE;
        }
      }
    }

    $private = $input['private'];
    $s3client = awsHelper::s3_URL_client();

    $tmp_id = uniqid('gu-', true); // is this unique enough for a temporary ID shared between (potentially) multiple servers?

    $bucket = ($private ? aws_config::PRIV_BUCKET : aws_config::PUB_BUCKET);

    $expires = (new DateTime())->modify("+5 minutes");

    try {

      $command = $s3client->getCommand(
        'PutObject',
        [
          'ACL' => 'private',
          'Body' => '',
          'Bucket' => $bucket,
          'ContentType' => 'application/octet-stream',
          'Key' => "tmp/".$tmp_id,
        ],
      );

      try {
        return [
          'success' => true,
          'URL' =>
            (string) $s3client->createPresignedRequest(
              $command,
              '+5 minutes',
            )->getURI(),
          'expires' => $expires->format(DateTime::ATOM),
          'tmp-key' => $tmp_id,
          'max-length' => $limit,
          'content-type' => 'application/octet-stream',
          'http-method' => 'PUT',
        ];
      } catch (Aws\Common\Exception\InvalidArgumentException $iae) {
        error_log($iae);
        return error('Failed to generate signed URL');
      }

    } catch (Guzzle\Common\Exception\InvalidArgumentException $iae) {
      error_log($iae);
      return error("Failed to generate command");
    }

  }

  public static function genFileURL(array $input): array {
    try {
      $input = api_validator::validate($input);
    } catch (ValidationException $ve) {
      return self::error((string) $ve, 400);
    }

    $s3client = awsHelper::s3_client();
    $dyclient = awsHelper::dydb_client();
    $glclient = awsHelper::gl_client();

    $dest_bucket =
      $input['private'] ? aws_config::PRIV_BUCKET : aws_config::PUB_BUCKET;
    $result = NULL;
    $tmp_file = NULL;
    $extension = $input['extension'] ?: '.txt';

    $size_limit = jump_config::MAX_FILE_SIZE;
    $local_limit = jump_config::MAX_LOCAL_FILE_SIZE;
    $used_promo_size = false;
    $used_promo_urls = false;

    $balance = ['success' => false, 'custom-urls' => 0, 'large-files' => 0];

    if (isset($input['promo-code'])) {
      $balance = self::getBalance(
        ['action' => 'getBalance', 'promo-code' => $input['promo-code']],
      );
      if (!$balance['success']) {
        return self::error("Invalid promo code", 400);
      } else if ($balance['large-files'] > 0) {
        $size_limit = jump_config::PROMO_MAX_FILE_SIZE;
        $local_limit = jump_config::PROMO_MAX_LOCAL_FILE_SIZE;
      }
    }

    $content_type =
      empty($input['content-type'])
        ? self::get_mime($extension)
        : $input['content-type'];

    if (isset($input['tmp-key'])) {
      // user already uploaded the file to S3
      $tmp_file = $input['tmp-key'];
    } else {

      if (isset($input['local-file'])) {

        //$tmp_file = uniqid('lf-', true);

        /* the API regex for this param should check this, but I don't want to take any chances */
        if (strpos($input['local-file'], '/') !== false) {
          return self::error(
            'Invalid local-file parameter, shenanigans detected',
            400,
          );
        }

        $tmp_file = $input['local-file'];

        if (!file_exists(jump_config::UPLOAD_DIR.'/'.$input['local-file'])) {
          return
            self::error('Invalid local-file parameter -- file not found');
        } else if (filesize(
                     jump_config::UPLOAD_DIR.'/'.$input['local-file'],
                   ) >
                   $local_limit) {
          return self::error('File too large', 400);
        }

      } else if (isset($input['file-data'])) {
        $body = base64_decode($input['file-data'], true);

        if (!$body) {
          return self::error(
            'file-data parameter invalid, should be base64 encoded',
            400,
          );
        }

        $tmp_file = uniqid('fd-', true);

        file_put_contents(jump_config::UPLOAD_DIR.'/'.$tmp_file, $body);

      } else {
        return self::error("Constraint verification failed", 400);
      }

      $file = jump_config::UPLOAD_DIR.'/'.$tmp_file;
      $size = filesize($file);

      if (!file_exists($file)) {
        return self::error('Problem with local file', 500);
      } else if ($size > jump_config::MAX_LOCAL_FILE_SIZE) {
        if ($size <= $local_limit) {
          $used_promo_size = TRUE;
        } else {
          unlink($file);
          return self::error('File too large', 400);
        }
      }

      try {
        $result = $s3client->putObject(
          [
            'Bucket' => $dest_bucket,
            'ACL' => 'private',
            'Key' => 'tmp/'.$tmp_file,
            'SourceFile' => $file,
          ],
        );
      } catch (S3Exception $s3e) {
        error_log('S3 file upload failed: '.(string) $s3e);
        return self::error('failed to upload file to S3', 500);
      }

      unlink($file);
    }

    try {
      $result = $s3client->headObject(
        ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
      );
    } catch (S3Exception $s3e) {
      return
        self::error('File tmp/'.$tmp_file.' not found in S3 bucket', 412);
    } catch (AccessDeniedException $ade) {
      return self::error('Internal exception', 503);
    }

    if ($result['ContentLength'] > jump_config::MAX_FILE_SIZE) {
      if ($result['ContentLength'] <= $size_limit) {
        $used_promo_size = TRUE;
      } else {
        $s3client->deleteObject(
          ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
        );
        return self::error('File too large', 400);
      }
    }

    if (isset($input['custom-url'])) {
      if (!isset($input['promo-code'])) {
        $s3client->deleteObject(
          ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
        );
        return self::error('A custom URL requires a promo code', 403);
      } else if (!$balance['success']) {
        $s3client->deleteObject(
          ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
        );
        return self::error('Invalid promo code', 400);
      } else if ($balance['custom-urls'] <= 0) {
        $s3client->deleteObject(
          ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
        );
        return
          self::error('Promo code has no remaining custom-url credits', 402);
      } else {
        if (self::jump_key_exists($input['custom-url'])) {
          $s3client->deleteObject(
            ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
          );
          return self::error('Key requested is not available', 409);
        } else {
          $new_key = $input['custom-url'];
          $used_promo_url = TRUE;
        }
      }
    } else {

      $new_key = '';
      try {
        $new_key = self::gen_uniq_key(3);
      } catch (KeygenException $ke) {
        error_log((string) $ke);
        return self::error((string) $ke, 503);
      }

    }

    // copy the temporary file to its new home
    try {
      $result = $s3client->copyObject(
        [
          'ACL' => $input['private'] ? 'private' : 'public-read',
          'Bucket' => $dest_bucket,
          'CopySource' => urlencode($dest_bucket.'/tmp/'.$tmp_file),
          'ContentType' => $content_type,
          'Key' => $new_key.$extension,
          'StorageClass' => 'REDUCED_REDUNDANCY',
          //'Metadata' => ['IP' => $_SERVER['REMOTE_ADDR']],
          'MetadataDirective' => 'REPLACE',
        ],
      );

    } catch (S3Exception $s3e) {
      error_log('S3 copyObject failed: '.(string) $s3e);
      return self::error('Failed to copy S3 object', 500);
    } catch (AccessDeniedException $ade) {
      return self::error('Internal exception', 503);
    }

    $pass = '<PASSWORD>';
    $salt = '';

    if (!empty($input['password'])) {
      $salt = uniqid('', true);
      $pass = hash('sha256', $input['password'].$salt);
    }

    try {
      $res = $dyclient->putItem(
        [
          'TableName' => aws_config::LINK_TABLE,
          'Item' => array_merge(
            [
              'Object ID' => ['S' => $new_key],
              'pass' => ['S' => $pass],
              'private_b' => ['BOOL' => $input['private']],
              'active_b' => ['BOOL' => true],
              'file_b' => ['BOOL' => true],
              'time' => ['S' => date(DateTime::W3C)],
              'filename' => ['S' => $new_key.$extension],
              'ext' => ['S' => $extension],
              'clicks' => ['N' => strval($input['clicks'] ?: 0)],
              'IP' => ['S' => $_SERVER['REMOTE_ADDR']],
            ],
            (!empty($salt) ? ['salt' => ['S' => $salt]] : []),
          ),
        ],
      );
    } catch (DynamoDbException $dydbe) {
      error_log('DynamoDb died: '.(string) $dydbe);
      return self::error('Failed to create new URL', 503);
    }
    $s3client->deleteObject(['Bucket' => $dest_bucket, 'Key' => $tmp_file]);

    $cdn_host = jump_config::cdn_host();
    $base = jump_config::base_url();

    if ($used_promo_size || $used_promo_urls) {

      try {
        $dyclient->updateItem(
          [
            'TableName' => aws_config::PROMO_TABLE,
            'Key' => ['code' => ['S' => $input['promo-code']]],
            'ExpressionAttributeNames' => [
              '#cu' => 'custom-urls',
              '#lf' => 'large-files',
            ],
            'ExpressionAttributeValues' => [
              ':val1' => [
                'N' => strval(
                  $balance['custom-urls'] - ((int) $used_promo_urls),
                ),
              ],
              ':val2' => [
                'N' => strval(
                  $balance['large-files'] - ((int) $used_promo_size),
                ),
              ],
            ],
            'UpdateExpression' => 'SET #cu = :val1, #lf = :val2',
          ],
        );
      } catch (DynamoDbException $dbe) {
        error_log("Failed to decrement promo code ".$input['promo-code']);
      }
    }

    $ret = [];

    if ($input['private']) {
      $ret = ['url' => $base.$new_key];
    } else {
      $ret = [
        'url' => $base.$new_key,
        'cdn-url' => jump_config::FILE_HOST.$new_key.$extension,
      ];
    }

    if (!$input['private'] && self::$cache !== NULL) {
      //self::$cache->add($new_key, jump_config::FILE_HOST.$new_key.$extension);
      self::$cache->add($new_key, $ret);
    }

    return self::success($ret);
    /*  array_merge(
     ['url' => $base.$new_key],
     $input['private']
     ? []
     : ['cdn-url' => jump_config::FILE_HOST.$new_key.$extension],
     ),
     );*/
  }

  public static function genURL($input): array {

    try {
      $input = api_validator::validate($input);
    } catch (ValidationException $ve) {
      return self::error((string) $ve, 400);
    }

    if (!filter_var($input['input-url'], FILTER_VALIDATE_URL)) {
      return self::error("Invalid URL detected.", 400);
    }

    //        $s3client = awsHelper::s3_client();
    $dyclient = awsHelper::dydb_client();
    //        $glclient = awsHelper::gl_client();<commit_msg>Tried to be too clever with caching, now only jumpTo generates a new cache entry<commit_after><?hh

require_once ('aws.hh');
require_once ('api/reference.hh');
require_once ('mimes.hh');

require_once ('api/validation.hh');
require_once ('api/router.hh');

class KeygenException extends Exception {
  protected $t;

  public function __construct(int $tries) {
    parent::__construct();
    $this->t = $tries;
  }

  public function __toString() {
    return 'Couldn\'t generate a key in '.$this->t.'tries';
  }
}

class jump_api {

  // basic URL scanning
  public static function filter_url(string $url): bool {

    $banned_domains = ['lovlytime\.club'];

    $banned_terms_regex = ['jenniferhot', 'fooBar'];

    $input = parse_url($url);

    return
      (preg_match(
         '/$'.implode('|', $banned_domains).'^/',
         $input['host'],
       ) ===
       1) ||
      (!empty($banned_terms_regex) &&
       preg_match('/'.implode('|', $banned_terms_regex).'/', $url) === 1);
  }

  public static function jump_key_exists(string $key): bool {
    $dyclient = awsHelper::dydb_client();

    $res = $dyclient->query(
      [
        'TableName' => aws_config::LINK_TABLE,
        'KeyConditions' => [
          'Object ID' => [
            'AttributeValueList' => [['S' => $key]],
            'ComparisonOperator' => 'EQ',
          ],
        ],
      ],
    );

    return $res['Count'] > 0;
  }

  private static function gen_uniq_key(int $maxtries): string {
    $tries = 0;
    $new_key = '';

    do {
      $tries++;
      if ($tries > $maxtries) {
        throw new KeygenException($maxtries);
      }
      $new_key = key_config::generate_key();
    } while (self::jump_key_exists($new_key));

    return $new_key;
  }

  public static function get_mime(string $extension): string {
    return
      mimes::$mime_types[strtolower(substr($extension, 1))] ?: "application/octet-stream";
  }

  public static $doc;
  public static $cache;

  public function init(): void {
    self::$doc = api_config::api_methods();
    self::$cache = new Memcached();
    if (!self::$cache->addServer('localhost', 11211)) {
      self::$cache = NULL;
    }

  }

  public static function error(string $message, int $code = 500): array {
    return ['success' => false, 'message' => $message, 'code' => $code];
  }

  public static function success(array $data, int $code = 400): array {
    return array_merge(['success' => true, 'code' => $code], $data);
  }

  public static function genUploadURL(array $input): array {

    try {
      $input = api_validator::validate($input);
    } catch (ValidationException $ve) {
      return self::error((string) $ve, 400);
    }

    $limit = jump_config::MAX_FILE_SIZE;
    if (isset($input['promo-code'])) {
      $balance = self::getBalance(
        ['action' => 'getBalance', 'promo-code' => $input['promo-code']],
      );
      if (!$balance['success']) {
        return self::error('Invalid promo code', 400);
      } else {
        if ($balance['large-files'] > 0) {
          $limit = jump_config::PROMO_MAX_FILE_SIZE;
        }
      }
    }

    $private = $input['private'];
    $s3client = awsHelper::s3_URL_client();

    $tmp_id = uniqid('gu-', true); // is this unique enough for a temporary ID shared between (potentially) multiple servers?

    $bucket = ($private ? aws_config::PRIV_BUCKET : aws_config::PUB_BUCKET);

    $expires = (new DateTime())->modify("+5 minutes");

    try {

      $command = $s3client->getCommand(
        'PutObject',
        [
          'ACL' => 'private',
          'Body' => '',
          'Bucket' => $bucket,
          'ContentType' => 'application/octet-stream',
          'Key' => "tmp/".$tmp_id,
        ],
      );

      try {
        return [
          'success' => true,
          'URL' =>
            (string) $s3client->createPresignedRequest(
              $command,
              '+5 minutes',
            )->getURI(),
          'expires' => $expires->format(DateTime::ATOM),
          'tmp-key' => $tmp_id,
          'max-length' => $limit,
          'content-type' => 'application/octet-stream',
          'http-method' => 'PUT',
        ];
      } catch (Aws\Common\Exception\InvalidArgumentException $iae) {
        error_log($iae);
        return error('Failed to generate signed URL');
      }

    } catch (Guzzle\Common\Exception\InvalidArgumentException $iae) {
      error_log($iae);
      return error("Failed to generate command");
    }

  }

  public static function genFileURL(array $input): array {
    try {
      $input = api_validator::validate($input);
    } catch (ValidationException $ve) {
      return self::error((string) $ve, 400);
    }

    $s3client = awsHelper::s3_client();
    $dyclient = awsHelper::dydb_client();
    $glclient = awsHelper::gl_client();

    $dest_bucket =
      $input['private'] ? aws_config::PRIV_BUCKET : aws_config::PUB_BUCKET;
    $result = NULL;
    $tmp_file = NULL;
    $extension = $input['extension'] ?: '.txt';

    $size_limit = jump_config::MAX_FILE_SIZE;
    $local_limit = jump_config::MAX_LOCAL_FILE_SIZE;
    $used_promo_size = false;
    $used_promo_urls = false;

    $balance = ['success' => false, 'custom-urls' => 0, 'large-files' => 0];

    if (isset($input['promo-code'])) {
      $balance = self::getBalance(
        ['action' => 'getBalance', 'promo-code' => $input['promo-code']],
      );
      if (!$balance['success']) {
        return self::error("Invalid promo code", 400);
      } else if ($balance['large-files'] > 0) {
        $size_limit = jump_config::PROMO_MAX_FILE_SIZE;
        $local_limit = jump_config::PROMO_MAX_LOCAL_FILE_SIZE;
      }
    }

    $content_type =
      empty($input['content-type'])
        ? self::get_mime($extension)
        : $input['content-type'];

    if (isset($input['tmp-key'])) {
      // user already uploaded the file to S3
      $tmp_file = $input['tmp-key'];
    } else {

      if (isset($input['local-file'])) {

        //$tmp_file = uniqid('lf-', true);

        /* the API regex for this param should check this, but I don't want to take any chances */
        if (strpos($input['local-file'], '/') !== false) {
          return self::error(
            'Invalid local-file parameter, shenanigans detected',
            400,
          );
        }

        $tmp_file = $input['local-file'];

        if (!file_exists(jump_config::UPLOAD_DIR.'/'.$input['local-file'])) {
          return
            self::error('Invalid local-file parameter -- file not found');
        } else if (filesize(
                     jump_config::UPLOAD_DIR.'/'.$input['local-file'],
                   ) >
                   $local_limit) {
          return self::error('File too large', 400);
        }

      } else if (isset($input['file-data'])) {
        $body = base64_decode($input['file-data'], true);

        if (!$body) {
          return self::error(
            'file-data parameter invalid, should be base64 encoded',
            400,
          );
        }

        $tmp_file = uniqid('fd-', true);

        file_put_contents(jump_config::UPLOAD_DIR.'/'.$tmp_file, $body);

      } else {
        return self::error("Constraint verification failed", 400);
      }

      $file = jump_config::UPLOAD_DIR.'/'.$tmp_file;
      $size = filesize($file);

      if (!file_exists($file)) {
        return self::error('Problem with local file', 500);
      } else if ($size > jump_config::MAX_LOCAL_FILE_SIZE) {
        if ($size <= $local_limit) {
          $used_promo_size = TRUE;
        } else {
          unlink($file);
          return self::error('File too large', 400);
        }
      }

      try {
        $result = $s3client->putObject(
          [
            'Bucket' => $dest_bucket,
            'ACL' => 'private',
            'Key' => 'tmp/'.$tmp_file,
            'SourceFile' => $file,
          ],
        );
      } catch (S3Exception $s3e) {
        error_log('S3 file upload failed: '.(string) $s3e);
        return self::error('failed to upload file to S3', 500);
      }

      unlink($file);
    }

    try {
      $result = $s3client->headObject(
        ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
      );
    } catch (S3Exception $s3e) {
      return
        self::error('File tmp/'.$tmp_file.' not found in S3 bucket', 412);
    } catch (AccessDeniedException $ade) {
      return self::error('Internal exception', 503);
    }

    if ($result['ContentLength'] > jump_config::MAX_FILE_SIZE) {
      if ($result['ContentLength'] <= $size_limit) {
        $used_promo_size = TRUE;
      } else {
        $s3client->deleteObject(
          ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
        );
        return self::error('File too large', 400);
      }
    }

    if (isset($input['custom-url'])) {
      if (!isset($input['promo-code'])) {
        $s3client->deleteObject(
          ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
        );
        return self::error('A custom URL requires a promo code', 403);
      } else if (!$balance['success']) {
        $s3client->deleteObject(
          ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
        );
        return self::error('Invalid promo code', 400);
      } else if ($balance['custom-urls'] <= 0) {
        $s3client->deleteObject(
          ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
        );
        return
          self::error('Promo code has no remaining custom-url credits', 402);
      } else {
        if (self::jump_key_exists($input['custom-url'])) {
          $s3client->deleteObject(
            ['Bucket' => $dest_bucket, 'Key' => 'tmp/'.$tmp_file],
          );
          return self::error('Key requested is not available', 409);
        } else {
          $new_key = $input['custom-url'];
          $used_promo_url = TRUE;
        }
      }
    } else {

      $new_key = '';
      try {
        $new_key = self::gen_uniq_key(3);
      } catch (KeygenException $ke) {
        error_log((string) $ke);
        return self::error((string) $ke, 503);
      }

    }

    // copy the temporary file to its new home
    try {
      $result = $s3client->copyObject(
        [
          'ACL' => $input['private'] ? 'private' : 'public-read',
          'Bucket' => $dest_bucket,
          'CopySource' => urlencode($dest_bucket.'/tmp/'.$tmp_file),
          'ContentType' => $content_type,
          'Key' => $new_key.$extension,
          'StorageClass' => 'REDUCED_REDUNDANCY',
          //'Metadata' => ['IP' => $_SERVER['REMOTE_ADDR']],
          'MetadataDirective' => 'REPLACE',
        ],
      );

    } catch (S3Exception $s3e) {
      error_log('S3 copyObject failed: '.(string) $s3e);
      return self::error('Failed to copy S3 object', 500);
    } catch (AccessDeniedException $ade) {
      return self::error('Internal exception', 503);
    }

    $pass = '<PASSWORD>';
    $salt = '';

    if (!empty($input['password'])) {
      $salt = uniqid('', true);
      $pass = hash('sha256', $input['password'].$salt);
    }

    try {
      $res = $dyclient->putItem(
        [
          'TableName' => aws_config::LINK_TABLE,
          'Item' => array_merge(
            [
              'Object ID' => ['S' => $new_key],
              'pass' => ['S' => $pass],
              'private_b' => ['BOOL' => $input['private']],
              'active_b' => ['BOOL' => true],
              'file_b' => ['BOOL' => true],
              'time' => ['S' => date(DateTime::W3C)],
              'filename' => ['S' => $new_key.$extension],
              'ext' => ['S' => $extension],
              'clicks' => ['N' => strval($input['clicks'] ?: 0)],
              'IP' => ['S' => $_SERVER['REMOTE_ADDR']],
            ],
            (!empty($salt) ? ['salt' => ['S' => $salt]] : []),
          ),
        ],
      );
    } catch (DynamoDbException $dydbe) {
      error_log('DynamoDb died: '.(string) $dydbe);
      return self::error('Failed to create new URL', 503);
    }
    $s3client->deleteObject(['Bucket' => $dest_bucket, 'Key' => $tmp_file]);

    $cdn_host = jump_config::cdn_host();
    $base = jump_config::base_url();

    if ($used_promo_size || $used_promo_urls) {

      try {
        $dyclient->updateItem(
          [
            'TableName' => aws_config::PROMO_TABLE,
            'Key' => ['code' => ['S' => $input['promo-code']]],
            'ExpressionAttributeNames' => [
              '#cu' => 'custom-urls',
              '#lf' => 'large-files',
            ],
            'ExpressionAttributeValues' => [
              ':val1' => [
                'N' => strval(
                  $balance['custom-urls'] - ((int) $used_promo_urls),
                ),
              ],
              ':val2' => [
                'N' => strval(
                  $balance['large-files'] - ((int) $used_promo_size),
                ),
              ],
            ],
            'UpdateExpression' => 'SET #cu = :val1, #lf = :val2',
          ],
        );
      } catch (DynamoDbException $dbe) {
        error_log("Failed to decrement promo code ".$input['promo-code']);
      }
    }

    $ret = [];

    if ($input['private']) {
      $ret = ['url' => $base.$new_key];
    } else {
      $ret = [
        'url' => $base.$new_key,
        'cdn-url' => jump_config::FILE_HOST.$new_key.$extension,
      ];
    }

    return self::success($ret);
    /*  array_merge(
     ['url' => $base.$new_key],
     $input['private']
     ? []
     : ['cdn-url' => jump_config::FILE_HOST.$new_key.$extension],
     ),
     );*/
  }

  public static function genURL($input): array {

    try {
      $input = api_validator::validate($input);
    } catch (ValidationException $ve) {
      return self::error((string) $ve, 400);
    }

    if (!filter_var($input['input-url'], FILTER_VALIDATE_URL)) {
      return self::error("Invalid URL detected.", 400);
    }

    //        $s3client = awsHelper::s3_client();
    $dyclient = awsHelper::dydb_client();
    //        $glclient = awsHelper::gl_client();
{'fraction_non_alphanumeric': 0.17794888247246285, 'fraction_numerical': 0.009981107189961858, 'mean_word_length': 2.202511415525114, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 259, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5795128', 'n_tokens_mistral': 9207, 'n_tokens_neox': 8892, 'n_words': 1753}