<?php

final class PhabricatorRepositoryURI
  extends PhabricatorRepositoryDAO
  implements
    PhabricatorApplicationTransactionInterface,
    PhabricatorPolicyInterface,
    PhabricatorExtendedPolicyInterface,
    PhabricatorConduitResultInterface {

  protected $repositoryPHID;
  protected $uri;
  protected $builtinProtocol;
  protected $builtinIdentifier;
  protected $credentialPHID;
  protected $ioType;
  protected $displayType;
  protected $isDisabled;

  private $repository = self::ATTACHABLE;

  const BUILTIN_PROTOCOL_SSH = 'ssh';
  const BUILTIN_PROTOCOL_HTTP = 'http';
  const BUILTIN_PROTOCOL_HTTPS = 'https';

  const BUILTIN_IDENTIFIER_ID = 'id';
  const BUILTIN_IDENTIFIER_SHORTNAME = 'shortname';
  const BUILTIN_IDENTIFIER_CALLSIGN = 'callsign';

  const DISPLAY_DEFAULT = 'default';
  const DISPLAY_NEVER = 'never';
  const DISPLAY_ALWAYS = 'always';

  const IO_DEFAULT = 'default';
  const IO_OBSERVE = 'observe';
  const IO_MIRROR = 'mirror';
  const IO_NONE = 'none';
  const IO_READ = 'read';
  const IO_READWRITE = 'readwrite';

  protected function getConfiguration() {
    return array(
      self::CONFIG_AUX_PHID => true,
      self::CONFIG_COLUMN_SCHEMA => array(
        'uri' => 'text255',
        'builtinProtocol' => 'text32?',
        'builtinIdentifier' => 'text32?',
        'credentialPHID' => 'phid?',
        'ioType' => 'text32',
        'displayType' => 'text32',
        'isDisabled' => 'bool',
      ),
      self::CONFIG_KEY_SCHEMA => array(
        'key_builtin' => array(
          'columns' => array(
            'repositoryPHID',
            'builtinProtocol',
            'builtinIdentifier',
          ),
          'unique' => true,
        ),
      ),
    ) + parent::getConfiguration();
  }

  public static function initializeNewURI() {
    return id(new self())
      ->setIoType(self::IO_DEFAULT)
      ->setDisplayType(self::DISPLAY_DEFAULT)
      ->setIsDisabled(0);
  }

  public function generatePHID() {
    return PhabricatorPHID::generateNewPHID(
      PhabricatorRepositoryURIPHIDType::TYPECONST);
  }

  public function attachRepository(PhabricatorRepository $repository) {
    $this->repository = $repository;
    return $this;
  }

  public function getRepository() {
    return $this->assertAttached($this->repository);
  }

  public function getRepositoryURIBuiltinKey() {
    if (!$this->getBuiltinProtocol()) {
      return null;
    }

    $parts = array(
      $this->getBuiltinProtocol(),
      $this->getBuiltinIdentifier(),
    );

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

  public function isBuiltin() {
    return (bool)$this->getBuiltinProtocol();
  }

  public function getEffectiveDisplayType() {
    $display = $this->getDisplayType();

    if ($display != self::DISPLAY_DEFAULT) {
      return $display;
    }

    return $this->getDefaultDisplayType();
  }

  public function getDefaultDisplayType() {
    switch ($this->getEffectiveIOType()) {
      case self::IO_MIRROR:
      case self::IO_OBSERVE:
      case self::IO_NONE:
        return self::DISPLAY_NEVER;
      case self::IO_READ:
      case self::IO_READWRITE:
        // By default, only show the "best" version of the builtin URI, not the
        // other redundant versions.
        $repository = $this->getRepository();
        $other_uris = $repository->getURIs();

        $identifier_value = array(
          self::BUILTIN_IDENTIFIER_SHORTNAME => 3,
          self::BUILTIN_IDENTIFIER_CALLSIGN => 2,
          self::BUILTIN_IDENTIFIER_ID => 1,
        );

        $have_identifiers = array();
        foreach ($other_uris as $other_uri) {
          if ($other_uri->getIsDisabled()) {
            continue;
          }

          $identifier = $other_uri->getBuiltinIdentifier();
          if (!$identifier) {
            continue;
          }

          $have_identifiers[$identifier] = $identifier_value[$identifier];
        }

        $best_identifier = max($have_identifiers);
        $this_identifier = $identifier_value[$this->getBuiltinIdentifier()];

        if ($this_identifier < $best_identifier) {
          return self::DISPLAY_NEVER;
        }

        return self::DISPLAY_ALWAYS;
    }

    return self::DISPLAY_NEVER;
  }


  public function getEffectiveIOType() {
    $io = $this->getIoType();

    if ($io != self::IO_DEFAULT) {
      return $io;
    }

    return $this->getDefaultIOType();
  }

  public function getDefaultIOType() {
    if ($this->isBuiltin()) {
      $repository = $this->getRepository();
      $other_uris = $repository->getURIs();

      $any_observe = false;
      foreach ($other_uris as $other_uri) {
        if ($other_uri->getIoType() == self::IO_OBSERVE) {
          $any_observe = true;
          break;
        }
      }

      if ($any_observe) {
        return self::IO_READ;
      } else {
        return self::IO_READWRITE;
      }
    }

    return self::IO_NONE;
  }

  public function getNormalizedURI() {
    $vcs = $this->getRepository()->getVersionControlSystem();

    $map = array(
      PhabricatorRepositoryType::REPOSITORY_TYPE_GIT =>
        ArcanistRepositoryURINormalizer::TYPE_GIT,
      PhabricatorRepositoryType::REPOSITORY_TYPE_SVN =>
        ArcanistRepositoryURINormalizer::TYPE_SVN,
      PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
        ArcanistRepositoryURINormalizer::TYPE_MERCURIAL,
    );

    $type = $map[$vcs];
    $display = (string)$this->getDisplayURI();

    $normalizer = new ArcanistRepositoryURINormalizer($type, $display);

    $domain_map = self::getURINormalizerDomainMap();
    $normalizer->setDomainMap($domain_map);

    return $normalizer->getNormalizedURI();
  }

  public function getDisplayURI() {
    return $this->getURIObject();
  }

  public function getEffectiveURI() {
    return $this->getURIObject();
  }

  public function getURIEnvelope() {
    $uri = $this->getEffectiveURI();

    $command_engine = $this->newCommandEngine();

    $is_http = $command_engine->isAnyHTTPProtocol();
    // For SVN, we use `--username` and `--password` flags separately in the
    // CommandEngine, so we don't need to add any credentials here.
    $is_svn = $this->getRepository()->isSVN();
    $credential_phid = $this->getCredentialPHID();

    if ($is_http && !$is_svn && $credential_phid) {
      $key = PassphrasePasswordKey::loadFromPHID(
        $credential_phid,
        PhabricatorUser::getOmnipotentUser());

      $uri->setUser($key->getUsernameEnvelope()->openEnvelope());
      $uri->setPass($key->getPasswordEnvelope()->openEnvelope());
    }

    return new PhutilOpaqueEnvelope((string)$uri);
  }

  private function getURIObject() {
    // Users can provide Git/SCP-style URIs in the form "user@host:path".
    // In the general case, these are not equivalent to any "ssh://..." form
    // because the path is relative.

    if ($this->isBuiltin()) {
      $builtin_protocol = $this->getForcedProtocol();
      $builtin_domain = $this->getForcedHost();
      $raw_uri = "{$builtin_protocol}://{$builtin_domain}";
    } else {
      $raw_uri = $this->getURI();
    }

    $port = $this->getForcedPort();

    $default_ports = array(
      'ssh' => 22,
      'http' => 80,
      'https' => 443,
    );

    $uri = new PhutilURI($raw_uri);

    // Make sure to remove any password from the URI before we do anything
    // with it; this should always be provided by the associated credential.
    $uri->setPass(null);

    $protocol = $this->getForcedProtocol();
    if ($protocol) {
      $uri->setProtocol($protocol);
    }

    if ($port) {
      $uri->setPort($port);
    }

    // Remove any explicitly set default ports.
    $uri_port = $uri->getPort();
    $uri_protocol = $uri->getProtocol();

    $uri_default = idx($default_ports, $uri_protocol);
    if ($uri_default && ($uri_default == $uri_port)) {
      $uri->setPort(null);
    }

    $user = $this->getForcedUser();
    if ($user) {
      $uri->setUser($user);
    }

    $host = $this->getForcedHost();
    if ($host) {
      $uri->setDomain($host);
    }

    $path = $this->getForcedPath();
    if ($path) {
      $uri->setPath($path);
    }

    return $uri;
  }


  private function getForcedProtocol() {
    $repository = $this->getRepository();

    switch ($this->getBuiltinProtocol()) {
      case self::BUILTIN_PROTOCOL_SSH:
        if ($repository->isSVN()) {
          return 'svn+ssh';
        } else {
          return 'ssh';
        }
      case self::BUILTIN_PROTOCOL_HTTP:
        return 'http';
      case self::BUILTIN_PROTOCOL_HTTPS:
        return 'https';
      default:
        return null;
    }
  }

  private function getForcedUser() {
    switch ($this->getBuiltinProtocol()) {
      case self::BUILTIN_PROTOCOL_SSH:
        return AlmanacKeys::getClusterSSHUser();
      default:
        return null;
    }
  }

  private function getForcedHost() {
    $phabricator_uri = PhabricatorEnv::getURI('/');
    $phabricator_uri = new PhutilURI($phabricator_uri);

    $phabricator_host = $phabricator_uri->getDomain();

    switch ($this->getBuiltinProtocol()) {
      case self::BUILTIN_PROTOCOL_SSH:
        $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host');
        if ($ssh_host !== null) {
          return $ssh_host;
        }
        return $phabricator_host;
      case self::BUILTIN_PROTOCOL_HTTP:
      case self::BUILTIN_PROTOCOL_HTTPS:
        return $phabricator_host;
      default:
        return null;
    }
  }

  private function getForcedPort() {
    $protocol = $this->getBuiltinProtocol();

    if ($protocol == self::BUILTIN_PROTOCOL_SSH) {
      return PhabricatorEnv::getEnvConfig('diffusion.ssh-port');
    }

    // If Phabricator is running on a nonstandard port, use that as the default
    // port for URIs with the same protocol.

    $is_http = ($protocol == self::BUILTIN_PROTOCOL_HTTP);
    $is_https = ($protocol == self::BUILTIN_PROTOCOL_HTTPS);

    if ($is_http || $is_https) {
      $uri = PhabricatorEnv::getURI('/');
      $uri = new PhutilURI($uri);

      $port = $uri->getPort();
      if (!$port) {
        return null;
      }

      $uri_protocol = $uri->getProtocol();
      $use_port =
        ($is_http && ($uri_protocol == 'http')) ||
        ($is_https && ($uri_protocol == 'https'));

      if (!$use_port) {
        return null;
      }

      return $port;
    }

    return null;
  }

  private function getForcedPath() {
    if (!$this->isBuiltin()) {
      return null;
    }

    $repository = $this->getRepository();

    $id = $repository->getID();
    $callsign = $repository->getCallsign();
    $short_name = $repository->getRepositorySlug();

    $clone_name = $repository->getCloneName();

    if ($repository->isGit()) {
      $suffix = '.git';
    } else if ($repository->isHg()) {
      $suffix = '/';
    } else {
      $suffix = '';
      $clone_name = '';
    }

    switch ($this->getBuiltinIdentifier()) {
      case self::BUILTIN_IDENTIFIER_ID:
        return "/diffusion/{$id}/{$clone_name}{$suffix}";
      case self::BUILTIN_IDENTIFIER_SHORTNAME:
        return "/source/{$short_name}{$suffix}";
      case self::BUILTIN_IDENTIFIER_CALLSIGN:
        return "/diffusion/{$callsign}/{$clone_name}{$suffix}";
      default:
        return null;
    }
  }

  public function getViewURI() {
    $id = $this->getID();
    return $this->getRepository()->getPathURI("uri/view/{$id}/");
  }

  public function getEditURI() {
    $id = $this->getID();
    return $this->getRepository()->getPathURI("uri/edit/{$id}/");
  }

  public function getAvailableIOTypeOptions() {
    $options = array(
      self::IO_DEFAULT,
      self::IO_NONE,
    );

    if ($this->isBuiltin()) {
      $options[] = self::IO_READ;
      $options[] = self::IO_READWRITE;
    } else {
      $options[] = self::IO_OBSERVE;
      $options[] = self::IO_MIRROR;
    }

    $map = array();
    $io_map = self::getIOTypeMap();
    foreach ($options as $option) {
      $spec = idx($io_map, $option, array());

      $label = idx($spec, 'label', $option);
      $short = idx($spec, 'short');

      $name = pht('%s: %s', $label, $short);
      $map[$option] = $name;
    }

    return $map;
  }

  public function getAvailableDisplayTypeOptions() {
    $options = array(
      self::DISPLAY_DEFAULT,
      self::DISPLAY_ALWAYS,
      self::DISPLAY_NEVER,
    );

    $map = array();
    $display_map = self::getDisplayTypeMap();
    foreach ($options as $option) {
      $spec = idx($display_map, $option, array());

      $label = idx($spec, 'label', $option);
      $short = idx($spec, 'short');

      $name = pht('%s: %s', $label, $short);
      $map[$option] = $name;
    }

    return $map;
  }

  public static function getIOTypeMap() {
    return array(
      self::IO_DEFAULT => array(
        'label' => pht('Default'),
        'short' => pht('Use default behavior.'),
      ),
      self::IO_OBSERVE => array(
        'icon' => 'fa-download',
        'color' => 'green',
        'label' => pht('Observe'),
        'note' => pht(
          'Changes to this URI will be observed and pulled.'),
        'short' => pht('Copy from a remote.'),
      ),
      self::IO_MIRROR => array(
        'icon' => 'fa-upload',
        'color' => 'green',
        'label' => pht('Mirror'),
        'note' => pht(
          'A copy of any changes will be pushed to this URI.'),
        'short' => pht('Push a copy to a remote.'),
      ),
      self::IO_NONE => array(
        'icon' => 'fa-times',
        'color' => 'grey',
        'label' => pht('No I/O'),
        'note' => pht(
          'No changes will be pushed or pulled from this URI.'),
        'short' => pht('Do not perform any I/O.'),
      ),
      self::IO_READ => array(
        'icon' => 'fa-folder',
        'color' => 'blue',
        'label' => pht('Read Only'),
        'note' => pht(
          'A read-only copy of the repository will be served from this URI.'),
        'short' => pht('Serve repository in read-only mode.'),
      ),
      self::IO_READWRITE => array(
        'icon' => 'fa-folder-open',
        'color' => 'blue',
        'label' => pht('Read/Write'),
        'note' => pht(
          'A read/write copy of the repository will be served from this URI.'),
        'short' => pht('Serve repository in read/write mode.'),
      ),
    );
  }

  public static function getDisplayTypeMap() {
    return array(
      self::DISPLAY_DEFAULT => array(
        'label' => pht('Default'),
        'short' => pht('Use default behavior.'),
      ),
      self::DISPLAY_ALWAYS => array(
        'icon' => 'fa-eye',
        'color' => 'green',
        'label' => pht('Visible'),
        'note' => pht('This URI will be shown to users as a clone URI.'),
        'short' => pht('Show as a clone URI.'),
      ),
      self::DISPLAY_NEVER => array(
        'icon' => 'fa-eye-slash',
        'color' => 'grey',
        'label' => pht('Hidden'),
        'note' => pht(
          'This URI will be hidden from users.'),
        'short' => pht('Do not show as a clone URI.'),
      ),
    );
  }

  public function newCommandEngine() {
    $repository = $this->getRepository();

    return DiffusionCommandEngine::newCommandEngine($repository)
      ->setCredentialPHID($this->getCredentialPHID())
      ->setURI($this->getEffectiveURI());
  }

  public function getURIScore() {
    $score = 0;

    $io_points = array(
      self::IO_READWRITE => 200,
      self::IO_READ => 100,
    );
    $score += idx($io_points, $this->getEffectiveIOType(), 0);

    $protocol_points = array(
      self::BUILTIN_PROTOCOL_SSH => 30,
      self::BUILTIN_PROTOCOL_HTTPS => 20,
      self::BUILTIN_PROTOCOL_HTTP => 10,
    );
    $score += idx($protocol_points, $this->getBuiltinProtocol(), 0);

    $identifier_points = array(
      self::BUILTIN_IDENTIFIER_SHORTNAME => 3,
      self::BUILTIN_IDENTIFIER_CALLSIGN => 2,
      self::BUILTIN_IDENTIFIER_ID => 1,
    );
    $score += idx($identifier_points, $this->getBuiltinIdentifier(), 0);

    return $score;
  }



/* -(  PhabricatorApplicationTransactionInterface  )------------------------- */


  public function getApplicationTransactionEditor() {
    return new DiffusionURIEditor();
  }

  public function getApplicationTransactionTemplate() {
    return new PhabricatorRepositoryURITransaction();
  }


/* -(  PhabricatorPolicyInterface  )----------------------------------------- */


  public function getCapabilities() {
    return array(
      PhabricatorPolicyCapability::CAN_VIEW,
      PhabricatorPolicyCapability::CAN_EDIT,
    );
  }

  public function getPolicy($capability) {
    switch ($capability) {
      case PhabricatorPolicyCapability::CAN_VIEW:
      case PhabricatorPolicyCapability::CAN_EDIT:
        return PhabricatorPolicies::getMostOpenPolicy();
    }
  }

  public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
    return false;
  }


/* -(  PhabricatorExtendedPolicyInterface  )--------------------------------- */


  public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
    $extended = array();

    switch ($capability) {
      case PhabricatorPolicyCapability::CAN_EDIT:
        // To edit a repository URI, you must be able to edit the
        // corresponding repository.
        $extended[] = array($this->getRepository(), $capability);
        break;
    }

    return $extended;
  }


/* -(  PhabricatorConduitResultInterface  )---------------------------------- */


  public function getFieldSpecificationsForConduit() {
    return array(
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('repositoryPHID')
        ->setType('phid')
        ->setDescription(pht('The associated repository PHID.')),
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('uri')
        ->setType('map<string, string>')
        ->setDescription(pht('The raw and effective URI.')),
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('io')
        ->setType('map<string, const>')
        ->setDescription(
          pht('The raw, default, and effective I/O Type settings.')),
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('display')
        ->setType('map<string, const>')
        ->setDescription(
          pht('The raw, default, and effective Display Type settings.')),
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('credentialPHID')
        ->setType('phid?')
        ->setDescription(
          pht('The associated credential PHID, if one exists.')),
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('disabled')
        ->setType('bool')
        ->setDescription(pht('True if the URI is disabled.')),
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('builtin')
        ->setType('map<string, string>')
        ->setDescription(
          pht('Information about builtin URIs.')),
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('dateCreated')
        ->setType('int')
        ->setDescription(
          pht('Epoch timestamp when the object was created.')),
      id(new PhabricatorConduitSearchFieldSpecification())
        ->setKey('dateModified')
        ->setType('int')
        ->setDescription(
          pht('Epoch timestamp when the object was last updated.')),
    );
  }

  public function getFieldValuesForConduit() {
    return array(
      'repositoryPHID' => $this->getRepositoryPHID(),
      'uri' => array(
        'raw' => $this->getURI(),
        'display' => (string)$this->getDisplayURI(),
        'effective' => (string)$this->getEffectiveURI(),
        'normalized' => (string)$this->getNormalizedURI(),
      ),
      'io' => array(
        'raw' => $this->getIOType(),
        'default' => $this->getDefaultIOType(),
        'effective' => $this->getEffectiveIOType(),
      ),
      'display' => array(
        'raw' => $this->getDisplayType(),
        'default' => $this->getDefaultDisplayType(),
        'effective' => $this->getEffectiveDisplayType(),
      ),
      'credentialPHID' => $this->getCredentialPHID(),
      'disabled' => (bool)$this->getIsDisabled(),
      'builtin' => array(
        'protocol' => $this->getBuiltinProtocol(),
        'identifier' => $this->getBuiltinIdentifier(),
      ),
      'dateCreated' => $this->getDateCreated(),
      'dateModified' => $this->getDateModified(),
    );
  }

  public function getConduitSearchAttachments() {
    return array();
  }

  public static function getURINormalizerDomainMap() {
    $domain_map = array();

    // See T13435. If the domain for a repository URI is same as the install
    // base URI, store it as a "<base-uri>" token instead of the actual domain
    // so that the index does not fall out of date if the install moves.

    $base_uri = PhabricatorEnv::getURI('/');
    $base_uri = new PhutilURI($base_uri);
    $base_domain = $base_uri->getDomain();
    $domain_map['<base-uri>'] = $base_domain;

    // Likewise, store a token for the "SSH Host" domain so it can be changed
    // without requiring an index rebuild.

    $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host');
    if (strlen($ssh_host)) {
      $domain_map['<ssh-host>'] = $ssh_host;
    }

    return $domain_map;
  }

}
