<?php

/**
 * Guard writes against CSRF. The Aphront structure takes care of most of this
 * for you, you just need to call:
 *
 *    AphrontWriteGuard::willWrite();
 *
 * ...before executing a write against any new kind of storage engine. MySQL
 * databases and the default file storage engines are already covered, but if
 * you introduce new types of datastores make sure their writes are guarded. If
 * you don't guard writes and make a mistake doing CSRF checks in a controller,
 * a CSRF vulnerability can escape undetected.
 *
 * If you need to execute writes on a page which doesn't have CSRF tokens (for
 * example, because you need to do logging), you can temporarily disable the
 * write guard by calling:
 *
 *    AphrontWriteGuard::beginUnguardedWrites();
 *    do_logging_write();
 *    AphrontWriteGuard::endUnguardedWrites();
 *
 * This is dangerous, because it disables the backup layer of CSRF protection
 * this class provides. You should need this only very, very rarely.
 *
 * @task protect  Protecting Writes
 * @task disable  Disabling Protection
 * @task manage   Managing Write Guards
 * @task internal Internals
 */
final class AphrontWriteGuard extends Phobject {

  private static $instance;
  private static $allowUnguardedWrites = false;

  private $callback;
  private $allowDepth = 0;


/* -(  Managing Write Guards  )---------------------------------------------- */


  /**
   * Construct a new write guard for a request. Only one write guard may be
   * active at a time. You must explicitly call @{method:dispose} when you are
   * done with a write guard:
   *
   *    $guard = new AphrontWriteGuard($callback);
   *    // ...
   *    $guard->dispose();
   *
   * Normally, you do not need to manage guards yourself -- the Aphront stack
   * handles it for you.
   *
   * This class accepts a callback, which will be invoked when a write is
   * attempted. The callback should validate the presence of a CSRF token in
   * the request, or abort the request (e.g., by throwing an exception) if a
   * valid token isn't present.
   *
   * @param   callable CSRF callback.
   * @return  this
   * @task    manage
   */
  public function __construct($callback) {
    if (self::$instance) {
      throw new Exception(
        pht(
          'An %s already exists. Dispose of the previous guard '.
          'before creating a new one.',
          __CLASS__));
    }
    if (self::$allowUnguardedWrites) {
      throw new Exception(
        pht(
          'An %s is being created in a context which permits '.
          'unguarded writes unconditionally. This is not allowed and '.
          'indicates a serious error.',
          __CLASS__));
    }
    $this->callback = $callback;
    self::$instance = $this;
  }


  /**
   * Dispose of the active write guard. You must call this method when you are
   * done with a write guard. You do not normally need to call this yourself.
   *
   * @return void
   * @task manage
   */
  public function dispose() {
    if (!self::$instance) {
      throw new Exception(pht(
        'Attempting to dispose of write guard, but no write guard is active!'));
    }

    if ($this->allowDepth > 0) {
      throw new Exception(
        pht(
          'Imbalanced %s: more % calls than %s calls.',
          __CLASS__,
          'beginUnguardedWrites()',
          'endUnguardedWrites()'));
    }
    self::$instance = null;
  }


  /**
   * Determine if there is an active write guard.
   *
   * @return bool
   * @task manage
   */
  public static function isGuardActive() {
    return (bool)self::$instance;
  }

  /**
   * Return on instance of AphrontWriteGuard if it's active, or null
   *
   * @return AphrontWriteGuard|null
   */
  public static function getInstance() {
    return self::$instance;
  }


/* -(  Protecting Writes  )-------------------------------------------------- */


  /**
   * Declare intention to perform a write, validating that writes are allowed.
   * You should call this method before executing a write whenever you implement
   * a new storage engine where information can be permanently kept.
   *
   * Writes are permitted if:
   *
   *   - The request has valid CSRF tokens.
   *   - Unguarded writes have been temporarily enabled by a call to
   *     @{method:beginUnguardedWrites}.
   *   - All write guarding has been disabled with
   *     @{method:allowDangerousUnguardedWrites}.
   *
   * If none of these conditions are true, this method will throw and prevent
   * the write.
   *
   * @return void
   * @task protect
   */
  public static function willWrite() {
    if (!self::$instance) {
      if (!self::$allowUnguardedWrites) {
        throw new Exception(
          pht(
            'Unguarded write! There must be an active %s to perform writes.',
            __CLASS__));
      } else {
        // Unguarded writes are being allowed unconditionally.
        return;
      }
    }

    $instance = self::$instance;
    if ($instance->allowDepth == 0) {
      call_user_func($instance->callback);
    }
  }


/* -(  Disabling Write Protection  )----------------------------------------- */


  /**
   * Enter a scope which permits unguarded writes. This works like
   * @{method:beginUnguardedWrites} but returns an object which will end
   * the unguarded write scope when its __destruct() method is called. This
   * is useful to more easily handle exceptions correctly in unguarded write
   * blocks:
   *
   *   // Restores the guard even if do_logging() throws.
   *   function unguarded_scope() {
   *     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
   *     do_logging();
   *   }
   *
   * @return AphrontScopedUnguardedWriteCapability Object which ends unguarded
   *            writes when it leaves scope.
   * @task disable
   */
  public static function beginScopedUnguardedWrites() {
    self::beginUnguardedWrites();
    return new AphrontScopedUnguardedWriteCapability();
  }


  /**
   * Begin a block which permits unguarded writes. You should use this very
   * sparingly, and only for things like logging where CSRF is not a concern.
   *
   * You must pair every call to @{method:beginUnguardedWrites} with a call to
   * @{method:endUnguardedWrites}:
   *
   *   AphrontWriteGuard::beginUnguardedWrites();
   *   do_logging();
   *   AphrontWriteGuard::endUnguardedWrites();
   *
   * @return void
   * @task disable
   */
  public static function beginUnguardedWrites() {
    if (!self::$instance) {
      return;
    }
    self::$instance->allowDepth++;
  }

  /**
   * Declare that you have finished performing unguarded writes. You must
   * call this exactly once for each call to @{method:beginUnguardedWrites}.
   *
   * @return void
   * @task disable
   */
  public static function endUnguardedWrites() {
    if (!self::$instance) {
      return;
    }
    if (self::$instance->allowDepth <= 0) {
      throw new Exception(
        pht(
          'Imbalanced %s: more %s calls than %s calls.',
          __CLASS__,
          'endUnguardedWrites()',
          'beginUnguardedWrites()'));
    }
    self::$instance->allowDepth--;
  }


  /**
   * Allow execution of unguarded writes. This is ONLY appropriate for use in
   * script contexts or other contexts where you are guaranteed to never be
   * vulnerable to CSRF concerns. Calling this method is EXTREMELY DANGEROUS
   * if you do not understand the consequences.
   *
   * If you need to perform unguarded writes on an otherwise guarded workflow
   * which is vulnerable to CSRF, use @{method:beginUnguardedWrites}.
   *
   * @return void
   * @task disable
   */
  public static function allowDangerousUnguardedWrites($allow) {
    if (self::$instance) {
      throw new Exception(
        pht(
          'You can not unconditionally disable %s by calling %s while a write '.
          'guard is active. Use %s to temporarily allow unguarded writes.',
          __CLASS__,
          __FUNCTION__.'()',
          'beginUnguardedWrites()'));
    }
    self::$allowUnguardedWrites = true;
  }

}
