Page MenuHomePhabricator

D20773.largetrue.diff
No OneTemporary

D20773.largetrue.diff

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/scripts/test/lipsum.php b/scripts/test/lipsum.php
deleted file mode 100755
--- a/scripts/test/lipsum.php
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env php
-<?php
-
-require_once dirname(__FILE__).'/../__init_script__.php';
-
-$args = new PhutilArgumentParser($argv);
-$args->setTagline(pht('test context-free grammars'));
-$args->setSynopsis(<<<EOHELP
-**lipsum.php** __class__
- Generate output from a named context-free grammar.
-EOHELP
- );
-$args->parseStandardArguments();
-$args->parse(
- array(
- array(
- 'name' => 'class',
- 'wildcard' => true,
- ),
- ));
-
-$class = $args->getArg('class');
-if (count($class) !== 1) {
- $args->printHelpAndExit();
-}
-$class = reset($class);
-
-$symbols = id(new PhutilClassMapQuery())
- ->setAncestorClass('PhutilContextFreeGrammar')
- ->execute();
-
-$symbols = ipull($symbols, 'name', 'name');
-
-if (empty($symbols[$class])) {
- $available = implode(', ', array_keys($symbols));
- throw new PhutilArgumentUsageException(
- pht(
- "Class '%s' is not a defined, concrete subclass of %s. ".
- "Available classes are: %s",
- $class,
- 'PhutilContextFreeGrammar',
- $available));
-}
-
-$object = newv($class, array());
-echo $object->generate()."\n";
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -15,36 +15,12 @@
'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',
@@ -112,7 +88,6 @@
'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',
@@ -126,18 +101,11 @@
'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',
@@ -147,26 +115,6 @@
'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',
@@ -176,7 +124,6 @@
'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',
@@ -220,7 +167,6 @@
'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',
@@ -231,7 +177,6 @@
'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',
@@ -248,7 +193,6 @@
'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',
@@ -256,12 +200,10 @@
'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',
'PhutilHTMLParser' => 'parser/html/PhutilHTMLParser.php',
'PhutilHTMLParserTestCase' => 'parser/html/__tests__/PhutilHTMLParserTestCase.php',
'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php',
@@ -274,11 +216,6 @@
'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',
@@ -291,7 +228,6 @@
'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',
@@ -301,7 +237,6 @@
'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',
@@ -310,7 +245,6 @@
'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',
@@ -318,7 +252,6 @@
'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',
@@ -336,15 +269,12 @@
'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',
@@ -356,7 +286,6 @@
'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',
@@ -379,48 +308,12 @@
'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',
- 'PhutilRemarkupHyperlinkEngineExtension' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php',
- 'PhutilRemarkupHyperlinkRef' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRef.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',
- 'PhutilRemarkupQuotedBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotedBlockRule.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',
@@ -443,7 +336,6 @@
'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',
@@ -467,9 +359,7 @@
'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',
@@ -487,13 +377,11 @@
'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',
@@ -622,17 +510,10 @@
'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',
@@ -642,7 +523,6 @@
'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',
@@ -658,41 +538,12 @@
'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',
@@ -766,7 +617,6 @@
'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
'PhutilAggregateException' => 'Exception',
'PhutilAllCapsEnglishLocale' => 'PhutilLocale',
- 'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilArgumentParser' => 'Phobject',
'PhutilArgumentParserException' => 'Exception',
'PhutilArgumentParserTestCase' => 'PhutilTestCase',
@@ -785,18 +635,11 @@
),
'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(
@@ -808,26 +651,6 @@
'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',
@@ -840,7 +663,6 @@
'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
'PhutilClassMapQuery' => 'Phobject',
'PhutilCloudWatchMetric' => 'Phobject',
- 'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilCommandString' => 'Phobject',
'PhutilConsole' => 'Phobject',
'PhutilConsoleBlock' => 'PhutilConsoleView',
@@ -884,7 +706,6 @@
'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
'PhutilDirectoryFixture' => 'Phobject',
'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache',
- 'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilDivinerSyntaxHighlighter' => 'Phobject',
'PhutilDocblockParser' => 'Phobject',
'PhutilDocblockParserTestCase' => 'PhutilTestCase',
@@ -895,7 +716,6 @@
'PhutilEmailAddress' => 'Phobject',
'PhutilEmailAddressTestCase' => 'PhutilTestCase',
'PhutilEmojiLocale' => 'PhutilLocale',
- 'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter',
'PhutilEnglishCanadaLocale' => 'PhutilLocale',
'PhutilErrorHandler' => 'Phobject',
'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
@@ -912,7 +732,6 @@
'PhutilExecutableFuture' => 'Future',
'PhutilExecutionEnvironment' => 'Phobject',
'PhutilExtensionsTestCase' => 'PhutilTestCase',
- 'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
'PhutilFileLock' => 'PhutilLock',
'PhutilFileLockTestCase' => 'PhutilTestCase',
@@ -920,12 +739,10 @@
'PhutilFrenchLocale' => 'PhutilLocale',
'PhutilGermanLocale' => 'PhutilLocale',
'PhutilGitBinaryAnalyzer' => 'PhutilBinaryAnalyzer',
- 'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilGitHubFuture' => 'FutureProxy',
'PhutilGitHubResponse' => 'Phobject',
'PhutilGitURI' => 'Phobject',
'PhutilGitURITestCase' => 'PhutilTestCase',
- 'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilHTMLParser' => 'Phobject',
'PhutilHTMLParserTestCase' => 'PhutilTestCase',
'PhutilHTTPEngineExtension' => 'Phobject',
@@ -941,11 +758,6 @@
'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
'PhutilHgsprintfTestCase' => 'PhutilTestCase',
'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon',
- 'PhutilICSParser' => 'Phobject',
- 'PhutilICSParserException' => 'Exception',
- 'PhutilICSParserTestCase' => 'PhutilTestCase',
- 'PhutilICSWriter' => 'Phobject',
- 'PhutilICSWriterTestCase' => 'PhutilTestCase',
'PhutilINIParserException' => 'Exception',
'PhutilIPAddress' => 'Phobject',
'PhutilIPAddressTestCase' => 'PhutilTestCase',
@@ -958,7 +770,6 @@
'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase',
'PhutilInvisibleSyntaxHighlighter' => 'Phobject',
'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException',
- 'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilJSON' => 'Phobject',
'PhutilJSONFragmentLexer' => 'PhutilLexer',
'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
@@ -968,7 +779,6 @@
'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase',
'PhutilJSONTestCase' => 'PhutilTestCase',
- 'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
'PhutilJavaFragmentLexer' => 'PhutilLexer',
'PhutilKeyValueCache' => 'Phobject',
'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy',
@@ -977,7 +787,6 @@
'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache',
'PhutilKeyValueCacheTestCase' => 'PhutilTestCase',
'PhutilKoreanLocale' => 'PhutilLocale',
- 'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter',
'PhutilLanguageGuesser' => 'Phobject',
'PhutilLanguageGuesserTestCase' => 'PhutilTestCase',
'PhutilLexer' => 'Phobject',
@@ -985,7 +794,6 @@
'PhutilLibraryConflictException' => 'Exception',
'PhutilLibraryMapBuilder' => 'Phobject',
'PhutilLibraryTestCase' => 'PhutilTestCase',
- 'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilLocale' => 'Phobject',
'PhutilLocaleTestCase' => 'PhutilTestCase',
'PhutilLock' => 'Phobject',
@@ -1003,15 +811,12 @@
'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',
@@ -1025,7 +830,6 @@
'Phobject',
'PhutilPerson',
),
- 'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilPhtTestCase' => 'PhutilTestCase',
'PhutilPirateEnglishLocale' => 'PhutilLocale',
'PhutilPortugueseBrazilLocale' => 'PhutilLocale',
@@ -1051,47 +855,12 @@
'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',
- 'PhutilRemarkupHyperlinkEngineExtension' => 'Phobject',
- 'PhutilRemarkupHyperlinkRef' => 'Phobject',
- 'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule',
- 'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule',
- 'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule',
- 'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule',
- 'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule',
- 'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule',
- 'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule',
- 'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule',
- 'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule',
- 'PhutilRemarkupQuotedBlockRule' => 'PhutilRemarkupBlockRule',
- 'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupQuotedBlockRule',
- 'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupQuotedBlockRule',
- 'PhutilRemarkupRule' => 'Phobject',
- 'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule',
- 'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule',
- 'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter',
- 'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule',
'PhutilRope' => 'Phobject',
'PhutilRopeTestCase' => 'PhutilTestCase',
'PhutilSafeHTML' => 'Phobject',
@@ -1113,7 +882,6 @@
'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase',
'PhutilSimpleOptionsTestCase' => 'PhutilTestCase',
'PhutilSimplifiedChineseLocale' => 'PhutilLocale',
- 'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilSlackFuture' => 'FutureProxy',
'PhutilSocketChannel' => 'PhutilChannel',
'PhutilSortVector' => 'Phobject',
@@ -1139,9 +907,7 @@
'PhutilTranslator' => 'Phobject',
'PhutilTranslatorTestCase' => 'PhutilTestCase',
'PhutilTsprintfTestCase' => 'PhutilTestCase',
- 'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilTwitchFuture' => 'FutureProxy',
- 'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilTypeCheckException' => 'Exception',
'PhutilTypeExtraParametersException' => 'Exception',
'PhutilTypeLexer' => 'PhutilLexer',
@@ -1159,13 +925,11 @@
'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',
diff --git a/src/aphront/storage/connection/AphrontDatabaseConnection.php b/src/aphront/storage/connection/AphrontDatabaseConnection.php
deleted file mode 100644
--- a/src/aphront/storage/connection/AphrontDatabaseConnection.php
+++ /dev/null
@@ -1,305 +0,0 @@
-<?php
-
-/**
- * @task xaction Transaction Management
- */
-abstract class AphrontDatabaseConnection
- extends Phobject
- implements PhutilQsprintfInterface {
-
- private $transactionState;
- private $readOnly;
- private $queryTimeout;
- private $locks = array();
- private $lastActiveEpoch;
- private $persistent;
-
- abstract public function getInsertID();
- abstract public function getAffectedRows();
- abstract public function selectAllResults();
- abstract public function executeQuery(PhutilQueryString $query);
- abstract public function executeRawQueries(array $raw_queries);
- abstract public function close();
- abstract public function openConnection();
-
- public function __destruct() {
- // NOTE: This does not actually close persistent connections: PHP maintains
- // them in the connection pool.
- $this->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/AphrontDatabaseTransactionState.php b/src/aphront/storage/connection/AphrontDatabaseTransactionState.php
deleted file mode 100644
--- a/src/aphront/storage/connection/AphrontDatabaseTransactionState.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-
-/**
- * Represents current transaction state of a connection.
- */
-final class AphrontDatabaseTransactionState extends Phobject {
-
- private $depth = 0;
- private $readLockLevel = 0;
- private $writeLockLevel = 0;
-
- public function getDepth() {
- return $this->depth;
- }
-
- public function increaseDepth() {
- return ++$this->depth;
- }
-
- public function decreaseDepth() {
- if ($this->depth == 0) {
- throw new Exception(
- pht(
- 'Too many calls to %s or %s!',
- 'saveTransaction()',
- 'killTransaction()'));
- }
-
- return --$this->depth;
- }
-
- public function getSavepointName() {
- return 'Aphront_Savepoint_'.$this->depth;
- }
-
- public function beginReadLocking() {
- $this->readLockLevel++;
- return $this;
- }
-
- public function endReadLocking() {
- if ($this->readLockLevel == 0) {
- throw new Exception(
- pht(
- 'Too many calls to %s!',
- __FUNCTION__.'()'));
- }
- $this->readLockLevel--;
- return $this;
- }
-
- public function isReadLocking() {
- return ($this->readLockLevel > 0);
- }
-
- public function beginWriteLocking() {
- $this->writeLockLevel++;
- return $this;
- }
-
- public function endWriteLocking() {
- if ($this->writeLockLevel == 0) {
- throw new Exception(
- pht(
- 'Too many calls to %s!',
- __FUNCTION__.'()'));
- }
- $this->writeLockLevel--;
- return $this;
- }
-
- public function isWriteLocking() {
- return ($this->writeLockLevel > 0);
- }
-
- public function __destruct() {
- if ($this->depth) {
- throw new Exception(
- pht(
- 'Process exited with an open transaction! The transaction '.
- 'will be implicitly rolled back. Calls to %s must always be '.
- 'paired with a call to %s or %s.',
- 'openTransaction()',
- 'saveTransaction()',
- 'killTransaction()'));
- }
- if ($this->readLockLevel) {
- throw new Exception(
- pht(
- 'Process exited with an open read lock! Call to %s '.
- 'must always be paired with a call to %s.',
- 'beginReadLocking()',
- 'endReadLocking()'));
- }
- if ($this->writeLockLevel) {
- throw new Exception(
- pht(
- 'Process exited with an open write lock! Call to %s '.
- 'must always be paired with a call to %s.',
- 'beginWriteLocking()',
- 'endWriteLocking()'));
- }
- }
-
-}
diff --git a/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php b/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php
deleted file mode 100644
--- a/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-
-final class AphrontIsolatedDatabaseConnection
- extends AphrontDatabaseConnection {
-
- private $configuration;
- private static $nextInsertID;
- private $insertID;
-
- private $transcript = array();
-
- private $allResults;
- private $affectedRows;
-
- public function __construct(array $configuration) {
- $this->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 '<S>';
- }
-
- public function escapeBinaryString($string) {
- return '<B>';
- }
-
- public function escapeColumnName($name) {
- return '<C>';
- }
-
- public function escapeMultilineComment($comment) {
- return '<K>';
- }
-
- public function escapeStringForLikeClause($value) {
- return '<L>';
- }
-
- 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 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 "<K>" (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),
- $display_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
deleted file mode 100644
--- a/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php
+++ /dev/null
@@ -1,405 +0,0 @@
-<?php
-
-abstract class AphrontBaseMySQLDatabaseConnection
- extends AphrontDatabaseConnection {
-
- private $configuration;
- private $connection;
- private $connectionPool = array();
- private $lastResult;
-
- private $nextError;
-
- abstract protected function connect();
- abstract protected function rawQuery($raw_query);
- abstract protected function rawQueries(array $raw_queries);
- abstract protected function fetchAssoc($result);
- abstract protected function getErrorCode($connection);
- abstract protected function getErrorDescription($connection);
- abstract protected function closeConnection();
- abstract protected function freeResult($result);
-
- public function __construct(array $configuration) {
- $this->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,
- ));
-
- // If we receive these errors, we'll retry the connection up to the
- // retry limit. For other errors, we'll fail immediately.
- $retry_codes = array(
- // "Connection Timeout"
- 2002 => true,
-
- // "Unable to Connect"
- 2003 => true,
- );
-
- $max_retries = max(1, $this->getConfiguration('retries', 3));
- for ($attempt = 1; $attempt <= $max_retries; $attempt++) {
- try {
- $conn = $this->connect();
- $profiler->endServiceCall($call_id, array());
- break;
- } catch (AphrontQueryException $ex) {
- $code = $ex->getCode();
- if (($attempt < $max_retries) && isset($retry_codes[$code])) {
- $message = pht(
- 'Retrying database connection to "%s" after connection '.
- 'failure (attempt %d; "%s"; error #%d): %s',
- $host,
- $attempt,
- get_class($ex),
- $code,
- $ex->getMessage());
-
- phlog($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 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' => $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 <process>" 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/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php b/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php
deleted file mode 100644
--- a/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php
+++ /dev/null
@@ -1,233 +0,0 @@
-<?php
-
-final class AphrontMySQLDatabaseConnection
- extends AphrontBaseMySQLDatabaseConnection {
-
- public function escapeUTF8String($string) {
- $this->validateUTF8String($string);
- return $this->escapeBinaryString($string);
- }
-
- public function escapeBinaryString($string) {
- return mysql_real_escape_string($string, $this->requireConnection());
- }
-
- public function getInsertID() {
- return mysql_insert_id($this->requireConnection());
- }
-
- public function getAffectedRows() {
- return mysql_affected_rows($this->requireConnection());
- }
-
- protected function closeConnection() {
- mysql_close($this->requireConnection());
- }
-
- protected function connect() {
- if (!function_exists('mysql_connect')) {
- // We have to '@' the actual call since it can spew all sorts of silly
- // noise, but it will also silence fatals caused by not having MySQL
- // installed, which has bitten me on three separate occasions. Make sure
- // such failures are explicit and loud.
- throw new Exception(
- pht(
- 'About to call %s, but the PHP MySQL extension is not available!',
- 'mysql_connect()'));
- }
-
- $user = $this->getConfiguration('user');
- $host = $this->getConfiguration('host');
- $port = $this->getConfiguration('port');
-
- if ($port) {
- $host .= ':'.$port;
- }
-
- $database = $this->getConfiguration('database');
-
- $pass = $this->getConfiguration('pass');
- if ($pass instanceof PhutilOpaqueEnvelope) {
- $pass = $pass->openEnvelope();
- }
-
- $timeout = $this->getConfiguration('timeout');
- $timeout_ini = 'mysql.connect_timeout';
- if ($timeout) {
- $old_timeout = ini_get($timeout_ini);
- ini_set($timeout_ini, $timeout);
- }
-
- try {
- $conn = @mysql_connect(
- $host,
- $user,
- $pass,
- $new_link = true,
- $flags = 0);
- } catch (Exception $ex) {
- if ($timeout) {
- ini_set($timeout_ini, $old_timeout);
- }
- throw $ex;
- }
-
- if ($timeout) {
- ini_set($timeout_ini, $old_timeout);
- }
-
- if (!$conn) {
- $errno = mysql_errno();
- $error = mysql_error();
- $this->throwConnectionException($errno, $error, $user, $host);
- }
-
- if ($database !== null) {
- $ret = @mysql_select_db($database, $conn);
- if (!$ret) {
- $this->throwQueryException($conn);
- }
- }
-
- $ok = @mysql_set_charset('utf8mb4', $conn);
- if (!$ok) {
- mysql_set_charset('binary', $conn);
- }
-
- return $conn;
- }
-
- protected function rawQuery($raw_query) {
- return @mysql_query($raw_query, $this->requireConnection());
- }
-
- /**
- * @phutil-external-symbol function mysql_multi_query
- * @phutil-external-symbol function mysql_fetch_result
- * @phutil-external-symbol function mysql_more_results
- * @phutil-external-symbol function mysql_next_result
- */
- protected function rawQueries(array $raw_queries) {
- $conn = $this->requireConnection();
- $results = array();
-
- if (!function_exists('mysql_multi_query')) {
- foreach ($raw_queries as $key => $raw_query) {
- $results[$key] = $this->processResult($this->rawQuery($raw_query));
- }
- return $results;
- }
-
- if (!mysql_multi_query(implode("\n;\n\n", $raw_queries), $conn)) {
- $ex = $this->processResult(false);
- return array_fill_keys(array_keys($raw_queries), $ex);
- }
-
- $processed_all = false;
- foreach ($raw_queries as $key => $raw_query) {
- $results[$key] = $this->processResult(@mysql_fetch_result($conn));
- if (!mysql_more_results($conn)) {
- $processed_all = true;
- break;
- }
- mysql_next_result($conn);
- }
-
- if (!$processed_all) {
- throw new Exception(
- pht('There are some results left in the result set.'));
- }
-
- return $results;
- }
-
- protected function freeResult($result) {
- mysql_free_result($result);
- }
-
- public function supportsParallelQueries() {
- // fb_parallel_query() doesn't support results with different columns.
- return false;
- }
-
- /**
- * @phutil-external-symbol function fb_parallel_query
- */
- public function executeParallelQueries(
- array $queries,
- array $conns = array()) {
- assert_instances_of($conns, __CLASS__);
-
- $map = array();
- $is_write = false;
- foreach ($queries as $id => $query) {
- $is_write = $is_write || $this->checkWrite($query);
- $conn = idx($conns, $id, $this);
-
- $host = $conn->getConfiguration('host');
- $port = 0;
- $match = null;
- if (preg_match('/(.+):(.+)/', $host, $match)) {
- list(, $host, $port) = $match;
- }
-
- $pass = $conn->getConfiguration('pass');
- if ($pass instanceof PhutilOpaqueEnvelope) {
- $pass = $pass->openEnvelope();
- }
-
- $map[$id] = array(
- 'sql' => $query,
- 'ip' => $host,
- 'port' => $port,
- 'username' => $conn->getConfiguration('user'),
- 'password' => $pass,
- 'db' => $conn->getConfiguration('database'),
- );
- }
-
- $profiler = PhutilServiceProfiler::getInstance();
- $call_id = $profiler->beginServiceCall(
- array(
- 'type' => 'multi-query',
- 'queries' => $queries,
- 'write' => $is_write,
- ));
-
- $map = fb_parallel_query($map);
-
- $profiler->endServiceCall($call_id, array());
-
- $results = array();
- $pos = 0;
- $err_pos = 0;
- foreach ($queries as $id => $query) {
- $errno = idx(idx($map, 'errno', array()), $err_pos);
- $err_pos++;
- if ($errno) {
- try {
- $this->throwQueryCodeException($errno, $map['error'][$id]);
- } catch (Exception $ex) {
- $results[$id] = $ex;
- }
- continue;
- }
- $results[$id] = $map['result'][$pos];
- $pos++;
- }
- return $results;
- }
-
- protected function fetchAssoc($result) {
- return mysql_fetch_assoc($result);
- }
-
- protected function getErrorCode($connection) {
- return mysql_errno($connection);
- }
-
- protected function getErrorDescription($connection) {
- return mysql_error($connection);
- }
-
-}
diff --git a/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php b/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
deleted file mode 100644
--- a/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php
+++ /dev/null
@@ -1,244 +0,0 @@
-<?php
-
-/**
- * @phutil-external-symbol class mysqli
- */
-final class AphrontMySQLiDatabaseConnection
- extends AphrontBaseMySQLDatabaseConnection {
-
- private $connectionOpen = false;
-
- public function escapeUTF8String($string) {
- $this->validateUTF8String($string);
- return $this->escapeBinaryString($string);
- }
-
- public function escapeBinaryString($string) {
- return $this->requireConnection()->escape_string($string);
- }
-
- public function getInsertID() {
- return $this->requireConnection()->insert_id;
- }
-
- public function getAffectedRows() {
- return $this->requireConnection()->affected_rows;
- }
-
- protected function closeConnection() {
- if ($this->connectionOpen) {
- $this->requireConnection()->close();
- $this->connectionOpen = false;
- }
- }
-
- protected function connect() {
- if (!class_exists('mysqli', false)) {
- throw new Exception(pht(
- 'About to call new %s, but the PHP MySQLi extension is not available!',
- 'mysqli()'));
- }
-
- $user = $this->getConfiguration('user');
- $host = $this->getConfiguration('host');
- $port = $this->getConfiguration('port');
- $database = $this->getConfiguration('database');
-
- $pass = $this->getConfiguration('pass');
- if ($pass instanceof PhutilOpaqueEnvelope) {
- $pass = $pass->openEnvelope();
- }
-
- // If the host is "localhost", the port is ignored and mysqli attempts to
- // connect over a socket.
- if ($port) {
- if ($host === 'localhost' || $host === null) {
- $host = '127.0.0.1';
- }
- }
-
- $conn = mysqli_init();
-
- $timeout = $this->getConfiguration('timeout');
- if ($timeout) {
- $conn->options(MYSQLI_OPT_CONNECT_TIMEOUT, $timeout);
- }
-
- if ($this->getPersistent()) {
- $host = 'p:'.$host;
- }
-
- @$conn->real_connect(
- $host,
- $user,
- $pass,
- $database,
- $port);
-
- $errno = $conn->connect_errno;
- if ($errno) {
- $error = $conn->connect_error;
- $this->throwConnectionException($errno, $error, $user, $host);
- }
-
- // See T13238. Attempt to prevent "LOAD DATA LOCAL INFILE", which allows a
- // malicious server to ask the client for any file. At time of writing,
- // this option MUST be set after "real_connect()" on all PHP versions.
- $conn->options(MYSQLI_OPT_LOCAL_INFILE, 0);
-
- $this->connectionOpen = true;
-
- $ok = @$conn->set_charset('utf8mb4');
- if (!$ok) {
- $ok = $conn->set_charset('binary');
- }
-
- return $conn;
- }
-
- protected function rawQuery($raw_query) {
- $conn = $this->requireConnection();
- $time_limit = $this->getQueryTimeout();
-
- // If we have a query time limit, run this query synchronously but use
- // the async API. This allows us to kill queries which take too long
- // without requiring any configuration on the server side.
- if ($time_limit && $this->supportsAsyncQueries()) {
- $conn->query($raw_query, MYSQLI_ASYNC);
-
- $read = array($conn);
- $error = array($conn);
- $reject = array($conn);
-
- $result = mysqli::poll($read, $error, $reject, $time_limit);
-
- if ($result === false) {
- $this->closeConnection();
- throw new Exception(
- pht('Failed to poll mysqli connection!'));
- } else if ($result === 0) {
- $this->closeConnection();
- throw new AphrontQueryTimeoutQueryException(
- pht(
- 'Query timed out after %s second(s)!',
- new PhutilNumber($time_limit)));
- }
-
- return @$conn->reap_async_query();
- }
-
- $trap = new PhutilErrorTrap();
-
- $result = @$conn->query($raw_query);
-
- $err = $trap->getErrorsAsString();
- $trap->destroy();
-
- // See T13238 and PHI1014. Sometimes, the call to "$conn->query()" may fail
- // without setting an error code on the connection. One way to reproduce
- // this is to use "LOAD DATA LOCAL INFILE" with "mysqli.allow_local_infile"
- // disabled.
-
- // If we have no result and no error code, raise a synthetic query error
- // with whatever error message was raised as a local PHP warning.
-
- if (!$result) {
- $error_code = $this->getErrorCode($conn);
- if (!$error_code) {
- if (strlen($err)) {
- $message = $err;
- } else {
- $message = pht(
- 'Call to "mysqli->query()" failed, but did not set an error '.
- 'code or emit an error message.');
- }
- $this->throwQueryCodeException(777777, $message);
- }
- }
-
- return $result;
- }
-
- protected function rawQueries(array $raw_queries) {
- $conn = $this->requireConnection();
-
- $have_result = false;
- $results = array();
-
- foreach ($raw_queries as $key => $raw_query) {
- if (!$have_result) {
- // End line in front of semicolon to allow single line comments at the
- // end of queries.
- $have_result = $conn->multi_query(implode("\n;\n\n", $raw_queries));
- } else {
- $have_result = $conn->next_result();
- }
-
- array_shift($raw_queries);
-
- $result = $conn->store_result();
- if (!$result && !$this->getErrorCode($conn)) {
- $result = true;
- }
- $results[$key] = $this->processResult($result);
- }
-
- if ($conn->more_results()) {
- throw new Exception(
- pht('There are some results left in the result set.'));
- }
-
- return $results;
- }
-
- protected function freeResult($result) {
- $result->free_result();
- }
-
- protected function fetchAssoc($result) {
- return $result->fetch_assoc();
- }
-
- protected function getErrorCode($connection) {
- return $connection->errno;
- }
-
- protected function getErrorDescription($connection) {
- return $connection->error;
- }
-
- public function supportsAsyncQueries() {
- return defined('MYSQLI_ASYNC');
- }
-
- public function asyncQuery($raw_query) {
- $this->checkWrite($raw_query);
- $async = $this->beginAsyncConnection();
- $async->query($raw_query, MYSQLI_ASYNC);
- return $async;
- }
-
- public static function resolveAsyncQueries(array $conns, array $asyncs) {
- assert_instances_of($conns, __CLASS__);
- assert_instances_of($asyncs, 'mysqli');
-
- $read = $error = $reject = array();
- foreach ($asyncs as $async) {
- $read[] = $error[] = $reject[] = $async;
- }
-
- if (!mysqli::poll($read, $error, $reject, 0)) {
- return array();
- }
-
- $results = array();
- foreach ($read as $async) {
- $key = array_search($async, $asyncs, $strict = true);
- $conn = $conns[$key];
- $conn->endAsyncConnection($async);
- $results[$key] = $conn->processResult($async->reap_async_query());
- }
- return $results;
- }
-
-}
diff --git a/src/aphront/storage/exception/AphrontAccessDeniedQueryException.php b/src/aphront/storage/exception/AphrontAccessDeniedQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontAccessDeniedQueryException.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-final class AphrontAccessDeniedQueryException
- extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontCharacterSetQueryException.php b/src/aphront/storage/exception/AphrontCharacterSetQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontCharacterSetQueryException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-final class AphrontCharacterSetQueryException extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontConnectionLostQueryException.php b/src/aphront/storage/exception/AphrontConnectionLostQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontConnectionLostQueryException.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-final class AphrontConnectionLostQueryException
- extends AphrontRecoverableQueryException {}
diff --git a/src/aphront/storage/exception/AphrontConnectionQueryException.php b/src/aphront/storage/exception/AphrontConnectionQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontConnectionQueryException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-final class AphrontConnectionQueryException extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontCountQueryException.php b/src/aphront/storage/exception/AphrontCountQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontCountQueryException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-final class AphrontCountQueryException extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontDeadlockQueryException.php b/src/aphront/storage/exception/AphrontDeadlockQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontDeadlockQueryException.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-final class AphrontDeadlockQueryException
- extends AphrontRecoverableQueryException {}
diff --git a/src/aphront/storage/exception/AphrontDuplicateKeyQueryException.php b/src/aphront/storage/exception/AphrontDuplicateKeyQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontDuplicateKeyQueryException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-final class AphrontDuplicateKeyQueryException extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontInvalidCredentialsQueryException.php b/src/aphront/storage/exception/AphrontInvalidCredentialsQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontInvalidCredentialsQueryException.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-final class AphrontInvalidCredentialsQueryException
- extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontLockTimeoutQueryException.php b/src/aphront/storage/exception/AphrontLockTimeoutQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontLockTimeoutQueryException.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-final class AphrontLockTimeoutQueryException
- extends AphrontRecoverableQueryException {}
diff --git a/src/aphront/storage/exception/AphrontNotSupportedQueryException.php b/src/aphront/storage/exception/AphrontNotSupportedQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontNotSupportedQueryException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-final class AphrontNotSupportedQueryException extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontObjectMissingQueryException.php b/src/aphront/storage/exception/AphrontObjectMissingQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontObjectMissingQueryException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-final class AphrontObjectMissingQueryException extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontParameterQueryException.php b/src/aphront/storage/exception/AphrontParameterQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontParameterQueryException.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-final class AphrontParameterQueryException extends AphrontQueryException {
-
- private $query;
-
- public function __construct($query, $message) {
- parent::__construct(pht('%s Query: %s', $message, $query));
- $this->query = $query;
- }
-
- public function getQuery() {
- return $this->query;
- }
-
-}
diff --git a/src/aphront/storage/exception/AphrontQueryException.php b/src/aphront/storage/exception/AphrontQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontQueryException.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * @concrete-extensible
- */
-class AphrontQueryException extends Exception {}
diff --git a/src/aphront/storage/exception/AphrontQueryTimeoutQueryException.php b/src/aphront/storage/exception/AphrontQueryTimeoutQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontQueryTimeoutQueryException.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-final class AphrontQueryTimeoutQueryException
- extends AphrontRecoverableQueryException {}
diff --git a/src/aphront/storage/exception/AphrontRecoverableQueryException.php b/src/aphront/storage/exception/AphrontRecoverableQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontRecoverableQueryException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-abstract class AphrontRecoverableQueryException extends AphrontQueryException {}
diff --git a/src/aphront/storage/exception/AphrontSchemaQueryException.php b/src/aphront/storage/exception/AphrontSchemaQueryException.php
deleted file mode 100644
--- a/src/aphront/storage/exception/AphrontSchemaQueryException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-final class AphrontSchemaQueryException extends AphrontQueryException {}
diff --git a/src/auth/PhutilAmazonAuthAdapter.php b/src/auth/PhutilAmazonAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilAmazonAuthAdapter.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Amazon OAuth2.
- */
-final class PhutilAmazonAuthAdapter extends PhutilOAuthAuthAdapter {
-
- public function getAdapterType() {
- return 'amazon';
- }
-
- public function getAdapterDomain() {
- return 'amazon.com';
- }
-
- public function getAccountID() {
- return $this->getOAuthAccountData('user_id');
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('email');
- }
-
- public function getAccountName() {
- return null;
- }
-
- public function getAccountImageURI() {
- return null;
- }
-
- public function getAccountURI() {
- return null;
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('name');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://www.amazon.com/ap/oa';
- }
-
- protected function getTokenBaseURI() {
- return 'https://api.amazon.com/auth/o2/token';
- }
-
- public function getScope() {
- return 'profile';
- }
-
- public function getExtraAuthenticateParameters() {
- return array(
- 'response_type' => 'code',
- );
- }
-
- public function getExtraTokenParameters() {
- return array(
- 'grant_type' => 'authorization_code',
- );
- }
-
- protected function loadOAuthAccountData() {
- $uri = new PhutilURI('https://api.amazon.com/user/profile');
- $uri->replaceQueryParam('access_token', $this->getAccessToken());
-
- $future = new HTTPSFuture($uri);
- list($body) = $future->resolvex();
-
- try {
- return phutil_json_decode($body);
- } catch (PhutilJSONParserException $ex) {
- throw new PhutilProxyException(
- pht('Expected valid JSON response from Amazon account data request.'),
- $ex);
- }
- }
-
-}
diff --git a/src/auth/PhutilAsanaAuthAdapter.php b/src/auth/PhutilAsanaAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilAsanaAuthAdapter.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Asana OAuth2.
- */
-final class PhutilAsanaAuthAdapter extends PhutilOAuthAuthAdapter {
-
- public function getAdapterType() {
- return 'asana';
- }
-
- public function getAdapterDomain() {
- return 'asana.com';
- }
-
- public function getAccountID() {
- return $this->getOAuthAccountData('id');
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('email');
- }
-
- public function getAccountName() {
- return null;
- }
-
- public function getAccountImageURI() {
- $photo = $this->getOAuthAccountData('photo', array());
- if (is_array($photo)) {
- return idx($photo, 'image_128x128');
- } else {
- return null;
- }
- }
-
- public function getAccountURI() {
- return null;
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('name');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://app.asana.com/-/oauth_authorize';
- }
-
- protected function getTokenBaseURI() {
- return 'https://app.asana.com/-/oauth_token';
- }
-
- public function getScope() {
- return null;
- }
-
- public function getExtraAuthenticateParameters() {
- return array(
- 'response_type' => 'code',
- );
- }
-
- public function getExtraTokenParameters() {
- return array(
- 'grant_type' => 'authorization_code',
- );
- }
-
- public function getExtraRefreshParameters() {
- return array(
- 'grant_type' => 'refresh_token',
- );
- }
-
- public function supportsTokenRefresh() {
- return true;
- }
-
- protected function loadOAuthAccountData() {
- return id(new PhutilAsanaFuture())
- ->setAccessToken($this->getAccessToken())
- ->setRawAsanaQuery('users/me')
- ->resolve();
- }
-
-}
diff --git a/src/auth/PhutilAuthAdapter.php b/src/auth/PhutilAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilAuthAdapter.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-/**
- * Abstract interface to an identity provider or authentication source, like
- * Twitter, Facebook or Google.
- *
- * Generally, adapters are handed some set of credentials particular to the
- * provider they adapt, and they turn those credentials into standard
- * information about the user's identity. For example, the LDAP adapter is given
- * a username and password (and some other configuration information), uses them
- * to talk to the LDAP server, and produces a username, email, and so forth.
- *
- * Since the credentials a provider requires are specific to each provider, the
- * base adapter does not specify how an adapter should be constructed or
- * configured -- only what information it is expected to be able to provide once
- * properly configured.
- */
-abstract class PhutilAuthAdapter extends Phobject {
-
- /**
- * Get a unique identifier associated with the identity. For most providers,
- * this is an account ID.
- *
- * The account ID needs to be unique within this adapter's configuration, such
- * that `<adapterKey, accountID>` is globally unique and always identifies the
- * same identity.
- *
- * If the adapter was unable to authenticate an identity, it should return
- * `null`.
- *
- * @return string|null Unique account identifier, or `null` if authentication
- * failed.
- */
- abstract public function getAccountID();
-
-
- /**
- * Get a string identifying this adapter, like "ldap". This string should be
- * unique to the adapter class.
- *
- * @return string Unique adapter identifier.
- */
- abstract public function getAdapterType();
-
-
- /**
- * Get a string identifying the domain this adapter is acting on. This allows
- * an adapter (like LDAP) to act against different identity domains without
- * conflating credentials. For providers like Facebook or Google, the adapters
- * just return the relevant domain name.
- *
- * @return string Domain the adapter is associated with.
- */
- abstract public function getAdapterDomain();
-
-
- /**
- * Generate a string uniquely identifying this adapter configuration. Within
- * the scope of a given key, all account IDs must uniquely identify exactly
- * one identity.
- *
- * @return string Unique identifier for this adapter configuration.
- */
- public function getAdapterKey() {
- return $this->getAdapterType().':'.$this->getAdapterDomain();
- }
-
-
- /**
- * Optionally, return an email address associated with this account.
- *
- * @return string|null An email address associated with the account, or
- * `null` if data is not available.
- */
- public function getAccountEmail() {
- return null;
- }
-
-
- /**
- * Optionally, return a human readable username associated with this account.
- *
- * @return string|null Account username, or `null` if data isn't available.
- */
- public function getAccountName() {
- return null;
- }
-
-
- /**
- * Optionally, return a URI corresponding to a human-viewable profile for
- * this account.
- *
- * @return string|null A profile URI associated with this account, or
- * `null` if the data isn't available.
- */
- public function getAccountURI() {
- return null;
- }
-
-
- /**
- * Optionally, return a profile image URI associated with this account.
- *
- * @return string|null URI for an account profile image, or `null` if one is
- * not available.
- */
- public function getAccountImageURI() {
- return null;
- }
-
-
- /**
- * Optionally, return a real name associated with this account.
- *
- * @return string|null A human real name, or `null` if this data is not
- * available.
- */
- public function getAccountRealName() {
- return null;
- }
-
-}
diff --git a/src/auth/PhutilBitbucketAuthAdapter.php b/src/auth/PhutilBitbucketAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilBitbucketAuthAdapter.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-final class PhutilBitbucketAuthAdapter extends PhutilOAuth1AuthAdapter {
-
- private $userInfo;
-
- public function getAccountID() {
- return idx($this->getUserInfo(), 'username');
- }
-
- public function getAccountName() {
- return idx($this->getUserInfo(), 'display_name');
- }
-
- public function getAccountURI() {
- $name = $this->getAccountID();
- if (strlen($name)) {
- return 'https://bitbucket.org/'.$name;
- }
- return null;
- }
-
- public function getAccountImageURI() {
- return idx($this->getUserInfo(), 'avatar');
- }
-
- public function getAccountRealName() {
- $parts = array(
- idx($this->getUserInfo(), 'first_name'),
- idx($this->getUserInfo(), 'last_name'),
- );
- $parts = array_filter($parts);
- return implode(' ', $parts);
- }
-
- public function getAdapterType() {
- return 'bitbucket';
- }
-
- public function getAdapterDomain() {
- return 'bitbucket.org';
- }
-
- protected function getRequestTokenURI() {
- return 'https://bitbucket.org/api/1.0/oauth/request_token';
- }
-
- protected function getAuthorizeTokenURI() {
- return 'https://bitbucket.org/api/1.0/oauth/authenticate';
- }
-
- protected function getValidateTokenURI() {
- return 'https://bitbucket.org/api/1.0/oauth/access_token';
- }
-
- private function getUserInfo() {
- if ($this->userInfo === null) {
- // We don't need any of the data in the handshake, but do need to
- // finish the process. This makes sure we've completed the handshake.
- $this->getHandshakeData();
-
- $uri = new PhutilURI('https://bitbucket.org/api/1.0/user');
-
- $data = $this->newOAuth1Future($uri)
- ->setMethod('GET')
- ->resolveJSON();
-
- $this->userInfo = idx($data, 'user', array());
- }
- return $this->userInfo;
- }
-
-}
diff --git a/src/auth/PhutilDisqusAuthAdapter.php b/src/auth/PhutilDisqusAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilDisqusAuthAdapter.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Disqus OAuth2.
- */
-final class PhutilDisqusAuthAdapter extends PhutilOAuthAuthAdapter {
-
- public function getAdapterType() {
- return 'disqus';
- }
-
- public function getAdapterDomain() {
- return 'disqus.com';
- }
-
- public function getAccountID() {
- return $this->getOAuthAccountData('id');
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('email');
- }
-
- public function getAccountName() {
- return $this->getOAuthAccountData('username');
- }
-
- public function getAccountImageURI() {
- return $this->getOAuthAccountData('avatar', 'permalink');
- }
-
- public function getAccountURI() {
- return $this->getOAuthAccountData('profileUrl');
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('name');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://disqus.com/api/oauth/2.0/authorize/';
- }
-
- protected function getTokenBaseURI() {
- return 'https://disqus.com/api/oauth/2.0/access_token/';
- }
-
- public function getScope() {
- return 'read';
- }
-
- public function getExtraAuthenticateParameters() {
- return array(
- 'response_type' => 'code',
- );
- }
-
- public function getExtraTokenParameters() {
- return array(
- 'grant_type' => 'authorization_code',
- );
- }
-
- protected function loadOAuthAccountData() {
- $uri = new PhutilURI('https://disqus.com/api/3.0/users/details.json');
- $uri->replaceQueryParam('api_key', $this->getClientID());
- $uri->replaceQueryParam('access_token', $this->getAccessToken());
- $uri = (string)$uri;
-
- $future = new HTTPSFuture($uri);
- $future->setMethod('GET');
- list($body) = $future->resolvex();
-
- try {
- $data = phutil_json_decode($body);
- return $data['response'];
- } catch (PhutilJSONParserException $ex) {
- throw new PhutilProxyException(
- pht('Expected valid JSON response from Disqus account data request.'),
- $ex);
- }
- }
-
-}
diff --git a/src/auth/PhutilEmptyAuthAdapter.php b/src/auth/PhutilEmptyAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilEmptyAuthAdapter.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-/**
- * Empty authentication adapter with no logic.
- *
- * This adapter can be used when you need an adapter for some technical reason
- * but it doesn't make sense to put logic inside it.
- */
-final class PhutilEmptyAuthAdapter extends PhutilAuthAdapter {
-
- private $accountID;
- private $adapterType;
- private $adapterDomain;
-
- public function setAdapterDomain($adapter_domain) {
- $this->adapterDomain = $adapter_domain;
- return $this;
- }
-
- public function getAdapterDomain() {
- return $this->adapterDomain;
- }
-
- public function setAdapterType($adapter_type) {
- $this->adapterType = $adapter_type;
- return $this;
- }
-
- public function getAdapterType() {
- return $this->adapterType;
- }
-
- public function setAccountID($account_id) {
- $this->accountID = $account_id;
- return $this;
- }
-
- public function getAccountID() {
- return $this->accountID;
- }
-
-}
diff --git a/src/auth/PhutilFacebookAuthAdapter.php b/src/auth/PhutilFacebookAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilFacebookAuthAdapter.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Facebook OAuth2.
- */
-final class PhutilFacebookAuthAdapter extends PhutilOAuthAuthAdapter {
-
- private $requireSecureBrowsing;
-
- public function setRequireSecureBrowsing($require_secure_browsing) {
- $this->requireSecureBrowsing = $require_secure_browsing;
- return $this;
- }
-
- public function getAdapterType() {
- return 'facebook';
- }
-
- public function getAdapterDomain() {
- return 'facebook.com';
- }
-
- public function getAccountID() {
- return $this->getOAuthAccountData('id');
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('email');
- }
-
- public function getAccountName() {
- $link = $this->getOAuthAccountData('link');
- if (!$link) {
- return null;
- }
-
- $matches = null;
- if (!preg_match('@/([^/]+)$@', $link, $matches)) {
- return null;
- }
-
- return $matches[1];
- }
-
- public function getAccountImageURI() {
- $picture = $this->getOAuthAccountData('picture');
- if ($picture) {
- $picture_data = idx($picture, 'data');
- if ($picture_data) {
- return idx($picture_data, 'url');
- }
- }
- return null;
- }
-
- public function getAccountURI() {
- return $this->getOAuthAccountData('link');
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('name');
- }
-
- public function getAccountSecuritySettings() {
- return $this->getOAuthAccountData('security_settings');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://www.facebook.com/dialog/oauth';
- }
-
- protected function getTokenBaseURI() {
- return 'https://graph.facebook.com/oauth/access_token';
- }
-
- protected function loadOAuthAccountData() {
- $fields = array(
- 'id',
- 'name',
- 'email',
- 'link',
- 'security_settings',
- 'picture',
- );
-
- $uri = new PhutilURI('https://graph.facebook.com/me');
- $uri->replaceQueryParam('access_token', $this->getAccessToken());
- $uri->replaceQueryParam('fields', implode(',', $fields));
- list($body) = id(new HTTPSFuture($uri))->resolvex();
-
- $data = null;
- try {
- $data = phutil_json_decode($body);
- } catch (PhutilJSONParserException $ex) {
- throw new PhutilProxyException(
- pht('Expected valid JSON response from Facebook account data request.'),
- $ex);
- }
-
- if ($this->requireSecureBrowsing) {
- if (empty($data['security_settings']['secure_browsing']['enabled'])) {
- throw new Exception(
- pht(
- 'This Phabricator install requires you to enable Secure Browsing '.
- 'on your Facebook account in order to use it to log in to '.
- 'Phabricator. For more information, see %s',
- 'https://www.facebook.com/help/156201551113407/'));
- }
- }
-
- return $data;
- }
-
-}
diff --git a/src/auth/PhutilGitHubAuthAdapter.php b/src/auth/PhutilGitHubAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilGitHubAuthAdapter.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Github OAuth2.
- */
-final class PhutilGitHubAuthAdapter extends PhutilOAuthAuthAdapter {
-
- public function getAdapterType() {
- return 'github';
- }
-
- public function getAdapterDomain() {
- return 'github.com';
- }
-
- public function getAccountID() {
- return $this->getOAuthAccountData('id');
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('email');
- }
-
- public function getAccountName() {
- return $this->getOAuthAccountData('login');
- }
-
- public function getAccountImageURI() {
- return $this->getOAuthAccountData('avatar_url');
- }
-
- public function getAccountURI() {
- $name = $this->getAccountName();
- if (strlen($name)) {
- return 'https://github.com/'.$name;
- }
- return null;
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('name');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://github.com/login/oauth/authorize';
- }
-
- protected function getTokenBaseURI() {
- return 'https://github.com/login/oauth/access_token';
- }
-
- protected function loadOAuthAccountData() {
- $uri = new PhutilURI('https://api.github.com/user');
- $uri->replaceQueryParam('access_token', $this->getAccessToken());
-
- $future = new HTTPSFuture($uri);
-
- // NOTE: GitHub requires a User-Agent string.
- $future->addHeader('User-Agent', __CLASS__);
-
- list($body) = $future->resolvex();
-
- try{
- return phutil_json_decode($body);
- } catch (PhutilJSONParserException $ex) {
- throw new PhutilProxyException(
- pht('Expected valid JSON response from GitHub account data request.'),
- $ex);
- }
- }
-
-}
diff --git a/src/auth/PhutilGoogleAuthAdapter.php b/src/auth/PhutilGoogleAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilGoogleAuthAdapter.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Google OAuth2.
- */
-final class PhutilGoogleAuthAdapter extends PhutilOAuthAuthAdapter {
-
- public function getAdapterType() {
- return 'google';
- }
-
- public function getAdapterDomain() {
- return 'google.com';
- }
-
- public function getAccountID() {
- return $this->getAccountEmail();
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('email');
- }
-
- public function getAccountName() {
- // Guess account name from email address, this is just a hint anyway.
- $email = $this->getAccountEmail();
- $email = explode('@', $email);
- $email = head($email);
- return $email;
- }
-
- public function getAccountImageURI() {
- $uri = $this->getOAuthAccountData('picture');
-
- // Change the "sz" parameter ("size") from the default to 100 to ask for
- // a 100x100px image.
- if ($uri !== null) {
- $uri = new PhutilURI($uri);
- $uri->replaceQueryParam('sz', 100);
- $uri = (string)$uri;
- }
-
- return $uri;
- }
-
- public function getAccountURI() {
- return $this->getOAuthAccountData('link');
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('name');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://accounts.google.com/o/oauth2/auth';
- }
-
- protected function getTokenBaseURI() {
- return 'https://accounts.google.com/o/oauth2/token';
- }
-
- public function getScope() {
- $scopes = array(
- 'email',
- 'profile',
- );
-
- return implode(' ', $scopes);
- }
-
- public function getExtraAuthenticateParameters() {
- return array(
- 'response_type' => 'code',
- );
- }
-
- public function getExtraTokenParameters() {
- return array(
- 'grant_type' => 'authorization_code',
- );
- }
-
- protected function loadOAuthAccountData() {
- $uri = new PhutilURI('https://www.googleapis.com/userinfo/v2/me');
- $uri->replaceQueryParam('access_token', $this->getAccessToken());
-
- $future = new HTTPSFuture($uri);
- list($status, $body) = $future->resolve();
-
- if ($status->isError()) {
- throw $status;
- }
-
- try {
- $result = phutil_json_decode($body);
- } catch (PhutilJSONParserException $ex) {
- throw new PhutilProxyException(
- pht('Expected valid JSON response from Google account data request.'),
- $ex);
- }
-
- return $result;
- }
-
-}
diff --git a/src/auth/PhutilJIRAAuthAdapter.php b/src/auth/PhutilJIRAAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilJIRAAuthAdapter.php
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for JIRA OAuth1.
- */
-final class PhutilJIRAAuthAdapter extends PhutilOAuth1AuthAdapter {
-
- // TODO: JIRA tokens expire (after 5 years) and we could surface and store
- // that.
-
- private $jiraBaseURI;
- private $adapterDomain;
- private $currentSession;
- private $userInfo;
-
- public function setJIRABaseURI($jira_base_uri) {
- $this->jiraBaseURI = $jira_base_uri;
- return $this;
- }
-
- public function getJIRABaseURI() {
- return $this->jiraBaseURI;
- }
-
- public function getAccountID() {
- // Make sure the handshake is finished; this method is used for its
- // side effect by Auth providers.
- $this->getHandshakeData();
-
- return idx($this->getUserInfo(), 'key');
- }
-
- public function getAccountName() {
- return idx($this->getUserInfo(), 'name');
- }
-
- public function getAccountImageURI() {
- $avatars = idx($this->getUserInfo(), 'avatarUrls');
- if ($avatars) {
- return idx($avatars, '48x48');
- }
- return null;
- }
-
- public function getAccountRealName() {
- return idx($this->getUserInfo(), 'displayName');
- }
-
- public function getAccountEmail() {
- return idx($this->getUserInfo(), 'emailAddress');
- }
-
- public function getAdapterType() {
- return 'jira';
- }
-
- public function getAdapterDomain() {
- return $this->adapterDomain;
- }
-
- public function setAdapterDomain($domain) {
- $this->adapterDomain = $domain;
- return $this;
- }
-
- protected function getSignatureMethod() {
- return 'RSA-SHA1';
- }
-
- protected function getRequestTokenURI() {
- return $this->getJIRAURI('plugins/servlet/oauth/request-token');
- }
-
- protected function getAuthorizeTokenURI() {
- return $this->getJIRAURI('plugins/servlet/oauth/authorize');
- }
-
- protected function getValidateTokenURI() {
- return $this->getJIRAURI('plugins/servlet/oauth/access-token');
- }
-
- private function getJIRAURI($path) {
- return rtrim($this->jiraBaseURI, '/').'/'.ltrim($path, '/');
- }
-
- private function getUserInfo() {
- if ($this->userInfo === null) {
- $this->currentSession = $this->newJIRAFuture('rest/auth/1/session', 'GET')
- ->resolveJSON();
-
- // The session call gives us the username, but not the user key or other
- // information. Make a second call to get additional information.
-
- $params = array(
- 'username' => $this->currentSession['name'],
- );
-
- $this->userInfo = $this->newJIRAFuture('rest/api/2/user', 'GET', $params)
- ->resolveJSON();
- }
-
- return $this->userInfo;
- }
-
- public static function newJIRAKeypair() {
- $config = array(
- 'digest_alg' => 'sha512',
- 'private_key_bits' => 4096,
- 'private_key_type' => OPENSSL_KEYTYPE_RSA,
- );
-
- $res = openssl_pkey_new($config);
- if (!$res) {
- throw new Exception(pht('%s failed!', 'openssl_pkey_new()'));
- }
-
- $private_key = null;
- $ok = openssl_pkey_export($res, $private_key);
- if (!$ok) {
- throw new Exception(pht('%s failed!', 'openssl_pkey_export()'));
- }
-
- $public_key = openssl_pkey_get_details($res);
- if (!$ok || empty($public_key['key'])) {
- throw new Exception(pht('%s failed!', 'openssl_pkey_get_details()'));
- }
- $public_key = $public_key['key'];
-
- return array($public_key, $private_key);
- }
-
-
- /**
- * JIRA indicates that the user has clicked the "Deny" button by passing a
- * well known `oauth_verifier` value ("denied"), which we check for here.
- */
- protected function willFinishOAuthHandshake() {
- $jira_magic_word = 'denied';
- if ($this->getVerifier() == $jira_magic_word) {
- throw new PhutilAuthUserAbortedException();
- }
- }
-
- public function newJIRAFuture($path, $method, $params = array()) {
- if ($method == 'GET') {
- $uri_params = $params;
- $body_params = array();
- } else {
- // For other types of requests, JIRA expects the request body to be
- // JSON encoded.
- $uri_params = array();
- $body_params = phutil_json_encode($params);
- }
-
- $uri = new PhutilURI($this->getJIRAURI($path), $uri_params);
-
- // JIRA returns a 415 error if we don't provide a Content-Type header.
-
- return $this->newOAuth1Future($uri, $body_params)
- ->setMethod($method)
- ->addHeader('Content-Type', 'application/json');
- }
-
-}
diff --git a/src/auth/PhutilLDAPAuthAdapter.php b/src/auth/PhutilLDAPAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilLDAPAuthAdapter.php
+++ /dev/null
@@ -1,505 +0,0 @@
-<?php
-
-/**
- * Retrieve identify information from LDAP accounts.
- */
-final class PhutilLDAPAuthAdapter extends PhutilAuthAdapter {
-
- private $hostname;
- private $port = 389;
-
- private $baseDistinguishedName;
- private $searchAttributes = array();
- private $usernameAttribute;
- private $realNameAttributes = array();
- private $ldapVersion = 3;
- private $ldapReferrals;
- private $ldapStartTLS;
- private $anonymousUsername;
- private $anonymousPassword;
- private $activeDirectoryDomain;
- private $alwaysSearch;
-
- private $loginUsername;
- private $loginPassword;
-
- private $ldapUserData;
- private $ldapConnection;
-
- public function getAdapterType() {
- return 'ldap';
- }
-
- public function setHostname($host) {
- $this->hostname = $host;
- return $this;
- }
-
- public function setPort($port) {
- $this->port = $port;
- return $this;
- }
-
- public function getAdapterDomain() {
- return 'self';
- }
-
- public function setBaseDistinguishedName($base_distinguished_name) {
- $this->baseDistinguishedName = $base_distinguished_name;
- return $this;
- }
-
- public function setSearchAttributes(array $search_attributes) {
- $this->searchAttributes = $search_attributes;
- return $this;
- }
-
- public function setUsernameAttribute($username_attribute) {
- $this->usernameAttribute = $username_attribute;
- return $this;
- }
-
- public function setRealNameAttributes(array $attributes) {
- $this->realNameAttributes = $attributes;
- return $this;
- }
-
- public function setLDAPVersion($ldap_version) {
- $this->ldapVersion = $ldap_version;
- return $this;
- }
-
- public function setLDAPReferrals($ldap_referrals) {
- $this->ldapReferrals = $ldap_referrals;
- return $this;
- }
-
- public function setLDAPStartTLS($ldap_start_tls) {
- $this->ldapStartTLS = $ldap_start_tls;
- return $this;
- }
-
- public function setAnonymousUsername($anonymous_username) {
- $this->anonymousUsername = $anonymous_username;
- return $this;
- }
-
- public function setAnonymousPassword(
- PhutilOpaqueEnvelope $anonymous_password) {
- $this->anonymousPassword = $anonymous_password;
- return $this;
- }
-
- public function setLoginUsername($login_username) {
- $this->loginUsername = $login_username;
- return $this;
- }
-
- public function setLoginPassword(PhutilOpaqueEnvelope $login_password) {
- $this->loginPassword = $login_password;
- return $this;
- }
-
- public function setActiveDirectoryDomain($domain) {
- $this->activeDirectoryDomain = $domain;
- return $this;
- }
-
- public function setAlwaysSearch($always_search) {
- $this->alwaysSearch = $always_search;
- return $this;
- }
-
- public function getAccountID() {
- return $this->readLDAPRecordAccountID($this->getLDAPUserData());
- }
-
- public function getAccountName() {
- return $this->readLDAPRecordAccountName($this->getLDAPUserData());
- }
-
- public function getAccountRealName() {
- return $this->readLDAPRecordRealName($this->getLDAPUserData());
- }
-
- public function getAccountEmail() {
- return $this->readLDAPRecordEmail($this->getLDAPUserData());
- }
-
- public function readLDAPRecordAccountID(array $record) {
- $key = $this->usernameAttribute;
- if (!strlen($key)) {
- $key = head($this->searchAttributes);
- }
- return $this->readLDAPData($record, $key);
- }
-
- public function readLDAPRecordAccountName(array $record) {
- return $this->readLDAPRecordAccountID($record);
- }
-
- public function readLDAPRecordRealName(array $record) {
- $parts = array();
- foreach ($this->realNameAttributes as $attribute) {
- $parts[] = $this->readLDAPData($record, $attribute);
- }
- $parts = array_filter($parts);
-
- if ($parts) {
- return implode(' ', $parts);
- }
-
- return null;
- }
-
- public function readLDAPRecordEmail(array $record) {
- return $this->readLDAPData($record, 'mail');
- }
-
- private function getLDAPUserData() {
- if ($this->ldapUserData === null) {
- $this->ldapUserData = $this->loadLDAPUserData();
- }
-
- return $this->ldapUserData;
- }
-
- private function readLDAPData(array $data, $key, $default = null) {
- $list = idx($data, $key);
- if ($list === null) {
- // At least in some cases (and maybe in all cases) the results from
- // ldap_search() are keyed in lowercase. If we missed on the first
- // try, retry with a lowercase key.
- $list = idx($data, phutil_utf8_strtolower($key));
- }
-
- // NOTE: In most cases, the property is an array, like:
- //
- // array(
- // 'count' => 1,
- // 0 => 'actual-value-we-want',
- // )
- //
- // However, in at least the case of 'dn', the property is a bare string.
-
- if (is_scalar($list) && strlen($list)) {
- return $list;
- } else if (is_array($list)) {
- return $list[0];
- } else {
- return $default;
- }
- }
-
- private function formatLDAPAttributeSearch($attribute, $login_user) {
- // If the attribute contains the literal token "${login}", treat it as a
- // query and substitute the user's login name for the token.
-
- if (strpos($attribute, '${login}') !== false) {
- $escaped_user = ldap_sprintf('%S', $login_user);
- $attribute = str_replace('${login}', $escaped_user, $attribute);
- return $attribute;
- }
-
- // Otherwise, treat it as a simple attribute search.
-
- return ldap_sprintf(
- '%Q=%S',
- $attribute,
- $login_user);
- }
-
- private function loadLDAPUserData() {
- $conn = $this->establishConnection();
-
- $login_user = $this->loginUsername;
- $login_pass = $this->loginPassword;
-
- if ($this->shouldBindWithoutIdentity()) {
- $distinguished_name = null;
- $search_query = null;
- foreach ($this->searchAttributes as $attribute) {
- $search_query = $this->formatLDAPAttributeSearch(
- $attribute,
- $login_user);
- $record = $this->searchLDAPForRecord($search_query);
- if ($record) {
- $distinguished_name = $this->readLDAPData($record, 'dn');
- break;
- }
- }
- if ($distinguished_name === null) {
- throw new PhutilAuthCredentialException();
- }
- } else {
- $search_query = $this->formatLDAPAttributeSearch(
- head($this->searchAttributes),
- $login_user);
- if ($this->activeDirectoryDomain) {
- $distinguished_name = ldap_sprintf(
- '%s@%Q',
- $login_user,
- $this->activeDirectoryDomain);
- } else {
- $distinguished_name = ldap_sprintf(
- '%Q,%Q',
- $search_query,
- $this->baseDistinguishedName);
- }
- }
-
- $this->bindLDAP($conn, $distinguished_name, $login_pass);
-
- $result = $this->searchLDAPForRecord($search_query);
- if (!$result) {
- // This is unusual (since the bind succeeded) but we've seen it at least
- // once in the wild, where the anonymous user is allowed to search but
- // the credentialed user is not.
-
- // If we don't have anonymous credentials, raise an explicit exception
- // here since we'll fail a typehint if we don't return an array anyway
- // and this is a more useful error.
-
- // If we do have anonymous credentials, we'll rebind and try the search
- // again below. Doing this automatically means things work correctly more
- // often without requiring additional configuration.
- if (!$this->shouldBindWithoutIdentity()) {
- // No anonymous credentials, so we just fail here.
- throw new Exception(
- pht(
- 'LDAP: Failed to retrieve record for user "%s" when searching. '.
- 'Credentialed users may not be able to search your LDAP server. '.
- 'Try configuring anonymous credentials or fully anonymous binds.',
- $login_user));
- } else {
- // Rebind as anonymous and try the search again.
- $user = $this->anonymousUsername;
- $pass = $this->anonymousPassword;
- $this->bindLDAP($conn, $user, $pass);
-
- $result = $this->searchLDAPForRecord($search_query);
- if (!$result) {
- throw new Exception(
- pht(
- 'LDAP: Failed to retrieve record for user "%s" when searching '.
- 'with both user and anonymous credentials.',
- $login_user));
- }
- }
- }
-
- return $result;
- }
-
- private function establishConnection() {
- if (!$this->ldapConnection) {
- $host = $this->hostname;
- $port = $this->port;
-
- $profiler = PhutilServiceProfiler::getInstance();
- $call_id = $profiler->beginServiceCall(
- array(
- 'type' => 'ldap',
- 'call' => 'connect',
- 'host' => $host,
- 'port' => $this->port,
- ));
-
- $conn = @ldap_connect($host, $this->port);
-
- $profiler->endServiceCall(
- $call_id,
- array(
- 'ok' => (bool)$conn,
- ));
-
- if (!$conn) {
- throw new Exception(
- pht('Unable to connect to LDAP server (%s:%d).', $host, $port));
- }
-
- $options = array(
- LDAP_OPT_PROTOCOL_VERSION => (int)$this->ldapVersion,
- LDAP_OPT_REFERRALS => (int)$this->ldapReferrals,
- );
-
- foreach ($options as $name => $value) {
- $ok = @ldap_set_option($conn, $name, $value);
- if (!$ok) {
- $this->raiseConnectionException(
- $conn,
- pht(
- "Unable to set LDAP option '%s' to value '%s'!",
- $name,
- $value));
- }
- }
-
- if ($this->ldapStartTLS) {
- $profiler = PhutilServiceProfiler::getInstance();
- $call_id = $profiler->beginServiceCall(
- array(
- 'type' => 'ldap',
- 'call' => 'start-tls',
- ));
-
- // NOTE: This boils down to a function call to ldap_start_tls_s() in
- // C, which is a service call.
- $ok = @ldap_start_tls($conn);
-
- $profiler->endServiceCall(
- $call_id,
- array());
-
- if (!$ok) {
- $this->raiseConnectionException(
- $conn,
- pht('Unable to start TLS connection when connecting to LDAP.'));
- }
- }
-
- if ($this->shouldBindWithoutIdentity()) {
- $user = $this->anonymousUsername;
- $pass = $this->anonymousPassword;
- $this->bindLDAP($conn, $user, $pass);
- }
-
- $this->ldapConnection = $conn;
- }
-
- return $this->ldapConnection;
- }
-
-
- private function searchLDAPForRecord($dn) {
- $conn = $this->establishConnection();
-
- $results = $this->searchLDAP('%Q', $dn);
-
- if (!$results) {
- return null;
- }
-
- if (count($results) > 1) {
- throw new Exception(
- pht(
- 'LDAP record query returned more than one result. The query must '.
- 'uniquely identify a record.'));
- }
-
- return head($results);
- }
-
- public function searchLDAP($pattern /* ... */) {
- $args = func_get_args();
- $query = call_user_func_array('ldap_sprintf', $args);
-
- $conn = $this->establishConnection();
-
- $profiler = PhutilServiceProfiler::getInstance();
- $call_id = $profiler->beginServiceCall(
- array(
- 'type' => 'ldap',
- 'call' => 'search',
- 'dn' => $this->baseDistinguishedName,
- 'query' => $query,
- ));
-
- $result = @ldap_search($conn, $this->baseDistinguishedName, $query);
-
- $profiler->endServiceCall($call_id, array());
-
- if (!$result) {
- $this->raiseConnectionException(
- $conn,
- pht('LDAP search failed.'));
- }
-
- $entries = @ldap_get_entries($conn, $result);
-
- if (!$entries) {
- $this->raiseConnectionException(
- $conn,
- pht('Failed to get LDAP entries from search result.'));
- }
-
- $results = array();
- for ($ii = 0; $ii < $entries['count']; $ii++) {
- $results[] = $entries[$ii];
- }
-
- return $results;
- }
-
- private function raiseConnectionException($conn, $message) {
- $errno = @ldap_errno($conn);
- $error = @ldap_error($conn);
-
- // This is `LDAP_INVALID_CREDENTIALS`.
- if ($errno == 49) {
- throw new PhutilAuthCredentialException();
- }
-
- if ($errno || $error) {
- $full_message = pht(
- "LDAP Exception: %s\nLDAP Error #%d: %s",
- $message,
- $errno,
- $error);
- } else {
- $full_message = pht(
- 'LDAP Exception: %s',
- $message);
- }
-
- throw new Exception($full_message);
- }
-
- private function bindLDAP($conn, $user, PhutilOpaqueEnvelope $pass) {
- $profiler = PhutilServiceProfiler::getInstance();
- $call_id = $profiler->beginServiceCall(
- array(
- 'type' => 'ldap',
- 'call' => 'bind',
- 'user' => $user,
- ));
-
- // NOTE: ldap_bind() dumps cleartext passwords into logs by default. Keep
- // it quiet.
- if (strlen($user)) {
- $ok = @ldap_bind($conn, $user, $pass->openEnvelope());
- } else {
- $ok = @ldap_bind($conn);
- }
-
- $profiler->endServiceCall($call_id, array());
-
- if (!$ok) {
- if (strlen($user)) {
- $this->raiseConnectionException(
- $conn,
- pht('Failed to bind to LDAP server (as user "%s").', $user));
- } else {
- $this->raiseConnectionException(
- $conn,
- pht('Failed to bind to LDAP server (without username).'));
- }
- }
- }
-
-
- /**
- * Determine if this adapter should attempt to bind to the LDAP server
- * without a user identity.
- *
- * Generally, we can bind directly if we have a username/password, or if the
- * "Always Search" flag is set, indicating that the empty username and
- * password are sufficient.
- *
- * @return bool True if the adapter should perform binds without identity.
- */
- private function shouldBindWithoutIdentity() {
- return $this->alwaysSearch || strlen($this->anonymousUsername);
- }
-
-}
diff --git a/src/auth/PhutilOAuth1AuthAdapter.php b/src/auth/PhutilOAuth1AuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilOAuth1AuthAdapter.php
+++ /dev/null
@@ -1,211 +0,0 @@
-<?php
-
-/**
- * Abstract adapter for OAuth1 providers.
- */
-abstract class PhutilOAuth1AuthAdapter extends PhutilAuthAdapter {
-
- private $consumerKey;
- private $consumerSecret;
- private $token;
- private $tokenSecret;
- private $verifier;
- private $handshakeData;
- private $callbackURI;
- private $privateKey;
-
- public function setPrivateKey(PhutilOpaqueEnvelope $private_key) {
- $this->privateKey = $private_key;
- return $this;
- }
-
- public function getPrivateKey() {
- return $this->privateKey;
- }
-
- public function setCallbackURI($callback_uri) {
- $this->callbackURI = $callback_uri;
- return $this;
- }
-
- public function getCallbackURI() {
- return $this->callbackURI;
- }
-
- public function setVerifier($verifier) {
- $this->verifier = $verifier;
- return $this;
- }
-
- public function getVerifier() {
- return $this->verifier;
- }
-
- public function setConsumerSecret(PhutilOpaqueEnvelope $consumer_secret) {
- $this->consumerSecret = $consumer_secret;
- return $this;
- }
-
- public function getConsumerSecret() {
- return $this->consumerSecret;
- }
-
- public function setConsumerKey($consumer_key) {
- $this->consumerKey = $consumer_key;
- return $this;
- }
-
- public function getConsumerKey() {
- return $this->consumerKey;
- }
-
- public function setTokenSecret($token_secret) {
- $this->tokenSecret = $token_secret;
- return $this;
- }
-
- public function getTokenSecret() {
- return $this->tokenSecret;
- }
-
- public function setToken($token) {
- $this->token = $token;
- return $this;
- }
-
- public function getToken() {
- return $this->token;
- }
-
- protected function getHandshakeData() {
- if ($this->handshakeData === null) {
- $this->finishOAuthHandshake();
- }
- return $this->handshakeData;
- }
-
- abstract protected function getRequestTokenURI();
- abstract protected function getAuthorizeTokenURI();
- abstract protected function getValidateTokenURI();
-
- protected function getSignatureMethod() {
- return 'HMAC-SHA1';
- }
-
- public function getContentSecurityPolicyFormActions() {
- return array(
- $this->getAuthorizeTokenURI(),
- );
- }
-
- protected function newOAuth1Future($uri, $data = array()) {
- $future = id(new PhutilOAuth1Future($uri, $data))
- ->setMethod('POST')
- ->setSignatureMethod($this->getSignatureMethod());
-
- $consumer_key = $this->getConsumerKey();
- if (strlen($consumer_key)) {
- $future->setConsumerKey($consumer_key);
- } else {
- throw new Exception(
- pht(
- '%s is required!',
- 'setConsumerKey()'));
- }
-
- $consumer_secret = $this->getConsumerSecret();
- if ($consumer_secret) {
- $future->setConsumerSecret($consumer_secret);
- }
-
- if (strlen($this->getToken())) {
- $future->setToken($this->getToken());
- }
-
- if (strlen($this->getTokenSecret())) {
- $future->setTokenSecret($this->getTokenSecret());
- }
-
- if ($this->getPrivateKey()) {
- $future->setPrivateKey($this->getPrivateKey());
- }
-
- return $future;
- }
-
- public function getClientRedirectURI() {
- $request_token_uri = $this->getRequestTokenURI();
-
- $future = $this->newOAuth1Future($request_token_uri);
- if (strlen($this->getCallbackURI())) {
- $future->setCallbackURI($this->getCallbackURI());
- }
-
- list($body) = $future->resolvex();
- $data = id(new PhutilQueryStringParser())->parseQueryString($body);
-
- // NOTE: Per the spec, this value MUST be the string 'true'.
- $confirmed = idx($data, 'oauth_callback_confirmed');
- if ($confirmed !== 'true') {
- throw new Exception(
- pht("Expected '%s' to be '%s'!", 'oauth_callback_confirmed', 'true'));
- }
-
- $this->readTokenAndTokenSecret($data);
-
- $authorize_token_uri = new PhutilURI($this->getAuthorizeTokenURI());
- $authorize_token_uri->replaceQueryParam('oauth_token', $this->getToken());
-
- return (string)$authorize_token_uri;
- }
-
- protected function finishOAuthHandshake() {
- $this->willFinishOAuthHandshake();
-
- if (!$this->getToken()) {
- throw new Exception(pht('Expected token to finish OAuth handshake!'));
- }
- if (!$this->getVerifier()) {
- throw new Exception(pht('Expected verifier to finish OAuth handshake!'));
- }
-
- $validate_uri = $this->getValidateTokenURI();
- $params = array(
- 'oauth_verifier' => $this->getVerifier(),
- );
-
- list($body) = $this->newOAuth1Future($validate_uri, $params)->resolvex();
- $data = id(new PhutilQueryStringParser())->parseQueryString($body);
-
- $this->readTokenAndTokenSecret($data);
-
- $this->handshakeData = $data;
- }
-
- private function readTokenAndTokenSecret(array $data) {
- $token = idx($data, 'oauth_token');
- if (!$token) {
- throw new Exception(pht("Expected '%s' in response!", 'oauth_token'));
- }
-
- $token_secret = idx($data, 'oauth_token_secret');
- if (!$token_secret) {
- throw new Exception(
- pht("Expected '%s' in response!", 'oauth_token_secret'));
- }
-
- $this->setToken($token);
- $this->setTokenSecret($token_secret);
-
- return $this;
- }
-
- /**
- * Hook that allows subclasses to take actions before the OAuth handshake
- * is completed.
- */
- protected function willFinishOAuthHandshake() {
- return;
- }
-
-}
diff --git a/src/auth/PhutilOAuthAuthAdapter.php b/src/auth/PhutilOAuthAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilOAuthAuthAdapter.php
+++ /dev/null
@@ -1,228 +0,0 @@
-<?php
-
-/**
- * Abstract adapter for OAuth2 providers.
- */
-abstract class PhutilOAuthAuthAdapter extends PhutilAuthAdapter {
-
- private $clientID;
- private $clientSecret;
- private $redirectURI;
- private $scope;
- private $state;
- private $code;
-
- private $accessTokenData;
- private $oauthAccountData;
-
- abstract protected function getAuthenticateBaseURI();
- abstract protected function getTokenBaseURI();
- abstract protected function loadOAuthAccountData();
-
- public function getAuthenticateURI() {
- $params = array(
- 'client_id' => $this->getClientID(),
- 'scope' => $this->getScope(),
- 'redirect_uri' => $this->getRedirectURI(),
- 'state' => $this->getState(),
- ) + $this->getExtraAuthenticateParameters();
-
- $uri = new PhutilURI($this->getAuthenticateBaseURI(), $params);
-
- return phutil_string_cast($uri);
- }
-
- public function getAdapterType() {
- $this_class = get_class($this);
- $type_name = str_replace('PhutilAuthAdapterOAuth', '', $this_class);
- return strtolower($type_name);
- }
-
- public function setState($state) {
- $this->state = $state;
- return $this;
- }
-
- public function getState() {
- return $this->state;
- }
-
- public function setCode($code) {
- $this->code = $code;
- return $this;
- }
-
- public function getCode() {
- return $this->code;
- }
-
- public function setRedirectURI($redirect_uri) {
- $this->redirectURI = $redirect_uri;
- return $this;
- }
-
- public function getRedirectURI() {
- return $this->redirectURI;
- }
-
- public function getExtraAuthenticateParameters() {
- return array();
- }
-
- public function getExtraTokenParameters() {
- return array();
- }
-
- public function getExtraRefreshParameters() {
- return array();
- }
-
- public function setScope($scope) {
- $this->scope = $scope;
- return $this;
- }
-
- public function getScope() {
- return $this->scope;
- }
-
- public function setClientSecret(PhutilOpaqueEnvelope $client_secret) {
- $this->clientSecret = $client_secret;
- return $this;
- }
-
- public function getClientSecret() {
- return $this->clientSecret;
- }
-
- public function setClientID($client_id) {
- $this->clientID = $client_id;
- return $this;
- }
-
- public function getClientID() {
- return $this->clientID;
- }
-
- public function getAccessToken() {
- return $this->getAccessTokenData('access_token');
- }
-
- public function getAccessTokenExpires() {
- return $this->getAccessTokenData('expires_epoch');
- }
-
- public function getRefreshToken() {
- return $this->getAccessTokenData('refresh_token');
- }
-
- protected function getAccessTokenData($key, $default = null) {
- if ($this->accessTokenData === null) {
- $this->accessTokenData = $this->loadAccessTokenData();
- }
-
- return idx($this->accessTokenData, $key, $default);
- }
-
- public function supportsTokenRefresh() {
- return false;
- }
-
- public function refreshAccessToken($refresh_token) {
- $this->accessTokenData = $this->loadRefreshTokenData($refresh_token);
- return $this;
- }
-
- protected function loadRefreshTokenData($refresh_token) {
- $params = array(
- 'refresh_token' => $refresh_token,
- ) + $this->getExtraRefreshParameters();
-
- // NOTE: Make sure we return the refresh_token so that subsequent
- // calls to getRefreshToken() return it; providers normally do not echo
- // it back for token refresh requests.
-
- return $this->makeTokenRequest($params) + array(
- 'refresh_token' => $refresh_token,
- );
- }
-
- protected function loadAccessTokenData() {
- $code = $this->getCode();
- if (!$code) {
- throw new PhutilInvalidStateException('setCode');
- }
-
- $params = array(
- 'code' => $this->getCode(),
- ) + $this->getExtraTokenParameters();
-
- return $this->makeTokenRequest($params);
- }
-
- private function makeTokenRequest(array $params) {
- $uri = $this->getTokenBaseURI();
- $query_data = array(
- 'client_id' => $this->getClientID(),
- 'client_secret' => $this->getClientSecret()->openEnvelope(),
- 'redirect_uri' => $this->getRedirectURI(),
- ) + $params;
-
- $future = new HTTPSFuture($uri, $query_data);
- $future->setMethod('POST');
- list($body) = $future->resolvex();
-
- $data = $this->readAccessTokenResponse($body);
-
- if (isset($data['expires_in'])) {
- $data['expires_epoch'] = $data['expires_in'];
- } else if (isset($data['expires'])) {
- $data['expires_epoch'] = $data['expires'];
- }
-
- // If we got some "expires" value back, interpret it as an epoch timestamp
- // if it's after the year 2010 and as a relative number of seconds
- // otherwise.
- if (isset($data['expires_epoch'])) {
- if ($data['expires_epoch'] < (60 * 60 * 24 * 365 * 40)) {
- $data['expires_epoch'] += time();
- }
- }
-
- if (isset($data['error'])) {
- throw new Exception(pht('Access token error: %s', $data['error']));
- }
-
- return $data;
- }
-
- protected function readAccessTokenResponse($body) {
- // NOTE: Most providers either return JSON or HTTP query strings, so try
- // both mechanisms. If your provider does something else, override this
- // method.
-
- $data = json_decode($body, true);
-
- if (!is_array($data)) {
- $data = array();
- parse_str($body, $data);
- }
-
- if (empty($data['access_token']) &&
- empty($data['error'])) {
- throw new Exception(
- pht('Failed to decode OAuth access token response: %s', $body));
- }
-
- return $data;
- }
-
- protected function getOAuthAccountData($key, $default = null) {
- if ($this->oauthAccountData === null) {
- $this->oauthAccountData = $this->loadOAuthAccountData();
- }
-
- return idx($this->oauthAccountData, $key, $default);
- }
-
-}
diff --git a/src/auth/PhutilPhabricatorAuthAdapter.php b/src/auth/PhutilPhabricatorAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilPhabricatorAuthAdapter.php
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Phabricator OAuth2.
- */
-final class PhutilPhabricatorAuthAdapter extends PhutilOAuthAuthAdapter {
-
- private $phabricatorBaseURI;
- private $adapterDomain;
-
- public function setPhabricatorBaseURI($uri) {
- $this->phabricatorBaseURI = $uri;
- return $this;
- }
-
- public function getPhabricatorBaseURI() {
- return $this->phabricatorBaseURI;
- }
-
- public function getAdapterDomain() {
- return $this->adapterDomain;
- }
-
- public function setAdapterDomain($domain) {
- $this->adapterDomain = $domain;
- return $this;
- }
-
- public function getAdapterType() {
- return 'phabricator';
- }
-
- public function getAccountID() {
- return $this->getOAuthAccountData('phid');
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('primaryEmail');
- }
-
- public function getAccountName() {
- return $this->getOAuthAccountData('userName');
- }
-
- public function getAccountImageURI() {
- return $this->getOAuthAccountData('image');
- }
-
- public function getAccountURI() {
- return $this->getOAuthAccountData('uri');
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('realName');
- }
-
- protected function getAuthenticateBaseURI() {
- return $this->getPhabricatorURI('oauthserver/auth/');
- }
-
- protected function getTokenBaseURI() {
- return $this->getPhabricatorURI('oauthserver/token/');
- }
-
- public function getScope() {
- return '';
- }
-
- public function getExtraAuthenticateParameters() {
- return array(
- 'response_type' => 'code',
- );
- }
-
- public function getExtraTokenParameters() {
- return array(
- 'grant_type' => 'authorization_code',
- );
- }
-
- protected function loadOAuthAccountData() {
- $uri = id(new PhutilURI($this->getPhabricatorURI('api/user.whoami')))
- ->replaceQueryParam('access_token', $this->getAccessToken());
- list($body) = id(new HTTPSFuture($uri))->resolvex();
-
- try {
- $data = phutil_json_decode($body);
- return $data['result'];
- } catch (PhutilJSONParserException $ex) {
- throw new Exception(
- pht(
- 'Expected valid JSON response from Phabricator %s request.',
- 'user.whoami'),
- $ex);
- }
- }
-
- private function getPhabricatorURI($path) {
- return rtrim($this->phabricatorBaseURI, '/').'/'.ltrim($path, '/');
- }
-
-}
diff --git a/src/auth/PhutilSlackAuthAdapter.php b/src/auth/PhutilSlackAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilSlackAuthAdapter.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Slack OAuth2.
- */
-final class PhutilSlackAuthAdapter extends PhutilOAuthAuthAdapter {
-
- public function getAdapterType() {
- return 'Slack';
- }
-
- public function getAdapterDomain() {
- return 'slack.com';
- }
-
- public function getAccountID() {
- $user = $this->getOAuthAccountData('user');
- return idx($user, 'id');
- }
-
- public function getAccountEmail() {
- $user = $this->getOAuthAccountData('user');
- return idx($user, 'email');
- }
-
- public function getAccountImageURI() {
- $user = $this->getOAuthAccountData('user');
- return idx($user, 'image_512');
- }
-
- public function getAccountRealName() {
- $user = $this->getOAuthAccountData('user');
- return idx($user, 'name');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://slack.com/oauth/authorize';
- }
-
- protected function getTokenBaseURI() {
- return 'https://slack.com/api/oauth.access';
- }
-
- public function getScope() {
- return 'identity.basic,identity.team,identity.avatar';
- }
-
- public function getExtraAuthenticateParameters() {
- return array(
- 'response_type' => 'code',
- );
- }
-
- protected function loadOAuthAccountData() {
- return id(new PhutilSlackFuture())
- ->setAccessToken($this->getAccessToken())
- ->setRawSlackQuery('users.identity')
- ->resolve();
- }
-
-}
diff --git a/src/auth/PhutilTwitchAuthAdapter.php b/src/auth/PhutilTwitchAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilTwitchAuthAdapter.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Twitch.tv OAuth2.
- */
-final class PhutilTwitchAuthAdapter extends PhutilOAuthAuthAdapter {
-
- public function getAdapterType() {
- return 'twitch';
- }
-
- public function getAdapterDomain() {
- return 'twitch.tv';
- }
-
- public function getAccountID() {
- return $this->getOAuthAccountData('_id');
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('email');
- }
-
- public function getAccountName() {
- return $this->getOAuthAccountData('name');
- }
-
- public function getAccountImageURI() {
- return $this->getOAuthAccountData('logo');
- }
-
- public function getAccountURI() {
- $name = $this->getAccountName();
- if ($name) {
- return 'http://www.twitch.tv/'.$name;
- }
- return null;
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('display_name');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://api.twitch.tv/kraken/oauth2/authorize';
- }
-
- protected function getTokenBaseURI() {
- return 'https://api.twitch.tv/kraken/oauth2/token';
- }
-
- public function getScope() {
- return 'user_read';
- }
-
- public function getExtraAuthenticateParameters() {
- return array(
- 'response_type' => 'code',
- );
- }
-
- public function getExtraTokenParameters() {
- return array(
- 'grant_type' => 'authorization_code',
- );
- }
-
- protected function loadOAuthAccountData() {
- return id(new PhutilTwitchFuture())
- ->setClientID($this->getClientID())
- ->setAccessToken($this->getAccessToken())
- ->setRawTwitchQuery('user')
- ->resolve();
- }
-
-}
diff --git a/src/auth/PhutilTwitterAuthAdapter.php b/src/auth/PhutilTwitterAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilTwitterAuthAdapter.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for Twitter OAuth1.
- */
-final class PhutilTwitterAuthAdapter extends PhutilOAuth1AuthAdapter {
-
- private $userInfo;
-
- public function getAccountID() {
- return idx($this->getHandshakeData(), 'user_id');
- }
-
- public function getAccountName() {
- return idx($this->getHandshakeData(), 'screen_name');
- }
-
- public function getAccountURI() {
- $name = $this->getAccountName();
- if (strlen($name)) {
- return 'https://twitter.com/'.$name;
- }
- return null;
- }
-
- public function getAccountImageURI() {
- $info = $this->getUserInfo();
- return idx($info, 'profile_image_url');
- }
-
- public function getAccountRealName() {
- $info = $this->getUserInfo();
- return idx($info, 'name');
- }
-
- public function getAdapterType() {
- return 'twitter';
- }
-
- public function getAdapterDomain() {
- return 'twitter.com';
- }
-
- protected function getRequestTokenURI() {
- return 'https://api.twitter.com/oauth/request_token';
- }
-
- protected function getAuthorizeTokenURI() {
- return 'https://api.twitter.com/oauth/authorize';
- }
-
- protected function getValidateTokenURI() {
- return 'https://api.twitter.com/oauth/access_token';
- }
-
- private function getUserInfo() {
- if ($this->userInfo === null) {
- $params = array(
- 'user_id' => $this->getAccountID(),
- );
-
- $uri = new PhutilURI(
- 'https://api.twitter.com/1.1/users/show.json',
- $params);
-
- $data = $this->newOAuth1Future($uri)
- ->setMethod('GET')
- ->resolveJSON();
-
- $this->userInfo = $data;
- }
- return $this->userInfo;
- }
-
-}
diff --git a/src/auth/PhutilWordPressAuthAdapter.php b/src/auth/PhutilWordPressAuthAdapter.php
deleted file mode 100644
--- a/src/auth/PhutilWordPressAuthAdapter.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-/**
- * Authentication adapter for WordPress.com OAuth2.
- */
-final class PhutilWordPressAuthAdapter extends PhutilOAuthAuthAdapter {
-
- public function getAdapterType() {
- return 'wordpress';
- }
-
- public function getAdapterDomain() {
- return 'wordpress.com';
- }
-
- public function getAccountID() {
- return $this->getOAuthAccountData('ID');
- }
-
- public function getAccountEmail() {
- return $this->getOAuthAccountData('email');
- }
-
- public function getAccountName() {
- return $this->getOAuthAccountData('username');
- }
-
- public function getAccountImageURI() {
- return $this->getOAuthAccountData('avatar_URL');
- }
-
- public function getAccountURI() {
- return $this->getOAuthAccountData('profile_URL');
- }
-
- public function getAccountRealName() {
- return $this->getOAuthAccountData('display_name');
- }
-
- protected function getAuthenticateBaseURI() {
- return 'https://public-api.wordpress.com/oauth2/authorize';
- }
-
- protected function getTokenBaseURI() {
- return 'https://public-api.wordpress.com/oauth2/token';
- }
-
- public function getScope() {
- return 'user_read';
- }
-
- public function getExtraAuthenticateParameters() {
- return array(
- 'response_type' => 'code',
- 'blog_id' => 0,
- );
- }
-
- public function getExtraTokenParameters() {
- return array(
- 'grant_type' => 'authorization_code',
- );
- }
-
- protected function loadOAuthAccountData() {
- return id(new PhutilWordPressFuture())
- ->setClientID($this->getClientID())
- ->setAccessToken($this->getAccessToken())
- ->setRawWordPressQuery('/me/')
- ->resolve();
- }
-
-}
diff --git a/src/auth/exception/PhutilAuthConfigurationException.php b/src/auth/exception/PhutilAuthConfigurationException.php
deleted file mode 100644
--- a/src/auth/exception/PhutilAuthConfigurationException.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * Authentication is not configured correctly.
- */
-final class PhutilAuthConfigurationException extends PhutilAuthException {}
diff --git a/src/auth/exception/PhutilAuthCredentialException.php b/src/auth/exception/PhutilAuthCredentialException.php
deleted file mode 100644
--- a/src/auth/exception/PhutilAuthCredentialException.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-/**
- * The user provided invalid credentials.
- */
-final class PhutilAuthCredentialException extends PhutilAuthException {}
diff --git a/src/auth/exception/PhutilAuthException.php b/src/auth/exception/PhutilAuthException.php
deleted file mode 100644
--- a/src/auth/exception/PhutilAuthException.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-/**
- * Abstract exception class for errors encountered during authentication
- * workflows.
- */
-abstract class PhutilAuthException extends Exception {}
diff --git a/src/auth/exception/PhutilAuthUserAbortedException.php b/src/auth/exception/PhutilAuthUserAbortedException.php
deleted file mode 100644
--- a/src/auth/exception/PhutilAuthUserAbortedException.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-/**
- * The user aborted the authentication workflow, by clicking "Cancel" or "Deny"
- * or taking some similar action.
- *
- * For example, in OAuth/OAuth2 workflows, the authentication provider
- * generally presents the user with a confirmation dialog with two options,
- * "Approve" and "Deny".
- *
- * If an adapter detects that the user has explicitly bailed out of the
- * workflow, it should throw this exception.
- */
-final class PhutilAuthUserAbortedException extends PhutilAuthException {}
diff --git a/src/future/query/QueryFuture.php b/src/future/query/QueryFuture.php
deleted file mode 100644
--- a/src/future/query/QueryFuture.php
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php
-
-/**
- * This class provides several approaches for querying data from the database:
- *
- * # Async queries: Used under MySQLi with MySQLnd.
- * # Parallel queries: Used under HPHP.
- * # Multi queries: Used under MySQLi or HPHP.
- * # Single queries: Used under MySQL.
- *
- * The class automatically decides which approach to use. Usage is like with
- * other futures:
- *
- * $futures = array();
- * $futures[] = new QueryFuture($conn1, 'SELECT 1');
- * $futures[] = new QueryFuture($conn1, 'DELETE FROM table');
- * $futures[] = new QueryFuture($conn2, 'SELECT 2');
- *
- * foreach (new FutureIterator($futures) as $future) {
- * try {
- * $result = $future->resolve();
- * } catch (AphrontQueryException $ex) {
- * }
- * }
- *
- * `$result` contains a list of dicts for select queries or number of modified
- * rows for modification queries.
- */
-final class QueryFuture extends Future {
-
- private static $futures = array();
-
- private $conn;
- private $query;
- private $id;
- private $async;
- private $profilerCallID;
-
- public function __construct(
- AphrontDatabaseConnection $conn,
- $pattern/* , ... */) {
-
- $this->conn = $conn;
-
- $args = func_get_args();
- $args = array_slice($args, 2);
- $this->query = vqsprintf($conn, $pattern, $args);
-
- self::$futures[] = $this;
- $this->id = last_key(self::$futures);
- }
-
- public function isReady() {
- if ($this->result !== null || $this->exception) {
- return true;
- }
-
- if (!$this->conn->supportsAsyncQueries()) {
- if ($this->conn->supportsParallelQueries()) {
- $queries = array();
- $conns = array();
- foreach (self::$futures as $id => $future) {
- $queries[$id] = $future->query;
- $conns[$id] = $future->conn;
- }
- $results = $this->conn->executeParallelQueries($queries, $conns);
- $this->processResults($results);
- return true;
- }
-
- $conns = array();
- $conn_queries = array();
- foreach (self::$futures as $id => $future) {
- $hash = spl_object_hash($future->conn);
- $conns[$hash] = $future->conn;
- $conn_queries[$hash][$id] = $future->query;
- }
- foreach ($conn_queries as $hash => $queries) {
- $this->processResults($conns[$hash]->executeRawQueries($queries));
- }
- return true;
- }
-
- if (!$this->async) {
- $profiler = PhutilServiceProfiler::getInstance();
- $this->profilerCallID = $profiler->beginServiceCall(
- array(
- 'type' => 'query',
- 'query' => $this->query,
- 'async' => true,
- ));
-
- $this->async = $this->conn->asyncQuery($this->query);
- return false;
- }
-
- $conns = array();
- $asyncs = array();
- foreach (self::$futures as $id => $future) {
- if ($future->async) {
- $conns[$id] = $future->conn;
- $asyncs[$id] = $future->async;
- }
- }
-
- $this->processResults($this->conn->resolveAsyncQueries($conns, $asyncs));
-
- if ($this->result !== null || $this->exception) {
- return true;
- }
- return false;
- }
-
- private function processResults(array $results) {
- foreach ($results as $id => $result) {
- $future = self::$futures[$id];
- if ($result instanceof Exception) {
- $future->exception = $result;
- } else {
- $future->result = $result;
- }
- unset(self::$futures[$id]);
- if ($future->profilerCallID) {
- $profiler = PhutilServiceProfiler::getInstance();
- $profiler->endServiceCall($future->profilerCallID, array());
- }
- }
- }
-}
diff --git a/src/grammar/PhutilLipsumContextFreeGrammar.php b/src/grammar/PhutilLipsumContextFreeGrammar.php
deleted file mode 100644
--- a/src/grammar/PhutilLipsumContextFreeGrammar.php
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-
-final class PhutilLipsumContextFreeGrammar
- extends PhutilContextFreeGrammar {
-
- protected function getRules() {
- return array(
- 'start' => array(
- '[words].',
- '[words].',
- '[words].',
- '[words]: [word], [word], [word] [word].',
- '[words]; [lowerwords].',
- '[words]!',
- '[words], "[words]."',
- '[words] ("[upperword] [upperword] [upperword]") [lowerwords].',
- '[words]?',
- ),
- 'words' => array(
- '[upperword] [lowerwords]',
- ),
- 'upperword' => array(
- 'Lorem',
- 'Ipsum',
- 'Dolor',
- 'Sit',
- 'Amet',
- ),
- 'lowerwords' => array(
- '[word]',
- '[word] [word]',
- '[word] [word] [word]',
- '[word] [word] [word] [word]',
- '[word] [word] [word] [word] [word]',
- '[word] [word] [word] [word] [word]',
- '[word] [word] [word] [word] [word] [word]',
- '[word] [word] [word] [word] [word] [word]',
- ),
- 'word' => array(
- 'ad',
- 'adipisicing',
- 'aliqua',
- 'aliquip',
- 'amet',
- 'anim',
- 'aute',
- 'cillum',
- 'commodo',
- 'consectetur',
- 'consequat',
- 'culpa',
- 'cupidatat',
- 'deserunt',
- 'do',
- 'dolor',
- 'dolore',
- 'duis',
- 'ea',
- 'eiusmod',
- 'elit',
- 'enim',
- 'esse',
- 'est',
- 'et',
- 'eu',
- 'ex',
- 'excepteur',
- 'exercitation',
- 'fugiat',
- 'id',
- 'in',
- 'incididunt',
- 'ipsum',
- 'irure',
- 'labore',
- 'laboris',
- 'laborum',
- 'lorem',
- 'magna',
- 'minim',
- 'mollit',
- 'nisi',
- 'non',
- 'nostrud',
- 'nulla',
- 'occaecat',
- 'officia',
- 'pariatur',
- 'proident',
- 'qui',
- 'quis',
- 'reprehenderit',
- 'sed',
- 'sint',
- 'sit',
- 'sunt',
- 'tempor',
- 'ullamco',
- 'ut',
- 'velit',
- 'veniam',
- 'voluptate',
- ),
- );
- }
-
-}
diff --git a/src/grammar/PhutilRealNameContextFreeGrammar.php b/src/grammar/PhutilRealNameContextFreeGrammar.php
deleted file mode 100644
--- a/src/grammar/PhutilRealNameContextFreeGrammar.php
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php
-
-final class PhutilRealNameContextFreeGrammar
- extends PhutilContextFreeGrammar {
-
- protected function getRules() {
- return array(
- 'start' => array(
- '[first] [last]',
- '[first] [last]',
- '[first] [last]',
- '[first] [last]',
- '[first] [last]',
- '[first] [last]',
- '[first] [last]',
- '[first] [last]',
- '[first] [last]-[last]',
- '[first] [middle] [last]',
- '[first] "[nick]" [last]',
- '[first] [particle] [particle] [particle]',
- ),
- 'first' => array(
- 'Mohamed',
- 'Youssef',
- 'Ahmed',
- 'Mahmoud',
- 'Mustafa',
- 'Fatma',
- 'Aya',
- 'Noam',
- 'Adam',
- 'Lucas',
- 'Noah',
- 'Jakub',
- 'Victor',
- 'Harry',
- 'Rasmus',
- 'Nathan',
- 'Emil',
- 'Charlie',
- 'Leon',
- 'Dylan',
- 'Alexander',
- 'Emma',
- 'Marie',
- 'Lea',
- 'Amelia',
- 'Hanna',
- 'Emily',
- 'Sofia',
- 'Julia',
- 'Santiago',
- 'Sebastian',
- 'Olivia',
- 'Madison',
- 'Isabella',
- 'Esther',
- 'Anya',
- 'Camila',
- 'Jack',
- 'Oliver',
- ),
- 'nick' => array(
- 'Buzz',
- 'Juggernaut',
- 'Haze',
- 'Hawk',
- 'Iceman',
- 'Killer',
- 'Apex',
- 'Ocelot',
- ),
- 'middle' => array(
- 'Rose',
- 'Grace',
- 'Jane',
- 'Louise',
- 'Jade',
- 'James',
- 'John',
- 'William',
- 'Thomas',
- 'Alexander',
- ),
- 'last' => array(
- '[termlast]',
- '[termlast]',
- '[termlast]',
- '[termlast]',
- '[termlast]',
- '[termlast]',
- '[termlast]',
- '[termlast]',
- 'O\'[termlast]',
- 'Mc[termlast]',
- ),
- 'termlast' => array(
- 'Smith',
- 'Johnson',
- 'Williams',
- 'Jones',
- 'Brown',
- 'Davis',
- 'Miller',
- 'Wilson',
- 'Moore',
- 'Taylor',
- 'Anderson',
- 'Thomas',
- 'Jackson',
- 'White',
- 'Harris',
- 'Martin',
- 'Thompson',
- 'Garcia',
- 'Marinez',
- 'Robinson',
- 'Clark',
- 'Rodrigues',
- 'Lewis',
- 'Lee',
- 'Walker',
- 'Hall',
- 'Allen',
- 'Young',
- 'Hernandex',
- 'King',
- 'Wang',
- 'Li',
- 'Zhang',
- 'Liu',
- 'Chen',
- 'Yang',
- 'Huang',
- 'Zhao',
- 'Wu',
- 'Zhou',
- 'Xu',
- 'Sun',
- 'Ma',
- ),
- 'particle' => array(
- 'Wu',
- 'Xu',
- 'Ma',
- 'Li',
- 'Liu',
- 'Shao',
- 'Lin',
- 'Khan',
- ),
- );
- }
-
-}
diff --git a/src/grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php b/src/grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php
deleted file mode 100644
--- a/src/grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php
+++ /dev/null
@@ -1,254 +0,0 @@
-<?php
-
-/**
- * Generates valid context-free code for most programming languages that could
- * pass as C. Except for PHP. But includes Java (mostly).
- */
-abstract class PhutilCLikeCodeSnippetContextFreeGrammar
- extends PhutilCodeSnippetContextFreeGrammar {
-
- protected function buildRuleSet() {
- return array(
- $this->getStmtTerminationGrammarSet(),
- $this->getVarNameGrammarSet(),
- $this->getNullExprGrammarSet(),
- $this->getNumberGrammarSet(),
- $this->getExprGrammarSet(),
- $this->getCondGrammarSet(),
- $this->getLoopGrammarSet(),
- $this->getStmtGrammarSet(),
- $this->getAssignmentGrammarSet(),
- $this->getArithExprGrammarSet(),
- $this->getBoolExprGrammarSet(),
- $this->getBoolValGrammarSet(),
- $this->getTernaryExprGrammarSet(),
-
- $this->getFuncNameGrammarSet(),
- $this->getFuncCallGrammarSet(),
- $this->getFuncCallParamGrammarSet(),
- $this->getFuncDeclGrammarSet(),
- $this->getFuncParamGrammarSet(),
- $this->getFuncBodyGrammarSet(),
- $this->getFuncReturnGrammarSet(),
- );
- }
-
- protected function getStartGrammarSet() {
- $start_grammar = parent::getStartGrammarSet();
-
- $start_grammar['start'][] = '[funcdecl]';
-
- return $start_grammar;
- }
-
- protected function getStmtTerminationGrammarSet() {
- return $this->buildGrammarSet('term', array(';'));
- }
-
- protected function getFuncCallGrammarSet() {
- return $this->buildGrammarSet('funccall',
- array(
- '[funcname]([funccallparam])',
- ));
- }
-
- protected function getFuncCallParamGrammarSet() {
- return $this->buildGrammarSet('funccallparam',
- array(
- '',
- '[expr]',
- '[expr], [expr]',
- ));
- }
-
- protected function getFuncDeclGrammarSet() {
- return $this->buildGrammarSet('funcdecl',
- array(
- 'function [funcname]([funcparam]) '.
- '{[funcbody, indent, block, trim=right]}',
- ));
- }
-
- protected function getFuncParamGrammarSet() {
- return $this->buildGrammarSet('funcparam',
- array(
- '',
- '[varname]',
- '[varname], [varname]',
- '[varname], [varname], [varname]',
- ));
- }
-
- protected function getFuncBodyGrammarSet() {
- return $this->buildGrammarSet('funcbody',
- array(
- "[stmt]\n[stmt]\n[funcreturn]",
- "[stmt]\n[stmt]\n[stmt]\n[funcreturn]",
- "[stmt]\n[stmt]\n[stmt]\n[stmt]\n[funcreturn]",
- ));
- }
-
- protected function getFuncReturnGrammarSet() {
- return $this->buildGrammarSet('funcreturn',
- array(
- 'return [expr][term]',
- '',
- ));
- }
-
- // Not really C, but put it here because of the curly braces and mostly shared
- // among Java and PHP
- protected function getClassDeclGrammarSet() {
- return $this->buildGrammarSet('classdecl',
- array(
- '[classinheritancemod] class [classname] {[classbody, indent, block]}',
- 'class [classname] {[classbody, indent, block]}',
- ));
- }
-
- protected function getClassNameGrammarSet() {
- return $this->buildGrammarSet('classname',
- array(
- 'MuffinHouse',
- 'MuffinReader',
- 'MuffinAwesomizer',
- 'SuperException',
- 'Librarian',
- 'Book',
- 'Ball',
- 'BallOfCode',
- 'AliceAndBobsSharedSecret',
- 'FileInputStream',
- 'FileOutputStream',
- 'BufferedReader',
- 'BufferedWriter',
- 'Cardigan',
- 'HouseOfCards',
- 'UmbrellaClass',
- 'GenericThing',
- ));
- }
-
- protected function getClassBodyGrammarSet() {
- return $this->buildGrammarSet('classbody',
- array(
- '[methoddecl]',
- "[methoddecl]\n\n[methoddecl]",
- "[propdecl]\n[propdecl]\n\n[methoddecl]\n\n[methoddecl]",
- "[propdecl]\n[propdecl]\n[propdecl]\n\n[methoddecl]\n\n[methoddecl]".
- "\n\n[methoddecl]",
- ));
- }
-
- protected function getVisibilityGrammarSet() {
- return $this->buildGrammarSet('visibility',
- array(
- 'private',
- 'protected',
- 'public',
- ));
- }
-
- protected function getClassInheritanceModGrammarSet() {
- return $this->buildGrammarSet('classinheritancemod',
- array(
- 'final',
- 'abstract',
- ));
- }
-
- // Keeping this separate so we won't give abstract methods a function body
- protected function getMethodInheritanceModGrammarSet() {
- return $this->buildGrammarSet('methodinheritancemod',
- array(
- 'final',
- ));
- }
-
- protected function getMethodDeclGrammarSet() {
- return $this->buildGrammarSet('methoddecl',
- array(
- '[visibility] [methodfuncdecl]',
- '[visibility] [methodfuncdecl]',
- '[methodinheritancemod] [visibility] [methodfuncdecl]',
- '[abstractmethoddecl]',
- ));
- }
-
- protected function getMethodFuncDeclGrammarSet() {
- return $this->buildGrammarSet('methodfuncdecl',
- array(
- 'function [funcname]([funcparam]) '.
- '{[methodbody, indent, block, trim=right]}',
- ));
- }
-
- protected function getMethodBodyGrammarSet() {
- return $this->buildGrammarSet('methodbody',
- array(
- "[methodstmt]\n[methodbody]",
- "[methodstmt]\n[funcreturn]",
- ));
- }
-
- protected function getMethodStmtGrammarSet() {
- $stmts = $this->getStmtGrammarSet();
-
- return $this->buildGrammarSet('methodstmt',
- array_merge(
- $stmts['stmt'],
- array(
- '[methodcall][term]',
- )));
- }
-
- protected function getMethodCallGrammarSet() {
- // Java/JavaScript
- return $this->buildGrammarSet('methodcall',
- array(
- 'this.[funccall]',
- '[varname].[funccall]',
- '[classname].[funccall]',
- ));
- }
-
- protected function getAbstractMethodDeclGrammarSet() {
- return $this->buildGrammarSet('abstractmethoddecl',
- array(
- 'abstract function [funcname]([funcparam])[term]',
- ));
- }
-
- protected function getPropDeclGrammarSet() {
- return $this->buildGrammarSet('propdecl',
- array(
- '[visibility] [varname][term]',
- ));
- }
-
- protected function getClassRuleSets() {
- return array(
- $this->getClassInheritanceModGrammarSet(),
- $this->getMethodInheritanceModGrammarSet(),
- $this->getClassDeclGrammarSet(),
- $this->getClassNameGrammarSet(),
- $this->getClassBodyGrammarSet(),
- $this->getMethodDeclGrammarSet(),
- $this->getMethodFuncDeclGrammarSet(),
- $this->getMethodBodyGrammarSet(),
- $this->getMethodStmtGrammarSet(),
- $this->getMethodCallGrammarSet(),
- $this->getAbstractMethodDeclGrammarSet(),
- $this->getPropDeclGrammarSet(),
- $this->getVisibilityGrammarSet(),
- );
- }
-
- public function generateClass() {
- $rules = array_merge($this->getRules(), $this->getClassRuleSets());
- $rules['start'] = array('[classdecl]');
- $count = 0;
- return $this->applyRules('[start]', $count, $rules);
- }
-
-}
diff --git a/src/grammar/code/PhutilCodeSnippetContextFreeGrammar.php b/src/grammar/code/PhutilCodeSnippetContextFreeGrammar.php
deleted file mode 100644
--- a/src/grammar/code/PhutilCodeSnippetContextFreeGrammar.php
+++ /dev/null
@@ -1,205 +0,0 @@
-<?php
-
-/**
- * Generates non-sense code snippets according to context-free rules, respecting
- * indentation etc.
- *
- * Also provides a common ruleset shared among many mainstream programming
- * languages (that is, not Lisp).
- */
-abstract class PhutilCodeSnippetContextFreeGrammar
- extends PhutilContextFreeGrammar {
-
- public function generate() {
- // A trailing newline is favorable for source code
- return trim(parent::generate())."\n";
- }
-
- final protected function getRules() {
- return array_merge(
- $this->getStartGrammarSet(),
- $this->getStmtGrammarSet(),
- array_mergev($this->buildRuleSet()));
- }
-
- abstract protected function buildRuleSet();
-
- protected function buildGrammarSet($name, array $set) {
- return array(
- $name => $set,
- );
- }
-
- protected function getStartGrammarSet() {
- return $this->buildGrammarSet('start',
- array(
- "[stmt]\n[stmt]",
- "[stmt]\n[stmt]\n[stmt]",
- "[stmt]\n[stmt]\n[stmt]\n[stmt]",
- ));
- }
-
- protected function getStmtGrammarSet() {
- return $this->buildGrammarSet('stmt',
- array(
- '[assignment][term]',
- '[assignment][term]',
- '[assignment][term]',
- '[assignment][term]',
- '[funccall][term]',
- '[funccall][term]',
- '[funccall][term]',
- '[funccall][term]',
- '[cond]',
- '[loop]',
- ));
- }
-
- protected function getFuncNameGrammarSet() {
- return $this->buildGrammarSet('funcname',
- array(
- 'do_something',
- 'nonempty',
- 'noOp',
- 'call_user_func',
- 'getenv',
- 'render',
- 'super',
- 'derpify',
- 'awesomize',
- 'equals',
- 'run',
- 'flee',
- 'fight',
- 'notify',
- 'listen',
- 'calculate',
- 'aim',
- 'open',
- ));
- }
-
- protected function getVarNameGrammarSet() {
- return $this->buildGrammarSet('varname',
- array(
- 'is_something',
- 'object',
- 'name',
- 'token',
- 'label',
- 'piece_of_the_pie',
- 'type',
- 'state',
- 'param',
- 'action',
- 'key',
- 'timeout',
- 'result',
- ));
- }
-
- protected function getNullExprGrammarSet() {
- return $this->buildGrammarSet('null', array('null'));
- }
-
- protected function getNumberGrammarSet() {
- return $this->buildGrammarSet('number',
- array(
- mt_rand(-1, 100),
- mt_rand(-100, 1000),
- mt_rand(-1000, 5000),
- mt_rand(0, 1).'.'.mt_rand(1, 1000),
- mt_rand(0, 50).'.'.mt_rand(1, 1000),
- ));
- }
-
- protected function getExprGrammarSet() {
- return $this->buildGrammarSet('expr',
- array(
- '[null]',
- '[number]',
- '[number]',
- '[varname]',
- '[varname]',
- '[boolval]',
- '[boolval]',
- '[boolexpr]',
- '[boolexpr]',
- '[funccall]',
- '[arithexpr]',
- '[arithexpr]',
- // Some random strings
- '"'.Filesystem::readRandomCharacters(4).'"',
- '"'.Filesystem::readRandomCharacters(5).'"',
- ));
- }
-
- protected function getBoolExprGrammarSet() {
- return $this->buildGrammarSet('boolexpr',
- array(
- '[varname]',
- '![varname]',
- '[varname] == [boolval]',
- '[varname] != [boolval]',
- '[ternary]',
- ));
- }
-
- protected function getBoolValGrammarSet() {
- return $this->buildGrammarSet('boolval',
- array(
- 'true',
- 'false',
- ));
- }
-
- protected function getArithExprGrammarSet() {
- return $this->buildGrammarSet('arithexpr',
- array(
- '[varname]++',
- '++[varname]',
- '[varname] + [number]',
- '[varname]--',
- '--[varname]',
- '[varname] - [number]',
- ));
- }
-
- protected function getAssignmentGrammarSet() {
- return $this->buildGrammarSet('assignment',
- array(
- '[varname] = [expr]',
- '[varname] = [arithexpr]',
- '[varname] += [expr]',
- ));
- }
-
- protected function getCondGrammarSet() {
- return $this->buildGrammarSet('cond',
- array(
- 'if ([boolexpr]) {[stmt, indent, block]}',
- 'if ([boolexpr]) {[stmt, indent, block]} else {[stmt, indent, block]}',
- ));
- }
-
- protected function getLoopGrammarSet() {
- return $this->buildGrammarSet('loop',
- array(
- 'while ([boolexpr]) {[stmt, indent, block]}',
- 'do {[stmt, indent, block]} while ([boolexpr])[term]',
- 'for ([assignment]; [boolexpr]; [expr]) {[stmt, indent, block]}',
- ));
- }
-
- protected function getTernaryExprGrammarSet() {
- return $this->buildGrammarSet('ternary',
- array(
- '[boolexpr] ? [expr] : [expr]',
- ));
- }
-
- protected function getStmtTerminationGrammarSet() {
- return $this->buildGrammarSet('term', array(''));
- }
-
-}
diff --git a/src/grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php b/src/grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php
deleted file mode 100644
--- a/src/grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php
+++ /dev/null
@@ -1,184 +0,0 @@
-<?php
-
-final class PhutilJavaCodeSnippetContextFreeGrammar
- extends PhutilCLikeCodeSnippetContextFreeGrammar {
-
- protected function buildRuleSet() {
- $parent_ruleset = parent::buildRuleSet();
- $rulesset = array_merge($parent_ruleset, $this->getClassRuleSets());
-
- $rulesset[] = $this->getTypeNameGrammarSet();
- $rulesset[] = $this->getNamespaceDeclGrammarSet();
- $rulesset[] = $this->getNamespaceNameGrammarSet();
- $rulesset[] = $this->getImportGrammarSet();
- $rulesset[] = $this->getMethodReturnTypeGrammarSet();
- $rulesset[] = $this->getMethodNameGrammarSet();
- $rulesset[] = $this->getVarDeclGrammarSet();
- $rulesset[] = $this->getClassDerivGrammarSet();
-
- return $rulesset;
- }
-
- protected function getStartGrammarSet() {
- return $this->buildGrammarSet('start',
- array(
- '[import, block][nmspdecl, block][classdecl, block]',
- ));
- }
-
- protected function getClassDeclGrammarSet() {
- return $this->buildGrammarSet('classdecl',
- array(
- '[classinheritancemod] [visibility] class [classname][classderiv] '.
- '{[classbody, indent, block]}',
- '[visibility] class [classname][classderiv] '.
- '{[classbody, indent, block]}',
- ));
- }
-
- protected function getClassDerivGrammarSet() {
- return $this->buildGrammarSet('classderiv',
- array(
- ' extends [classname]',
- '',
- '',
- ));
- }
-
- protected function getTypeNameGrammarSet() {
- return $this->buildGrammarSet('type',
- array(
- 'int',
- 'boolean',
- 'char',
- 'short',
- 'long',
- 'float',
- 'double',
- '[classname]',
- '[type][]',
- ));
- }
-
- protected function getMethodReturnTypeGrammarSet() {
- return $this->buildGrammarSet('methodreturn',
- array(
- '[type]',
- 'void',
- ));
- }
-
- protected function getNamespaceDeclGrammarSet() {
- return $this->buildGrammarSet('nmspdecl',
- array(
- 'package [nmspname][term]',
- ));
- }
-
- protected function getNamespaceNameGrammarSet() {
- return $this->buildGrammarSet('nmspname',
- array(
- 'java.lang',
- 'java.io',
- 'com.example.proj.std',
- 'derp.example.www',
- ));
- }
-
- protected function getImportGrammarSet() {
- return $this->buildGrammarSet('import',
- array(
- 'import [nmspname][term]',
- 'import [nmspname].*[term]',
- 'import [nmspname].[classname][term]',
- ));
- }
-
- protected function getExprGrammarSet() {
- $expr = parent::getExprGrammarSet();
-
- $expr['expr'][] = 'new [classname]([funccallparam])';
-
- $expr['expr'][] = '[methodcall]';
- $expr['expr'][] = '[methodcall]';
- $expr['expr'][] = '[methodcall]';
- $expr['expr'][] = '[methodcall]';
-
- // Add some 'char's
- for ($ii = 0; $ii < 2; $ii++) {
- $expr['expr'][] = "'".Filesystem::readRandomCharacters(1)."'";
- }
-
- return $expr;
- }
-
- protected function getStmtGrammarSet() {
- $stmt = parent::getStmtGrammarSet();
-
- $stmt['stmt'][] = '[vardecl]';
- $stmt['stmt'][] = '[vardecl]';
- // `try` to `throw` a `Ball`!
- $stmt['stmt'][] = 'throw [classname][term]';
-
- return $stmt;
- }
-
- protected function getPropDeclGrammarSet() {
- return $this->buildGrammarSet('propdecl',
- array(
- '[visibility] [type] [varname][term]',
- ));
- }
-
- protected function getVarDeclGrammarSet() {
- return $this->buildGrammarSet('vardecl',
- array(
- '[type] [varname][term]',
- '[type] [assignment][term]',
- ));
- }
-
- protected function getFuncNameGrammarSet() {
- return $this->buildGrammarSet('funcname',
- array(
- '[methodname]',
- '[classname].[methodname]',
- // This is just silly (too much recursion)
- // '[classname].[funcname]',
- // Don't do this for now, it just clutters up output (thanks to rec.)
- // '[nmspname].[classname].[methodname]',
- ));
- }
-
- // Renamed from `funcname`
- protected function getMethodNameGrammarSet() {
- $funcnames = head(parent::getFuncNameGrammarSet());
- return $this->buildGrammarSet('methodname', $funcnames);
- }
-
- protected function getMethodFuncDeclGrammarSet() {
- return $this->buildGrammarSet('methodfuncdecl',
- array(
- '[methodreturn] [methodname]([funcparam]) '.
- '{[methodbody, indent, block, trim=right]}',
- ));
- }
-
- protected function getFuncParamGrammarSet() {
- return $this->buildGrammarSet('funcparam',
- array(
- '',
- '[type] [varname]',
- '[type] [varname], [type] [varname]',
- '[type] [varname], [type] [varname], [type] [varname]',
- ));
- }
-
- protected function getAbstractMethodDeclGrammarSet() {
- return $this->buildGrammarSet('abstractmethoddecl',
- array(
- 'abstract [methodreturn] [methodname]([funcparam])[term]',
- ));
- }
-
-}
diff --git a/src/grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php b/src/grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php
deleted file mode 100644
--- a/src/grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-final class PhutilPHPCodeSnippetContextFreeGrammar
- extends PhutilCLikeCodeSnippetContextFreeGrammar {
-
- protected function buildRuleSet() {
- return array_merge(parent::buildRuleSet(), $this->getClassRuleSets());
- }
-
- protected function getStartGrammarSet() {
- $start_grammar = parent::getStartGrammarSet();
-
- $start_grammar['start'][] = '[classdecl]';
- $start_grammar['start'][] = '[classdecl]';
-
- return $start_grammar;
- }
-
- protected function getExprGrammarSet() {
- $expr = parent::getExprGrammarSet();
-
- $expr['expr'][] = 'new [classname]([funccallparam])';
-
- $expr['expr'][] = '[classname]::[funccall]';
-
- return $expr;
- }
-
- protected function getVarNameGrammarSet() {
- $varnames = parent::getVarNameGrammarSet();
-
- foreach ($varnames as $vn_key => $vn_val) {
- foreach ($vn_val as $vv_key => $vv_value) {
- $varnames[$vn_key][$vv_key] = '$'.$vv_value;
- }
- }
-
- return $varnames;
- }
-
- protected function getFuncNameGrammarSet() {
- return $this->buildGrammarSet('funcname',
- array_mergev(get_defined_functions()));
- }
-
- protected function getMethodCallGrammarSet() {
- return $this->buildGrammarSet('methodcall',
- array(
- '$this->[funccall]',
- 'self::[funccall]',
- 'static::[funccall]',
- '[varname]->[funccall]',
- '[classname]::[funccall]',
- ));
- }
-
-}
diff --git a/src/markup/engine/PhutilRemarkupEngine.php b/src/markup/engine/PhutilRemarkupEngine.php
deleted file mode 100644
--- a/src/markup/engine/PhutilRemarkupEngine.php
+++ /dev/null
@@ -1,302 +0,0 @@
-<?php
-
-final class PhutilRemarkupEngine extends PhutilMarkupEngine {
-
- const MODE_DEFAULT = 0;
- const MODE_TEXT = 1;
- const MODE_HTML_MAIL = 2;
-
- const MAX_CHILD_DEPTH = 32;
-
- private $blockRules = array();
- private $config = array();
- private $mode;
- private $metadata = array();
- private $states = array();
- private $postprocessRules = array();
- private $storage;
-
- public function setConfig($key, $value) {
- $this->config[$key] = $value;
- return $this;
- }
-
- public function getConfig($key, $default = null) {
- return idx($this->config, $key, $default);
- }
-
- public function setMode($mode) {
- $this->mode = $mode;
- return $this;
- }
-
- public function isTextMode() {
- return $this->mode & self::MODE_TEXT;
- }
-
- public function isHTMLMailMode() {
- return $this->mode & self::MODE_HTML_MAIL;
- }
-
- public function setBlockRules(array $rules) {
- assert_instances_of($rules, 'PhutilRemarkupBlockRule');
-
- $rules = msortv($rules, 'getPriorityVector');
-
- $this->blockRules = $rules;
- foreach ($this->blockRules as $rule) {
- $rule->setEngine($this);
- }
-
- $post_rules = array();
- foreach ($this->blockRules as $block_rule) {
- foreach ($block_rule->getMarkupRules() as $rule) {
- $key = $rule->getPostprocessKey();
- if ($key !== null) {
- $post_rules[$key] = $rule;
- }
- }
- }
-
- $this->postprocessRules = $post_rules;
-
- return $this;
- }
-
- public function getTextMetadata($key, $default = null) {
- if (isset($this->metadata[$key])) {
- return $this->metadata[$key];
- }
- return idx($this->metadata, $key, $default);
- }
-
- public function setTextMetadata($key, $value) {
- $this->metadata[$key] = $value;
- return $this;
- }
-
- public function storeText($text) {
- if ($this->isTextMode()) {
- $text = phutil_safe_html($text);
- }
- return $this->storage->store($text);
- }
-
- public function overwriteStoredText($token, $new_text) {
- if ($this->isTextMode()) {
- $new_text = phutil_safe_html($new_text);
- }
- $this->storage->overwrite($token, $new_text);
- return $this;
- }
-
- public function markupText($text) {
- return $this->postprocessText($this->preprocessText($text));
- }
-
- public function pushState($state) {
- if (empty($this->states[$state])) {
- $this->states[$state] = 0;
- }
- $this->states[$state]++;
- return $this;
- }
-
- public function popState($state) {
- if (empty($this->states[$state])) {
- throw new Exception(pht("State '%s' pushed more than popped!", $state));
- }
- $this->states[$state]--;
- if (!$this->states[$state]) {
- unset($this->states[$state]);
- }
- return $this;
- }
-
- public function getState($state) {
- return !empty($this->states[$state]);
- }
-
- public function preprocessText($text) {
- $this->metadata = array();
- $this->storage = new PhutilRemarkupBlockStorage();
-
- $blocks = $this->splitTextIntoBlocks($text);
-
- $output = array();
- foreach ($blocks as $block) {
- $output[] = $this->markupBlock($block);
- }
- $output = $this->flattenOutput($output);
-
- $map = $this->storage->getMap();
- $this->storage = null;
- $metadata = $this->metadata;
-
-
- return array(
- 'output' => $output,
- 'storage' => $map,
- 'metadata' => $metadata,
- );
- }
-
- private function splitTextIntoBlocks($text, $depth = 0) {
- // Apply basic block and paragraph normalization to the text. NOTE: We don't
- // strip trailing whitespace because it is semantic in some contexts,
- // notably inlined diffs that the author intends to show as a code block.
- $text = phutil_split_lines($text, true);
- $block_rules = $this->blockRules;
- $blocks = array();
- $cursor = 0;
- $prev_block = array();
-
- while (isset($text[$cursor])) {
- $starting_cursor = $cursor;
- foreach ($block_rules as $block_rule) {
- $num_lines = $block_rule->getMatchingLineCount($text, $cursor);
-
- if ($num_lines) {
- if ($blocks) {
- $prev_block = last($blocks);
- }
-
- $curr_block = array(
- 'start' => $cursor,
- 'num_lines' => $num_lines,
- 'rule' => $block_rule,
- 'is_empty' => self::isEmptyBlock($text, $cursor, $num_lines),
- 'children' => array(),
- );
-
- if ($prev_block
- && self::shouldMergeBlocks($text, $prev_block, $curr_block)) {
- $blocks[last_key($blocks)]['num_lines'] += $curr_block['num_lines'];
- $blocks[last_key($blocks)]['is_empty'] =
- $blocks[last_key($blocks)]['is_empty'] && $curr_block['is_empty'];
- } else {
- $blocks[] = $curr_block;
- }
-
- $cursor += $num_lines;
- break;
- }
- }
-
- if ($starting_cursor === $cursor) {
- throw new Exception(pht('Block in text did not match any block rule.'));
- }
- }
-
- foreach ($blocks as $key => $block) {
- $lines = array_slice($text, $block['start'], $block['num_lines']);
- $blocks[$key]['text'] = implode('', $lines);
- }
-
- // Stop splitting child blocks apart if we get too deep. This arrests
- // any blocks which have looping child rules, and stops the stack from
- // exploding if someone writes a hilarious comment with 5,000 levels of
- // quoted text.
-
- if ($depth < self::MAX_CHILD_DEPTH) {
- foreach ($blocks as $key => $block) {
- $rule = $block['rule'];
- if (!$rule->supportsChildBlocks()) {
- continue;
- }
-
- list($parent_text, $child_text) = $rule->extractChildText(
- $block['text']);
- $blocks[$key]['text'] = $parent_text;
- $blocks[$key]['children'] = $this->splitTextIntoBlocks(
- $child_text,
- $depth + 1);
- }
- }
-
- return $blocks;
- }
-
- private function markupBlock(array $block) {
- $children = array();
- foreach ($block['children'] as $child) {
- $children[] = $this->markupBlock($child);
- }
-
- if ($children) {
- $children = $this->flattenOutput($children);
- } else {
- $children = null;
- }
-
- return $block['rule']->markupText($block['text'], $children);
- }
-
- private function flattenOutput(array $output) {
- if ($this->isTextMode()) {
- $output = implode("\n\n", $output)."\n";
- } else {
- $output = phutil_implode_html("\n\n", $output);
- }
-
- return $output;
- }
-
- private static function shouldMergeBlocks($text, $prev_block, $curr_block) {
- $block_rules = ipull(array($prev_block, $curr_block), 'rule');
-
- $default_rule = 'PhutilRemarkupDefaultBlockRule';
- try {
- assert_instances_of($block_rules, $default_rule);
-
- // If the last block was empty keep merging
- if ($prev_block['is_empty']) {
- return true;
- }
-
- // If this line is blank keep merging
- if ($curr_block['is_empty']) {
- return true;
- }
-
- // If the current line and the last line have content, keep merging
- if (strlen(trim($text[$curr_block['start'] - 1]))) {
- if (strlen(trim($text[$curr_block['start']]))) {
- return true;
- }
- }
- } catch (Exception $e) {}
-
- return false;
- }
-
- private static function isEmptyBlock($text, $start, $num_lines) {
- for ($cursor = $start; $cursor < $start + $num_lines; $cursor++) {
- if (strlen(trim($text[$cursor]))) {
- return false;
- }
- }
- return true;
- }
-
- public function postprocessText(array $dict) {
- $this->metadata = idx($dict, 'metadata', array());
-
- $this->storage = new PhutilRemarkupBlockStorage();
- $this->storage->setMap(idx($dict, 'storage', array()));
-
- foreach ($this->blockRules as $block_rule) {
- $block_rule->postprocess();
- }
-
- foreach ($this->postprocessRules as $rule) {
- $rule->didMarkupText();
- }
-
- return $this->restoreText(idx($dict, 'output'));
- }
-
- public function restoreText($text) {
- return $this->storage->restore($text, $this->isTextMode());
- }
-}
diff --git a/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php b/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php
deleted file mode 100644
--- a/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-
-/**
- * Test cases for @{class:PhutilRemarkupEngine}.
- */
-final class PhutilRemarkupEngineTestCase extends PhutilTestCase {
-
- public function testEngine() {
- $root = dirname(__FILE__).'/remarkup/';
- foreach (Filesystem::listDirectory($root, $hidden = false) as $file) {
- $this->markupText($root.$file);
- }
- }
-
- private function markupText($markup_file) {
- $contents = Filesystem::readFile($markup_file);
- $file = basename($markup_file);
-
- $parts = explode("\n~~~~~~~~~~\n", $contents);
- $this->assertEqual(3, count($parts), $markup_file);
-
- list($input_remarkup, $expected_output, $expected_text) = $parts;
-
- $input_remarkup = $this->unescapeTrailingWhitespace($input_remarkup);
- $expected_output = $this->unescapeTrailingWhitespace($expected_output);
- $expected_text = $this->unescapeTrailingWhitespace($expected_text);
-
- $engine = $this->buildNewTestEngine();
-
- switch ($file) {
- case 'raw-escape.txt':
-
- // NOTE: Here, we want to test PhutilRemarkupEscapeRemarkupRule and
- // PhutilRemarkupBlockStorage, which are triggered by "\1". In the
- // test, "~" is used as a placeholder for "\1" since it's hard to type
- // "\1".
-
- $input_remarkup = str_replace('~', "\1", $input_remarkup);
- $expected_output = str_replace('~', "\1", $expected_output);
- $expected_text = str_replace('~', "\1", $expected_text);
- break;
- case 'toc.txt':
- $engine->setConfig('header.generate-toc', true);
- break;
- case 'link-same-window.txt':
- $engine->setConfig('uri.same-window', true);
- break;
- case 'link-square.txt':
- $engine->setConfig('uri.base', 'http://www.example.com/');
- $engine->setConfig('uri.here', 'http://www.example.com/page/');
- break;
- }
-
- $actual_output = (string)$engine->markupText($input_remarkup);
-
- switch ($file) {
- case 'toc.txt':
- $table_of_contents =
- PhutilRemarkupHeaderBlockRule::renderTableOfContents($engine);
- $actual_output = $table_of_contents."\n\n".$actual_output;
- break;
- }
-
- $this->assertEqual(
- $expected_output,
- $actual_output,
- pht("Failed to markup HTML in file '%s'.", $file));
-
- $engine->setMode(PhutilRemarkupEngine::MODE_TEXT);
- $actual_output = (string)$engine->markupText($input_remarkup);
-
- $this->assertEqual(
- $expected_text,
- $actual_output,
- pht("Failed to markup text in file '%s'.", $file));
- }
-
- private function buildNewTestEngine() {
- $engine = new PhutilRemarkupEngine();
-
- $engine->setConfig(
- 'uri.allowed-protocols',
- array(
- 'http' => true,
- 'mailto' => true,
- 'tel' => true,
- ));
-
- $rules = array();
- $rules[] = new PhutilRemarkupEscapeRemarkupRule();
- $rules[] = new PhutilRemarkupMonospaceRule();
- $rules[] = new PhutilRemarkupDocumentLinkRule();
- $rules[] = new PhutilRemarkupHyperlinkRule();
- $rules[] = new PhutilRemarkupBoldRule();
- $rules[] = new PhutilRemarkupItalicRule();
- $rules[] = new PhutilRemarkupDelRule();
- $rules[] = new PhutilRemarkupUnderlineRule();
- $rules[] = new PhutilRemarkupHighlightRule();
-
- $blocks = array();
- $blocks[] = new PhutilRemarkupQuotesBlockRule();
- $blocks[] = new PhutilRemarkupReplyBlockRule();
- $blocks[] = new PhutilRemarkupHeaderBlockRule();
- $blocks[] = new PhutilRemarkupHorizontalRuleBlockRule();
- $blocks[] = new PhutilRemarkupCodeBlockRule();
- $blocks[] = new PhutilRemarkupLiteralBlockRule();
- $blocks[] = new PhutilRemarkupNoteBlockRule();
- $blocks[] = new PhutilRemarkupTableBlockRule();
- $blocks[] = new PhutilRemarkupSimpleTableBlockRule();
- $blocks[] = new PhutilRemarkupDefaultBlockRule();
- $blocks[] = new PhutilRemarkupListBlockRule();
- $blocks[] = new PhutilRemarkupInterpreterBlockRule();
-
- foreach ($blocks as $block) {
- if (!($block instanceof PhutilRemarkupCodeBlockRule)) {
- $block->setMarkupRules($rules);
- }
- }
-
- $engine->setBlockRules($blocks);
-
- return $engine;
- }
-
-
- private function unescapeTrailingWhitespace($input) {
- // Remove up to one "~" at the end of each line so trailing whitespace may
- // be written in tests as " ~".
- return preg_replace('/~$/m', '', $input);
- }
-
-}
diff --git a/src/markup/engine/__tests__/remarkup/across-newlines.txt b/src/markup/engine/__tests__/remarkup/across-newlines.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/across-newlines.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-**duck
-quack**
-~~~~~~~~~~
-<p><strong>duck
-quack</strong></p>
-~~~~~~~~~~
-**duck quack**
diff --git a/src/markup/engine/__tests__/remarkup/backticks-whitespace.txt b/src/markup/engine/__tests__/remarkup/backticks-whitespace.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/backticks-whitespace.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-```x```
-
-```
-y
-```
-~~~~~~~~~~
-<div class="remarkup-code-block" data-code-lang="text" data-sigil="remarkup-code-block"><pre class="remarkup-code">x</pre></div>
-
-
-
-<div class="remarkup-code-block" data-code-lang="text" data-sigil="remarkup-code-block"><pre class="remarkup-code">y</pre></div>
-~~~~~~~~~~
- x
-
-
-
- y
diff --git a/src/markup/engine/__tests__/remarkup/block-then-list.txt b/src/markup/engine/__tests__/remarkup/block-then-list.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/block-then-list.txt
+++ /dev/null
@@ -1,12 +0,0 @@
- lang=txt
- code block
-
- - still a code block
-~~~~~~~~~~
-<div class="remarkup-code-block" data-code-lang="txt" data-sigil="remarkup-code-block"><pre class="remarkup-code">code block
-
-- still a code block</pre></div>
-~~~~~~~~~~
- code block
-
- - still a code block
diff --git a/src/markup/engine/__tests__/remarkup/code-block-whitespace.txt b/src/markup/engine/__tests__/remarkup/code-block-whitespace.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/code-block-whitespace.txt
+++ /dev/null
@@ -1,9 +0,0 @@
- lang=txt
- x
- y
-~~~~~~~~~~
-<div class="remarkup-code-block" data-code-lang="txt" data-sigil="remarkup-code-block"><pre class="remarkup-code"> x
-y</pre></div>
-~~~~~~~~~~
- x
- y
diff --git a/src/markup/engine/__tests__/remarkup/del.txt b/src/markup/engine/__tests__/remarkup/del.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/del.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-omg~~ wtf~~~~~ bbq~~~ lol~~~
-~~deleted text~~~
-~~This is a great idea~~~ die forever please
-~~~~~~~
-~~~~~~~~~~
-<p>omg~~ wtf~~~~~ bbq~~~ lol~~~
-<del>deleted text</del>
-<del>This is a great idea~</del> die forever please
-~~~~~~</p>
-~~~~~~~~~~
-omg~~ wtf~~~~~ bbq~~~ lol~~ ~~deleted text~~ ~~This is a great idea~~~ die forever please ~~~~~~~
diff --git a/src/markup/engine/__tests__/remarkup/diff.txt b/src/markup/engine/__tests__/remarkup/diff.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/diff.txt
+++ /dev/null
@@ -1,36 +0,0 @@
-here is a diff
-
- lang=diff
- @@ derp derp @@
- x
- y
-
- - x
- - y
- + z
-
-derp derp
-~~~~~~~~~~
-<p>here is a diff</p>
-
-<div class="remarkup-code-block" data-code-lang="diff" data-sigil="remarkup-code-block"><pre class="remarkup-code">@@ derp derp @@
-x
-y
-
-- x
-- y
-+ z</pre></div>
-
-<p>derp derp</p>
-~~~~~~~~~~
-here is a diff
-
- @@ derp derp @@
- x
- y
-
- - x
- - y
- + z
-
-derp derp
diff --git a/src/markup/engine/__tests__/remarkup/disallowed-link.txt b/src/markup/engine/__tests__/remarkup/disallowed-link.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/disallowed-link.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-javascript://www.example.com/
-~~~~~~~~~~
-<p>javascript://www.example.com/</p>
-~~~~~~~~~~
-javascript://www.example.com/
diff --git a/src/markup/engine/__tests__/remarkup/entities.txt b/src/markup/engine/__tests__/remarkup/entities.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/entities.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-< > & "
-~~~~~~~~~~
-<p>&lt; &gt; &amp; &quot;</p>
-~~~~~~~~~~
-< > & "
diff --git a/src/markup/engine/__tests__/remarkup/header-skip.txt b/src/markup/engine/__tests__/remarkup/header-skip.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/header-skip.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-#2 is my favorite.
-
-#project
-~~~~~~~~~~
-<p>#2 is my favorite.</p>
-
-<p>#project</p>
-~~~~~~~~~~
-#2 is my favorite.
-
-#project
diff --git a/src/markup/engine/__tests__/remarkup/headers.txt b/src/markup/engine/__tests__/remarkup/headers.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/headers.txt
+++ /dev/null
@@ -1,57 +0,0 @@
-@nolint (UTF8)
-
-=a=
-
-blah blah blah
-
-
-= b =
-
-Markdown-Style Large Header
-====
-
-Markdown-Style Small Header
-----
-
-=== Remarkup-Style Smaller Header
-
-
-= ☃☃☃ UTF8 Header ☃☃☃ =
-~~~~~~~~~~
-<p>@nolint (UTF8)</p>
-
-<h2 class="remarkup-header">a</h2>
-
-<p>blah blah blah</p>
-
-<h2 class="remarkup-header">b</h2>
-
-<h2 class="remarkup-header">Markdown-Style Large Header</h2>
-
-<h3 class="remarkup-header">Markdown-Style Small Header</h3>
-
-<h4 class="remarkup-header">Remarkup-Style Smaller Header</h4>
-
-<h2 class="remarkup-header">☃☃☃ UTF8 Header ☃☃☃</h2>
-~~~~~~~~~~
-@nolint (UTF8)
-
-a
-=
-
-blah blah blah
-
-b
-=
-
-Markdown-Style Large Header
-===========================
-
-Markdown-Style Small Header
----------------------------
-
-Remarkup-Style Smaller Header
------------------------------
-
-☃☃☃ UTF8 Header ☃☃☃
-===================
diff --git a/src/markup/engine/__tests__/remarkup/highlight.txt b/src/markup/engine/__tests__/remarkup/highlight.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/highlight.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-how about we !!highlight!! some !!TEXT!!!
-wow this must be **!!very important!!**
-omg!!!!!
-~~~~~~~~~~
-<p>how about we <span class="remarkup-highlight">highlight</span> some <span class="remarkup-highlight">TEXT!</span>
-wow this must be <strong><span class="remarkup-highlight">very important</span></strong>
-omg!!!!!</p>
-~~~~~~~~~~
-how about we !!highlight!! some !!TEXT!!! wow this must be **!!very important!!** omg!!!!!
diff --git a/src/markup/engine/__tests__/remarkup/horizonal-rule.txt b/src/markup/engine/__tests__/remarkup/horizonal-rule.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/horizonal-rule.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-___
-
-_____
-
-***
-
-* * * * * * *
-
----
-
-- - - - - - -
-
- ---
-~~~~~~~~~~
-<hr class="remarkup-hr" />
-
-<hr class="remarkup-hr" />
-
-<hr class="remarkup-hr" />
-
-<hr class="remarkup-hr" />
-
-<hr class="remarkup-hr" />
-
-<hr class="remarkup-hr" />
-
-<hr class="remarkup-hr" />
-~~~~~~~~~~
-___
-
-_____
-
-***
-
-* * * * * * *
-
----
-
-- - - - - - -
-
- ---
diff --git a/src/markup/engine/__tests__/remarkup/important.txt b/src/markup/engine/__tests__/remarkup/important.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/important.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-IMPORTANT: interesting **stuff**
-
-(IMPORTANT) interesting **stuff**
-~~~~~~~~~~
-<div class="remarkup-important"><span class="remarkup-note-word">IMPORTANT:</span> interesting <strong>stuff</strong></div>
-
-
-
-<div class="remarkup-important">interesting <strong>stuff</strong></div>
-~~~~~~~~~~
-IMPORTANT: interesting **stuff**
-
-
-
-(IMPORTANT) interesting **stuff**
diff --git a/src/markup/engine/__tests__/remarkup/interpreter-test.txt b/src/markup/engine/__tests__/remarkup/interpreter-test.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/interpreter-test.txt
+++ /dev/null
@@ -1,58 +0,0 @@
-phutil_test_block_interpreter (foo=bar) {{{
-content
-}}}
-
-phutil_test_block_interpreter {{{ content
-content }}}
-
-phutil_test_block_interpreter {{{ content }}}
-
-phutil_test_block_interpreter(x=y){{{content}}}
-
-phutil_fake_test_block_interpreter {{{ content }}}
-~~~~~~~~~~
-Content: (content)
-Argv: (foo=bar)
-
-
-
-Content: ( content
-content )
-Argv: ()
-
-
-
-Content: ( content )
-Argv: ()
-
-
-
-Content: (content)
-Argv: (x=y)
-
-
-
-<div class="remarkup-interpreter-error">No interpreter found: phutil_fake_test_block_interpreter</div>
-~~~~~~~~~~
-Content: (content)
-Argv: (foo=bar)
-
-
-
-Content: ( content
-content )
-Argv: ()
-
-
-
-Content: ( content )
-Argv: ()
-
-
-
-Content: (content)
-Argv: (x=y)
-
-
-
-(No interpreter found: phutil_fake_test_block_interpreter)
diff --git a/src/markup/engine/__tests__/remarkup/just-backticks.txt b/src/markup/engine/__tests__/remarkup/just-backticks.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/just-backticks.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-```
-~~~~~~~~~~
-<div class="remarkup-code-block" data-code-lang="text" data-sigil="remarkup-code-block"><pre class="remarkup-code"></pre></div>
-~~~~~~~~~~
-
diff --git a/src/markup/engine/__tests__/remarkup/leading-newline.txt b/src/markup/engine/__tests__/remarkup/leading-newline.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/leading-newline.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-
-a
-~~~~~~~~~~
-<p>a</p>
-~~~~~~~~~~
-a
diff --git a/src/markup/engine/__tests__/remarkup/link-alternate.txt b/src/markup/engine/__tests__/remarkup/link-alternate.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-alternate.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-[Example](http://www.example.com/)
-
-x[0][1](**ptr);
-
-~~~~~~~~~~
-<p><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">Example</a></p>
-
-<p>x[0][1](**ptr);</p>
-~~~~~~~~~~
-Example <http://www.example.com/>
-
-x[0][1](**ptr);
diff --git a/src/markup/engine/__tests__/remarkup/link-brackets.txt b/src/markup/engine/__tests__/remarkup/link-brackets.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-brackets.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-<http://www.zany.com/omg/weird_url,,,>
-~~~~~~~~~~
-<p><a href="http://www.zany.com/omg/weird_url,,," class="remarkup-link" target="_blank" rel="noreferrer">http://www.zany.com/omg/weird_url,,,</a></p>
-~~~~~~~~~~
-http://www.zany.com/omg/weird_url,,,
diff --git a/src/markup/engine/__tests__/remarkup/link-edge-cases.txt b/src/markup/engine/__tests__/remarkup/link-edge-cases.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-edge-cases.txt
+++ /dev/null
@@ -1,35 +0,0 @@
-http://www.example.com/
-
-(http://www.example.com/)
-
-<http://www.example.com/>
-
-http://www.example.com/wiki/example_(disambiguation)
-
-(example http://www.example.com/)
-
-Quick! http://www.example.com/!
-~~~~~~~~~~
-<p><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a></p>
-
-<p>(<a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a>)</p>
-
-<p><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a></p>
-
-<p><a href="http://www.example.com/wiki/example_(disambiguation)" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/wiki/example_(disambiguation)</a></p>
-
-<p>(example <a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a>)</p>
-
-<p>Quick! <a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a>!</p>
-~~~~~~~~~~
-http://www.example.com/
-
-(http://www.example.com/)
-
-http://www.example.com/
-
-http://www.example.com/wiki/example_(disambiguation)
-
-(example http://www.example.com/)
-
-Quick! http://www.example.com/!
diff --git a/src/markup/engine/__tests__/remarkup/link-mailto.txt b/src/markup/engine/__tests__/remarkup/link-mailto.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-mailto.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-[[ mailto:alincoln@example.com | mail me ]]
-
-[ mail me ]( mailto:alincoln@example.com )
-
-[[mailto:alincoln@example.com]]
-
-~~~~~~~~~~
-<p><a href="mailto:alincoln@example.com" class="remarkup-link" target="_blank" rel="noreferrer">mail me</a></p>
-
-<p><a href="mailto:alincoln@example.com" class="remarkup-link" target="_blank" rel="noreferrer">mail me</a></p>
-
-<p><a href="mailto:alincoln@example.com" class="remarkup-link" target="_blank" rel="noreferrer">alincoln@example.com</a></p>
-~~~~~~~~~~
-mail me <alincoln@example.com>
-
-mail me <alincoln@example.com>
-
-alincoln@example.com
diff --git a/src/markup/engine/__tests__/remarkup/link-mixed.txt b/src/markup/engine/__tests__/remarkup/link-mixed.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-mixed.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-[[http://www.example.com/ | Example]](http://www.alternate.org/)
-
-(http://www.alternate.org/)[[http://www.example.com/ | Example]]
-
-<http://www.example.com/ [[http://www.example.net/ | Example]]>
-
-~~~~~~~~~~
-<p><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">Example</a>(<a href="http://www.alternate.org/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.alternate.org/</a>)</p>
-
-<p>(<a href="http://www.alternate.org/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.alternate.org/</a>)<a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">Example</a></p>
-
-<p>&lt;<a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a> <a href="http://www.example.net/" class="remarkup-link" target="_blank" rel="noreferrer">Example</a>&gt;</p>
-~~~~~~~~~~
-Example <http://www.example.com/>(http://www.alternate.org/)
-
-(http://www.alternate.org/)Example <http://www.example.com/>
-
-<http://www.example.com/ Example <http://www.example.net/>>
diff --git a/src/markup/engine/__tests__/remarkup/link-noreferrer.txt b/src/markup/engine/__tests__/remarkup/link-noreferrer.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-noreferrer.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-[[ /\evil.com ]]
-
-[[ /
-/evil.com ]]
-
-~~~~~~~~~~
-<p><a href="/\evil.com" class="remarkup-link" target="_blank" rel="noreferrer">/\evil.com</a></p>
-
-<p><a href="/
-/evil.com" class="remarkup-link" target="_blank" rel="noreferrer">/
-/evil.com</a></p>
-~~~~~~~~~~
-/\evil.com
-
-/
-/evil.com
diff --git a/src/markup/engine/__tests__/remarkup/link-same-window.txt b/src/markup/engine/__tests__/remarkup/link-same-window.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-same-window.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-[[http://www.example.com/]]
-
-http://www.example.com/
-~~~~~~~~~~
-<p><a href="http://www.example.com/" class="remarkup-link" rel="noreferrer">http://www.example.com/</a></p>
-
-<p><a href="http://www.example.com/" class="remarkup-link" rel="noreferrer">http://www.example.com/</a></p>
-~~~~~~~~~~
-http://www.example.com/
-
-http://www.example.com/
diff --git a/src/markup/engine/__tests__/remarkup/link-square.txt b/src/markup/engine/__tests__/remarkup/link-square.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-square.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-[[http://www.example.com/]]
-
-[[http://www.example.com/ | example.com]]
-
-[[/x/]]
-
-[[#anchor]]
-
-[[#anchor | Anchors ]]
-~~~~~~~~~~
-<p><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a></p>
-
-<p><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">example.com</a></p>
-
-<p><a href="http://www.example.com/x/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/x/</a></p>
-
-<p><a href="http://www.example.com/page/#anchor" class="remarkup-link" rel="noreferrer">http://www.example.com/page/#anchor</a></p>
-
-<p><a href="http://www.example.com/page/#anchor" class="remarkup-link" rel="noreferrer">Anchors</a></p>
-~~~~~~~~~~
-http://www.example.com/
-
-example.com <http://www.example.com/>
-
-http://www.example.com/x/
-
-http://www.example.com/page/#anchor
-
-Anchors <http://www.example.com/page/#anchor>
diff --git a/src/markup/engine/__tests__/remarkup/link-tel.txt b/src/markup/engine/__tests__/remarkup/link-tel.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-tel.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-[[ tel:18005555555 | call me ]]
-
-[ call me ]( tel:18005555555 )
-
-[[tel:18005555555]]
-
-~~~~~~~~~~
-<p><a href="tel:18005555555" class="remarkup-link" target="_blank" rel="noreferrer">call me</a></p>
-
-<p><a href="tel:18005555555" class="remarkup-link" target="_blank" rel="noreferrer">call me</a></p>
-
-<p><a href="tel:18005555555" class="remarkup-link" target="_blank" rel="noreferrer">18005555555</a></p>
-~~~~~~~~~~
-call me <18005555555>
-
-call me <18005555555>
-
-18005555555
diff --git a/src/markup/engine/__tests__/remarkup/link-with-angle-brackets.txt b/src/markup/engine/__tests__/remarkup/link-with-angle-brackets.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-with-angle-brackets.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-http://<www>.example.com/
-~~~~~~~~~~
-<p>http://&lt;www&gt;.example.com/</p>
-~~~~~~~~~~
-http://<www>.example.com/
diff --git a/src/markup/engine/__tests__/remarkup/link-with-angle-link-anchor.txt b/src/markup/engine/__tests__/remarkup/link-with-angle-link-anchor.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-with-angle-link-anchor.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-<http://x.y#http://x.y#>
-~~~~~~~~~~
-<p>&lt;http://x.y#http://x.y#&gt;</p>
-~~~~~~~~~~
-<http://x.y#http://x.y#>
diff --git a/src/markup/engine/__tests__/remarkup/link-with-link-anchor.txt b/src/markup/engine/__tests__/remarkup/link-with-link-anchor.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-with-link-anchor.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-http://x.y#http://x.y#
-~~~~~~~~~~
-<p>http://x.y#http://x.y#</p>
-~~~~~~~~~~
-http://x.y#http://x.y#
diff --git a/src/markup/engine/__tests__/remarkup/link-with-punctuation.txt b/src/markup/engine/__tests__/remarkup/link-with-punctuation.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-with-punctuation.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-http://www.example.com/,
-http://www.example.com/..
-http://www.example.com/!!!
-~~~~~~~~~~
-<p><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a>,
-<a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a>..
-<a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a>!!!</p>
-~~~~~~~~~~
-http://www.example.com/, http://www.example.com/.. http://www.example.com/!!!
diff --git a/src/markup/engine/__tests__/remarkup/link-with-tilde.txt b/src/markup/engine/__tests__/remarkup/link-with-tilde.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link-with-tilde.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-http://www.example.com/~~
-~~~~~~~~~~
-<p><a href="http://www.example.com/~" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/~</a></p>
-~~~~~~~~~~
-http://www.example.com/~~
diff --git a/src/markup/engine/__tests__/remarkup/link.txt b/src/markup/engine/__tests__/remarkup/link.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/link.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-http://www.example.com/
-~~~~~~~~~~
-<p><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com/</a></p>
-~~~~~~~~~~
-http://www.example.com/
diff --git a/src/markup/engine/__tests__/remarkup/list-alternate-style.txt b/src/markup/engine/__tests__/remarkup/list-alternate-style.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-alternate-style.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-- a
--- b
---- c
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">b<ul class="remarkup-list">
-<li class="remarkup-list-item">c</li>
-</ul></li>
-</ul></li>
-</ul>
-~~~~~~~~~~
-- a
- - b
- - c
diff --git a/src/markup/engine/__tests__/remarkup/list-blow-stack.txt b/src/markup/engine/__tests__/remarkup/list-blow-stack.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-blow-stack.txt
+++ /dev/null
@@ -1,138 +0,0 @@
-- a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
-
-
-derp
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-<li class="remarkup-list-item">a</li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul></li>
-</ul>
-
-<p>derp</p>
-~~~~~~~~~~
-- a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
- - a
-
-derp
diff --git a/src/markup/engine/__tests__/remarkup/list-checkboxes.txt b/src/markup/engine/__tests__/remarkup/list-checkboxes.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-checkboxes.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-- [] a
-- [ ] b
-- [X] c
-- d
-
-[ ] A
-[X] B
- [ ] C
- [ ] D
-
-[1] footnote
-
-~~~~~~~~~~
-<ul class="remarkup-list remarkup-list-with-checkmarks">
-<li class="remarkup-list-item remarkup-unchecked-item"><input type="checkbox" disabled="disabled" /> a</li>
-<li class="remarkup-list-item remarkup-unchecked-item"><input type="checkbox" disabled="disabled" /> b</li>
-<li class="remarkup-list-item remarkup-checked-item"><input type="checkbox" checked="checked" disabled="disabled" /> c</li>
-<li class="remarkup-list-item">d</li>
-</ul>
-
-<ul class="remarkup-list remarkup-list-with-checkmarks">
-<li class="remarkup-list-item remarkup-unchecked-item"><input type="checkbox" disabled="disabled" /> A</li>
-<li class="remarkup-list-item remarkup-checked-item"><input type="checkbox" checked="checked" disabled="disabled" /> B<ul class="remarkup-list remarkup-list-with-checkmarks">
-<li class="remarkup-list-item remarkup-unchecked-item"><input type="checkbox" disabled="disabled" /> C</li>
-<li class="remarkup-list-item remarkup-unchecked-item"><input type="checkbox" disabled="disabled" /> D</li>
-</ul></li>
-</ul>
-
-<p>[1] footnote</p>
-~~~~~~~~~~
-[ ] a
-[ ] b
-[X] c
-- d
-
-[ ] A
-[X] B
- [ ] C
- [ ] D
-
-[1] footnote
diff --git a/src/markup/engine/__tests__/remarkup/list-crazystairs.txt b/src/markup/engine/__tests__/remarkup/list-crazystairs.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-crazystairs.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-## Fruit
-- Apple
-- Banana
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item phantom-item"><ol class="remarkup-list">
-<li class="remarkup-list-item">Fruit</li>
-</ol></li>
-<li class="remarkup-list-item">Apple</li>
-<li class="remarkup-list-item">Banana</li>
-</ul>
-~~~~~~~~~~
- 1. Fruit
-- Apple
-- Banana
diff --git a/src/markup/engine/__tests__/remarkup/list-first-style-wins.txt b/src/markup/engine/__tests__/remarkup/list-first-style-wins.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-first-style-wins.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-# item
-- item
-- item
-
-derp
-~~~~~~~~~~
-<ol class="remarkup-list">
-<li class="remarkup-list-item">item</li>
-<li class="remarkup-list-item">item</li>
-<li class="remarkup-list-item">item</li>
-</ol>
-
-<p>derp</p>
-~~~~~~~~~~
-1. item
-2. item
-3. item
-
-derp
diff --git a/src/markup/engine/__tests__/remarkup/list-hash.txt b/src/markup/engine/__tests__/remarkup/list-hash.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-hash.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-# item
-# item
-# item
-
-derp
-~~~~~~~~~~
-<ol class="remarkup-list">
-<li class="remarkup-list-item">item</li>
-<li class="remarkup-list-item">item</li>
-<li class="remarkup-list-item">item</li>
-</ol>
-
-<p>derp</p>
-~~~~~~~~~~
-1. item
-2. item
-3. item
-
-derp
diff --git a/src/markup/engine/__tests__/remarkup/list-header-last.txt b/src/markup/engine/__tests__/remarkup/list-header-last.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-header-last.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# At the end of a block, this should be a list.
-~~~~~~~~~~
-<ol class="remarkup-list">
-<li class="remarkup-list-item">At the end of a block, this should be a list.</li>
-</ol>
-~~~~~~~~~~
-1. At the end of a block, this should be a list.
diff --git a/src/markup/engine/__tests__/remarkup/list-header.txt b/src/markup/engine/__tests__/remarkup/list-header.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-header.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-## Small Header
-
-This should be a small header.
-~~~~~~~~~~
-<h3 class="remarkup-header">Small Header</h3>
-
-<p>This should be a small header.</p>
-~~~~~~~~~~
-Small Header
-------------
-
-This should be a small header.
diff --git a/src/markup/engine/__tests__/remarkup/list-mixed-styles.txt b/src/markup/engine/__tests__/remarkup/list-mixed-styles.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-mixed-styles.txt
+++ /dev/null
@@ -1,15 +0,0 @@
- - a
- -- b
- --- c
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">b<ul class="remarkup-list">
-<li class="remarkup-list-item">c</li>
-</ul></li>
-</ul></li>
-</ul>
-~~~~~~~~~~
-- a
- - b
- - c
diff --git a/src/markup/engine/__tests__/remarkup/list-multi.txt b/src/markup/engine/__tests__/remarkup/list-multi.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-multi.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-- a
- -- b
- -- c
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">a<ul class="remarkup-list">
-<li class="remarkup-list-item">b</li>
-<li class="remarkup-list-item">c</li>
-</ul></li>
-</ul>
-~~~~~~~~~~
-- a
- - b
- - c
diff --git a/src/markup/engine/__tests__/remarkup/list-multiline.txt b/src/markup/engine/__tests__/remarkup/list-multiline.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-multiline.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-- a
- a
-- b
-b
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">a a</li>
-<li class="remarkup-list-item">b</li>
-</ul>
-
-<p>b</p>
-~~~~~~~~~~
-- a a
-- b
-
-b
diff --git a/src/markup/engine/__tests__/remarkup/list-nest.txt b/src/markup/engine/__tests__/remarkup/list-nest.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-nest.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-- item
- - sub
-- item
- # sub
- # sub
-- item
-
-derp
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">item<ul class="remarkup-list">
-<li class="remarkup-list-item">sub</li>
-</ul></li>
-<li class="remarkup-list-item">item<ol class="remarkup-list">
-<li class="remarkup-list-item">sub</li>
-<li class="remarkup-list-item">sub</li>
-</ol></li>
-<li class="remarkup-list-item">item</li>
-</ul>
-
-<p>derp</p>
-~~~~~~~~~~
-- item
- - sub
-- item
- 1. sub
- 2. sub
-- item
-
-derp
diff --git a/src/markup/engine/__tests__/remarkup/list-paragraphs.txt b/src/markup/engine/__tests__/remarkup/list-paragraphs.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-paragraphs.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-- This is a list item
- with several paragraphs.
-
- This is the second paragraph
- of the first list item.
-- This is the second item
- in the list.
- - This is a sublist.
-- This is the third item in the list.
-
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">This is a list item with several paragraphs.
-<br /><br />
-This is the second paragraph of the first list item.</li>
-<li class="remarkup-list-item">This is the second item in the list.<ul class="remarkup-list">
-<li class="remarkup-list-item">This is a sublist.</li>
-</ul></li>
-<li class="remarkup-list-item">This is the third item in the list.</li>
-</ul>
-~~~~~~~~~~
-- This is a list item with several paragraphs.
-
- This is the second paragraph of the first list item.
-- This is the second item in the list.
- - This is a sublist.
-- This is the third item in the list.
diff --git a/src/markup/engine/__tests__/remarkup/list-staircase.txt b/src/markup/engine/__tests__/remarkup/list-staircase.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-staircase.txt
+++ /dev/null
@@ -1,23 +0,0 @@
- - top
- - mid
-# bot
-
-derp
-~~~~~~~~~~
-<ol class="remarkup-list">
-<li class="remarkup-list-item phantom-item"><ul class="remarkup-list">
-<li class="remarkup-list-item phantom-item"><ul class="remarkup-list">
-<li class="remarkup-list-item">top</li>
-</ul></li>
-<li class="remarkup-list-item">mid</li>
-</ul></li>
-<li class="remarkup-list-item">bot</li>
-</ol>
-
-<p>derp</p>
-~~~~~~~~~~
- - top
- - mid
-1. bot
-
-derp
diff --git a/src/markup/engine/__tests__/remarkup/list-star.txt b/src/markup/engine/__tests__/remarkup/list-star.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-star.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-* item
-* item
-* item
-
-derp
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">item</li>
-<li class="remarkup-list-item">item</li>
-<li class="remarkup-list-item">item</li>
-</ul>
-
-<p>derp</p>
-~~~~~~~~~~
-- item
-- item
-- item
-
-derp
diff --git a/src/markup/engine/__tests__/remarkup/list-then-a-list.txt b/src/markup/engine/__tests__/remarkup/list-then-a-list.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-then-a-list.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-1) one
-
-- a
-~~~~~~~~~~
-<ol class="remarkup-list">
-<li class="remarkup-list-item">one</li>
-</ol>
-
-<ul class="remarkup-list">
-<li class="remarkup-list-item">a</li>
-</ul>
-~~~~~~~~~~
-1. one
-
-- a
diff --git a/src/markup/engine/__tests__/remarkup/list-vs-codeblock.txt b/src/markup/engine/__tests__/remarkup/list-vs-codeblock.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list-vs-codeblock.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-This should be a list:
-
- - apple
- - banana
-
-~~~~~~~~~~
-<p>This should be a list:</p>
-
-<ul class="remarkup-list">
-<li class="remarkup-list-item">apple</li>
-<li class="remarkup-list-item">banana</li>
-</ul>
-~~~~~~~~~~
-This should be a list:
-
-- apple
-- banana
diff --git a/src/markup/engine/__tests__/remarkup/list.txt b/src/markup/engine/__tests__/remarkup/list.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/list.txt
+++ /dev/null
@@ -1,13 +0,0 @@
- - < > & "
-
-text block
-~~~~~~~~~~
-<ul class="remarkup-list">
-<li class="remarkup-list-item">&lt; &gt; &amp; &quot;</li>
-</ul>
-
-<p>text block</p>
-~~~~~~~~~~
-- < > & "
-
-text block
diff --git a/src/markup/engine/__tests__/remarkup/monospaced-in-monospaced.txt b/src/markup/engine/__tests__/remarkup/monospaced-in-monospaced.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/monospaced-in-monospaced.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-query ##SELECT * FROM `table`##
-
-`SELECT * FROM ##table##`
-
-`**x**`
-
-~~~~~~~~~~
-<p>query <tt class="remarkup-monospaced">SELECT * FROM `table`</tt></p>
-
-<p><tt class="remarkup-monospaced">SELECT * FROM ##table##</tt></p>
-
-<p><tt class="remarkup-monospaced">**x**</tt></p>
-~~~~~~~~~~
-query ##SELECT * FROM `table`##
-
-`SELECT * FROM ##table##`
-
-`**x**`
diff --git a/src/markup/engine/__tests__/remarkup/monospaced-plural.txt b/src/markup/engine/__tests__/remarkup/monospaced-plural.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/monospaced-plural.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-`Zebra`s
-
-I can`t and I won`t.
-~~~~~~~~~~
-<p><tt class="remarkup-monospaced">Zebra</tt>s</p>
-
-<p>I can`t and I won`t.</p>
-~~~~~~~~~~
-`Zebra`s
-
-I can`t and I won`t.
diff --git a/src/markup/engine/__tests__/remarkup/monospaced.txt b/src/markup/engine/__tests__/remarkup/monospaced.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/monospaced.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-cmd ##ls --color > /dev/null##
-~~~~~~~~~~
-<p>cmd <tt class="remarkup-monospaced">ls --color &gt; /dev/null</tt></p>
-~~~~~~~~~~
-cmd ##ls --color > /dev/null##
diff --git a/src/markup/engine/__tests__/remarkup/newline-then-block.txt b/src/markup/engine/__tests__/remarkup/newline-then-block.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/newline-then-block.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-This is a paragraph.
-
-
- lang=txt
- First line of code block.
- Second line of code block.
-
-
-<table>
- <tr>
- <td>Cell 1</td>
- <td>Cell 2</td>
- </tr>
-</table>
-~~~~~~~~~~
-<p>This is a paragraph.</p>
-
-<div class="remarkup-code-block" data-code-lang="txt" data-sigil="remarkup-code-block"><pre class="remarkup-code">First line of code block.
-Second line of code block.</pre></div>
-
-<div class="remarkup-table-wrap"><table class="remarkup-table">
-<tr><td>Cell 1</td><td>Cell 2</td></tr>
-</table></div>
-~~~~~~~~~~
-This is a paragraph.
-
- First line of code block.
- Second line of code block.
-
-| Cell 1 | Cell 2 |
diff --git a/src/markup/engine/__tests__/remarkup/note-multiline.txt b/src/markup/engine/__tests__/remarkup/note-multiline.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/note-multiline.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-NOTE: a
-a
-
-b
-~~~~~~~~~~
-<div class="remarkup-note"><span class="remarkup-note-word">NOTE:</span> a
-a</div>
-
-<p>b</p>
-~~~~~~~~~~
-NOTE: a
-a
-
-b
diff --git a/src/markup/engine/__tests__/remarkup/note.txt b/src/markup/engine/__tests__/remarkup/note.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/note.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-NOTE: interesting **stuff**
-
-(NOTE) interesting **stuff**
-~~~~~~~~~~
-<div class="remarkup-note"><span class="remarkup-note-word">NOTE:</span> interesting <strong>stuff</strong></div>
-
-
-
-<div class="remarkup-note">interesting <strong>stuff</strong></div>
-~~~~~~~~~~
-NOTE: interesting **stuff**
-
-
-
-(NOTE) interesting **stuff**
diff --git a/src/markup/engine/__tests__/remarkup/ordered-list-with-numbers.txt b/src/markup/engine/__tests__/remarkup/ordered-list-with-numbers.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/ordered-list-with-numbers.txt
+++ /dev/null
@@ -1,64 +0,0 @@
-# aasdx
-# asdf
-
-1. asa
- # asdf
-234) asdf
-
-234) asd
-
-1. asd
-234) asd
-
-10. ten
-11. eleven
-12. twelve
-
-1/ This explicitly should not be formatted as a list.
-~~~~~~~~~~
-<ol class="remarkup-list">
-<li class="remarkup-list-item">aasdx</li>
-<li class="remarkup-list-item">asdf</li>
-</ol>
-
-<ol class="remarkup-list">
-<li class="remarkup-list-item">asa<ol class="remarkup-list">
-<li class="remarkup-list-item">asdf</li>
-</ol></li>
-<li class="remarkup-list-item">asdf</li>
-</ol>
-
-<ol class="remarkup-list" start="234">
-<li class="remarkup-list-item">asd</li>
-</ol>
-
-<ol class="remarkup-list">
-<li class="remarkup-list-item">asd</li>
-<li class="remarkup-list-item">asd</li>
-</ol>
-
-<ol class="remarkup-list" start="10">
-<li class="remarkup-list-item">ten</li>
-<li class="remarkup-list-item">eleven</li>
-<li class="remarkup-list-item">twelve</li>
-</ol>
-
-<p>1/ This explicitly should not be formatted as a list.</p>
-~~~~~~~~~~
-1. aasdx
-2. asdf
-
-1. asa
- 1. asdf
-2. asdf
-
-234. asd
-
-1. asd
-2. asd
-
-10. ten
-11. eleven
-12. twelve
-
-1/ This explicitly should not be formatted as a list.
diff --git a/src/markup/engine/__tests__/remarkup/percent-block-adjacent.txt b/src/markup/engine/__tests__/remarkup/percent-block-adjacent.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/percent-block-adjacent.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-%%%a%%%
-%%%b%%%
-
-%%%a
-b%%%
-
-%%%a%%%
-
-%%%b%%%
-~~~~~~~~~~
-<p class="remarkup-literal">a
-<br />b</p>
-
-<p class="remarkup-literal">a
-<br />b</p>
-
-<p class="remarkup-literal">a</p>
-
-<p class="remarkup-literal">b</p>
-~~~~~~~~~~
-a
-b
-
-a
-b
-
-a
-
-b
diff --git a/src/markup/engine/__tests__/remarkup/percent-block-multiline.txt b/src/markup/engine/__tests__/remarkup/percent-block-multiline.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/percent-block-multiline.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-**foo**
-%%%- first
-- second
-- third%%%
-[[http://hello | world]]
-~~~~~~~~~~
-<p><strong>foo</strong></p>
-
-<p class="remarkup-literal">- first
-<br />- second
-<br />- third</p>
-
-<p><a href="http://hello" class="remarkup-link" target="_blank" rel="noreferrer">world</a></p>
-~~~~~~~~~~
-**foo**
-
-- first
-- second
-- third
-
-world <http://hello>
diff --git a/src/markup/engine/__tests__/remarkup/percent-block-oneline.txt b/src/markup/engine/__tests__/remarkup/percent-block-oneline.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/percent-block-oneline.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-%%%[[http://hello | world]] **bold**%%%
-
- %%%[[http://hello | world]] **bold**%%%
-~~~~~~~~~~
-<p class="remarkup-literal">[[http://hello | world]] **bold**</p>
-
-<p class="remarkup-literal">[[http://hello | world]] **bold**</p>
-~~~~~~~~~~
-[[http://hello | world]] **bold**
-
-[[http://hello | world]] **bold**
diff --git a/src/markup/engine/__tests__/remarkup/percent-block-solo.txt b/src/markup/engine/__tests__/remarkup/percent-block-solo.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/percent-block-solo.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-%%%
-**x**%%%
-~~~~~~~~~~
-<p class="remarkup-literal">
-<br />**x**</p>
-~~~~~~~~~~
-
-**x**
diff --git a/src/markup/engine/__tests__/remarkup/quoted-angry.txt b/src/markup/engine/__tests__/remarkup/quoted-angry.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/quoted-angry.txt
+++ /dev/null
@@ -1,5 +0,0 @@
->>> REQUESTING CHANGES BECAUSE I'M ANGRY!
-~~~~~~~~~~
-<blockquote><blockquote><blockquote><p>REQUESTING CHANGES BECAUSE I&#039;M ANGRY!</p></blockquote></blockquote></blockquote>
-~~~~~~~~~~
->>> REQUESTING CHANGES BECAUSE I'M ANGRY!
diff --git a/src/markup/engine/__tests__/remarkup/quoted-code-block.txt b/src/markup/engine/__tests__/remarkup/quoted-code-block.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/quoted-code-block.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-> This should be a code block:
->
-> ```lang=php
-> <?php
-> $foo = 'bar';
-> ```
-~~~~~~~~~~
-<blockquote><p>This should be a code block:</p>
-
-<div class="remarkup-code-block" data-code-lang="php" data-sigil="remarkup-code-block"><pre class="remarkup-code"><span class="o">&lt;?php</span>
-<span class="nv">$foo</span> <span class="k">=</span> <span class="s">&#039;bar&#039;</span><span class="k">;</span></pre></div></blockquote>
-~~~~~~~~~~
-> This should be a code block:
->
-> <?php
-> $foo = 'bar';
diff --git a/src/markup/engine/__tests__/remarkup/quoted-indent-block.txt b/src/markup/engine/__tests__/remarkup/quoted-indent-block.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/quoted-indent-block.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-> xyz
-~~~~~~~~~~
-<blockquote><div class="remarkup-code-block" data-code-lang="text" data-sigil="remarkup-code-block"><pre class="remarkup-code">xyz</pre></div></blockquote>
-~~~~~~~~~~
-> xyz
diff --git a/src/markup/engine/__tests__/remarkup/quoted-lists.txt b/src/markup/engine/__tests__/remarkup/quoted-lists.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/quoted-lists.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-> # X
-> # Y
->
-> B
->
-> * C
-~~~~~~~~~~
-<blockquote><ol class="remarkup-list">
-<li class="remarkup-list-item">X</li>
-<li class="remarkup-list-item">Y</li>
-</ol>
-
-<p>B</p>
-
-<ul class="remarkup-list">
-<li class="remarkup-list-item">C</li>
-</ul></blockquote>
-~~~~~~~~~~
-> 1. X
-> 2. Y
->
-> B
->
-> - C
diff --git a/src/markup/engine/__tests__/remarkup/quoted-quote.txt b/src/markup/engine/__tests__/remarkup/quoted-quote.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/quoted-quote.txt
+++ /dev/null
@@ -1,19 +0,0 @@
->>! In U, W wrote:
-> - Y
->
-> Z
-~~~~~~~~~~
-<blockquote class="remarkup-reply-block">
-<div class="remarkup-reply-head">In U, W wrote:</div>
-<div class="remarkup-reply-body"><ul class="remarkup-list">
-<li class="remarkup-list-item">Y</li>
-</ul>
-
-<p>Z</p></div>
-</blockquote>
-~~~~~~~~~~
-In U, W wrote:
-
-> - Y
->
-> Z
diff --git a/src/markup/engine/__tests__/remarkup/quotes.txt b/src/markup/engine/__tests__/remarkup/quotes.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/quotes.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-> Dear Sir,
-> I am utterly disgusted with the quality
-> of your inflight food service.
-~~~~~~~~~~
-<blockquote><p>Dear Sir,
-I am utterly disgusted with the quality
-of your inflight food service.</p></blockquote>
-~~~~~~~~~~
-> Dear Sir, I am utterly disgusted with the quality of your inflight food service.
diff --git a/src/markup/engine/__tests__/remarkup/raw-escape.txt b/src/markup/engine/__tests__/remarkup/raw-escape.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/raw-escape.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-~1~~
-
-~2Z
-
-~a
-~~~~~~~~~~
-<p>~1~</p>
-
-<p>~2Z</p>
-
-<p>~a</p>
-~~~~~~~~~~
-~1~~
-
-~2Z
-
-~a
diff --git a/src/markup/engine/__tests__/remarkup/reply-basic.txt b/src/markup/engine/__tests__/remarkup/reply-basic.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/reply-basic.txt
+++ /dev/null
@@ -1,11 +0,0 @@
->>! In comment #123, alincoln wrote:
-> Four score and twenty years ago...
-~~~~~~~~~~
-<blockquote class="remarkup-reply-block">
-<div class="remarkup-reply-head">In comment #123, alincoln wrote:</div>
-<div class="remarkup-reply-body"><p>Four score and twenty years ago...</p></div>
-</blockquote>
-~~~~~~~~~~
-In comment #123, alincoln wrote:
-
-> Four score and twenty years ago...
diff --git a/src/markup/engine/__tests__/remarkup/reply-nested.txt b/src/markup/engine/__tests__/remarkup/reply-nested.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/reply-nested.txt
+++ /dev/null
@@ -1,48 +0,0 @@
->>! Previously, fruit:
->
-> - Apple
-> - Banana
-> - Cherry
->
->>>! More previously, vegetables:
->>
->> - Potato
->> - Potato
->> - Potato
->
-> The end.
-
-~~~~~~~~~~
-<blockquote class="remarkup-reply-block">
-<div class="remarkup-reply-head">Previously, fruit:</div>
-<div class="remarkup-reply-body"><ul class="remarkup-list">
-<li class="remarkup-list-item">Apple</li>
-<li class="remarkup-list-item">Banana</li>
-<li class="remarkup-list-item">Cherry</li>
-</ul>
-
-<blockquote class="remarkup-reply-block">
-<div class="remarkup-reply-head">More previously, vegetables:</div>
-<div class="remarkup-reply-body"><ul class="remarkup-list">
-<li class="remarkup-list-item">Potato</li>
-<li class="remarkup-list-item">Potato</li>
-<li class="remarkup-list-item">Potato</li>
-</ul></div>
-</blockquote>
-
-<p>The end.</p></div>
-</blockquote>
-~~~~~~~~~~
-Previously, fruit:
-
-> - Apple
-> - Banana
-> - Cherry
->
-> More previously, vegetables:
->
->> - Potato
->> - Potato
->> - Potato
->
-> The end.
diff --git a/src/markup/engine/__tests__/remarkup/simple-table-with-empty-row.txt b/src/markup/engine/__tests__/remarkup/simple-table-with-empty-row.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/simple-table-with-empty-row.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-| Alpaca |
-| |
-| Zebra |
-~~~~~~~~~~
-<div class="remarkup-table-wrap"><table class="remarkup-table">
-<tr><td>Alpaca</td></tr>
-<tr><td></td></tr>
-<tr><td>Zebra</td></tr>
-</table></div>
-~~~~~~~~~~
-| Alpaca |
-| |
-| Zebra |
diff --git a/src/markup/engine/__tests__/remarkup/simple-table-with-leading-space.txt b/src/markup/engine/__tests__/remarkup/simple-table-with-leading-space.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/simple-table-with-leading-space.txt
+++ /dev/null
@@ -1,7 +0,0 @@
- |a|b|
-~~~~~~~~~~
-<div class="remarkup-table-wrap"><table class="remarkup-table">
-<tr><td>a</td><td>b</td></tr>
-</table></div>
-~~~~~~~~~~
-| a | b |
diff --git a/src/markup/engine/__tests__/remarkup/simple-table-with-link.txt b/src/markup/engine/__tests__/remarkup/simple-table-with-link.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/simple-table-with-link.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-| [[ http://example.com | name ]] | [x] |
-~~~~~~~~~~
-<div class="remarkup-table-wrap"><table class="remarkup-table">
-<tr><td><a href="http://example.com" class="remarkup-link" target="_blank" rel="noreferrer">name</a></td><td>[x]</td></tr>
-</table></div>
-~~~~~~~~~~
-| name <http://example.com> | [x] |
diff --git a/src/markup/engine/__tests__/remarkup/simple-table.txt b/src/markup/engine/__tests__/remarkup/simple-table.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/simple-table.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-| analyze_resources | original | mobile only | www only | both |
-| | -------- | ----------- | -------- | ---- |
-| //real// | 31 s | 24 s | 31 s | 31 s
-| --------
-| //user// | 49 s | 25 s | 31 s | 49 s
-| --------
-| //sys// | 24 s | 12 s | 13 s | 24 s
-| -------
-~~~~~~~~~~
-<div class="remarkup-table-wrap"><table class="remarkup-table">
-<tr><td>analyze_resources</td><th>original</th><th>mobile only</th><th>www only</th><th>both</th></tr>
-<tr><th><em>real</em></th><td>31 s</td><td>24 s</td><td>31 s</td><td>31 s</td></tr>
-<tr><th><em>user</em></th><td>49 s</td><td>25 s</td><td>31 s</td><td>49 s</td></tr>
-<tr><th><em>sys</em></th><td>24 s</td><td>12 s</td><td>13 s</td><td>24 s</td></tr>
-</table></div>
-~~~~~~~~~~
-| analyze_resources | original | mobile only | www only | both |
-| | -------- | ----------- | -------- | ---- |
-| //real// | 31 s | 24 s | 31 s | 31 s |
-| ----------------- | | | | |
-| //user// | 49 s | 25 s | 31 s | 49 s |
-| ----------------- | | | | |
-| //sys// | 24 s | 12 s | 13 s | 24 s |
-| ----------------- | | | | |
diff --git a/src/markup/engine/__tests__/remarkup/simple.txt b/src/markup/engine/__tests__/remarkup/simple.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/simple.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-hello
-~~~~~~~~~~
-<p>hello</p>
-~~~~~~~~~~
-hello
diff --git a/src/markup/engine/__tests__/remarkup/table-with-direct-content.txt b/src/markup/engine/__tests__/remarkup/table-with-direct-content.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/table-with-direct-content.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-<table>quack</table>
-~~~~~~~~~~
-&lt;table&gt;quack&lt;/table&gt;
-~~~~~~~~~~
-<table>quack</table>
diff --git a/src/markup/engine/__tests__/remarkup/table-with-leading-space.txt b/src/markup/engine/__tests__/remarkup/table-with-leading-space.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/table-with-leading-space.txt
+++ /dev/null
@@ -1,7 +0,0 @@
- <table><tr><td>cell</td></tr></table>
-~~~~~~~~~~
-<div class="remarkup-table-wrap"><table class="remarkup-table">
-<tr><td>cell</td></tr>
-</table></div>
-~~~~~~~~~~
-| cell |
diff --git a/src/markup/engine/__tests__/remarkup/table-with-long-header.txt b/src/markup/engine/__tests__/remarkup/table-with-long-header.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/table-with-long-header.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-|x|
-||--
-~~~~~~~~~~
-<div class="remarkup-table-wrap"><table class="remarkup-table">
-<tr><td>x</td></tr>
-</table></div>
-~~~~~~~~~~
-| x |
diff --git a/src/markup/engine/__tests__/remarkup/table.txt b/src/markup/engine/__tests__/remarkup/table.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/table.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-<table>
-<tr><th>Table</th><th>Storage</th></tr>
-<tr><td>`differential_diff`</td><td>InnoDB</td></tr>
-<tr><td>`edge`</td><td>?</td></tr>
-</table>
-~~~~~~~~~~
-<div class="remarkup-table-wrap"><table class="remarkup-table">
-<tr><th>Table</th><th>Storage</th></tr>
-<tr><td><tt class="remarkup-monospaced">differential_diff</tt></td><td>InnoDB</td></tr>
-<tr><td><tt class="remarkup-monospaced">edge</tt></td><td>?</td></tr>
-</table></div>
-~~~~~~~~~~
-| Table | Storage |
-| ------------------- | ------- |
-| `differential_diff` | InnoDB |
-| `edge` | ? |
diff --git a/src/markup/engine/__tests__/remarkup/tick-block-multi.txt b/src/markup/engine/__tests__/remarkup/tick-block-multi.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/tick-block-multi.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-```code
-
-more code
-
-more code```
-
-~~~~~~~~~~
-<div class="remarkup-code-block" data-code-lang="text" data-sigil="remarkup-code-block"><pre class="remarkup-code">code
-
-more code
-
-more code</pre></div>
-~~~~~~~~~~
- code
-
- more code
-
- more code
diff --git a/src/markup/engine/__tests__/remarkup/tick-block.txt b/src/markup/engine/__tests__/remarkup/tick-block.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/tick-block.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-```code```
-~~~~~~~~~~
-<div class="remarkup-code-block" data-code-lang="text" data-sigil="remarkup-code-block"><pre class="remarkup-code">code</pre></div>
-~~~~~~~~~~
- code
diff --git a/src/markup/engine/__tests__/remarkup/toc.txt b/src/markup/engine/__tests__/remarkup/toc.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/toc.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-= [[ http://www.example.com/ | link_name ]] =
-
-== **bold** ==
-
-= http://www.example.com =
-
-~~~~~~~~~~
-<ul>
-<li><a href="#http-www-example-com-lin">link_name</a></li>
-<ul>
-<li><a href="#bold"><strong>bold</strong></a></li>
-</ul>
-<li><a href="#http-www-example-com">http://www.example.com</a></li>
-</ul>
-
-<h2 class="remarkup-header"><a name="http-www-example-com-lin"></a><a href="http://www.example.com/" class="remarkup-link" target="_blank" rel="noreferrer">link_name</a></h2>
-
-<h3 class="remarkup-header"><a name="bold"></a><strong>bold</strong></h3>
-
-<h2 class="remarkup-header"><a name="http-www-example-com"></a><a href="http://www.example.com" class="remarkup-link" target="_blank" rel="noreferrer">http://www.example.com</a></h2>
-~~~~~~~~~~
-[[ http://www.example.com/ | link_name ]]
-=========================================
-
-**bold**
---------
-
-http://www.example.com
-======================
diff --git a/src/markup/engine/__tests__/remarkup/trailing-whitespace-codeblock.txt b/src/markup/engine/__tests__/remarkup/trailing-whitespace-codeblock.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/trailing-whitespace-codeblock.txt
+++ /dev/null
@@ -1,39 +0,0 @@
- lang=txt
- code block
- code block
-
-
-
-
- code block
-
-
-
-
- code block
-~~~~~~~~~~
-<div class="remarkup-code-block" data-code-lang="txt" data-sigil="remarkup-code-block"><pre class="remarkup-code">code block
-code block
-
-
-
-
-code block
-
-
-
-
-code block</pre></div>
-~~~~~~~~~~
- code block
- code block
-
-
-
-
- code block
-
-
-
-
- code block
diff --git a/src/markup/engine/__tests__/remarkup/underline.txt b/src/markup/engine/__tests__/remarkup/underline.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/underline.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-omg__ wtf_____ bbq___ lol__
-__underlined text__
-__This is a great idea___ die forever please
-__
-/__notunderlined__/ and also /__notunderlined__.c
-~~~~~~~~~~
-<p>omg__ wtf_____ bbq___ lol__
-<u>underlined text</u>
-<u>This is a great idea_</u> die forever please
-__
-/__notunderlined__/ and also /__notunderlined__.c</p>
-~~~~~~~~~~
-omg__ wtf_____ bbq___ lol__ __underlined text__ __This is a great idea___ die forever please __ /__notunderlined__/ and also /__notunderlined__.c
diff --git a/src/markup/engine/__tests__/remarkup/warning.txt b/src/markup/engine/__tests__/remarkup/warning.txt
deleted file mode 100644
--- a/src/markup/engine/__tests__/remarkup/warning.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-WARNING: interesting **stuff**
-
-(WARNING) interesting **stuff**
-~~~~~~~~~~
-<div class="remarkup-warning"><span class="remarkup-note-word">WARNING:</span> interesting <strong>stuff</strong></div>
-
-
-
-<div class="remarkup-warning">interesting <strong>stuff</strong></div>
-~~~~~~~~~~
-WARNING: interesting **stuff**
-
-
-
-(WARNING) interesting **stuff**
diff --git a/src/markup/engine/remarkup/PhutilRemarkupBlockStorage.php b/src/markup/engine/remarkup/PhutilRemarkupBlockStorage.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/PhutilRemarkupBlockStorage.php
+++ /dev/null
@@ -1,176 +0,0 @@
-<?php
-
-/**
- * Remarkup prevents several classes of text-processing problems by replacing
- * tokens in the text as they are marked up. For example, if you write something
- * like this:
- *
- * //D12//
- *
- * It is processed in several stages. First the "D12" matches and is replaced
- * with a token, in the form of "<0x01><ID number><literal "Z">". The first
- * byte, "<0x01>" is a single byte with value 1 that marks a token. If this is
- * token ID "444", the text may now look like this:
- *
- * //<0x01>444Z//
- *
- * Now the italics match and are replaced, using the next token ID:
- *
- * <0x01>445Z
- *
- * When processing completes, all the tokens are replaced with their final
- * equivalents. For example, token 444 is evaluated to:
- *
- * <a href="http://...">...</a>
- *
- * Then token 445 is evaluated:
- *
- * <em><0x01>444Z</em>
- *
- * ...and all tokens it contains are replaced:
- *
- * <em><a href="http://...">...</a></em>
- *
- * If we didn't do this, the italics rule could match the "//" in "http://",
- * or any other number of processing mistakes could occur, some of which create
- * security risks.
- *
- * This class generates keys, and stores the map of keys to replacement text.
- */
-final class PhutilRemarkupBlockStorage extends Phobject {
-
- const MAGIC_BYTE = "\1";
-
- private $map = array();
- private $index = 0;
-
- public function store($text) {
- $key = self::MAGIC_BYTE.(++$this->index).'Z';
- $this->map[$key] = $text;
- return $key;
- }
-
- public function restore($corpus, $text_mode = false) {
- $map = $this->map;
-
- if (!$text_mode) {
- foreach ($map as $key => $content) {
- $map[$key] = phutil_escape_html($content);
- }
- $corpus = phutil_escape_html($corpus);
- }
-
- // NOTE: Tokens may contain other tokens: for example, a table may have
- // links inside it. So we can't do a single simple find/replace, because
- // we need to find and replace child tokens inside the content of parent
- // tokens.
-
- // However, we know that rules which have child tokens must always store
- // all their child tokens first, before they store their parent token: you
- // have to pass the "store(text)" API a block of text with tokens already
- // in it, so you must have created child tokens already.
-
- // Thus, all child tokens will appear in the list before parent tokens, so
- // if we start at the beginning of the list and replace all the tokens we
- // find in each piece of content, we'll end up expanding all subtokens
- // correctly.
-
- $map[] = $corpus;
- $seen = array();
- foreach ($map as $key => $content) {
- $seen[$key] = true;
-
- // If the content contains no token magic, we don't need to replace
- // anything.
- if (strpos($content, self::MAGIC_BYTE) === false) {
- continue;
- }
-
- $matches = null;
- preg_match_all(
- '/'.self::MAGIC_BYTE.'\d+Z/',
- $content,
- $matches,
- PREG_OFFSET_CAPTURE);
-
- $matches = $matches[0];
-
- // See PHI1114. We're replacing all the matches in one pass because this
- // is significantly faster than doing "substr_replace()" in a loop if the
- // corpus is large and we have a large number of matches.
-
- // Build a list of string pieces in "$parts" by interleaving the
- // plain strings between each token and the replacement token text, then
- // implode the whole thing when we're done.
-
- $parts = array();
- $pos = 0;
- foreach ($matches as $next) {
- $subkey = $next[0];
-
- // If we've matched a token pattern but don't actually have any
- // corresponding token, just skip this match. This should not be
- // possible, and should perhaps be an error.
- if (!isset($seen[$subkey])) {
- if (!isset($map[$subkey])) {
- throw new Exception(
- pht(
- 'Matched token key "%s" while processing remarkup block, but '.
- 'this token does not exist in the token map.',
- $subkey));
- } else {
- throw new Exception(
- pht(
- 'Matched token key "%s" while processing remarkup block, but '.
- 'this token appears later in the list than the key being '.
- 'processed ("%s").',
- $subkey,
- $key));
- }
- }
-
- $subpos = $next[1];
-
- // If there were any non-token bytes since the last token, add them.
- if ($subpos > $pos) {
- $parts[] = substr($content, $pos, $subpos - $pos);
- }
-
- // Add the token replacement text.
- $parts[] = $map[$subkey];
-
- // Move the non-token cursor forward over the token.
- $pos = $subpos + strlen($subkey);
- }
-
- // Add any leftover non-token bytes after the last token.
- $parts[] = substr($content, $pos);
-
- $content = implode('', $parts);
-
- $map[$key] = $content;
- }
- $corpus = last($map);
-
- if (!$text_mode) {
- $corpus = phutil_safe_html($corpus);
- }
-
- return $corpus;
- }
-
- public function overwrite($key, $new_text) {
- $this->map[$key] = $new_text;
- return $this;
- }
-
- public function getMap() {
- return $this->map;
- }
-
- public function setMap(array $map) {
- $this->map = $map;
- return $this;
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-abstract class PhutilRemarkupBlockInterpreter extends Phobject {
-
- private $engine;
-
- final public function setEngine($engine) {
- $this->engine = $engine;
- return $this;
- }
-
- final public function getEngine() {
- return $this->engine;
- }
-
- /**
- * @return string
- */
- abstract public function getInterpreterName();
-
- abstract public function markupContent($content, array $argv);
-
- protected function markupError($string) {
- if ($this->getEngine()->isTextMode()) {
- return '('.$string.')';
- } else {
- return phutil_tag(
- 'div',
- array(
- 'class' => 'remarkup-interpreter-error',
- ),
- $string);
- }
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php
+++ /dev/null
@@ -1,170 +0,0 @@
-<?php
-
-abstract class PhutilRemarkupBlockRule extends Phobject {
-
- private $engine;
- private $rules = array();
-
- /**
- * Determine the order in which blocks execute. Blocks with smaller priority
- * numbers execute sooner than blocks with larger priority numbers. The
- * default priority for blocks is `500`.
- *
- * Priorities are used to disambiguate syntax which can match multiple
- * patterns. For example, ` - Lorem ipsum...` may be a code block or a
- * list.
- *
- * @return int Priority at which this block should execute.
- */
- public function getPriority() {
- return 500;
- }
-
- final public function getPriorityVector() {
- return id(new PhutilSortVector())
- ->addInt($this->getPriority())
- ->addString(get_class($this));
- }
-
- abstract public function markupText($text, $children);
-
- /**
- * This will get an array of unparsed lines and return the number of lines
- * from the first array value that it can parse.
- *
- * @param array $lines
- * @param int $cursor
- *
- * @return int
- */
- abstract public function getMatchingLineCount(array $lines, $cursor);
-
- protected function didMarkupText() {
- return;
- }
-
- final public function setEngine(PhutilRemarkupEngine $engine) {
- $this->engine = $engine;
- $this->updateRules();
- return $this;
- }
-
- final protected function getEngine() {
- return $this->engine;
- }
-
- public function setMarkupRules(array $rules) {
- assert_instances_of($rules, 'PhutilRemarkupRule');
- $this->rules = $rules;
- $this->updateRules();
- return $this;
- }
-
- private function updateRules() {
- $engine = $this->getEngine();
- if ($engine) {
- $this->rules = msort($this->rules, 'getPriority');
- foreach ($this->rules as $rule) {
- $rule->setEngine($engine);
- }
- }
- return $this;
- }
-
- final public function getMarkupRules() {
- return $this->rules;
- }
-
- final public function postprocess() {
- $this->didMarkupText();
- }
-
- final protected function applyRules($text) {
- foreach ($this->getMarkupRules() as $rule) {
- $text = $rule->apply($text);
- }
- return $text;
- }
-
- public function supportsChildBlocks() {
- return false;
- }
-
- public function extractChildText($text) {
- throw new PhutilMethodNotImplementedException();
- }
-
- protected function renderRemarkupTable(array $out_rows) {
- assert_instances_of($out_rows, 'array');
-
- if ($this->getEngine()->isTextMode()) {
- $lengths = array();
- foreach ($out_rows as $r => $row) {
- foreach ($row['content'] as $c => $cell) {
- $text = $this->getEngine()->restoreText($cell['content']);
- $lengths[$c][$r] = phutil_utf8_strlen($text);
- }
- }
- $max_lengths = array_map('max', $lengths);
-
- $out = array();
- foreach ($out_rows as $r => $row) {
- $headings = false;
- foreach ($row['content'] as $c => $cell) {
- $length = $max_lengths[$c] - $lengths[$c][$r];
- $out[] = '| '.$cell['content'].str_repeat(' ', $length).' ';
- if ($cell['type'] == 'th') {
- $headings = true;
- }
- }
- $out[] = "|\n";
-
- if ($headings) {
- foreach ($row['content'] as $c => $cell) {
- $char = ($cell['type'] == 'th' ? '-' : ' ');
- $out[] = '| '.str_repeat($char, $max_lengths[$c]).' ';
- }
- $out[] = "|\n";
- }
- }
-
- return rtrim(implode('', $out), "\n");
- }
-
- if ($this->getEngine()->isHTMLMailMode()) {
- $table_attributes = array(
- 'style' => 'border-collapse: separate;
- border-spacing: 1px;
- background: #d3d3d3;
- margin: 12px 0;',
- );
- $cell_attributes = array(
- 'style' => 'background: #ffffff;
- padding: 3px 6px;',
- );
- } else {
- $table_attributes = array(
- 'class' => 'remarkup-table',
- );
- $cell_attributes = array();
- }
-
- $out = array();
- $out[] = "\n";
- foreach ($out_rows as $row) {
- $cells = array();
- foreach ($row['content'] as $cell) {
- $cells[] = phutil_tag(
- $cell['type'],
- $cell_attributes,
- $cell['content']);
- }
- $out[] = phutil_tag($row['type'], array(), $cells);
- $out[] = "\n";
- }
-
- $table = phutil_tag('table', $table_attributes, $out);
- return phutil_tag_div('remarkup-table-wrap', $table);
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php
+++ /dev/null
@@ -1,252 +0,0 @@
-<?php
-
-final class PhutilRemarkupCodeBlockRule extends PhutilRemarkupBlockRule {
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $num_lines = 0;
- $match_ticks = null;
- if (preg_match('/^(\s{2,}).+/', $lines[$cursor])) {
- $match_ticks = false;
- } else if (preg_match('/^\s*(```)/', $lines[$cursor])) {
- $match_ticks = true;
- } else {
- return $num_lines;
- }
-
- $num_lines++;
-
- if ($match_ticks &&
- preg_match('/^\s*(```)(.*)(```)\s*$/', $lines[$cursor])) {
- return $num_lines;
- }
-
- $cursor++;
-
- while (isset($lines[$cursor])) {
- if ($match_ticks) {
- if (preg_match('/```\s*$/', $lines[$cursor])) {
- $num_lines++;
- break;
- }
- $num_lines++;
- } else {
- if (strlen(trim($lines[$cursor]))) {
- if (!preg_match('/^\s{2,}/', $lines[$cursor])) {
- break;
- }
- }
- $num_lines++;
- }
- $cursor++;
- }
-
- return $num_lines;
- }
-
- public function markupText($text, $children) {
- if (preg_match('/^\s*```/', $text)) {
- // If this is a ```-style block, trim off the backticks and any leading
- // blank line.
- $text = preg_replace('/^\s*```(\s*\n)?/', '', $text);
- $text = preg_replace('/```\s*$/', '', $text);
- }
-
- $lines = explode("\n", $text);
- while ($lines && !strlen(last($lines))) {
- unset($lines[last_key($lines)]);
- }
-
- $options = array(
- 'counterexample' => false,
- 'lang' => null,
- 'name' => null,
- 'lines' => null,
- );
-
- $parser = new PhutilSimpleOptions();
- $custom = $parser->parse(head($lines));
- if ($custom) {
- $valid = true;
- foreach ($custom as $key => $value) {
- if (!array_key_exists($key, $options)) {
- $valid = false;
- break;
- }
- }
- if ($valid) {
- array_shift($lines);
- $options = $custom + $options;
- }
- }
-
- // Normalize the text back to a 0-level indent.
- $min_indent = 80;
- foreach ($lines as $line) {
- for ($ii = 0; $ii < strlen($line); $ii++) {
- if ($line[$ii] != ' ') {
- $min_indent = min($ii, $min_indent);
- break;
- }
- }
- }
-
- $text = implode("\n", $lines);
- if ($min_indent) {
- $indent_string = str_repeat(' ', $min_indent);
- $text = preg_replace('/^'.$indent_string.'/m', '', $text);
- }
-
- if ($this->getEngine()->isTextMode()) {
- $out = array();
-
- $header = array();
- if ($options['counterexample']) {
- $header[] = 'counterexample';
- }
- if ($options['name'] != '') {
- $header[] = 'name='.$options['name'];
- }
- if ($header) {
- $out[] = implode(', ', $header);
- }
-
- $text = preg_replace('/^/m', ' ', $text);
- $out[] = $text;
-
- return implode("\n", $out);
- }
-
- if (empty($options['lang'])) {
- // If the user hasn't specified "lang=..." explicitly, try to guess the
- // language. If we fail, fall back to configured defaults.
- $lang = PhutilLanguageGuesser::guessLanguage($text);
- if (!$lang) {
- $lang = nonempty(
- $this->getEngine()->getConfig('phutil.codeblock.language-default'),
- 'text');
- }
- $options['lang'] = $lang;
- }
-
- $code_body = $this->highlightSource($text, $options);
-
- $name_header = null;
- $block_style = null;
- if ($this->getEngine()->isHTMLMailMode()) {
- $map = $this->getEngine()->getConfig('phutil.codeblock.style-map');
-
- if ($map) {
- $raw_body = id(new PhutilPygmentizeParser())
- ->setMap($map)
- ->parse((string)$code_body);
- $code_body = phutil_safe_html($raw_body);
- }
-
- $style_rules = array(
- 'padding: 6px 12px;',
- 'font-size: 13px;',
- 'font-weight: bold;',
- 'display: inline-block;',
- 'border-top-left-radius: 3px;',
- 'border-top-right-radius: 3px;',
- 'color: rgba(0,0,0,.75);',
- );
-
- if ($options['counterexample']) {
- $style_rules[] = 'background: #f7e6e6';
- } else {
- $style_rules[] = 'background: rgba(71, 87, 120, 0.08);';
- }
-
- $header_attributes = array(
- 'style' => implode(' ', $style_rules),
- );
-
- $block_style = 'margin: 12px 0;';
- } else {
- $header_attributes = array(
- 'class' => 'remarkup-code-header',
- );
- }
-
- if ($options['name']) {
- $name_header = phutil_tag(
- 'div',
- $header_attributes,
- $options['name']);
- }
-
- $class = 'remarkup-code-block';
- if ($options['counterexample']) {
- $class = 'remarkup-code-block code-block-counterexample';
- }
-
- $attributes = array(
- 'class' => $class,
- 'style' => $block_style,
- 'data-code-lang' => $options['lang'],
- 'data-sigil' => 'remarkup-code-block',
- );
-
- return phutil_tag(
- 'div',
- $attributes,
- array($name_header, $code_body));
- }
-
- private function highlightSource($text, array $options) {
- if ($options['counterexample']) {
- $aux_class = ' remarkup-counterexample';
- } else {
- $aux_class = null;
- }
-
- $aux_style = null;
-
- if ($this->getEngine()->isHTMLMailMode()) {
- $aux_style = array(
- 'font: 11px/15px "Menlo", "Consolas", "Monaco", monospace;',
- 'padding: 12px;',
- 'margin: 0;',
- );
-
- if ($options['counterexample']) {
- $aux_style[] = 'background: #f7e6e6;';
- } else {
- $aux_style[] = 'background: rgba(71, 87, 120, 0.08);';
- }
-
- $aux_style = implode(' ', $aux_style);
- }
-
- if ($options['lines']) {
- // Put a minimum size on this because the scrollbar is otherwise
- // unusable.
- $height = max(6, (int)$options['lines']);
- $aux_style = $aux_style
- .' '
- .'max-height: '
- .(2 * $height)
- .'em; overflow: auto;';
- }
-
- $engine = $this->getEngine()->getConfig('syntax-highlighter.engine');
- if (!$engine) {
- $engine = 'PhutilDefaultSyntaxHighlighterEngine';
- }
- $engine = newv($engine, array());
- $engine->setConfig(
- 'pygments.enabled',
- $this->getEngine()->getConfig('pygments.enabled'));
- return phutil_tag(
- 'pre',
- array(
- 'class' => 'remarkup-code'.$aux_class,
- 'style' => $aux_style,
- ),
- PhutilSafeHTML::applyFunction(
- 'rtrim',
- $engine->highlightSource($options['lang'], $text)));
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-final class PhutilRemarkupDefaultBlockRule extends PhutilRemarkupBlockRule {
-
- public function getPriority() {
- return 750;
- }
-
- public function getMatchingLineCount(array $lines, $cursor) {
- return 1;
- }
-
- public function markupText($text, $children) {
- $engine = $this->getEngine();
-
- $text = trim($text);
- $text = $this->applyRules($text);
-
- if ($engine->isTextMode()) {
- if (!$this->getEngine()->getConfig('preserve-linebreaks')) {
- $text = preg_replace('/ *\n */', ' ', $text);
- }
- return $text;
- }
-
- if ($engine->getConfig('preserve-linebreaks')) {
- $text = phutil_escape_html_newlines($text);
- }
-
- if (!strlen($text)) {
- return null;
- }
-
- $default_attributes = $engine->getConfig('default.p.attributes');
- if ($default_attributes) {
- $attributes = $default_attributes;
- } else {
- $attributes = array();
- }
-
- return phutil_tag('p', $attributes, $text);
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php
+++ /dev/null
@@ -1,162 +0,0 @@
-<?php
-
-final class PhutilRemarkupHeaderBlockRule extends PhutilRemarkupBlockRule {
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $num_lines = 0;
- if (preg_match('/^(={1,5}|#{2,5}|# ).*+$/', $lines[$cursor])) {
- $num_lines = 1;
- } else {
- if (isset($lines[$cursor + 1])) {
- $line = $lines[$cursor].$lines[$cursor + 1];
- if (preg_match('/^([^\n]+)\n[-=]{2,}\s*$/', $line)) {
- $num_lines = 2;
- $cursor++;
- }
- }
- }
-
- if ($num_lines) {
- $cursor++;
- while (isset($lines[$cursor]) && !strlen(trim($lines[$cursor]))) {
- $num_lines++;
- $cursor++;
- }
- }
-
- return $num_lines;
- }
-
- const KEY_HEADER_TOC = 'headers.toc';
-
- public function markupText($text, $children) {
- $text = trim($text);
-
- $lines = phutil_split_lines($text);
- if (count($lines) > 1) {
- $level = ($lines[1][0] == '=') ? 1 : 2;
- $text = trim($lines[0]);
- } else {
- $level = 0;
- for ($ii = 0; $ii < min(5, strlen($text)); $ii++) {
- if ($text[$ii] == '=' || $text[$ii] == '#') {
- ++$level;
- } else {
- break;
- }
- }
- $text = trim($text, ' =#');
- }
-
- $engine = $this->getEngine();
-
- if ($engine->isTextMode()) {
- $char = ($level == 1) ? '=' : '-';
- return $text."\n".str_repeat($char, phutil_utf8_strlen($text));
- }
-
- $use_anchors = $engine->getConfig('header.generate-toc');
-
- $anchor = null;
- if ($use_anchors) {
- $anchor = $this->generateAnchor($level, $text);
- }
-
- $text = phutil_tag(
- 'h'.($level + 1),
- array(
- 'class' => 'remarkup-header',
- ),
- array($anchor, $this->applyRules($text)));
-
- return $text;
- }
-
- private function generateAnchor($level, $text) {
- $anchor = strtolower($text);
- $anchor = preg_replace('/[^a-z0-9]/', '-', $anchor);
- $anchor = preg_replace('/--+/', '-', $anchor);
- $anchor = trim($anchor, '-');
- $anchor = substr($anchor, 0, 24);
- $anchor = trim($anchor, '-');
- $base = $anchor;
-
- $key = self::KEY_HEADER_TOC;
- $engine = $this->getEngine();
- $anchors = $engine->getTextMetadata($key, array());
-
- $suffix = 1;
- while (!strlen($anchor) || isset($anchors[$anchor])) {
- $anchor = $base.'-'.$suffix;
- $anchor = trim($anchor, '-');
- $suffix++;
- }
-
- // When a document contains a link inside a header, like this:
- //
- // = [[ http://wwww.example.com/ | example ]] =
- //
- // ...we want to generate a TOC entry with just "example", but link the
- // header itself. We push the 'toc' state so all the link rules generate
- // just names.
- $engine->pushState('toc');
- $text = $this->applyRules($text);
- $text = $engine->restoreText($text);
-
- $anchors[$anchor] = array($level, $text);
- $engine->popState('toc');
-
- $engine->setTextMetadata($key, $anchors);
-
- return phutil_tag(
- 'a',
- array(
- 'name' => $anchor,
- ),
- '');
- }
-
- public static function renderTableOfContents(PhutilRemarkupEngine $engine) {
-
- $key = self::KEY_HEADER_TOC;
- $anchors = $engine->getTextMetadata($key, array());
-
- if (count($anchors) < 2) {
- // Don't generate a TOC if there are no headers, or if there's only
- // one header (since such a TOC would be silly).
- return null;
- }
-
- $depth = 0;
- $toc = array();
- foreach ($anchors as $anchor => $info) {
- list($level, $name) = $info;
-
- while ($depth < $level) {
- $toc[] = hsprintf('<ul>');
- $depth++;
- }
- while ($depth > $level) {
- $toc[] = hsprintf('</ul>');
- $depth--;
- }
-
- $toc[] = phutil_tag(
- 'li',
- array(),
- phutil_tag(
- 'a',
- array(
- 'href' => '#'.$anchor,
- ),
- $name));
- }
- while ($depth > 0) {
- $toc[] = hsprintf('</ul>');
- $depth--;
- }
-
- return phutil_implode_html("\n", $toc);
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-final class PhutilRemarkupHorizontalRuleBlockRule
- extends PhutilRemarkupBlockRule {
-
- /**
- * This rule executes at priority `300`, so it can preempt the list block
- * rule and claim blocks which begin `---`.
- */
- public function getPriority() {
- return 300;
- }
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $num_lines = 0;
- $pattern = '/^\s*(?:_{3,}|\*\s?\*\s?\*(\s|\*)*|\-\s?\-\s?\-(\s|\-)*)$/';
- if (preg_match($pattern, rtrim($lines[$cursor], "\n\r"))) {
- $num_lines++;
- $cursor++;
- while (isset($lines[$cursor]) && !strlen(trim($lines[$cursor]))) {
- $num_lines++;
- $cursor++;
- }
- }
-
- return $num_lines;
- }
-
- public function markupText($text, $children) {
- if ($this->getEngine()->isTextMode()) {
- return rtrim($text);
- }
-
- return phutil_tag('hr', array('class' => 'remarkup-hr'));
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-final class PhutilRemarkupInlineBlockRule extends PhutilRemarkupBlockRule {
-
- public function getMatchingLineCount(array $lines, $cursor) {
- return 1;
- }
-
- public function markupText($text, $children) {
- return $this->applyRules($text);
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-final class PhutilRemarkupInterpreterBlockRule extends PhutilRemarkupBlockRule {
-
- const START_BLOCK_PATTERN = '/^([\w]+)\s*(?:\(([^)]+)\)\s*)?{{{/';
- const END_BLOCK_PATTERN = '/}}}\s*$/';
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $num_lines = 0;
-
- if (preg_match(self::START_BLOCK_PATTERN, $lines[$cursor])) {
- $num_lines++;
-
- while (isset($lines[$cursor])) {
- if (preg_match(self::END_BLOCK_PATTERN, $lines[$cursor])) {
- break;
- }
- $num_lines++;
- $cursor++;
- }
- }
-
- return $num_lines;
- }
-
- public function markupText($text, $children) {
- $lines = explode("\n", $text);
- $first_key = head_key($lines);
- $last_key = last_key($lines);
- while (trim($lines[$last_key]) === '') {
- unset($lines[$last_key]);
- $last_key = last_key($lines);
- }
- $matches = null;
-
- preg_match(self::START_BLOCK_PATTERN, head($lines), $matches);
-
- $argv = array();
- if (isset($matches[2])) {
- $argv = id(new PhutilSimpleOptions())->parse($matches[2]);
- }
-
- $interpreters = id(new PhutilClassMapQuery())
- ->setAncestorClass('PhutilRemarkupBlockInterpreter')
- ->execute();
-
- foreach ($interpreters as $interpreter) {
- $interpreter->setEngine($this->getEngine());
- }
-
- $lines[$first_key] = preg_replace(
- self::START_BLOCK_PATTERN,
- '',
- $lines[$first_key]);
- $lines[$last_key] = preg_replace(
- self::END_BLOCK_PATTERN,
- '',
- $lines[$last_key]);
-
- if (trim($lines[$first_key]) === '') {
- unset($lines[$first_key]);
- }
- if (trim($lines[$last_key]) === '') {
- unset($lines[$last_key]);
- }
-
- $content = implode("\n", $lines);
-
- $interpreters = mpull($interpreters, null, 'getInterpreterName');
-
- if (isset($interpreters[$matches[1]])) {
- return $interpreters[$matches[1]]->markupContent($content, $argv);
- }
-
- $message = pht('No interpreter found: %s', $matches[1]);
-
- if ($this->getEngine()->isTextMode()) {
- return '('.$message.')';
- }
-
- return phutil_tag(
- 'div',
- array(
- 'class' => 'remarkup-interpreter-error',
- ),
- $message);
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php
+++ /dev/null
@@ -1,567 +0,0 @@
-<?php
-
-final class PhutilRemarkupListBlockRule extends PhutilRemarkupBlockRule {
-
- /**
- * This rule must apply before the Code block rule because it needs to
- * win blocks which begin ` - Lorem ipsum`.
- */
- public function getPriority() {
- return 400;
- }
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $num_lines = 0;
-
- $first_line = $cursor;
- $is_one_line = false;
- while (isset($lines[$cursor])) {
- if (!$num_lines) {
- if (preg_match(self::START_BLOCK_PATTERN, $lines[$cursor])) {
- $num_lines++;
- $cursor++;
- $is_one_line = true;
- continue;
- }
- } else {
- if (preg_match(self::CONT_BLOCK_PATTERN, $lines[$cursor])) {
- $num_lines++;
- $cursor++;
- $is_one_line = false;
- continue;
- }
-
- // Allow lists to continue across multiple paragraphs, as long as lines
- // are indented or a single empty line separates indented lines.
-
- $this_empty = !strlen(trim($lines[$cursor]));
- $this_indented = preg_match('/^ /', $lines[$cursor]);
-
- $next_empty = true;
- $next_indented = false;
- if (isset($lines[$cursor + 1])) {
- $next_empty = !strlen(trim($lines[$cursor + 1]));
- $next_indented = preg_match('/^ /', $lines[$cursor + 1]);
- }
-
- if ($this_empty || $this_indented) {
- if (($this_indented && !$this_empty) ||
- ($next_indented && !$next_empty)) {
- $num_lines++;
- $cursor++;
- continue;
- }
- }
-
- if ($this_empty) {
- $num_lines++;
- }
- }
-
- break;
- }
-
- // If this list only has one item in it, and the list marker is "#", and
- // it's not the last line in the input, parse it as a header instead of a
- // list. This produces better behavior for alternate Markdown headers.
-
- if ($is_one_line) {
- if (($first_line + $num_lines) < count($lines)) {
- if (strncmp($lines[$first_line], '#', 1) === 0) {
- return 0;
- }
- }
- }
-
- return $num_lines;
- }
-
- /**
- * The maximum sub-list depth you can nest to. Avoids silliness and blowing
- * the stack.
- */
- const MAXIMUM_LIST_NESTING_DEPTH = 12;
- const START_BLOCK_PATTERN = '@^\s*(?:[-*#]+|([1-9][0-9]*)[.)]|\[\D?\])\s+@';
- const CONT_BLOCK_PATTERN = '@^\s*(?:[-*#]+|[0-9]+[.)]|\[\D?\])\s+@';
- const STRIP_BLOCK_PATTERN = '@^\s*(?:[-*#]+|[0-9]+[.)])\s*@';
-
- public function markupText($text, $children) {
- $items = array();
- $lines = explode("\n", $text);
-
- // We allow users to delimit lists using either differing indentation
- // levels:
- //
- // - a
- // - b
- //
- // ...or differing numbers of item-delimiter characters:
- //
- // - a
- // -- b
- //
- // If they use the second style but block-indent the whole list, we'll
- // get the depth counts wrong for the first item. To prevent this,
- // un-indent every item by the minimum indentation level for the whole
- // block before we begin parsing.
-
- $regex = self::START_BLOCK_PATTERN;
- $min_space = PHP_INT_MAX;
- foreach ($lines as $ii => $line) {
- $matches = null;
- if (preg_match($regex, $line)) {
- $regex = self::CONT_BLOCK_PATTERN;
- if (preg_match('/^(\s+)/', $line, $matches)) {
- $space = strlen($matches[1]);
- } else {
- $space = 0;
- }
- $min_space = min($min_space, $space);
- }
- }
-
- $regex = self::START_BLOCK_PATTERN;
- if ($min_space) {
- foreach ($lines as $key => $line) {
- if (preg_match($regex, $line)) {
- $regex = self::CONT_BLOCK_PATTERN;
- $lines[$key] = substr($line, $min_space);
- }
- }
- }
-
-
- // The input text may have linewraps in it, like this:
- //
- // - derp derp derp derp
- // derp derp derp derp
- // - blarp blarp blarp blarp
- //
- // Group text lines together into list items, stored in $items. So the
- // result in the above case will be:
- //
- // array(
- // array(
- // "- derp derp derp derp",
- // " derp derp derp derp",
- // ),
- // array(
- // "- blarp blarp blarp blarp",
- // ),
- // );
-
- $item = array();
- $starts_at = null;
- $regex = self::START_BLOCK_PATTERN;
- foreach ($lines as $line) {
- $match = null;
- if (preg_match($regex, $line, $match)) {
- if (!$starts_at && !empty($match[1])) {
- $starts_at = $match[1];
- }
- $regex = self::CONT_BLOCK_PATTERN;
- if ($item) {
- $items[] = $item;
- $item = array();
- }
- }
- $item[] = $line;
- }
- if ($item) {
- $items[] = $item;
- }
- if (!$starts_at) {
- $starts_at = 1;
- }
-
-
- // Process each item to normalize the text, remove line wrapping, and
- // determine its depth (indentation level) and style (ordered vs unordered).
- //
- // We preserve consecutive linebreaks and interpret them as paragraph
- // breaks.
- //
- // Given the above example, the processed array will look like:
- //
- // array(
- // array(
- // 'text' => 'derp derp derp derp derp derp derp derp',
- // 'depth' => 0,
- // 'style' => '-',
- // ),
- // array(
- // 'text' => 'blarp blarp blarp blarp',
- // 'depth' => 0,
- // 'style' => '-',
- // ),
- // );
-
- $has_marks = false;
- foreach ($items as $key => $item) {
- // Trim space around newlines, to strip trailing whitespace and formatting
- // indentation.
- $item = preg_replace('/ *(\n+) */', '\1', implode("\n", $item));
-
- // Replace single newlines with a space. Preserve multiple newlines as
- // paragraph breaks.
- $item = preg_replace('/(?<!\n)\n(?!\n)/', ' ', $item);
-
- $item = rtrim($item);
-
- if (!strlen($item)) {
- unset($items[$key]);
- continue;
- }
-
- $matches = null;
- if (preg_match('/^\s*([-*#]{2,})/', $item, $matches)) {
- // Alternate-style indents; use number of list item symbols.
- $depth = strlen($matches[1]) - 1;
- } else if (preg_match('/^(\s+)/', $item, $matches)) {
- // Markdown-style indents; use indent depth.
- $depth = strlen($matches[1]);
- } else {
- $depth = 0;
- }
-
- if (preg_match('/^\s*(?:#|[0-9])/', $item)) {
- $style = '#';
- } else {
- $style = '-';
- }
-
- // Strip leading indicators off the item.
- $text = preg_replace(self::STRIP_BLOCK_PATTERN, '', $item);
-
- // Look for "[]", "[ ]", "[*]", "[x]", etc., which we render as a
- // checkbox. We don't render [1], [2], etc., as checkboxes, as these
- // are often used as footnotes.
- $mark = null;
- $matches = null;
- if (preg_match('/^\s*\[(\D?)\]\s*/', $text, $matches)) {
- if (strlen(trim($matches[1]))) {
- $mark = true;
- } else {
- $mark = false;
- }
- $has_marks = true;
- $text = substr($text, strlen($matches[0]));
- }
-
- $items[$key] = array(
- 'text' => $text,
- 'depth' => $depth,
- 'style' => $style,
- 'mark' => $mark,
- );
- }
- $items = array_values($items);
-
-
- // Users can create a sub-list by indenting any deeper amount than the
- // previous list, so these are both valid:
- //
- // - a
- // - b
- //
- // - a
- // - b
- //
- // In the former case, we'll have depths (0, 2). In the latter case, depths
- // (0, 4). We don't actually care about how many spaces there are, only
- // how many list indentation levels (that is, we want to map both of
- // those cases to (0, 1), indicating "outermost list" and "first sublist").
- //
- // This is made more complicated because lists at two different indentation
- // levels might be at the same list level:
- //
- // - a
- // - b
- // - c
- // - d
- //
- // Here, 'b' and 'd' are at the same list level (2) but different indent
- // levels (2, 4).
- //
- // Users can also create "staircases" like this:
- //
- // - a
- // - b
- // # c
- //
- // While this is silly, we'd like to render it as faithfully as possible.
- //
- // In order to do this, we convert the list of nodes into a tree,
- // normalizing indentation levels and inserting dummy nodes as necessary to
- // make the tree well-formed. See additional notes at buildTree().
- //
- // In the case above, the result is a tree like this:
- //
- // - <null>
- // - <null>
- // - a
- // - b
- // # c
-
- $l = 0;
- $r = count($items);
- $tree = $this->buildTree($items, $l, $r, $cur_level = 0);
-
-
- // We may need to open a list on a <null> node, but they do not have
- // list style information yet. We need to propagate list style information
- // backward through the tree. In the above example, the tree now looks
- // like this:
- //
- // - <null (style=#)>
- // - <null (style=-)>
- // - a
- // - b
- // # c
-
- $this->adjustTreeStyleInformation($tree);
-
- // Finally, we have enough information to render the tree.
-
- $out = $this->renderTree($tree, 0, $has_marks, $starts_at);
-
- if ($this->getEngine()->isTextMode()) {
- $out = implode('', $out);
- $out = rtrim($out, "\n");
- $out = preg_replace('/ +$/m', '', $out);
- return $out;
- }
-
- return phutil_implode_html('', $out);
- }
-
- /**
- * See additional notes in @{method:markupText}.
- */
- private function buildTree(array $items, $l, $r, $cur_level) {
- if ($l == $r) {
- return array();
- }
-
- if ($cur_level > self::MAXIMUM_LIST_NESTING_DEPTH) {
- // This algorithm is recursive and we don't need you blowing the stack
- // with your oh-so-clever 50,000-item-deep list. Cap indentation levels
- // at a reasonable number and just shove everything deeper up to this
- // level.
- $nodes = array();
- for ($ii = $l; $ii < $r; $ii++) {
- $nodes[] = array(
- 'level' => $cur_level,
- 'items' => array(),
- ) + $items[$ii];
- }
- return $nodes;
- }
-
- $min = $l;
- for ($ii = $r - 1; $ii >= $l; $ii--) {
- if ($items[$ii]['depth'] <= $items[$min]['depth']) {
- $min = $ii;
- }
- }
-
- $min_depth = $items[$min]['depth'];
-
- $nodes = array();
- if ($min != $l) {
- $nodes[] = array(
- 'text' => null,
- 'level' => $cur_level,
- 'style' => null,
- 'mark' => null,
- 'items' => $this->buildTree($items, $l, $min, $cur_level + 1),
- );
- }
-
- $last = $min;
- for ($ii = $last + 1; $ii < $r; $ii++) {
- if ($items[$ii]['depth'] == $min_depth) {
- $nodes[] = array(
- 'level' => $cur_level,
- 'items' => $this->buildTree($items, $last + 1, $ii, $cur_level + 1),
- ) + $items[$last];
- $last = $ii;
- }
- }
- $nodes[] = array(
- 'level' => $cur_level,
- 'items' => $this->buildTree($items, $last + 1, $r, $cur_level + 1),
- ) + $items[$last];
-
- return $nodes;
- }
-
-
- /**
- * See additional notes in @{method:markupText}.
- */
- private function adjustTreeStyleInformation(array &$tree) {
- // The effect here is just to walk backward through the nodes at this level
- // and apply the first style in the list to any empty nodes we inserted
- // before it. As we go, also recurse down the tree.
-
- $style = '-';
- for ($ii = count($tree) - 1; $ii >= 0; $ii--) {
- if ($tree[$ii]['style'] !== null) {
- // This is the earliest node we've seen with style, so set the
- // style to its style.
- $style = $tree[$ii]['style'];
- } else {
- // This node has no style, so apply the current style.
- $tree[$ii]['style'] = $style;
- }
- if ($tree[$ii]['items']) {
- $this->adjustTreeStyleInformation($tree[$ii]['items']);
- }
- }
- }
-
-
- /**
- * See additional notes in @{method:markupText}.
- */
- private function renderTree(
- array $tree,
- $level,
- $has_marks,
- $starts_at = 1) {
-
- $style = idx(head($tree), 'style');
-
- $out = array();
-
- if (!$this->getEngine()->isTextMode()) {
- switch ($style) {
- case '#':
- $tag = 'ol';
- break;
- case '-':
- $tag = 'ul';
- break;
- }
-
- $start_attr = null;
- if (ctype_digit($starts_at) && $starts_at > 1) {
- $start_attr = hsprintf(' start="%d"', $starts_at);
- }
-
- if ($has_marks) {
- $out[] = hsprintf(
- '<%s class="remarkup-list remarkup-list-with-checkmarks"%s>',
- $tag,
- $start_attr);
- } else {
- $out[] = hsprintf(
- '<%s class="remarkup-list"%s>',
- $tag,
- $start_attr);
- }
-
- $out[] = "\n";
- }
-
- $number = $starts_at;
- foreach ($tree as $item) {
- if ($this->getEngine()->isTextMode()) {
- if ($item['text'] === null) {
- // Don't render anything.
- } else {
- $indent = str_repeat(' ', 2 * $level);
- $out[] = $indent;
- if ($item['mark'] !== null) {
- if ($item['mark']) {
- $out[] = '[X] ';
- } else {
- $out[] = '[ ] ';
- }
- } else {
- switch ($style) {
- case '#':
- $out[] = $number.'. ';
- $number++;
- break;
- case '-':
- $out[] = '- ';
- break;
- }
- }
-
- $parts = preg_split('/\n{2,}/', $item['text']);
- foreach ($parts as $key => $part) {
- if ($key != 0) {
- $out[] = "\n\n ".$indent;
- }
- $out[] = $this->applyRules($part);
- }
- $out[] = "\n";
- }
- } else {
- if ($item['text'] === null) {
- $out[] = hsprintf('<li class="remarkup-list-item phantom-item">');
- } else {
- if ($item['mark'] !== null) {
- if ($item['mark'] == true) {
- $out[] = hsprintf(
- '<li class="remarkup-list-item remarkup-checked-item">');
- } else {
- $out[] = hsprintf(
- '<li class="remarkup-list-item remarkup-unchecked-item">');
- }
- $out[] = phutil_tag(
- 'input',
- array(
- 'type' => 'checkbox',
- 'checked' => ($item['mark'] ? 'checked' : null),
- 'disabled' => 'disabled',
- ));
- $out[] = ' ';
- } else {
- $out[] = hsprintf('<li class="remarkup-list-item">');
- }
-
- $parts = preg_split('/\n{2,}/', $item['text']);
- foreach ($parts as $key => $part) {
- if ($key != 0) {
- $out[] = array(
- "\n",
- phutil_tag('br'),
- phutil_tag('br'),
- "\n",
- );
- }
- $out[] = $this->applyRules($part);
- }
- }
- }
-
- if ($item['items']) {
- $subitems = $this->renderTree($item['items'], $level + 1, $has_marks);
- foreach ($subitems as $i) {
- $out[] = $i;
- }
- }
- if (!$this->getEngine()->isTextMode()) {
- $out[] = hsprintf("</li>\n");
- }
- }
-
- if (!$this->getEngine()->isTextMode()) {
- switch ($style) {
- case '#':
- $out[] = hsprintf('</ol>');
- break;
- case '-':
- $out[] = hsprintf('</ul>');
- break;
- }
- }
-
- return $out;
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-final class PhutilRemarkupLiteralBlockRule extends PhutilRemarkupBlockRule {
-
- public function getPriority() {
- return 450;
- }
-
- public function getMatchingLineCount(array $lines, $cursor) {
- // NOTE: We're consuming all continguous blocks of %%% literals, so this:
- //
- // %%%a%%%
- // %%%b%%%
- //
- // ...is equivalent to:
- //
- // %%%a
- // b%%%
- //
- // If they are separated by a blank newline, they are parsed as two
- // different blocks. This more clearly represents the original text in the
- // output text and assists automated escaping of blocks coming into the
- // system.
-
- $num_lines = 0;
- while (preg_match('/^\s*%%%/', $lines[$cursor])) {
- $num_lines++;
-
- // If the line has ONLY "%%%", the block opener doesn't get to double
- // up as a block terminator.
- if (preg_match('/^\s*%%%\s*\z/', $lines[$cursor])) {
- $num_lines++;
- $cursor++;
- }
-
- while (isset($lines[$cursor])) {
- if (!preg_match('/%%%\s*$/', $lines[$cursor])) {
- $num_lines++;
- $cursor++;
- continue;
- }
- break;
- }
-
- $cursor++;
-
- $found_empty = false;
- while (isset($lines[$cursor])) {
- if (!strlen(trim($lines[$cursor]))) {
- $num_lines++;
- $cursor++;
- $found_empty = true;
- continue;
- }
- break;
- }
-
- if ($found_empty) {
- // If there's an empty line after the block, stop merging blocks.
- break;
- }
-
- if (!isset($lines[$cursor])) {
- // If we're at the end of the input, stop looking for more lines.
- break;
- }
- }
-
- return $num_lines;
- }
-
- public function markupText($text, $children) {
- $text = rtrim($text);
- $text = phutil_split_lines($text, $retain_endings = true);
- foreach ($text as $key => $line) {
- $line = preg_replace('/^\s*%%%/', '', $line);
- $line = preg_replace('/%%%(\s*)\z/', '\1', $line);
- $text[$key] = $line;
- }
-
- if ($this->getEngine()->isTextMode()) {
- return implode('', $text);
- }
-
- return phutil_tag(
- 'p',
- array(
- 'class' => 'remarkup-literal',
- ),
- phutil_implode_html(phutil_tag('br', array()), $text));
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php
+++ /dev/null
@@ -1,121 +0,0 @@
-<?php
-
-final class PhutilRemarkupNoteBlockRule extends PhutilRemarkupBlockRule {
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $num_lines = 0;
-
- if (preg_match($this->getRegEx(), $lines[$cursor])) {
- $num_lines++;
- $cursor++;
-
- while (isset($lines[$cursor])) {
- if (trim($lines[$cursor])) {
- $num_lines++;
- $cursor++;
- continue;
- }
- break;
- }
- }
-
- return $num_lines;
- }
-
- public function markupText($text, $children) {
- $matches = array();
- preg_match($this->getRegEx(), $text, $matches);
-
- if (idx($matches, 'showword')) {
- $word = $matches['showword'];
- $show = true;
- } else {
- $word = $matches['hideword'];
- $show = false;
- }
-
- $class_suffix = phutil_utf8_strtolower($word);
-
- // This is the "(IMPORTANT)" or "NOTE:" part.
- $word_part = rtrim(substr($text, 0, strlen($matches[0])));
-
- // This is the actual text.
- $text_part = substr($text, strlen($matches[0]));
- $text_part = $this->applyRules(rtrim($text_part));
-
- $text_mode = $this->getEngine()->isTextMode();
- $html_mail_mode = $this->getEngine()->isHTMLMailMode();
- if ($text_mode) {
- return $word_part.' '.$text_part;
- }
-
- if ($show) {
- $content = array(
- phutil_tag(
- 'span',
- array(
- 'class' => 'remarkup-note-word',
- ),
- $word_part),
- ' ',
- $text_part,
- );
- } else {
- $content = $text_part;
- }
-
- if ($html_mail_mode) {
- if ($class_suffix == 'important') {
- $attributes = array(
- 'style' => 'margin: 16px 0;
- padding: 12px;
- border-left: 3px solid #c0392b;
- background: #f4dddb;',
- );
- } else if ($class_suffix == 'note') {
- $attributes = array(
- 'style' => 'margin: 16px 0;
- padding: 12px;
- border-left: 3px solid #2980b9;
- background: #daeaf3;',
- );
- } else if ($class_suffix == 'warning') {
- $attributes = array(
- 'style' => 'margin: 16px 0;
- padding: 12px;
- border-left: 3px solid #f1c40f;
- background: #fdf5d4;',
- );
- }
- } else {
- $attributes = array(
- 'class' => 'remarkup-'.$class_suffix,
- );
- }
-
- return phutil_tag(
- 'div',
- $attributes,
- $content);
- }
-
- private function getRegEx() {
- $words = array(
- 'NOTE',
- 'IMPORTANT',
- 'WARNING',
- );
-
- foreach ($words as $k => $word) {
- $words[$k] = preg_quote($word, '/');
- }
- $words = implode('|', $words);
-
- return
- '/^(?:'.
- '(?:\((?P<hideword>'.$words.')\))'.
- '|'.
- '(?:(?P<showword>'.$words.'):))\s*'.
- '/';
- }
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupQuotedBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupQuotedBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupQuotedBlockRule.php
+++ /dev/null
@@ -1,108 +0,0 @@
-<?php
-
-abstract class PhutilRemarkupQuotedBlockRule
- extends PhutilRemarkupBlockRule {
-
- final public function supportsChildBlocks() {
- return true;
- }
-
- final protected function normalizeQuotedBody($text) {
- $text = phutil_split_lines($text, true);
- foreach ($text as $key => $line) {
- $text[$key] = substr($line, 1);
- }
-
- // If every line in the block is empty or begins with at least one leading
- // space, strip the initial space off each line. When we quote text, we
- // normally add "> " (with a space) to the beginning of each line, which
- // can disrupt some other rules. If the block appears to have this space
- // in front of each line, remove it.
-
- $strip_space = true;
- foreach ($text as $key => $line) {
- $len = strlen($line);
-
- if (!$len) {
- // We'll still strip spaces if there are some completely empty
- // lines, they may have just had trailing whitespace trimmed.
- continue;
- }
-
- // If this line is part of a nested quote block, just ignore it when
- // realigning this quote block. It's either an author attribution
- // line with ">>!", or we'll deal with it in a subrule when processing
- // the nested quote block.
- if ($line[0] == '>') {
- continue;
- }
-
- if ($line[0] == ' ' || $line[0] == "\n") {
- continue;
- }
-
- // The first character of this line is something other than a space, so
- // we can't strip spaces.
- $strip_space = false;
- break;
- }
-
- if ($strip_space) {
- foreach ($text as $key => $line) {
- $len = strlen($line);
- if (!$len) {
- continue;
- }
-
- if ($line[0] !== ' ') {
- continue;
- }
-
- $text[$key] = substr($line, 1);
- }
- }
-
- // Strip leading empty lines.
- foreach ($text as $key => $line) {
- if (!strlen(trim($line))) {
- unset($text[$key]);
- }
- }
-
- return implode('', $text);
- }
-
- final protected function getQuotedText($text) {
- $text = rtrim($text, "\n");
-
- $no_whitespace = array(
- // For readability, we render nested quotes as ">> quack",
- // not "> > quack".
- '>' => true,
-
- // If the line is empty except for a newline, do not add an
- // unnecessary dangling space.
- "\n" => true,
- );
-
- $text = phutil_split_lines($text, true);
- foreach ($text as $key => $line) {
- $c = null;
- if (isset($line[0])) {
- $c = $line[0];
- } else {
- $c = null;
- }
-
- if (isset($no_whitespace[$c])) {
- $text[$key] = '>'.$line;
- } else {
- $text[$key] = '> '.$line;
- }
- }
- $text = implode('', $text);
-
- return $text;
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-final class PhutilRemarkupQuotesBlockRule
- extends PhutilRemarkupQuotedBlockRule {
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $pos = $cursor;
-
- if (preg_match('/^>/', $lines[$pos])) {
- do {
- ++$pos;
- } while (isset($lines[$pos]) && preg_match('/^>/', $lines[$pos]));
- }
-
- return ($pos - $cursor);
- }
-
- public function extractChildText($text) {
- return array('', $this->normalizeQuotedBody($text));
- }
-
- public function markupText($text, $children) {
- if ($this->getEngine()->isTextMode()) {
- return $this->getQuotedText($children);
- }
-
- $attributes = array();
- if ($this->getEngine()->isHTMLMailMode()) {
- $style = array(
- 'border-left: 3px solid #a7b5bf;',
- 'color: #464c5c;',
- 'font-style: italic;',
- 'margin: 4px 0 12px 0;',
- 'padding: 4px 12px;',
- 'background-color: #f8f9fc;',
- );
-
- $attributes['style'] = implode(' ', $style);
- }
-
- return phutil_tag(
- 'blockquote',
- $attributes,
- $children);
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-
-final class PhutilRemarkupReplyBlockRule
- extends PhutilRemarkupQuotedBlockRule {
-
- public function getPriority() {
- return 400;
- }
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $pos = $cursor;
-
- if (preg_match('/^>>!/', $lines[$pos])) {
- do {
- ++$pos;
- } while (isset($lines[$pos]) && preg_match('/^>/', $lines[$pos]));
- }
-
- return ($pos - $cursor);
- }
-
- public function extractChildText($text) {
- $text = phutil_split_lines($text, true);
-
- $head = substr(reset($text), 3);
-
- $body = array_slice($text, 1);
- $body = implode('', $body);
- $body = $this->normalizeQuotedBody($body);
-
- return array(trim($head), $body);
- }
-
- public function markupText($text, $children) {
- $text = $this->applyRules($text);
-
- if ($this->getEngine()->isTextMode()) {
- $children = $this->getQuotedText($children);
- return $text."\n\n".$children;
- }
-
- if ($this->getEngine()->isHTMLMailMode()) {
- $block_attributes = array(
- 'style' => 'border-left: 3px solid #8C98B8;
- color: #6B748C;
- font-style: italic;
- margin: 4px 0 12px 0;
- padding: 8px 12px;
- background-color: #F8F9FC;',
- );
- $head_attributes = array(
- 'style' => 'font-style: normal;
- padding-bottom: 4px;',
- );
- $reply_attributes = array(
- 'style' => 'margin: 0;
- padding: 0;
- border: 0;
- color: rgb(107, 116, 140);',
- );
- } else {
- $block_attributes = array(
- 'class' => 'remarkup-reply-block',
- );
- $head_attributes = array(
- 'class' => 'remarkup-reply-head',
- );
- $reply_attributes = array(
- 'class' => 'remarkup-reply-body',
- );
- }
-
- return phutil_tag(
- 'blockquote',
- $block_attributes,
- array(
- "\n",
- phutil_tag(
- 'div',
- $head_attributes,
- $text),
- "\n",
- phutil_tag(
- 'div',
- $reply_attributes,
- $children),
- "\n",
- ));
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php
+++ /dev/null
@@ -1,96 +0,0 @@
-<?php
-
-final class PhutilRemarkupSimpleTableBlockRule extends PhutilRemarkupBlockRule {
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $num_lines = 0;
- while (isset($lines[$cursor])) {
- if (preg_match('/^(\s*\|.*+\n?)+$/', $lines[$cursor])) {
- $num_lines++;
- $cursor++;
- } else {
- break;
- }
- }
-
- return $num_lines;
- }
-
- public function markupText($text, $children) {
- $matches = array();
-
- $rows = array();
- foreach (explode("\n", $text) as $line) {
- // Ignore ending delimiters.
- $line = rtrim($line, '|');
-
- // NOTE: The complexity in this regular expression allows us to match
- // a table like "| a | [[ href | b ]] | c |".
-
- preg_match_all(
- '/\|'.
- '('.
- '(?:'.
- '(?:\\[\\[.*?\\]\\])'. // [[ ... | ... ]], a link
- '|'.
- '(?:[^|[]+)'. // Anything but "|" or "[".
- '|'.
- '(?:\\[[^\\|[])'. // "[" followed by anything but "[" or "|"
- ')*'.
- ')/', $line, $matches);
-
- $any_header = false;
- $any_content = false;
-
- $cells = array();
- foreach ($matches[1] as $cell) {
- $cell = trim($cell);
-
- // If this row only has empty cells and "--" cells, and it has at
- // least one "--" cell, it's marking the rows above as <th> cells
- // instead of <td> cells.
-
- // If it has other types of cells, it's always a content row.
-
- // If it has only empty cells, it's an empty row.
-
- if (strlen($cell)) {
- if (preg_match('/^--+\z/', $cell)) {
- $any_header = true;
- } else {
- $any_content = true;
- }
- }
-
- $cells[] = array('type' => 'td', 'content' => $this->applyRules($cell));
- }
-
- $is_header = ($any_header && !$any_content);
-
- if (!$is_header) {
- $rows[] = array('type' => 'tr', 'content' => $cells);
- } else if ($rows) {
- // Mark previous row with headings.
- foreach ($cells as $i => $cell) {
- if ($cell['content']) {
- $last_key = last_key($rows);
- if (!isset($rows[$last_key]['content'][$i])) {
- // If this row has more cells than the previous row, there may
- // not be a cell above this one to turn into a <th />.
- continue;
- }
-
- $rows[$last_key]['content'][$i]['type'] = 'th';
- }
- }
- }
- }
-
- if (!$rows) {
- return $this->applyRules($text);
- }
-
- return $this->renderRemarkupTable($rows);
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php
+++ /dev/null
@@ -1,142 +0,0 @@
-<?php
-
-final class PhutilRemarkupTableBlockRule extends PhutilRemarkupBlockRule {
-
- public function getMatchingLineCount(array $lines, $cursor) {
- $num_lines = 0;
-
- if (preg_match('/^\s*<table>/i', $lines[$cursor])) {
- $num_lines++;
- $cursor++;
-
- while (isset($lines[$cursor])) {
- $num_lines++;
- if (preg_match('@</table>\s*$@i', $lines[$cursor])) {
- break;
- }
- $cursor++;
- }
- }
-
- return $num_lines;
- }
-
- public function markupText($text, $children) {
- $root = id(new PhutilHTMLParser())
- ->parseDocument($text);
-
- $nodes = $root->selectChildrenWithTags(array('table'));
-
- $out = array();
- $seen_table = false;
- foreach ($nodes as $node) {
- if ($node->isContentNode()) {
- $content = $node->getContent();
-
- if (!strlen(trim($content))) {
- // Ignore whitespace.
- continue;
- }
-
- // If we find other content, fail the rule. This can happen if the
- // input is two consecutive table tags on one line with some text
- // in between them, which we currently forbid.
- return $text;
- } else {
- // If we have multiple table tags, just return the raw text.
- if ($seen_table) {
- return $text;
- }
- $seen_table = true;
-
- $out[] = $this->newTable($node);
- }
- }
-
- if ($this->getEngine()->isTextMode()) {
- return implode('', $out);
- } else {
- return phutil_implode_html('', $out);
- }
- }
-
- private function newTable(PhutilDOMNode $table) {
- $nodes = $table->selectChildrenWithTags(
- array(
- 'colgroup',
- 'tr',
- ));
-
- $colgroup = null;
- $rows = array();
-
- foreach ($nodes as $node) {
- if ($node->isContentNode()) {
- $content = $node->getContent();
-
- // If this is whitespace, ignore it.
- if (!strlen(trim($content))) {
- continue;
- }
-
- // If we have nonempty content between the rows, this isn't a valid
- // table. We can't really do anything reasonable with this, so just
- // fail out and render the raw text.
- return $table->newRawString();
- }
-
- if ($node->getTagName() === 'colgroup') {
- // This table has multiple "<colgroup />" tags. Just bail out.
- if ($colgroup !== null) {
- return $table->newRawString();
- }
-
- // This table has a "<colgroup />" after a "<tr />". We could parse
- // this, but just reject it out of an abundance of caution.
- if ($rows) {
- return $table->newRawString();
- }
-
- $colgroup = $node;
- continue;
- }
-
- $rows[] = $node;
- }
-
- $row_specs = array();
-
- foreach ($rows as $row) {
- $cells = $row->selectChildrenWithTags(array('td', 'th'));
-
- $cell_specs = array();
- foreach ($cells as $cell) {
- if ($cell->isContentNode()) {
- $content = $node->getContent();
-
- if (!strlen(trim($content))) {
- continue;
- }
-
- return $table->newRawString();
- }
-
- $content = $cell->newRawContentString();
- $content = $this->applyRules($content);
-
- $cell_specs[] = array(
- 'type' => $cell->getTagName(),
- 'content' => $content,
- );
- }
-
- $row_specs[] = array(
- 'type' => 'tr',
- 'content' => $cell_specs,
- );
- }
-
- return $this->renderRemarkupTable($row_specs);
- }
-
-}
diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-final class PhutilRemarkupTestInterpreterRule
- extends PhutilRemarkupBlockInterpreter {
-
- public function getInterpreterName() {
- return 'phutil_test_block_interpreter';
- }
-
- public function markupContent($content, array $argv) {
- return sprintf(
- "Content: (%s)\nArgv: (%s)",
- $content,
- phutil_build_http_querystring($argv));
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-final class PhutilRemarkupBoldRule extends PhutilRemarkupRule {
-
- public function getPriority() {
- return 1000.0;
- }
-
- public function apply($text) {
- if ($this->getEngine()->isTextMode()) {
- return $text;
- }
-
- return $this->replaceHTML(
- '@\\*\\*(.+?)\\*\\*@s',
- array($this, 'applyCallback'),
- $text);
- }
-
- protected function applyCallback(array $matches) {
- return hsprintf('<strong>%s</strong>', $matches[1]);
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-final class PhutilRemarkupDelRule extends PhutilRemarkupRule {
-
- public function getPriority() {
- return 1000.0;
- }
-
- public function apply($text) {
- if ($this->getEngine()->isTextMode()) {
- return $text;
- }
-
- return $this->replaceHTML(
- '@(?<!~)~~([^\s~].*?~*)~~@s',
- array($this, 'applyCallback'),
- $text);
- }
-
- protected function applyCallback(array $matches) {
- return hsprintf('<del>%s</del>', $matches[1]);
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php
+++ /dev/null
@@ -1,175 +0,0 @@
-<?php
-
-final class PhutilRemarkupDocumentLinkRule extends PhutilRemarkupRule {
-
- public function getPriority() {
- return 150.0;
- }
-
- public function apply($text) {
- // Handle mediawiki-style links: [[ href | name ]]
- $text = preg_replace_callback(
- '@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U',
- array($this, 'markupDocumentLink'),
- $text);
-
- // Handle markdown-style links: [name](href)
- $text = preg_replace_callback(
- '@'.
- '\B'.
- '\\[([^\\]]+)\\]'.
- '\\('.
- '(\s*'.
- // See T12343. This is making some kind of effort to implement
- // parenthesis balancing rules. It won't get nested parentheses
- // right, but should do OK for Wikipedia pages, which seem to be
- // the most important use case.
-
- // Match zero or more non-parenthesis, non-space characters.
- '[^\s()]*'.
- // Match zero or more sequences of "(...)", where two balanced
- // parentheses enclose zero or more normal characters. If we
- // match some, optionally match more stuff at the end.
- '(?:(?:\\([^ ()]*\\))+[^\s()]*)?'.
- '\s*)'.
- '\\)'.
- '\B'.
- '@U',
- array($this, 'markupAlternateLink'),
- $text);
-
- return $text;
- }
-
- protected function renderHyperlink($link, $name) {
- $engine = $this->getEngine();
-
- $is_anchor = false;
- if (strncmp($link, '/', 1) == 0) {
- $base = $engine->getConfig('uri.base');
- $base = rtrim($base, '/');
- $link = $base.$link;
- } else if (strncmp($link, '#', 1) == 0) {
- $here = $engine->getConfig('uri.here');
- $link = $here.$link;
-
- $is_anchor = true;
- }
-
- if ($engine->isTextMode()) {
- // If present, strip off "mailto:" or "tel:".
- $link = preg_replace('/^(?:mailto|tel):/', '', $link);
-
- if (!strlen($name)) {
- return $link;
- }
-
- return $name.' <'.$link.'>';
- }
-
- if (!strlen($name)) {
- $name = $link;
- $name = preg_replace('/^(?:mailto|tel):/', '', $name);
- }
-
- if ($engine->getState('toc')) {
- return $name;
- }
-
- $same_window = $engine->getConfig('uri.same-window', false);
- if ($same_window) {
- $target = null;
- } else {
- $target = '_blank';
- }
-
- // For anchors on the same page, always stay here.
- if ($is_anchor) {
- $target = null;
- }
-
- return phutil_tag(
- 'a',
- array(
- 'href' => $link,
- 'class' => 'remarkup-link',
- 'target' => $target,
- 'rel' => 'noreferrer',
- ),
- $name);
- }
-
- public function markupAlternateLink(array $matches) {
- $uri = trim($matches[2]);
-
- if (!strlen($uri)) {
- return $matches[0];
- }
-
- // NOTE: We apply some special rules to avoid false positives here. The
- // major concern is that we do not want to convert `x[0][1](y)` in a
- // discussion about C source code into a link. To this end, we:
- //
- // - Don't match at word boundaries;
- // - require the URI to contain a "/" character or "@" character; and
- // - reject URIs which being with a quote character.
-
- if ($uri[0] == '"' || $uri[0] == "'" || $uri[0] == '`') {
- return $matches[0];
- }
-
- if (strpos($uri, '/') === false &&
- strpos($uri, '@') === false &&
- strncmp($uri, 'tel:', 4)) {
- return $matches[0];
- }
-
- return $this->markupDocumentLink(
- array(
- $matches[0],
- $matches[2],
- $matches[1],
- ));
- }
-
- public function markupDocumentLink(array $matches) {
- $uri = trim($matches[1]);
- $name = trim(idx($matches, 2));
-
- // If whatever is being linked to begins with "/" or "#", or has "://",
- // or is "mailto:" or "tel:", treat it as a URI instead of a wiki page.
- $is_uri = preg_match('@(^/)|(://)|(^#)|(^(?:mailto|tel):)@', $uri);
-
- if ($is_uri && strncmp('/', $uri, 1) && strncmp('#', $uri, 1)) {
- $protocols = $this->getEngine()->getConfig(
- 'uri.allowed-protocols',
- array());
-
- try {
- $protocol = id(new PhutilURI($uri))->getProtocol();
- if (!idx($protocols, $protocol)) {
- // Don't treat this as a URI if it's not an allowed protocol.
- $is_uri = false;
- }
- } catch (Exception $ex) {
- // We can end up here if we try to parse an ambiguous URI, see
- // T12796.
- $is_uri = false;
- }
- }
-
- // As a special case, skip "[[ / ]]" so that Phriction picks it up as a
- // link to the Phriction root. It is more useful to be able to use this
- // syntax to link to the root document than the home page of the install.
- if ($uri == '/') {
- $is_uri = false;
- }
-
- if (!$is_uri) {
- return $matches[0];
- }
-
- return $this->getEngine()->storeText($this->renderHyperlink($uri, $name));
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-final class PhutilRemarkupEscapeRemarkupRule extends PhutilRemarkupRule {
-
- public function getPriority() {
- return 0;
- }
-
- public function apply($text) {
- if (strpos($text, "\1") === false) {
- return $text;
- }
-
- $replace = $this->getEngine()->storeText("\1");
-
- return str_replace("\1", $replace, $text);
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-final class PhutilRemarkupHighlightRule extends PhutilRemarkupRule {
-
- public function getPriority() {
- return 1000.0;
- }
-
- public function apply($text) {
- if ($this->getEngine()->isTextMode()) {
- return $text;
- }
-
- return $this->replaceHTML(
- '@!!(.+?)(!{2,})@',
- array($this, 'applyCallback'),
- $text);
- }
-
- protected function applyCallback(array $matches) {
- // Remove the two exclamation points that represent syntax.
- $excitement = substr($matches[2], 2);
-
- // If the internal content consists of ONLY exclamation points, leave it
- // untouched so "!!!!!" is five exclamation points instead of one
- // highlighted exclamation point.
- if (preg_match('/^!+\z/', $matches[1])) {
- return $matches[0];
- }
-
- // $excitement now has two fewer !'s than we started with.
- return hsprintf('<span class="remarkup-highlight">%s%s</span>',
- $matches[1], $excitement);
-
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-abstract class PhutilRemarkupHyperlinkEngineExtension
- extends Phobject {
-
- private $engine;
-
- final public function getHyperlinkEngineKey() {
- return $this->getPhobjectClassConstant('LINKENGINEKEY', 32);
- }
-
- final public static function getAllLinkEngines() {
- return id(new PhutilClassMapQuery())
- ->setAncestorClass(__CLASS__)
- ->setUniqueMethod('getHyperlinkEngineKey')
- ->execute();
- }
-
- final public function setEngine(PhutilRemarkupEngine $engine) {
- $this->engine = $engine;
- return $this;
- }
-
- final public function getEngine() {
- return $this->engine;
- }
-
- abstract public function processHyperlinks(array $hyperlinks);
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRef.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRef.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRef.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-final class PhutilRemarkupHyperlinkRef
- extends Phobject {
-
- private $token;
- private $uri;
- private $embed;
- private $result;
-
- public function __construct(array $map) {
- $this->token = $map['token'];
- $this->uri = $map['uri'];
- $this->embed = ($map['mode'] === '{');
- }
-
- public function getToken() {
- return $this->token;
- }
-
- public function getURI() {
- return $this->uri;
- }
-
- public function isEmbed() {
- return $this->embed;
- }
-
- public function setResult($result) {
- $this->result = $result;
- return $this;
- }
-
- public function getResult() {
- return $this->result;
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php
+++ /dev/null
@@ -1,234 +0,0 @@
-<?php
-
-final class PhutilRemarkupHyperlinkRule extends PhutilRemarkupRule {
-
- const KEY_HYPERLINKS = 'hyperlinks';
-
- public function getPriority() {
- return 400.0;
- }
-
- public function apply($text) {
- // Hyperlinks with explicit "<>" around them get linked exactly, without
- // the "<>". Angle brackets are basically special and mean "this is a URL
- // with weird characters". This is assumed to be reasonable because they
- // don't appear in normal text or normal URLs.
- $text = preg_replace_callback(
- '@<(\w{3,}://[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+?)>@',
- array($this, 'markupHyperlinkAngle'),
- $text);
-
- // We match "{uri}", but do not link it by default.
- $text = preg_replace_callback(
- '@{(\w{3,}://[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+?)}@',
- array($this, 'markupHyperlinkCurly'),
- $text);
-
- // Anything else we match "ungreedily", which means we'll look for
- // stuff that's probably puncutation or otherwise not part of the URL and
- // not link it. This lets someone write "QuicK! Go to
- // http://www.example.com/!". We also apply some paren balancing rules.
-
- // NOTE: We're explicitly avoiding capturing stored blocks, so text like
- // `http://www.example.com/[[x | y]]` doesn't get aggressively captured.
- $text = preg_replace_callback(
- '@(\w{3,}://[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+)@',
- array($this, 'markupHyperlinkUngreedy'),
- $text);
-
- return $text;
- }
-
- public function markupHyperlinkAngle(array $matches) {
- return $this->markupHyperlink('<', $matches);
- }
-
- public function markupHyperlinkCurly(array $matches) {
- return $this->markupHyperlink('{', $matches);
- }
-
- protected function markupHyperlink($mode, array $matches) {
- $raw_uri = $matches[1];
-
- try {
- $uri = new PhutilURI($raw_uri);
- } catch (Exception $ex) {
- return $matches[0];
- }
-
- $engine = $this->getEngine();
-
- $token = $engine->storeText($raw_uri);
-
- $list_key = self::KEY_HYPERLINKS;
- $link_list = $engine->getTextMetadata($list_key, array());
-
- $link_list[] = array(
- 'token' => $token,
- 'uri' => $raw_uri,
- 'mode' => $mode,
- );
-
- $engine->setTextMetadata($list_key, $link_list);
-
- return $token;
- }
-
- protected function renderHyperlink($link, $is_embed) {
- // If the URI is "{uri}" and no handler picked it up, we just render it
- // as plain text.
- if ($is_embed) {
- return $this->renderRawLink($link, $is_embed);
- }
-
- $engine = $this->getEngine();
-
- $same_window = $engine->getConfig('uri.same-window', false);
- if ($same_window) {
- $target = null;
- } else {
- $target = '_blank';
- }
-
- return phutil_tag(
- 'a',
- array(
- 'href' => $link,
- 'class' => 'remarkup-link',
- 'target' => $target,
- 'rel' => 'noreferrer',
- ),
- $link);
- }
-
- private function renderRawLink($link, $is_embed) {
- if ($is_embed) {
- return '{'.$link.'}';
- } else {
- return $link;
- }
- }
-
- protected function markupHyperlinkUngreedy($matches) {
- $match = $matches[1];
- $tail = null;
- $trailing = null;
- if (preg_match('/[;,.:!?]+$/', $match, $trailing)) {
- $tail = $trailing[0];
- $match = substr($match, 0, -strlen($tail));
- }
-
- // If there's a closing paren at the end but no balancing open paren in
- // the URL, don't link the close paren. This is an attempt to gracefully
- // handle the two common paren cases, Wikipedia links and English language
- // parentheticals, e.g.:
- //
- // http://en.wikipedia.org/wiki/Noun_(disambiguation)
- // (see also http://www.example.com)
- //
- // We could apply a craftier heuristic here which tries to actually balance
- // the parens, but this is probably sufficient.
- if (preg_match('/\\)$/', $match) && !preg_match('/\\(/', $match)) {
- $tail = ')'.$tail;
- $match = substr($match, 0, -1);
- }
-
- try {
- $uri = new PhutilURI($match);
- } catch (Exception $ex) {
- return $matches[0];
- }
-
- $link = $this->markupHyperlink(null, array(null, $match));
-
- return hsprintf('%s%s', $link, $tail);
- }
-
- public function didMarkupText() {
- $engine = $this->getEngine();
-
- $protocols = $engine->getConfig('uri.allowed-protocols', array());
- $is_toc = $engine->getState('toc');
- $is_text = $engine->isTextMode();
- $is_mail = $engine->isHTMLMailMode();
-
- $list_key = self::KEY_HYPERLINKS;
- $raw_list = $engine->getTextMetadata($list_key, array());
-
- $links = array();
- foreach ($raw_list as $key => $link) {
- $token = $link['token'];
- $raw_uri = $link['uri'];
- $mode = $link['mode'];
-
- $is_embed = ($mode === '{');
- $is_literal = ($mode === '<');
-
- // If we're rendering in a "Table of Contents" or a plain text mode,
- // we're going to render the raw URI without modifications.
- if ($is_toc || $is_text) {
- $result = $this->renderRawLink($raw_uri, $is_embed);
- $engine->overwriteStoredText($token, $result);
- continue;
- }
-
- // If this URI doesn't use a whitelisted protocol, don't link it. This
- // is primarily intended to prevent "javascript://" silliness.
- $uri = new PhutilURI($raw_uri);
- $protocol = $uri->getProtocol();
- $valid_protocol = idx($protocols, $protocol);
- if (!$valid_protocol) {
- $result = $this->renderRawLink($raw_uri, $is_embed);
- $engine->overwriteStoredText($token, $result);
- continue;
- }
-
- // If the URI is written as "<uri>", we'll render it literally even if
- // some handler would otherwise deal with it.
- // If we're rendering for HTML mail, we also render literally.
- if ($is_literal || $is_mail) {
- $result = $this->renderHyperlink($raw_uri, $is_embed);
- $engine->overwriteStoredText($token, $result);
- continue;
- }
-
- // Otherwise, this link is a valid resource which extensions are allowed
- // to handle.
- $links[$key] = $link;
- }
-
- if (!$links) {
- return;
- }
-
- foreach ($links as $key => $link) {
- $links[$key] = new PhutilRemarkupHyperlinkRef($link);
- }
-
- $extensions = PhutilRemarkupHyperlinkEngineExtension::getAllLinkEngines();
- foreach ($extensions as $extension) {
- $extension = id(clone $extension)
- ->setEngine($engine)
- ->processHyperlinks($links);
-
- foreach ($links as $key => $link) {
- $result = $link->getResult();
- if ($result !== null) {
- $engine->overwriteStoredText($link->getToken(), $result);
- unset($links[$key]);
- }
- }
-
- if (!$links) {
- break;
- }
- }
-
- // Render any remaining links in a normal way.
- foreach ($links as $link) {
- $result = $this->renderHyperlink($link->getURI(), $link->isEmbed());
- $engine->overwriteStoredText($link->getToken(), $result);
- }
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-final class PhutilRemarkupItalicRule extends PhutilRemarkupRule {
-
- public function getPriority() {
- return 1000.0;
- }
-
- public function apply($text) {
- if ($this->getEngine()->isTextMode()) {
- return $text;
- }
-
- return $this->replaceHTML(
- '@(?<!:)//(.+?)//@s',
- array($this, 'applyCallback'),
- $text);
- }
-
- protected function applyCallback(array $matches) {
- return hsprintf('<em>%s</em>', $matches[1]);
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-
-final class PhutilRemarkupLinebreaksRule extends PhutilRemarkupRule {
-
- public function apply($text) {
- if ($this->getEngine()->isTextMode()) {
- return $text;
- }
-
- return phutil_escape_html_newlines($text);
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-final class PhutilRemarkupMonospaceRule extends PhutilRemarkupRule {
-
- public function getPriority() {
- return 100.0;
- }
-
- public function apply($text) {
- // NOTE: We don't require a trailing non-boundary on the backtick syntax,
- // to permit the use case of naming and pluralizing a class, like
- // "Load all the `PhutilArray`s and then iterate over them." In theory, the
- // required \B on the leading backtick should protect us from most
- // collateral damage.
-
- return preg_replace_callback(
- '@##([\s\S]+?)##|\B`(.+?)`@',
- array($this, 'markupMonospacedText'),
- $text);
- }
-
- protected function markupMonospacedText(array $matches) {
- if ($this->getEngine()->isTextMode()) {
- $result = $matches[0];
-
- } else
- if ($this->getEngine()->isHTMLMailMode()) {
- $match = isset($matches[2]) ? $matches[2] : $matches[1];
- $result = phutil_tag(
- 'tt',
- array(
- 'style' => 'background: #ebebeb; font-size: 13px;',
- ),
- $match);
-
- } else {
- $match = isset($matches[2]) ? $matches[2] : $matches[1];
- $result = phutil_tag(
- 'tt',
- array(
- 'class' => 'remarkup-monospaced',
- ),
- $match);
- }
-
- return $this->getEngine()->storeText($result);
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRule.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-
-abstract class PhutilRemarkupRule extends Phobject {
-
- private $engine;
- private $replaceCallback;
-
- public function setEngine(PhutilRemarkupEngine $engine) {
- $this->engine = $engine;
- return $this;
- }
-
- public function getEngine() {
- return $this->engine;
- }
-
- public function getPriority() {
- return 500.0;
- }
-
- abstract public function apply($text);
-
- public function getPostprocessKey() {
- return spl_object_hash($this);
- }
-
- public function didMarkupText() {
- return;
- }
-
- protected function replaceHTML($pattern, $callback, $text) {
- $this->replaceCallback = $callback;
- return phutil_safe_html(preg_replace_callback(
- $pattern,
- array($this, 'replaceHTMLCallback'),
- phutil_escape_html($text)));
- }
-
- private function replaceHTMLCallback(array $match) {
- return phutil_escape_html(call_user_func(
- $this->replaceCallback,
- array_map('phutil_safe_html', $match)));
- }
-
-
- /**
- * Safely generate a tag.
- *
- * In Remarkup contexts, it's not safe to use arbitrary text in tag
- * attributes: even though it will be escaped, it may contain replacement
- * tokens which are then replaced with markup.
- *
- * This method acts as @{function:phutil_tag}, but checks attributes before
- * using them.
- *
- * @param string Tag name.
- * @param dict<string, wild> Tag attributes.
- * @param wild Tag content.
- * @return PhutilSafeHTML Tag object.
- */
- protected function newTag($name, array $attrs, $content = null) {
- foreach ($attrs as $key => $attr) {
- if ($attr !== null) {
- $attrs[$key] = $this->assertFlatText($attr);
- }
- }
-
- return phutil_tag($name, $attrs, $content);
- }
-
- /**
- * Assert that a text token is flat (it contains no replacement tokens).
- *
- * Because tokens can be replaced with markup, it is dangerous to use
- * arbitrary input text in tag attributes. Normally, rule precedence should
- * prevent this. Asserting that text is flat before using it as an attribute
- * provides an extra layer of security.
- *
- * Normally, you can call @{method:newTag} rather than calling this method
- * directly. @{method:newTag} will check attributes for you.
- *
- * @param wild Ostensibly flat text.
- * @return string Flat text.
- */
- protected function assertFlatText($text) {
- $text = (string)hsprintf('%s', phutil_safe_html($text));
- $rich = (strpos($text, PhutilRemarkupBlockStorage::MAGIC_BYTE) !== false);
- if ($rich) {
- throw new Exception(
- pht(
- 'Remarkup rule precedence is dangerous: rendering text with tokens '.
- 'as flat text!'));
- }
-
- return $text;
- }
-
- /**
- * Check whether text is flat (contains no replacement tokens) or not.
- *
- * @param wild Ostensibly flat text.
- * @return bool True if the text is flat.
- */
- protected function isFlatText($text) {
- $text = (string)hsprintf('%s', phutil_safe_html($text));
- return (strpos($text, PhutilRemarkupBlockStorage::MAGIC_BYTE) === false);
- }
-
-}
diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php
deleted file mode 100644
--- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-final class PhutilRemarkupUnderlineRule extends PhutilRemarkupRule {
-
- public function getPriority() {
- return 1000.0;
- }
-
- public function apply($text) {
- if ($this->getEngine()->isTextMode()) {
- return $text;
- }
-
- return $this->replaceHTML(
- '@(?<!_|/)__([^\s_/].*?_*)__(?!/|\.\S)@s',
- array($this, 'applyCallback'),
- $text);
- }
-
- protected function applyCallback(array $matches) {
- return hsprintf('<u>%s</u>', $matches[1]);
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php b/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarAbsoluteDateTime.php
+++ /dev/null
@@ -1,287 +0,0 @@
-<?php
-
-final class PhutilCalendarAbsoluteDateTime
- extends PhutilCalendarDateTime {
-
- private $year;
- private $month;
- private $day;
- private $hour = 0;
- private $minute = 0;
- private $second = 0;
- private $timezone;
-
- public static function newFromISO8601($value, $timezone = 'UTC') {
- $pattern =
- '/^'.
- '(?P<y>\d{4})(?P<m>\d{2})(?P<d>\d{2})'.
- '(?:'.
- 'T(?P<h>\d{2})(?P<i>\d{2})(?P<s>\d{2})(?<z>Z)?'.
- ')?'.
- '\z/';
-
- $matches = null;
- $ok = preg_match($pattern, $value, $matches);
- if (!$ok) {
- throw new Exception(
- pht(
- 'Expected ISO8601 datetime in the format "19990105T112233Z", '.
- 'found "%s".',
- $value));
- }
-
- if (isset($matches['z'])) {
- if ($timezone != 'UTC') {
- throw new Exception(
- pht(
- 'ISO8601 date ends in "Z" indicating UTC, but a timezone other '.
- 'than UTC ("%s") was specified.',
- $timezone));
- }
- }
-
- $datetime = id(new self())
- ->setYear((int)$matches['y'])
- ->setMonth((int)$matches['m'])
- ->setDay((int)$matches['d'])
- ->setTimezone($timezone);
-
- if (isset($matches['h'])) {
- $datetime
- ->setHour((int)$matches['h'])
- ->setMinute((int)$matches['i'])
- ->setSecond((int)$matches['s']);
- } else {
- $datetime
- ->setIsAllDay(true);
- }
-
- return $datetime;
- }
-
- public static function newFromEpoch($epoch, $timezone = 'UTC') {
- $date = new DateTime('@'.$epoch);
-
- $zone = new DateTimeZone($timezone);
- $date->setTimezone($zone);
-
- return id(new self())
- ->setYear((int)$date->format('Y'))
- ->setMonth((int)$date->format('m'))
- ->setDay((int)$date->format('d'))
- ->setHour((int)$date->format('H'))
- ->setMinute((int)$date->format('i'))
- ->setSecond((int)$date->format('s'))
- ->setTimezone($timezone);
- }
-
- public static function newFromDictionary(array $dict) {
- static $keys;
- if ($keys === null) {
- $keys = array_fuse(
- array(
- 'kind',
- 'year',
- 'month',
- 'day',
- 'hour',
- 'minute',
- 'second',
- 'timezone',
- 'isAllDay',
- ));
- }
-
- foreach ($dict as $key => $value) {
- if (!isset($keys[$key])) {
- throw new Exception(
- pht(
- 'Unexpected key "%s" in datetime dictionary, expected keys: %s.',
- $key,
- implode(', ', array_keys($keys))));
- }
- }
-
- if (idx($dict, 'kind') !== 'absolute') {
- throw new Exception(
- pht(
- 'Expected key "%s" with value "%s" in datetime dictionary.',
- 'kind',
- 'absolute'));
- }
-
- if (!isset($dict['year'])) {
- throw new Exception(
- pht(
- 'Expected key "%s" in datetime dictionary.',
- 'year'));
- }
-
- $datetime = id(new self())
- ->setYear(idx($dict, 'year'))
- ->setMonth(idx($dict, 'month', 1))
- ->setDay(idx($dict, 'day', 1))
- ->setHour(idx($dict, 'hour', 0))
- ->setMinute(idx($dict, 'minute', 0))
- ->setSecond(idx($dict, 'second', 0))
- ->setTimezone(idx($dict, 'timezone'))
- ->setIsAllDay((bool)idx($dict, 'isAllDay', false));
-
- return $datetime;
- }
-
- public function newRelativeDateTime($duration) {
- if (is_string($duration)) {
- $duration = PhutilCalendarDuration::newFromISO8601($duration);
- }
-
- if (!($duration instanceof PhutilCalendarDuration)) {
- throw new Exception(
- pht(
- 'Expected "PhutilCalendarDuration" object or ISO8601 duration '.
- 'string.'));
- }
-
- return id(new PhutilCalendarRelativeDateTime())
- ->setOrigin($this)
- ->setDuration($duration);
- }
-
- public function toDictionary() {
- return array(
- 'kind' => 'absolute',
- 'year' => (int)$this->getYear(),
- 'month' => (int)$this->getMonth(),
- 'day' => (int)$this->getDay(),
- 'hour' => (int)$this->getHour(),
- 'minute' => (int)$this->getMinute(),
- 'second' => (int)$this->getSecond(),
- 'timezone' => $this->getTimezone(),
- 'isAllDay' => (bool)$this->getIsAllDay(),
- );
- }
-
- public function setYear($year) {
- $this->year = $year;
- return $this;
- }
-
- public function getYear() {
- return $this->year;
- }
-
- public function setMonth($month) {
- $this->month = $month;
- return $this;
- }
-
- public function getMonth() {
- return $this->month;
- }
-
- public function setDay($day) {
- $this->day = $day;
- return $this;
- }
-
- public function getDay() {
- return $this->day;
- }
-
- public function setHour($hour) {
- $this->hour = $hour;
- return $this;
- }
-
- public function getHour() {
- return $this->hour;
- }
-
- public function setMinute($minute) {
- $this->minute = $minute;
- return $this;
- }
-
- public function getMinute() {
- return $this->minute;
- }
-
- public function setSecond($second) {
- $this->second = $second;
- return $this;
- }
-
- public function getSecond() {
- return $this->second;
- }
-
- public function setTimezone($timezone) {
- $this->timezone = $timezone;
- return $this;
- }
-
- public function getTimezone() {
- return $this->timezone;
- }
-
- private function getEffectiveTimezone() {
- $date_timezone = $this->getTimezone();
- $viewer_timezone = $this->getViewerTimezone();
-
- // Because all-day events are always "floating", the effective timezone
- // is the viewer timezone if it is available. Otherwise, we'll return a
- // DateTime object with the correct values, but it will be incorrectly
- // adjusted forward or backward to the viewer's zone later.
-
- $zones = array();
- if ($this->getIsAllDay()) {
- $zones[] = $viewer_timezone;
- $zones[] = $date_timezone;
- } else {
- $zones[] = $date_timezone;
- $zones[] = $viewer_timezone;
- }
- $zones = array_filter($zones);
-
- if (!$zones) {
- throw new Exception(
- pht(
- 'Datetime has no timezone or viewer timezone.'));
- }
-
- return head($zones);
- }
-
- public function newPHPDateTimeZone() {
- $zone = $this->getEffectiveTimezone();
- return new DateTimeZone($zone);
- }
-
- public function newPHPDateTime() {
- $zone = $this->newPHPDateTimeZone();
-
- $y = $this->getYear();
- $m = $this->getMonth();
- $d = $this->getDay();
-
- if ($this->getIsAllDay()) {
- $h = 0;
- $i = 0;
- $s = 0;
- } else {
- $h = $this->getHour();
- $i = $this->getMinute();
- $s = $this->getSecond();
- }
-
- $format = sprintf('%04d-%02d-%02d %02d:%02d:%02d', $y, $m, $d, $h, $i, $s);
-
- return new DateTime($format, $zone);
- }
-
-
- public function newAbsoluteDateTime() {
- return clone $this;
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarContainerNode.php b/src/parser/calendar/data/PhutilCalendarContainerNode.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarContainerNode.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-abstract class PhutilCalendarContainerNode
- extends PhutilCalendarNode {
-
- private $children = array();
-
- final public function getChildren() {
- return $this->children;
- }
-
- final public function getChildrenOfType($type) {
- $result = array();
-
- foreach ($this->getChildren() as $key => $child) {
- if ($child->getNodeType() != $type) {
- continue;
- }
- $result[$key] = $child;
- }
-
- return $result;
- }
-
- final public function appendChild(PhutilCalendarNode $node) {
- $this->children[] = $node;
- return $this;
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarDateTime.php b/src/parser/calendar/data/PhutilCalendarDateTime.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarDateTime.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-abstract class PhutilCalendarDateTime
- extends Phobject {
-
- private $viewerTimezone;
- private $isAllDay = false;
-
- public function setViewerTimezone($viewer_timezone) {
- $this->viewerTimezone = $viewer_timezone;
- return $this;
- }
-
- public function getViewerTimezone() {
- return $this->viewerTimezone;
- }
-
- public function setIsAllDay($is_all_day) {
- $this->isAllDay = $is_all_day;
- return $this;
- }
-
- public function getIsAllDay() {
- return $this->isAllDay;
- }
-
- public function getEpoch() {
- $datetime = $this->newPHPDateTime();
- return (int)$datetime->format('U');
- }
-
- public function getISO8601() {
- $datetime = $this->newPHPDateTime();
-
- if ($this->getIsAllDay()) {
- return $datetime->format('Ymd');
- } else if ($this->getTimezone()) {
- // With a timezone, the event occurs at a specific second universally.
- // We return the UTC representation of that point in time.
- $datetime->setTimezone(new DateTimeZone('UTC'));
- return $datetime->format('Ymd\\THis\\Z');
- } else {
- // With no timezone, events are "floating" and occur at local time.
- // We return a representation without the "Z".
- return $datetime->format('Ymd\\THis');
- }
- }
-
- abstract public function newPHPDateTimeZone();
- abstract public function newPHPDateTime();
- abstract public function newAbsoluteDateTime();
-
- abstract public function getTimezone();
-}
diff --git a/src/parser/calendar/data/PhutilCalendarDocumentNode.php b/src/parser/calendar/data/PhutilCalendarDocumentNode.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarDocumentNode.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-final class PhutilCalendarDocumentNode
- extends PhutilCalendarContainerNode {
-
- const NODETYPE = 'document';
-
- public function getEvents() {
- return $this->getChildrenOfType(PhutilCalendarEventNode::NODETYPE);
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarDuration.php b/src/parser/calendar/data/PhutilCalendarDuration.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarDuration.php
+++ /dev/null
@@ -1,181 +0,0 @@
-<?php
-
-final class PhutilCalendarDuration extends Phobject {
-
- private $isNegative = false;
- private $weeks = 0;
- private $days = 0;
- private $hours = 0;
- private $minutes = 0;
- private $seconds = 0;
-
- public static function newFromDictionary(array $dict) {
- static $keys;
- if ($keys === null) {
- $keys = array_fuse(
- array(
- 'isNegative',
- 'weeks',
- 'days',
- 'hours',
- 'minutes',
- 'seconds',
- ));
- }
-
- foreach ($dict as $key => $value) {
- if (!isset($keys[$key])) {
- throw new Exception(
- pht(
- 'Unexpected key "%s" in duration dictionary, expected keys: %s.',
- $key,
- implode(', ', array_keys($keys))));
- }
- }
-
- $duration = id(new self())
- ->setIsNegative(idx($dict, 'isNegative', false))
- ->setWeeks(idx($dict, 'weeks', 0))
- ->setDays(idx($dict, 'days', 0))
- ->setHours(idx($dict, 'hours', 0))
- ->setMinutes(idx($dict, 'minutes', 0))
- ->setSeconds(idx($dict, 'seconds', 0));
-
- return $duration;
- }
-
- public function toDictionary() {
- return array(
- 'isNegative' => $this->getIsNegative(),
- 'weeks' => $this->getWeeks(),
- 'days' => $this->getDays(),
- 'hours' => $this->getHours(),
- 'minutes' => $this->getMinutes(),
- 'seconds' => $this->getSeconds(),
- );
- }
-
- public static function newFromISO8601($value) {
- $pattern =
- '/^'.
- '(?P<sign>[+-])?'.
- 'P'.
- '(?:'.
- '(?P<W>\d+)W'.
- '|'.
- '(?:(?:(?P<D>\d+)D)?'.
- '(?:T(?:(?P<H>\d+)H)?(?:(?P<M>\d+)M)?(?:(?P<S>\d+)S)?)?'.
- ')'.
- ')'.
- '\z/';
-
- $matches = null;
- $ok = preg_match($pattern, $value, $matches);
- if (!$ok) {
- throw new Exception(
- pht(
- 'Expected ISO8601 duration in the format "P12DT3H4M5S", found '.
- '"%s".',
- $value));
- }
-
- $is_negative = (idx($matches, 'sign') == '-');
-
- return id(new self())
- ->setIsNegative($is_negative)
- ->setWeeks((int)idx($matches, 'W', 0))
- ->setDays((int)idx($matches, 'D', 0))
- ->setHours((int)idx($matches, 'H', 0))
- ->setMinutes((int)idx($matches, 'M', 0))
- ->setSeconds((int)idx($matches, 'S', 0));
- }
-
- public function toISO8601() {
- $parts = array();
- $parts[] = 'P';
-
- $weeks = $this->getWeeks();
- if ($weeks) {
- $parts[] = $weeks.'W';
- } else {
- $days = $this->getDays();
- if ($days) {
- $parts[] = $days.'D';
- }
-
- $parts[] = 'T';
-
- $hours = $this->getHours();
- if ($hours) {
- $parts[] = $hours.'H';
- }
-
- $minutes = $this->getMinutes();
- if ($minutes) {
- $parts[] = $minutes.'M';
- }
-
- $seconds = $this->getSeconds();
- if ($seconds) {
- $parts[] = $seconds.'S';
- }
- }
-
- return implode('', $parts);
- }
-
- public function setIsNegative($is_negative) {
- $this->isNegative = $is_negative;
- return $this;
- }
-
- public function getIsNegative() {
- return $this->isNegative;
- }
-
- public function setWeeks($weeks) {
- $this->weeks = $weeks;
- return $this;
- }
-
- public function getWeeks() {
- return $this->weeks;
- }
-
- public function setDays($days) {
- $this->days = $days;
- return $this;
- }
-
- public function getDays() {
- return $this->days;
- }
-
- public function setHours($hours) {
- $this->hours = $hours;
- return $this;
- }
-
- public function getHours() {
- return $this->hours;
- }
-
- public function setMinutes($minutes) {
- $this->minutes = $minutes;
- return $this;
- }
-
- public function getMinutes() {
- return $this->minutes;
- }
-
- public function setSeconds($seconds) {
- $this->seconds = $seconds;
- return $this;
- }
-
- public function getSeconds() {
- return $this->seconds;
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarEventNode.php b/src/parser/calendar/data/PhutilCalendarEventNode.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarEventNode.php
+++ /dev/null
@@ -1,172 +0,0 @@
-<?php
-
-final class PhutilCalendarEventNode
- extends PhutilCalendarContainerNode {
-
- const NODETYPE = 'event';
-
- private $uid;
- private $name;
- private $description;
- private $startDateTime;
- private $endDateTime;
- private $duration;
- private $createdDateTime;
- private $modifiedDateTime;
- private $organizer;
- private $attendees = array();
- private $recurrenceRule;
- private $recurrenceExceptions = array();
- private $recurrenceDates = array();
- private $recurrenceID;
-
- public function setUID($uid) {
- $this->uid = $uid;
- return $this;
- }
-
- public function getUID() {
- return $this->uid;
- }
-
- public function setName($name) {
- $this->name = $name;
- return $this;
- }
-
- public function getName() {
- return $this->name;
- }
-
- public function setDescription($description) {
- $this->description = $description;
- return $this;
- }
-
- public function getDescription() {
- return $this->description;
- }
-
- public function setStartDateTime(PhutilCalendarDateTime $start) {
- $this->startDateTime = $start;
- return $this;
- }
-
- public function getStartDateTime() {
- return $this->startDateTime;
- }
-
- public function setEndDateTime(PhutilCalendarDateTime $end) {
- $this->endDateTime = $end;
- return $this;
- }
-
- public function getEndDateTime() {
- $end = $this->endDateTime;
- if ($end) {
- return $end;
- }
-
- $start = $this->getStartDateTime();
- $duration = $this->getDuration();
- if ($start && $duration) {
- return id(new PhutilCalendarRelativeDateTime())
- ->setOrigin($start)
- ->setDuration($duration);
- }
-
- // If no end date or duration are specified, the event is instantaneous.
- return $start;
- }
-
- public function setDuration(PhutilCalendarDuration $duration) {
- $this->duration = $duration;
- return $this;
- }
-
- public function getDuration() {
- return $this->duration;
- }
-
- public function setCreatedDateTime(PhutilCalendarDateTime $created) {
- $this->createdDateTime = $created;
- return $this;
- }
-
- public function getCreatedDateTime() {
- return $this->createdDateTime;
- }
-
- public function setModifiedDateTime(PhutilCalendarDateTime $modified) {
- $this->modifiedDateTime = $modified;
- return $this;
- }
-
- public function getModifiedDateTime() {
- return $this->modifiedDateTime;
- }
-
- public function setOrganizer(PhutilCalendarUserNode $organizer) {
- $this->organizer = $organizer;
- return $this;
- }
-
- public function getOrganizer() {
- return $this->organizer;
- }
-
- public function setAttendees(array $attendees) {
- assert_instances_of($attendees, 'PhutilCalendarUserNode');
- $this->attendees = $attendees;
- return $this;
- }
-
- public function getAttendees() {
- return $this->attendees;
- }
-
- public function addAttendee(PhutilCalendarUserNode $attendee) {
- $this->attendees[] = $attendee;
- return $this;
- }
-
- public function setRecurrenceRule(
- PhutilCalendarRecurrenceRule $recurrence_rule) {
- $this->recurrenceRule = $recurrence_rule;
- return $this;
- }
-
- public function getRecurrenceRule() {
- return $this->recurrenceRule;
- }
-
- public function setRecurrenceExceptions(array $recurrence_exceptions) {
- assert_instances_of($recurrence_exceptions, 'PhutilCalendarDateTime');
- $this->recurrenceExceptions = $recurrence_exceptions;
- return $this;
- }
-
- public function getRecurrenceExceptions() {
- return $this->recurrenceExceptions;
- }
-
- public function setRecurrenceDates(array $recurrence_dates) {
- assert_instances_of($recurrence_dates, 'PhutilCalendarDateTime');
- $this->recurrenceDates = $recurrence_dates;
- return $this;
- }
-
- public function getRecurrenceDates() {
- return $this->recurrenceDates;
- }
-
- public function setRecurrenceID($recurrence_id) {
- $this->recurrenceID = $recurrence_id;
- return $this;
- }
-
- public function getRecurrenceID() {
- return $this->recurrenceID;
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarNode.php b/src/parser/calendar/data/PhutilCalendarNode.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarNode.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-abstract class PhutilCalendarNode extends Phobject {
-
- private $attributes = array();
-
- final public function getNodeType() {
- return $this->getPhobjectClassConstant('NODETYPE');
- }
-
- final public function setAttribute($key, $value) {
- $this->attributes[$key] = $value;
- return $this;
- }
-
- final public function getAttribute($key, $default = null) {
- return idx($this->attributes, $key, $default);
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarProxyDateTime.php b/src/parser/calendar/data/PhutilCalendarProxyDateTime.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarProxyDateTime.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-abstract class PhutilCalendarProxyDateTime
- extends PhutilCalendarDateTime {
-
- private $proxy;
-
- final protected function setProxy(PhutilCalendarDateTime $proxy) {
- $this->proxy = $proxy;
- return $this;
- }
-
- final protected function getProxy() {
- return $this->proxy;
- }
-
- public function __clone() {
- $this->proxy = clone $this->proxy;
- }
-
- public function setViewerTimezone($timezone) {
- $this->getProxy()->setViewerTimezone($timezone);
- return $this;
- }
-
- public function getViewerTimezone() {
- return $this->getProxy()->getViewerTimezone();
- }
-
- public function setIsAllDay($is_all_day) {
- $this->getProxy()->setIsAllDay($is_all_day);
- return $this;
- }
-
- public function getIsAllDay() {
- return $this->getProxy()->getIsAllDay();
- }
-
- public function newPHPDateTimezone() {
- return $this->getProxy()->newPHPDateTimezone();
- }
-
- public function newPHPDateTime() {
- return $this->getProxy()->newPHPDateTime();
- }
-
- public function getTimezone() {
- return $this->getProxy()->getTimezone();
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarRawNode.php b/src/parser/calendar/data/PhutilCalendarRawNode.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarRawNode.php
+++ /dev/null
@@ -1,8 +0,0 @@
-<?php
-
-final class PhutilCalendarRawNode
- extends PhutilCalendarContainerNode {
-
- const NODETYPE = 'raw';
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceList.php b/src/parser/calendar/data/PhutilCalendarRecurrenceList.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarRecurrenceList.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-final class PhutilCalendarRecurrenceList
- extends PhutilCalendarRecurrenceSource {
-
- private $dates = array();
- private $order;
-
- public function setDates(array $dates) {
- assert_instances_of($dates, 'PhutilCalendarDateTime');
- $this->dates = $dates;
- return $this;
- }
-
- public function getDates() {
- return $this->dates;
- }
-
- public function resetSource() {
- foreach ($this->getDates() as $date) {
- $date->setViewerTimezone($this->getViewerTimezone());
- }
-
- $order = msort($this->getDates(), 'getEpoch');
- $order = array_reverse($order);
- $this->order = $order;
-
- return $this;
- }
-
- public function getNextEvent($cursor) {
- while ($this->order) {
- $next = array_pop($this->order);
- if ($next->getEpoch() >= $cursor) {
- return $next;
- }
- }
-
- return null;
- }
-
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php b/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarRecurrenceRule.php
+++ /dev/null
@@ -1,1820 +0,0 @@
-<?php
-
-final class PhutilCalendarRecurrenceRule
- extends PhutilCalendarRecurrenceSource {
-
- private $startDateTime;
- private $frequency;
- private $frequencyScale;
- private $interval = 1;
- private $bySecond = array();
- private $byMinute = array();
- private $byHour = array();
- private $byDay = array();
- private $byMonthDay = array();
- private $byYearDay = array();
- private $byWeekNumber = array();
- private $byMonth = array();
- private $bySetPosition = array();
- private $weekStart = self::WEEKDAY_MONDAY;
- private $count;
- private $until;
-
- private $cursorSecond;
- private $cursorMinute;
- private $cursorHour;
- private $cursorHourState;
- private $cursorWeek;
- private $cursorWeekday;
- private $cursorWeekState;
- private $cursorDay;
- private $cursorDayState;
- private $cursorMonth;
- private $cursorYear;
-
- private $setSeconds;
- private $setMinutes;
- private $setHours;
- private $setDays;
- private $setMonths;
- private $setWeeks;
- private $setYears;
-
- private $stateSecond;
- private $stateMinute;
- private $stateHour;
- private $stateDay;
- private $stateWeek;
- private $stateMonth;
- private $stateYear;
-
- private $baseYear;
- private $isAllDay;
- private $activeSet = array();
- private $nextSet = array();
- private $minimumEpoch;
-
- const FREQUENCY_SECONDLY = 'SECONDLY';
- const FREQUENCY_MINUTELY = 'MINUTELY';
- const FREQUENCY_HOURLY = 'HOURLY';
- const FREQUENCY_DAILY = 'DAILY';
- const FREQUENCY_WEEKLY = 'WEEKLY';
- const FREQUENCY_MONTHLY = 'MONTHLY';
- const FREQUENCY_YEARLY = 'YEARLY';
-
- const SCALE_SECONDLY = 1;
- const SCALE_MINUTELY = 2;
- const SCALE_HOURLY = 3;
- const SCALE_DAILY = 4;
- const SCALE_WEEKLY = 5;
- const SCALE_MONTHLY = 6;
- const SCALE_YEARLY = 7;
-
- const WEEKDAY_SUNDAY = 'SU';
- const WEEKDAY_MONDAY = 'MO';
- const WEEKDAY_TUESDAY = 'TU';
- const WEEKDAY_WEDNESDAY = 'WE';
- const WEEKDAY_THURSDAY = 'TH';
- const WEEKDAY_FRIDAY = 'FR';
- const WEEKDAY_SATURDAY = 'SA';
-
- const WEEKINDEX_SUNDAY = 0;
- const WEEKINDEX_MONDAY = 1;
- const WEEKINDEX_TUESDAY = 2;
- const WEEKINDEX_WEDNESDAY = 3;
- const WEEKINDEX_THURSDAY = 4;
- const WEEKINDEX_FRIDAY = 5;
- const WEEKINDEX_SATURDAY = 6;
-
- public function toDictionary() {
- $parts = array();
-
- $parts['FREQ'] = $this->getFrequency();
-
- $interval = $this->getInterval();
- if ($interval != 1) {
- $parts['INTERVAL'] = $interval;
- }
-
- $by_second = $this->getBySecond();
- if ($by_second) {
- $parts['BYSECOND'] = $by_second;
- }
-
- $by_minute = $this->getByMinute();
- if ($by_minute) {
- $parts['BYMINUTE'] = $by_minute;
- }
-
- $by_hour = $this->getByHour();
- if ($by_hour) {
- $parts['BYHOUR'] = $by_hour;
- }
-
- $by_day = $this->getByDay();
- if ($by_day) {
- $parts['BYDAY'] = $by_day;
- }
-
- $by_month = $this->getByMonth();
- if ($by_month) {
- $parts['BYMONTH'] = $by_month;
- }
-
- $by_monthday = $this->getByMonthDay();
- if ($by_monthday) {
- $parts['BYMONTHDAY'] = $by_monthday;
- }
-
- $by_yearday = $this->getByYearDay();
- if ($by_yearday) {
- $parts['BYYEARDAY'] = $by_yearday;
- }
-
- $by_weekno = $this->getByWeekNumber();
- if ($by_weekno) {
- $parts['BYWEEKNO'] = $by_weekno;
- }
-
- $by_setpos = $this->getBySetPosition();
- if ($by_setpos) {
- $parts['BYSETPOS'] = $by_setpos;
- }
-
- $wkst = $this->getWeekStart();
- if ($wkst != self::WEEKDAY_MONDAY) {
- $parts['WKST'] = $wkst;
- }
-
- $count = $this->getCount();
- if ($count) {
- $parts['COUNT'] = $count;
- }
-
- $until = $this->getUntil();
- if ($until) {
- $parts['UNTIL'] = $until->getISO8601();
- }
-
- return $parts;
- }
-
- public static function newFromDictionary(array $dict) {
- static $expect;
- if ($expect === null) {
- $expect = array_fuse(
- array(
- 'FREQ',
- 'INTERVAL',
- 'BYSECOND',
- 'BYMINUTE',
- 'BYHOUR',
- 'BYDAY',
- 'BYMONTH',
- 'BYMONTHDAY',
- 'BYYEARDAY',
- 'BYWEEKNO',
- 'BYSETPOS',
- 'WKST',
- 'UNTIL',
- 'COUNT',
- ));
- }
-
- foreach ($dict as $key => $value) {
- if (empty($expect[$key])) {
- throw new Exception(
- pht(
- 'RRULE dictionary includes unknown key "%s". Expected keys '.
- 'are: %s.',
- $key,
- implode(', ', array_keys($expect))));
- }
- }
-
- $rrule = id(new self())
- ->setFrequency(idx($dict, 'FREQ'))
- ->setInterval(idx($dict, 'INTERVAL', 1))
- ->setBySecond(idx($dict, 'BYSECOND', array()))
- ->setByMinute(idx($dict, 'BYMINUTE', array()))
- ->setByHour(idx($dict, 'BYHOUR', array()))
- ->setByDay(idx($dict, 'BYDAY', array()))
- ->setByMonth(idx($dict, 'BYMONTH', array()))
- ->setByMonthDay(idx($dict, 'BYMONTHDAY', array()))
- ->setByYearDay(idx($dict, 'BYYEARDAY', array()))
- ->setByWeekNumber(idx($dict, 'BYWEEKNO', array()))
- ->setBySetPosition(idx($dict, 'BYSETPOS', array()))
- ->setWeekStart(idx($dict, 'WKST', self::WEEKDAY_MONDAY));
-
- $count = idx($dict, 'COUNT');
- if ($count) {
- $rrule->setCount($count);
- }
-
- $until = idx($dict, 'UNTIL');
- if ($until) {
- $until = PhutilCalendarAbsoluteDateTime::newFromISO8601($until);
- $rrule->setUntil($until);
- }
-
- return $rrule;
- }
-
- public function toRRULE() {
- $dict = $this->toDictionary();
-
- $parts = array();
- foreach ($dict as $key => $value) {
- if (is_array($value)) {
- $value = implode(',', $value);
- }
- $parts[] = "{$key}={$value}";
- }
-
- return implode(';', $parts);
- }
-
- public static function newFromRRULE($rrule) {
- $parts = explode(';', $rrule);
-
- $dict = array();
- foreach ($parts as $part) {
- list($key, $value) = explode('=', $part, 2);
- switch ($key) {
- case 'FREQ':
- case 'INTERVAL':
- case 'WKST':
- case 'COUNT':
- case 'UNTIL';
- break;
- default:
- $value = explode(',', $value);
- break;
- }
- $dict[$key] = $value;
- }
-
- $int_lists = array_fuse(
- array(
- // NOTE: "BYDAY" is absent, and takes a list like "MO, TU, WE".
- 'BYSECOND',
- 'BYMINUTE',
- 'BYHOUR',
- 'BYMONTH',
- 'BYMONTHDAY',
- 'BYYEARDAY',
- 'BYWEEKNO',
- 'BYSETPOS',
- ));
-
- $int_values = array_fuse(
- array(
- 'COUNT',
- 'INTERVAL',
- ));
-
- foreach ($dict as $key => $value) {
- if (isset($int_values[$key])) {
- // None of these values may be negative.
- if (!preg_match('/^\d+\z/', $value)) {
- throw new Exception(
- pht(
- 'Unexpected value "%s" in "%s" RULE property: expected an '.
- 'integer.',
- $value,
- $key));
- }
- $dict[$key] = (int)$value;
- }
-
- if (isset($int_lists[$key])) {
- foreach ($value as $k => $v) {
- if (!preg_match('/^-?\d+\z/', $v)) {
- throw new Exception(
- pht(
- 'Unexpected value "%s" in "%s" RRULE property: expected '.
- 'only integers.',
- $v,
- $key));
- }
- $value[$k] = (int)$v;
- }
- $dict[$key] = $value;
- }
- }
-
- return self::newFromDictionary($dict);
- }
-
- private static function getAllWeekdayConstants() {
- return array_keys(self::getWeekdayIndexMap());
- }
-
- private static function getWeekdayIndexMap() {
- static $map = array(
- self::WEEKDAY_SUNDAY => self::WEEKINDEX_SUNDAY,
- self::WEEKDAY_MONDAY => self::WEEKINDEX_MONDAY,
- self::WEEKDAY_TUESDAY => self::WEEKINDEX_TUESDAY,
- self::WEEKDAY_WEDNESDAY => self::WEEKINDEX_WEDNESDAY,
- self::WEEKDAY_THURSDAY => self::WEEKINDEX_THURSDAY,
- self::WEEKDAY_FRIDAY => self::WEEKINDEX_FRIDAY,
- self::WEEKDAY_SATURDAY => self::WEEKINDEX_SATURDAY,
- );
-
- return $map;
- }
-
- private static function getWeekdayIndex($weekday) {
- $map = self::getWeekdayIndexMap();
- if (!isset($map[$weekday])) {
- $constants = array_keys($map);
- throw new Exception(
- pht(
- 'Weekday "%s" is not a valid weekday constant. Valid constants '.
- 'are: %s.',
- $weekday,
- implode(', ', $constants)));
- }
-
- return $map[$weekday];
- }
-
- public function setStartDateTime(PhutilCalendarDateTime $start) {
- $this->startDateTime = $start;
- return $this;
- }
-
- public function getStartDateTime() {
- return $this->startDateTime;
- }
-
- public function setCount($count) {
- if ($count < 1) {
- throw new Exception(
- pht(
- 'RRULE COUNT value "%s" is invalid: count must be at least 1.',
- $count));
- }
-
- $this->count = $count;
- return $this;
- }
-
- public function getCount() {
- return $this->count;
- }
-
- public function setUntil(PhutilCalendarDateTime $until) {
- $this->until = $until;
- return $this;
- }
-
- public function getUntil() {
- return $this->until;
- }
-
- public function setFrequency($frequency) {
- static $map = array(
- self::FREQUENCY_SECONDLY => self::SCALE_SECONDLY,
- self::FREQUENCY_MINUTELY => self::SCALE_MINUTELY,
- self::FREQUENCY_HOURLY => self::SCALE_HOURLY,
- self::FREQUENCY_DAILY => self::SCALE_DAILY,
- self::FREQUENCY_WEEKLY => self::SCALE_WEEKLY,
- self::FREQUENCY_MONTHLY => self::SCALE_MONTHLY,
- self::FREQUENCY_YEARLY => self::SCALE_YEARLY,
- );
-
- if (empty($map[$frequency])) {
- throw new Exception(
- pht(
- 'RRULE FREQ "%s" is invalid. Valid frequencies are: %s.',
- $frequency,
- implode(', ', array_keys($map))));
- }
-
- $this->frequency = $frequency;
- $this->frequencyScale = $map[$frequency];
-
- return $this;
- }
-
- public function getFrequency() {
- return $this->frequency;
- }
-
- public function getFrequencyScale() {
- return $this->frequencyScale;
- }
-
- public function setInterval($interval) {
- if (!is_int($interval)) {
- throw new Exception(
- pht(
- 'RRULE INTERVAL "%s" is invalid: interval must be an integer.',
- $interval));
- }
-
- if ($interval < 1) {
- throw new Exception(
- pht(
- 'RRULE INTERVAL "%s" is invalid: interval must be 1 or more.',
- $interval));
- }
-
- $this->interval = $interval;
- return $this;
- }
-
- public function getInterval() {
- return $this->interval;
- }
-
- public function setBySecond(array $by_second) {
- $this->assertByRange('BYSECOND', $by_second, 0, 60);
- $this->bySecond = array_fuse($by_second);
- return $this;
- }
-
- public function getBySecond() {
- return $this->bySecond;
- }
-
- public function setByMinute(array $by_minute) {
- $this->assertByRange('BYMINUTE', $by_minute, 0, 59);
- $this->byMinute = array_fuse($by_minute);
- return $this;
- }
-
- public function getByMinute() {
- return $this->byMinute;
- }
-
- public function setByHour(array $by_hour) {
- $this->assertByRange('BYHOUR', $by_hour, 0, 23);
- $this->byHour = array_fuse($by_hour);
- return $this;
- }
-
- public function getByHour() {
- return $this->byHour;
- }
-
- public function setByDay(array $by_day) {
- $constants = self::getAllWeekdayConstants();
- $constants = implode('|', $constants);
-
- $pattern = '/^(?:[+-]?([1-9]\d?))?('.$constants.')\z/';
- foreach ($by_day as $key => $value) {
- $matches = null;
- if (!preg_match($pattern, $value, $matches)) {
- throw new Exception(
- pht(
- 'RRULE BYDAY value "%s" is invalid: rule part must be in the '.
- 'expected form (like "MO", "-3TH", or "+2SU").',
- $value));
- }
-
- // The maximum allowed value is 53, which corresponds to "the 53rd
- // Monday every year" or similar when evaluated against a YEARLY rule.
-
- $maximum = 53;
- $magnitude = (int)$matches[1];
- if ($magnitude > $maximum) {
- throw new Exception(
- pht(
- 'RRULE BYDAY value "%s" has an offset with magnitude "%s", but '.
- 'the maximum permitted value is "%s".',
- $value,
- $magnitude,
- $maximum));
- }
-
- // Normalize "+3FR" into "3FR".
- $by_day[$key] = ltrim($value, '+');
- }
-
- $this->byDay = array_fuse($by_day);
- return $this;
- }
-
- public function getByDay() {
- return $this->byDay;
- }
-
- public function setByMonthDay(array $by_month_day) {
- $this->assertByRange('BYMONTHDAY', $by_month_day, -31, 31, false);
- $this->byMonthDay = array_fuse($by_month_day);
- return $this;
- }
-
- public function getByMonthDay() {
- return $this->byMonthDay;
- }
-
- public function setByYearDay($by_year_day) {
- $this->assertByRange('BYYEARDAY', $by_year_day, -366, 366, false);
- $this->byYearDay = array_fuse($by_year_day);
- return $this;
- }
-
- public function getByYearDay() {
- return $this->byYearDay;
- }
-
- public function setByMonth(array $by_month) {
- $this->assertByRange('BYMONTH', $by_month, 1, 12);
- $this->byMonth = array_fuse($by_month);
- return $this;
- }
-
- public function getByMonth() {
- return $this->byMonth;
- }
-
- public function setByWeekNumber(array $by_week_number) {
- $this->assertByRange('BYWEEKNO', $by_week_number, -53, 53, false);
- $this->byWeekNumber = array_fuse($by_week_number);
- return $this;
- }
-
- public function getByWeekNumber() {
- return $this->byWeekNumber;
- }
-
- public function setBySetPosition(array $by_set_position) {
- $this->assertByRange('BYSETPOS', $by_set_position, -366, 366, false);
- $this->bySetPosition = $by_set_position;
- return $this;
- }
-
- public function getBySetPosition() {
- return $this->bySetPosition;
- }
-
- public function setWeekStart($week_start) {
- // Make sure this is a valid weekday constant.
- self::getWeekdayIndex($week_start);
-
- $this->weekStart = $week_start;
- return $this;
- }
-
- public function getWeekStart() {
- return $this->weekStart;
- }
-
- public function resetSource() {
- $frequency = $this->getFrequency();
-
- if ($this->getByMonthDay()) {
- switch ($frequency) {
- case self::FREQUENCY_WEEKLY:
- // RFC5545: "The BYMONTHDAY rule part MUST NOT be specified when the
- // FREQ rule part is set to WEEKLY."
- throw new Exception(
- pht(
- 'RRULE specifies BYMONTHDAY with FREQ set to WEEKLY, which '.
- 'violates RFC5545.'));
- break;
- default:
- break;
- }
-
- }
-
- if ($this->getByYearDay()) {
- switch ($frequency) {
- case self::FREQUENCY_DAILY:
- case self::FREQUENCY_WEEKLY:
- case self::FREQUENCY_MONTHLY:
- // RFC5545: "The BYYEARDAY rule part MUST NOT be specified when the
- // FREQ rule part is set to DAILY, WEEKLY, or MONTHLY."
- throw new Exception(
- pht(
- 'RRULE specifies BYYEARDAY with FREQ of DAILY, WEEKLY or '.
- 'MONTHLY, which violates RFC5545.'));
- default:
- break;
- }
- }
-
- // TODO
- // RFC5545: "The BYDAY rule part MUST NOT be specified with a numeric
- // value when the FREQ rule part is not set to MONTHLY or YEARLY."
- // RFC5545: "Furthermore, the BYDAY rule part MUST NOT be specified with a
- // numeric value with the FREQ rule part set to YEARLY when the BYWEEKNO
- // rule part is specified."
-
-
- $date = $this->getStartDateTime();
-
- $this->cursorSecond = $date->getSecond();
- $this->cursorMinute = $date->getMinute();
- $this->cursorHour = $date->getHour();
-
- $this->cursorDay = $date->getDay();
- $this->cursorMonth = $date->getMonth();
- $this->cursorYear = $date->getYear();
-
- $year_map = $this->getYearMap($this->cursorYear, $this->getWeekStart());
- $key = $this->cursorMonth.'M'.$this->cursorDay.'D';
- $this->cursorWeek = $year_map['info'][$key]['week'];
- $this->cursorWeekday = $year_map['info'][$key]['weekday'];
-
- $this->setSeconds = array();
- $this->setMinutes = array();
- $this->setHours = array();
- $this->setDays = array();
- $this->setMonths = array();
- $this->setYears = array();
-
- $this->stateSecond = null;
- $this->stateMinute = null;
- $this->stateHour = null;
- $this->stateDay = null;
- $this->stateWeek = null;
- $this->stateMonth = null;
- $this->stateYear = null;
-
- // If we have a BYSETPOS, we need to generate the entire set before we
- // can filter it and return results. Normally, we start generating at
- // the start date, but we need to go back one interval to generate
- // BYSETPOS events so we can make sure the entire set is generated.
- if ($this->getBySetPosition()) {
- $interval = $this->getInterval();
- switch ($frequency) {
- case self::FREQUENCY_YEARLY:
- $this->cursorYear -= $interval;
- break;
- case self::FREQUENCY_MONTHLY:
- $this->cursorMonth -= $interval;
- $this->rewindMonth();
- break;
- case self::FREQUENCY_WEEKLY:
- $this->cursorWeek -= $interval;
- $this->rewindWeek();
- break;
- case self::FREQUENCY_DAILY:
- $this->cursorDay -= $interval;
- $this->rewindDay();
- break;
- case self::FREQUENCY_HOURLY:
- $this->cursorHour -= $interval;
- $this->rewindHour();
- break;
- case self::FREQUENCY_MINUTELY:
- $this->cursorMinute -= $interval;
- $this->rewindMinute();
- break;
- case self::FREQUENCY_SECONDLY:
- default:
- throw new Exception(
- pht(
- 'RRULE specifies BYSETPOS with FREQ "%s", but this is invalid.',
- $frequency));
- }
- }
-
- // We can generate events from before the cursor when evaluating rules
- // with BYSETPOS or FREQ=WEEKLY.
- $this->minimumEpoch = $this->getStartDateTime()->getEpoch();
-
- $cursor_state = array(
- 'year' => $this->cursorYear,
- 'month' => $this->cursorMonth,
- 'week' => $this->cursorWeek,
- 'day' => $this->cursorDay,
- 'hour' => $this->cursorHour,
- );
-
- $this->cursorDayState = $cursor_state;
- $this->cursorWeekState = $cursor_state;
- $this->cursorHourState = $cursor_state;
-
- $by_hour = $this->getByHour();
- $by_minute = $this->getByMinute();
- $by_second = $this->getBySecond();
-
- $scale = $this->getFrequencyScale();
-
- // We return all-day events if the start date is an all-day event and we
- // don't have more granular selectors or a more granular frequency.
- $this->isAllDay = $date->getIsAllDay()
- && !$by_hour
- && !$by_minute
- && !$by_second
- && ($scale > self::SCALE_HOURLY);
- }
-
- public function getNextEvent($cursor) {
- while (true) {
- $event = $this->generateNextEvent();
- if (!$event) {
- break;
- }
-
- $epoch = $event->getEpoch();
- if ($this->minimumEpoch) {
- if ($epoch < $this->minimumEpoch) {
- continue;
- }
- }
-
- if ($epoch < $cursor) {
- continue;
- }
-
- break;
- }
-
- return $event;
- }
-
- private function generateNextEvent() {
- if ($this->activeSet) {
- return array_pop($this->activeSet);
- }
-
- $this->baseYear = $this->cursorYear;
-
- $by_setpos = $this->getBySetPosition();
- if ($by_setpos) {
- $old_state = $this->getSetPositionState();
- }
-
- while (!$this->activeSet) {
- $this->activeSet = $this->nextSet;
- $this->nextSet = array();
-
- while (true) {
- if ($this->isAllDay) {
- $this->nextDay();
- } else {
- $this->nextSecond();
- }
-
- $result = id(new PhutilCalendarAbsoluteDateTime())
- ->setTimezone($this->getStartDateTime()->getTimezone())
- ->setViewerTimezone($this->getViewerTimezone())
- ->setYear($this->stateYear)
- ->setMonth($this->stateMonth)
- ->setDay($this->stateDay);
-
- if ($this->isAllDay) {
- $result->setIsAllDay(true);
- } else {
- $result
- ->setHour($this->stateHour)
- ->setMinute($this->stateMinute)
- ->setSecond($this->stateSecond);
- }
-
- // If we don't have BYSETPOS, we're all done. We put this into the
- // set and will immediately return it.
- if (!$by_setpos) {
- $this->activeSet[] = $result;
- break;
- }
-
- // Otherwise, check if we've completed a set. The set is complete if
- // the state has moved past the span we were examining (for example,
- // with a YEARLY event, if the state is now in the next year).
- $new_state = $this->getSetPositionState();
- if ($new_state == $old_state) {
- $this->activeSet[] = $result;
- continue;
- }
-
- $this->activeSet = $this->applySetPos($this->activeSet, $by_setpos);
- $this->activeSet = array_reverse($this->activeSet);
- $this->nextSet[] = $result;
- $old_state = $new_state;
- break;
- }
- }
-
- return array_pop($this->activeSet);
- }
-
-
- protected function nextSecond() {
- if ($this->setSeconds) {
- $this->stateSecond = array_pop($this->setSeconds);
- return;
- }
-
- $frequency = $this->getFrequency();
- $interval = $this->getInterval();
- $is_secondly = ($frequency == self::FREQUENCY_SECONDLY);
- $by_second = $this->getBySecond();
-
- while (!$this->setSeconds) {
- $this->nextMinute();
-
- if ($is_secondly || $by_second) {
- $seconds = $this->newSecondsSet(
- ($is_secondly ? $interval : 1),
- $by_second);
- } else {
- $seconds = array(
- $this->cursorSecond,
- );
- }
-
- $this->setSeconds = array_reverse($seconds);
- }
-
- $this->stateSecond = array_pop($this->setSeconds);
- }
-
- protected function nextMinute() {
- if ($this->setMinutes) {
- $this->stateMinute = array_pop($this->setMinutes);
- return;
- }
-
- $frequency = $this->getFrequency();
- $interval = $this->getInterval();
- $scale = $this->getFrequencyScale();
- $is_minutely = ($frequency === self::FREQUENCY_MINUTELY);
- $by_minute = $this->getByMinute();
-
- while (!$this->setMinutes) {
- $this->nextHour();
-
- if ($is_minutely || $by_minute) {
- $minutes = $this->newMinutesSet(
- ($is_minutely ? $interval : 1),
- $by_minute);
- } else if ($scale < self::SCALE_MINUTELY) {
- $minutes = $this->newMinutesSet(
- 1,
- array());
- } else {
- $minutes = array(
- $this->cursorMinute,
- );
- }
-
- $this->setMinutes = array_reverse($minutes);
- }
-
- $this->stateMinute = array_pop($this->setMinutes);
- }
-
- protected function nextHour() {
- if ($this->setHours) {
- $this->stateHour = array_pop($this->setHours);
- return;
- }
-
- $frequency = $this->getFrequency();
- $interval = $this->getInterval();
- $scale = $this->getFrequencyScale();
- $is_hourly = ($frequency === self::FREQUENCY_HOURLY);
- $by_hour = $this->getByHour();
-
- while (!$this->setHours) {
- $this->nextDay();
-
- $is_dynamic = $is_hourly
- || $by_hour
- || ($scale < self::SCALE_HOURLY);
-
- if ($is_dynamic) {
- $hours = $this->newHoursSet(
- ($is_hourly ? $interval : 1),
- $by_hour);
- } else {
- $hours = array(
- $this->cursorHour,
- );
- }
-
- $this->setHours = array_reverse($hours);
- }
-
- $this->stateHour = array_pop($this->setHours);
- }
-
- protected function nextDay() {
- if ($this->setDays) {
- $info = array_pop($this->setDays);
- $this->setDayState($info);
- return;
- }
-
- $frequency = $this->getFrequency();
- $interval = $this->getInterval();
- $scale = $this->getFrequencyScale();
- $is_daily = ($frequency === self::FREQUENCY_DAILY);
- $is_weekly = ($frequency === self::FREQUENCY_WEEKLY);
-
- $by_day = $this->getByDay();
- $by_monthday = $this->getByMonthDay();
- $by_yearday = $this->getByYearDay();
- $by_weekno = $this->getByWeekNumber();
- $by_month = $this->getByMonth();
- $week_start = $this->getWeekStart();
-
- while (!$this->setDays) {
- if ($is_weekly) {
- $this->nextWeek();
- } else {
- $this->nextMonth();
- }
-
- // NOTE: We normally handle BYMONTH when iterating months, but it acts
- // like a filter if FREQ=WEEKLY.
-
- $is_dynamic = $is_daily
- || $is_weekly
- || $by_day
- || $by_monthday
- || $by_yearday
- || $by_weekno
- || ($by_month && $is_weekly)
- || ($scale < self::SCALE_DAILY);
-
- if ($is_dynamic) {
- $weeks = $this->newDaysSet(
- ($is_daily ? $interval : 1),
- $by_day,
- $by_monthday,
- $by_yearday,
- $by_weekno,
- $by_month,
- $week_start);
- } else {
- // The cursor day may not actually exist in the current month, so
- // make sure the day is valid before we generate a set which contains
- // it.
- $year_map = $this->getYearMap($this->stateYear, $week_start);
- if ($this->cursorDay > $year_map['monthDays'][$this->stateMonth]) {
- $weeks = array(
- array(),
- );
- } else {
- $key = $this->stateMonth.'M'.$this->cursorDay.'D';
- $weeks = array(
- array($year_map['info'][$key]),
- );
- }
- }
-
- // Unpack the weeks into days.
- $days = array_mergev($weeks);
-
- $this->setDays = array_reverse($days);
- }
-
- $info = array_pop($this->setDays);
- $this->setDayState($info);
- }
-
- private function setDayState(array $info) {
- $this->stateDay = $info['monthday'];
- $this->stateWeek = $info['week'];
- $this->stateMonth = $info['month'];
- }
-
- protected function nextMonth() {
- if ($this->setMonths) {
- $this->stateMonth = array_pop($this->setMonths);
- return;
- }
-
- $frequency = $this->getFrequency();
- $interval = $this->getInterval();
- $scale = $this->getFrequencyScale();
- $is_monthly = ($frequency === self::FREQUENCY_MONTHLY);
-
- $by_month = $this->getByMonth();
-
- // If we have a BYMONTHDAY, we consider that set of days in every month.
- // For example, "FREQ=YEARLY;BYMONTHDAY=3" means "the third day of every
- // month", so we need to expand the month set if the constraint is present.
- $by_monthday = $this->getByMonthDay();
-
- // Likewise, we need to generate all months if we have BYYEARDAY or
- // BYWEEKNO or BYDAY.
- $by_yearday = $this->getByYearDay();
- $by_weekno = $this->getByWeekNumber();
- $by_day = $this->getByDay();
-
- while (!$this->setMonths) {
- $this->nextYear();
-
- $is_dynamic = $is_monthly
- || $by_month
- || $by_monthday
- || $by_yearday
- || $by_weekno
- || $by_day
- || ($scale < self::SCALE_MONTHLY);
-
- if ($is_dynamic) {
- $months = $this->newMonthsSet(
- ($is_monthly ? $interval : 1),
- $by_month);
- } else {
- $months = array(
- $this->cursorMonth,
- );
- }
-
- $this->setMonths = array_reverse($months);
- }
-
- $this->stateMonth = array_pop($this->setMonths);
- }
-
- protected function nextWeek() {
- if ($this->setWeeks) {
- $this->stateWeek = array_pop($this->setWeeks);
- return;
- }
-
- $frequency = $this->getFrequency();
- $interval = $this->getInterval();
- $scale = $this->getFrequencyScale();
- $by_weekno = $this->getByWeekNumber();
-
- while (!$this->setWeeks) {
- $this->nextYear();
-
- $weeks = $this->newWeeksSet(
- $interval,
- $by_weekno);
-
- $this->setWeeks = array_reverse($weeks);
- }
-
- $this->stateWeek = array_pop($this->setWeeks);
- }
-
- protected function nextYear() {
- $this->stateYear = $this->cursorYear;
-
- $frequency = $this->getFrequency();
- $is_yearly = ($frequency === self::FREQUENCY_YEARLY);
-
- if ($is_yearly) {
- $interval = $this->getInterval();
- } else {
- $interval = 1;
- }
-
- $this->cursorYear = $this->cursorYear + $interval;
-
- if ($this->cursorYear > ($this->baseYear + 100)) {
- throw new Exception(
- pht(
- 'RRULE evaluation failed to generate more events in the next 100 '.
- 'years. This RRULE is likely invalid or degenerate.'));
- }
-
- }
-
- private function newSecondsSet($interval, $set) {
- // TODO: This doesn't account for leap seconds. In theory, it probably
- // should, although this shouldn't impact any real events.
- $seconds_in_minute = 60;
-
- if ($this->cursorSecond >= $seconds_in_minute) {
- $this->cursorSecond -= $seconds_in_minute;
- return array();
- }
-
- list($cursor, $result) = $this->newIteratorSet(
- $this->cursorSecond,
- $interval,
- $set,
- $seconds_in_minute);
-
- $this->cursorSecond = ($cursor - $seconds_in_minute);
-
- return $result;
- }
-
- private function newMinutesSet($interval, $set) {
- // NOTE: This value is legitimately a constant! Amazing!
- $minutes_in_hour = 60;
-
- if ($this->cursorMinute >= $minutes_in_hour) {
- $this->cursorMinute -= $minutes_in_hour;
- return array();
- }
-
- list($cursor, $result) = $this->newIteratorSet(
- $this->cursorMinute,
- $interval,
- $set,
- $minutes_in_hour);
-
- $this->cursorMinute = ($cursor - $minutes_in_hour);
-
- return $result;
- }
-
- private function newHoursSet($interval, $set) {
- // TODO: This doesn't account for hours caused by daylight savings time.
- // It probably should, although this seems unlikely to impact any real
- // events.
- $hours_in_day = 24;
-
- // If the hour cursor is behind the current time, we need to forward it in
- // INTERVAL increments so we end up with the right offset.
- list($skip, $this->cursorHourState) = $this->advanceCursorState(
- $this->cursorHourState,
- self::SCALE_HOURLY,
- $interval,
- $this->getWeekStart());
-
- if ($skip) {
- return array();
- }
-
- list($cursor, $result) = $this->newIteratorSet(
- $this->cursorHour,
- $interval,
- $set,
- $hours_in_day);
-
- $this->cursorHour = ($cursor - $hours_in_day);
-
- return $result;
- }
-
- private function newWeeksSet($interval, $set) {
- $week_start = $this->getWeekStart();
-
- list($skip, $this->cursorWeekState) = $this->advanceCursorState(
- $this->cursorWeekState,
- self::SCALE_WEEKLY,
- $interval,
- $week_start);
-
- if ($skip) {
- return array();
- }
-
- $year_map = $this->getYearMap($this->stateYear, $week_start);
-
- $result = array();
- while (true) {
- if (!isset($year_map['weekMap'][$this->cursorWeek])) {
- break;
- }
- $result[] = $this->cursorWeek;
- $this->cursorWeek += $interval;
- }
-
- $this->cursorWeek -= $year_map['weekCount'];
-
- return $result;
- }
-
- private function newDaysSet(
- $interval_day,
- $by_day,
- $by_monthday,
- $by_yearday,
- $by_weekno,
- $by_month,
- $week_start) {
-
- $frequency = $this->getFrequency();
- $is_yearly = ($frequency == self::FREQUENCY_YEARLY);
- $is_monthly = ($frequency == self::FREQUENCY_MONTHLY);
- $is_weekly = ($frequency == self::FREQUENCY_WEEKLY);
-
- $selection = array();
- if ($is_weekly) {
- $year_map = $this->getYearMap($this->stateYear, $week_start);
-
- if (isset($year_map['weekMap'][$this->stateWeek])) {
- foreach ($year_map['weekMap'][$this->stateWeek] as $key) {
- $selection[] = $year_map['info'][$key];
- }
- }
- } else {
- // If the day cursor is behind the current year and month, we need to
- // forward it in INTERVAL increments so we end up with the right offset
- // in the current month.
- list($skip, $this->cursorDayState) = $this->advanceCursorState(
- $this->cursorDayState,
- self::SCALE_DAILY,
- $interval_day,
- $week_start);
-
- if (!$skip) {
- $year_map = $this->getYearMap($this->stateYear, $week_start);
- while (true) {
- $month_idx = $this->stateMonth;
- $month_days = $year_map['monthDays'][$month_idx];
- if ($this->cursorDay > $month_days) {
- // NOTE: The year map is now out of date, but we're about to break
- // out of the loop anyway so it doesn't matter.
- break;
- }
-
- $day_idx = $this->cursorDay;
-
- $key = "{$month_idx}M{$day_idx}D";
- $selection[] = $year_map['info'][$key];
-
- $this->cursorDay += $interval_day;
- }
- }
- }
-
- // As a special case, BYDAY applies to relative month offsets if BYMONTH
- // is present in a YEARLY rule.
- if ($is_yearly) {
- if ($this->getByMonth()) {
- $is_yearly = false;
- $is_monthly = true;
- }
- }
-
- // As a special case, BYDAY makes us examine all week days. This doesn't
- // check BYMONTHDAY or BYYEARDAY because they are not valid with WEEKLY.
- $filter_weekday = true;
- if ($is_weekly) {
- if ($by_day) {
- $filter_weekday = false;
- }
- }
-
- $weeks = array();
- foreach ($selection as $key => $info) {
- if ($is_weekly) {
- if ($filter_weekday) {
- if ($info['weekday'] != $this->cursorWeekday) {
- continue;
- }
- }
- } else {
- if ($info['month'] != $this->stateMonth) {
- continue;
- }
- }
-
- if ($by_day) {
- if (empty($by_day[$info['weekday']])) {
- if ($is_yearly) {
- if (empty($by_day[$info['weekday.yearly']]) &&
- empty($by_day[$info['-weekday.yearly']])) {
- continue;
- }
- } else if ($is_monthly) {
- if (empty($by_day[$info['weekday.monthly']]) &&
- empty($by_day[$info['-weekday.monthly']])) {
- continue;
- }
- } else {
- continue;
- }
- }
- }
-
- if ($by_monthday) {
- if (empty($by_monthday[$info['monthday']]) &&
- empty($by_monthday[$info['-monthday']])) {
- continue;
- }
- }
-
- if ($by_yearday) {
- if (empty($by_yearday[$info['yearday']]) &&
- empty($by_yearday[$info['-yearday']])) {
- continue;
- }
- }
-
- if ($by_weekno) {
- if (empty($by_weekno[$info['week']]) &&
- empty($by_weekno[$info['-week']])) {
- continue;
- }
- }
-
- if ($by_month) {
- if (empty($by_month[$info['month']])) {
- continue;
- }
- }
-
- $weeks[$info['week']][] = $info;
- }
-
- return array_values($weeks);
- }
-
- private function newMonthsSet($interval, $set) {
- // NOTE: This value is also a real constant! Wow!
- $months_in_year = 12;
-
- if ($this->cursorMonth > $months_in_year) {
- $this->cursorMonth -= $months_in_year;
- return array();
- }
-
- list($cursor, $result) = $this->newIteratorSet(
- $this->cursorMonth,
- $interval,
- $set,
- $months_in_year + 1);
-
- $this->cursorMonth = ($cursor - $months_in_year);
-
- return $result;
- }
-
- public static function getYearMap($year, $week_start) {
- static $maps = array();
-
- $key = "{$year}/{$week_start}";
- if (isset($maps[$key])) {
- return $maps[$key];
- }
-
- $map = self::newYearMap($year, $week_start);
- $maps[$key] = $map;
-
- return $maps[$key];
- }
-
- private static function newYearMap($year, $weekday_start) {
- $weekday_index = self::getWeekdayIndex($weekday_start);
-
- $is_leap = (($year % 4 === 0) && ($year % 100 !== 0)) ||
- ($year % 400 === 0);
-
- // There may be some clever way to figure out which day of the week a given
- // year starts on and avoid the cost of a DateTime construction, but I
- // wasn't able to turn it up and we only need to do this once per year.
- $datetime = new DateTime("{$year}-01-01", new DateTimeZone('UTC'));
- $weekday = (int)$datetime->format('w');
-
- if ($is_leap) {
- $max_day = 366;
- } else {
- $max_day = 365;
- }
-
- $month_days = array(
- 1 => 31,
- 2 => $is_leap ? 29 : 28,
- 3 => 31,
- 4 => 30,
- 5 => 31,
- 6 => 30,
- 7 => 31,
- 8 => 31,
- 9 => 30,
- 10 => 31,
- 11 => 30,
- 12 => 31,
- );
-
- // Per the spec, the first week of the year must contain at least four
- // days. If the week starts on a Monday but the year starts on a Saturday,
- // the first couple of days don't count as a week. In this case, the first
- // week will begin on January 3.
- $first_week_size = 0;
- $first_weekday = $weekday;
- for ($year_day = 1; $year_day <= $max_day; $year_day++) {
- $first_weekday = ($first_weekday + 1) % 7;
- $first_week_size++;
- if ($first_weekday === $weekday_index) {
- break;
- }
- }
-
- if ($first_week_size >= 4) {
- $week_number = 1;
- } else {
- $week_number = 0;
- }
-
- $info_map = array();
-
- $weekday_map = self::getWeekdayIndexMap();
- $weekday_map = array_flip($weekday_map);
-
- $yearly_counts = array();
- $monthly_counts = array();
-
- $month_number = 1;
- $month_day = 1;
- for ($year_day = 1; $year_day <= $max_day; $year_day++) {
- $key = "{$month_number}M{$month_day}D";
-
- $short_day = $weekday_map[$weekday];
- if (empty($yearly_counts[$short_day])) {
- $yearly_counts[$short_day] = 0;
- }
- $yearly_counts[$short_day]++;
-
- if (empty($monthly_counts[$month_number][$short_day])) {
- $monthly_counts[$month_number][$short_day] = 0;
- }
- $monthly_counts[$month_number][$short_day]++;
-
- $info = array(
- 'year' => $year,
- 'key' => $key,
- 'month' => $month_number,
- 'monthday' => $month_day,
- '-monthday' => -$month_days[$month_number] + $month_day - 1,
- 'yearday' => $year_day,
- '-yearday' => -$max_day + $year_day - 1,
- 'week' => $week_number,
- 'weekday' => $short_day,
- 'weekday.yearly' => $yearly_counts[$short_day],
- 'weekday.monthly' => $monthly_counts[$month_number][$short_day],
- );
-
- $info_map[$key] = $info;
-
- $weekday = ($weekday + 1) % 7;
- if ($weekday === $weekday_index) {
- $week_number++;
- }
-
- $month_day = ($month_day + 1);
- if ($month_day > $month_days[$month_number]) {
- $month_day = 1;
- $month_number++;
- }
- }
-
- // Check how long the final week is. If it doesn't have four days, this
- // is really the first week of the next year.
- $final_week = array();
- foreach ($info_map as $key => $info) {
- if ($info['week'] == $week_number) {
- $final_week[] = $key;
- }
- }
-
- if (count($final_week) < 4) {
- $week_number = $week_number - 1;
- $next_year = self::getYearMap($year + 1, $weekday_start);
- $next_year_weeks = $next_year['weekCount'];
- } else {
- $next_year_weeks = null;
- }
-
- if ($first_week_size < 4) {
- $last_year = self::getYearMap($year - 1, $weekday_start);
- $last_year_weeks = $last_year['weekCount'];
- } else {
- $last_year_weeks = null;
- }
-
- // Now that we know how many weeks the year has, we can compute the
- // negative offsets.
- foreach ($info_map as $key => $info) {
- $week = $info['week'];
-
- if ($week === 0) {
- // If this day is part of the first partial week of the year, give
- // it the week number of the last week of the prior year instead.
- $info['week'] = $last_year_weeks;
- $info['-week'] = -1;
- } else if ($week > $week_number) {
- // If this day is part of the last partial week of the year, give
- // it week numbers from the next year.
- $info['week'] = 1;
- $info['-week'] = -$next_year_weeks;
- } else {
- $info['-week'] = -$week_number + $week - 1;
- }
-
- // Do all the arithmetic to figure out if this is the -19th Thursday
- // in the year and such.
- $month_number = $info['month'];
- $short_day = $info['weekday'];
- $monthly_count = $monthly_counts[$month_number][$short_day];
- $monthly_index = $info['weekday.monthly'];
- $info['-weekday.monthly'] = -$monthly_count + $monthly_index - 1;
- $info['-weekday.monthly'] .= $short_day;
- $info['weekday.monthly'] .= $short_day;
-
- $yearly_count = $yearly_counts[$short_day];
- $yearly_index = $info['weekday.yearly'];
- $info['-weekday.yearly'] = -$yearly_count + $yearly_index - 1;
- $info['-weekday.yearly'] .= $short_day;
- $info['weekday.yearly'] .= $short_day;
-
- $info_map[$key] = $info;
- }
-
- $week_map = array();
- foreach ($info_map as $key => $info) {
- $week_map[$info['week']][] = $key;
- }
-
- return array(
- 'info' => $info_map,
- 'weekCount' => $week_number,
- 'dayCount' => $max_day,
- 'monthDays' => $month_days,
- 'weekMap' => $week_map,
- );
- }
-
- private function newIteratorSet($cursor, $interval, $set, $limit) {
- if ($interval < 1) {
- throw new Exception(
- pht(
- 'Invalid iteration interval ("%d"), must be at least 1.',
- $interval));
- }
-
- $result = array();
- $seen = array();
-
- $ii = $cursor;
- while (true) {
- if (!$set || isset($set[$ii])) {
- $result[] = $ii;
- }
-
- $ii = ($ii + $interval);
-
- if ($ii >= $limit) {
- break;
- }
- }
-
- sort($result);
- $result = array_values($result);
-
- return array($ii, $result);
- }
-
- private function applySetPos(array $values, array $setpos) {
- $select = array();
-
- $count = count($values);
- foreach ($setpos as $pos) {
- if ($pos > 0 && $pos <= $count) {
- $select[] = ($pos - 1);
- } else if ($pos < 0 && $pos >= -$count) {
- $select[] = ($count + $pos);
- }
- }
-
- sort($select);
- $select = array_unique($select);
-
- return array_select_keys($values, $select);
- }
-
- private function assertByRange(
- $source,
- array $values,
- $min,
- $max,
- $allow_zero = true) {
-
- foreach ($values as $value) {
- if (!is_int($value)) {
- throw new Exception(
- pht(
- 'Value "%s" in RRULE "%s" parameter is invalid: values must be '.
- 'integers.',
- $value,
- $source));
- }
-
- if ($value < $min || $value > $max) {
- throw new Exception(
- pht(
- 'Value "%s" in RRULE "%s" parameter is invalid: it must be '.
- 'between %s and %s.',
- $value,
- $source,
- $min,
- $max));
- }
-
- if (!$value && !$allow_zero) {
- throw new Exception(
- pht(
- 'Value "%s" in RRULE "%s" parameter is invalid: it must not '.
- 'be zero.',
- $value,
- $source));
- }
- }
- }
-
- private function getSetPositionState() {
- $scale = $this->getFrequencyScale();
-
- $parts = array();
- $parts[] = $this->stateYear;
-
- if ($scale == self::SCALE_WEEKLY) {
- $parts[] = $this->stateWeek;
- } else {
- if ($scale < self::SCALE_YEARLY) {
- $parts[] = $this->stateMonth;
- }
- if ($scale < self::SCALE_MONTHLY) {
- $parts[] = $this->stateDay;
- }
- if ($scale < self::SCALE_DAILY) {
- $parts[] = $this->stateHour;
- }
- if ($scale < self::SCALE_HOURLY) {
- $parts[] = $this->stateMinute;
- }
- }
-
- return implode('/', $parts);
- }
-
- private function rewindMonth() {
- while ($this->cursorMonth < 1) {
- $this->cursorYear--;
- $this->cursorMonth += 12;
- }
- }
-
- private function rewindWeek() {
- $week_start = $this->getWeekStart();
- while ($this->cursorWeek < 1) {
- $this->cursorYear--;
- $year_map = $this->getYearMap($this->cursorYear, $week_start);
- $this->cursorWeek += $year_map['weekCount'];
- }
- }
-
- private function rewindDay() {
- $week_start = $this->getWeekStart();
- while ($this->cursorDay < 1) {
- $year_map = $this->getYearMap($this->cursorYear, $week_start);
- $this->cursorDay += $year_map['monthDays'][$this->cursorMonth];
- $this->cursorMonth--;
- $this->rewindMonth();
- }
- }
-
- private function rewindHour() {
- while ($this->cursorHour < 0) {
- $this->cursorHour += 24;
- $this->cursorDay--;
- $this->rewindDay();
- }
- }
-
- private function rewindMinute() {
- while ($this->cursorMinute < 0) {
- $this->cursorMinute += 60;
- $this->cursorHour--;
- $this->rewindHour();
- }
- }
-
- private function advanceCursorState(
- array $cursor,
- $scale,
- $interval,
- $week_start) {
-
- $state = array(
- 'year' => $this->stateYear,
- 'month' => $this->stateMonth,
- 'week' => $this->stateWeek,
- 'day' => $this->stateDay,
- 'hour' => $this->stateHour,
- );
-
- // In the common case when the interval is 1, we'll visit every possible
- // value so we don't need to do any math and can just jump to the first
- // hour, day, etc.
- if ($interval == 1) {
- if ($this->isCursorBehind($cursor, $state, $scale)) {
- switch ($scale) {
- case self::SCALE_DAILY:
- $this->cursorDay = 1;
- break;
- case self::SCALE_HOURLY:
- $this->cursorHour = 0;
- break;
- case self::SCALE_WEEKLY:
- $this->cursorWeek = 1;
- break;
- }
- }
-
- return array(false, $state);
- }
-
- $year_map = $this->getYearMap($cursor['year'], $week_start);
- while ($this->isCursorBehind($cursor, $state, $scale)) {
- switch ($scale) {
- case self::SCALE_DAILY:
- $cursor['day'] += $interval;
- break;
- case self::SCALE_HOURLY:
- $cursor['hour'] += $interval;
- break;
- case self::SCALE_WEEKLY:
- $cursor['week'] += $interval;
- break;
- }
-
- if ($scale <= self::SCALE_HOURLY) {
- while ($cursor['hour'] >= 24) {
- $cursor['hour'] -= 24;
- $cursor['day']++;
- }
- }
-
- if ($scale == self::SCALE_WEEKLY) {
- while ($cursor['week'] > $year_map['weekCount']) {
- $cursor['week'] -= $year_map['weekCount'];
- $cursor['year']++;
- $year_map = $this->getYearMap($cursor['year'], $week_start);
- }
- }
-
- if ($scale <= self::SCALE_DAILY) {
- while ($cursor['day'] > $year_map['monthDays'][$cursor['month']]) {
- $cursor['day'] -= $year_map['monthDays'][$cursor['month']];
- $cursor['month']++;
- if ($cursor['month'] > 12) {
- $cursor['month'] -= 12;
- $cursor['year']++;
- $year_map = $this->getYearMap($cursor['year'], $week_start);
- }
- }
- }
- }
-
- switch ($scale) {
- case self::SCALE_DAILY:
- $this->cursorDay = $cursor['day'];
- break;
- case self::SCALE_HOURLY:
- $this->cursorHour = $cursor['hour'];
- break;
- case self::SCALE_WEEKLY:
- $this->cursorWeek = $cursor['week'];
- break;
- }
-
- $skip = $this->isCursorBehind($state, $cursor, $scale);
-
- return array($skip, $cursor);
- }
-
- private function isCursorBehind(array $cursor, array $state, $scale) {
- if ($cursor['year'] < $state['year']) {
- return true;
- } else if ($cursor['year'] > $state['year']) {
- return false;
- }
-
- if ($scale == self::SCALE_WEEKLY) {
- return false;
- }
-
- if ($cursor['month'] < $state['month']) {
- return true;
- } else if ($cursor['month'] > $state['month']) {
- return false;
- }
-
- if ($scale >= self::SCALE_DAILY) {
- return false;
- }
-
- if ($cursor['day'] < $state['day']) {
- return true;
- } else if ($cursor['day'] > $state['day']) {
- return false;
- }
-
- if ($scale >= self::SCALE_HOURLY) {
- return false;
- }
-
- if ($cursor['hour'] < $state['hour']) {
- return true;
- } else if ($cursor['hour'] > $state['hour']) {
- return false;
- }
-
- return false;
- }
-
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php b/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarRecurrenceSet.php
+++ /dev/null
@@ -1,162 +0,0 @@
-<?php
-
-final class PhutilCalendarRecurrenceSet
- extends Phobject {
-
- private $sources = array();
- private $viewerTimezone = 'UTC';
-
- public function addSource(PhutilCalendarRecurrenceSource $source) {
- $this->sources[] = $source;
- return $this;
- }
-
- public function setViewerTimezone($viewer_timezone) {
- $this->viewerTimezone = $viewer_timezone;
- return $this;
- }
-
- public function getViewerTimezone() {
- return $this->viewerTimezone;
- }
-
- public function getEventsBetween(
- PhutilCalendarDateTime $start = null,
- PhutilCalendarDateTime $end = null,
- $limit = null) {
-
- if ($end === null && $limit === null) {
- throw new Exception(
- pht(
- 'Recurring event range queries must have an end date, a limit, or '.
- 'both.'));
- }
-
- $timezone = $this->getViewerTimezone();
-
- $sources = array();
- foreach ($this->sources as $source) {
- $source = clone $source;
- $source->setViewerTimezone($timezone);
- $source->resetSource();
-
- $sources[] = array(
- 'source' => $source,
- 'state' => null,
- 'epoch' => null,
- );
- }
-
- if ($start) {
- $start = clone $start;
- $start->setViewerTimezone($timezone);
- $min_epoch = $start->getEpoch();
- } else {
- $min_epoch = 0;
- }
-
- if ($end) {
- $end = clone $end;
- $end->setViewerTimezone($timezone);
- $end_epoch = $end->getEpoch();
- } else {
- $end_epoch = null;
- }
-
- $results = array();
- $index = 0;
- $cursor = 0;
- while (true) {
- // Get the next event for each source which we don't have a future
- // event for.
- foreach ($sources as $key => $source) {
- $state = $source['state'];
- $epoch = $source['epoch'];
-
- if ($state !== null && $epoch >= $cursor) {
- // We have an event for this source, and it's a future event, so
- // we don't need to do anything.
- continue;
- }
-
- $next = $source['source']->getNextEvent($cursor);
- if ($next === null) {
- // This source doesn't have any more events, so we're all done.
- unset($sources[$key]);
- continue;
- }
-
- $next_epoch = $next->getEpoch();
-
- if ($end_epoch !== null && $next_epoch > $end_epoch) {
- // We have an end time and the next event from this source is
- // past that end, so we know there are no more relevant events
- // coming from this source.
- unset($sources[$key]);
- continue;
- }
-
- $sources[$key]['state'] = $next;
- $sources[$key]['epoch'] = $next_epoch;
- }
-
- if (!$sources) {
- // We've run out of sources which can produce valid events in the
- // window, so we're all done.
- break;
- }
-
- // Find the minimum event time across all sources.
- $next_epoch = null;
- foreach ($sources as $source) {
- if ($next_epoch === null) {
- $next_epoch = $source['epoch'];
- } else {
- $next_epoch = min($next_epoch, $source['epoch']);
- }
- }
-
- $is_exception = false;
- $next_source = null;
- foreach ($sources as $source) {
- if ($source['epoch'] == $next_epoch) {
- if ($source['source']->getIsExceptionSource()) {
- $is_exception = true;
- } else {
- $next_source = $source;
- }
- }
- }
-
- // If this is an exception, it means the event does NOT occur. We
- // skip it and move on. If it's not an exception, it does occur, so
- // we record it.
- if (!$is_exception) {
-
- // Only actually include this event in the results if it starts after
- // any specified start time. We increment the index regardless, so we
- // return results with proper offsets.
- if ($next_source['epoch'] >= $min_epoch) {
- $results[$index] = $next_source['state'];
- }
- $index++;
-
- if ($limit !== null && (count($results) >= $limit)) {
- break;
- }
- }
-
- $cursor = $next_epoch + 1;
-
- // If we have an end of the window and we've reached it, we're done.
- if ($end_epoch) {
- if ($cursor > $end_epoch) {
- break;
- }
- }
- }
-
- return $results;
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarRecurrenceSource.php b/src/parser/calendar/data/PhutilCalendarRecurrenceSource.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarRecurrenceSource.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-abstract class PhutilCalendarRecurrenceSource
- extends Phobject {
-
- private $isExceptionSource;
- private $viewerTimezone;
-
- public function setIsExceptionSource($is_exception_source) {
- $this->isExceptionSource = $is_exception_source;
- return $this;
- }
-
- public function getIsExceptionSource() {
- return $this->isExceptionSource;
- }
-
- public function setViewerTimezone($viewer_timezone) {
- $this->viewerTimezone = $viewer_timezone;
- return $this;
- }
-
- public function getViewerTimezone() {
- return $this->viewerTimezone;
- }
-
- public function resetSource() {
- return;
- }
-
- abstract public function getNextEvent($cursor);
-
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php b/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarRelativeDateTime.php
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-
-final class PhutilCalendarRelativeDateTime
- extends PhutilCalendarProxyDateTime {
-
- private $duration;
-
- public function setOrigin(PhutilCalendarDateTime $origin) {
- return $this->setProxy($origin);
- }
-
- public function getOrigin() {
- return $this->getProxy();
- }
-
- public function setDuration(PhutilCalendarDuration $duration) {
- $this->duration = $duration;
- return $this;
- }
-
- public function getDuration() {
- return $this->duration;
- }
-
- public function newPHPDateTime() {
- $datetime = parent::newPHPDateTime();
- $duration = $this->getDuration();
-
- if ($duration->getIsNegative()) {
- $sign = '-';
- } else {
- $sign = '+';
- }
-
- $map = array(
- 'weeks' => $duration->getWeeks(),
- 'days' => $duration->getDays(),
- 'hours' => $duration->getHours(),
- 'minutes' => $duration->getMinutes(),
- 'seconds' => $duration->getSeconds(),
- );
-
- foreach ($map as $unit => $value) {
- if (!$value) {
- continue;
- }
- $datetime->modify("{$sign}{$value} {$unit}");
- }
-
- return $datetime;
- }
-
- public function newAbsoluteDateTime() {
- $clone = clone $this;
-
- if ($clone->getTimezone()) {
- $clone->setViewerTimezone(null);
- }
-
- $datetime = $clone->newPHPDateTime();
-
- return id(new PhutilCalendarAbsoluteDateTime())
- ->setYear((int)$datetime->format('Y'))
- ->setMonth((int)$datetime->format('m'))
- ->setDay((int)$datetime->format('d'))
- ->setHour((int)$datetime->format('H'))
- ->setMinute((int)$datetime->format('i'))
- ->setSecond((int)$datetime->format('s'))
- ->setIsAllDay($clone->getIsAllDay())
- ->setTimezone($clone->getTimezone())
- ->setViewerTimezone($this->getViewerTimezone());
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarRootNode.php b/src/parser/calendar/data/PhutilCalendarRootNode.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarRootNode.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-final class PhutilCalendarRootNode
- extends PhutilCalendarContainerNode {
-
- const NODETYPE = 'root';
-
- public function getDocuments() {
- return $this->getChildrenOfType(PhutilCalendarDocumentNode::NODETYPE);
- }
-
-}
diff --git a/src/parser/calendar/data/PhutilCalendarUserNode.php b/src/parser/calendar/data/PhutilCalendarUserNode.php
deleted file mode 100644
--- a/src/parser/calendar/data/PhutilCalendarUserNode.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-final class PhutilCalendarUserNode extends PhutilCalendarNode {
-
- private $name;
- private $uri;
- private $status;
-
- const STATUS_INVITED = 'invited';
- const STATUS_ACCEPTED = 'accepted';
- const STATUS_DECLINED = 'declined';
-
- public function setName($name) {
- $this->name = $name;
- return $this;
- }
-
- public function getName() {
- return $this->name;
- }
-
- public function setURI($uri) {
- $this->uri = $uri;
- return $this;
- }
-
- public function getURI() {
- return $this->uri;
- }
-
- public function setStatus($status) {
- $this->status = $status;
- return $this;
- }
-
- public function getStatus() {
- return $this->status;
- }
-
-}
diff --git a/src/parser/calendar/data/__tests__/PhutilCalendarDateTimeTestCase.php b/src/parser/calendar/data/__tests__/PhutilCalendarDateTimeTestCase.php
deleted file mode 100644
--- a/src/parser/calendar/data/__tests__/PhutilCalendarDateTimeTestCase.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-final class PhutilCalendarDateTimeTestCase extends PhutilTestCase {
-
- public function testDateTimeDuration() {
- $start = PhutilCalendarAbsoluteDateTime::newFromISO8601('20161128T090000Z')
- ->setTimezone('America/Los_Angeles')
- ->setViewerTimezone('America/Chicago')
- ->setIsAllDay(true);
-
- $this->assertEqual(
- '20161128',
- $start->getISO8601());
-
- $end = $start
- ->newAbsoluteDateTime()
- ->setHour(0)
- ->setMinute(0)
- ->setSecond(0)
- ->newRelativeDateTime('P1D')
- ->newAbsoluteDateTime();
-
- $this->assertEqual(
- '20161129',
- $end->getISO8601());
-
- // This is a date which explicitly has no specified timezone.
- $start = PhutilCalendarAbsoluteDateTime::newFromISO8601('20161128', null)
- ->setViewerTimezone('UTC');
-
- $this->assertEqual(
- '20161128',
- $start->getISO8601());
-
- $end = $start
- ->newAbsoluteDateTime()
- ->setHour(0)
- ->setMinute(0)
- ->setSecond(0)
- ->newRelativeDateTime('P1D')
- ->newAbsoluteDateTime();
-
- $this->assertEqual(
- '20161129',
- $end->getISO8601());
- }
-
-
-}
diff --git a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php
deleted file mode 100644
--- a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php
+++ /dev/null
@@ -1,1750 +0,0 @@
-<?php
-
-final class PhutilCalendarRecurrenceRuleTestCase extends PhutilTestCase {
-
- public function testSimpleRecurrenceRules() {
- $start = PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z');
-
- $rrule = id(new PhutilCalendarRecurrenceRule())
- ->setStartDateTime($start)
- ->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_DAILY);
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($rrule);
-
- $result = $set->getEventsBetween(null, null, 3);
-
- $expect = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- );
-
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Simple daily event.'));
-
-
-
- $rrule = id(new PhutilCalendarRecurrenceRule())
- ->setStartDateTime($start)
- ->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_HOURLY)
- ->setByHour(array(12, 13));
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($rrule);
-
- $result = $set->getEventsBetween(null, null, 5);
-
- $expect = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T130000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T130000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- );
-
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Hourly event with BYHOUR.'));
-
-
- $rrule = id(new PhutilCalendarRecurrenceRule())
- ->setStartDateTime($start)
- ->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY);
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($rrule);
-
- $result = $set->getEventsBetween(null, null, 2);
-
- $expect = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20170101T120000Z'),
- );
-
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Yearly event.'));
-
-
- // This is an efficiency test for bizarre rules: it defines a secondly
- // event which only occurs one a year, and generates 3 instances of it.
- // This implementation should be fast enough that this test doesn't take
- // a significant amount of time.
-
- $rrule = id(new PhutilCalendarRecurrenceRule())
- ->setStartDateTime($start)
- ->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_SECONDLY)
- ->setByMonth(array(1))
- ->setByMonthDay(array(1))
- ->setByHour(array(12))
- ->setByMinute(array(0))
- ->setBySecond(array(0));
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($rrule);
-
- $result = $set->getEventsBetween(null, null, 3);
-
- $expect = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20170101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20180101T120000Z'),
- );
-
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Secondly event with many constraints.'));
- }
-
- public function testYearlyRecurrenceRules() {
- $tests = array();
- $expect = array();
-
- $tests[] = array();
- $expect[] = array(
- '19970902',
- '19980902',
- '19990902',
- );
-
- $tests[] = array(
- 'INTERVAL' => 2,
- );
- $expect[] = array(
- '19970902',
- '19990902',
- '20010902',
- );
-
- $tests[] = array(
- 'DTSTART' => '20000229',
- );
- $expect[] = array(
- '20000229',
- '20040229',
- '20080229',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- );
- $expect[] = array(
- '19980102',
- '19980302',
- '19990102',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- );
- $expect[] = array(
- '19970903',
- '19971001',
- '19971003',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYMONTHDAY' => array(5, 7),
- );
- $expect[] = array(
- '19980105',
- '19980107',
- '19980305',
- );
-
- $tests[] = array(
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19970902',
- '19970904',
- '19970909',
- );
-
- $tests[] = array(
- 'BYDAY' => array('SU'),
- );
- $expect[] = array(
- '19970907',
- '19970914',
- '19970921',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101',
- '19980106',
- '19980108',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101',
- '19980203',
- '19980303',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- 'BYMONTH' => array(1, 3),
- );
- $expect[] = array(
- '19980101',
- '19980303',
- '20010301',
- );
-
- $tests[] = array(
- 'BYDAY' => array('1TU', '-1TH'),
- );
- $expect[] = array(
- '19971225',
- '19980106',
- '19981231',
- );
-
- // Same test as above, just making sure the optional "+" syntax works.
- $tests[] = array(
- 'BYDAY' => array('+1TU', '-1TH'),
- );
- $expect[] = array(
- '19971225',
- '19980106',
- '19981231',
- );
-
- $tests[] = array(
- 'BYDAY' => array('3TU', '-3TH'),
- );
- $expect[] = array(
- '19971211',
- '19980120',
- '19981217',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('1TU', '-1TH'),
- );
- $expect[] = array(
- '19980106',
- '19980129',
- '19980303',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('3TU', '-3TH'),
- );
- $expect[] = array(
- '19980115',
- '19980120',
- '19980312',
- );
-
- $tests[] = array(
- 'BYYEARDAY' => array(1, 100, 200, 365),
- 'COUNT' => 4,
- );
- $expect[] = array(
- '19971231',
- '19980101',
- '19980410',
- '19980719',
- );
-
- $tests[] = array(
- 'BYYEARDAY' => array(-365, -266, -166, -1),
- 'COUNT' => 4,
- );
- $expect[] = array(
- '19971231',
- '19980101',
- '19980410',
- '19980719',
- );
-
- $tests[] = array(
- 'BYYEARDAY' => array(1, 100, 200, 365),
- 'BYMONTH' => array(4, 7),
- 'COUNT' => 4,
- );
- $expect[] = array(
- '19980410',
- '19980719',
- '19990410',
- '19990719',
- );
-
- $tests[] = array(
- 'BYYEARDAY' => array(-365, -266, -166, -1),
- 'BYMONTH' => array(4, 7),
- 'COUNT' => 4,
- );
- $expect[] = array(
- '19980410',
- '19980719',
- '19990410',
- '19990719',
- );
-
- $tests[] = array(
- 'BYWEEKNO' => array(20),
- );
- $expect[] = array(
- '19980511',
- '19980512',
- '19980513',
- );
-
- $tests[] = array(
- 'BYWEEKNO' => array(1),
- 'BYDAY' => array('MO'),
- );
- $expect[] = array(
- '19971229',
- '19990104',
- '20000103',
- );
-
- $tests[] = array(
- 'BYWEEKNO' => array(52),
- 'BYDAY' => array('SU'),
- );
- $expect[] = array(
- '19971228',
- '19981227',
- '20000102',
- );
-
- $tests[] = array(
- 'BYWEEKNO' => array(-1),
- 'BYDAY' => array('SU'),
- );
- $expect[] = array(
- '19971228',
- '19990103',
- '20000102',
- );
-
- $tests[] = array(
- 'BYWEEKNO' => array(53),
- 'BYDAY' => array('MO'),
- );
- $expect[] = array(
- '19981228',
- '20041227',
- '20091228',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- );
- $expect[] = array(
- '19970902T060000Z',
- '19970902T180000Z',
- '19980902T060000Z',
- );
-
- $tests[] = array(
- 'BYMINUTE' => array(15, 30),
- );
- $expect[] = array(
- '19970902T001500Z',
- '19970902T003000Z',
- '19980902T001500Z',
- );
-
- $tests[] = array(
- 'BYSECOND' => array(10, 20),
- );
- $expect[] = array(
- '19970902T000010Z',
- '19970902T000020Z',
- '19980902T000010Z',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- 'BYMINUTE' => array(15, 30),
- );
- $expect[] = array(
- '19970902T061500Z',
- '19970902T063000Z',
- '19970902T181500Z',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- 'BYSECOND' => array(10, 20),
- );
- $expect[] = array(
- '19970902T060010Z',
- '19970902T060020Z',
- '19970902T180010Z',
- );
-
- $tests[] = array(
- 'BYMINUTE' => array(15, 30),
- 'BYSECOND' => array(10, 20),
- );
- $expect[] = array(
- '19970902T001510Z',
- '19970902T001520Z',
- '19970902T003010Z',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- 'BYMINUTE' => array(15, 30),
- 'BYSECOND' => array(10, 20),
- );
- $expect[] = array(
- '19970902T061510Z',
- '19970902T061520Z',
- '19970902T063010Z',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(15),
- 'BYHOUR' => array(6, 18),
- 'BYSETPOS' => array(3, -3),
- );
- $expect[] = array(
- '19971115T180000Z',
- '19980215T060000Z',
- '19981115T180000Z',
- );
-
- $this->assertRules(
- array(
- 'FREQ' => 'YEARLY',
- 'COUNT' => 3,
- 'DTSTART' => '19970902',
- ),
- $tests,
- $expect);
- }
-
- public function testMonthlyRecurrenceRules() {
- $tests = array();
- $expect = array();
-
- $tests[] = array();
- $expect[] = array(
- '19970902',
- '19971002',
- '19971102',
- );
-
- $tests[] = array(
- 'INTERVAL' => 2,
- );
- $expect[] = array(
- '19970902',
- '19971102',
- '19980102',
- );
-
- $tests[] = array(
- 'INTERVAL' => 18,
- );
- $expect[] = array(
- '19970902',
- '19990302',
- '20000902',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- );
- $expect[] = array(
- '19980102',
- '19980302',
- '19990102',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- );
- $expect[] = array(
- '19970903',
- '19971001',
- '19971003',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(5, 7),
- 'BYMONTH' => array(1, 3),
- );
- $expect[] = array(
- '19980105',
- '19980107',
- '19980305',
- );
-
- $tests[] = array(
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19970902',
- '19970904',
- '19970909',
- );
-
- $tests[] = array(
- 'BYDAY' => array('3MO'),
- );
- $expect[] = array(
- '19970915',
- '19971020',
- '19971117',
- );
-
- $tests[] = array(
- 'BYDAY' => array('1TU', '-1TH'),
- );
- $expect[] = array(
- '19970902',
- '19970925',
- '19971007',
- );
-
- $tests[] = array(
- 'BYDAY' => array('3TU', '-3TH'),
- );
- $expect[] = array(
- '19970911',
- '19970916',
- '19971016',
- );
-
- $tests[] = array(
- 'BYDAY' => array('TU', 'TH'),
- 'BYMONTH' => array(1, 3),
- );
- $expect[] = array(
- '19980101',
- '19980106',
- '19980108',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('1TU', '-1TH'),
- );
- $expect[] = array(
- '19980106',
- '19980129',
- '19980303',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('3TU', '-3TH'),
- );
- $expect[] = array(
- '19980115',
- '19980120',
- '19980312',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101',
- '19980203',
- '19980303',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYMONTHDAY' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101',
- '19980303',
- '20010301',
- );
-
- $tests[] = array(
- 'BYDAY' => array('MO', 'TU', 'WE', 'TH', 'FR'),
- 'BYSETPOS' => array(-1),
- );
- $expect[] = array(
- '19970930',
- '19971031',
- '19971128',
- );
-
- $tests[] = array(
- 'BYDAY' => array('1MO', '1TU', '1WE', '1TH', '1FR', '-1FR'),
- 'BYMONTHDAY' => array(1, -1, -2),
- );
- $expect[] = array(
- '19971001',
- '19971031',
- '19971201',
- );
-
- $tests[] = array(
- 'BYDAY' => array('1MO', '1TU', '1WE', '1TH', 'FR'),
- 'BYMONTHDAY' => array(1, -1, -2),
- );
- $expect[] = array(
- '19971001',
- '19971031',
- '19971201',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- );
- $expect[] = array(
- '19970902T060000Z',
- '19970902T180000Z',
- '19971002T060000Z',
- );
-
- $tests[] = array(
- 'BYMINUTE' => array(6, 18),
- );
- $expect[] = array(
- '19970902T000600Z',
- '19970902T001800Z',
- '19971002T000600Z',
- );
-
- $tests[] = array(
- 'BYSECOND' => array(6, 18),
- );
- $expect[] = array(
- '19970902T000006Z',
- '19970902T000018Z',
- '19971002T000006Z',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(13, 17),
- 'BYHOUR' => array(6, 18),
- 'BYSETPOS' => array(3, -3),
- );
- $expect[] = array(
- '19970913T180000Z',
- '19970917T060000Z',
- '19971013T180000Z',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(13, 17),
- 'BYHOUR' => array(6, 18),
- 'BYSETPOS' => array(3, 3, -3),
- );
- $expect[] = array(
- '19970913T180000Z',
- '19970917T060000Z',
- '19971013T180000Z',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(13, 17),
- 'BYHOUR' => array(6, 18),
- 'BYSETPOS' => array(4, -1),
- );
- $expect[] = array(
- '19970917T180000Z',
- '19971017T180000Z',
- '19971117T180000Z',
- );
-
- $this->assertRules(
- array(
- 'FREQ' => 'MONTHLY',
- 'COUNT' => 3,
- 'DTSTART' => '19970902',
- ),
- $tests,
- $expect);
- }
-
- public function testWeeklyRecurrenceRules() {
- $tests = array();
- $expect = array();
-
- $tests[] = array();
- $expect[] = array(
- '19970902',
- '19970909',
- '19970916',
- );
-
- $tests[] = array(
- 'INTERVAL' => 2,
- );
- $expect[] = array(
- '19970902',
- '19970916',
- '19970930',
- );
-
- $tests[] = array(
- 'INTERVAL' => 20,
- );
- $expect[] = array(
- '19970902',
- '19980120',
- '19980609',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- );
- $expect[] = array(
- '19980106',
- '19980113',
- '19980120',
- );
-
- $tests[] = array(
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19970902',
- '19970904',
- '19970909',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101',
- '19980106',
- '19980108',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- );
- $expect[] = array(
- '19970902T060000Z',
- '19970902T180000Z',
- '19970909T060000Z',
- );
-
- $tests[] = array(
- 'BYDAY' => array('TU', 'TH'),
- 'BYHOUR' => array(6, 18),
- 'BYSETPOS' => array(3, -3),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T180000Z',
- '19970904T060000Z',
- '19970909T180000Z',
- );
-
- $this->assertRules(
- array(
- 'FREQ' => 'WEEKLY',
- 'COUNT' => 3,
- 'DTSTART' => '19970902',
- ),
- $tests,
- $expect);
- }
-
- public function testDailyRecurrenceRules() {
- $tests = array();
- $expect = array();
-
- $tests[] = array();
- $expect[] = array(
- '19970902',
- '19970903',
- '19970904',
- );
-
- $tests[] = array(
- 'INTERVAL' => 2,
- );
- $expect[] = array(
- '19970902',
- '19970904',
- '19970906',
- );
-
- $tests[] = array(
- 'INTERVAL' => 92,
- );
- $expect[] = array(
- '19970902',
- '19971203',
- '19980305',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- );
- $expect[] = array(
- '19980101',
- '19980102',
- '19980103',
- );
-
- // This is testing that INTERVAL is respected in the presence of a BYMONTH
- // filter which skips some months.
- $tests[] = array(
- 'BYMONTH' => array(12),
- 'INTERVAL' => 17,
- );
- $expect[] = array(
- '19971213',
- '19971230',
- '19981205',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- );
- $expect[] = array(
- '19970903',
- '19971001',
- '19971003',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYMONTHDAY' => array(5, 7),
- );
- $expect[] = array(
- '19980105',
- '19980107',
- '19980305',
- );
-
- $tests[] = array(
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19970902',
- '19970904',
- '19970909',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101',
- '19980106',
- '19980108',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101',
- '19980203',
- '19980303',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYMONTHDAY' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101',
- '19980303',
- '20010301',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- 'BYMINUTE' => array(15, 45),
- 'BYSETPOS' => array(3, -3),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T181500Z',
- '19970903T064500Z',
- '19970903T181500Z',
- );
-
- $this->assertRules(
- array(
- 'FREQ' => 'DAILY',
- 'COUNT' => 3,
- 'DTSTART' => '19970902',
- ),
- $tests,
- $expect);
- }
-
- public function testHourlyRecurrenceRules() {
- $tests = array();
- $expect = array();
-
- $tests[] = array();
- $expect[] = array(
- '19970902T090000Z',
- '19970902T100000Z',
- '19970902T110000Z',
- );
-
- $tests[] = array(
- 'INTERVAL' => 2,
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970902T110000Z',
- '19970902T130000Z',
- );
-
- $tests[] = array(
- 'INTERVAL' => 769,
- );
- $expect[] = array(
- '19970902T090000Z',
- '19971004T100000Z',
- '19971105T110000Z',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- );
- $expect[] = array(
- '19980101T000000Z',
- '19980101T010000Z',
- '19980101T020000Z',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- );
- $expect[] = array(
- '19970903T000000Z',
- '19970903T010000Z',
- '19970903T020000Z',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYMONTHDAY' => array(5, 7),
- );
- $expect[] = array(
- '19980105T000000Z',
- '19980105T010000Z',
- '19980105T020000Z',
- );
-
- $tests[] = array(
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970902T100000Z',
- '19970902T110000Z',
- );
-
- $tests[] = array(
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101T000000Z',
- '19980101T010000Z',
- '19980101T020000Z',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101T000000Z',
- '19980101T010000Z',
- '19980101T020000Z',
- );
-
- $tests[] = array(
- 'BYMONTHDAY' => array(1, 3),
- 'BYMONTH' => array(1, 3),
- 'BYDAY' => array('TU', 'TH'),
- );
- $expect[] = array(
- '19980101T000000Z',
- '19980101T010000Z',
- '19980101T020000Z',
- );
-
- $tests[] = array(
- 'COUNT' => 4,
- 'BYYEARDAY' => array(1, 100, 200, 365),
- );
- $expect[] = array(
- '19971231T000000Z',
- '19971231T010000Z',
- '19971231T020000Z',
- '19971231T030000Z',
- );
-
- $tests[] = array(
- 'COUNT' => 4,
- 'BYYEARDAY' => array(-365, -266, -166, -1),
- );
- $expect[] = array(
- '19971231T000000Z',
- '19971231T010000Z',
- '19971231T020000Z',
- '19971231T030000Z',
- );
-
- $tests[] = array(
- 'COUNT' => 4,
- 'BYMONTH' => array(4, 7),
- 'BYYEARDAY' => array(1, 100, 200, 365),
- );
- $expect[] = array(
- '19980410T000000Z',
- '19980410T010000Z',
- '19980410T020000Z',
- '19980410T030000Z',
- );
-
- $tests[] = array(
- 'COUNT' => 4,
- 'BYMONTH' => array(4, 7),
- 'BYYEARDAY' => array(-365, -266, -166, -1),
- );
- $expect[] = array(
- '19980410T000000Z',
- '19980410T010000Z',
- '19980410T020000Z',
- '19980410T030000Z',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- );
- $expect[] = array(
- '19970902T180000Z',
- '19970903T060000Z',
- '19970903T180000Z',
- );
-
- $tests[] = array(
- 'BYMINUTE' => array(15, 45),
- 'BYSECOND' => array(15, 45),
- 'BYSETPOS' => array(3, -3),
- );
- $expect[] = array(
- '19970902T091545Z',
- '19970902T094515Z',
- '19970902T101545Z',
- );
-
- $this->assertRules(
- array(
- 'FREQ' => 'HOURLY',
- 'COUNT' => 3,
- 'DTSTART' => '19970902T090000Z',
- ),
- $tests,
- $expect);
- }
-
- public function testMinutelyRecurrenceRules() {
- $tests = array();
- $expect = array();
-
- $tests[] = array(
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970902T090100Z',
- '19970902T090200Z',
- );
-
- $tests[] = array(
- 'INTERVAL' => 2,
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970902T090200Z',
- '19970902T090400Z',
- );
-
- $tests[] = array(
- 'BYHOUR' => array(6, 18),
- 'BYMINUTE' => array(6, 18),
- 'BYSECOND' => array(6, 18),
- );
- $expect[] = array(
- '19970902T180606Z',
- '19970902T180618Z',
- '19970902T181806Z',
- );
-
- $tests[] = array(
- 'BYSECOND' => array(15, 30, 45),
- 'BYSETPOS' => array(3, -3),
- );
- $expect[] = array(
- '19970902T090015Z',
- '19970902T090045Z',
- '19970902T090115Z',
- );
-
- $this->assertRules(
- array(
- 'FREQ' => 'MINUTELY',
- 'COUNT' => 3,
- 'DTSTART' => '19970902T090000Z',
- ),
- $tests,
- $expect);
- }
-
- public function testSecondlyRecurrenceRules() {
- $tests = array();
- $expect = array();
-
- $tests[] = array();
- $expect[] = array(
- '19970902T090000Z',
- '19970902T090001Z',
- '19970902T090002Z',
- );
-
- $tests[] = array(
- 'INTERVAL' => 2,
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970902T090002Z',
- '19970902T090004Z',
- );
-
- $tests[] = array(
- 'INTERVAL' => 90061,
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970903T100101Z',
- '19970904T110202Z',
- );
-
- $tests[] = array(
- 'BYSECOND' => array(0),
- 'BYMINUTE' => array(1),
- 'DTSTART' => '20100322T120100Z',
- );
- $expect[] = array(
- '20100322T120100Z',
- '20100322T130100Z',
- '20100322T140100Z',
- );
-
- $this->assertRules(
- array(
- 'FREQ' => 'SECONDLY',
- 'COUNT' => 3,
- 'DTSTART' => '19970902T090000Z',
- ),
- $tests,
- $expect);
- }
-
- public function testRFC5545RecurrenceRules() {
- // These tests are derived from the examples in RFC5545.
- $tests = array();
- $expect = array();
-
- $tests[] = array(
- 'FREQ' => 'DAILY',
- 'COUNT' => 10,
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970903T090000Z',
- '19970904T090000Z',
- '19970905T090000Z',
- '19970906T090000Z',
- '19970907T090000Z',
- '19970908T090000Z',
- '19970909T090000Z',
- '19970910T090000Z',
- '19970911T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'DAILY',
- 'INTERVAL' => 2,
- 'DTSTART' => '19970902T090000Z',
- 'COUNT' => 5,
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970904T090000Z',
- '19970906T090000Z',
- '19970908T090000Z',
- '19970910T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'BYMONTH' => array(1),
- 'BYDAY' => array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'),
- 'DTSTART' => '19970902T090000Z',
- 'COUNT' => 3,
- );
- $expect[] = array(
- '19980101T090000Z',
- '19980102T090000Z',
- '19980103T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'COUNT' => 3,
- 'BYDAY' => array('1FR'),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970905T090000Z',
- '19971003T090000Z',
- '19971107T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'INTERVAL' => 2,
- 'COUNT' => 5,
- 'BYDAY' => array('1SU', '-1SU'),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970907T090000Z',
- '19970928T090000Z',
- '19971102T090000Z',
- '19971130T090000Z',
- '19980104T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'COUNT' => 6,
- 'BYDAY' => array('-2MO'),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970922T090000Z',
- '19971020T090000Z',
- '19971117T090000Z',
- '19971222T090000Z',
- '19980119T090000Z',
- '19980216T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'COUNT' => 6,
- 'BYMONTHDAY' => array(-3),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970928T090000Z',
- '19971029T090000Z',
- '19971128T090000Z',
- '19971229T090000Z',
- '19980129T090000Z',
- '19980226T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'COUNT' => 5,
- 'BYMONTHDAY' => array(2, 15),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970915T090000Z',
- '19971002T090000Z',
- '19971015T090000Z',
- '19971102T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'COUNT' => 5,
- 'BYMONTHDAY' => array(-1, 1),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970930T090000Z',
- '19971001T090000Z',
- '19971031T090000Z',
- '19971101T090000Z',
- '19971130T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'COUNT' => 7,
- 'INTERVAL' => 18,
- 'BYMONTHDAY' => array(10, 11, 12, 13, 14, 15),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970910T090000Z',
- '19970911T090000Z',
- '19970912T090000Z',
- '19970913T090000Z',
- '19970914T090000Z',
- '19970915T090000Z',
- '19990310T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'COUNT' => 6,
- 'INTERVAL' => 2,
- 'BYDAY' => array('TU'),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970909T090000Z',
- '19970916T090000Z',
- '19970923T090000Z',
- '19970930T090000Z',
- '19971104T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'COUNT' => 10,
- 'BYMONTH' => array(6, 7),
- 'DTSTART' => '19970610T090000Z',
- );
- $expect[] = array(
- '19970610T090000Z',
- '19970710T090000Z',
- '19980610T090000Z',
- '19980710T090000Z',
- '19990610T090000Z',
- '19990710T090000Z',
- '20000610T090000Z',
- '20000710T090000Z',
- '20010610T090000Z',
- '20010710T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'COUNT' => 4,
- 'INTERVAL' => 3,
- 'BYYEARDAY' => array(1, 100, 200),
- 'DTSTART' => '19970101T090000Z',
- );
- $expect[] = array(
- '19970101T090000Z',
- '19970410T090000Z',
- '19970719T090000Z',
- '20000101T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'COUNT' => 3,
- 'BYDAY' => array('20MO'),
- 'DTSTART' => '19970519T090000Z',
- );
- $expect[] = array(
- '19970519T090000Z',
- '19980518T090000Z',
- '19990517T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'COUNT' => 3,
- 'BYWEEKNO' => array(20),
- 'BYDAY' => array('MO'),
- 'DTSTART' => '19970512T090000Z',
- );
- $expect[] = array(
- '19970512T090000Z',
- '19980511T090000Z',
- '19990517T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'BYDAY' => array('TH'),
- 'BYMONTH' => array(3),
- 'DTSTART' => '19970313T090000Z',
- 'COUNT' => 5,
- );
- $expect[] = array(
- '19970313T090000Z',
- '19970320T090000Z',
- '19970327T090000Z',
- '19980305T090000Z',
- '19980312T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'BYDAY' => array('TH'),
- 'BYMONTH' => array(6, 7, 8),
- 'DTSTART' => '19970101T090000Z',
- 'COUNT' => 15,
- );
- $expect[] = array(
- '19970605T090000Z',
- '19970612T090000Z',
- '19970619T090000Z',
- '19970626T090000Z',
- '19970703T090000Z',
- '19970710T090000Z',
- '19970717T090000Z',
- '19970724T090000Z',
- '19970731T090000Z',
- '19970807T090000Z',
- '19970814T090000Z',
- '19970821T090000Z',
- '19970828T090000Z',
- '19980604T090000Z',
- '19980611T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'BYDAY' => array('FR'),
- 'BYMONTHDAY' => array(13),
- 'COUNT' => 4,
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19980213T090000Z',
- '19980313T090000Z',
- '19981113T090000Z',
- '19990813T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'BYDAY' => array('SA'),
- 'BYMONTHDAY' => array(7, 8, 9, 10, 11, 12, 13),
- 'COUNT' => 10,
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970913T090000Z',
- '19971011T090000Z',
- '19971108T090000Z',
- '19971213T090000Z',
- '19980110T090000Z',
- '19980207T090000Z',
- '19980307T090000Z',
- '19980411T090000Z',
- '19980509T090000Z',
- '19980613T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'YEARLY',
- 'INTERVAL' => 4,
- 'BYMONTH' => array(11),
- 'BYDAY' => array('TU'),
- 'BYMONTHDAY' => array(2, 3, 4, 5, 6, 7, 8),
- 'COUNT' => 6,
- 'DTSTART' => '19961105T090000Z',
- );
- $expect[] = array(
- '19961105T090000Z',
- '20001107T090000Z',
- '20041102T090000Z',
- '20081104T090000Z',
- '20121106T090000Z',
- '20161108T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'BYDAY' => array('TU', 'WE', 'TH'),
- 'BYSETPOS' => array(3),
- 'COUNT' => 3,
- 'DTSTART' => '19970904T090000Z',
- );
- $expect[] = array(
- '19970904T090000Z',
- '19971007T090000Z',
- '19971106T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MONTHLY',
- 'BYDAY' => array('MO', 'TU', 'WE', 'TH', 'FR'),
- 'BYSETPOS' => array(-2),
- 'COUNT' => 3,
- 'DTSTART' => '19970929T090000Z',
- );
- $expect[] = array(
- '19970929T090000Z',
- '19971030T090000Z',
- '19971127T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'HOURLY',
- 'INTERVAL' => 3,
- 'DTSTART' => '19970929T090000Z',
- 'COUNT' => 3,
- );
- $expect[] = array(
- '19970929T090000Z',
- '19970929T120000Z',
- '19970929T150000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MINUTELY',
- 'INTERVAL' => 15,
- 'COUNT' => 6,
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970902T091500Z',
- '19970902T093000Z',
- '19970902T094500Z',
- '19970902T100000Z',
- '19970902T101500Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'MINUTELY',
- 'INTERVAL' => 90,
- 'COUNT' => 4,
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970902T103000Z',
- '19970902T120000Z',
- '19970902T133000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'WEEKLY',
- 'COUNT' => 10,
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970909T090000Z',
- '19970916T090000Z',
- '19970923T090000Z',
- '19970930T090000Z',
- '19971007T090000Z',
- '19971014T090000Z',
- '19971021T090000Z',
- '19971028T090000Z',
- '19971104T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'WEEKLY',
- 'INTERVAL' => 2,
- 'COUNT' => 6,
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970916T090000Z',
- '19970930T090000Z',
- '19971014T090000Z',
- '19971028T090000Z',
- '19971111T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'WEEKLY',
- 'COUNT' => 10,
- 'WKST' => 'SU',
- 'BYDAY' => array('TU', 'TH'),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970904T090000Z',
- '19970909T090000Z',
- '19970911T090000Z',
- '19970916T090000Z',
- '19970918T090000Z',
- '19970923T090000Z',
- '19970925T090000Z',
- '19970930T090000Z',
- '19971002T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'WEEKLY',
- 'INTERVAL' => 2,
- 'COUNT' => 8,
- 'WKST' => 'SU',
- 'BYDAY' => array('TU', 'TH'),
- 'DTSTART' => '19970902T090000Z',
- );
- $expect[] = array(
- '19970902T090000Z',
- '19970904T090000Z',
- '19970916T090000Z',
- '19970918T090000Z',
- '19970930T090000Z',
- '19971002T090000Z',
- '19971014T090000Z',
- '19971016T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'WEEKLY',
- 'INTERVAL' => 2,
- 'COUNT' => 4,
- 'BYDAY' => array('TU', 'SU'),
- 'WKST' => 'MO',
- 'DTSTART' => '19970805T090000Z',
- );
- $expect[] = array(
- '19970805T090000Z',
- '19970810T090000Z',
- '19970819T090000Z',
- '19970824T090000Z',
- );
-
- $tests[] = array(
- 'FREQ' => 'WEEKLY',
- 'INTERVAL' => 2,
- 'COUNT' => 4,
- 'BYDAY' => array('TU', 'SU'),
- 'WKST' => 'SU',
- 'DTSTART' => '19970805T090000Z',
- );
- $expect[] = array(
- '19970805T090000Z',
- '19970817T090000Z',
- '19970819T090000Z',
- '19970831T090000Z',
- );
-
-
- $this->assertRules(array(), $tests, $expect);
- }
-
-
- private function assertRules(array $defaults, array $tests, array $expect) {
- foreach ($tests as $key => $test) {
- $options = $test + $defaults;
-
- $start = PhutilCalendarAbsoluteDateTime::newFromISO8601(
- $options['DTSTART']);
-
- $rrule = id(new PhutilCalendarRecurrenceRule())
- ->setStartDateTime($start)
- ->setFrequency($options['FREQ']);
-
- $interval = idx($options, 'INTERVAL');
- if ($interval) {
- $rrule->setInterval($interval);
- }
-
- $by_day = idx($options, 'BYDAY');
- if ($by_day) {
- $rrule->setByDay($by_day);
- }
-
- $by_month = idx($options, 'BYMONTH');
- if ($by_month) {
- $rrule->setByMonth($by_month);
- }
-
- $by_monthday = idx($options, 'BYMONTHDAY');
- if ($by_monthday) {
- $rrule->setByMonthDay($by_monthday);
- }
-
- $by_yearday = idx($options, 'BYYEARDAY');
- if ($by_yearday) {
- $rrule->setByYearDay($by_yearday);
- }
-
- $by_weekno = idx($options, 'BYWEEKNO');
- if ($by_weekno) {
- $rrule->setByWeekNumber($by_weekno);
- }
-
- $by_hour = idx($options, 'BYHOUR');
- if ($by_hour) {
- $rrule->setByHour($by_hour);
- }
-
- $by_minute = idx($options, 'BYMINUTE');
- if ($by_minute) {
- $rrule->setByMinute($by_minute);
- }
-
- $by_second = idx($options, 'BYSECOND');
- if ($by_second) {
- $rrule->setBySecond($by_second);
- }
-
- $by_setpos = idx($options, 'BYSETPOS');
- if ($by_setpos) {
- $rrule->setBySetPosition($by_setpos);
- }
-
- $week_start = idx($options, 'WKST');
- if ($week_start) {
- $rrule->setWeekStart($week_start);
- }
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($rrule);
-
- $result = $set->getEventsBetween(null, null, $options['COUNT']);
-
- $this->assertEqual(
- $expect[$key],
- mpull($result, 'getISO8601'));
- }
- }
-
-
-}
diff --git a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php b/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php
deleted file mode 100644
--- a/src/parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php
+++ /dev/null
@@ -1,196 +0,0 @@
-<?php
-
-final class PhutilCalendarRecurrenceTestCase extends PhutilTestCase {
-
- public function testCalendarRecurrenceLists() {
- $set = id(new PhutilCalendarRecurrenceSet());
- $result = $set->getEventsBetween(null, null, 0xFFFF);
- $this->assertEqual(
- array(),
- $result,
- pht('Set with no sources.'));
-
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource(new PhutilCalendarRecurrenceList());
- $result = $set->getEventsBetween(null, null, 0xFFFF);
- $this->assertEqual(
- array(),
- $result,
- pht('Set with empty list source.'));
-
-
- $list = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- );
-
- $source = id(new PhutilCalendarRecurrenceList())
- ->setDates($list);
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($source);
-
- $expect = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- );
-
- $result = $set->getEventsBetween(null, null, 0xFFFF);
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Simple date list.'));
-
- $list_a = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- );
-
- $list_b = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- );
-
- $source_a = id(new PhutilCalendarRecurrenceList())
- ->setDates($list_a);
-
- $source_b = id(new PhutilCalendarRecurrenceList())
- ->setDates($list_b);
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($source_a)
- ->addSource($source_b);
-
- $expect = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- );
-
- $result = $set->getEventsBetween(null, null, 0xFFFF);
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Multiple date lists.'));
-
- $list_a = array(
- // This is Jan 1, 3, 5, 7, 8 and 10, but listed out-of-order.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160105T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160108T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160110T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160107T120000Z'),
- );
-
- $list_b = array(
- // This is Jan 2, 4, 5, 8, but listed out of order.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160104T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160105T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160108T120000Z'),
- );
-
- $list_c = array(
- // We're going to use this as an exception list.
-
- // This is Jan 7 (listed in one other source), 8 (listed in two)
- // and 9 (listed in none).
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160107T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160108T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160109T120000Z'),
- );
-
- $expect = array(
- // From source A.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- // From source B.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- // From source A.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- // From source B.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160104T120000Z'),
- // From source A and B. Should appear only once.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160105T120000Z'),
- // The 6th appears in no source.
- // The 7th, 8th and 9th are excluded.
- // The 10th is from source A.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160110T120000Z'),
- );
-
- $list_a = id(new PhutilCalendarRecurrenceList())
- ->setDates($list_a);
-
- $list_b = id(new PhutilCalendarRecurrenceList())
- ->setDates($list_b);
-
- $list_c = id(new PhutilCalendarRecurrenceList())
- ->setDates($list_c)
- ->setIsExceptionSource(true);
-
- $date_set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($list_b)
- ->addSource($list_c)
- ->addSource($list_a);
-
- $date_set->setViewerTimezone('UTC');
-
- $result = $date_set->getEventsBetween(null, null, 0xFFFF);
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Set of all results in multiple lists with exclusions.'));
-
-
- $expect = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- );
- $result = $date_set->getEventsBetween(null, null, 1);
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Multiple lists, one result.'));
-
- $expect = array(
- 2 => PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- 3 => PhutilCalendarAbsoluteDateTime::newFromISO8601('20160104T120000Z'),
- );
- $result = $date_set->getEventsBetween(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160104T120000Z'));
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Multiple lists, time window.'));
- }
-
- public function testCalendarRecurrenceOffsets() {
- $list = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160101T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120000Z'),
- );
-
- $source = id(new PhutilCalendarRecurrenceList())
- ->setDates($list);
-
- $set = id(new PhutilCalendarRecurrenceSet())
- ->addSource($source);
-
- $t1 = PhutilCalendarAbsoluteDateTime::newFromISO8601('20160102T120001Z');
- $t2 = PhutilCalendarAbsoluteDateTime::newFromISO8601('20160103T120000Z');
-
- $expect = array(
- 2 => $t2,
- );
-
- $result = $set->getEventsBetween($t1, null, 0xFFFF);
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Correct event indexes with start date.'));
- }
-
-}
diff --git a/src/parser/calendar/ics/PhutilICSParser.php b/src/parser/calendar/ics/PhutilICSParser.php
deleted file mode 100644
--- a/src/parser/calendar/ics/PhutilICSParser.php
+++ /dev/null
@@ -1,919 +0,0 @@
-<?php
-
-final class PhutilICSParser extends Phobject {
-
- private $stack;
- private $node;
- private $document;
- private $lines;
- private $cursor;
-
- private $warnings;
-
- const PARSE_MISSING_END = 'missing-end';
- const PARSE_INITIAL_UNFOLD = 'initial-unfold';
- const PARSE_UNEXPECTED_CHILD = 'unexpected-child';
- const PARSE_EXTRA_END = 'extra-end';
- const PARSE_MISMATCHED_SECTIONS = 'mismatched-sections';
- const PARSE_ROOT_PROPERTY = 'root-property';
- const PARSE_BAD_BASE64 = 'bad-base64';
- const PARSE_BAD_BOOLEAN = 'bad-boolean';
- const PARSE_UNEXPECTED_TEXT = 'unexpected-text';
- const PARSE_MALFORMED_DOUBLE_QUOTE = 'malformed-double-quote';
- const PARSE_MALFORMED_PARAMETER_NAME = 'malformed-parameter';
- const PARSE_MALFORMED_PROPERTY = 'malformed-property';
- const PARSE_MISSING_VALUE = 'missing-value';
- const PARSE_UNESCAPED_BACKSLASH = 'unescaped-backslash';
- const PARSE_MULTIPLE_PARAMETERS = 'multiple-parameters';
- const PARSE_EMPTY_DATETIME = 'empty-datetime';
- const PARSE_MANY_DATETIME = 'many-datetime';
- const PARSE_BAD_DATETIME = 'bad-datetime';
- const PARSE_EMPTY_DURATION = 'empty-duration';
- const PARSE_MANY_DURATION = 'many-duration';
- const PARSE_BAD_DURATION = 'bad-duration';
-
- const WARN_TZID_UTC = 'warn-tzid-utc';
- const WARN_TZID_GUESS = 'warn-tzid-guess';
- const WARN_TZID_IGNORED = 'warn-tzid-ignored';
-
- public function parseICSData($data) {
- $this->stack = array();
- $this->node = null;
- $this->cursor = null;
- $this->warnings = array();
-
- $lines = $this->unfoldICSLines($data);
- $this->lines = $lines;
-
- $root = $this->newICSNode('<ROOT>');
- $this->stack[] = $root;
- $this->node = $root;
-
- foreach ($lines as $key => $line) {
- $this->cursor = $key;
- $matches = null;
- if (preg_match('(^BEGIN:(.*)\z)', $line, $matches)) {
- $this->beginParsingNode($matches[1]);
- } else if (preg_match('(^END:(.*)\z)', $line, $matches)) {
- $this->endParsingNode($matches[1]);
- } else {
- if (count($this->stack) < 2) {
- $this->raiseParseFailure(
- self::PARSE_ROOT_PROPERTY,
- pht(
- 'Found unexpected property at ICS document root.'));
- }
- $this->parseICSProperty($line);
- }
- }
-
- if (count($this->stack) > 1) {
- $this->raiseParseFailure(
- self::PARSE_MISSING_END,
- pht(
- 'Expected all "BEGIN:" sections in ICS document to have '.
- 'corresponding "END:" sections.'));
- }
-
- $this->node = null;
- $this->lines = null;
- $this->cursor = null;
-
- return $root;
- }
-
- private function getNode() {
- return $this->node;
- }
-
- private function unfoldICSLines($data) {
- $lines = phutil_split_lines($data, $retain_endings = false);
- $this->lines = $lines;
-
- // ICS files are wrapped at 75 characters, with overlong lines continued
- // on the following line with an initial space or tab. Unwrap all of the
- // lines in the file.
-
- // This unwrapping is specifically byte-oriented, not character oriented,
- // and RFC5545 anticipates that simple implementations may even split UTF8
- // characters in the middle.
-
- $last = null;
- foreach ($lines as $idx => $line) {
- $this->cursor = $idx;
- if (!preg_match('/^[ \t]/', $line)) {
- $last = $idx;
- continue;
- }
-
- if ($last === null) {
- $this->raiseParseFailure(
- self::PARSE_INITIAL_UNFOLD,
- pht(
- 'First line of ICS file begins with a space or tab, but this '.
- 'marks a line which should be unfolded.'));
- }
-
- $lines[$last] = $lines[$last].substr($line, 1);
- unset($lines[$idx]);
- }
-
- return $lines;
- }
-
- private function beginParsingNode($type) {
- $node = $this->getNode();
- $new_node = $this->newICSNode($type);
-
- if ($node instanceof PhutilCalendarContainerNode) {
- $node->appendChild($new_node);
- } else {
- $this->raiseParseFailure(
- self::PARSE_UNEXPECTED_CHILD,
- pht(
- 'Found unexpected node "%s" inside node "%s".',
- $new_node->getAttribute('ics.type'),
- $node->getAttribute('ics.type')));
- }
-
- $this->stack[] = $new_node;
- $this->node = $new_node;
-
- return $this;
- }
-
- private function newICSNode($type) {
- switch ($type) {
- case '<ROOT>':
- $node = new PhutilCalendarRootNode();
- break;
- case 'VCALENDAR':
- $node = new PhutilCalendarDocumentNode();
- break;
- case 'VEVENT':
- $node = new PhutilCalendarEventNode();
- break;
- default:
- $node = new PhutilCalendarRawNode();
- break;
- }
-
- $node->setAttribute('ics.type', $type);
-
- return $node;
- }
-
- private function endParsingNode($type) {
- $node = $this->getNode();
- if ($node instanceof PhutilCalendarRootNode) {
- $this->raiseParseFailure(
- self::PARSE_EXTRA_END,
- pht(
- 'Found unexpected "END" without a "BEGIN".'));
- }
-
- $old_type = $node->getAttribute('ics.type');
- if ($old_type != $type) {
- $this->raiseParseFailure(
- self::PARSE_MISMATCHED_SECTIONS,
- pht(
- 'Found mismatched "BEGIN" ("%s") and "END" ("%s") sections.',
- $old_type,
- $type));
- }
-
- array_pop($this->stack);
- $this->node = last($this->stack);
-
- return $this;
- }
-
- private function parseICSProperty($line) {
- $matches = null;
-
- // Properties begin with an alphanumeric name with no escaping, followed
- // by either a ";" (to begin a list of parameters) or a ":" (to begin
- // the actual field body).
-
- $ok = preg_match('(^([A-Za-z0-9-]+)([;:])(.*)\z)', $line, $matches);
- if (!$ok) {
- $this->raiseParseFailure(
- self::PARSE_MALFORMED_PROPERTY,
- pht(
- 'Found malformed property in ICS document.'));
- }
-
- $name = $matches[1];
- $body = $matches[3];
- $has_parameters = ($matches[2] == ';');
-
- $parameters = array();
- if ($has_parameters) {
- // Parameters are a sensible name, a literal "=", a pile of magic,
- // and then maybe a comma and another parameter.
-
- while (true) {
- // We're going to get the first couple of parts first.
- $ok = preg_match('(^([^=]+)=)', $body, $matches);
- if (!$ok) {
- $this->raiseParseFailure(
- self::PARSE_MALFORMED_PARAMETER_NAME,
- pht(
- 'Found malformed property in ICS document: %s',
- $body));
- }
-
- $param_name = $matches[1];
- $body = substr($body, strlen($matches[0]));
-
- // Now we're going to match zero or more values.
- $param_values = array();
- while (true) {
- // The value can either be a double-quoted string or an unquoted
- // string, with some characters forbidden.
- if (strlen($body) && $body[0] == '"') {
- $is_quoted = true;
- $ok = preg_match(
- '(^"([^\x00-\x08\x10-\x19"]*)")',
- $body,
- $matches);
- if (!$ok) {
- $this->raiseParseFailure(
- self::PARSE_MALFORMED_DOUBLE_QUOTE,
- pht(
- 'Found malformed double-quoted string in ICS document '.
- 'parameter value.'));
- }
- } else {
- $is_quoted = false;
-
- // It's impossible for this not to match since it can match
- // nothing, and it's valid for it to match nothing.
- preg_match('(^([^\x00-\x08\x10-\x19";:,]*))', $body, $matches);
- }
-
- // NOTE: RFC5545 says "Property parameter values that are not in
- // quoted-strings are case-insensitive." -- that is, the quoted and
- // unquoted representations are not equivalent. Thus, preserve the
- // original formatting in case we ever need to respect this.
-
- $param_values[] = array(
- 'value' => $this->unescapeParameterValue($matches[1]),
- 'quoted' => $is_quoted,
- );
-
- $body = substr($body, strlen($matches[0]));
- if (!strlen($body)) {
- $this->raiseParseFailure(
- self::PARSE_MISSING_VALUE,
- pht(
- 'Expected ":" after parameters in ICS document property.'));
- }
-
- // If we have a comma now, we're going to read another value. Strip
- // it off and keep going.
- if ($body[0] == ',') {
- $body = substr($body, 1);
- continue;
- }
-
- // If we have a semicolon, we're going to read another parameter.
- if ($body[0] == ';') {
- break;
- }
-
- // If we have a colon, this is the last value and also the last
- // property. Break, then handle the colon below.
- if ($body[0] == ':') {
- break;
- }
-
- $short_body = id(new PhutilUTF8StringTruncator())
- ->setMaximumGlyphs(32)
- ->truncateString($body);
-
- // We aren't expecting anything else.
- $this->raiseParseFailure(
- self::PARSE_UNEXPECTED_TEXT,
- pht(
- 'Found unexpected text ("%s") after reading parameter value.',
- $short_body));
- }
-
- $parameters[] = array(
- 'name' => $param_name,
- 'values' => $param_values,
- );
-
- if ($body[0] == ';') {
- $body = substr($body, 1);
- continue;
- }
-
- if ($body[0] == ':') {
- $body = substr($body, 1);
- break;
- }
- }
- }
-
- $value = $this->unescapeFieldValue($name, $parameters, $body);
-
- $node = $this->getNode();
-
-
- $raw = $node->getAttribute('ics.properties', array());
- $raw[] = array(
- 'name' => $name,
- 'parameters' => $parameters,
- 'value' => $value,
- );
- $node->setAttribute('ics.properties', $raw);
-
- switch ($node->getAttribute('ics.type')) {
- case 'VEVENT':
- $this->didParseEventProperty($node, $name, $parameters, $value);
- break;
- }
- }
-
- private function unescapeParameterValue($data) {
- // The parameter grammar is adjusted by RFC6868 to permit escaping with
- // carets. Remove that escaping.
-
- // This escaping is a bit weird because it's trying to be backwards
- // compatible and the original spec didn't think about this and didn't
- // provide much room to fix things.
-
- $out = '';
- $esc = false;
- foreach (phutil_utf8v($data) as $c) {
- if (!$esc) {
- if ($c != '^') {
- $out .= $c;
- } else {
- $esc = true;
- }
- } else {
- switch ($c) {
- case 'n':
- $out .= "\n";
- break;
- case '^':
- $out .= '^';
- break;
- case "'":
- // NOTE: This is "<caret> <single quote>" being decoded into a
- // double quote!
- $out .= '"';
- break;
- default:
- // NOTE: The caret is NOT an escape for any other characters.
- // This is a "MUST" requirement of RFC6868.
- $out .= '^'.$c;
- break;
- }
- }
- }
-
- // NOTE: Because caret on its own just means "caret" for backward
- // compatibility, we don't warn if we're still in escaped mode once we
- // reach the end of the string.
-
- return $out;
- }
-
- private function unescapeFieldValue($name, array $parameters, $data) {
- // NOTE: The encoding of the field value data is dependent on the field
- // name (which defines a default encoding) and the parameters (which may
- // include "VALUE", specifying a type of the data.
-
- $default_types = array(
- 'CALSCALE' => 'TEXT',
- 'METHOD' => 'TEXT',
- 'PRODID' => 'TEXT',
- 'VERSION' => 'TEXT',
-
- 'ATTACH' => 'URI',
- 'CATEGORIES' => 'TEXT',
- 'CLASS' => 'TEXT',
- 'COMMENT' => 'TEXT',
- 'DESCRIPTION' => 'TEXT',
-
- // TODO: The spec appears to contradict itself: it says that the value
- // type is FLOAT, but it also says that this property value is actually
- // two semicolon-separated values, which is not what FLOAT is defined as.
- 'GEO' => 'TEXT',
-
- 'LOCATION' => 'TEXT',
- 'PERCENT-COMPLETE' => 'INTEGER',
- 'PRIORITY' => 'INTEGER',
- 'RESOURCES' => 'TEXT',
- 'STATUS' => 'TEXT',
- 'SUMMARY' => 'TEXT',
-
- 'COMPLETED' => 'DATE-TIME',
- 'DTEND' => 'DATE-TIME',
- 'DUE' => 'DATE-TIME',
- 'DTSTART' => 'DATE-TIME',
- 'DURATION' => 'DURATION',
- 'FREEBUSY' => 'PERIOD',
- 'TRANSP' => 'TEXT',
-
- 'TZID' => 'TEXT',
- 'TZNAME' => 'TEXT',
- 'TZOFFSETFROM' => 'UTC-OFFSET',
- 'TZOFFSETTO' => 'UTC-OFFSET',
- 'TZURL' => 'URI',
-
- 'ATTENDEE' => 'CAL-ADDRESS',
- 'CONTACT' => 'TEXT',
- 'ORGANIZER' => 'CAL-ADDRESS',
- 'RECURRENCE-ID' => 'DATE-TIME',
- 'RELATED-TO' => 'TEXT',
- 'URL' => 'URI',
- 'UID' => 'TEXT',
- 'EXDATE' => 'DATE-TIME',
- 'RDATE' => 'DATE-TIME',
- 'RRULE' => 'RECUR',
-
- 'ACTION' => 'TEXT',
- 'REPEAT' => 'INTEGER',
- 'TRIGGER' => 'DURATION',
-
- 'CREATED' => 'DATE-TIME',
- 'DTSTAMP' => 'DATE-TIME',
- 'LAST-MODIFIED' => 'DATE-TIME',
- 'SEQUENCE' => 'INTEGER',
-
- 'REQUEST-STATUS' => 'TEXT',
- );
-
- $value_type = idx($default_types, $name, 'TEXT');
-
- foreach ($parameters as $parameter) {
- if ($parameter['name'] == 'VALUE') {
- $value_type = idx(head($parameter['values']), 'value');
- }
- }
-
- switch ($value_type) {
- case 'BINARY':
- $result = base64_decode($data, true);
- if ($result === false) {
- $this->raiseParseFailure(
- self::PARSE_BAD_BASE64,
- pht(
- 'Unable to decode base64 data: %s',
- $data));
- }
- break;
- case 'BOOLEAN':
- $map = array(
- 'true' => true,
- 'false' => false,
- );
- $result = phutil_utf8_strtolower($data);
- if (!isset($map[$result])) {
- $this->raiseParseFailure(
- self::PARSE_BAD_BOOLEAN,
- pht(
- 'Unexpected BOOLEAN value "%s".',
- $data));
- }
- $result = $map[$result];
- break;
- case 'CAL-ADDRESS':
- $result = $data;
- break;
- case 'DATE':
- // This is a comma-separated list of "YYYYMMDD" values.
- $result = explode(',', $data);
- break;
- case 'DATE-TIME':
- if (!strlen($data)) {
- $result = array();
- } else {
- $result = explode(',', $data);
- }
- break;
- case 'DURATION':
- if (!strlen($data)) {
- $result = array();
- } else {
- $result = explode(',', $data);
- }
- break;
- case 'FLOAT':
- $result = explode(',', $data);
- foreach ($result as $k => $v) {
- $result[$k] = (float)$v;
- }
- break;
- case 'INTEGER':
- $result = explode(',', $data);
- foreach ($result as $k => $v) {
- $result[$k] = (int)$v;
- }
- break;
- case 'PERIOD':
- $result = explode(',', $data);
- break;
- case 'RECUR':
- $result = $data;
- break;
- case 'TEXT':
- $result = $this->unescapeTextValue($data);
- break;
- case 'TIME':
- $result = explode(',', $data);
- break;
- case 'URI':
- $result = $data;
- break;
- case 'UTC-OFFSET':
- $result = $data;
- break;
- default:
- // RFC5545 says we MUST preserve the data for any types we don't
- // recognize.
- $result = $data;
- break;
- }
-
- return array(
- 'type' => $value_type,
- 'value' => $result,
- 'raw' => $data,
- );
- }
-
- private function unescapeTextValue($data) {
- $result = array();
-
- $buf = '';
- $esc = false;
- foreach (phutil_utf8v($data) as $c) {
- if (!$esc) {
- if ($c == '\\') {
- $esc = true;
- } else if ($c == ',') {
- $result[] = $buf;
- $buf = '';
- } else {
- $buf .= $c;
- }
- } else {
- switch ($c) {
- case 'n':
- case 'N':
- $buf .= "\n";
- break;
- default:
- $buf .= $c;
- break;
- }
- $esc = false;
- }
- }
-
- if ($esc) {
- $this->raiseParseFailure(
- self::PARSE_UNESCAPED_BACKSLASH,
- pht(
- 'ICS document contains TEXT value ending with unescaped '.
- 'backslash.'));
- }
-
- $result[] = $buf;
-
- return $result;
- }
-
- private function raiseParseFailure($code, $message) {
- if ($this->lines && isset($this->lines[$this->cursor])) {
- $message = pht(
- "ICS Parse Error near line %s:\n\n>>> %s\n\n%s",
- $this->cursor + 1,
- $this->lines[$this->cursor],
- $message);
- } else {
- $message = pht(
- 'ICS Parse Error: %s',
- $message);
- }
-
- throw id(new PhutilICSParserException($message))
- ->setParserFailureCode($code);
- }
-
- private function raiseWarning($code, $message) {
- $this->warnings[] = array(
- 'code' => $code,
- 'line' => $this->cursor,
- 'text' => $this->lines[$this->cursor],
- 'message' => $message,
- );
-
- return $this;
- }
-
- public function getWarnings() {
- return $this->warnings;
- }
-
- private function didParseEventProperty(
- PhutilCalendarEventNode $node,
- $name,
- array $parameters,
- array $value) {
-
- switch ($name) {
- case 'UID':
- $text = $this->newTextFromProperty($parameters, $value);
- $node->setUID($text);
- break;
- case 'CREATED':
- $datetime = $this->newDateTimeFromProperty($parameters, $value);
- $node->setCreatedDateTime($datetime);
- break;
- case 'DTSTAMP':
- $datetime = $this->newDateTimeFromProperty($parameters, $value);
- $node->setModifiedDateTime($datetime);
- break;
- case 'SUMMARY':
- $text = $this->newTextFromProperty($parameters, $value);
- $node->setName($text);
- break;
- case 'DESCRIPTION':
- $text = $this->newTextFromProperty($parameters, $value);
- $node->setDescription($text);
- break;
- case 'DTSTART':
- $datetime = $this->newDateTimeFromProperty($parameters, $value);
- $node->setStartDateTime($datetime);
- break;
- case 'DTEND':
- $datetime = $this->newDateTimeFromProperty($parameters, $value);
- $node->setEndDateTime($datetime);
- break;
- case 'DURATION':
- $duration = $this->newDurationFromProperty($parameters, $value);
- $node->setDuration($duration);
- break;
- case 'RRULE':
- $rrule = $this->newRecurrenceRuleFromProperty($parameters, $value);
- $node->setRecurrenceRule($rrule);
- break;
- case 'RECURRENCE-ID':
- $text = $this->newTextFromProperty($parameters, $value);
- $node->setRecurrenceID($text);
- break;
- case 'ATTENDEE':
- $attendee = $this->newAttendeeFromProperty($parameters, $value);
- $node->addAttendee($attendee);
- break;
- }
-
- }
-
- private function newTextFromProperty(array $parameters, array $value) {
- $value = $value['value'];
- return implode("\n\n", $value);
- }
-
- private function newAttendeeFromProperty(array $parameters, array $value) {
- $uri = $value['value'];
-
- switch (idx($parameters, 'PARTSTAT')) {
- case 'ACCEPTED':
- $status = PhutilCalendarUserNode::STATUS_ACCEPTED;
- break;
- case 'DECLINED':
- $status = PhutilCalendarUserNode::STATUS_DECLINED;
- break;
- case 'NEEDS-ACTION':
- default:
- $status = PhutilCalendarUserNode::STATUS_INVITED;
- break;
- }
-
- $name = $this->getScalarParameterValue($parameters, 'CN');
-
- return id(new PhutilCalendarUserNode())
- ->setURI($uri)
- ->setName($name)
- ->setStatus($status);
- }
-
- private function newDateTimeFromProperty(array $parameters, array $value) {
- $value = $value['value'];
-
- if (!$value) {
- $this->raiseParseFailure(
- self::PARSE_EMPTY_DATETIME,
- pht(
- 'Expected DATE-TIME to have exactly one value, found none.'));
-
- }
-
- if (count($value) > 1) {
- $this->raiseParseFailure(
- self::PARSE_MANY_DATETIME,
- pht(
- 'Expected DATE-TIME to have exactly one value, found more than '.
- 'one.'));
- }
-
- $value = head($value);
- $tzid = $this->getScalarParameterValue($parameters, 'TZID');
-
- if (preg_match('/Z\z/', $value)) {
- if ($tzid) {
- $this->raiseWarning(
- self::WARN_TZID_UTC,
- pht(
- 'DATE-TIME "%s" uses "Z" to specify UTC, but also has a TZID '.
- 'parameter with value "%s". This violates RFC5545. The TZID '.
- 'will be ignored, and the value will be interpreted as UTC.',
- $value,
- $tzid));
- }
- $tzid = 'UTC';
- } else if ($tzid !== null) {
- $tzid = $this->guessTimezone($tzid);
- }
-
- try {
- $datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601(
- $value,
- $tzid);
- } catch (Exception $ex) {
- $this->raiseParseFailure(
- self::PARSE_BAD_DATETIME,
- pht(
- 'Error parsing DATE-TIME: %s',
- $ex->getMessage()));
- }
-
- return $datetime;
- }
-
- private function newDurationFromProperty(array $parameters, array $value) {
- $value = $value['value'];
-
- if (!$value) {
- $this->raiseParseFailure(
- self::PARSE_EMPTY_DURATION,
- pht(
- 'Expected DURATION to have exactly one value, found none.'));
-
- }
-
- if (count($value) > 1) {
- $this->raiseParseFailure(
- self::PARSE_MANY_DURATION,
- pht(
- 'Expected DURATION to have exactly one value, found more than '.
- 'one.'));
- }
-
- $value = head($value);
-
- try {
- $duration = PhutilCalendarDuration::newFromISO8601($value);
- } catch (Exception $ex) {
- $this->raiseParseFailure(
- self::PARSE_BAD_DURATION,
- pht(
- 'Invalid DURATION: %s',
- $ex->getMessage()));
- }
-
- return $duration;
- }
-
- private function newRecurrenceRuleFromProperty(array $parameters, $value) {
- return PhutilCalendarRecurrenceRule::newFromRRULE($value['value']);
- }
-
- private function getScalarParameterValue(
- array $parameters,
- $name,
- $default = null) {
-
- $match = null;
- foreach ($parameters as $parameter) {
- if ($parameter['name'] == $name) {
- $match = $parameter;
- }
- }
-
- if ($match === null) {
- return $default;
- }
-
- $value = $match['values'];
- if (!$value) {
- // Parameter is specified, but with no value, like "KEY=". Just return
- // the default, as though the parameter was not specified.
- return $default;
- }
-
- if (count($value) > 1) {
- $this->raiseParseFailure(
- self::PARSE_MULTIPLE_PARAMETERS,
- pht(
- 'Expected parameter "%s" to have at most one value, but found '.
- 'more than one.',
- $name));
- }
-
- return idx(head($value), 'value');
- }
-
- private function guessTimezone($tzid) {
- $map = DateTimeZone::listIdentifiers();
- $map = array_fuse($map);
- if (isset($map[$tzid])) {
- // This is a real timezone we recognize, so just use it as provided.
- return $tzid;
- }
-
- // These are alternate names for timezones.
- static $aliases;
-
- if ($aliases === null) {
- $aliases = array(
- 'Etc/GMT' => 'UTC',
- );
-
- // Load the map of Windows timezones.
- $root_path = dirname(phutil_get_library_root('phutil'));
- $windows_path = $root_path.'/resources/timezones/windows_timezones.json';
- $windows_data = Filesystem::readFile($windows_path);
- $windows_zones = phutil_json_decode($windows_data);
-
- $aliases = $aliases + $windows_zones;
- }
-
- if (isset($aliases[$tzid])) {
- return $aliases[$tzid];
- }
-
- // Look for something that looks like "UTC+3" or "GMT -05.00". If we find
- // anything, pick a timezone with that offset.
- $offset_pattern =
- '/'.
- '(?:UTC|GMT)'.
- '\s*'.
- '(?P<sign>[+-])'.
- '\s*'.
- '(?P<h>\d+)'.
- '(?:'.
- '[:.](?P<m>\d+)'.
- ')?'.
- '/i';
-
- $matches = null;
- if (preg_match($offset_pattern, $tzid, $matches)) {
- $hours = (int)$matches['h'];
- $minutes = (int)idx($matches, 'm');
- $offset = ($hours * 60 * 60) + ($minutes * 60);
-
- if (idx($matches, 'sign') == '-') {
- $offset = -$offset;
- }
-
- // NOTE: We could possibly do better than this, by using the event start
- // time to guess a timezone. However, that won't work for recurring
- // events and would require us to do this work after finishing initial
- // parsing. Since these unusual offset-based timezones appear to be rare,
- // the benefit may not be worth the complexity.
- $now = new DateTime('@'.time());
-
- foreach ($map as $identifier) {
- $zone = new DateTimeZone($identifier);
- if ($zone->getOffset($now) == $offset) {
- $this->raiseWarning(
- self::WARN_TZID_GUESS,
- pht(
- 'TZID "%s" is unknown, guessing "%s" based on pattern "%s".',
- $tzid,
- $identifier,
- $matches[0]));
- return $identifier;
- }
- }
- }
-
- $this->raiseWarning(
- self::WARN_TZID_IGNORED,
- pht(
- 'TZID "%s" is unknown, using UTC instead.',
- $tzid));
-
- return 'UTC';
- }
-
-}
diff --git a/src/parser/calendar/ics/PhutilICSParserException.php b/src/parser/calendar/ics/PhutilICSParserException.php
deleted file mode 100644
--- a/src/parser/calendar/ics/PhutilICSParserException.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-final class PhutilICSParserException extends Exception {
-
- private $parserFailureCode;
-
- public function setParserFailureCode($code) {
- $this->parserFailureCode = $code;
- return $this;
- }
-
- public function getParserFailureCode() {
- return $this->parserFailureCode;
- }
-
-}
diff --git a/src/parser/calendar/ics/PhutilICSWriter.php b/src/parser/calendar/ics/PhutilICSWriter.php
deleted file mode 100644
--- a/src/parser/calendar/ics/PhutilICSWriter.php
+++ /dev/null
@@ -1,387 +0,0 @@
-<?php
-
-final class PhutilICSWriter extends Phobject {
-
- public function writeICSDocument(PhutilCalendarRootNode $node) {
- $out = array();
-
- foreach ($node->getChildren() as $child) {
- $out[] = $this->writeNode($child);
- }
-
- return implode('', $out);
- }
-
- private function writeNode(PhutilCalendarNode $node) {
- if (!$this->getICSNodeType($node)) {
- return null;
- }
-
- $out = array();
-
- $out[] = $this->writeBeginNode($node);
- $out[] = $this->writeNodeProperties($node);
-
- if ($node instanceof PhutilCalendarContainerNode) {
- foreach ($node->getChildren() as $child) {
- $out[] = $this->writeNode($child);
- }
- }
-
- $out[] = $this->writeEndNode($node);
-
- return implode('', $out);
- }
-
- private function writeBeginNode(PhutilCalendarNode $node) {
- $type = $this->getICSNodeType($node);
- return $this->wrapICSLine("BEGIN:{$type}");
- }
-
- private function writeEndNode(PhutilCalendarNode $node) {
- $type = $this->getICSNodeType($node);
- return $this->wrapICSLine("END:{$type}");
- }
-
- private function writeNodeProperties(PhutilCalendarNode $node) {
- $properties = $this->getNodeProperties($node);
-
- $out = array();
- foreach ($properties as $property) {
- $propname = $property['name'];
- $propvalue = $property['value'];
-
- $propline = array();
- $propline[] = $propname;
-
- foreach ($property['parameters'] as $parameter) {
- $paramname = $parameter['name'];
- $paramvalue = $parameter['value'];
- $propline[] = ";{$paramname}={$paramvalue}";
- }
-
- $propline[] = ":{$propvalue}";
- $propline = implode('', $propline);
-
- $out[] = $this->wrapICSLine($propline);
- }
-
- return implode('', $out);
- }
-
- private function getICSNodeType(PhutilCalendarNode $node) {
- switch ($node->getNodeType()) {
- case PhutilCalendarDocumentNode::NODETYPE:
- return 'VCALENDAR';
- case PhutilCalendarEventNode::NODETYPE:
- return 'VEVENT';
- default:
- return null;
- }
- }
-
- private function wrapICSLine($line) {
- $out = array();
- $buf = '';
-
- // NOTE: The line may contain sequences of combining characters which are
- // more than 80 bytes in length. If it does, we'll split them in the
- // middle of the sequence. This is okay and generally anticipated by
- // RFC5545, which even allows implementations to split multibyte
- // characters. The sequence will be stitched back together properly by
- // whatever is parsing things.
-
- foreach (phutil_utf8v($line) as $character) {
- // If adding this character would bring the line over 75 bytes, start
- // a new line.
- if (strlen($buf) + strlen($character) > 75) {
- $out[] = $buf."\r\n";
- $buf = ' ';
- }
-
- $buf .= $character;
- }
-
- $out[] = $buf."\r\n";
-
- return implode('', $out);
- }
-
- private function getNodeProperties(PhutilCalendarNode $node) {
- switch ($node->getNodeType()) {
- case PhutilCalendarDocumentNode::NODETYPE:
- return $this->getDocumentNodeProperties($node);
- case PhutilCalendarEventNode::NODETYPE:
- return $this->getEventNodeProperties($node);
- default:
- return array();
- }
- }
-
- private function getDocumentNodeProperties(
- PhutilCalendarDocumentNode $event) {
- $properties = array();
-
- $properties[] = $this->newTextProperty(
- 'VERSION',
- '2.0');
-
- $properties[] = $this->newTextProperty(
- 'PRODID',
- '-//Phacility//Phabricator//EN');
-
- return $properties;
- }
-
- private function getEventNodeProperties(PhutilCalendarEventNode $event) {
- $properties = array();
-
- $uid = $event->getUID();
- if (!strlen($uid)) {
- throw new Exception(
- pht(
- 'Unable to write ICS document: event has no UID, but each event '.
- 'MUST have a UID.'));
- }
- $properties[] = $this->newTextProperty(
- 'UID',
- $uid);
-
- $created = $event->getCreatedDateTime();
- if ($created) {
- $properties[] = $this->newDateTimeProperty(
- 'CREATED',
- $event->getCreatedDateTime());
- }
-
- $dtstamp = $event->getModifiedDateTime();
- if (!$dtstamp) {
- throw new Exception(
- pht(
- 'Unable to write ICS document: event has no modified time, but '.
- 'each event MUST have a modified time.'));
- }
- $properties[] = $this->newDateTimeProperty(
- 'DTSTAMP',
- $dtstamp);
-
- $dtstart = $event->getStartDateTime();
- if ($dtstart) {
- $properties[] = $this->newDateTimeProperty(
- 'DTSTART',
- $dtstart);
- }
-
- $dtend = $event->getEndDateTime();
- if ($dtend) {
- $properties[] = $this->newDateTimeProperty(
- 'DTEND',
- $event->getEndDateTime());
- }
-
- $name = $event->getName();
- if (strlen($name)) {
- $properties[] = $this->newTextProperty(
- 'SUMMARY',
- $name);
- }
-
- $description = $event->getDescription();
- if (strlen($description)) {
- $properties[] = $this->newTextProperty(
- 'DESCRIPTION',
- $description);
- }
-
- $organizer = $event->getOrganizer();
- if ($organizer) {
- $properties[] = $this->newUserProperty(
- 'ORGANIZER',
- $organizer);
- }
-
- $attendees = $event->getAttendees();
- if ($attendees) {
- foreach ($attendees as $attendee) {
- $properties[] = $this->newUserProperty(
- 'ATTENDEE',
- $attendee);
- }
- }
-
- $rrule = $event->getRecurrenceRule();
- if ($rrule) {
- $properties[] = $this->newRRULEProperty(
- 'RRULE',
- $rrule);
- }
-
- $recurrence_id = $event->getRecurrenceID();
- if ($recurrence_id) {
- $properties[] = $this->newTextProperty(
- 'RECURRENCE-ID',
- $recurrence_id);
- }
-
- $exdates = $event->getRecurrenceExceptions();
- if ($exdates) {
- $properties[] = $this->newDateTimesProperty(
- 'EXDATE',
- $exdates);
- }
-
- $rdates = $event->getRecurrenceDates();
- if ($rdates) {
- $properties[] = $this->newDateTimesProperty(
- 'RDATE',
- $rdates);
- }
-
- return $properties;
- }
-
- private function newTextProperty(
- $name,
- $value,
- array $parameters = array()) {
-
- $map = array(
- '\\' => '\\\\',
- ',' => '\\,',
- "\n" => '\\n',
- );
-
- $value = (array)$value;
- foreach ($value as $k => $v) {
- $v = str_replace(array_keys($map), array_values($map), $v);
- $value[$k] = $v;
- }
-
- $value = implode(',', $value);
-
- return $this->newProperty($name, $value, $parameters);
- }
-
- private function newDateTimeProperty(
- $name,
- PhutilCalendarDateTime $value,
- array $parameters = array()) {
-
- return $this->newDateTimesProperty($name, array($value), $parameters);
- }
-
- private function newDateTimesProperty(
- $name,
- array $values,
- array $parameters = array()) {
- assert_instances_of($values, 'PhutilCalendarDateTime');
-
- if (head($values)->getIsAllDay()) {
- $parameters[] = array(
- 'name' => 'VALUE',
- 'values' => array(
- 'DATE',
- ),
- );
- }
-
- $datetimes = array();
- foreach ($values as $value) {
- $datetimes[] = $value->getISO8601();
- }
- $datetimes = implode(';', $datetimes);
-
- return $this->newProperty($name, $datetimes, $parameters);
- }
-
- private function newUserProperty(
- $name,
- PhutilCalendarUserNode $value,
- array $parameters = array()) {
-
- $parameters[] = array(
- 'name' => 'CN',
- 'values' => array(
- $value->getName(),
- ),
- );
-
- $partstat = null;
- switch ($value->getStatus()) {
- case PhutilCalendarUserNode::STATUS_INVITED:
- $partstat = 'NEEDS-ACTION';
- break;
- case PhutilCalendarUserNode::STATUS_ACCEPTED:
- $partstat = 'ACCEPTED';
- break;
- case PhutilCalendarUserNode::STATUS_DECLINED:
- $partstat = 'DECLINED';
- break;
- }
-
- if ($partstat !== null) {
- $parameters[] = array(
- 'name' => 'PARTSTAT',
- 'values' => array(
- $partstat,
- ),
- );
- }
-
- // TODO: We could reasonably fill in "ROLE" and "RSVP" here too, but it
- // isn't clear if these are important to external programs or not.
-
- return $this->newProperty($name, $value->getURI(), $parameters);
- }
-
- private function newRRULEProperty(
- $name,
- PhutilCalendarRecurrenceRule $rule,
- array $parameters = array()) {
-
- $value = $rule->toRRULE();
- return $this->newProperty($name, $value, $parameters);
- }
-
- private function newProperty(
- $name,
- $value,
- array $parameters = array()) {
-
- $map = array(
- '^' => '^^',
- "\n" => '^n',
- '"' => "^'",
- );
-
- $writable_params = array();
- foreach ($parameters as $k => $parameter) {
- $value_list = array();
- foreach ($parameter['values'] as $v) {
- $v = str_replace(array_keys($map), array_values($map), $v);
-
- // If the parameter value isn't a very simple one, quote it.
-
- // RFC5545 says that we MUST quote it if it has a colon, a semicolon,
- // or a comma, and that we MUST quote it if it's a URI.
- if (!preg_match('/^[A-Za-z0-9-]*\z/', $v)) {
- $v = '"'.$v.'"';
- }
-
- $value_list[] = $v;
- }
-
- $writable_params[] = array(
- 'name' => $parameter['name'],
- 'value' => implode(',', $value_list),
- );
- }
-
- return array(
- 'name' => $name,
- 'value' => $value,
- 'parameters' => $writable_params,
- );
- }
-
-}
diff --git a/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php b/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/PhutilICSParserTestCase.php
+++ /dev/null
@@ -1,341 +0,0 @@
-<?php
-
-final class PhutilICSParserTestCase extends PhutilTestCase {
-
- public function testICSParser() {
- $event = $this->parseICSSingleEvent('simple.ics');
-
- $this->assertEqual(
- array(
- array(
- 'name' => 'CREATED',
- 'parameters' => array(),
- 'value' => array(
- 'type' => 'DATE-TIME',
- 'value' => array(
- '20160908T172702Z',
- ),
- 'raw' => '20160908T172702Z',
- ),
- ),
- array(
- 'name' => 'UID',
- 'parameters' => array(),
- 'value' => array(
- 'type' => 'TEXT',
- 'value' => array(
- '1CEB57AF-0C9C-402D-B3BD-D75BD4843F68',
- ),
- 'raw' => '1CEB57AF-0C9C-402D-B3BD-D75BD4843F68',
- ),
- ),
- array(
- 'name' => 'DTSTART',
- 'parameters' => array(
- array(
- 'name' => 'TZID',
- 'values' => array(
- array(
- 'value' => 'America/Los_Angeles',
- 'quoted' => false,
- ),
- ),
- ),
- ),
- 'value' => array(
- 'type' => 'DATE-TIME',
- 'value' => array(
- '20160915T090000',
- ),
- 'raw' => '20160915T090000',
- ),
- ),
- array(
- 'name' => 'DTEND',
- 'parameters' => array(
- array(
- 'name' => 'TZID',
- 'values' => array(
- array(
- 'value' => 'America/Los_Angeles',
- 'quoted' => false,
- ),
- ),
- ),
- ),
- 'value' => array(
- 'type' => 'DATE-TIME',
- 'value' => array(
- '20160915T100000',
- ),
- 'raw' => '20160915T100000',
- ),
- ),
- array(
- 'name' => 'SUMMARY',
- 'parameters' => array(),
- 'value' => array(
- 'type' => 'TEXT',
- 'value' => array(
- 'Simple Event',
- ),
- 'raw' => 'Simple Event',
- ),
- ),
- array(
- 'name' => 'DESCRIPTION',
- 'parameters' => array(),
- 'value' => array(
- 'type' => 'TEXT',
- 'value' => array(
- 'This is a simple event.',
- ),
- 'raw' => 'This is a simple event.',
- ),
- ),
- ),
- $event->getAttribute('ics.properties'));
-
- $this->assertEqual(
- 'Simple Event',
- $event->getName());
-
- $this->assertEqual(
- 'This is a simple event.',
- $event->getDescription());
-
- $this->assertEqual(
- 1473955200,
- $event->getStartDateTime()->getEpoch());
-
- $this->assertEqual(
- 1473955200 + phutil_units('1 hour in seconds'),
- $event->getEndDateTime()->getEpoch());
- }
-
- public function testICSOddTimezone() {
- $event = $this->parseICSSingleEvent('zimbra-timezone.ics');
-
- $start = $event->getStartDateTime();
-
- $this->assertEqual(
- '20170303T140000Z',
- $start->getISO8601());
- }
-
- public function testICSFloatingTime() {
- // This tests "floating" event times, which have no absolute time and are
- // supposed to be interpreted using the viewer's timezone. It also uses
- // a duration, and the duration needs to float along with the viewer
- // timezone.
-
- $event = $this->parseICSSingleEvent('floating.ics');
-
- $start = $event->getStartDateTime();
-
- $caught = null;
- try {
- $start->getEpoch();
- } catch (Exception $ex) {
- $caught = $ex;
- }
-
- $this->assertTrue(
- ($caught instanceof Exception),
- pht('Expected exception for floating time with no viewer timezone.'));
-
- $newyears_utc = strtotime('2015-01-01 00:00:00 UTC');
- $this->assertEqual(1420070400, $newyears_utc);
-
- $start->setViewerTimezone('UTC');
- $this->assertEqual(
- $newyears_utc,
- $start->getEpoch());
-
- $start->setViewerTimezone('America/Los_Angeles');
- $this->assertEqual(
- $newyears_utc + phutil_units('8 hours in seconds'),
- $start->getEpoch());
-
- $start->setViewerTimezone('America/New_York');
- $this->assertEqual(
- $newyears_utc + phutil_units('5 hours in seconds'),
- $start->getEpoch());
-
- $end = $event->getEndDateTime();
- $end->setViewerTimezone('UTC');
- $this->assertEqual(
- $newyears_utc + phutil_units('24 hours in seconds'),
- $end->getEpoch());
-
- $end->setViewerTimezone('America/Los_Angeles');
- $this->assertEqual(
- $newyears_utc + phutil_units('32 hours in seconds'),
- $end->getEpoch());
-
- $end->setViewerTimezone('America/New_York');
- $this->assertEqual(
- $newyears_utc + phutil_units('29 hours in seconds'),
- $end->getEpoch());
- }
-
- public function testICSVALARM() {
- $event = $this->parseICSSingleEvent('valarm.ics');
-
- // For now, we parse but ignore VALARM sections. This test just makes
- // sure they survive parsing.
-
- $start_epoch = strtotime('2016-10-19 22:00:00 UTC');
- $this->assertEqual(1476914400, $start_epoch);
-
- $this->assertEqual(
- $start_epoch,
- $event->getStartDateTime()->getEpoch());
- }
-
- public function testICSDuration() {
- $event = $this->parseICSSingleEvent('duration.ics');
-
- // Raw value is "20160719T095722Z".
- $start_epoch = strtotime('2016-07-19 09:57:22 UTC');
- $this->assertEqual(1468922242, $start_epoch);
-
- // Raw value is "P1DT17H4M23S".
- $duration =
- phutil_units('1 day in seconds') +
- phutil_units('17 hours in seconds') +
- phutil_units('4 minutes in seconds') +
- phutil_units('23 seconds in seconds');
-
- $this->assertEqual(
- $start_epoch,
- $event->getStartDateTime()->getEpoch());
-
- $this->assertEqual(
- $start_epoch + $duration,
- $event->getEndDateTime()->getEpoch());
- }
-
- public function testICSWeeklyEvent() {
- $event = $this->parseICSSingleEvent('weekly.ics');
-
- $start = $event->getStartDateTime();
- $start->setViewerTimezone('UTC');
-
- $rrule = $event->getRecurrenceRule()
- ->setStartDateTime($start);
-
- $rset = id(new PhutilCalendarRecurrenceSet())
- ->addSource($rrule);
-
- $result = $rset->getEventsBetween(null, null, 3);
-
- $expect = array(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20150811'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20150818'),
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20150825'),
- );
-
- $this->assertEqual(
- mpull($expect, 'getISO8601'),
- mpull($result, 'getISO8601'),
- pht('Weekly recurring event.'));
- }
-
- public function testICSParserErrors() {
- $map = array(
- 'err-missing-end.ics' => PhutilICSParser::PARSE_MISSING_END,
- 'err-bad-base64.ics' => PhutilICSParser::PARSE_BAD_BASE64,
- 'err-bad-boolean.ics' => PhutilICSParser::PARSE_BAD_BOOLEAN,
- 'err-extra-end.ics' => PhutilICSParser::PARSE_EXTRA_END,
- 'err-initial-unfold.ics' => PhutilICSParser::PARSE_INITIAL_UNFOLD,
- 'err-malformed-double-quote.ics' =>
- PhutilICSParser::PARSE_MALFORMED_DOUBLE_QUOTE,
- 'err-malformed-parameter.ics' =>
- PhutilICSParser::PARSE_MALFORMED_PARAMETER_NAME,
- 'err-malformed-property.ics' =>
- PhutilICSParser::PARSE_MALFORMED_PROPERTY,
- 'err-missing-value.ics' => PhutilICSParser::PARSE_MISSING_VALUE,
- 'err-mixmatched-sections.ics' =>
- PhutilICSParser::PARSE_MISMATCHED_SECTIONS,
- 'err-root-property.ics' => PhutilICSParser::PARSE_ROOT_PROPERTY,
- 'err-unescaped-backslash.ics' =>
- PhutilICSParser::PARSE_UNESCAPED_BACKSLASH,
- 'err-unexpected-text.ics' => PhutilICSParser::PARSE_UNEXPECTED_TEXT,
- 'err-multiple-parameters.ics' =>
- PhutilICSParser::PARSE_MULTIPLE_PARAMETERS,
- 'err-empty-datetime.ics' =>
- PhutilICSParser::PARSE_EMPTY_DATETIME,
- 'err-many-datetime.ics' =>
- PhutilICSParser::PARSE_MANY_DATETIME,
- 'err-bad-datetime.ics' =>
- PhutilICSParser::PARSE_BAD_DATETIME,
- 'err-empty-duration.ics' =>
- PhutilICSParser::PARSE_EMPTY_DURATION,
- 'err-many-duration.ics' =>
- PhutilICSParser::PARSE_MANY_DURATION,
- 'err-bad-duration.ics' =>
- PhutilICSParser::PARSE_BAD_DURATION,
-
- 'simple.ics' => null,
- 'good-boolean.ics' => null,
- 'multiple-vcalendars.ics' => null,
- );
-
- foreach ($map as $test_file => $expect) {
- $caught = null;
- try {
- $this->parseICSDocument($test_file);
- } catch (PhutilICSParserException $ex) {
- $caught = $ex;
- }
-
- if ($expect === null) {
- $this->assertTrue(
- ($caught === null),
- pht(
- 'Expected no exception parsing "%s", got: %s',
- $test_file,
- (string)$ex));
- } else {
- if ($caught) {
- $code = $ex->getParserFailureCode();
- $explain = pht(
- 'Expected one exception parsing "%s", got a different '.
- 'one: %s',
- $test_file,
- (string)$ex);
- } else {
- $code = null;
- $explain = pht(
- 'Expected exception parsing "%s", got none.',
- $test_file);
- }
-
- $this->assertEqual($expect, $code, $explain);
- }
- }
- }
-
- private function parseICSSingleEvent($name) {
- $root = $this->parseICSDocument($name);
-
- $documents = $root->getDocuments();
- $this->assertEqual(1, count($documents));
- $document = head($documents);
-
- $events = $document->getEvents();
- $this->assertEqual(1, count($events));
-
- return head($events);
- }
-
- private function parseICSDocument($name) {
- $path = dirname(__FILE__).'/data/'.$name;
- $data = Filesystem::readFile($path);
- return id(new PhutilICSParser())
- ->parseICSData($data);
- }
-
-
-}
diff --git a/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php b/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php
+++ /dev/null
@@ -1,144 +0,0 @@
-<?php
-
-final class PhutilICSWriterTestCase extends PhutilTestCase {
-
- public function testICSWriterTeaTime() {
- $teas = array(
- 'earl grey tea',
- 'English breakfast tea',
- 'black tea',
- 'green tea',
- 't-rex',
- 'oolong tea',
- 'mint tea',
- 'tea with milk',
- );
-
- $teas = implode(', ', $teas);
-
- $event = id(new PhutilCalendarEventNode())
- ->setUID('tea-time')
- ->setName('Tea Time')
- ->setDescription(
- "Tea and, perhaps, crumpets.\n".
- "Your presence is requested!\n".
- "This is a long list of types of tea to test line wrapping: {$teas}.")
- ->setCreatedDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160915T070000Z'))
- ->setModifiedDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160915T070000Z'))
- ->setStartDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160916T150000Z'))
- ->setEndDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160916T160000Z'));
-
- $ics_data = $this->writeICSSingleEvent($event);
-
- $this->assertICS('writer-tea-time.ics', $ics_data);
- }
-
- public function testICSWriterChristmas() {
- $start = PhutilCalendarAbsoluteDateTime::newFromISO8601('20001225T000000Z');
- $end = PhutilCalendarAbsoluteDateTime::newFromISO8601('20001226T000000Z');
-
- $rrule = id(new PhutilCalendarRecurrenceRule())
- ->setFrequency(PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY)
- ->setByMonth(array(12))
- ->setByMonthDay(array(25));
-
- $event = id(new PhutilCalendarEventNode())
- ->setUID('recurring-christmas')
- ->setName('Christmas')
- ->setDescription('Festival holiday first occurring in the year 2000.')
- ->setStartDateTime($start)
- ->setEndDateTime($end)
- ->setCreatedDateTime($start)
- ->setModifiedDateTime($start)
- ->setRecurrenceRule($rrule)
- ->setRecurrenceExceptions(
- array(
- // In 2007, Christmas was cancelled.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20071225T000000Z'),
- ))
- ->setRecurrenceDates(
- array(
- // We had an extra early Christmas in 2009.
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20091125T000000Z'),
- ));
-
- $ics_data = $this->writeICSSingleEvent($event);
- $this->assertICS('writer-recurring-christmas.ics', $ics_data);
- }
-
- public function testICSWriterAllDay() {
- $event = id(new PhutilCalendarEventNode())
- ->setUID('christmas-day')
- ->setName('Christmas 2016')
- ->setDescription('A minor religious holiday.')
- ->setCreatedDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160901T232425Z'))
- ->setModifiedDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20160901T232425Z'))
- ->setStartDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20161225'))
- ->setEndDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20161226'));
-
- $ics_data = $this->writeICSSingleEvent($event);
-
- $this->assertICS('writer-christmas.ics', $ics_data);
- }
-
- public function testICSWriterUsers() {
- $event = id(new PhutilCalendarEventNode())
- ->setUID('office-party')
- ->setName('Office Party')
- ->setCreatedDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20161001T120000Z'))
- ->setModifiedDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20161001T120000Z'))
- ->setStartDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20161215T200000Z'))
- ->setEndDateTime(
- PhutilCalendarAbsoluteDateTime::newFromISO8601('20161215T230000Z'))
- ->setOrganizer(
- id(new PhutilCalendarUserNode())
- ->setName('Big Boss')
- ->setURI('mailto:big.boss@example.com'))
- ->addAttendee(
- id(new PhutilCalendarUserNode())
- ->setName('Milton')
- ->setStatus(PhutilCalendarUserNode::STATUS_INVITED)
- ->setURI('mailto:milton@example.com'))
- ->addAttendee(
- id(new PhutilCalendarUserNode())
- ->setName('Nancy')
- ->setStatus(PhutilCalendarUserNode::STATUS_ACCEPTED)
- ->setURI('mailto:nancy@example.com'));
-
- $ics_data = $this->writeICSSingleEvent($event);
- $this->assertICS('writer-office-party.ics', $ics_data);
- }
-
- private function writeICSSingleEvent(PhutilCalendarEventNode $event) {
- $calendar = id(new PhutilCalendarDocumentNode())
- ->appendChild($event);
-
- $root = id(new PhutilCalendarRootNode())
- ->appendChild($calendar);
-
- return $this->writeICS($root);
- }
-
- private function writeICS(PhutilCalendarRootNode $root) {
- return id(new PhutilICSWriter())
- ->writeICSDocument($root);
- }
-
- private function assertICS($name, $actual) {
- $path = dirname(__FILE__).'/data/'.$name;
- $data = Filesystem::readFile($path);
- $this->assertEqual($data, $actual, pht('ICS: %s', $name));
- }
-
-}
diff --git a/src/parser/calendar/ics/__tests__/data/duration.ics b/src/parser/calendar/ics/__tests__/data/duration.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/duration.ics
+++ /dev/null
@@ -1,8 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DTSTART:20160719T095722Z
-DURATION:P1DT17H4M23S
-SUMMARY:Duration Event
-DESCRIPTION:This is an event with a complex duration.
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-base64.ics b/src/parser/calendar/ics/__tests__/data/err-bad-base64.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-bad-base64.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DATA;VALUE=BINARY;ENCODING=BASE64:<QUACK! QUACK!>
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-boolean.ics b/src/parser/calendar/ics/__tests__/data/err-bad-boolean.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-bad-boolean.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DUCK;VALUE=BOOLEAN:QUACK
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-bad-datetime.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DTSTART:quack
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics b/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-bad-duration.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DURATION:quack
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-empty-datetime.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DTSTART:
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics b/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-empty-duration.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DURATION:
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-extra-end.ics b/src/parser/calendar/ics/__tests__/data/err-extra-end.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-extra-end.ics
+++ /dev/null
@@ -1 +0,0 @@
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-initial-unfold.ics b/src/parser/calendar/ics/__tests__/data/err-initial-unfold.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-initial-unfold.ics
+++ /dev/null
@@ -1,2 +0,0 @@
- BEGIN:VCALENDAR
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-malformed-double-quote.ics b/src/parser/calendar/ics/__tests__/data/err-malformed-double-quote.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-malformed-double-quote.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-A;B="C:D
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-malformed-parameter.ics b/src/parser/calendar/ics/__tests__/data/err-malformed-parameter.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-malformed-parameter.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-A;B:C
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-malformed-property.ics b/src/parser/calendar/ics/__tests__/data/err-malformed-property.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-malformed-property.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-PEANUTBUTTER&JELLY:sandwich
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics b/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-many-datetime.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DTSTART:20130101,20130101
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-many-duration.ics b/src/parser/calendar/ics/__tests__/data/err-many-duration.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-many-duration.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DURATION:P1W,P2W
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-missing-end.ics b/src/parser/calendar/ics/__tests__/data/err-missing-end.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-missing-end.ics
+++ /dev/null
@@ -1,2 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
diff --git a/src/parser/calendar/ics/__tests__/data/err-missing-value.ics b/src/parser/calendar/ics/__tests__/data/err-missing-value.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-missing-value.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-TRIANGLE;color=red
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-mixmatched-sections.ics b/src/parser/calendar/ics/__tests__/data/err-mixmatched-sections.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-mixmatched-sections.ics
+++ /dev/null
@@ -1,4 +0,0 @@
-BEGIN:A
-BEGIN:B
-END:A
-END:B
diff --git a/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics b/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-multiple-parameters.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DTSTART;TZID=A,B:20160915T090000
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-root-property.ics b/src/parser/calendar/ics/__tests__/data/err-root-property.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-root-property.ics
+++ /dev/null
@@ -1 +0,0 @@
-NAME:value
diff --git a/src/parser/calendar/ics/__tests__/data/err-unescaped-backslash.ics b/src/parser/calendar/ics/__tests__/data/err-unescaped-backslash.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-unescaped-backslash.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-STORY:The duck coughed up an unescaped backslash: \
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/err-unexpected-text.ics b/src/parser/calendar/ics/__tests__/data/err-unexpected-text.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/err-unexpected-text.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-SQUARE;color=red"
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/floating.ics b/src/parser/calendar/ics/__tests__/data/floating.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/floating.ics
+++ /dev/null
@@ -1,8 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DTSTART:20150101T000000
-DURATION:P1D
-SUMMARY:New Year's 2015
-DESCRIPTION:This is an event with a floating start time.
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/good-boolean.ics b/src/parser/calendar/ics/__tests__/data/good-boolean.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/good-boolean.ics
+++ /dev/null
@@ -1,5 +0,0 @@
-BEGIN:VCALENDAR
-BEGIN:VEVENT
-DUCK;VALUE=BOOLEAN:TRUE
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/multiple-vcalendars.ics b/src/parser/calendar/ics/__tests__/data/multiple-vcalendars.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/multiple-vcalendars.ics
+++ /dev/null
@@ -1,4 +0,0 @@
-BEGIN:VCALENDAR
-END:VCALENDAR
-BEGIN:VCALENDAR
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/simple.ics b/src/parser/calendar/ics/__tests__/data/simple.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/simple.ics
+++ /dev/null
@@ -1,12 +0,0 @@
-BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-CREATED:20160908T172702Z
-UID:1CEB57AF-0C9C-402D-B3BD-D75BD4843F68
-DTSTART;TZID=America/Los_Angeles:20160915T090000
-DTEND;TZID=America/Los_Angeles:20160915T100000
-SUMMARY:Simple Event
-DESCRIPTION:This is a simple event.
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/valarm.ics b/src/parser/calendar/ics/__tests__/data/valarm.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/valarm.ics
+++ /dev/null
@@ -1,16 +0,0 @@
-BEGIN:VCALENDAR
-VERSION:2.0
-BEGIN:VEVENT
-CREATED:20161027T173727
-DTSTAMP:20161027T173727
-LAST-MODIFIED:20161027T173727
-UID:aic4zm86mg
-SUMMARY:alarm event
-DTSTART;TZID=Europe/Berlin:20161020T000000
-DTEND;TZID=Europe/Berlin:20161020T010000
-BEGIN:VALARM
-ACTION:AUDIO
-TRIGGER:-PT15M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/weekly.ics b/src/parser/calendar/ics/__tests__/data/weekly.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/weekly.ics
+++ /dev/null
@@ -1,14 +0,0 @@
-BEGIN:VCALENDAR
-VERSION:2.0
-BEGIN:VEVENT
-TRANSP:OPAQUE
-DTEND;VALUE=DATE:20150812
-LAST-MODIFIED:20160822T130015Z
-UID:4AE69E91-4A51-4B77-8849-85981E037A83
-DTSTAMP:20161129T152151Z
-SUMMARY:Weekly Event
-DTSTART;VALUE=DATE:20150811
-CREATED:20141109T163445Z
-RRULE:FREQ=WEEKLY
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/writer-christmas.ics b/src/parser/calendar/ics/__tests__/data/writer-christmas.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/writer-christmas.ics
+++ /dev/null
@@ -1,13 +0,0 @@
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Phacility//Phabricator//EN
-BEGIN:VEVENT
-UID:christmas-day
-CREATED:20160901T232425Z
-DTSTAMP:20160901T232425Z
-DTSTART;VALUE=DATE:20161225
-DTEND;VALUE=DATE:20161226
-SUMMARY:Christmas 2016
-DESCRIPTION:A minor religious holiday.
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/writer-office-party.ics b/src/parser/calendar/ics/__tests__/data/writer-office-party.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/writer-office-party.ics
+++ /dev/null
@@ -1,15 +0,0 @@
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Phacility//Phabricator//EN
-BEGIN:VEVENT
-UID:office-party
-CREATED:20161001T120000Z
-DTSTAMP:20161001T120000Z
-DTSTART:20161215T200000Z
-DTEND:20161215T230000Z
-SUMMARY:Office Party
-ORGANIZER;CN="Big Boss":mailto:big.boss@example.com
-ATTENDEE;CN=Milton;PARTSTAT=NEEDS-ACTION:mailto:milton@example.com
-ATTENDEE;CN=Nancy;PARTSTAT=ACCEPTED:mailto:nancy@example.com
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/writer-recurring-christmas.ics b/src/parser/calendar/ics/__tests__/data/writer-recurring-christmas.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/writer-recurring-christmas.ics
+++ /dev/null
@@ -1,16 +0,0 @@
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Phacility//Phabricator//EN
-BEGIN:VEVENT
-UID:recurring-christmas
-CREATED:20001225T000000Z
-DTSTAMP:20001225T000000Z
-DTSTART:20001225T000000Z
-DTEND:20001226T000000Z
-SUMMARY:Christmas
-DESCRIPTION:Festival holiday first occurring in the year 2000.
-RRULE:FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=25
-EXDATE:20071225T000000Z
-RDATE:20091125T000000Z
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/writer-tea-time.ics b/src/parser/calendar/ics/__tests__/data/writer-tea-time.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/writer-tea-time.ics
+++ /dev/null
@@ -1,16 +0,0 @@
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Phacility//Phabricator//EN
-BEGIN:VEVENT
-UID:tea-time
-CREATED:20160915T070000Z
-DTSTAMP:20160915T070000Z
-DTSTART:20160916T150000Z
-DTEND:20160916T160000Z
-SUMMARY:Tea Time
-DESCRIPTION:Tea and\, perhaps\, crumpets.\nYour presence is requested!\nThi
- s is a long list of types of tea to test line wrapping: earl grey tea\, En
- glish breakfast tea\, black tea\, green tea\, t-rex\, oolong tea\, mint te
- a\, tea with milk.
-END:VEVENT
-END:VCALENDAR
diff --git a/src/parser/calendar/ics/__tests__/data/zimbra-timezone.ics b/src/parser/calendar/ics/__tests__/data/zimbra-timezone.ics
deleted file mode 100644
--- a/src/parser/calendar/ics/__tests__/data/zimbra-timezone.ics
+++ /dev/null
@@ -1,12 +0,0 @@
-BEGIN:VCALENDAR
-VERSION:2.0
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-CREATED:20161104T220244Z
-UID:zimbra-timezone
-SUMMARY:Zimbra Timezone
-DTSTART;TZID="(GMT-05.00) Auto-Detected":20170303T090000
-DTSTAMP:20161104T220244Z
-SEQUENCE:0
-END:VEVENT
-END:VCALENDAR
diff --git a/src/xsprintf/AphrontDatabaseTableRef.php b/src/xsprintf/AphrontDatabaseTableRef.php
deleted file mode 100644
--- a/src/xsprintf/AphrontDatabaseTableRef.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-final class AphrontDatabaseTableRef
- extends Phobject
- implements AphrontDatabaseTableRefInterface {
-
- private $database;
- private $table;
-
- public function __construct($database, $table) {
- $this->database = $database;
- $this->table = $table;
- }
-
- public function getAphrontRefDatabaseName() {
- return $this->database;
- }
-
- public function getAphrontRefTableName() {
- return $this->table;
- }
-
-}
diff --git a/src/xsprintf/AphrontDatabaseTableRefInterface.php b/src/xsprintf/AphrontDatabaseTableRefInterface.php
deleted file mode 100644
--- a/src/xsprintf/AphrontDatabaseTableRefInterface.php
+++ /dev/null
@@ -1,8 +0,0 @@
-<?php
-
-interface AphrontDatabaseTableRefInterface {
-
- public function getAphrontRefDatabaseName();
- public function getAphrontRefTableName();
-
-}
diff --git a/src/xsprintf/PhutilQsprintfInterface.php b/src/xsprintf/PhutilQsprintfInterface.php
deleted file mode 100644
--- a/src/xsprintf/PhutilQsprintfInterface.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-interface PhutilQsprintfInterface {
- public function escapeBinaryString($string);
- public function escapeUTF8String($string);
- public function escapeColumnName($string);
- public function escapeMultilineComment($string);
- public function escapeStringForLikeClause($string);
-}
diff --git a/src/xsprintf/PhutilQueryString.php b/src/xsprintf/PhutilQueryString.php
deleted file mode 100644
--- a/src/xsprintf/PhutilQueryString.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-final class PhutilQueryString extends Phobject {
-
- private $maskedString;
- private $unmaskedString;
-
- public function __construct(PhutilQsprintfInterface $escaper, array $argv) {
- // Immediately render the query into a static scalar value.
-
- // This makes sure we throw immediately if there are errors in the
- // parameters, which is much better than throwing later on.
-
- // This also makes sure that later mutations to objects passed as
- // parameters won't affect the outcome. Consider:
- //
- // $object->setTableName('X');
- // $query = qsprintf($conn, '%R', $object);
- // $object->setTableName('Y');
- //
- // We'd like "$query" to reference "X", reflecting the object as it
- // existed when it was passed to "qsprintf(...)". It's surprising if the
- // modification to the object after "qsprintf(...)" can affect "$query".
-
- $masked_string = xsprintf(
- 'xsprintf_query',
- array(
- 'escaper' => $escaper,
- 'unmasked' => false,
- ),
- $argv);
-
- $unmasked_string = xsprintf(
- 'xsprintf_query',
- array(
- 'escaper' => $escaper,
- 'unmasked' => true,
- ),
- $argv);
-
- $this->maskedString = $masked_string;
- $this->unmaskedString = $unmasked_string;
- }
-
- public function __toString() {
- return $this->getMaskedString();
- }
-
- public function getUnmaskedString() {
- return $this->unmaskedString;
- }
-
- public function getMaskedString() {
- return $this->maskedString;
- }
-
-}
diff --git a/src/xsprintf/qsprintf.php b/src/xsprintf/qsprintf.php
deleted file mode 100644
--- a/src/xsprintf/qsprintf.php
+++ /dev/null
@@ -1,516 +0,0 @@
-<?php
-
-/**
- * Format an SQL query. This function behaves like `sprintf`, except that all
- * the normal conversions (like "%s") will be properly escaped, and additional
- * conversions are supported:
- *
- * %nd, %ns, %nf, %nB
- * "Nullable" versions of %d, %s, %f and %B. Will produce 'NULL' if the
- * argument is a strict null.
- *
- * %=d, %=s, %=f
- * "Nullable Test" versions of %d, %s and %f. If you pass a value, you
- * get "= 3"; if you pass null, you get "IS NULL". For instance, this
- * will work properly if `hatID' is a nullable column and $hat is null.
- *
- * qsprintf($escaper, 'WHERE hatID %=d', $hat);
- *
- * %Ld, %Ls, %Lf, %LB
- * "List" versions of %d, %s, %f and %B. These are appropriate for use in
- * an "IN" clause. For example:
- *
- * qsprintf($escaper, 'WHERE hatID IN (%Ld)', $list_of_hats);
- *
- * %B ("Binary String")
- * Escapes a string for insertion into a pure binary column, ignoring
- * tests for characters outside of the basic multilingual plane.
- *
- * %C, %LC, %LK ("Column", "Key Column")
- * Escapes a column name or a list of column names. The "%LK" variant
- * escapes a list of key column specifications which may look like
- * "column(32)".
- *
- * %K ("Comment")
- * Escapes a comment.
- *
- * %Q, %LA, %LO, %LQ, %LJ ("Query Fragment")
- * Injects a query fragment from a prior call to qsprintf(). The list
- * variants join a list of query fragments with AND, OR, comma, or space.
- *
- * %Z ("Raw Query")
- * Injects a raw, unescaped query fragment. Dangerous!
- *
- * %R ("Database and Table Reference")
- * Behaves like "%T.%T" and prints a full reference to a table including
- * the database. Accepts a AphrontDatabaseTableRefInterface.
- *
- * %P ("Password or Secret")
- * Behaves like "%s", but shows "********" when the query is printed in
- * logs or traces. Accepts a PhutilOpaqueEnvelope.
- *
- * %~ ("Substring")
- * Escapes a substring query for a LIKE (or NOT LIKE) clause. For example:
- *
- * // Find all rows with $search as a substring of `name`.
- * qsprintf($escaper, 'WHERE name LIKE %~', $search);
- *
- * See also %> 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 new PhutilQueryString($escaper, $args);
-}
-
-function vqsprintf(PhutilQsprintfInterface $escaper, $pattern, array $argv) {
- array_unshift($argv, $pattern);
- 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];
-
- 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;
-
- $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(
- $query,
- pht(
- 'Parameter to "%s" conversion in "qsprintf(...)" is not an '.
- 'instance of AphrontDatabaseTableRefInterface.',
- '%R'));
- }
- break;
-
- case 'P':
- if (!($value instanceof PhutilOpaqueEnvelope)) {
- throw new AphrontParameterQueryException(
- $query,
- 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
deleted file mode 100644
--- a/src/xsprintf/queryfx.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-function queryfx(AphrontDatabaseConnection $conn, $sql /* , ... */) {
- $argv = func_get_args();
- $query = call_user_func_array('qsprintf', $argv);
-
- $conn->setLastActiveEpoch(time());
- $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;
-}

File Metadata

Mime Type
text/plain
Expires
Tue, Apr 8, 9:06 PM (1 w, 2 h ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/ft/5d/g43tt5h2ivswd2qt
Default Alt Text
D20773.largetrue.diff (543 KB)

Event Timeline