[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/diffusion/request/ -> DiffusionRequest.php (source)

   1  <?php
   2  
   3  /**
   4   * Contains logic to parse Diffusion requests, which have a complicated URI
   5   * structure.
   6   *
   7   * @task new Creating Requests
   8   * @task uri Managing Diffusion URIs
   9   */
  10  abstract class DiffusionRequest {
  11  
  12    protected $callsign;
  13    protected $path;
  14    protected $line;
  15    protected $branch;
  16    protected $lint;
  17  
  18    protected $symbolicCommit;
  19    protected $symbolicType;
  20    protected $stableCommit;
  21  
  22    protected $repository;
  23    protected $repositoryCommit;
  24    protected $repositoryCommitData;
  25    protected $arcanistProjects;
  26  
  27    private $initFromConduit = true;
  28    private $user;
  29    private $branchObject = false;
  30  
  31    abstract public function supportsBranches();
  32    abstract protected function isStableCommit($symbol);
  33  
  34    protected function didInitialize() {
  35      return null;
  36    }
  37  
  38  
  39  /* -(  Creating Requests  )-------------------------------------------------- */
  40  
  41  
  42    /**
  43     * Create a new synthetic request from a parameter dictionary. If you need
  44     * a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you
  45     * can use this method to build one.
  46     *
  47     * Parameters are:
  48     *
  49     *   - `callsign` Repository callsign. Provide this or `repository`.
  50     *   - `user` Viewing user. Required if `callsign` is provided.
  51     *   - `repository` Repository object. Provide this or `callsign`.
  52     *   - `branch` Optional, branch name.
  53     *   - `path` Optional, file path.
  54     *   - `commit` Optional, commit identifier.
  55     *   - `line` Optional, line range.
  56     *
  57     * @param   map                 See documentation.
  58     * @return  DiffusionRequest    New request object.
  59     * @task new
  60     */
  61    final public static function newFromDictionary(array $data) {
  62      if (isset($data['repository']) && isset($data['callsign'])) {
  63        throw new Exception(
  64          "Specify 'repository' or 'callsign', but not both.");
  65      } else if (!isset($data['repository']) && !isset($data['callsign'])) {
  66        throw new Exception(
  67          "One of 'repository' and 'callsign' is required.");
  68      } else if (isset($data['callsign']) && empty($data['user'])) {
  69        throw new Exception(
  70          "Parameter 'user' is required if 'callsign' is provided.");
  71      }
  72  
  73      if (isset($data['repository'])) {
  74        $object = self::newFromRepository($data['repository']);
  75      } else {
  76        $object = self::newFromCallsign($data['callsign'], $data['user']);
  77      }
  78  
  79      $object->initializeFromDictionary($data);
  80  
  81      return $object;
  82    }
  83  
  84  
  85    /**
  86     * Create a new request from an Aphront request dictionary. This is an
  87     * internal method that you generally should not call directly; instead,
  88     * call @{method:newFromDictionary}.
  89     *
  90     * @param   map                 Map of Aphront request data.
  91     * @return  DiffusionRequest    New request object.
  92     * @task new
  93     */
  94    final public static function newFromAphrontRequestDictionary(
  95      array $data,
  96      AphrontRequest $request) {
  97  
  98      $callsign = phutil_unescape_uri_path_component(idx($data, 'callsign'));
  99      $object = self::newFromCallsign($callsign, $request->getUser());
 100  
 101      $use_branches = $object->supportsBranches();
 102      $parsed = self::parseRequestBlob(idx($data, 'dblob'), $use_branches);
 103  
 104      $object->setUser($request->getUser());
 105      $object->initializeFromDictionary($parsed);
 106      $object->lint = $request->getStr('lint');
 107      return $object;
 108    }
 109  
 110  
 111    /**
 112     * Internal.
 113     *
 114     * @task new
 115     */
 116    final private function __construct() {
 117      // <private>
 118    }
 119  
 120  
 121    /**
 122     * Internal. Use @{method:newFromDictionary}, not this method.
 123     *
 124     * @param   string              Repository callsign.
 125     * @param   PhabricatorUser     Viewing user.
 126     * @return  DiffusionRequest    New request object.
 127     * @task new
 128     */
 129    final private static function newFromCallsign(
 130      $callsign,
 131      PhabricatorUser $viewer) {
 132  
 133      $repository = id(new PhabricatorRepositoryQuery())
 134        ->setViewer($viewer)
 135        ->withCallsigns(array($callsign))
 136        ->executeOne();
 137  
 138      if (!$repository) {
 139        throw new Exception("No such repository '{$callsign}'.");
 140      }
 141  
 142      return self::newFromRepository($repository);
 143    }
 144  
 145  
 146    /**
 147     * Internal. Use @{method:newFromDictionary}, not this method.
 148     *
 149     * @param   PhabricatorRepository   Repository object.
 150     * @return  DiffusionRequest        New request object.
 151     * @task new
 152     */
 153    final private static function newFromRepository(
 154      PhabricatorRepository $repository) {
 155  
 156      $map = array(
 157        PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',
 158        PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',
 159        PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
 160          'DiffusionMercurialRequest',
 161      );
 162  
 163      $class = idx($map, $repository->getVersionControlSystem());
 164  
 165      if (!$class) {
 166        throw new Exception('Unknown version control system!');
 167      }
 168  
 169      $object = new $class();
 170  
 171      $object->repository = $repository;
 172      $object->callsign   = $repository->getCallsign();
 173  
 174      return $object;
 175    }
 176  
 177  
 178    /**
 179     * Internal. Use @{method:newFromDictionary}, not this method.
 180     *
 181     * @param map Map of parsed data.
 182     * @return void
 183     * @task new
 184     */
 185    final private function initializeFromDictionary(array $data) {
 186      $this->path            = idx($data, 'path');
 187      $this->line            = idx($data, 'line');
 188      $this->initFromConduit = idx($data, 'initFromConduit', true);
 189  
 190      $this->symbolicCommit = idx($data, 'commit');
 191      if ($this->supportsBranches()) {
 192        $this->branch = idx($data, 'branch');
 193      }
 194  
 195      if (!$this->getUser()) {
 196        $user = idx($data, 'user');
 197        if (!$user) {
 198          throw new Exception(
 199            'You must provide a PhabricatorUser in the dictionary!');
 200        }
 201        $this->setUser($user);
 202      }
 203  
 204      $this->didInitialize();
 205    }
 206  
 207    final protected function shouldInitFromConduit() {
 208      return $this->initFromConduit;
 209    }
 210  
 211    final public function setUser(PhabricatorUser $user) {
 212      $this->user = $user;
 213      return $this;
 214    }
 215    final public function getUser() {
 216      return $this->user;
 217    }
 218  
 219    public function getRepository() {
 220      return $this->repository;
 221    }
 222  
 223    public function getCallsign() {
 224      return $this->callsign;
 225    }
 226  
 227    public function setPath($path) {
 228      $this->path = $path;
 229      return $this;
 230    }
 231  
 232    public function getPath() {
 233      return $this->path;
 234    }
 235  
 236    public function getLine() {
 237      return $this->line;
 238    }
 239  
 240    public function getCommit() {
 241  
 242      // TODO: Probably remove all of this.
 243  
 244      if ($this->getSymbolicCommit() !== null) {
 245        return $this->getSymbolicCommit();
 246      }
 247  
 248      return $this->getStableCommit();
 249    }
 250  
 251    /**
 252     * Get the symbolic commit associated with this request.
 253     *
 254     * A symbolic commit may be a commit hash, an abbreviated commit hash, a
 255     * branch name, a tag name, or an expression like "HEAD^^^". The symbolic
 256     * commit may also be absent.
 257     *
 258     * This method always returns the symbol present in the original request,
 259     * in unmodified form.
 260     *
 261     * See also @{method:getStableCommit}.
 262     *
 263     * @return string|null  Symbolic commit, if one was present in the request.
 264     */
 265    public function getSymbolicCommit() {
 266      return $this->symbolicCommit;
 267    }
 268  
 269  
 270    /**
 271     * Modify the request to move the symbolic commit elsewhere.
 272     *
 273     * @param string New symbolic commit.
 274     * @return this
 275     */
 276    public function updateSymbolicCommit($symbol) {
 277      $this->symbolicCommit = $symbol;
 278      $this->symbolicType = null;
 279      $this->stableCommit = null;
 280      return $this;
 281    }
 282  
 283  
 284    /**
 285     * Get the ref type (`commit` or `tag`) of the location associated with this
 286     * request.
 287     *
 288     * If a symbolic commit is present in the request, this method identifies
 289     * the type of the symbol. Otherwise, it identifies the type of symbol of
 290     * the location the request is implicitly associated with. This will probably
 291     * always be `commit`.
 292     *
 293     * @return string   Symbolic commit type (`commit` or `tag`).
 294     */
 295    public function getSymbolicType() {
 296      if ($this->symbolicType === null) {
 297        // As a side effect, this resolves the symbolic type.
 298        $this->getStableCommit();
 299      }
 300      return $this->symbolicType;
 301    }
 302  
 303  
 304    /**
 305     * Retrieve the stable, permanent commit name identifying the repository
 306     * location associated with this request.
 307     *
 308     * This returns a non-symbolic identifier for the current commit: in Git and
 309     * Mercurial, a 40-character SHA1; in SVN, a revision number.
 310     *
 311     * See also @{method:getSymbolicCommit}.
 312     *
 313     * @return string Stable commit name, like a git hash or SVN revision. Not
 314     *                a symbolic commit reference.
 315     */
 316    public function getStableCommit() {
 317      if (!$this->stableCommit) {
 318        if ($this->isStableCommit($this->symbolicCommit)) {
 319          $this->stableCommit = $this->symbolicCommit;
 320          $this->symbolicType = 'commit';
 321        } else {
 322          $this->queryStableCommit();
 323        }
 324      }
 325      return $this->stableCommit;
 326    }
 327  
 328  
 329    public function getBranch() {
 330      return $this->branch;
 331    }
 332  
 333    public function getLint() {
 334      return $this->lint;
 335    }
 336  
 337    protected function getArcanistBranch() {
 338      return $this->getBranch();
 339    }
 340  
 341    public function loadBranch() {
 342      // TODO: Get rid of this and do real Queries on real objects.
 343  
 344      if ($this->branchObject === false) {
 345        $this->branchObject = PhabricatorRepositoryBranch::loadBranch(
 346          $this->getRepository()->getID(),
 347          $this->getArcanistBranch());
 348      }
 349  
 350      return $this->branchObject;
 351    }
 352  
 353    public function loadCoverage() {
 354      // TODO: This should also die.
 355      $branch = $this->loadBranch();
 356      if (!$branch) {
 357        return;
 358      }
 359  
 360      $path = $this->getPath();
 361      $path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
 362  
 363      $coverage_row = queryfx_one(
 364        id(new PhabricatorRepository())->establishConnection('r'),
 365        'SELECT * FROM %T WHERE branchID = %d AND pathID = %d
 366          ORDER BY commitID DESC LIMIT 1',
 367        'repository_coverage',
 368        $branch->getID(),
 369        $path_map[$path]);
 370  
 371      if (!$coverage_row) {
 372        return null;
 373      }
 374  
 375      return idx($coverage_row, 'coverage');
 376    }
 377  
 378  
 379    public function loadCommit() {
 380      if (empty($this->repositoryCommit)) {
 381        $repository = $this->getRepository();
 382  
 383        // TODO: (T603) This should be a real query, but we need to sort out
 384        // the viewer.
 385        $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
 386          'repositoryID = %d AND commitIdentifier = %s',
 387          $repository->getID(),
 388          $this->getStableCommit());
 389        if ($commit) {
 390          $commit->attachRepository($repository);
 391        }
 392        $this->repositoryCommit = $commit;
 393      }
 394      return $this->repositoryCommit;
 395    }
 396  
 397    public function loadArcanistProjects() {
 398      if (empty($this->arcanistProjects)) {
 399        $projects = id(new PhabricatorRepositoryArcanistProject())->loadAllWhere(
 400          'repositoryID = %d',
 401          $this->getRepository()->getID());
 402        $this->arcanistProjects = $projects;
 403      }
 404      return $this->arcanistProjects;
 405    }
 406  
 407    public function loadCommitData() {
 408      if (empty($this->repositoryCommitData)) {
 409        $commit = $this->loadCommit();
 410        $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
 411          'commitID = %d',
 412          $commit->getID());
 413        if (!$data) {
 414          $data = new PhabricatorRepositoryCommitData();
 415          $data->setCommitMessage(
 416            '(This commit has not been fully parsed yet.)');
 417        }
 418        $this->repositoryCommitData = $data;
 419      }
 420      return $this->repositoryCommitData;
 421    }
 422  
 423  /* -(  Managing Diffusion URIs  )-------------------------------------------- */
 424  
 425  
 426    /**
 427     * Generate a Diffusion URI using this request to provide defaults. See
 428     * @{method:generateDiffusionURI} for details. This method is the same, but
 429     * preserves the request parameters if they are not overridden.
 430     *
 431     * @param   map         See @{method:generateDiffusionURI}.
 432     * @return  PhutilURI   Generated URI.
 433     * @task uri
 434     */
 435    public function generateURI(array $params) {
 436      if (empty($params['stable'])) {
 437        $default_commit = $this->getSymbolicCommit();
 438      } else {
 439        $default_commit = $this->getStableCommit();
 440      }
 441  
 442      $defaults = array(
 443        'callsign'  => $this->getCallsign(),
 444        'path'      => $this->getPath(),
 445        'branch'    => $this->getBranch(),
 446        'commit'    => $default_commit,
 447        'lint'      => idx($params, 'lint', $this->getLint()),
 448      );
 449      foreach ($defaults as $key => $val) {
 450        if (!isset($params[$key])) { // Overwrite NULL.
 451          $params[$key] = $val;
 452        }
 453      }
 454      return self::generateDiffusionURI($params);
 455    }
 456  
 457  
 458    /**
 459     * Generate a Diffusion URI from a parameter map. Applies the correct encoding
 460     * and formatting to the URI. Parameters are:
 461     *
 462     *   - `action` One of `history`, `browse`, `change`, `lastmodified`,
 463     *     `branch`, `tags`, `branches`,  or `revision-ref`. The action specified
 464     *      by the URI.
 465     *   - `callsign` Repository callsign.
 466     *   - `branch` Optional if action is not `branch`, branch name.
 467     *   - `path` Optional, path to file.
 468     *   - `commit` Optional, commit identifier.
 469     *   - `line` Optional, line range.
 470     *   - `lint` Optional, lint code.
 471     *   - `params` Optional, query parameters.
 472     *
 473     * The function generates the specified URI and returns it.
 474     *
 475     * @param   map         See documentation.
 476     * @return  PhutilURI   Generated URI.
 477     * @task uri
 478     */
 479    public static function generateDiffusionURI(array $params) {
 480      $action = idx($params, 'action');
 481  
 482      $callsign = idx($params, 'callsign');
 483      $path     = idx($params, 'path');
 484      $branch   = idx($params, 'branch');
 485      $commit   = idx($params, 'commit');
 486      $line     = idx($params, 'line');
 487  
 488      if (strlen($callsign)) {
 489        $callsign = phutil_escape_uri_path_component($callsign).'/';
 490      }
 491  
 492      if (strlen($branch)) {
 493        $branch = phutil_escape_uri_path_component($branch).'/';
 494      }
 495  
 496      if (strlen($path)) {
 497        $path = ltrim($path, '/');
 498        $path = str_replace(array(';', '$'), array(';;', '$$'), $path);
 499        $path = phutil_escape_uri($path);
 500      }
 501  
 502      $path = "{$branch}{$path}";
 503  
 504      if (strlen($commit)) {
 505        $commit = str_replace('$', '$$', $commit);
 506        $commit = ';'.phutil_escape_uri($commit);
 507      }
 508  
 509      if (strlen($line)) {
 510        $line = '$'.phutil_escape_uri($line);
 511      }
 512  
 513      $req_callsign = false;
 514      $req_branch   = false;
 515      $req_commit   = false;
 516  
 517      switch ($action) {
 518        case 'history':
 519        case 'browse':
 520        case 'change':
 521        case 'lastmodified':
 522        case 'tags':
 523        case 'branches':
 524        case 'lint':
 525          $req_callsign = true;
 526          break;
 527        case 'branch':
 528          $req_callsign = true;
 529          $req_branch = true;
 530          break;
 531        case 'commit':
 532          $req_callsign = true;
 533          $req_commit = true;
 534          break;
 535      }
 536  
 537      if ($req_callsign && !strlen($callsign)) {
 538        throw new Exception(
 539          "Diffusion URI action '{$action}' requires callsign!");
 540      }
 541  
 542      if ($req_commit && !strlen($commit)) {
 543        throw new Exception(
 544          "Diffusion URI action '{$action}' requires commit!");
 545      }
 546  
 547      switch ($action) {
 548        case 'change':
 549        case 'history':
 550        case 'browse':
 551        case 'lastmodified':
 552        case 'tags':
 553        case 'branches':
 554        case 'lint':
 555        case 'pathtree':
 556          $uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}";
 557          break;
 558        case 'branch':
 559          if (strlen($path)) {
 560            $uri = "/diffusion/{$callsign}repository/{$path}";
 561          } else {
 562            $uri = "/diffusion/{$callsign}";
 563          }
 564          break;
 565        case 'external':
 566          $commit = ltrim($commit, ';');
 567          $uri = "/diffusion/external/{$commit}/";
 568          break;
 569        case 'rendering-ref':
 570          // This isn't a real URI per se, it's passed as a query parameter to
 571          // the ajax changeset stuff but then we parse it back out as though
 572          // it came from a URI.
 573          $uri = rawurldecode("{$path}{$commit}");
 574          break;
 575        case 'commit':
 576          $commit = ltrim($commit, ';');
 577          $callsign = rtrim($callsign, '/');
 578          $uri = "/r{$callsign}{$commit}";
 579          break;
 580        default:
 581          throw new Exception("Unknown Diffusion URI action '{$action}'!");
 582      }
 583  
 584      if ($action == 'rendering-ref') {
 585        return $uri;
 586      }
 587  
 588      $uri = new PhutilURI($uri);
 589  
 590      if (isset($params['lint'])) {
 591        $params['params'] = idx($params, 'params', array()) + array(
 592          'lint' => $params['lint'],
 593        );
 594      }
 595  
 596      if (idx($params, 'params')) {
 597        $uri->setQueryParams($params['params']);
 598      }
 599  
 600      return $uri;
 601    }
 602  
 603  
 604    /**
 605     * Internal. Public only for unit tests.
 606     *
 607     * Parse the request URI into components.
 608     *
 609     * @param   string  URI blob.
 610     * @param   bool    True if this VCS supports branches.
 611     * @return  map     Parsed URI.
 612     *
 613     * @task uri
 614     */
 615    public static function parseRequestBlob($blob, $supports_branches) {
 616      $result = array(
 617        'branch'  => null,
 618        'path'    => null,
 619        'commit'  => null,
 620        'line'    => null,
 621      );
 622  
 623      $matches = null;
 624  
 625      if ($supports_branches) {
 626        // Consume the front part of the URI, up to the first "/". This is the
 627        // path-component encoded branch name.
 628        if (preg_match('@^([^/]+)/@', $blob, $matches)) {
 629          $result['branch'] = phutil_unescape_uri_path_component($matches[1]);
 630          $blob = substr($blob, strlen($matches[1]) + 1);
 631        }
 632      }
 633  
 634      // Consume the back part of the URI, up to the first "$". Use a negative
 635      // lookbehind to prevent matching '$$'. We double the '$' symbol when
 636      // encoding so that files with names like "money/$100" will survive.
 637      $pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d-,]+)$@';
 638      if (preg_match($pattern, $blob, $matches)) {
 639        $result['line'] = $matches[1];
 640        $blob = substr($blob, 0, -(strlen($matches[1]) + 1));
 641      }
 642  
 643      // We've consumed the line number if it exists, so unescape "$" in the
 644      // rest of the string.
 645      $blob = str_replace('$$', '$', $blob);
 646  
 647      // Consume the commit name, stopping on ';;'. We allow any character to
 648      // appear in commits names, as they can sometimes be symbolic names (like
 649      // tag names or refs).
 650      if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) {
 651        $result['commit'] = $matches[1];
 652        $blob = substr($blob, 0, -(strlen($matches[1]) + 1));
 653      }
 654  
 655      // We've consumed the commit if it exists, so unescape ";" in the rest
 656      // of the string.
 657      $blob = str_replace(';;', ';', $blob);
 658  
 659      if (strlen($blob)) {
 660        $result['path'] = $blob;
 661      }
 662  
 663      $parts = explode('/', $result['path']);
 664      foreach ($parts as $part) {
 665        // Prevent any hyjinx since we're ultimately shipping this to the
 666        // filesystem under a lot of workflows.
 667        if ($part == '..') {
 668          throw new Exception('Invalid path URI.');
 669        }
 670      }
 671  
 672      return $result;
 673    }
 674  
 675    /**
 676     * Check that the working copy of the repository is present and readable.
 677     *
 678     * @param   string  Path to the working copy.
 679     */
 680    protected function validateWorkingCopy($path) {
 681      if (!is_readable(dirname($path))) {
 682        $this->raisePermissionException();
 683      }
 684  
 685      if (!Filesystem::pathExists($path)) {
 686        $this->raiseCloneException();
 687      }
 688    }
 689  
 690    protected function raisePermissionException() {
 691      $host = php_uname('n');
 692      $callsign = $this->getRepository()->getCallsign();
 693      throw new DiffusionSetupException(
 694        "The clone of this repository ('{$callsign}') on the local machine ".
 695        "('{$host}') could not be read. Ensure that the repository is in a ".
 696        "location where the web server has read permissions.");
 697    }
 698  
 699    protected function raiseCloneException() {
 700      $host = php_uname('n');
 701      $callsign = $this->getRepository()->getCallsign();
 702      throw new DiffusionSetupException(
 703        "The working copy for this repository ('{$callsign}') hasn't been ".
 704        "cloned yet on this machine ('{$host}'). Make sure you've started the ".
 705        "Phabricator daemons. If this problem persists for longer than a clone ".
 706        "should take, check the daemon logs (in the Daemon Console) to see if ".
 707        "there were errors cloning the repository. Consult the 'Diffusion User ".
 708        "Guide' in the documentation for help setting up repositories.");
 709    }
 710  
 711    private function queryStableCommit() {
 712      if ($this->symbolicCommit) {
 713        $ref = $this->symbolicCommit;
 714      } else {
 715        if ($this->supportsBranches()) {
 716          $ref = $this->getResolvableBranchName($this->getBranch());
 717        } else {
 718          $ref = 'HEAD';
 719        }
 720      }
 721  
 722      $results = $this->resolveRefs(array($ref));
 723  
 724      $matches = idx($results, $ref, array());
 725      if (count($matches) !== 1) {
 726        $message = pht('Ref "%s" is ambiguous or does not exist.', $ref);
 727        throw id(new DiffusionRefNotFoundException($message))
 728          ->setRef($ref);
 729      }
 730  
 731      $match = head($matches);
 732  
 733      $this->stableCommit = $match['identifier'];
 734      $this->symbolicType = $match['type'];
 735    }
 736  
 737    protected function getResolvableBranchName($branch) {
 738      return $branch;
 739    }
 740  
 741    private function resolveRefs(array $refs) {
 742      if ($this->shouldInitFromConduit()) {
 743        return DiffusionQuery::callConduitWithDiffusionRequest(
 744          $this->getUser(),
 745          $this,
 746          'diffusion.resolverefs',
 747          array(
 748            'refs' => $refs,
 749          ));
 750      } else {
 751        return id(new DiffusionLowLevelResolveRefsQuery())
 752          ->setRepository($this->getRepository())
 753          ->withRefs($refs)
 754          ->execute();
 755      }
 756    }
 757  
 758  
 759  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1