diff --git a/scripts/repository/commit_hook.php b/scripts/repository/commit_hook.php index ccbfde08ff..28829edeb2 100755 --- a/scripts/repository/commit_hook.php +++ b/scripts/repository/commit_hook.php @@ -1,237 +1,237 @@ #!/usr/bin/env php <?php // NOTE: This script will sometimes emit a warning like this on startup: // // No entry for terminal type "unknown"; // using dumb terminal settings. // // This can be fixed by adding "TERM=dumb" to the shebang line, but doing so // causes some systems to hang mysteriously. See T7119. // Commit hooks execute in an unusual context where the environment may be // unavailable, particularly in SVN. The first parameter to this script is // either a bare repository identifier ("X"), or a repository identifier // followed by an instance identifier ("X:instance"). If we have an instance // identifier, unpack it into the environment before we start up. This allows // subclasses of PhabricatorConfigSiteSource to read it and build an instance // environment. $hook_start = microtime(true); if ($argc > 1) { $context = $argv[1]; $context = explode(':', $context, 2); $argv[1] = $context[0]; if (count($context) > 1) { $_ENV['PHABRICATOR_INSTANCE'] = $context[1]; putenv('PHABRICATOR_INSTANCE='.$context[1]); } } $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; if ($argc < 2) { throw new Exception(pht('usage: commit-hook <repository>')); } $engine = id(new DiffusionCommitHookEngine()) ->setStartTime($hook_start); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIdentifiers(array($argv[1])) ->needProjectPHIDs(true) ->executeOne(); if (!$repository) { throw new Exception(pht('No such repository "%s"!', $argv[1])); } if (!$repository->isHosted()) { // In Mercurial, the "pretxnchangegroup" hook fires for both pulls and // pushes. Normally we only install the hook for hosted repositories, but // if a hosted repository is later converted into an observed repository we // can end up with an observed repository that has the hook installed. // If we're running hooks from an observed repository, just exit without // taking action. For more discussion, see PHI24. return 0; } $engine->setRepository($repository); $args = new PhutilArgumentParser($argv); $args->parsePartial( array( array( 'name' => 'hook-mode', 'param' => 'mode', 'help' => pht('Hook execution mode.'), ), )); $argv = array_merge( array($argv[0]), $args->getUnconsumedArgumentVector()); // Figure out which user is writing the commit. $hook_mode = $args->getArg('hook-mode'); if ($hook_mode !== null) { $known_modes = array( 'svn-revprop' => true, ); if (empty($known_modes[$hook_mode])) { throw new Exception( pht( 'Invalid Hook Mode: This hook was invoked in "%s" mode, but this '. 'is not a recognized hook mode. Valid modes are: %s.', $hook_mode, implode(', ', array_keys($known_modes)))); } } $is_svnrevprop = ($hook_mode == 'svn-revprop'); if ($is_svnrevprop) { // For now, we let these through if the repository allows dangerous changes // and prevent them if it doesn't. See T11208 for discussion. $revprop_key = $argv[5]; if ($repository->shouldAllowDangerousChanges()) { $err = 0; } else { $err = 1; $console = PhutilConsole::getConsole(); $console->writeErr( pht( "DANGEROUS CHANGE: Dangerous change protection is enabled for this ". "repository, so you can not change revision properties (you are ". "attempting to edit \"%s\").\n". "Edit the repository configuration before making dangerous changes.", $revprop_key)); } exit($err); } else if ($repository->isGit() || $repository->isHg()) { $username = getenv(DiffusionCommitHookEngine::ENV_USER); - if (!strlen($username)) { + if (!phutil_nonempty_string($username)) { throw new Exception( pht( 'No Direct Pushes: You are pushing directly to a hosted repository. '. 'This will not work. See "No Direct Pushes" in the documentation '. 'for more information.')); } if ($repository->isHg()) { // We respond to several different hooks in Mercurial. $engine->setMercurialHook($argv[2]); } } else if ($repository->isSVN()) { // NOTE: In Subversion, the entire environment gets wiped so we can't read // DiffusionCommitHookEngine::ENV_USER. Instead, we've set "--tunnel-user" to // specify the correct user; read this user out of the commit log. if ($argc < 4) { throw new Exception(pht('usage: commit-hook <repository> <repo> <txn>')); } $svn_repo = $argv[2]; $svn_txn = $argv[3]; list($username) = execx('svnlook author -t %s %s', $svn_txn, $svn_repo); $username = rtrim($username, "\n"); $engine->setSubversionTransactionInfo($svn_txn, $svn_repo); } else { throw new Exception(pht('Unknown repository type.')); } $user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUsernames(array($username)) ->executeOne(); if (!$user) { throw new Exception(pht('No such user "%s"!', $username)); } $engine->setViewer($user); // Read stdin for the hook engine. if ($repository->isHg()) { // Mercurial leaves stdin open, so we can't just read it until EOF. $stdin = ''; } else { // Git and Subversion write data into stdin and then close it. Read the // data. $stdin = @file_get_contents('php://stdin'); if ($stdin === false) { throw new Exception(pht('Failed to read stdin!')); } } $engine->setStdin($stdin); $engine->setOriginalArgv(array_slice($argv, 2)); $remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS); -if (strlen($remote_address)) { +if (phutil_nonempty_string($remote_address)) { $engine->setRemoteAddress($remote_address); } $remote_protocol = getenv(DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL); -if (strlen($remote_protocol)) { +if (phutil_nonempty_string($remote_protocol)) { $engine->setRemoteProtocol($remote_protocol); } $request_identifier = getenv(DiffusionCommitHookEngine::ENV_REQUEST); -if (strlen($request_identifier)) { +if (phutil_nonempty_string($request_identifier)) { $engine->setRequestIdentifier($request_identifier); } try { $err = $engine->execute(); } catch (DiffusionCommitHookRejectException $ex) { $console = PhutilConsole::getConsole(); if (PhabricatorEnv::getEnvConfig('phabricator.serious-business')) { $preamble = pht('*** PUSH REJECTED BY COMMIT HOOK ***'); } else { $preamble = pht(<<<EOTXT +---------------------------------------------------------------+ | * * * PUSH REJECTED BY EVIL DRAGON BUREAUCRATS * * * | +---------------------------------------------------------------+ \ \ ^ /^ \ / \ // \ \ |\___/| / \// .\ \ /V V \__ / // | \ \ *----* / / \/_/ // | \ \ \ | @___@` \/_ // | \ \ \/\ \ 0/0/| \/_ // | \ \ \ \ 0/0/0/0/| \/// | \ \ | | 0/0/0/0/0/_|_ / ( // | \ _\ | / 0/0/0/0/0/0/`/,_ _ _/ ) ; -. | _ _\.-~ / / ,-} _ *-.|.-~-. .~ ~ * \__/ `/\ / ~-. _ .-~ / \____(Oo) *. } { / ( (..) .----~-.\ \-` .~ //___\\\\ \ DENIED! ///.----..< \ _ -~ // \\\\ ///-._ _ _ _ _ _ _{^ - - - - ~ EOTXT ); } $console->writeErr("%s\n\n", $preamble); $console->writeErr("%s\n\n", $ex->getMessage()); $err = 1; } exit($err); diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth.php index 45e9a823db..2b3b52cf4b 100755 --- a/scripts/ssh/ssh-auth.php +++ b/scripts/ssh/ssh-auth.php @@ -1,161 +1,161 @@ #!/usr/bin/env php <?php $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/init/init-script.php'; $error_log = id(new PhutilErrorLog()) ->setLogName(pht('SSH Error Log')) ->setLogPath(PhabricatorEnv::getEnvConfig('log.ssh-error.path')) ->activateLog(); // TODO: For now, this is using "parseParital()", not "parse()". This allows // the script to accept (and ignore) additional arguments. This preserves // backward compatibility until installs have time to migrate to the new // syntax. $args = id(new PhutilArgumentParser($argv)) ->parsePartial( array( array( 'name' => 'sshd-key', 'param' => 'k', 'help' => pht( 'Accepts the "%%k" parameter from "AuthorizedKeysCommand".'), ), )); $sshd_key = $args->getArg('sshd-key'); // NOTE: We are caching a datastructure rather than the flat key file because // the path on disk to "ssh-exec" is arbitrarily mutable at runtime. See T12397. $cache = PhabricatorCaches::getMutableCache(); $authstruct_key = PhabricatorAuthSSHKeyQuery::AUTHSTRUCT_CACHEKEY; $authstruct_raw = $cache->getKey($authstruct_key); $authstruct = null; -if (strlen($authstruct_raw)) { +if (phutil_nonempty_string($authstruct_raw)) { try { $authstruct = phutil_json_decode($authstruct_raw); } catch (Exception $ex) { // Ignore any issues with the cached data; we'll just rebuild the // structure below. } } if ($authstruct === null) { $keys = id(new PhabricatorAuthSSHKeyQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIsActive(true) ->execute(); if (!$keys) { echo pht('No keys found.')."\n"; exit(1); } $key_list = array(); foreach ($keys as $ssh_key) { $key_argv = array(); $object = $ssh_key->getObject(); if ($object instanceof PhabricatorUser) { $key_argv[] = '--phabricator-ssh-user'; $key_argv[] = $object->getUsername(); } else if ($object instanceof AlmanacDevice) { if (!$ssh_key->getIsTrusted()) { // If this key is not a trusted device key, don't allow SSH // authentication. continue; } $key_argv[] = '--phabricator-ssh-device'; $key_argv[] = $object->getName(); } else { // We don't know what sort of key this is; don't permit SSH auth. continue; } $key_argv[] = '--phabricator-ssh-key'; $key_argv[] = $ssh_key->getID(); // Strip out newlines and other nonsense from the key type and key body. $type = $ssh_key->getKeyType(); $type = preg_replace('@[\x00-\x20]+@', '', $type); - if (!strlen($type)) { + if (!phutil_nonempty_string($type)) { continue; } $key = $ssh_key->getKeyBody(); $key = preg_replace('@[\x00-\x20]+@', '', $key); - if (!strlen($key)) { + if (!phutil_nonempty_string($key)) { continue; } $key_list[] = array( 'argv' => $key_argv, 'type' => $type, 'key' => $key, ); } $authstruct = array( 'keys' => $key_list, ); $authstruct_raw = phutil_json_encode($authstruct); $ttl = phutil_units('24 hours in seconds'); $cache->setKey($authstruct_key, $authstruct_raw, $ttl); } // If we've received an "--sshd-key" argument and it matches some known key, // only emit that key. (For now, if the key doesn't match, we'll fall back to // emitting all keys.) if ($sshd_key !== null) { $matches = array(); foreach ($authstruct['keys'] as $key => $key_struct) { if ($key_struct['key'] === $sshd_key) { $matches[$key] = $key_struct; } } if ($matches) { $authstruct['keys'] = $matches; } } $bin = $root.'/bin/ssh-exec'; $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); $lines = array(); foreach ($authstruct['keys'] as $key_struct) { $key_argv = $key_struct['argv']; $key = $key_struct['key']; $type = $key_struct['type']; $cmd = csprintf('%s %Ls', $bin, $key_argv); - if (strlen($instance)) { + if (phutil_nonempty_string($instance)) { $cmd = csprintf('PHABRICATOR_INSTANCE=%s %C', $instance, $cmd); } // This is additional escaping for the SSH 'command="..."' string. $cmd = addcslashes($cmd, '"\\'); $options = array( 'command="'.$cmd.'"', 'no-port-forwarding', 'no-X11-forwarding', 'no-agent-forwarding', 'no-pty', ); $options = implode(',', $options); $lines[] = $options.' '.$type.' '.$key."\n"; } $authfile = implode('', $lines); echo $authfile; exit(0); diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index 70c95d28da..7b5b6adb8f 100755 --- a/scripts/ssh/ssh-exec.php +++ b/scripts/ssh/ssh-exec.php @@ -1,349 +1,349 @@ #!/usr/bin/env php <?php $ssh_start_time = microtime(true); $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/init/init-script.php'; $error_log = id(new PhutilErrorLog()) ->setLogName(pht('SSH Error Log')) ->setLogPath(PhabricatorEnv::getEnvConfig('log.ssh-error.path')) ->activateLog(); $ssh_log = PhabricatorSSHLog::getLog(); $request_identifier = Filesystem::readRandomCharacters(12); $ssh_log->setData( array( 'Q' => $request_identifier, )); $args = new PhutilArgumentParser($argv); $args->setTagline(pht('execute SSH requests')); $args->setSynopsis(<<<EOSYNOPSIS **ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__] **ssh-exec** --phabricator-ssh-device __device__ [--ssh-command __commmand__] Execute authenticated SSH requests. This script is normally invoked via SSHD, but can be invoked manually for testing. EOSYNOPSIS ); $args->parseStandardArguments(); $args->parse( array( array( 'name' => 'phabricator-ssh-user', 'param' => 'username', 'help' => pht( 'If the request authenticated with a user key, the name of the '. 'user.'), ), array( 'name' => 'phabricator-ssh-device', 'param' => 'name', 'help' => pht( 'If the request authenticated with a device key, the name of the '. 'device.'), ), array( 'name' => 'phabricator-ssh-key', 'param' => 'id', 'help' => pht( 'The ID of the SSH key which authenticated this request. This is '. 'used to allow logs to report when specific keys were used, to make '. 'it easier to manage credentials.'), ), array( 'name' => 'ssh-command', 'param' => 'command', 'help' => pht( 'Provide a command to execute. This makes testing this script '. 'easier. When running normally, the command is read from the '. 'environment (%s), which is populated by sshd.', 'SSH_ORIGINAL_COMMAND'), ), )); try { $remote_address = null; $ssh_client = getenv('SSH_CLIENT'); if ($ssh_client) { // This has the format "<ip> <remote-port> <local-port>". Grab the IP. $remote_address = head(explode(' ', $ssh_client)); $ssh_log->setData( array( 'r' => $remote_address, )); } $key_id = $args->getArg('phabricator-ssh-key'); if ($key_id) { $ssh_log->setData( array( 'k' => $key_id, )); } $user_name = $args->getArg('phabricator-ssh-user'); $device_name = $args->getArg('phabricator-ssh-device'); $user = null; $device = null; $is_cluster_request = false; if ($user_name && $device_name) { throw new Exception( pht( 'The %s and %s flags are mutually exclusive. You can not '. 'authenticate as both a user ("%s") and a device ("%s"). '. 'Specify one or the other, but not both.', '--phabricator-ssh-user', '--phabricator-ssh-device', $user_name, $device_name)); - } else if (strlen($user_name)) { + } else if (phutil_nonempty_string($user_name)) { $user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUsernames(array($user_name)) ->executeOne(); if (!$user) { throw new Exception( pht( 'Invalid username ("%s"). There is no user with this username.', $user_name)); } id(new PhabricatorAuthSessionEngine()) ->willServeRequestForUser($user); - } else if (strlen($device_name)) { + } else if (phutil_nonempty_string($device_name)) { if (!$remote_address) { throw new Exception( pht( 'Unable to identify remote address from the %s environment '. 'variable. Device authentication is accepted only from trusted '. 'sources.', 'SSH_CLIENT')); } if (!PhabricatorEnv::isClusterAddress($remote_address)) { throw new Exception( pht( 'This request originates from outside of the cluster address range. '. 'Requests signed with a trusted device key must originate from '. 'trusted hosts.')); } $device = id(new AlmanacDeviceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withNames(array($device_name)) ->executeOne(); if (!$device) { throw new Exception( pht( 'Invalid device name ("%s"). There is no device with this name.', $device_name)); } if ($device->isDisabled()) { throw new Exception( pht( 'This request has authenticated as a device ("%s"), but this '. 'device is disabled.', $device->getName())); } // We're authenticated as a device, but we're going to read the user out of // the command below. $is_cluster_request = true; } else { throw new Exception( pht( 'This script must be invoked with either the %s or %s flag.', '--phabricator-ssh-user', '--phabricator-ssh-device')); } if ($args->getArg('ssh-command')) { $original_command = $args->getArg('ssh-command'); } else { $original_command = getenv('SSH_ORIGINAL_COMMAND'); } $original_argv = id(new PhutilShellLexer()) ->splitArguments($original_command); if ($device) { // If we're authenticating as a device, the first argument may be a // "@username" argument to act as a particular user. $first_argument = head($original_argv); if (preg_match('/^@/', $first_argument)) { $act_as_name = array_shift($original_argv); $act_as_name = substr($act_as_name, 1); $user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUsernames(array($act_as_name)) ->executeOne(); if (!$user) { throw new Exception( pht( 'Device request identifies an acting user with an invalid '. 'username ("%s"). There is no user with this username.', $act_as_name)); } } else { $user = PhabricatorUser::getOmnipotentUser(); } } if ($user->isOmnipotent()) { $user_name = 'device/'.$device->getName(); } else { $user_name = $user->getUsername(); } $ssh_log->setData( array( 'u' => $user_name, 'P' => $user->getPHID(), )); if (!$device) { if (!$user->canEstablishSSHSessions()) { throw new Exception( pht( 'Your account ("%s") does not have permission to establish SSH '. 'sessions. Visit the web interface for more information.', $user_name)); } } $workflows = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorSSHWorkflow') ->setUniqueMethod('getName') ->execute(); $command_list = array_keys($workflows); $command_list = implode(', ', $command_list); $error_lines = array(); $error_lines[] = pht( 'Welcome to %s.', PlatformSymbols::getPlatformServerName()); $error_lines[] = pht( 'You are logged in as %s.', $user_name); if (!$original_argv) { $error_lines[] = pht( 'You have not specified a command to run. This means you are requesting '. 'an interactive shell, but this server does not provide interactive '. 'shells over SSH.'); $error_lines[] = pht( '(Usually, you should run a command like "git clone" or "hg push" '. 'instead of connecting directly with SSH.)'); $error_lines[] = pht( 'Supported commands are: %s.', $command_list); $error_lines = implode("\n\n", $error_lines); throw new PhutilArgumentUsageException($error_lines); } $log_argv = implode(' ', $original_argv); $log_argv = id(new PhutilUTF8StringTruncator()) ->setMaximumCodepoints(128) ->truncateString($log_argv); $ssh_log->setData( array( 'C' => $original_argv[0], 'U' => $log_argv, )); $command = head($original_argv); $parseable_argv = $original_argv; array_unshift($parseable_argv, 'phabricator-ssh-exec'); $parsed_args = new PhutilArgumentParser($parseable_argv); if (empty($workflows[$command])) { $error_lines[] = pht( 'You have specified the command "%s", but that command is not '. 'supported by this server. As received by this server, your entire '. 'argument list was:', $command); $error_lines[] = csprintf(' $ ssh ... -- %Ls', $parseable_argv); $error_lines[] = pht( 'Supported commands are: %s.', $command_list); $error_lines = implode("\n\n", $error_lines); throw new PhutilArgumentUsageException($error_lines); } $workflow = $parsed_args->parseWorkflows($workflows); $workflow->setSSHUser($user); $workflow->setOriginalArguments($original_argv); $workflow->setIsClusterRequest($is_cluster_request); $workflow->setRequestIdentifier($request_identifier); $sock_stdin = fopen('php://stdin', 'r'); if (!$sock_stdin) { throw new Exception(pht('Unable to open stdin.')); } $sock_stdout = fopen('php://stdout', 'w'); if (!$sock_stdout) { throw new Exception(pht('Unable to open stdout.')); } $sock_stderr = fopen('php://stderr', 'w'); if (!$sock_stderr) { throw new Exception(pht('Unable to open stderr.')); } $socket_channel = new PhutilSocketChannel( $sock_stdin, $sock_stdout); $error_channel = new PhutilSocketChannel(null, $sock_stderr); $metrics_channel = new PhutilMetricsChannel($socket_channel); $workflow->setIOChannel($metrics_channel); $workflow->setErrorChannel($error_channel); $rethrow = null; try { $err = $workflow->execute($parsed_args); $metrics_channel->flush(); $error_channel->flush(); } catch (Exception $ex) { $rethrow = $ex; } // Always write this if we got as far as building a metrics channel. $ssh_log->setData( array( 'i' => $metrics_channel->getBytesRead(), 'o' => $metrics_channel->getBytesWritten(), )); if ($rethrow) { throw $rethrow; } } catch (Exception $ex) { fwrite(STDERR, "phabricator-ssh-exec: ".$ex->getMessage()."\n"); $err = 1; } $ssh_log->setData( array( 'c' => $err, 'T' => phutil_microseconds_since($ssh_start_time), )); exit($err); diff --git a/scripts/symbols/generate_ctags_symbols.php b/scripts/symbols/generate_ctags_symbols.php index e93b0c5cbc..13972385ab 100755 --- a/scripts/symbols/generate_ctags_symbols.php +++ b/scripts/symbols/generate_ctags_symbols.php @@ -1,138 +1,138 @@ #!/usr/bin/env php <?php $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; $args = new PhutilArgumentParser($argv); $args->setSynopsis(<<<EOSYNOPSIS **generate_ctags_symbols.php** [__options__] Generate repository symbols using Exuberant Ctags. Paths are read from stdin. EOSYNOPSIS ); $args->parseStandardArguments(); if (ctags_check_executable() == false) { echo phutil_console_format( "%s\n\n%s\n", pht( 'Could not find Exuberant Ctags. Make sure it is installed and '. 'available in executable path.'), pht( 'Exuberant Ctags project page: %s', 'http://ctags.sourceforge.net/')); exit(1); } if (posix_isatty(STDIN)) { echo phutil_console_format( "%s\n", pht( 'Usage: %s', "find . -type f -name '*.py' | ./generate_ctags_symbols.php")); exit(1); } $input = file_get_contents('php://stdin'); $data = array(); $futures = array(); foreach (explode("\n", trim($input)) as $file) { - if (!strlen($file)) { + if (!phutil_nonempty_string($file)) { continue; } $file = Filesystem::readablePath($file); $futures[$file] = ctags_get_parser_future($file); } $futures = new FutureIterator($futures); foreach ($futures->limit(8) as $file => $future) { $tags = $future->resolve(); $tags = explode("\n", $tags[1]); foreach ($tags as $tag) { $parts = explode(';', $tag); // Skip lines that we can not parse. if (count($parts) < 2) { continue; } // Split ctags information. $tag_info = explode("\t", $parts[0]); // Split exuberant ctags "extension fields" (additional information). $parts[1] = trim($parts[1], "\t \""); $extension_fields = explode("\t", $parts[1]); // Skip lines that we can not parse. if (count($tag_info) < 3 || count($extension_fields) < 2) { continue; } // Default context to empty. $extension_fields[] = ''; list($token, $file_path, $line_num) = $tag_info; list($type, $language, $context) = $extension_fields; // Skip lines with tokens containing a space. if (strpos($token, ' ') !== false) { continue; } // Strip "language:" $language = substr($language, 9); // To keep consistent with "Separate with commas, for example: php, py" // in Arcanist Project edit form. $language = str_ireplace('python', 'py', $language); // Also, "normalize" C++ and C#. $language = str_ireplace('c++', 'cpp', $language); $language = str_ireplace('c#', 'cs', $language); // Ruby has "singleton method", for example. $type = substr(str_replace(' ', '_', $type), 0, 12); // class:foo, struct:foo, union:foo, enum:foo, ... $context = last(explode(':', $context, 2)); $ignore = array( 'variable' => true, ); if (empty($ignore[$type])) { print_symbol($file_path, $line_num, $type, $token, $context, $language); } } } function ctags_get_parser_future($path) { $future = new ExecFuture('ctags -n --fields=Kls -o - %s', $path); return $future; } function ctags_check_executable() { $result = exec_manual('ctags --version'); return !empty($result[1]); } function print_symbol($file, $line_num, $type, $token, $context, $language) { // Get rid of relative path. $file = explode('/', $file); if ($file[0] == '.' || $file[0] == '..') { array_shift($file); } $file = '/'.implode('/', $file); $parts = array( $context, $token, $type, strtolower($language), $line_num, $file, ); echo implode(' ', $parts)."\n"; } diff --git a/scripts/symbols/generate_php_symbols.php b/scripts/symbols/generate_php_symbols.php index af87d580d8..cc8cab3817 100755 --- a/scripts/symbols/generate_php_symbols.php +++ b/scripts/symbols/generate_php_symbols.php @@ -1,127 +1,127 @@ #!/usr/bin/env php <?php $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; $args = new PhutilArgumentParser($argv); $args->setSynopsis(<<<EOSYNOPSIS **generate_php_symbols.php** [__options__] Generate repository symbols using XHPAST. Paths are read from stdin. EOSYNOPSIS ); $args->parseStandardArguments(); if (posix_isatty(STDIN)) { echo phutil_console_format( "%s\n", pht( 'Usage: %s', "find . -type f -name '*.php' | ./generate_php_symbols.php")); exit(1); } $input = file_get_contents('php://stdin'); $data = array(); $futures = array(); foreach (explode("\n", trim($input)) as $file) { - if (!strlen($file)) { + if (!phutil_nonempty_string($file)) { continue; } $file = Filesystem::readablePath($file); $data[$file] = Filesystem::readFile($file); $futures[$file] = PhutilXHPASTBinary::getParserFuture($data[$file]); } $futures = new FutureIterator($futures); foreach ($futures->limit(8) as $file => $future) { $tree = XHPASTTree::newFromDataAndResolvedExecFuture( $data[$file], $future->resolve()); $root = $tree->getRootNode(); $scopes = array(); $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($functions as $function) { $name = $function->getChildByIndex(2); // Skip anonymous functions. if (!$name->getConcreteString()) { continue; } print_symbol($file, 'function', $name); } $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1); print_symbol($file, 'class', $class_name); $scopes[] = array($class, $class_name); } $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); foreach ($interfaces as $interface) { $interface_name = $interface->getChildByIndex(1); // We don't differentiate classes and interfaces in highlighters. print_symbol($file, 'class', $interface_name); $scopes[] = array($interface, $interface_name); } $constants = $root->selectDescendantsOfType('n_CONSTANT_DECLARATION_LIST'); foreach ($constants as $constant_list) { foreach ($constant_list->getChildren() as $constant) { $constant_name = $constant->getChildByIndex(0); print_symbol($file, 'constant', $constant_name); } } foreach ($scopes as $scope) { // This prints duplicate symbols in the case of nested classes. // Luckily, PHP doesn't allow those. list($class, $class_name) = $scope; $consts = $class->selectDescendantsOfType( 'n_CLASS_CONSTANT_DECLARATION_LIST'); foreach ($consts as $const_list) { foreach ($const_list->getChildren() as $const) { $const_name = $const->getChildByIndex(0); print_symbol($file, 'class_const', $const_name, $class_name); } } $members = $class->selectDescendantsOfType( 'n_CLASS_MEMBER_DECLARATION_LIST'); foreach ($members as $member_list) { foreach ($member_list->getChildren() as $member) { if ($member->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') { continue; } $member_name = $member->getChildByIndex(0); print_symbol($file, 'member', $member_name, $class_name); } } $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); foreach ($methods as $method) { $method_name = $method->getChildByIndex(2); print_symbol($file, 'method', $method_name, $class_name); } } } function print_symbol($file, $type, XHPASTNode $node, $context = null) { $parts = array( $context ? $context->getConcreteString() : '', // Variable tokens are `$name`, not just `name`, so strip the "$"" off of // class field names ltrim($node->getConcreteString(), '$'), $type, 'php', $node->getLineNumber(), '/'.ltrim($file, './'), ); echo implode(' ', $parts)."\n"; } diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index f35f3f1279..58fc6f9346 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -1,970 +1,970 @@ <?php /** * @task data Accessing Request Data * @task cookie Managing Cookies * @task cluster Working With a Phabricator Cluster */ final class AphrontRequest extends Phobject { // NOTE: These magic request-type parameters are automatically included in // certain requests (e.g., by phabricator_form(), JX.Request, // JX.Workflow, and ConduitClient) and help us figure out what sort of // response the client expects. const TYPE_AJAX = '__ajax__'; const TYPE_FORM = '__form__'; const TYPE_CONDUIT = '__conduit__'; const TYPE_WORKFLOW = '__wflow__'; const TYPE_CONTINUE = '__continue__'; const TYPE_PREVIEW = '__preview__'; const TYPE_HISEC = '__hisec__'; const TYPE_QUICKSAND = '__quicksand__'; private $host; private $path; private $requestData; private $user; private $applicationConfiguration; private $site; private $controller; private $uriData = array(); private $cookiePrefix; private $submitKey; public function __construct($host, $path) { $this->host = $host; $this->path = $path; } public function setURIMap(array $uri_data) { $this->uriData = $uri_data; return $this; } public function getURIMap() { return $this->uriData; } public function getURIData($key, $default = null) { return idx($this->uriData, $key, $default); } /** * Read line range parameter data from the request. * * Applications like Paste, Diffusion, and Harbormaster use "$12-14" in the * URI to allow users to link to particular lines. * * @param string URI data key to pull line range information from. * @param int|null Maximum length of the range. * @return null|pair<int, int> Null, or beginning and end of the range. */ public function getURILineRange($key, $limit) { $range = $this->getURIData($key); return self::parseURILineRange($range, $limit); } public static function parseURILineRange($range, $limit) { - if (!strlen($range)) { + if (!phutil_nonempty_string($range)) { return null; } $range = explode('-', $range, 2); foreach ($range as $key => $value) { $value = (int)$value; if (!$value) { // If either value is "0", discard the range. return null; } $range[$key] = $value; } // If the range is like "$10", treat it like "$10-10". if (count($range) == 1) { $range[] = head($range); } // If the range is "$7-5", treat it like "$5-7". if ($range[1] < $range[0]) { $range = array_reverse($range); } // If the user specified something like "$1-999999999" and we have a limit, // clamp it to a more reasonable range. if ($limit !== null) { if ($range[1] - $range[0] > $limit) { $range[1] = $range[0] + $limit; } } return $range; } public function setApplicationConfiguration( $application_configuration) { $this->applicationConfiguration = $application_configuration; return $this; } public function getApplicationConfiguration() { return $this->applicationConfiguration; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } public function getHost() { // The "Host" header may include a port number, or may be a malicious // header in the form "realdomain.com:ignored@evil.com". Invoke the full // parser to extract the real domain correctly. See here for coverage of // a similar issue in Django: // // https://www.djangoproject.com/weblog/2012/oct/17/security/ $uri = new PhutilURI('http://'.$this->host); return $uri->getDomain(); } public function setSite(AphrontSite $site) { $this->site = $site; return $this; } public function getSite() { return $this->site; } public function setController(AphrontController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } /* -( Accessing Request Data )--------------------------------------------- */ /** * @task data */ public function setRequestData(array $request_data) { $this->requestData = $request_data; return $this; } /** * @task data */ public function getRequestData() { return $this->requestData; } /** * @task data */ public function getInt($name, $default = null) { if (isset($this->requestData[$name])) { // Converting from array to int is "undefined". Don't rely on whatever // PHP decides to do. if (is_array($this->requestData[$name])) { return $default; } return (int)$this->requestData[$name]; } else { return $default; } } /** * @task data */ public function getBool($name, $default = null) { if (isset($this->requestData[$name])) { if ($this->requestData[$name] === 'true') { return true; } else if ($this->requestData[$name] === 'false') { return false; } else { return (bool)$this->requestData[$name]; } } else { return $default; } } /** * @task data */ public function getStr($name, $default = null) { if (isset($this->requestData[$name])) { $str = (string)$this->requestData[$name]; // Normalize newline craziness. $str = str_replace( array("\r\n", "\r"), array("\n", "\n"), $str); return $str; } else { return $default; } } /** * @task data */ public function getJSONMap($name, $default = array()) { if (!isset($this->requestData[$name])) { return $default; } $raw_data = phutil_string_cast($this->requestData[$name]); $raw_data = trim($raw_data); - if (!strlen($raw_data)) { + if (!phutil_nonempty_string($raw_data)) { return $default; } if ($raw_data[0] !== '{') { throw new Exception( pht( 'Request parameter "%s" is not formatted properly. Expected a '. 'JSON object, but value does not start with "{".', $name)); } try { $json_object = phutil_json_decode($raw_data); } catch (PhutilJSONParserException $ex) { throw new Exception( pht( 'Request parameter "%s" is not formatted properly. Expected a '. 'JSON object, but encountered a syntax error: %s.', $name, $ex->getMessage())); } return $json_object; } /** * @task data */ public function getArr($name, $default = array()) { if (isset($this->requestData[$name]) && is_array($this->requestData[$name])) { return $this->requestData[$name]; } else { return $default; } } /** * @task data */ public function getStrList($name, $default = array()) { if (!isset($this->requestData[$name])) { return $default; } $list = $this->getStr($name); $list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY); return $list; } /** * @task data */ public function getExists($name) { return array_key_exists($name, $this->requestData); } public function getFileExists($name) { return isset($_FILES[$name]) && (idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE); } public function isHTTPGet() { return ($_SERVER['REQUEST_METHOD'] == 'GET'); } public function isHTTPPost() { return ($_SERVER['REQUEST_METHOD'] == 'POST'); } public function isAjax() { return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand(); } public function isWorkflow() { return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand(); } public function isQuicksand() { return $this->getExists(self::TYPE_QUICKSAND); } public function isConduit() { return $this->getExists(self::TYPE_CONDUIT); } public static function getCSRFTokenName() { return '__csrf__'; } public static function getCSRFHeaderName() { return 'X-Phabricator-Csrf'; } public static function getViaHeaderName() { return 'X-Phabricator-Via'; } public function validateCSRF() { $token_name = self::getCSRFTokenName(); $token = $this->getStr($token_name); // No token in the request, check the HTTP header which is added for Ajax // requests. if (empty($token)) { $token = self::getHTTPHeader(self::getCSRFHeaderName()); } $valid = $this->getUser()->validateCSRFToken($token); if (!$valid) { // Add some diagnostic details so we can figure out if some CSRF issues // are JS problems or people accessing Ajax URIs directly with their // browsers. $info = array(); $info[] = pht( 'You are trying to save some data to permanent storage, but the '. 'request your browser made included an incorrect token. Reload the '. 'page and try again. You may need to clear your cookies.'); if ($this->isAjax()) { $info[] = pht('This was an Ajax request.'); } else { $info[] = pht('This was a Web request.'); } if ($token) { $info[] = pht('This request had an invalid CSRF token.'); } else { $info[] = pht('This request had no CSRF token.'); } // Give a more detailed explanation of how to avoid the exception // in developer mode. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { // TODO: Clean this up, see T1921. $info[] = pht( "To avoid this error, use %s to construct forms. If you are already ". "using %s, make sure the form 'action' uses a relative URI (i.e., ". "begins with a '%s'). Forms using absolute URIs do not include CSRF ". "tokens, to prevent leaking tokens to external sites.\n\n". "If this page performs writes which do not require CSRF protection ". "(usually, filling caches or logging), you can use %s to ". "temporarily bypass CSRF protection while writing. You should use ". "this only for writes which can not be protected with normal CSRF ". "mechanisms.\n\n". "Some UI elements (like %s) also have methods which will allow you ". "to render links as forms (like %s).", 'phabricator_form()', 'phabricator_form()', '/', 'AphrontWriteGuard::beginScopedUnguardedWrites()', 'PhabricatorActionListView', 'setRenderAsForm(true)'); } $message = implode("\n", $info); // This should only be able to happen if you load a form, pull your // internet for 6 hours, and then reconnect and immediately submit, // but give the user some indication of what happened since the workflow // is incredibly confusing otherwise. throw new AphrontMalformedRequestException( pht('Invalid Request (CSRF)'), $message, true); } return true; } public function isFormPost() { $post = $this->getExists(self::TYPE_FORM) && !$this->getExists(self::TYPE_HISEC) && $this->isHTTPPost(); if (!$post) { return false; } return $this->validateCSRF(); } public function hasCSRF() { try { $this->validateCSRF(); return true; } catch (AphrontMalformedRequestException $ex) { return false; } } public function isFormOrHisecPost() { $post = $this->getExists(self::TYPE_FORM) && $this->isHTTPPost(); if (!$post) { return false; } return $this->validateCSRF(); } public function setCookiePrefix($prefix) { $this->cookiePrefix = $prefix; return $this; } private function getPrefixedCookieName($name) { if (strlen($this->cookiePrefix)) { return $this->cookiePrefix.'_'.$name; } else { return $name; } } public function getCookie($name, $default = null) { $name = $this->getPrefixedCookieName($name); $value = idx($_COOKIE, $name, $default); // Internally, PHP deletes cookies by setting them to the value 'deleted' // with an expiration date in the past. // At least in Safari, the browser may send this cookie anyway in some // circumstances. After logging out, the 302'd GET to /login/ consistently // includes deleted cookies on my local install. If a cookie value is // literally 'deleted', pretend it does not exist. if ($value === 'deleted') { return null; } return $value; } public function clearCookie($name) { $this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30)); unset($_COOKIE[$name]); } /** * Get the domain which cookies should be set on for this request, or null * if the request does not correspond to a valid cookie domain. * * @return PhutilURI|null Domain URI, or null if no valid domain exists. * * @task cookie */ private function getCookieDomainURI() { if (PhabricatorEnv::getEnvConfig('security.require-https') && !$this->isHTTPS()) { return null; } $host = $this->getHost(); // If there's no base domain configured, just use whatever the request // domain is. This makes setup easier, and we'll tell administrators to // configure a base domain during the setup process. $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); - if (!strlen($base_uri)) { + if (!phutil_nonempty_string($base_uri)) { return new PhutilURI('http://'.$host.'/'); } $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris'); $allowed_uris = array_merge( array($base_uri), $alternates); foreach ($allowed_uris as $allowed_uri) { $uri = new PhutilURI($allowed_uri); if ($uri->getDomain() == $host) { return $uri; } } return null; } /** * Determine if security policy rules will allow cookies to be set when * responding to the request. * * @return bool True if setCookie() will succeed. If this method returns * false, setCookie() will throw. * * @task cookie */ public function canSetCookies() { return (bool)$this->getCookieDomainURI(); } /** * Set a cookie which does not expire for a long time. * * To set a temporary cookie, see @{method:setTemporaryCookie}. * * @param string Cookie name. * @param string Cookie value. * @return this * @task cookie */ public function setCookie($name, $value) { $far_future = time() + (60 * 60 * 24 * 365 * 5); return $this->setCookieWithExpiration($name, $value, $far_future); } /** * Set a cookie which expires soon. * * To set a durable cookie, see @{method:setCookie}. * * @param string Cookie name. * @param string Cookie value. * @return this * @task cookie */ public function setTemporaryCookie($name, $value) { return $this->setCookieWithExpiration($name, $value, 0); } /** * Set a cookie with a given expiration policy. * * @param string Cookie name. * @param string Cookie value. * @param int Epoch timestamp for cookie expiration. * @return this * @task cookie */ private function setCookieWithExpiration( $name, $value, $expire) { $is_secure = false; $base_domain_uri = $this->getCookieDomainURI(); if (!$base_domain_uri) { $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $accessed_as = $this->getHost(); throw new AphrontMalformedRequestException( pht('Bad Host Header'), pht( 'This server is configured as "%s", but you are using the domain '. 'name "%s" to access a page which is trying to set a cookie. '. 'Access this service on the configured primary domain or a '. 'configured alternate domain. Cookies will not be set on other '. 'domains for security reasons.', $configured_as, $accessed_as), true); } $base_domain = $base_domain_uri->getDomain(); $is_secure = ($base_domain_uri->getProtocol() == 'https'); $name = $this->getPrefixedCookieName($name); if (php_sapi_name() == 'cli') { // Do nothing, to avoid triggering "Cannot modify header information" // warnings. // TODO: This is effectively a test for whether we're running in a unit // test or not. Move this actual call to HTTPSink? } else { setcookie( $name, $value, $expire, $path = '/', $base_domain, $is_secure, $http_only = true); } $_COOKIE[$name] = $value; return $this; } public function setUser($user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function getViewer() { return $this->user; } public function getRequestURI() { $uri_path = phutil_escape_uri($this->getPath()); $uri_query = idx($_SERVER, 'QUERY_STRING', ''); return id(new PhutilURI($uri_path.'?'.$uri_query)) ->removeQueryParam('__path__'); } public function getAbsoluteRequestURI() { $uri = $this->getRequestURI(); $uri->setDomain($this->getHost()); if ($this->isHTTPS()) { $protocol = 'https'; } else { $protocol = 'http'; } $uri->setProtocol($protocol); // If the request used a nonstandard port, preserve it while building the // absolute URI. // First, get the default port for the request protocol. $default_port = id(new PhutilURI($protocol.'://example.com/')) ->getPortWithProtocolDefault(); // NOTE: See note in getHost() about malicious "Host" headers. This // construction defuses some obscure potential attacks. $port = id(new PhutilURI($protocol.'://'.$this->host)) ->getPort(); if (($port !== null) && ($port !== $default_port)) { $uri->setPort($port); } return $uri; } public function isDialogFormPost() { return $this->isFormPost() && $this->getStr('__dialog__'); } public function getRemoteAddress() { $address = PhabricatorEnv::getRemoteAddress(); if (!$address) { return null; } return $address->getAddress(); } public function isHTTPS() { if (empty($_SERVER['HTTPS'])) { return false; } if (!strcasecmp($_SERVER['HTTPS'], 'off')) { return false; } return true; } public function isContinueRequest() { return $this->isFormOrHisecPost() && $this->getStr('__continue__'); } public function isPreviewRequest() { return $this->isFormPost() && $this->getStr('__preview__'); } /** * Get application request parameters in a flattened form suitable for * inclusion in an HTTP request, excluding parameters with special meanings. * This is primarily useful if you want to ask the user for more input and * then resubmit their request. * * @return dict<string, string> Original request parameters. */ public function getPassthroughRequestParameters($include_quicksand = false) { return self::flattenData( $this->getPassthroughRequestData($include_quicksand)); } /** * Get request data other than "magic" parameters. * * @return dict<string, wild> Request data, with magic filtered out. */ public function getPassthroughRequestData($include_quicksand = false) { $data = $this->getRequestData(); // Remove magic parameters like __dialog__ and __ajax__. foreach ($data as $key => $value) { if ($include_quicksand && $key == self::TYPE_QUICKSAND) { continue; } if (!strncmp($key, '__', 2)) { unset($data[$key]); } } return $data; } /** * Flatten an array of key-value pairs (possibly including arrays as values) * into a list of key-value pairs suitable for submitting via HTTP request * (with arrays flattened). * * @param dict<string, wild> Data to flatten. * @return dict<string, string> Flat data suitable for inclusion in an HTTP * request. */ public static function flattenData(array $data) { $result = array(); foreach ($data as $key => $value) { if (is_array($value)) { foreach (self::flattenData($value) as $fkey => $fvalue) { $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1); $result[$key.$fkey] = $fvalue; } } else { $result[$key] = (string)$value; } } ksort($result); return $result; } /** * Read the value of an HTTP header from `$_SERVER`, or a similar datasource. * * This function accepts a canonical header name, like `"Accept-Encoding"`, * and looks up the appropriate value in `$_SERVER` (in this case, * `"HTTP_ACCEPT_ENCODING"`). * * @param string Canonical header name, like `"Accept-Encoding"`. * @param wild Default value to return if header is not present. * @param array? Read this instead of `$_SERVER`. * @return string|wild Header value if present, or `$default` if not. */ public static function getHTTPHeader($name, $default = null, $data = null) { // PHP mangles HTTP headers by uppercasing them and replacing hyphens with // underscores, then prepending 'HTTP_'. $php_index = strtoupper($name); $php_index = str_replace('-', '_', $php_index); $try_names = array(); $try_names[] = 'HTTP_'.$php_index; if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') { // These headers may be available under alternate names. See // http://www.php.net/manual/en/reserved.variables.server.php#110763 $try_names[] = $php_index; } if ($data === null) { $data = $_SERVER; } foreach ($try_names as $try_name) { if (array_key_exists($try_name, $data)) { return $data[$try_name]; } } return $default; } /* -( Working With a Phabricator Cluster )--------------------------------- */ /** * Is this a proxied request originating from within the Phabricator cluster? * * IMPORTANT: This means the request is dangerous! * * These requests are **more dangerous** than normal requests (they can not * be safely proxied, because proxying them may cause a loop). Cluster * requests are not guaranteed to come from a trusted source, and should * never be treated as safer than normal requests. They are strictly less * safe. */ public function isProxiedClusterRequest() { return (bool)self::getHTTPHeader('X-Phabricator-Cluster'); } /** * Build a new @{class:HTTPSFuture} which proxies this request to another * node in the cluster. * * IMPORTANT: This is very dangerous! * * The future forwards authentication information present in the request. * Proxied requests must only be sent to trusted hosts. (We attempt to * enforce this.) * * This is not a general-purpose proxying method; it is a specialized * method with niche applications and severe security implications. * * @param string URI identifying the host we are proxying the request to. * @return HTTPSFuture New proxy future. * * @phutil-external-symbol class PhabricatorStartup */ public function newClusterProxyFuture($uri) { $uri = new PhutilURI($uri); $domain = $uri->getDomain(); $ip = gethostbyname($domain); if (!$ip) { throw new Exception( pht( 'Unable to resolve domain "%s"!', $domain)); } if (!PhabricatorEnv::isClusterAddress($ip)) { throw new Exception( pht( 'Refusing to proxy a request to IP address ("%s") which is not '. 'in the cluster address block (this address was derived by '. 'resolving the domain "%s").', $ip, $domain)); } $uri->setPath($this->getPath()); $uri->removeAllQueryParams(); foreach (self::flattenData($_GET) as $query_key => $query_value) { $uri->appendQueryParam($query_key, $query_value); } $input = PhabricatorStartup::getRawInput(); $future = id(new HTTPSFuture($uri)) ->addHeader('Host', self::getHost()) ->addHeader('X-Phabricator-Cluster', true) ->setMethod($_SERVER['REQUEST_METHOD']) ->write($input); if (isset($_SERVER['PHP_AUTH_USER'])) { $future->setHTTPBasicAuthCredentials( $_SERVER['PHP_AUTH_USER'], new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', ''))); } $headers = array(); $seen = array(); // NOTE: apache_request_headers() might provide a nicer way to do this, // but isn't available under FCGI until PHP 5.4.0. foreach ($_SERVER as $key => $value) { if (!preg_match('/^HTTP_/', $key)) { continue; } // Unmangle the header as best we can. $key = substr($key, strlen('HTTP_')); $key = str_replace('_', ' ', $key); $key = strtolower($key); $key = ucwords($key); $key = str_replace(' ', '-', $key); // By default, do not forward headers. $should_forward = false; // Forward "X-Hgarg-..." headers. if (preg_match('/^X-Hgarg-/', $key)) { $should_forward = true; } if ($should_forward) { $headers[] = array($key, $value); $seen[$key] = true; } } // In some situations, this may not be mapped into the HTTP_X constants. // CONTENT_LENGTH is similarly affected, but we trust cURL to take care // of that if it matters, since we're handing off a request body. if (empty($seen['Content-Type'])) { if (isset($_SERVER['CONTENT_TYPE'])) { $headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']); } } foreach ($headers as $header) { list($key, $value) = $header; switch ($key) { case 'Host': case 'Authorization': // Don't forward these headers, we've already handled them elsewhere. unset($headers[$key]); break; default: break; } } foreach ($headers as $header) { list($key, $value) = $header; $future->addHeader($key, $value); } return $future; } public function updateEphemeralCookies() { $submit_cookie = PhabricatorCookies::COOKIE_SUBMIT; $submit_key = $this->getCookie($submit_cookie); - if (strlen($submit_key)) { + if (phutil_nonempty_string($submit_key)) { $this->clearCookie($submit_cookie); $this->submitKey = $submit_key; } } public function getSubmitKey() { return $this->submitKey; } } diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 550a5a0316..3bdd2c00bc 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -1,879 +1,879 @@ <?php /** * @task routing URI Routing * @task response Response Handling * @task exception Exception Handling */ final class AphrontApplicationConfiguration extends Phobject { private $request; private $host; private $path; private $console; public function buildRequest() { $parser = new PhutilQueryStringParser(); $data = array(); $data += $_POST; $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', '')); $cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix'); $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($data); $request->setApplicationConfiguration($this); $request->setCookiePrefix($cookie_prefix); $request->updateEphemeralCookies(); return $request; } public function buildRedirectController($uri, $external) { return array( new PhabricatorRedirectController(), array( 'uri' => $uri, 'external' => $external, ), ); } public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function getConsole() { return $this->console; } public function setConsole($console) { $this->console = $console; return $this; } public function setHost($host) { $this->host = $host; return $this; } public function getHost() { return $this->host; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } /** * @phutil-external-symbol class PhabricatorStartup */ public static function runHTTPRequest(AphrontHTTPSink $sink) { if (isset($_SERVER['HTTP_X_SETUP_SELFCHECK'])) { $response = self::newSelfCheckResponse(); return self::writeResponse($sink, $response); } PhabricatorStartup::beginStartupPhase('multimeter'); $multimeter = MultimeterControl::newInstance(); $multimeter->setEventContext('<http-init>'); $multimeter->setEventViewer('<none>'); // Build a no-op write guard for the setup phase. We'll replace this with a // real write guard later on, but we need to survive setup and build a // request object first. $write_guard = new AphrontWriteGuard('id'); PhabricatorStartup::beginStartupPhase('preflight'); $response = PhabricatorSetupCheck::willPreflightRequest(); if ($response) { return self::writeResponse($sink, $response); } PhabricatorStartup::beginStartupPhase('env.init'); self::readHTTPPOSTData(); try { PhabricatorEnv::initializeWebEnvironment(); $database_exception = null; } catch (PhabricatorClusterStrandedException $ex) { $database_exception = $ex; } // If we're in developer mode, set a flag so that top-level exception // handlers can add more information. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $sink->setShowStackTraces(true); } if ($database_exception) { $issue = PhabricatorSetupIssue::newDatabaseConnectionIssue( $database_exception, true); $response = PhabricatorSetupCheck::newIssueResponse($issue); return self::writeResponse($sink, $response); } $multimeter->setSampleRate( PhabricatorEnv::getEnvConfig('debug.sample-rate')); $debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit'); if ($debug_time_limit) { PhabricatorStartup::setDebugTimeLimit($debug_time_limit); } // This is the earliest we can get away with this, we need env config first. PhabricatorStartup::beginStartupPhase('log.access'); PhabricatorAccessLog::init(); $access_log = PhabricatorAccessLog::getLog(); PhabricatorStartup::setAccessLog($access_log); $address = PhabricatorEnv::getRemoteAddress(); if ($address) { $address_string = $address->getAddress(); } else { $address_string = '-'; } $access_log->setData( array( 'R' => AphrontRequest::getHTTPHeader('Referer', '-'), 'r' => $address_string, 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), )); DarkConsoleXHProfPluginAPI::hookProfiler(); // We just activated the profiler, so we don't need to keep track of // startup phases anymore: it can take over from here. PhabricatorStartup::beginStartupPhase('startup.done'); DarkConsoleErrorLogPluginAPI::registerErrorHandler(); $response = PhabricatorSetupCheck::willProcessRequest(); if ($response) { return self::writeResponse($sink, $response); } $host = AphrontRequest::getHTTPHeader('Host'); $path = PhabricatorStartup::getRequestPath(); $application = new self(); $application->setHost($host); $application->setPath($path); $request = $application->buildRequest(); // Now that we have a request, convert the write guard into one which // actually checks CSRF tokens. $write_guard->dispose(); $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); // Build the server URI implied by the request headers. If an administrator // has not configured "phabricator.base-uri" yet, we'll use this to generate // links. $request_protocol = ($request->isHTTPS() ? 'https' : 'http'); $request_base_uri = "{$request_protocol}://{$host}/"; PhabricatorEnv::setRequestBaseURI($request_base_uri); $access_log->setData( array( 'U' => (string)$request->getRequestURI()->getPath(), )); $processing_exception = null; try { $response = $application->processRequest( $request, $access_log, $sink, $multimeter); $response_code = $response->getHTTPResponseCode(); } catch (Exception $ex) { $processing_exception = $ex; $response_code = 500; } $write_guard->dispose(); $access_log->setData( array( 'c' => $response_code, 'T' => PhabricatorStartup::getMicrosecondsSinceStart(), )); $multimeter->newEvent( MultimeterEvent::TYPE_REQUEST_TIME, $multimeter->getEventContext(), PhabricatorStartup::getMicrosecondsSinceStart()); $access_log->write(); $multimeter->saveEvents(); DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log); PhabricatorStartup::disconnectRateLimits( array( 'viewer' => $request->getUser(), )); if ($processing_exception) { throw $processing_exception; } } public function processRequest( AphrontRequest $request, PhutilDeferredLog $access_log, AphrontHTTPSink $sink, MultimeterControl $multimeter) { $this->setRequest($request); list($controller, $uri_data) = $this->buildController(); $controller_class = get_class($controller); $access_log->setData( array( 'C' => $controller_class, )); $multimeter->setEventContext('web.'.$controller_class); $request->setController($controller); $request->setURIMap($uri_data); $controller->setRequest($request); // If execution throws an exception and then trying to render that // exception throws another exception, we want to show the original // exception, as it is likely the root cause of the rendering exception. $original_exception = null; try { $response = $controller->willBeginExecution(); if ($request->getUser() && $request->getUser()->getPHID()) { $access_log->setData( array( 'u' => $request->getUser()->getUserName(), 'P' => $request->getUser()->getPHID(), )); $multimeter->setEventViewer('user.'.$request->getUser()->getPHID()); } if (!$response) { $controller->willProcessRequest($uri_data); $response = $controller->handleRequest($request); $this->validateControllerResponse($controller, $response); } } catch (Exception $ex) { $original_exception = $ex; } catch (Throwable $ex) { $original_exception = $ex; } $response_exception = null; try { if ($original_exception) { $response = $this->handleThrowable($original_exception); } $response = $this->produceResponse($request, $response); $response = $controller->willSendResponse($response); $response->setRequest($request); self::writeResponse($sink, $response); } catch (Exception $ex) { $response_exception = $ex; } catch (Throwable $ex) { $response_exception = $ex; } if ($response_exception) { // If we encountered an exception while building a normal response, then // encountered another exception while building a response for the first // exception, throw an aggregate exception that will be unpacked by the // higher-level handler. This is above our pay grade. if ($original_exception) { throw new PhutilAggregateException( pht( 'Encountered a processing exception, then another exception when '. 'trying to build a response for the first exception.'), array( $response_exception, $original_exception, )); } // If we built a response successfully and then ran into an exception // trying to render it, try to handle and present that exception to the // user using the standard handler. // The problem here might be in rendering (more common) or in the actual // response mechanism (less common). If it's in rendering, we can likely // still render a nice exception page: the majority of rendering issues // are in main page content, not content shared with the exception page. $handling_exception = null; try { $response = $this->handleThrowable($response_exception); $response = $this->produceResponse($request, $response); $response = $controller->willSendResponse($response); $response->setRequest($request); self::writeResponse($sink, $response); } catch (Exception $ex) { $handling_exception = $ex; } catch (Throwable $ex) { $handling_exception = $ex; } // If we didn't have any luck with that, raise the original response // exception. As above, this is the root cause exception and more likely // to be useful. This will go to the fallback error handler at top // level. if ($handling_exception) { throw $response_exception; } } return $response; } private static function writeResponse( AphrontHTTPSink $sink, AphrontResponse $response) { $unexpected_output = PhabricatorStartup::endOutputCapture(); if ($unexpected_output) { $unexpected_output = pht( "Unexpected output:\n\n%s", $unexpected_output); phlog($unexpected_output); if ($response instanceof AphrontWebpageResponse) { $response->setUnexpectedOutput($unexpected_output); } } $sink->writeResponse($response); } /* -( URI Routing )-------------------------------------------------------- */ /** * Build a controller to respond to the request. * * @return pair<AphrontController,dict> Controller and dictionary of request * parameters. * @task routing */ private function buildController() { $request = $this->getRequest(); // If we're configured to operate in cluster mode, reject requests which // were not received on a cluster interface. // // For example, a host may have an internal address like "170.0.0.1", and // also have a public address like "51.23.95.16". Assuming the cluster // is configured on a range like "170.0.0.0/16", we want to reject the // requests received on the public interface. // // Ideally, nodes in a cluster should only be listening on internal // interfaces, but they may be configured in such a way that they also // listen on external interfaces, since this is easy to forget about or // get wrong. As a broad security measure, reject requests received on any // interfaces which aren't on the whitelist. $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses'); if ($cluster_addresses) { $server_addr = idx($_SERVER, 'SERVER_ADDR'); if (!$server_addr) { if (php_sapi_name() == 'cli') { // This is a command line script (probably something like a unit // test) so it's fine that we don't have SERVER_ADDR defined. } else { throw new AphrontMalformedRequestException( pht('No %s', 'SERVER_ADDR'), pht( 'This service is configured to operate in cluster mode, but '. '%s is not defined in the request context. Your webserver '. 'configuration needs to forward %s to PHP so the software can '. 'reject requests received on external interfaces.', 'SERVER_ADDR', 'SERVER_ADDR')); } } else { if (!PhabricatorEnv::isClusterAddress($server_addr)) { throw new AphrontMalformedRequestException( pht('External Interface'), pht( 'This service is configured in cluster mode and the address '. 'this request was received on ("%s") is not whitelisted as '. 'a cluster address.', $server_addr)); } } } $site = $this->buildSiteForRequest($request); if ($site->shouldRequireHTTPS()) { if (!$request->isHTTPS()) { // Don't redirect intracluster requests: doing so drops headers and // parameters, imposes a performance penalty, and indicates a // misconfiguration. if ($request->isProxiedClusterRequest()) { throw new AphrontMalformedRequestException( pht('HTTPS Required'), pht( 'This request reached a site which requires HTTPS, but the '. 'request is not marked as HTTPS.')); } $https_uri = $request->getRequestURI(); $https_uri->setDomain($request->getHost()); $https_uri->setProtocol('https'); // In this scenario, we'll be redirecting to HTTPS using an absolute // URI, so we need to permit an external redirect. return $this->buildRedirectController($https_uri, true); } } $maps = $site->getRoutingMaps(); $path = $request->getPath(); $result = $this->routePath($maps, $path); if ($result) { return $result; } // If we failed to match anything but don't have a trailing slash, try // to add a trailing slash and issue a redirect if that resolves. // NOTE: We only do this for GET, since redirects switch to GET and drop // data like POST parameters. if (!preg_match('@/$@', $path) && $request->isHTTPGet()) { $result = $this->routePath($maps, $path.'/'); if ($result) { $target_uri = $request->getAbsoluteRequestURI(); // We need to restore URI encoding because the webserver has // interpreted it. For example, this allows us to redirect a path // like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be // resolved meaningfully by an application. $target_path = phutil_escape_uri($path.'/'); $target_uri->setPath($target_path); $target_uri = (string)$target_uri; return $this->buildRedirectController($target_uri, true); } } $result = $site->new404Controller($request); if ($result) { return array($result, array()); } throw new Exception( pht( 'Aphront site ("%s") failed to build a 404 controller.', get_class($site))); } /** * Map a specific path to the corresponding controller. For a description * of routing, see @{method:buildController}. * * @param list<AphrontRoutingMap> List of routing maps. * @param string Path to route. * @return pair<AphrontController,dict> Controller and dictionary of request * parameters. * @task routing */ private function routePath(array $maps, $path) { foreach ($maps as $map) { $result = $map->routePath($path); if ($result) { return array($result->getController(), $result->getURIData()); } } } private function buildSiteForRequest(AphrontRequest $request) { $sites = PhabricatorSite::getAllSites(); $site = null; foreach ($sites as $candidate) { $site = $candidate->newSiteForRequest($request); if ($site) { break; } } if (!$site) { $path = $request->getPath(); $host = $request->getHost(); throw new AphrontMalformedRequestException( pht('Site Not Found'), pht( 'This request asked for "%s" on host "%s", but no site is '. 'configured which can serve this request.', $path, $host), true); } $request->setSite($site); return $site; } /* -( Response Handling )-------------------------------------------------- */ /** * Tests if a response is of a valid type. * * @param wild Supposedly valid response. * @return bool True if the object is of a valid type. * @task response */ private function isValidResponseObject($response) { if ($response instanceof AphrontResponse) { return true; } if ($response instanceof AphrontResponseProducerInterface) { return true; } return false; } /** * Verifies that the return value from an @{class:AphrontController} is * of an allowed type. * * @param AphrontController Controller which returned the response. * @param wild Supposedly valid response. * @return void * @task response */ private function validateControllerResponse( AphrontController $controller, $response) { if ($this->isValidResponseObject($response)) { return; } throw new Exception( pht( 'Controller "%s" returned an invalid response from call to "%s". '. 'This method must return an object of class "%s", or an object '. 'which implements the "%s" interface.', get_class($controller), 'handleRequest()', 'AphrontResponse', 'AphrontResponseProducerInterface')); } /** * Verifies that the return value from an * @{class:AphrontResponseProducerInterface} is of an allowed type. * * @param AphrontResponseProducerInterface Object which produced * this response. * @param wild Supposedly valid response. * @return void * @task response */ private function validateProducerResponse( AphrontResponseProducerInterface $producer, $response) { if ($this->isValidResponseObject($response)) { return; } throw new Exception( pht( 'Producer "%s" returned an invalid response from call to "%s". '. 'This method must return an object of class "%s", or an object '. 'which implements the "%s" interface.', get_class($producer), 'produceAphrontResponse()', 'AphrontResponse', 'AphrontResponseProducerInterface')); } /** * Verifies that the return value from an * @{class:AphrontRequestExceptionHandler} is of an allowed type. * * @param AphrontRequestExceptionHandler Object which produced this * response. * @param wild Supposedly valid response. * @return void * @task response */ private function validateErrorHandlerResponse( AphrontRequestExceptionHandler $handler, $response) { if ($this->isValidResponseObject($response)) { return; } throw new Exception( pht( 'Exception handler "%s" returned an invalid response from call to '. '"%s". This method must return an object of class "%s", or an object '. 'which implements the "%s" interface.', get_class($handler), 'handleRequestException()', 'AphrontResponse', 'AphrontResponseProducerInterface')); } /** * Resolves a response object into an @{class:AphrontResponse}. * * Controllers are permitted to return actual responses of class * @{class:AphrontResponse}, or other objects which implement * @{interface:AphrontResponseProducerInterface} and can produce a response. * * If a controller returns a response producer, invoke it now and produce * the real response. * * @param AphrontRequest Request being handled. * @param AphrontResponse|AphrontResponseProducerInterface Response, or * response producer. * @return AphrontResponse Response after any required production. * @task response */ private function produceResponse(AphrontRequest $request, $response) { $original = $response; // Detect cycles on the exact same objects. It's still possible to produce // infinite responses as long as they're all unique, but we can only // reasonably detect cycles, not guarantee that response production halts. $seen = array(); while (true) { // NOTE: It is permissible for an object to be both a response and a // response producer. If so, being a producer is "stronger". This is // used by AphrontProxyResponse. // If this response is a valid response, hand over the request first. if ($response instanceof AphrontResponse) { $response->setRequest($request); } // If this isn't a producer, we're all done. if (!($response instanceof AphrontResponseProducerInterface)) { break; } $hash = spl_object_hash($response); if (isset($seen[$hash])) { throw new Exception( pht( 'Failure while producing response for object of class "%s": '. 'encountered production cycle (identical object, of class "%s", '. 'was produced twice).', get_class($original), get_class($response))); } $seen[$hash] = true; $new_response = $response->produceAphrontResponse(); $this->validateProducerResponse($response, $new_response); $response = $new_response; } return $response; } /* -( Error Handling )----------------------------------------------------- */ /** * Convert an exception which has escaped the controller into a response. * * This method delegates exception handling to available subclasses of * @{class:AphrontRequestExceptionHandler}. * * @param Throwable Exception which needs to be handled. * @return wild Response or response producer, or null if no available * handler can produce a response. * @task exception */ private function handleThrowable($throwable) { $handlers = AphrontRequestExceptionHandler::getAllHandlers(); $request = $this->getRequest(); foreach ($handlers as $handler) { if ($handler->canHandleRequestThrowable($request, $throwable)) { $response = $handler->handleRequestThrowable($request, $throwable); $this->validateErrorHandlerResponse($handler, $response); return $response; } } throw $throwable; } private static function newSelfCheckResponse() { $path = PhabricatorStartup::getRequestPath(); $query = idx($_SERVER, 'QUERY_STRING', ''); $pairs = id(new PhutilQueryStringParser()) ->parseQueryStringToPairList($query); $params = array(); foreach ($pairs as $v) { $params[] = array( 'name' => $v[0], 'value' => $v[1], ); } $raw_input = @file_get_contents('php://input'); if ($raw_input !== false) { $base64_input = base64_encode($raw_input); } else { $base64_input = null; } $result = array( 'path' => $path, 'params' => $params, 'user' => idx($_SERVER, 'PHP_AUTH_USER'), 'pass' => idx($_SERVER, 'PHP_AUTH_PW'), 'raw.base64' => $base64_input, // This just makes sure that the response compresses well, so reasonable // algorithms should want to gzip or deflate it. 'filler' => str_repeat('Q', 1024 * 16), ); return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($result); } private static function readHTTPPOSTData() { $request_method = idx($_SERVER, 'REQUEST_METHOD'); if ($request_method === 'PUT') { // For PUT requests, do nothing: in particular, do NOT read input. This // allows us to stream input later and process very large PUT requests, // like those coming from Git LFS. return; } // For POST requests, we're going to read the raw input ourselves here // if we can. Among other things, this corrects variable names with // the "." character in them, which PHP normally converts into "_". // If "enable_post_data_reading" is on, the documentation suggests we // can not read the body. In practice, we seem to be able to. This may // need to be resolved at some point, likely by instructing installs // to disable this option. // If the content type is "multipart/form-data", we need to build both // $_POST and $_FILES, which is involved. The body itself is also more // difficult to parse than other requests. $raw_input = PhabricatorStartup::getRawInput(); $parser = new PhutilQueryStringParser(); - if (strlen($raw_input)) { + if (phutil_nonempty_string($raw_input)) { $content_type = idx($_SERVER, 'CONTENT_TYPE'); $is_multipart = preg_match('@^multipart/form-data@i', $content_type); if ($is_multipart) { $multipart_parser = id(new AphrontMultipartParser()) ->setContentType($content_type); $multipart_parser->beginParse(); $multipart_parser->continueParse($raw_input); $parts = $multipart_parser->endParse(); // We're building and then parsing a query string so that requests // with arrays (like "x[]=apple&x[]=banana") work correctly. This also // means we can't use "phutil_build_http_querystring()", since it // can't build a query string with duplicate names. $query_string = array(); foreach ($parts as $part) { if (!$part->isVariable()) { continue; } $name = $part->getName(); $value = $part->getVariableValue(); $query_string[] = rawurlencode($name).'='.rawurlencode($value); } $query_string = implode('&', $query_string); $post = $parser->parseQueryString($query_string); $files = array(); foreach ($parts as $part) { if ($part->isVariable()) { continue; } $files[$part->getName()] = $part->getPHPFileDictionary(); } $_FILES = $files; } else { $post = $parser->parseQueryString($raw_input); } $_POST = $post; PhabricatorStartup::rebuildRequest(); } else if ($_POST) { $post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW); if (is_array($post)) { $_POST = $post; PhabricatorStartup::rebuildRequest(); } } } } diff --git a/src/aphront/response/AphrontAjaxResponse.php b/src/aphront/response/AphrontAjaxResponse.php index 2187defc8f..b9e60809fc 100644 --- a/src/aphront/response/AphrontAjaxResponse.php +++ b/src/aphront/response/AphrontAjaxResponse.php @@ -1,89 +1,89 @@ <?php final class AphrontAjaxResponse extends AphrontResponse { private $content; private $error; private $disableConsole; public function setContent($content) { $this->content = $content; return $this; } public function setError($error) { $this->error = $error; return $this; } public function setDisableConsole($disable) { $this->disableConsole = $disable; return $this; } private function getConsole() { if ($this->disableConsole) { $console = null; } else { $request = $this->getRequest(); $console = $request->getApplicationConfiguration()->getConsole(); } return $console; } public function buildResponseString() { $request = $this->getRequest(); $console = $this->getConsole(); if ($console) { // NOTE: We're stripping query parameters here both for readability and // to mitigate BREACH and similar attacks. The parameters are available // in the "Request" tab, so this should not impact usability. See T3684. $path = $request->getPath(); Javelin::initBehavior( 'dark-console', array( 'uri' => $path, 'key' => $console->getKey($request), 'color' => $console->getColor(), 'quicksand' => $request->isQuicksand(), )); } // Flatten the response first, so we initialize any behaviors and metadata // we need to. $content = array( 'payload' => $this->content, ); $this->encodeJSONForHTTPResponse($content); $response = CelerityAPI::getStaticResourceResponse(); if ($request) { $viewer = $request->getViewer(); if ($viewer) { $postprocessor_key = $viewer->getUserSetting( PhabricatorAccessibilitySetting::SETTINGKEY); - if (strlen($postprocessor_key)) { + if (phutil_nonempty_string($postprocessor_key)) { $response->setPostprocessorKey($postprocessor_key); } } } $object = $response->buildAjaxResponse( $content['payload'], $this->error); $response_json = $this->encodeJSONForHTTPResponse($object); return $this->addJSONShield($response_json); } public function getHeaders() { $headers = array( array('Content-Type', 'text/plain; charset=UTF-8'), ); $headers = array_merge(parent::getHeaders(), $headers); return $headers; } } diff --git a/src/aphront/response/AphrontFileResponse.php b/src/aphront/response/AphrontFileResponse.php index 6bae4c808f..980d1ee5dd 100644 --- a/src/aphront/response/AphrontFileResponse.php +++ b/src/aphront/response/AphrontFileResponse.php @@ -1,167 +1,167 @@ <?php final class AphrontFileResponse extends AphrontResponse { private $content; private $contentIterator; private $contentLength; private $compressResponse; private $mimeType; private $download; private $rangeMin; private $rangeMax; private $allowOrigins = array(); public function addAllowOrigin($origin) { $this->allowOrigins[] = $origin; return $this; } public function setDownload($download) { - if (!strlen($download)) { + if (!phutil_nonempty_string($download)) { $download = 'untitled'; } $this->download = $download; return $this; } public function getDownload() { return $this->download; } public function setMimeType($mime_type) { $this->mimeType = $mime_type; return $this; } public function getMimeType() { return $this->mimeType; } public function setContent($content) { $this->setContentLength(strlen($content)); $this->content = $content; return $this; } public function setContentIterator($iterator) { $this->contentIterator = $iterator; return $this; } public function buildResponseString() { return $this->content; } public function getContentIterator() { if ($this->contentIterator) { return $this->contentIterator; } return parent::getContentIterator(); } public function setContentLength($length) { $this->contentLength = $length; return $this; } public function getContentLength() { return $this->contentLength; } public function setCompressResponse($compress_response) { $this->compressResponse = $compress_response; return $this; } public function getCompressResponse() { return $this->compressResponse; } public function setRange($min, $max) { $this->rangeMin = $min; $this->rangeMax = $max; return $this; } public function getHeaders() { $headers = array( array('Content-Type', $this->getMimeType()), // This tells clients that we can support requests with a "Range" header, // which allows downloads to be resumed, in some browsers, some of the // time, if the stars align. array('Accept-Ranges', 'bytes'), ); if ($this->rangeMin !== null || $this->rangeMax !== null) { $len = $this->getContentLength(); $min = $this->rangeMin; $max = $this->rangeMax; if ($max === null) { $max = ($len - 1); } $headers[] = array('Content-Range', "bytes {$min}-{$max}/{$len}"); $content_len = ($max - $min) + 1; } else { $content_len = $this->getContentLength(); } if (!$this->shouldCompressResponse()) { $headers[] = array('Content-Length', $content_len); } if (strlen($this->getDownload())) { $headers[] = array('X-Download-Options', 'noopen'); $filename = $this->getDownload(); $filename = addcslashes($filename, '"\\'); $headers[] = array( 'Content-Disposition', 'attachment; filename="'.$filename.'"', ); } if ($this->allowOrigins) { $headers[] = array( 'Access-Control-Allow-Origin', implode(',', $this->allowOrigins), ); } $headers = array_merge(parent::getHeaders(), $headers); return $headers; } protected function shouldCompressResponse() { return $this->getCompressResponse(); } public function parseHTTPRange($range) { $begin = null; $end = null; $matches = null; if (preg_match('/^bytes=(\d+)-(\d*)$/', $range, $matches)) { // Note that the "Range" header specifies bytes differently than // we do internally: the range 0-1 has 2 bytes (byte 0 and byte 1). $begin = (int)$matches[1]; // The "Range" may be "200-299" or "200-", meaning "until end of file". if (strlen($matches[2])) { $range_end = (int)$matches[2]; $end = $range_end + 1; } else { $range_end = null; } $this->setHTTPResponseCode(206); $this->setRange($begin, $range_end); } return array($begin, $end); } } diff --git a/src/aphront/response/AphrontWebpageResponse.php b/src/aphront/response/AphrontWebpageResponse.php index de642f9b19..00a953ece8 100644 --- a/src/aphront/response/AphrontWebpageResponse.php +++ b/src/aphront/response/AphrontWebpageResponse.php @@ -1,48 +1,48 @@ <?php final class AphrontWebpageResponse extends AphrontHTMLResponse { private $content; private $unexpectedOutput; public function setContent($content) { $this->content = $content; return $this; } public function setUnexpectedOutput($unexpected_output) { $this->unexpectedOutput = $unexpected_output; return $this; } public function getUnexpectedOutput() { return $this->unexpectedOutput; } public function buildResponseString() { $unexpected_output = $this->getUnexpectedOutput(); - if (strlen($unexpected_output)) { + if (phutil_nonempty_string($unexpected_output)) { $style = array( 'background: linear-gradient(180deg, #eeddff, #ddbbff);', 'white-space: pre-wrap;', 'z-index: 200000;', 'position: relative;', 'padding: 16px;', 'font-family: monospace;', 'text-shadow: 1px 1px 1px white;', ); $unexpected_header = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), $unexpected_output); } else { $unexpected_header = ''; } return hsprintf('%s%s', $unexpected_header, $this->content); } } diff --git a/src/aphront/sink/AphrontPHPHTTPSink.php b/src/aphront/sink/AphrontPHPHTTPSink.php index 4953b6e610..1aaa1810ff 100644 --- a/src/aphront/sink/AphrontPHPHTTPSink.php +++ b/src/aphront/sink/AphrontPHPHTTPSink.php @@ -1,36 +1,36 @@ <?php /** * Concrete HTTP sink which uses "echo" and "header()" to emit data. */ final class AphrontPHPHTTPSink extends AphrontHTTPSink { protected function emitHTTPStatus($code, $message = '') { if ($code != 200) { $header = "HTTP/1.0 {$code}"; - if (strlen($message)) { + if (phutil_nonempty_string($message)) { $header .= " {$message}"; } header($header); } } protected function emitHeader($name, $value) { header("{$name}: {$value}", $replace = false); } protected function emitData($data) { echo $data; // NOTE: We don't call flush() here because it breaks HTTPS under Apache. // See T7620 for discussion. Even without an explicit flush, PHP appears to // have reasonable behavior here: the echo will block if internal buffers // are full, and data will be sent to the client once enough of it has // been buffered. } protected function isWritable() { return !connection_aborted(); } } diff --git a/src/aphront/site/AphrontSite.php b/src/aphront/site/AphrontSite.php index 85bf42fe6c..08813462c6 100644 --- a/src/aphront/site/AphrontSite.php +++ b/src/aphront/site/AphrontSite.php @@ -1,44 +1,44 @@ <?php abstract class AphrontSite extends Phobject { abstract public function getPriority(); abstract public function getDescription(); abstract public function shouldRequireHTTPS(); abstract public function newSiteForRequest(AphrontRequest $request); abstract public function getRoutingMaps(); public function new404Controller(AphrontRequest $request) { return new Phabricator404Controller(); } protected function isHostMatch($host, array $uris) { foreach ($uris as $uri) { - if (!strlen($uri)) { + if (!phutil_nonempty_string($uri)) { continue; } $domain = id(new PhutilURI($uri))->getDomain(); if ($domain === $host) { return true; } } return false; } protected function newRoutingMap() { return id(new AphrontRoutingMap()) ->setSite($this); } final public static function getAllSites() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setSortMethod('getPriority') ->execute(); } } diff --git a/src/aphront/site/PhabricatorPlatformSite.php b/src/aphront/site/PhabricatorPlatformSite.php index a3193e65d1..e6c733684b 100644 --- a/src/aphront/site/PhabricatorPlatformSite.php +++ b/src/aphront/site/PhabricatorPlatformSite.php @@ -1,57 +1,57 @@ <?php final class PhabricatorPlatformSite extends PhabricatorSite { public function getDescription() { return pht('Serves the core platform and applications.'); } public function getPriority() { return 1000; } public function newSiteForRequest(AphrontRequest $request) { // If no base URI has been configured yet, match this site so the user // can follow setup instructions. $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); - if (!strlen($base_uri)) { + if (!phutil_nonempty_string($base_uri)) { return new PhabricatorPlatformSite(); } $uris = array(); $uris[] = $base_uri; $uris[] = PhabricatorEnv::getEnvConfig('phabricator.production-uri'); $allowed = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris'); if ($allowed) { foreach ($allowed as $uri) { $uris[] = $uri; } } $host = $request->getHost(); if ($this->isHostMatch($host, $uris)) { return new PhabricatorPlatformSite(); } return null; } public function getRoutingMaps() { $applications = PhabricatorApplication::getAllInstalledApplications(); $maps = array(); foreach ($applications as $application) { $maps[] = $this->newRoutingMap() ->setApplication($application) ->setRoutes($application->getRoutes()); } return $maps; } public function new404Controller(AphrontRequest $request) { return new PhabricatorPlatform404Controller(); } } diff --git a/src/aphront/site/PhabricatorResourceSite.php b/src/aphront/site/PhabricatorResourceSite.php index 88f7777607..3fca474a17 100644 --- a/src/aphront/site/PhabricatorResourceSite.php +++ b/src/aphront/site/PhabricatorResourceSite.php @@ -1,41 +1,41 @@ <?php final class PhabricatorResourceSite extends PhabricatorSite { public function getDescription() { return pht('Serves static resources like images, CSS and JS.'); } public function getPriority() { return 2000; } public function newSiteForRequest(AphrontRequest $request) { $host = $request->getHost(); $uri = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); - if (!strlen($uri)) { + if (!phutil_nonempty_string($uri)) { return null; } if ($this->isHostMatch($host, array($uri))) { return new PhabricatorResourceSite(); } return null; } public function getRoutingMaps() { $applications = PhabricatorApplication::getAllInstalledApplications(); $maps = array(); foreach ($applications as $application) { $maps[] = $this->newRoutingMap() ->setApplication($application) ->setRoutes($application->getResourceRoutes()); } return $maps; } } diff --git a/src/aphront/site/PhabricatorShortSite.php b/src/aphront/site/PhabricatorShortSite.php index d4c36aecbe..05cf2c3584 100644 --- a/src/aphront/site/PhabricatorShortSite.php +++ b/src/aphront/site/PhabricatorShortSite.php @@ -1,44 +1,44 @@ <?php final class PhabricatorShortSite extends PhabricatorSite { public function getDescription() { return pht('Serves shortened URLs.'); } public function getPriority() { return 2500; } public function newSiteForRequest(AphrontRequest $request) { $host = $request->getHost(); $uri = PhabricatorEnv::getEnvConfig('phurl.short-uri'); - if (!strlen($uri)) { + if (!phutil_nonempty_string($uri)) { return null; } $phurl_installed = PhabricatorApplication::isClassInstalled( 'PhabricatorPhurlApplication'); if (!$phurl_installed) { return false; } if ($this->isHostMatch($host, array($uri))) { return new PhabricatorShortSite(); } return null; } public function getRoutingMaps() { $app = PhabricatorApplication::getByClass('PhabricatorPhurlApplication'); $maps = array(); $maps[] = $this->newRoutingMap() ->setApplication($app) ->setRoutes($app->getShortRoutes()); return $maps; } } diff --git a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php index 9247966d75..4a9cfbb8a6 100644 --- a/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php +++ b/src/applications/project/phid/PhabricatorProjectProjectPHIDType.php @@ -1,121 +1,121 @@ <?php final class PhabricatorProjectProjectPHIDType extends PhabricatorPHIDType { const TYPECONST = 'PROJ'; public function getTypeName() { return pht('Project'); } public function getTypeIcon() { return 'fa-briefcase bluegrey'; } public function newObject() { return new PhabricatorProject(); } public function getPHIDTypeApplicationClass() { return 'PhabricatorProjectApplication'; } protected function buildQueryForObjects( PhabricatorObjectQuery $query, array $phids) { return id(new PhabricatorProjectQuery()) ->withPHIDs($phids) ->needImages(true); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { foreach ($handles as $phid => $handle) { $project = $objects[$phid]; $name = $project->getDisplayName(); $id = $project->getID(); $slug = $project->getPrimarySlug(); $handle->setName($name); - if (strlen($slug)) { + if (phutil_nonempty_string($slug)) { $handle->setObjectName('#'.$slug); $handle->setMailStampName('#'.$slug); $handle->setURI("/tag/{$slug}/"); } else { // We set the name to the project's PHID to avoid a parse error when a // project has no hashtag (as is the case with milestones by default). // See T12659 for more details. $handle->setCommandLineObjectName($project->getPHID()); $handle->setURI("/project/view/{$id}/"); } $handle->setImageURI($project->getProfileImageURI()); $handle->setIcon($project->getDisplayIconIcon()); $handle->setTagColor($project->getDisplayColor()); if ($project->isArchived()) { $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); } } } public static function getProjectMonogramPatternFragment() { // NOTE: See some discussion in ProjectRemarkupRule. return '[^\s,#]+'; } public function canLoadNamedObject($name) { $fragment = self::getProjectMonogramPatternFragment(); return preg_match('/^#'.$fragment.'$/i', $name); } public function loadNamedObjects( PhabricatorObjectQuery $query, array $names) { // If the user types "#YoloSwag", we still want to match "#yoloswag", so // we normalize, query, and then map back to the original inputs. $map = array(); foreach ($names as $key => $slug) { $map[$this->normalizeSlug(substr($slug, 1))][] = $slug; } $projects = id(new PhabricatorProjectQuery()) ->setViewer($query->getViewer()) ->withSlugs(array_keys($map)) ->needSlugs(true) ->execute(); $result = array(); foreach ($projects as $project) { $slugs = $project->getSlugs(); $slug_strs = mpull($slugs, 'getSlug'); foreach ($slug_strs as $slug) { $slug_map = idx($map, $slug, array()); foreach ($slug_map as $original) { $result[$original] = $project; } } } return $result; } private function normalizeSlug($slug) { // NOTE: We're using phutil_utf8_strtolower() (and not PhabricatorSlug's // normalize() method) because this normalization should be only somewhat // liberal. We want "#YOLO" to match against "#yolo", but "#\\yo!!lo" // should not. normalize() strips out most punctuation and leads to // excessively aggressive matches. return phutil_utf8_strtolower($slug); } } diff --git a/support/startup/preamble-utils.php b/support/startup/preamble-utils.php index 8dd3b502d6..dfb5619a43 100644 --- a/support/startup/preamble-utils.php +++ b/support/startup/preamble-utils.php @@ -1,77 +1,77 @@ <?php /** * Parse the "X_FORWARDED_FOR" HTTP header to determine the original client * address. * * @param int Number of devices to trust. * @return void */ function preamble_trust_x_forwarded_for_header($layers = 1) { if (!is_int($layers) || ($layers < 1)) { echo 'preamble_trust_x_forwarded_for_header(<layers>): '. '"layers" parameter must an integer larger than 0.'."\n"; echo "\n"; exit(1); } if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { return; } $forwarded_for = $_SERVER['HTTP_X_FORWARDED_FOR']; - if (!strlen($forwarded_for)) { + if (!phutil_nonempty_string($forwarded_for)) { return; } $address = preamble_get_x_forwarded_for_address($forwarded_for, $layers); $_SERVER['REMOTE_ADDR'] = $address; } function preamble_get_x_forwarded_for_address($raw_header, $layers) { // The raw header may be a list of IPs, like "1.2.3.4, 4.5.6.7", if the // request the load balancer received also had this header. In particular, // this happens routinely with requests received through a CDN, but can also // happen illegitimately if the client just makes up an "X-Forwarded-For" // header full of lies. // We can only trust the N elements at the end of the list which correspond // to network-adjacent devices we control. Usually, we're behind a single // load balancer and "N" is 1, so we want to take the last element in the // list. // In some cases, "N" may be more than 1, if the network is configured so // that that requests are routed through multiple layers of load balancers // and proxies. In this case, we want to take the Nth-to-last element of // the list. $addresses = explode(',', $raw_header); // If we have more than one trustworthy device on the network path, discard // corresponding elements from the list. For example, if we have 7 devices, // we want to discard the last 6 elements of the list. // The final device address does not appear in the list, since devices do // not append their own addresses to "X-Forwarded-For". $discard_addresses = ($layers - 1); // However, we don't want to throw away all of the addresses. Some requests // may originate from within the network, and may thus not have as many // addresses as we expect. If we have fewer addresses than trustworthy // devices, discard all but one address. $max_discard = (count($addresses) - 1); $discard_count = min($discard_addresses, $max_discard); if ($discard_count) { $addresses = array_slice($addresses, 0, -$discard_count); } $original_address = end($addresses); $original_address = trim($original_address); return $original_address; }