diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bcc7b83..c26f3bc 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,1147 +1,1149 @@ 2, 'class' => array( 'AASTNode' => 'parser/aast/api/AASTNode.php', 'AASTNodeList' => 'parser/aast/api/AASTNodeList.php', 'AASTToken' => 'parser/aast/api/AASTToken.php', 'AASTTree' => 'parser/aast/api/AASTTree.php', 'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php', 'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php', 'AphrontAccessDeniedQueryException' => 'aphront/storage/exception/AphrontAccessDeniedQueryException.php', 'AphrontBaseMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php', 'AphrontCharacterSetQueryException' => 'aphront/storage/exception/AphrontCharacterSetQueryException.php', 'AphrontConnectionLostQueryException' => 'aphront/storage/exception/AphrontConnectionLostQueryException.php', 'AphrontConnectionQueryException' => 'aphront/storage/exception/AphrontConnectionQueryException.php', 'AphrontCountQueryException' => 'aphront/storage/exception/AphrontCountQueryException.php', 'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php', 'AphrontDatabaseTableRef' => 'xsprintf/AphrontDatabaseTableRef.php', 'AphrontDatabaseTableRefInterface' => 'xsprintf/AphrontDatabaseTableRefInterface.php', 'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php', 'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php', 'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php', 'AphrontHTTPHeaderParser' => 'aphront/headerparser/AphrontHTTPHeaderParser.php', 'AphrontHTTPHeaderParserTestCase' => 'aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php', 'AphrontInvalidCredentialsQueryException' => 'aphront/storage/exception/AphrontInvalidCredentialsQueryException.php', 'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php', 'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php', 'AphrontMultipartParser' => 'aphront/multipartparser/AphrontMultipartParser.php', 'AphrontMultipartParserTestCase' => 'aphront/multipartparser/__tests__/AphrontMultipartParserTestCase.php', 'AphrontMultipartPart' => 'aphront/multipartparser/AphrontMultipartPart.php', 'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php', 'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php', 'AphrontNotSupportedQueryException' => 'aphront/storage/exception/AphrontNotSupportedQueryException.php', 'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php', 'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php', 'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php', 'AphrontQueryTimeoutQueryException' => 'aphront/storage/exception/AphrontQueryTimeoutQueryException.php', 'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php', 'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php', 'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php', 'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php', 'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php', 'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php', 'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php', 'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php', 'CommandException' => 'future/exec/CommandException.php', 'ConduitClient' => 'conduit/ConduitClient.php', 'ConduitClientException' => 'conduit/ConduitClientException.php', 'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php', 'ConduitFuture' => 'conduit/ConduitFuture.php', 'ExecFuture' => 'future/exec/ExecFuture.php', 'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php', 'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php', 'FileFinder' => 'filesystem/FileFinder.php', 'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php', 'FileList' => 'filesystem/FileList.php', 'Filesystem' => 'filesystem/Filesystem.php', 'FilesystemException' => 'filesystem/FilesystemException.php', 'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php', 'Future' => 'future/Future.php', 'FutureIterator' => 'future/FutureIterator.php', 'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php', 'FutureProxy' => 'future/FutureProxy.php', 'HTTPFuture' => 'future/http/HTTPFuture.php', 'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php', 'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php', 'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php', 'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php', 'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php', 'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php', 'HTTPSFuture' => 'future/http/HTTPSFuture.php', 'ImmediateFuture' => 'future/ImmediateFuture.php', 'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.php', 'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php', 'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php', 'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php', 'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php', 'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php', 'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php', 'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php', 'PhageAction' => 'phage/action/PhageAction.php', 'PhageAgentAction' => 'phage/action/PhageAgentAction.php', 'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php', 'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php', 'PhageExecuteAction' => 'phage/action/PhageExecuteAction.php', 'PhageLocalAction' => 'phage/action/PhageLocalAction.php', 'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php', 'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php', 'PhagePlanAction' => 'phage/action/PhagePlanAction.php', 'Phobject' => 'object/Phobject.php', 'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php', 'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php', 'PhutilAWSCloudFormationFuture' => 'future/aws/PhutilAWSCloudFormationFuture.php', 'PhutilAWSCloudWatchFuture' => 'future/aws/PhutilAWSCloudWatchFuture.php', 'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php', 'PhutilAWSException' => 'future/aws/PhutilAWSException.php', 'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php', 'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php', 'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php', 'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php', 'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php', 'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php', 'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php', 'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php', 'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php', 'PhutilAggregateException' => 'error/PhutilAggregateException.php', 'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php', 'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php', 'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php', 'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php', 'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php', 'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php', 'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php', 'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php', 'PhutilArgumentSpellingCorrector' => 'parser/argument/PhutilArgumentSpellingCorrector.php', 'PhutilArgumentSpellingCorrectorTestCase' => 'parser/argument/__tests__/PhutilArgumentSpellingCorrectorTestCase.php', 'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php', 'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php', 'PhutilArray' => 'utils/PhutilArray.php', 'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php', 'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php', 'PhutilAsanaAuthAdapter' => 'auth/PhutilAsanaAuthAdapter.php', 'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php', 'PhutilAuthAdapter' => 'auth/PhutilAuthAdapter.php', 'PhutilAuthConfigurationException' => 'auth/exception/PhutilAuthConfigurationException.php', 'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php', 'PhutilAuthException' => 'auth/exception/PhutilAuthException.php', 'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php', 'PhutilBacktraceSignalHandler' => 'future/exec/PhutilBacktraceSignalHandler.php', 'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php', 'PhutilBinaryAnalyzer' => 'filesystem/binary/PhutilBinaryAnalyzer.php', 'PhutilBinaryAnalyzerTestCase' => 'filesystem/binary/__tests__/PhutilBinaryAnalyzerTestCase.php', 'PhutilBitbucketAuthAdapter' => 'auth/PhutilBitbucketAuthAdapter.php', 'PhutilBootloader' => 'moduleutils/PhutilBootloader.php', 'PhutilBootloaderException' => 'moduleutils/PhutilBootloaderException.php', 'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php', 'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php', 'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php', 'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php', 'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php', 'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php', 'PhutilCIDRList' => 'ip/PhutilCIDRList.php', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php', 'PhutilCalendarAbsoluteDateTime' => 'parser/calendar/data/PhutilCalendarAbsoluteDateTime.php', 'PhutilCalendarContainerNode' => 'parser/calendar/data/PhutilCalendarContainerNode.php', 'PhutilCalendarDateTime' => 'parser/calendar/data/PhutilCalendarDateTime.php', 'PhutilCalendarDateTimeTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarDateTimeTestCase.php', 'PhutilCalendarDocumentNode' => 'parser/calendar/data/PhutilCalendarDocumentNode.php', 'PhutilCalendarDuration' => 'parser/calendar/data/PhutilCalendarDuration.php', 'PhutilCalendarEventNode' => 'parser/calendar/data/PhutilCalendarEventNode.php', 'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php', 'PhutilCalendarProxyDateTime' => 'parser/calendar/data/PhutilCalendarProxyDateTime.php', 'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php', 'PhutilCalendarRecurrenceList' => 'parser/calendar/data/PhutilCalendarRecurrenceList.php', 'PhutilCalendarRecurrenceRule' => 'parser/calendar/data/PhutilCalendarRecurrenceRule.php', 'PhutilCalendarRecurrenceRuleTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php', 'PhutilCalendarRecurrenceSet' => 'parser/calendar/data/PhutilCalendarRecurrenceSet.php', 'PhutilCalendarRecurrenceSource' => 'parser/calendar/data/PhutilCalendarRecurrenceSource.php', 'PhutilCalendarRecurrenceTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php', 'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php', 'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php', 'PhutilCalendarUserNode' => 'parser/calendar/data/PhutilCalendarUserNode.php', 'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php', 'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php', 'PhutilChannel' => 'channel/PhutilChannel.php', 'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php', 'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php', 'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php', 'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php', 'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php', 'PhutilCloudWatchMetric' => 'future/aws/PhutilCloudWatchMetric.php', 'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php', 'PhutilCommandString' => 'xsprintf/PhutilCommandString.php', 'PhutilConsole' => 'console/PhutilConsole.php', 'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php', 'PhutilConsoleError' => 'console/view/PhutilConsoleError.php', 'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php', 'PhutilConsoleInfo' => 'console/view/PhutilConsoleInfo.php', 'PhutilConsoleList' => 'console/view/PhutilConsoleList.php', 'PhutilConsoleLogLine' => 'console/view/PhutilConsoleLogLine.php', 'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php', 'PhutilConsoleMetrics' => 'console/PhutilConsoleMetrics.php', 'PhutilConsoleMetricsSignalHandler' => 'future/exec/PhutilConsoleMetricsSignalHandler.php', 'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php', 'PhutilConsoleServer' => 'console/PhutilConsoleServer.php', 'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php', 'PhutilConsoleSkip' => 'console/view/PhutilConsoleSkip.php', 'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php', 'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php', 'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php', 'PhutilConsoleView' => 'console/view/PhutilConsoleView.php', 'PhutilConsoleWarning' => 'console/view/PhutilConsoleWarning.php', 'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php', 'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php', 'PhutilCowsay' => 'utils/PhutilCowsay.php', 'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php', 'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php', 'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php', 'PhutilDaemon' => 'daemon/PhutilDaemon.php', 'PhutilDaemonHandle' => 'daemon/PhutilDaemonHandle.php', 'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php', 'PhutilDaemonOverseerModule' => 'daemon/PhutilDaemonOverseerModule.php', 'PhutilDaemonPool' => 'daemon/PhutilDaemonPool.php', 'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php', 'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php', 'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php', 'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php', 'PhutilDiffBinaryAnalyzer' => 'filesystem/binary/PhutilDiffBinaryAnalyzer.php', 'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php', 'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php', 'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php', 'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.php', 'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php', 'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php', 'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php', 'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php', 'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php', 'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php', 'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php', 'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php', 'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php', 'PhutilEmojiLocale' => 'internationalization/locales/PhutilEmojiLocale.php', 'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php', 'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php', 'PhutilErrorHandler' => 'error/PhutilErrorHandler.php', 'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php', 'PhutilErrorTrap' => 'error/PhutilErrorTrap.php', 'PhutilEvent' => 'events/PhutilEvent.php', 'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php', 'PhutilEventEngine' => 'events/PhutilEventEngine.php', 'PhutilEventListener' => 'events/PhutilEventListener.php', 'PhutilEventType' => 'events/constant/PhutilEventType.php', 'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php', 'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php', 'PhutilExecChannel' => 'channel/PhutilExecChannel.php', 'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php', 'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php', 'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php', 'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php', 'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php', 'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php', 'PhutilFileLock' => 'filesystem/PhutilFileLock.php', 'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php', 'PhutilFileTree' => 'filesystem/PhutilFileTree.php', 'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php', 'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php', 'PhutilGitBinaryAnalyzer' => 'filesystem/binary/PhutilGitBinaryAnalyzer.php', 'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php', 'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php', 'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php', 'PhutilGitURI' => 'parser/PhutilGitURI.php', 'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php', 'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php', 'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php', 'PhutilHTTPResponse' => 'parser/http/PhutilHTTPResponse.php', 'PhutilHTTPResponseParser' => 'parser/http/PhutilHTTPResponseParser.php', 'PhutilHTTPResponseParserTestCase' => 'parser/http/__tests__/PhutilHTTPResponseParserTestCase.php', 'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php', 'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php', 'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php', 'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php', 'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php', 'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php', 'PhutilICSParser' => 'parser/calendar/ics/PhutilICSParser.php', 'PhutilICSParserException' => 'parser/calendar/ics/PhutilICSParserException.php', 'PhutilICSParserTestCase' => 'parser/calendar/ics/__tests__/PhutilICSParserTestCase.php', 'PhutilICSWriter' => 'parser/calendar/ics/PhutilICSWriter.php', 'PhutilICSWriterTestCase' => 'parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php', 'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php', 'PhutilIPAddress' => 'ip/PhutilIPAddress.php', 'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php', 'PhutilIPv4Address' => 'ip/PhutilIPv4Address.php', 'PhutilIPv6Address' => 'ip/PhutilIPv6Address.php', 'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php', 'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php', 'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php', 'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php', 'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php', 'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php', 'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php', 'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php', 'PhutilJSON' => 'parser/PhutilJSON.php', 'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php', 'PhutilJSONFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php', 'PhutilJSONParser' => 'parser/PhutilJSONParser.php', 'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php', 'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php', 'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php', 'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php', 'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php', 'PhutilJavaCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php', 'PhutilJavaFragmentLexer' => 'lexer/PhutilJavaFragmentLexer.php', 'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php', 'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php', 'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php', 'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php', 'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php', 'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php', 'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php', 'PhutilLDAPAuthAdapter' => 'auth/PhutilLDAPAuthAdapter.php', 'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php', 'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php', 'PhutilLexer' => 'lexer/PhutilLexer.php', 'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php', 'PhutilLibraryConflictException' => 'moduleutils/PhutilLibraryConflictException.php', 'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php', 'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php', 'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php', 'PhutilLocale' => 'internationalization/PhutilLocale.php', 'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php', 'PhutilLock' => 'filesystem/PhutilLock.php', 'PhutilLockException' => 'filesystem/PhutilLockException.php', 'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php', 'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php', 'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php', 'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php', 'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php', 'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php', 'PhutilMercurialBinaryAnalyzer' => 'filesystem/binary/PhutilMercurialBinaryAnalyzer.php', 'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php', 'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php', 'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php', 'PhutilModuleUtilsTestCase' => 'moduleutils/__tests__/PhutilModuleUtilsTestCase.php', 'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php', 'PhutilNumber' => 'internationalization/PhutilNumber.php', 'PhutilOAuth1AuthAdapter' => 'auth/PhutilOAuth1AuthAdapter.php', 'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php', 'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php', 'PhutilOAuthAuthAdapter' => 'auth/PhutilOAuthAuthAdapter.php', 'PhutilOnDiskKeyValueCache' => 'cache/PhutilOnDiskKeyValueCache.php', 'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php', 'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php', 'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php', 'PhutilPHPCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php', 'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php', 'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php', 'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php', 'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php', 'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php', 'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php', 'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php', 'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php', 'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php', 'PhutilPerson' => 'internationalization/PhutilPerson.php', 'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php', 'PhutilPhabricatorAuthAdapter' => 'auth/PhutilPhabricatorAuthAdapter.php', 'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php', 'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php', 'PhutilPortugueseBrazilLocale' => 'internationalization/locales/PhutilPortugueseBrazilLocale.php', 'PhutilPortuguesePortugalLocale' => 'internationalization/locales/PhutilPortuguesePortugalLocale.php', 'PhutilPostmarkFuture' => 'future/postmark/PhutilPostmarkFuture.php', 'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php', 'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php', 'PhutilProseDiff' => 'utils/PhutilProseDiff.php', 'PhutilProseDiffTestCase' => 'utils/__tests__/PhutilProseDiffTestCase.php', 'PhutilProseDifferenceEngine' => 'utils/PhutilProseDifferenceEngine.php', 'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php', 'PhutilProxyException' => 'error/PhutilProxyException.php', 'PhutilProxyIterator' => 'utils/PhutilProxyIterator.php', 'PhutilPygmentizeBinaryAnalyzer' => 'filesystem/binary/PhutilPygmentizeBinaryAnalyzer.php', 'PhutilPygmentizeParser' => 'parser/PhutilPygmentizeParser.php', 'PhutilPygmentizeParserTestCase' => 'parser/__tests__/PhutilPygmentizeParserTestCase.php', 'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php', 'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php', 'PhutilQsprintfInterface' => 'xsprintf/PhutilQsprintfInterface.php', + 'PhutilQueryString' => 'xsprintf/PhutilQueryString.php', 'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php', 'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php', 'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php', 'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php', 'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php', 'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php', 'PhutilRealNameContextFreeGrammar' => 'grammar/PhutilRealNameContextFreeGrammar.php', 'PhutilRemarkupBlockInterpreter' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php', 'PhutilRemarkupBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php', 'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php', 'PhutilRemarkupBoldRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php', 'PhutilRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php', 'PhutilRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php', 'PhutilRemarkupDelRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php', 'PhutilRemarkupDocumentLinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php', 'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php', 'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php', 'PhutilRemarkupEscapeRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php', 'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php', 'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php', 'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php', 'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php', 'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php', 'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php', 'PhutilRemarkupItalicRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php', 'PhutilRemarkupLinebreaksRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php', 'PhutilRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php', 'PhutilRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php', 'PhutilRemarkupMonospaceRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php', 'PhutilRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php', 'PhutilRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php', 'PhutilRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php', 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php', 'PhutilRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php', 'PhutilRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php', 'PhutilRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php', 'PhutilRemarkupUnderlineRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php', 'PhutilRope' => 'utils/PhutilRope.php', 'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php', 'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php', 'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php', 'PhutilSafeHTMLTestCase' => 'markup/__tests__/PhutilSafeHTMLTestCase.php', 'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php', 'PhutilSearchQueryCompiler' => 'search/PhutilSearchQueryCompiler.php', 'PhutilSearchQueryCompilerSyntaxException' => 'search/PhutilSearchQueryCompilerSyntaxException.php', 'PhutilSearchQueryCompilerTestCase' => 'search/__tests__/PhutilSearchQueryCompilerTestCase.php', 'PhutilSearchQueryToken' => 'search/PhutilSearchQueryToken.php', 'PhutilSearchStemmer' => 'search/PhutilSearchStemmer.php', 'PhutilSearchStemmerTestCase' => 'search/__tests__/PhutilSearchStemmerTestCase.php', 'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php', 'PhutilShellLexer' => 'lexer/PhutilShellLexer.php', 'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php', 'PhutilSignalHandler' => 'future/exec/PhutilSignalHandler.php', 'PhutilSignalRouter' => 'future/exec/PhutilSignalRouter.php', 'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php', 'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php', 'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php', 'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php', 'PhutilSimplifiedChineseLocale' => 'internationalization/locales/PhutilSimplifiedChineseLocale.php', 'PhutilSlackAuthAdapter' => 'auth/PhutilSlackAuthAdapter.php', 'PhutilSlackFuture' => 'future/slack/PhutilSlackFuture.php', 'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php', 'PhutilSortVector' => 'utils/PhutilSortVector.php', 'PhutilSpanishSpainLocale' => 'internationalization/locales/PhutilSpanishSpainLocale.php', 'PhutilSprite' => 'sprites/PhutilSprite.php', 'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php', 'PhutilStreamIterator' => 'utils/PhutilStreamIterator.php', 'PhutilSubversionBinaryAnalyzer' => 'filesystem/binary/PhutilSubversionBinaryAnalyzer.php', 'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php', 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php', 'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php', 'PhutilSystem' => 'utils/PhutilSystem.php', 'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php', 'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php', 'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php', 'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php', 'PhutilTraditionalChineseLocale' => 'internationalization/locales/PhutilTraditionalChineseLocale.php', 'PhutilTranslation' => 'internationalization/PhutilTranslation.php', 'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php', 'PhutilTranslator' => 'internationalization/PhutilTranslator.php', 'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php', 'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php', 'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php', 'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php', 'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php', 'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php', 'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php', 'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php', 'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php', 'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php', 'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php', 'PhutilURI' => 'parser/PhutilURI.php', 'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php', 'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php', 'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php', 'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php', 'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php', 'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php', 'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php', 'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php', 'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php', 'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php', 'PhutilWordPressAuthAdapter' => 'auth/PhutilWordPressAuthAdapter.php', 'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php', 'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php', 'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php', 'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php', 'QueryFuture' => 'future/query/QueryFuture.php', 'TempFile' => 'filesystem/TempFile.php', 'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php', 'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php', 'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php', 'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php', 'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php', 'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php', 'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php', 'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php', ), 'function' => array( 'array_fuse' => 'utils/utils.php', 'array_interleave' => 'utils/utils.php', 'array_mergev' => 'utils/utils.php', 'array_select_keys' => 'utils/utils.php', 'assert_instances_of' => 'utils/utils.php', 'assert_stringlike' => 'utils/utils.php', 'coalesce' => 'utils/utils.php', 'csprintf' => 'xsprintf/csprintf.php', 'exec_manual' => 'future/exec/execx.php', 'execx' => 'future/exec/execx.php', 'head' => 'utils/utils.php', 'head_key' => 'utils/utils.php', 'hgsprintf' => 'xsprintf/hgsprintf.php', 'hsprintf' => 'markup/render.php', 'id' => 'utils/utils.php', 'idx' => 'utils/utils.php', 'idxv' => 'utils/utils.php', 'ifilter' => 'utils/utils.php', 'igroup' => 'utils/utils.php', 'ipull' => 'utils/utils.php', 'isort' => 'utils/utils.php', 'jsprintf' => 'xsprintf/jsprintf.php', 'last' => 'utils/utils.php', 'last_key' => 'utils/utils.php', 'ldap_sprintf' => 'xsprintf/ldapsprintf.php', 'mfilter' => 'utils/utils.php', 'mgroup' => 'utils/utils.php', 'mpull' => 'utils/utils.php', 'msort' => 'utils/utils.php', 'msortv' => 'utils/utils.php', 'newv' => 'utils/utils.php', 'nonempty' => 'utils/utils.php', 'phlog' => 'error/phlog.php', 'pht' => 'internationalization/pht.php', 'phutil_censor_credentials' => 'utils/utils.php', 'phutil_console_confirm' => 'console/format.php', 'phutil_console_format' => 'console/format.php', 'phutil_console_get_terminal_width' => 'console/format.php', 'phutil_console_prompt' => 'console/format.php', 'phutil_console_require_tty' => 'console/format.php', 'phutil_console_select' => 'console/format.php', 'phutil_console_wrap' => 'console/format.php', 'phutil_count' => 'internationalization/pht.php', 'phutil_date_format' => 'utils/viewutils.php', 'phutil_deprecated' => 'moduleutils/moduleutils.php', 'phutil_error_listener_example' => 'error/phlog.php', 'phutil_escape_html' => 'markup/render.php', 'phutil_escape_html_newlines' => 'markup/render.php', 'phutil_escape_uri' => 'markup/render.php', 'phutil_escape_uri_path_component' => 'markup/render.php', 'phutil_fnmatch' => 'utils/utils.php', 'phutil_format_bytes' => 'utils/viewutils.php', 'phutil_format_relative_time' => 'utils/viewutils.php', 'phutil_format_relative_time_detailed' => 'utils/viewutils.php', 'phutil_format_units_generic' => 'utils/viewutils.php', 'phutil_fwrite_nonblocking_stream' => 'utils/utils.php', 'phutil_get_current_library_name' => 'moduleutils/moduleutils.php', 'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php', 'phutil_get_library_root' => 'moduleutils/moduleutils.php', 'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php', 'phutil_get_signal_name' => 'future/exec/execx.php', 'phutil_get_system_locale' => 'utils/utf8.php', 'phutil_hashes_are_identical' => 'utils/utils.php', 'phutil_implode_html' => 'markup/render.php', 'phutil_ini_decode' => 'utils/utils.php', 'phutil_is_hiphop_runtime' => 'utils/utils.php', 'phutil_is_system_locale_available' => 'utils/utf8.php', 'phutil_is_utf8' => 'utils/utf8.php', 'phutil_is_utf8_slowly' => 'utils/utf8.php', 'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php', 'phutil_is_windows' => 'utils/utils.php', 'phutil_json_decode' => 'utils/utils.php', 'phutil_json_encode' => 'utils/utils.php', 'phutil_load_library' => 'moduleutils/core.php', 'phutil_loggable_string' => 'utils/utils.php', 'phutil_microseconds_since' => 'utils/utils.php', 'phutil_parse_bytes' => 'utils/viewutils.php', 'phutil_passthru' => 'future/exec/execx.php', 'phutil_person' => 'internationalization/pht.php', 'phutil_register_library' => 'moduleutils/core.php', 'phutil_register_library_map' => 'moduleutils/core.php', 'phutil_safe_html' => 'markup/render.php', 'phutil_set_system_locale' => 'utils/utf8.php', 'phutil_split_lines' => 'utils/utils.php', 'phutil_tag' => 'markup/render.php', 'phutil_tag_div' => 'markup/render.php', 'phutil_unescape_uri_path_component' => 'markup/render.php', 'phutil_units' => 'utils/utils.php', 'phutil_utf8_console_strlen' => 'utils/utf8.php', 'phutil_utf8_convert' => 'utils/utf8.php', 'phutil_utf8_encode_codepoint' => 'utils/utf8.php', 'phutil_utf8_hard_wrap' => 'utils/utf8.php', 'phutil_utf8_hard_wrap_html' => 'utils/utf8.php', 'phutil_utf8_is_cjk' => 'utils/utf8.php', 'phutil_utf8_is_combining_character' => 'utils/utf8.php', 'phutil_utf8_strlen' => 'utils/utf8.php', 'phutil_utf8_strtolower' => 'utils/utf8.php', 'phutil_utf8_strtoupper' => 'utils/utf8.php', 'phutil_utf8_strtr' => 'utils/utf8.php', 'phutil_utf8_ucwords' => 'utils/utf8.php', 'phutil_utf8ize' => 'utils/utf8.php', 'phutil_utf8v' => 'utils/utf8.php', 'phutil_utf8v_codepoints' => 'utils/utf8.php', 'phutil_utf8v_combine_characters' => 'utils/utf8.php', 'phutil_utf8v_combined' => 'utils/utf8.php', 'phutil_validate_json' => 'utils/utils.php', 'phutil_var_export' => 'utils/utils.php', 'ppull' => 'utils/utils.php', 'pregsprintf' => 'xsprintf/pregsprintf.php', 'qsprintf' => 'xsprintf/qsprintf.php', 'qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php', 'qsprintf_check_type' => 'xsprintf/qsprintf.php', 'queryfx' => 'xsprintf/queryfx.php', 'queryfx_all' => 'xsprintf/queryfx.php', 'queryfx_one' => 'xsprintf/queryfx.php', 'tsprintf' => 'xsprintf/tsprintf.php', 'urisprintf' => 'xsprintf/urisprintf.php', 'vcsprintf' => 'xsprintf/csprintf.php', 'vjsprintf' => 'xsprintf/jsprintf.php', 'vqsprintf' => 'xsprintf/qsprintf.php', 'vurisprintf' => 'xsprintf/urisprintf.php', 'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php', 'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php', 'xsprintf' => 'xsprintf/xsprintf.php', 'xsprintf_callback_example' => 'xsprintf/xsprintf.php', 'xsprintf_command' => 'xsprintf/csprintf.php', 'xsprintf_javascript' => 'xsprintf/jsprintf.php', 'xsprintf_ldap' => 'xsprintf/ldapsprintf.php', 'xsprintf_mercurial' => 'xsprintf/hgsprintf.php', 'xsprintf_query' => 'xsprintf/qsprintf.php', 'xsprintf_regex' => 'xsprintf/pregsprintf.php', 'xsprintf_terminal' => 'xsprintf/tsprintf.php', 'xsprintf_uri' => 'xsprintf/urisprintf.php', ), 'xmap' => array( 'AASTNode' => 'Phobject', 'AASTNodeList' => array( 'Phobject', 'Countable', 'Iterator', ), 'AASTToken' => 'Phobject', 'AASTTree' => 'Phobject', 'AbstractDirectedGraph' => 'Phobject', 'AbstractDirectedGraphTestCase' => 'PhutilTestCase', 'AphrontAccessDeniedQueryException' => 'AphrontQueryException', 'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontCharacterSetQueryException' => 'AphrontQueryException', 'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException', 'AphrontConnectionQueryException' => 'AphrontQueryException', 'AphrontCountQueryException' => 'AphrontQueryException', 'AphrontDatabaseConnection' => array( 'Phobject', 'PhutilQsprintfInterface', ), 'AphrontDatabaseTableRef' => array( 'Phobject', 'AphrontDatabaseTableRefInterface', ), 'AphrontDatabaseTransactionState' => 'Phobject', 'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException', 'AphrontDuplicateKeyQueryException' => 'AphrontQueryException', 'AphrontHTTPHeaderParser' => 'Phobject', 'AphrontHTTPHeaderParserTestCase' => 'PhutilTestCase', 'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException', 'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException', 'AphrontMultipartParser' => 'Phobject', 'AphrontMultipartParserTestCase' => 'PhutilTestCase', 'AphrontMultipartPart' => 'Phobject', 'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection', 'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection', 'AphrontNotSupportedQueryException' => 'AphrontQueryException', 'AphrontObjectMissingQueryException' => 'AphrontQueryException', 'AphrontParameterQueryException' => 'AphrontQueryException', 'AphrontQueryException' => 'Exception', 'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException', 'AphrontRecoverableQueryException' => 'AphrontQueryException', 'AphrontRequestStream' => 'Phobject', 'AphrontSchemaQueryException' => 'AphrontQueryException', 'AphrontScopedUnguardedWriteCapability' => 'Phobject', 'AphrontWriteGuard' => 'Phobject', 'BaseHTTPFuture' => 'Future', 'CaseInsensitiveArray' => 'PhutilArray', 'CaseInsensitiveArrayTestCase' => 'PhutilTestCase', 'CommandException' => 'Exception', 'ConduitClient' => 'Phobject', 'ConduitClientException' => 'Exception', 'ConduitClientTestCase' => 'PhutilTestCase', 'ConduitFuture' => 'FutureProxy', 'ExecFuture' => 'PhutilExecutableFuture', 'ExecFutureTestCase' => 'PhutilTestCase', 'ExecPassthruTestCase' => 'PhutilTestCase', 'FileFinder' => 'Phobject', 'FileFinderTestCase' => 'PhutilTestCase', 'FileList' => 'Phobject', 'Filesystem' => 'Phobject', 'FilesystemException' => 'Exception', 'FilesystemTestCase' => 'PhutilTestCase', 'Future' => 'Phobject', 'FutureIterator' => array( 'Phobject', 'Iterator', ), 'FutureIteratorTestCase' => 'PhutilTestCase', 'FutureProxy' => 'Future', 'HTTPFuture' => 'BaseHTTPFuture', 'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatus' => 'Exception', 'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus', 'HTTPSFuture' => 'BaseHTTPFuture', 'ImmediateFuture' => 'Future', 'LibphutilUSEnglishTranslation' => 'PhutilTranslation', 'LinesOfALarge' => array( 'Phobject', 'Iterator', ), 'LinesOfALargeExecFuture' => 'LinesOfALarge', 'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase', 'LinesOfALargeFile' => 'LinesOfALarge', 'LinesOfALargeFileTestCase' => 'PhutilTestCase', 'MFilterTestHelper' => 'Phobject', 'PHPASTParserTestCase' => 'PhutilTestCase', 'PhageAction' => 'Phobject', 'PhageAgentAction' => 'PhageAction', 'PhageAgentBootloader' => 'Phobject', 'PhageAgentTestCase' => 'PhutilTestCase', 'PhageExecuteAction' => 'PhageAction', 'PhageLocalAction' => 'PhageAgentAction', 'PhagePHPAgent' => 'Phobject', 'PhagePHPAgentBootloader' => 'PhageAgentBootloader', 'PhagePlanAction' => 'PhageAction', 'Phobject' => 'Iterator', 'PhobjectTestCase' => 'PhutilTestCase', 'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache', 'PhutilAWSCloudFormationFuture' => 'PhutilAWSFuture', 'PhutilAWSCloudWatchFuture' => 'PhutilAWSFuture', 'PhutilAWSEC2Future' => 'PhutilAWSFuture', 'PhutilAWSException' => 'Exception', 'PhutilAWSFuture' => 'FutureProxy', 'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSS3Future' => 'PhutilAWSFuture', 'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow', 'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', 'PhutilAWSv4Signature' => 'Phobject', 'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase', 'PhutilAggregateException' => 'Exception', 'PhutilAllCapsEnglishLocale' => 'PhutilLocale', 'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilArgumentParser' => 'Phobject', 'PhutilArgumentParserException' => 'Exception', 'PhutilArgumentParserTestCase' => 'PhutilTestCase', 'PhutilArgumentSpecification' => 'Phobject', 'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException', 'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase', 'PhutilArgumentSpellingCorrector' => 'Phobject', 'PhutilArgumentSpellingCorrectorTestCase' => 'PhutilTestCase', 'PhutilArgumentUsageException' => 'PhutilArgumentParserException', 'PhutilArgumentWorkflow' => 'Phobject', 'PhutilArray' => array( 'Phobject', 'Countable', 'ArrayAccess', 'Iterator', ), 'PhutilArrayTestCase' => 'PhutilTestCase', 'PhutilArrayWithDefaultValue' => 'PhutilArray', 'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilAsanaFuture' => 'FutureProxy', 'PhutilAuthAdapter' => 'Phobject', 'PhutilAuthConfigurationException' => 'PhutilAuthException', 'PhutilAuthCredentialException' => 'PhutilAuthException', 'PhutilAuthException' => 'Exception', 'PhutilAuthUserAbortedException' => 'PhutilAuthException', 'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler', 'PhutilBallOfPHP' => 'Phobject', 'PhutilBinaryAnalyzer' => 'Phobject', 'PhutilBinaryAnalyzerTestCase' => 'PhutilTestCase', 'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilBootloaderException' => 'Exception', 'PhutilBritishEnglishLocale' => 'PhutilLocale', 'PhutilBufferedIterator' => array( 'Phobject', 'Iterator', ), 'PhutilBufferedIteratorTestCase' => 'PhutilTestCase', 'PhutilBugtraqParser' => 'Phobject', 'PhutilBugtraqParserTestCase' => 'PhutilTestCase', 'PhutilCIDRBlock' => 'Phobject', 'PhutilCIDRList' => 'Phobject', 'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar', 'PhutilCalendarAbsoluteDateTime' => 'PhutilCalendarDateTime', 'PhutilCalendarContainerNode' => 'PhutilCalendarNode', 'PhutilCalendarDateTime' => 'Phobject', 'PhutilCalendarDateTimeTestCase' => 'PhutilTestCase', 'PhutilCalendarDocumentNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarDuration' => 'Phobject', 'PhutilCalendarEventNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarNode' => 'Phobject', 'PhutilCalendarProxyDateTime' => 'PhutilCalendarDateTime', 'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarRecurrenceList' => 'PhutilCalendarRecurrenceSource', 'PhutilCalendarRecurrenceRule' => 'PhutilCalendarRecurrenceSource', 'PhutilCalendarRecurrenceRuleTestCase' => 'PhutilTestCase', 'PhutilCalendarRecurrenceSet' => 'Phobject', 'PhutilCalendarRecurrenceSource' => 'Phobject', 'PhutilCalendarRecurrenceTestCase' => 'PhutilTestCase', 'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime', 'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode', 'PhutilCalendarUserNode' => 'PhutilCalendarNode', 'PhutilCallbackFilterIterator' => 'FilterIterator', 'PhutilCallbackSignalHandler' => 'PhutilSignalHandler', 'PhutilChannel' => 'Phobject', 'PhutilChannelChannel' => 'PhutilChannel', 'PhutilChannelTestCase' => 'PhutilTestCase', 'PhutilChunkedIterator' => array( 'Phobject', 'Iterator', ), 'PhutilChunkedIteratorTestCase' => 'PhutilTestCase', 'PhutilClassMapQuery' => 'Phobject', 'PhutilCloudWatchMetric' => 'Phobject', 'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilCommandString' => 'Phobject', 'PhutilConsole' => 'Phobject', 'PhutilConsoleBlock' => 'PhutilConsoleView', 'PhutilConsoleError' => 'PhutilConsoleLogLine', 'PhutilConsoleFormatter' => 'Phobject', 'PhutilConsoleInfo' => 'PhutilConsoleLogLine', 'PhutilConsoleList' => 'PhutilConsoleView', 'PhutilConsoleLogLine' => 'PhutilConsoleView', 'PhutilConsoleMessage' => 'Phobject', 'PhutilConsoleMetrics' => 'Phobject', 'PhutilConsoleMetricsSignalHandler' => 'PhutilSignalHandler', 'PhutilConsoleProgressBar' => 'Phobject', 'PhutilConsoleServer' => 'Phobject', 'PhutilConsoleServerChannel' => 'PhutilChannelChannel', 'PhutilConsoleSkip' => 'PhutilConsoleLogLine', 'PhutilConsoleStdinNotInteractiveException' => 'Exception', 'PhutilConsoleSyntaxHighlighter' => 'Phobject', 'PhutilConsoleTable' => 'PhutilConsoleView', 'PhutilConsoleView' => 'Phobject', 'PhutilConsoleWarning' => 'PhutilConsoleLogLine', 'PhutilConsoleWrapTestCase' => 'PhutilTestCase', 'PhutilContextFreeGrammar' => 'Phobject', 'PhutilCowsay' => 'Phobject', 'PhutilCowsayTestCase' => 'PhutilTestCase', 'PhutilCsprintfTestCase' => 'PhutilTestCase', 'PhutilCzechLocale' => 'PhutilLocale', 'PhutilDaemon' => 'Phobject', 'PhutilDaemonHandle' => 'Phobject', 'PhutilDaemonOverseer' => 'Phobject', 'PhutilDaemonOverseerModule' => 'Phobject', 'PhutilDaemonPool' => 'Phobject', 'PhutilDefaultSyntaxHighlighter' => 'Phobject', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase', 'PhutilDeferredLog' => 'Phobject', 'PhutilDeferredLogTestCase' => 'PhutilTestCase', 'PhutilDiffBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph', 'PhutilDirectoryFixture' => 'Phobject', 'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache', 'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilDivinerSyntaxHighlighter' => 'Phobject', 'PhutilDocblockParser' => 'Phobject', 'PhutilDocblockParserTestCase' => 'PhutilTestCase', 'PhutilEditDistanceMatrix' => 'Phobject', 'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase', 'PhutilEditorConfig' => 'Phobject', 'PhutilEditorConfigTestCase' => 'PhutilTestCase', 'PhutilEmailAddress' => 'Phobject', 'PhutilEmailAddressTestCase' => 'PhutilTestCase', 'PhutilEmojiLocale' => 'PhutilLocale', 'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter', 'PhutilEnglishCanadaLocale' => 'PhutilLocale', 'PhutilErrorHandler' => 'Phobject', 'PhutilErrorHandlerTestCase' => 'PhutilTestCase', 'PhutilErrorTrap' => 'Phobject', 'PhutilEvent' => 'Phobject', 'PhutilEventConstants' => 'Phobject', 'PhutilEventEngine' => 'Phobject', 'PhutilEventListener' => 'Phobject', 'PhutilEventType' => 'PhutilEventConstants', 'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator', 'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon', 'PhutilExecChannel' => 'PhutilChannel', 'PhutilExecPassthru' => 'PhutilExecutableFuture', 'PhutilExecutableFuture' => 'Future', 'PhutilExecutionEnvironment' => 'Phobject', 'PhutilExtensionsTestCase' => 'PhutilTestCase', 'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilFatalDaemon' => 'PhutilTortureTestDaemon', 'PhutilFileLock' => 'PhutilLock', 'PhutilFileLockTestCase' => 'PhutilTestCase', 'PhutilFileTree' => 'Phobject', 'PhutilFrenchLocale' => 'PhutilLocale', 'PhutilGermanLocale' => 'PhutilLocale', 'PhutilGitBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilGitHubFuture' => 'FutureProxy', 'PhutilGitHubResponse' => 'Phobject', 'PhutilGitURI' => 'Phobject', 'PhutilGitURITestCase' => 'PhutilTestCase', 'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilHTTPEngineExtension' => 'Phobject', 'PhutilHTTPResponse' => 'Phobject', 'PhutilHTTPResponseParser' => 'Phobject', 'PhutilHTTPResponseParserTestCase' => 'PhutilTestCase', 'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon', 'PhutilHashingIterator' => array( 'PhutilProxyIterator', 'Iterator', ), 'PhutilHashingIteratorTestCase' => 'PhutilTestCase', 'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow', 'PhutilHgsprintfTestCase' => 'PhutilTestCase', 'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon', 'PhutilICSParser' => 'Phobject', 'PhutilICSParserException' => 'Exception', 'PhutilICSParserTestCase' => 'PhutilTestCase', 'PhutilICSWriter' => 'Phobject', 'PhutilICSWriterTestCase' => 'PhutilTestCase', 'PhutilINIParserException' => 'Exception', 'PhutilIPAddress' => 'Phobject', 'PhutilIPAddressTestCase' => 'PhutilTestCase', 'PhutilIPv4Address' => 'PhutilIPAddress', 'PhutilIPv6Address' => 'PhutilIPAddress', 'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache', 'PhutilInteractiveEditor' => 'Phobject', 'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilInvalidStateException' => 'Exception', 'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase', 'PhutilInvisibleSyntaxHighlighter' => 'Phobject', 'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilJSON' => 'Phobject', 'PhutilJSONFragmentLexer' => 'PhutilLexer', 'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase', 'PhutilJSONParser' => 'Phobject', 'PhutilJSONParserException' => 'Exception', 'PhutilJSONParserTestCase' => 'PhutilTestCase', 'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel', 'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase', 'PhutilJSONTestCase' => 'PhutilTestCase', 'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar', 'PhutilJavaFragmentLexer' => 'PhutilLexer', 'PhutilKeyValueCache' => 'Phobject', 'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy', 'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy', 'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache', 'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache', 'PhutilKeyValueCacheTestCase' => 'PhutilTestCase', 'PhutilKoreanLocale' => 'PhutilLocale', 'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter', 'PhutilLanguageGuesser' => 'Phobject', 'PhutilLanguageGuesserTestCase' => 'PhutilTestCase', 'PhutilLexer' => 'Phobject', 'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter', 'PhutilLibraryConflictException' => 'Exception', 'PhutilLibraryMapBuilder' => 'Phobject', 'PhutilLibraryTestCase' => 'PhutilTestCase', 'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilLocale' => 'Phobject', 'PhutilLocaleTestCase' => 'PhutilTestCase', 'PhutilLock' => 'Phobject', 'PhutilLockException' => 'Exception', 'PhutilLogFileChannel' => 'PhutilChannelChannel', 'PhutilLunarPhase' => 'Phobject', 'PhutilLunarPhaseTestCase' => 'PhutilTestCase', 'PhutilMarkupEngine' => 'Phobject', 'PhutilMarkupTestCase' => 'PhutilTestCase', 'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache', 'PhutilMercurialBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilMethodNotImplementedException' => 'Exception', 'PhutilMetricsChannel' => 'PhutilChannelChannel', 'PhutilMissingSymbolException' => 'Exception', 'PhutilModuleUtilsTestCase' => 'PhutilTestCase', 'PhutilNiceDaemon' => 'PhutilTortureTestDaemon', 'PhutilNumber' => 'Phobject', 'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter', 'PhutilOAuth1Future' => 'FutureProxy', 'PhutilOAuth1FutureTestCase' => 'PhutilTestCase', 'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter', 'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache', 'PhutilOpaqueEnvelope' => 'Phobject', 'PhutilOpaqueEnvelopeKey' => 'Phobject', 'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase', 'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar', 'PhutilPHPFragmentLexer' => 'PhutilLexer', 'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase', 'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase', 'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel', 'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase', 'PhutilParserGenerator' => 'Phobject', 'PhutilParserGeneratorException' => 'Exception', 'PhutilParserGeneratorTestCase' => 'PhutilTestCase', 'PhutilPayPalAPIFuture' => 'FutureProxy', 'PhutilPersonTest' => array( 'Phobject', 'PhutilPerson', ), 'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilPhtTestCase' => 'PhutilTestCase', 'PhutilPirateEnglishLocale' => 'PhutilLocale', 'PhutilPortugueseBrazilLocale' => 'PhutilLocale', 'PhutilPortuguesePortugalLocale' => 'PhutilLocale', 'PhutilPostmarkFuture' => 'FutureProxy', 'PhutilPregsprintfTestCase' => 'PhutilTestCase', 'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon', 'PhutilProseDiff' => 'Phobject', 'PhutilProseDiffTestCase' => 'PhutilTestCase', 'PhutilProseDifferenceEngine' => 'Phobject', 'PhutilProtocolChannel' => 'PhutilChannelChannel', 'PhutilProxyException' => 'Exception', 'PhutilProxyIterator' => array( 'Phobject', 'Iterator', ), 'PhutilPygmentizeBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilPygmentizeParser' => 'Phobject', 'PhutilPygmentizeParserTestCase' => 'PhutilTestCase', 'PhutilPygmentsSyntaxHighlighter' => 'Phobject', 'PhutilPythonFragmentLexer' => 'PhutilLexer', + 'PhutilQueryString' => 'Phobject', 'PhutilQueryStringParser' => 'Phobject', 'PhutilQueryStringParserTestCase' => 'PhutilTestCase', 'PhutilRainbowSyntaxHighlighter' => 'Phobject', 'PhutilRawEnglishLocale' => 'PhutilLocale', 'PhutilReadableSerializer' => 'Phobject', 'PhutilReadableSerializerTestCase' => 'PhutilTestCase', 'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhutilRemarkupBlockInterpreter' => 'Phobject', 'PhutilRemarkupBlockRule' => 'Phobject', 'PhutilRemarkupBlockStorage' => 'Phobject', 'PhutilRemarkupBoldRule' => 'PhutilRemarkupRule', 'PhutilRemarkupCodeBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupDefaultBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupDelRule' => 'PhutilRemarkupRule', 'PhutilRemarkupDocumentLinkRule' => 'PhutilRemarkupRule', 'PhutilRemarkupEngine' => 'PhutilMarkupEngine', 'PhutilRemarkupEngineTestCase' => 'PhutilTestCase', 'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule', 'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule', 'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule', 'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule', 'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule', 'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule', 'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupRule' => 'Phobject', 'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter', 'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule', 'PhutilRope' => 'Phobject', 'PhutilRopeTestCase' => 'PhutilTestCase', 'PhutilSafeHTML' => 'Phobject', 'PhutilSafeHTMLTestCase' => 'PhutilTestCase', 'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon', 'PhutilSearchQueryCompiler' => 'Phobject', 'PhutilSearchQueryCompilerSyntaxException' => 'Exception', 'PhutilSearchQueryCompilerTestCase' => 'PhutilTestCase', 'PhutilSearchQueryToken' => 'Phobject', 'PhutilSearchStemmer' => 'Phobject', 'PhutilSearchStemmerTestCase' => 'PhutilTestCase', 'PhutilServiceProfiler' => 'Phobject', 'PhutilShellLexer' => 'PhutilLexer', 'PhutilShellLexerTestCase' => 'PhutilTestCase', 'PhutilSignalHandler' => 'Phobject', 'PhutilSignalRouter' => 'Phobject', 'PhutilSimpleOptions' => 'Phobject', 'PhutilSimpleOptionsLexer' => 'PhutilLexer', 'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase', 'PhutilSimpleOptionsTestCase' => 'PhutilTestCase', 'PhutilSimplifiedChineseLocale' => 'PhutilLocale', 'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilSlackFuture' => 'FutureProxy', 'PhutilSocketChannel' => 'PhutilChannel', 'PhutilSortVector' => 'Phobject', 'PhutilSpanishSpainLocale' => 'PhutilLocale', 'PhutilSprite' => 'Phobject', 'PhutilSpriteSheet' => 'Phobject', 'PhutilStreamIterator' => array( 'Phobject', 'Iterator', ), 'PhutilSubversionBinaryAnalyzer' => 'PhutilBinaryAnalyzer', 'PhutilSyntaxHighlighter' => 'Phobject', 'PhutilSyntaxHighlighterEngine' => 'Phobject', 'PhutilSyntaxHighlighterException' => 'Exception', 'PhutilSystem' => 'Phobject', 'PhutilSystemTestCase' => 'PhutilTestCase', 'PhutilTerminalString' => 'Phobject', 'PhutilTestPhobject' => 'Phobject', 'PhutilTortureTestDaemon' => 'PhutilDaemon', 'PhutilTraditionalChineseLocale' => 'PhutilLocale', 'PhutilTranslation' => 'Phobject', 'PhutilTranslationTestCase' => 'PhutilTestCase', 'PhutilTranslator' => 'Phobject', 'PhutilTranslatorTestCase' => 'PhutilTestCase', 'PhutilTsprintfTestCase' => 'PhutilTestCase', 'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilTwitchFuture' => 'FutureProxy', 'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter', 'PhutilTypeCheckException' => 'Exception', 'PhutilTypeExtraParametersException' => 'Exception', 'PhutilTypeLexer' => 'PhutilLexer', 'PhutilTypeMissingParametersException' => 'Exception', 'PhutilTypeSpec' => 'Phobject', 'PhutilTypeSpecTestCase' => 'PhutilTestCase', 'PhutilURI' => 'Phobject', 'PhutilURITestCase' => 'PhutilTestCase', 'PhutilUSEnglishLocale' => 'PhutilLocale', 'PhutilUTF8StringTruncator' => 'Phobject', 'PhutilUTF8TestCase' => 'PhutilTestCase', 'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException', 'PhutilUrisprintfTestCase' => 'PhutilTestCase', 'PhutilUtilsTestCase' => 'PhutilTestCase', 'PhutilVeryWowEnglishLocale' => 'PhutilLocale', 'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilWordPressFuture' => 'FutureProxy', 'PhutilXHPASTBinary' => 'Phobject', 'PhutilXHPASTSyntaxHighlighter' => 'Phobject', 'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase', 'QueryFuture' => 'Future', 'TempFile' => 'Phobject', 'TestAbstractDirectedGraph' => 'AbstractDirectedGraph', 'XHPASTNode' => 'AASTNode', 'XHPASTNodeTestCase' => 'PhutilTestCase', 'XHPASTSyntaxErrorException' => 'Exception', 'XHPASTToken' => 'AASTToken', 'XHPASTTree' => 'AASTTree', 'XHPASTTreeTestCase' => 'PhutilTestCase', 'XsprintfUnknownConversionException' => 'InvalidArgumentException', ), )); diff --git a/src/aphront/storage/connection/AphrontDatabaseConnection.php b/src/aphront/storage/connection/AphrontDatabaseConnection.php index 49dfb66..b3bd2c8 100644 --- a/src/aphront/storage/connection/AphrontDatabaseConnection.php +++ b/src/aphront/storage/connection/AphrontDatabaseConnection.php @@ -1,284 +1,305 @@ close(); } final public function setLastActiveEpoch($epoch) { $this->lastActiveEpoch = $epoch; return $this; } final public function getLastActiveEpoch() { return $this->lastActiveEpoch; } final public function setPersistent($persistent) { $this->persistent = $persistent; return $this; } final public function getPersistent() { return $this->persistent; } public function queryData($pattern/* , $arg, $arg, ... */) { $args = func_get_args(); array_unshift($args, $this); return call_user_func_array('queryfx_all', $args); } public function query($pattern/* , $arg, $arg, ... */) { $args = func_get_args(); array_unshift($args, $this); return call_user_func_array('queryfx', $args); } public function supportsAsyncQueries() { return false; } public function supportsParallelQueries() { return false; } public function setReadOnly($read_only) { $this->readOnly = $read_only; return $this; } public function getReadOnly() { return $this->readOnly; } public function setQueryTimeout($query_timeout) { $this->queryTimeout = $query_timeout; return $this; } public function getQueryTimeout() { return $this->queryTimeout; } public function asyncQuery($raw_query) { throw new Exception(pht('Async queries are not supported.')); } public static function resolveAsyncQueries(array $conns, array $asyncs) { throw new Exception(pht('Async queries are not supported.')); } + /** + * Is this connection idle and safe to close? + * + * A connection is "idle" if it can be safely closed without loss of state. + * Connections inside a transaction or holding locks are not idle, even + * though they may not actively be executing queries. + * + * @return bool True if the connection is idle and can be safely closed. + */ + public function isIdle() { + if ($this->isInsideTransaction()) { + return false; + } + + if ($this->isHoldingAnyLock()) { + return false; + } + + return true; + } + /* -( Global Locks )------------------------------------------------------- */ public function rememberLock($lock) { if (isset($this->locks[$lock])) { throw new Exception( pht( 'Trying to remember lock "%s", but this lock has already been '. 'remembered.', $lock)); } $this->locks[$lock] = true; return $this; } public function forgetLock($lock) { if (empty($this->locks[$lock])) { throw new Exception( pht( 'Trying to forget lock "%s", but this connection does not remember '. 'that lock.', $lock)); } unset($this->locks[$lock]); return $this; } public function forgetAllLocks() { $this->locks = array(); return $this; } public function isHoldingAnyLock() { return (bool)$this->locks; } /* -( Transaction Management )--------------------------------------------- */ /** * Begin a transaction, or set a savepoint if the connection is already * transactional. * * @return this * @task xaction */ public function openTransaction() { $state = $this->getTransactionState(); $point = $state->getSavepointName(); $depth = $state->getDepth(); $new_transaction = ($depth == 0); if ($new_transaction) { $this->query('START TRANSACTION'); } else { $this->query('SAVEPOINT '.$point); } $state->increaseDepth(); return $this; } /** * Commit a transaction, or stage a savepoint for commit once the entire * transaction completes if inside a transaction stack. * * @return this * @task xaction */ public function saveTransaction() { $state = $this->getTransactionState(); $depth = $state->decreaseDepth(); if ($depth == 0) { $this->query('COMMIT'); } return $this; } /** * Rollback a transaction, or unstage the last savepoint if inside a * transaction stack. * * @return this */ public function killTransaction() { $state = $this->getTransactionState(); $depth = $state->decreaseDepth(); if ($depth == 0) { $this->query('ROLLBACK'); } else { $this->query('ROLLBACK TO SAVEPOINT '.$state->getSavepointName()); } return $this; } /** * Returns true if the connection is transactional. * * @return bool True if the connection is currently transactional. * @task xaction */ public function isInsideTransaction() { $state = $this->getTransactionState(); return ($state->getDepth() > 0); } /** * Get the current @{class:AphrontDatabaseTransactionState} object, or create * one if none exists. * * @return AphrontDatabaseTransactionState Current transaction state. * @task xaction */ protected function getTransactionState() { if (!$this->transactionState) { $this->transactionState = new AphrontDatabaseTransactionState(); } return $this->transactionState; } /** * @task xaction */ public function beginReadLocking() { $this->getTransactionState()->beginReadLocking(); return $this; } /** * @task xaction */ public function endReadLocking() { $this->getTransactionState()->endReadLocking(); return $this; } /** * @task xaction */ public function isReadLocking() { return $this->getTransactionState()->isReadLocking(); } /** * @task xaction */ public function beginWriteLocking() { $this->getTransactionState()->beginWriteLocking(); return $this; } /** * @task xaction */ public function endWriteLocking() { $this->getTransactionState()->endWriteLocking(); return $this; } /** * @task xaction */ public function isWriteLocking() { return $this->getTransactionState()->isWriteLocking(); } } diff --git a/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php b/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php index d44ca14..9638b6f 100644 --- a/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php +++ b/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php @@ -1,129 +1,132 @@ configuration = $configuration; if (self::$nextInsertID === null) { // Generate test IDs into a distant ID space to reduce the risk of // collisions and make them distinctive. self::$nextInsertID = 55555000000 + mt_rand(0, 1000); } } public function openConnection() { return; } public function close() { return; } public function escapeUTF8String($string) { return ''; } public function escapeBinaryString($string) { return ''; } public function escapeColumnName($name) { return ''; } public function escapeMultilineComment($comment) { return ''; } public function escapeStringForLikeClause($value) { return ''; } private function getConfiguration($key, $default = null) { return idx($this->configuration, $key, $default); } public function getInsertID() { return $this->insertID; } public function getAffectedRows() { return $this->affectedRows; } public function selectAllResults() { return $this->allResults; } - public function executeRawQuery($raw_query) { + public function executeQuery(PhutilQueryString $query) { // NOTE: "[\s<>K]*" allows any number of (properly escaped) comments to // appear prior to the allowed keyword, since this connection escapes // them as "" (above). + $display_query = $query->getMaskedString(); + $raw_query = $query->getUnmaskedString(); + $keywords = array( 'INSERT', 'UPDATE', 'DELETE', 'START', 'SAVEPOINT', 'COMMIT', 'ROLLBACK', ); $preg_keywords = array(); foreach ($keywords as $key => $word) { $preg_keywords[] = preg_quote($word, '/'); } $preg_keywords = implode('|', $preg_keywords); if (!preg_match('/^[\s<>K]*('.$preg_keywords.')\s*/i', $raw_query)) { throw new AphrontNotSupportedQueryException( pht( "Database isolation currently only supports some queries. You are ". "trying to issue a query which does not begin with an allowed ". "keyword (%s): '%s'.", implode(', ', $keywords), - $raw_query)); + $display_query)); } - $this->transcript[] = $raw_query; + $this->transcript[] = $display_query; // NOTE: This method is intentionally simplified for now, since we're only // using it to stub out inserts/updates. In the future it will probably need // to grow more powerful. $this->allResults = array(); // NOTE: We jitter the insert IDs to keep tests honest; a test should cover // the relationship between objects, not their exact insertion order. This // guarantees that IDs are unique but makes it impossible to hard-code tests // against this specific implementation detail. self::$nextInsertID += mt_rand(1, 10); $this->insertID = self::$nextInsertID; $this->affectedRows = 1; } public function executeRawQueries(array $raw_queries) { $results = array(); foreach ($raw_queries as $id => $raw_query) { $results[$id] = array(); } return $results; } public function getQueryTranscript() { return $this->transcript; } } diff --git a/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php b/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php index 19b0b8d..c1d0640 100644 --- a/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php +++ b/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php @@ -1,384 +1,387 @@ configuration = $configuration; } public function __clone() { $this->establishConnection(); } public function openConnection() { $this->requireConnection(); } public function close() { if ($this->lastResult) { $this->lastResult = null; } if ($this->connection) { $this->closeConnection(); $this->connection = null; } } public function escapeColumnName($name) { return '`'.str_replace('`', '``', $name).'`'; } public function escapeMultilineComment($comment) { // These can either terminate a comment, confuse the hell out of the parser, // make MySQL execute the comment as a query, or, in the case of semicolon, // are quasi-dangerous because the semicolon could turn a broken query into // a working query plus an ignored query. static $map = array( '--' => '(DOUBLEDASH)', '*/' => '(STARSLASH)', '//' => '(SLASHSLASH)', '#' => '(HASH)', '!' => '(BANG)', ';' => '(SEMICOLON)', ); $comment = str_replace( array_keys($map), array_values($map), $comment); // For good measure, kill anything else that isn't a nice printable // character. $comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment); return '/* '.$comment.' */'; } public function escapeStringForLikeClause($value) { $value = addcslashes($value, '\%_'); $value = $this->escapeUTF8String($value); return $value; } protected function getConfiguration($key, $default = null) { return idx($this->configuration, $key, $default); } private function establishConnection() { $host = $this->getConfiguration('host'); $database = $this->getConfiguration('database'); $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall( array( 'type' => 'connect', 'host' => $host, 'database' => $database, )); $retries = max(1, $this->getConfiguration('retries', 3)); while ($retries--) { try { $conn = $this->connect(); $profiler->endServiceCall($call_id, array()); break; } catch (AphrontQueryException $ex) { if ($retries && $ex->getCode() == 2003) { $class = get_class($ex); $message = $ex->getMessage(); phlog(pht('Retrying (%d) after %s: %s', $retries, $class, $message)); } else { $profiler->endServiceCall($call_id, array()); throw $ex; } } } $this->connection = $conn; } protected function requireConnection() { if (!$this->connection) { if ($this->connectionPool) { $this->connection = array_pop($this->connectionPool); } else { $this->establishConnection(); } } return $this->connection; } protected function beginAsyncConnection() { $connection = $this->requireConnection(); $this->connection = null; return $connection; } protected function endAsyncConnection($connection) { if ($this->connection) { $this->connectionPool[] = $this->connection; } $this->connection = $connection; } public function selectAllResults() { $result = array(); $res = $this->lastResult; if ($res == null) { throw new Exception(pht('No query result to fetch from!')); } while (($row = $this->fetchAssoc($res))) { $result[] = $row; } return $result; } - public function executeRawQuery($raw_query) { + public function executeQuery(PhutilQueryString $query) { + $display_query = $query->getMaskedString(); + $raw_query = $query->getUnmaskedString(); + $this->lastResult = null; $retries = max(1, $this->getConfiguration('retries', 3)); while ($retries--) { try { $this->requireConnection(); $is_write = $this->checkWrite($raw_query); $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall( array( 'type' => 'query', 'config' => $this->configuration, - 'query' => $raw_query, + 'query' => $display_query, 'write' => $is_write, )); $result = $this->rawQuery($raw_query); $profiler->endServiceCall($call_id, array()); if ($this->nextError) { $result = null; } if ($result) { $this->lastResult = $result; break; } $this->throwQueryException($this->connection); } catch (AphrontConnectionLostQueryException $ex) { $can_retry = ($retries > 0); if ($this->isInsideTransaction()) { // Zero out the transaction state to prevent a second exception // ("program exited with open transaction") from being thrown, since // we're about to throw a more relevant/useful one instead. $state = $this->getTransactionState(); while ($state->getDepth()) { $state->decreaseDepth(); } $can_retry = false; } if ($this->isHoldingAnyLock()) { $this->forgetAllLocks(); $can_retry = false; } $this->close(); if (!$can_retry) { throw $ex; } } } } public function executeRawQueries(array $raw_queries) { if (!$raw_queries) { return array(); } $is_write = false; foreach ($raw_queries as $key => $raw_query) { $is_write = $is_write || $this->checkWrite($raw_query); $raw_queries[$key] = rtrim($raw_query, "\r\n\t ;"); } $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall( array( 'type' => 'multi-query', 'config' => $this->configuration, 'queries' => $raw_queries, 'write' => $is_write, )); $results = $this->rawQueries($raw_queries); $profiler->endServiceCall($call_id, array()); return $results; } protected function processResult($result) { if (!$result) { try { $this->throwQueryException($this->requireConnection()); } catch (Exception $ex) { return $ex; } } else if (is_bool($result)) { return $this->getAffectedRows(); } $rows = array(); while (($row = $this->fetchAssoc($result))) { $rows[] = $row; } $this->freeResult($result); return $rows; } protected function checkWrite($raw_query) { // NOTE: The opening "(" allows queries in the form of: // // (SELECT ...) UNION (SELECT ...) $is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\s/', $raw_query); if ($is_write) { if ($this->getReadOnly()) { throw new Exception( pht( 'Attempting to issue a write query on a read-only '. 'connection (to database "%s")!', $this->getConfiguration('database'))); } AphrontWriteGuard::willWrite(); return true; } return false; } protected function throwQueryException($connection) { if ($this->nextError) { $errno = $this->nextError; $error = pht('Simulated error.'); $this->nextError = null; } else { $errno = $this->getErrorCode($connection); $error = $this->getErrorDescription($connection); } $this->throwQueryCodeException($errno, $error); } private function throwCommonException($errno, $error) { $message = pht('#%d: %s', $errno, $error); switch ($errno) { case 2013: // Connection Dropped throw new AphrontConnectionLostQueryException($message); case 2006: // Gone Away $more = pht( 'This error may occur if your configured MySQL "wait_timeout" or '. '"max_allowed_packet" values are too small. This may also indicate '. 'that something used the MySQL "KILL " command to kill '. 'the connection running the query.'); throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}"); case 1213: // Deadlock throw new AphrontDeadlockQueryException($message); case 1205: // Lock wait timeout exceeded throw new AphrontLockTimeoutQueryException($message); case 1062: // Duplicate Key // NOTE: In some versions of MySQL we get a key name back here, but // older versions just give us a key index ("key 2") so it's not // portable to parse the key out of the error and attach it to the // exception. throw new AphrontDuplicateKeyQueryException($message); case 1044: // Access denied to database case 1142: // Access denied to table case 1143: // Access denied to column case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS). throw new AphrontAccessDeniedQueryException($message); case 1045: // Access denied (auth) throw new AphrontInvalidCredentialsQueryException($message); case 1146: // No such table case 1049: // No such database case 1054: // Unknown column "..." in field list throw new AphrontSchemaQueryException($message); } // TODO: 1064 is syntax error, and quite terrible in production. return null; } protected function throwConnectionException($errno, $error, $user, $host) { $this->throwCommonException($errno, $error); $message = pht( 'Attempt to connect to %s@%s failed with error #%d: %s.', $user, $host, $errno, $error); throw new AphrontConnectionQueryException($message, $errno); } protected function throwQueryCodeException($errno, $error) { $this->throwCommonException($errno, $error); $message = pht( '#%d: %s', $errno, $error); throw new AphrontQueryException($message, $errno); } /** * Force the next query to fail with a simulated error. This should be used * ONLY for unit tests. */ public function simulateErrorOnNextQuery($error) { $this->nextError = $error; return $this; } /** * Check inserts for characters outside of the BMP. Even with the strictest * settings, MySQL will silently truncate data when it encounters these, which * can lead to data loss and security problems. */ protected function validateUTF8String($string) { if (phutil_is_utf8($string)) { return; } throw new AphrontCharacterSetQueryException( pht( 'Attempting to construct a query using a non-utf8 string when '. 'utf8 is expected. Use the `%%B` conversion to escape binary '. 'strings data.')); } } diff --git a/src/future/exec/ExecFuture.php b/src/future/exec/ExecFuture.php index 1fbec1d..5e0f006 100644 --- a/src/future/exec/ExecFuture.php +++ b/src/future/exec/ExecFuture.php @@ -1,965 +1,976 @@ array('pipe', 'r'), // stdin 1 => array('pipe', 'w'), // stdout 2 => array('pipe', 'w'), // stderr ); /* -( Creating ExecFutures )----------------------------------------------- */ /** * Create a new ExecFuture. * * $future = new ExecFuture('wc -l %s', $file_path); * * @param string `sprintf()`-style command string which will be passed * through @{function:csprintf} with the rest of the arguments. * @param ... Zero or more additional arguments for @{function:csprintf}. * @return ExecFuture ExecFuture for running the specified command. * @task create */ public function __construct($command) { $argv = func_get_args(); $this->command = call_user_func_array('csprintf', $argv); $this->stdin = new PhutilRope(); } /* -( Command Information )------------------------------------------------ */ /** * Retrieve the raw command to be executed. * * @return string Raw command. * @task info */ public function getCommand() { return $this->command; } /** * Retrieve the byte limit for the stderr buffer. * * @return int Maximum buffer size, in bytes. * @task info */ public function getStderrSizeLimit() { return $this->stderrSizeLimit; } /** * Retrieve the byte limit for the stdout buffer. * * @return int Maximum buffer size, in bytes. * @task info */ public function getStdoutSizeLimit() { return $this->stdoutSizeLimit; } /** * Get the process's pid. This only works after execution is initiated, e.g. * by a call to start(). * * @return int Process ID of the executing process. * @task info */ public function getPID() { $status = $this->procGetStatus(); return $status['pid']; } /* -( Configuring Execution )---------------------------------------------- */ /** * Set a maximum size for the stdout read buffer. To limit stderr, see * @{method:setStderrSizeLimit}. The major use of these methods is to use less * memory if you are running a command which sometimes produces huge volumes * of output that you don't really care about. * * NOTE: Setting this to 0 means "no buffer", not "unlimited buffer". * * @param int Maximum size of the stdout read buffer. * @return this * @task config */ public function setStdoutSizeLimit($limit) { $this->stdoutSizeLimit = $limit; return $this; } /** * Set a maximum size for the stderr read buffer. * See @{method:setStdoutSizeLimit} for discussion. * * @param int Maximum size of the stderr read buffer. * @return this * @task config */ public function setStderrSizeLimit($limit) { $this->stderrSizeLimit = $limit; return $this; } /** * Set the maximum internal read buffer size this future. The future will * block reads once the internal stdout or stderr buffer exceeds this size. * * NOTE: If you @{method:resolve} a future with a read buffer limit, you may * block forever! * * TODO: We should probably release the read buffer limit during * @{method:resolve}, or otherwise detect this. For now, be careful. * * @param int|null Maximum buffer size, or `null` for unlimited. * @return this */ public function setReadBufferSize($read_buffer_size) { $this->readBufferSize = $read_buffer_size; return $this; } /** * Set whether to use non-blocking streams on Windows. * * @param bool Whether to use non-blocking streams. * @return this * @task config */ public function setUseWindowsFileStreams($use_streams) { if (phutil_is_windows()) { $this->useWindowsFileStreams = $use_streams; } return $this; } /* -( Interacting With Commands )------------------------------------------ */ /** * Read and return output from stdout and stderr, if any is available. This * method keeps a read cursor on each stream, but the entire streams are * still returned when the future resolves. You can call read() again after * resolving the future to retrieve only the parts of the streams you did not * previously read: * * $future = new ExecFuture('...'); * // ... * list($stdout) = $future->read(); // Returns output so far * list($stdout) = $future->read(); // Returns new output since first call * // ... * list($stdout) = $future->resolvex(); // Returns ALL output * list($stdout) = $future->read(); // Returns unread output * * NOTE: If you set a limit with @{method:setStdoutSizeLimit} or * @{method:setStderrSizeLimit}, this method will not be able to read data * past the limit. * * NOTE: If you call @{method:discardBuffers}, all the stdout/stderr data * will be thrown away and the cursors will be reset. * * @return pair <$stdout, $stderr> pair with new output since the last call * to this method. * @task interact */ public function read() { $stdout = $this->readStdout(); $result = array( $stdout, (string)substr($this->stderr, $this->stderrPos), ); $this->stderrPos = strlen($this->stderr); return $result; } public function readStdout() { if ($this->start) { $this->isReady(); // Sync } $result = (string)substr($this->stdout, $this->stdoutPos); $this->stdoutPos = strlen($this->stdout); return $result; } /** * Write data to stdin of the command. * * @param string Data to write. * @param bool If true, keep the pipe open for writing. By default, the pipe * will be closed as soon as possible so that commands which * listen for EOF will execute. If you want to keep the pipe open * past the start of command execution, do an empty write with * `$keep_pipe = true` first. * @return this * @task interact */ public function write($data, $keep_pipe = false) { if (strlen($data)) { if (!$this->stdin) { throw new Exception(pht('Writing to a closed pipe!')); } $this->stdin->append($data); } $this->closePipe = !$keep_pipe; return $this; } /** * Permanently discard the stdout and stderr buffers and reset the read * cursors. This is basically useful only if you are streaming a large amount * of data from some process: * * $future = new ExecFuture('zcat huge_file.gz'); * do { * $done = $future->resolve(0.1); // Every 100ms, * list($stdout) = $future->read(); // read output... * echo $stdout; // send it somewhere... * $future->discardBuffers(); // and then free the buffers. * } while ($done === null); * * Conceivably you might also need to do this if you're writing a client using * @{class:ExecFuture} and `netcat`, but you probably should not do that. * * NOTE: This completely discards the data. It won't be available when the * future resolves. This is almost certainly only useful if you need the * buffer memory for some reason. * * @return this * @task interact */ public function discardBuffers() { $this->discardStdoutBuffer(); $this->stderr = ''; $this->stderrPos = 0; return $this; } public function discardStdoutBuffer() { $this->stdout = ''; $this->stdoutPos = 0; return $this; } /** * Returns true if this future was killed by a timeout configured with * @{method:setTimeout}. * * @return bool True if the future was killed for exceeding its time limit. */ public function getWasKilledByTimeout() { return $this->killedByTimeout; } /* -( Configuring Execution )---------------------------------------------- */ /** * Set a hard limit on execution time. If the command runs longer, it will * be terminated and the future will resolve with an error code. You can test * if a future was killed by a timeout with @{method:getWasKilledByTimeout}. * * The subprocess will be sent a `TERM` signal, and then a `KILL` signal a * short while later if it fails to exit. * * @param int Maximum number of seconds this command may execute for before * it is signaled. * @return this * @task config */ public function setTimeout($seconds) { $this->terminateTimeout = $seconds; $this->killTimeout = $seconds + min($seconds, 60); return $this; } /* -( Resolving Execution )------------------------------------------------ */ /** * Resolve a command you expect to exit with return code 0. Works like * @{method:resolve}, but throws if $err is nonempty. Returns only * $stdout and $stderr. See also @{function:execx}. * * list($stdout, $stderr) = $future->resolvex(); * * @param float Optional timeout after which resolution will pause and * execution will return to the caller. * @return pair <$stdout, $stderr> pair. * @task resolve */ public function resolvex($timeout = null) { list($err, $stdout, $stderr) = $this->resolve($timeout); if ($err) { $cmd = $this->command; + + if ($this->getWasKilledByTimeout()) { + // NOTE: The timeout can be a float and PhutilNumber only handles + // integers, so just use "%s" to render it. + $message = pht( + 'Command killed by timeout after running for more than %s seconds.', + $this->terminateTimeout); + } else { + $message = pht('Command failed with error #%d!', $err); + } + throw new CommandException( - pht('Command failed with error #%d!', $err), + $message, $cmd, $err, $stdout, $stderr); } return array($stdout, $stderr); } /** * Resolve a command you expect to return valid JSON. Works like * @{method:resolvex}, but also throws if stderr is nonempty, or stdout is not * valid JSON. Returns a PHP array, decoded from the JSON command output. * * @param float Optional timeout after which resolution will pause and * execution will return to the caller. * @return array PHP array, decoded from JSON command output. * @task resolve */ public function resolveJSON($timeout = null) { list($stdout, $stderr) = $this->resolvex($timeout); if (strlen($stderr)) { $cmd = $this->command; throw new CommandException( pht( "JSON command '%s' emitted text to stderr when none was expected: %d", $cmd, $stderr), $cmd, 0, $stdout, $stderr); } try { return phutil_json_decode($stdout); } catch (PhutilJSONParserException $ex) { $cmd = $this->command; throw new CommandException( pht( "JSON command '%s' did not produce a valid JSON object on stdout: %s", $cmd, $stdout), $cmd, 0, $stdout, $stderr); } } /** * Resolve the process by abruptly terminating it. * * @return list List of results. * @task resolve */ public function resolveKill() { if (!$this->result) { $signal = 9; if ($this->proc) { proc_terminate($this->proc, $signal); } $this->result = array( 128 + $signal, $this->stdout, $this->stderr, ); $this->closeProcess(); } return $this->result; } /* -( Internals )---------------------------------------------------------- */ /** * Provides read sockets to the future core. * * @return list List of read sockets. * @task internal */ public function getReadSockets() { list($stdin, $stdout, $stderr) = $this->pipes; $sockets = array(); if (isset($stdout) && !feof($stdout)) { $sockets[] = $stdout; } if (isset($stderr) && !feof($stderr)) { $sockets[] = $stderr; } return $sockets; } /** * Provides write sockets to the future core. * * @return list List of write sockets. * @task internal */ public function getWriteSockets() { list($stdin, $stdout, $stderr) = $this->pipes; $sockets = array(); if (isset($stdin) && $this->stdin->getByteLength() && !feof($stdin)) { $sockets[] = $stdin; } return $sockets; } /** * Determine if the read buffer is empty. * * @return bool True if the read buffer is empty. * @task internal */ public function isReadBufferEmpty() { return !strlen($this->stdout); } /** * Determine if the write buffer is empty. * * @return bool True if the write buffer is empty. * @task internal */ public function isWriteBufferEmpty() { return !$this->getWriteBufferSize(); } /** * Determine the number of bytes in the write buffer. * * @return int Number of bytes in the write buffer. * @task internal */ public function getWriteBufferSize() { if (!$this->stdin) { return 0; } return $this->stdin->getByteLength(); } /** * Reads some bytes from a stream, discarding output once a certain amount * has been accumulated. * * @param resource Stream to read from. * @param int Maximum number of bytes to return from $stream. If * additional bytes are available, they will be read and * discarded. * @param string Human-readable description of stream, for exception * message. * @param int Maximum number of bytes to read. * @return string The data read from the stream. * @task internal */ private function readAndDiscard($stream, $limit, $description, $length) { $output = ''; if ($length <= 0) { return ''; } do { $data = fread($stream, min($length, 64 * 1024)); if (false === $data) { throw new Exception(pht('Failed to read from %s', $description)); } $read_bytes = strlen($data); if ($read_bytes > 0 && $limit > 0) { if ($read_bytes > $limit) { $data = substr($data, 0, $limit); } $output .= $data; $limit -= strlen($data); } if (strlen($output) >= $length) { break; } } while ($read_bytes > 0); return $output; } /** * Begin or continue command execution. * * @return bool True if future has resolved. * @task internal */ public function isReady() { // NOTE: We have soft dependencies on PhutilServiceProfiler and // PhutilErrorTrap here. These dependencies are soft to avoid the need to // build them into the Phage agent. Under normal circumstances, these // classes are always available. if (!$this->pipes) { // NOTE: See note above about Phage. if (class_exists('PhutilServiceProfiler')) { $profiler = PhutilServiceProfiler::getInstance(); $this->profilerCallID = $profiler->beginServiceCall( array( 'type' => 'exec', 'command' => (string)$this->command, )); } if (!$this->start) { // We might already have started the timer via initiating resolution. $this->start = microtime(true); } $unmasked_command = $this->command; if ($unmasked_command instanceof PhutilCommandString) { $unmasked_command = $unmasked_command->getUnmaskedString(); } $pipes = array(); if (phutil_is_windows()) { // See T4395. proc_open under Windows uses "cmd /C [cmd]", which will // strip the first and last quote when there aren't exactly two quotes // (and some other conditions as well). This results in a command that // looks like `command" "path to my file" "something something` which is // clearly wrong. By surrounding the command string with quotes we can // be sure this process is harmless. if (strpos($unmasked_command, '"') !== false) { $unmasked_command = '"'.$unmasked_command.'"'; } } if ($this->hasEnv()) { $env = $this->getEnv(); } else { $env = null; } $cwd = $this->getCWD(); // NOTE: See note above about Phage. if (class_exists('PhutilErrorTrap')) { $trap = new PhutilErrorTrap(); } else { $trap = null; } $spec = self::$descriptorSpec; if ($this->useWindowsFileStreams) { $this->windowsStdoutTempFile = new TempFile(); $this->windowsStderrTempFile = new TempFile(); $spec = array( 0 => self::$descriptorSpec[0], // stdin 1 => fopen($this->windowsStdoutTempFile, 'wb'), // stdout 2 => fopen($this->windowsStderrTempFile, 'wb'), // stderr ); if (!$spec[1] || !$spec[2]) { throw new Exception(pht( 'Unable to create temporary files for '. 'Windows stdout / stderr streams')); } } $proc = @proc_open( $unmasked_command, $spec, $pipes, $cwd, $env); if ($this->useWindowsFileStreams) { fclose($spec[1]); fclose($spec[2]); $pipes = array( 0 => head($pipes), // stdin 1 => fopen($this->windowsStdoutTempFile, 'rb'), // stdout 2 => fopen($this->windowsStderrTempFile, 'rb'), // stderr ); if (!$pipes[1] || !$pipes[2]) { throw new Exception(pht( 'Unable to open temporary files for '. 'reading Windows stdout / stderr streams')); } } if ($trap) { $err = $trap->getErrorsAsString(); $trap->destroy(); } else { $err = error_get_last(); } if (!is_resource($proc)) { throw new Exception( pht( 'Failed to `%s`: %s', 'proc_open()', $err)); } $this->pipes = $pipes; $this->proc = $proc; list($stdin, $stdout, $stderr) = $pipes; if (!phutil_is_windows()) { // On Windows, we redirect process standard output and standard error // through temporary files, and then use stream_select to determine // if there's more data to read. if ((!stream_set_blocking($stdout, false)) || (!stream_set_blocking($stderr, false)) || (!stream_set_blocking($stdin, false))) { $this->__destruct(); throw new Exception(pht('Failed to set streams nonblocking.')); } } $this->tryToCloseStdin(); return false; } if (!$this->proc) { return true; } list($stdin, $stdout, $stderr) = $this->pipes; while (isset($this->stdin) && $this->stdin->getByteLength()) { $write_segment = $this->stdin->getAnyPrefix(); $bytes = fwrite($stdin, $write_segment); if ($bytes === false) { throw new Exception(pht('Unable to write to stdin!')); } else if ($bytes) { $this->stdin->removeBytesFromHead($bytes); } else { // Writes are blocked for now. break; } } $this->tryToCloseStdin(); // Read status before reading pipes so that we can never miss data that // arrives between our last read and the process exiting. $status = $this->procGetStatus(); $read_buffer_size = $this->readBufferSize; $max_stdout_read_bytes = PHP_INT_MAX; $max_stderr_read_bytes = PHP_INT_MAX; if ($read_buffer_size !== null) { $max_stdout_read_bytes = $read_buffer_size - strlen($this->stdout); $max_stderr_read_bytes = $read_buffer_size - strlen($this->stderr); } if ($max_stdout_read_bytes > 0) { $this->stdout .= $this->readAndDiscard( $stdout, $this->getStdoutSizeLimit() - strlen($this->stdout), 'stdout', $max_stdout_read_bytes); } if ($max_stderr_read_bytes > 0) { $this->stderr .= $this->readAndDiscard( $stderr, $this->getStderrSizeLimit() - strlen($this->stderr), 'stderr', $max_stderr_read_bytes); } $is_done = false; if (!$status['running']) { // We may still have unread bytes on stdout or stderr, particularly if // this future is being buffered and streamed. If we do, we don't want to // consider the subprocess to have exited until we've read everything. // See T9724 for context. if (feof($stdout) && feof($stderr)) { $is_done = true; } } if ($is_done) { if ($this->useWindowsFileStreams) { fclose($stdout); fclose($stderr); } // If the subprocess got nuked with `kill -9`, we get a -1 exitcode. // Upgrade this to a slightly more informative value by examining the // terminating signal code. $err = $status['exitcode']; if ($err == -1) { if ($status['signaled']) { $err = 128 + $status['termsig']; } } $this->result = array( $err, $this->stdout, $this->stderr, ); $this->closeProcess(); return true; } $elapsed = (microtime(true) - $this->start); if ($this->terminateTimeout && ($elapsed >= $this->terminateTimeout)) { if (!$this->didTerminate) { $this->killedByTimeout = true; $this->sendTerminateSignal(); return false; } } if ($this->killTimeout && ($elapsed >= $this->killTimeout)) { $this->killedByTimeout = true; $this->resolveKill(); return true; } } /** * @return void * @task internal */ public function __destruct() { if (!$this->proc) { return; } // NOTE: If we try to proc_close() an open process, we hang indefinitely. To // avoid this, kill the process explicitly if it's still running. $status = $this->procGetStatus(); if ($status['running']) { $this->sendTerminateSignal(); if (!$this->waitForExit(5)) { $this->resolveKill(); } } else { $this->closeProcess(); } } /** * Close and free resources if necessary. * * @return void * @task internal */ private function closeProcess() { foreach ($this->pipes as $pipe) { if (isset($pipe)) { @fclose($pipe); } } $this->pipes = array(null, null, null); if ($this->proc) { @proc_close($this->proc); $this->proc = null; } $this->stdin = null; if ($this->profilerCallID !== null) { $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall( $this->profilerCallID, array( 'err' => $this->result ? idx($this->result, 0) : null, )); $this->profilerCallID = null; } } /** * Execute `proc_get_status()`, but avoid pitfalls. * * @return dict Process status. * @task internal */ private function procGetStatus() { // After the process exits, we only get one chance to read proc_get_status() // before it starts returning garbage. Make sure we don't throw away the // last good read. if ($this->procStatus) { if (!$this->procStatus['running']) { return $this->procStatus; } } $this->procStatus = proc_get_status($this->proc); return $this->procStatus; } /** * Try to close stdin, if we're done using it. This keeps us from hanging if * the process on the other end of the pipe is waiting for EOF. * * @return void * @task internal */ private function tryToCloseStdin() { if (!$this->closePipe) { // We've been told to keep the pipe open by a call to write(..., true). return; } if ($this->stdin->getByteLength()) { // We still have bytes to write. return; } list($stdin) = $this->pipes; if (!$stdin) { // We've already closed stdin. return; } // There's nothing stopping us from closing stdin, so close it. @fclose($stdin); $this->pipes[0] = null; } public function getDefaultWait() { $wait = parent::getDefaultWait(); $next_timeout = $this->getNextTimeout(); if ($next_timeout) { if (!$this->start) { $this->start = microtime(true); } $elapsed = (microtime(true) - $this->start); $wait = max(0, min($next_timeout - $elapsed, $wait)); } return $wait; } private function getNextTimeout() { if ($this->didTerminate) { return $this->killTimeout; } else { return $this->terminateTimeout; } } private function sendTerminateSignal() { $this->didTerminate = true; proc_terminate($this->proc); return $this; } private function waitForExit($duration) { $start = microtime(true); while (true) { $status = $this->procGetStatus(); if (!$status['running']) { return true; } $waited = (microtime(true) - $start); if ($waited > $duration) { return false; } } } } diff --git a/src/xsprintf/PhutilQueryString.php b/src/xsprintf/PhutilQueryString.php new file mode 100644 index 0000000..b941242 --- /dev/null +++ b/src/xsprintf/PhutilQueryString.php @@ -0,0 +1,39 @@ +escaper = $escaper; + $this->argv = $argv; + + // This makes sure we throw immediately if there are errors in the + // parameters. + $this->getMaskedString(); + } + + public function __toString() { + return $this->getMaskedString(); + } + + public function getUnmaskedString() { + return $this->renderString(true); + } + + public function getMaskedString() { + return $this->renderString(false); + } + + private function renderString($unmasked) { + return xsprintf( + 'xsprintf_query', + array( + 'escaper' => $this->escaper, + 'unmasked' => $unmasked, + ), + $this->argv); + } + +} diff --git a/src/xsprintf/qsprintf.php b/src/xsprintf/qsprintf.php index 79ed2a8..206cbba 100644 --- a/src/xsprintf/qsprintf.php +++ b/src/xsprintf/qsprintf.php @@ -1,339 +1,514 @@ and %<. * * %> ("Prefix") * Escapes a prefix query for a LIKE clause. For example: * * // Find all rows where `name` starts with $prefix. * qsprintf($escaper, 'WHERE name LIKE %>', $prefix); * * %< ("Suffix") * Escapes a suffix query for a LIKE clause. For example: * * // Find all rows where `name` ends with $suffix. * qsprintf($escaper, 'WHERE name LIKE %<', $suffix); + * + * %T ("Table") + * Escapes a table name. In most cases, you should use "%R" instead. */ function qsprintf(PhutilQsprintfInterface $escaper, $pattern /* , ... */) { $args = func_get_args(); array_shift($args); - return xsprintf('xsprintf_query', $escaper, $args); + return new PhutilQueryString($escaper, $args); } function vqsprintf(PhutilQsprintfInterface $escaper, $pattern, array $argv) { array_unshift($argv, $pattern); - return xsprintf('xsprintf_query', $escaper, $argv); + return new PhutilQueryString($escaper, $argv); } /** * @{function:xsprintf} callback for encoding SQL queries. See * @{function:qsprintf}. */ function xsprintf_query($userdata, &$pattern, &$pos, &$value, &$length) { - $type = $pattern[$pos]; - $escaper = $userdata; - $next = (strlen($pattern) > $pos + 1) ? $pattern[$pos + 1] : null; + $type = $pattern[$pos]; + if (is_array($userdata)) { + $escaper = $userdata['escaper']; + $unmasked = $userdata['unmasked']; + } else { + $escaper = $userdata; + $unmasked = false; + } + + $next = (strlen($pattern) > $pos + 1) ? $pattern[$pos + 1] : null; $nullable = false; - $done = false; + $done = false; $prefix = ''; if (!($escaper instanceof PhutilQsprintfInterface)) { throw new InvalidArgumentException(pht('Invalid database escaper.')); } switch ($type) { case '=': // Nullable test switch ($next) { case 'd': case 'f': case 's': $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; if ($value === null) { $value = 'IS NULL'; $done = true; } else { $prefix = '= '; $type = $next; } break; default: throw new Exception( pht( 'Unknown conversion, try %s, %s, or %s.', '%=d', '%=s', '%=f')); } break; case 'n': // Nullable... switch ($next) { case 'd': // ...integer. case 'f': // ...float. case 's': // ...string. case 'B': // ...binary string. $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = $next; $nullable = true; break; default: throw new XsprintfUnknownConversionException("%n{$next}"); } break; case 'L': // List of.. qsprintf_check_type($value, "L{$next}", $pattern); $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; $done = true; switch ($next) { case 'd': // ...integers. $value = implode(', ', array_map('intval', $value)); break; case 'f': // ...floats. $value = implode(', ', array_map('floatval', $value)); break; case 's': // ...strings. foreach ($value as $k => $v) { $value[$k] = "'".$escaper->escapeUTF8String((string)$v)."'"; } $value = implode(', ', $value); break; case 'B': // ...binary strings. foreach ($value as $k => $v) { $value[$k] = "'".$escaper->escapeBinaryString((string)$v)."'"; } $value = implode(', ', $value); break; case 'C': // ...columns. foreach ($value as $k => $v) { $value[$k] = $escaper->escapeColumnName($v); } $value = implode(', ', $value); break; + case 'K': // ...key columns. + // This is like "%LC", but for escaping column lists passed to key + // specifications. These should be escaped as "`column`(123)". For + // example: + // + // ALTER TABLE `x` ADD KEY `y` (`u`(16), `v`(32)); + + foreach ($value as $k => $v) { + $matches = null; + if (preg_match('/\((\d+)\)\z/', $v, $matches)) { + $v = substr($v, 0, -(strlen($matches[1]) + 2)); + $prefix_len = '('.((int)$matches[1]).')'; + } else { + $prefix_len = ''; + } + + $value[$k] = $escaper->escapeColumnName($v).$prefix_len; + } + + $value = implode(', ', $value); + break; + case 'Q': + // TODO: Here, and in "%LO", "%LA", and "%LJ", we should eventually + // stop accepting strings. + foreach ($value as $k => $v) { + if (is_string($v)) { + continue; + } + $value[$k] = $v->getUnmaskedString(); + } + $value = implode(', ', $value); + break; + case 'O': + foreach ($value as $k => $v) { + if (is_string($v)) { + continue; + } + $value[$k] = $v->getUnmaskedString(); + } + if (count($value) == 1) { + $value = '('.head($value).')'; + } else { + $value = '(('.implode(') OR (', $value).'))'; + } + break; + case 'A': + foreach ($value as $k => $v) { + if (is_string($v)) { + continue; + } + $value[$k] = $v->getUnmaskedString(); + } + if (count($value) == 1) { + $value = '('.head($value).')'; + } else { + $value = '(('.implode(') AND (', $value).'))'; + } + break; + case 'J': + foreach ($value as $k => $v) { + if (is_string($v)) { + continue; + } + $value[$k] = $v->getUnmaskedString(); + } + $value = implode(' ', $value); + break; default: throw new XsprintfUnknownConversionException("%L{$next}"); } break; } if (!$done) { qsprintf_check_type($value, $type, $pattern); switch ($type) { case 's': // String if ($nullable && $value === null) { $value = 'NULL'; } else { $value = "'".$escaper->escapeUTF8String((string)$value)."'"; } $type = 's'; break; case 'B': // Binary String if ($nullable && $value === null) { $value = 'NULL'; } else { $value = "'".$escaper->escapeBinaryString((string)$value)."'"; } $type = 's'; break; case 'Q': // Query Fragment + if ($value instanceof PhutilQueryString) { + $value = $value->getUnmaskedString(); + } + $type = 's'; + break; + + case 'Z': // Raw Query Fragment $type = 's'; break; case '~': // Like Substring case '>': // Like Prefix case '<': // Like Suffix $value = $escaper->escapeStringForLikeClause($value); switch ($type) { case '~': $value = "'%".$value."%'"; break; case '>': $value = "'".$value."%'"; break; case '<': $value = "'%".$value."'"; break; } $type = 's'; break; case 'f': // Float if ($nullable && $value === null) { $value = 'NULL'; } else { $value = (float)$value; } $type = 's'; break; case 'd': // Integer if ($nullable && $value === null) { $value = 'NULL'; } else { $value = (int)$value; } $type = 's'; break; case 'T': // Table case 'C': // Column $value = $escaper->escapeColumnName($value); $type = 's'; break; case 'K': // Komment $value = $escaper->escapeMultilineComment($value); $type = 's'; break; case 'R': // Database + Table Reference $database_name = $value->getAphrontRefDatabaseName(); $database_name = $escaper->escapeColumnName($database_name); $table_name = $value->getAphrontRefTableName(); $table_name = $escaper->escapeColumnName($table_name); $value = $database_name.'.'.$table_name; $type = 's'; break; + case 'P': // Password or Secret + if ($unmasked) { + $value = $value->openEnvelope(); + $value = "'".$escaper->escapeUTF8String($value)."'"; + } else { + $value = '********'; + } + $type = 's'; + break; + default: throw new XsprintfUnknownConversionException($type); } } if ($prefix) { $value = $prefix.$value; } + $pattern[$pos] = $type; } function qsprintf_check_type($value, $type, $query) { switch ($type) { case 'Ld': case 'Ls': case 'LC': + case 'LK': case 'LB': case 'Lf': + case 'LQ': + case 'LA': + case 'LO': + case 'LJ': if (!is_array($value)) { throw new AphrontParameterQueryException( $query, pht('Expected array argument for %%%s conversion.', $type)); } if (empty($value)) { throw new AphrontParameterQueryException( $query, pht('Array for %%%s conversion is empty.', $type)); } foreach ($value as $scalar) { qsprintf_check_scalar_type($scalar, $type, $query); } break; default: qsprintf_check_scalar_type($value, $type, $query); break; } } function qsprintf_check_scalar_type($value, $type, $query) { switch ($type) { + case 'LQ': + case 'LA': + case 'LO': + case 'LJ': + // TODO: See T13217. Remove this eventually. + if (is_string($value)) { + phlog( + pht( + 'UNSAFE: Raw string ("%s") passed to query ("%s") subclause '. + 'for "%%%s" conversion. Subclause conversions should be passed '. + 'a list of PhutilQueryString objects.', + $value, + $query, + $type)); + break; + } + + if (!($value instanceof PhutilQueryString)) { + throw new AphrontParameterQueryException( + $query, + pht( + 'Expected a list of PhutilQueryString objects for %%%s '. + 'conversion.', + $type)); + } + break; + case 'Q': + // TODO: See T13217. Remove this eventually. + if (is_string($value)) { + phlog( + pht( + 'UNSAFE: Raw string ("%s") passed to query ("%s") for "%%Q" '. + 'conversion. %%Q should be passed a query string.', + $value, + $query)); + break; + } + + if (!($value instanceof PhutilQueryString)) { + throw new AphrontParameterQueryException( + $query, + pht('Expected a PhutilQueryString for %%%s conversion.', $type)); + } + break; + + case 'Z': + if (!is_string($value)) { + throw new AphrontParameterQueryException( + $query, + pht('Value for "%%Z" conversion should be a raw string.')); + } + break; + case 'LC': + case 'LK': case 'T': case 'C': if (!is_string($value)) { throw new AphrontParameterQueryException( $query, pht('Expected a string for %%%s conversion.', $type)); } break; case 'Ld': case 'Lf': case 'd': case 'f': if (!is_null($value) && !is_numeric($value)) { throw new AphrontParameterQueryException( $query, pht('Expected a numeric scalar or null for %%%s conversion.', $type)); } break; case 'Ls': case 's': case 'LB': case 'B': case '~': case '>': case '<': case 'K': if (!is_null($value) && !is_scalar($value)) { throw new AphrontParameterQueryException( $query, pht('Expected a scalar or null for %%%s conversion.', $type)); } break; case 'R': if (!($value instanceof AphrontDatabaseTableRefInterface)) { throw new AphrontParameterQueryException( pht( 'Parameter to "%s" conversion in "qsprintf(...)" is not an '. 'instance of AphrontDatabaseTableRefInterface.', '%R')); } break; + case 'P': + if (!($value instanceof PhutilOpaqueEnvelope)) { + throw new AphrontParameterQueryException( + pht( + 'Parameter to "%s" conversion in "qsprintf(...)" is not an '. + 'instance of PhutilOpaqueEnvelope.', + '%P')); + } + break; + default: throw new XsprintfUnknownConversionException($type); } } diff --git a/src/xsprintf/queryfx.php b/src/xsprintf/queryfx.php index e88c47e..d1eef61 100644 --- a/src/xsprintf/queryfx.php +++ b/src/xsprintf/queryfx.php @@ -1,27 +1,27 @@ setLastActiveEpoch(time()); - $conn->executeRawQuery($query); + $conn->executeQuery($query); } function queryfx_all(AphrontDatabaseConnection $conn, $sql /* , ... */) { $argv = func_get_args(); call_user_func_array('queryfx', $argv); return $conn->selectAllResults(); } function queryfx_one(AphrontDatabaseConnection $conn, $sql /* , ... */) { $argv = func_get_args(); $ret = call_user_func_array('queryfx_all', $argv); if (count($ret) > 1) { throw new AphrontCountQueryException( pht('Query returned more than one row.')); } else if (count($ret)) { return reset($ret); } return null; }