mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-04 20:52:43 +01:00
(stable) Promote 2019 Week 36
This commit is contained in:
commit
19af9d74f8
254 changed files with 18597 additions and 201 deletions
3
.arclint
3
.arclint
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"exclude": [
|
||||
"(^externals/)",
|
||||
"(^webroot/rsrc/externals/(?!javelin/))"
|
||||
"(^webroot/rsrc/externals/(?!javelin/))",
|
||||
"(/__tests__/data/)"
|
||||
],
|
||||
"linters": {
|
||||
"chmod": {
|
||||
|
|
|
@ -176,15 +176,27 @@ phutil_register_library_map(array(
|
|||
'Aphront400Response' => 'aphront/response/Aphront400Response.php',
|
||||
'Aphront403Response' => 'aphront/response/Aphront403Response.php',
|
||||
'Aphront404Response' => 'aphront/response/Aphront404Response.php',
|
||||
'AphrontAccessDeniedQueryException' => 'infrastructure/storage/exception/AphrontAccessDeniedQueryException.php',
|
||||
'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php',
|
||||
'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php',
|
||||
'AphrontBarView' => 'view/widget/bars/AphrontBarView.php',
|
||||
'AphrontBaseMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php',
|
||||
'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php',
|
||||
'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php',
|
||||
'AphrontCharacterSetQueryException' => 'infrastructure/storage/exception/AphrontCharacterSetQueryException.php',
|
||||
'AphrontConnectionLostQueryException' => 'infrastructure/storage/exception/AphrontConnectionLostQueryException.php',
|
||||
'AphrontConnectionQueryException' => 'infrastructure/storage/exception/AphrontConnectionQueryException.php',
|
||||
'AphrontController' => 'aphront/AphrontController.php',
|
||||
'AphrontCountQueryException' => 'infrastructure/storage/exception/AphrontCountQueryException.php',
|
||||
'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php',
|
||||
'AphrontDatabaseConnection' => 'infrastructure/storage/connection/AphrontDatabaseConnection.php',
|
||||
'AphrontDatabaseTableRef' => 'infrastructure/storage/xsprintf/AphrontDatabaseTableRef.php',
|
||||
'AphrontDatabaseTableRefInterface' => 'infrastructure/storage/xsprintf/AphrontDatabaseTableRefInterface.php',
|
||||
'AphrontDatabaseTransactionState' => 'infrastructure/storage/connection/AphrontDatabaseTransactionState.php',
|
||||
'AphrontDeadlockQueryException' => 'infrastructure/storage/exception/AphrontDeadlockQueryException.php',
|
||||
'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
|
||||
'AphrontDialogView' => 'view/AphrontDialogView.php',
|
||||
'AphrontDuplicateKeyQueryException' => 'infrastructure/storage/exception/AphrontDuplicateKeyQueryException.php',
|
||||
'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php',
|
||||
'AphrontException' => 'aphront/exception/AphrontException.php',
|
||||
'AphrontFileHTTPParameterType' => 'aphront/httpparametertype/AphrontFileHTTPParameterType.php',
|
||||
|
@ -217,6 +229,8 @@ phutil_register_library_map(array(
|
|||
'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
|
||||
'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
|
||||
'AphrontIntHTTPParameterType' => 'aphront/httpparametertype/AphrontIntHTTPParameterType.php',
|
||||
'AphrontInvalidCredentialsQueryException' => 'infrastructure/storage/exception/AphrontInvalidCredentialsQueryException.php',
|
||||
'AphrontIsolatedDatabaseConnection' => 'infrastructure/storage/connection/AphrontIsolatedDatabaseConnection.php',
|
||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php',
|
||||
'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php',
|
||||
'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php',
|
||||
|
@ -224,19 +238,28 @@ phutil_register_library_map(array(
|
|||
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php',
|
||||
'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php',
|
||||
'AphrontListHTTPParameterType' => 'aphront/httpparametertype/AphrontListHTTPParameterType.php',
|
||||
'AphrontLockTimeoutQueryException' => 'infrastructure/storage/exception/AphrontLockTimeoutQueryException.php',
|
||||
'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php',
|
||||
'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
|
||||
'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
|
||||
'AphrontMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
|
||||
'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
|
||||
'AphrontMySQLiDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
|
||||
'AphrontNotSupportedQueryException' => 'infrastructure/storage/exception/AphrontNotSupportedQueryException.php',
|
||||
'AphrontNullView' => 'view/AphrontNullView.php',
|
||||
'AphrontObjectMissingQueryException' => 'infrastructure/storage/exception/AphrontObjectMissingQueryException.php',
|
||||
'AphrontPHIDHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDHTTPParameterType.php',
|
||||
'AphrontPHIDListHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php',
|
||||
'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
|
||||
'AphrontPageView' => 'view/page/AphrontPageView.php',
|
||||
'AphrontParameterQueryException' => 'infrastructure/storage/exception/AphrontParameterQueryException.php',
|
||||
'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
|
||||
'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
|
||||
'AphrontProjectListHTTPParameterType' => 'aphront/httpparametertype/AphrontProjectListHTTPParameterType.php',
|
||||
'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php',
|
||||
'AphrontQueryException' => 'infrastructure/storage/exception/AphrontQueryException.php',
|
||||
'AphrontQueryTimeoutQueryException' => 'infrastructure/storage/exception/AphrontQueryTimeoutQueryException.php',
|
||||
'AphrontRecoverableQueryException' => 'infrastructure/storage/exception/AphrontRecoverableQueryException.php',
|
||||
'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php',
|
||||
'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
|
||||
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
|
||||
|
@ -247,6 +270,7 @@ phutil_register_library_map(array(
|
|||
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
|
||||
'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
|
||||
'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
|
||||
'AphrontSchemaQueryException' => 'infrastructure/storage/exception/AphrontSchemaQueryException.php',
|
||||
'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
|
||||
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
|
||||
'AphrontSite' => 'aphront/site/AphrontSite.php',
|
||||
|
@ -997,6 +1021,7 @@ phutil_register_library_map(array(
|
|||
'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php',
|
||||
'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php',
|
||||
'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
|
||||
'DiffusionServiceRef' => 'applications/diffusion/ref/DiffusionServiceRef.php',
|
||||
'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
|
||||
'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
|
||||
'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php',
|
||||
|
@ -4176,6 +4201,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
|
||||
'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
|
||||
'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php',
|
||||
'PhabricatorPreambleTestCase' => 'infrastructure/util/__tests__/PhabricatorPreambleTestCase.php',
|
||||
'PhabricatorPrimaryEmailUserLogType' => 'applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php',
|
||||
'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php',
|
||||
'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php',
|
||||
|
@ -4629,6 +4655,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
|
||||
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
|
||||
'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php',
|
||||
'PhabricatorSearchSettingsPanel' => 'applications/settings/panel/PhabricatorSearchSettingsPanel.php',
|
||||
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
|
||||
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
|
||||
'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
|
||||
|
@ -5512,6 +5539,93 @@ phutil_register_library_map(array(
|
|||
'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php',
|
||||
'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php',
|
||||
'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php',
|
||||
'PhutilAmazonAuthAdapter' => 'applications/auth/adapter/PhutilAmazonAuthAdapter.php',
|
||||
'PhutilAsanaAuthAdapter' => 'applications/auth/adapter/PhutilAsanaAuthAdapter.php',
|
||||
'PhutilAuthAdapter' => 'applications/auth/adapter/PhutilAuthAdapter.php',
|
||||
'PhutilAuthConfigurationException' => 'applications/auth/exception/PhutilAuthConfigurationException.php',
|
||||
'PhutilAuthCredentialException' => 'applications/auth/exception/PhutilAuthCredentialException.php',
|
||||
'PhutilAuthException' => 'applications/auth/exception/PhutilAuthException.php',
|
||||
'PhutilAuthUserAbortedException' => 'applications/auth/exception/PhutilAuthUserAbortedException.php',
|
||||
'PhutilBitbucketAuthAdapter' => 'applications/auth/adapter/PhutilBitbucketAuthAdapter.php',
|
||||
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilCLikeCodeSnippetContextFreeGrammar.php',
|
||||
'PhutilCalendarAbsoluteDateTime' => 'applications/calendar/parser/data/PhutilCalendarAbsoluteDateTime.php',
|
||||
'PhutilCalendarContainerNode' => 'applications/calendar/parser/data/PhutilCalendarContainerNode.php',
|
||||
'PhutilCalendarDateTime' => 'applications/calendar/parser/data/PhutilCalendarDateTime.php',
|
||||
'PhutilCalendarDateTimeTestCase' => 'applications/calendar/parser/data/__tests__/PhutilCalendarDateTimeTestCase.php',
|
||||
'PhutilCalendarDocumentNode' => 'applications/calendar/parser/data/PhutilCalendarDocumentNode.php',
|
||||
'PhutilCalendarDuration' => 'applications/calendar/parser/data/PhutilCalendarDuration.php',
|
||||
'PhutilCalendarEventNode' => 'applications/calendar/parser/data/PhutilCalendarEventNode.php',
|
||||
'PhutilCalendarNode' => 'applications/calendar/parser/data/PhutilCalendarNode.php',
|
||||
'PhutilCalendarProxyDateTime' => 'applications/calendar/parser/data/PhutilCalendarProxyDateTime.php',
|
||||
'PhutilCalendarRawNode' => 'applications/calendar/parser/data/PhutilCalendarRawNode.php',
|
||||
'PhutilCalendarRecurrenceList' => 'applications/calendar/parser/data/PhutilCalendarRecurrenceList.php',
|
||||
'PhutilCalendarRecurrenceRule' => 'applications/calendar/parser/data/PhutilCalendarRecurrenceRule.php',
|
||||
'PhutilCalendarRecurrenceRuleTestCase' => 'applications/calendar/parser/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php',
|
||||
'PhutilCalendarRecurrenceSet' => 'applications/calendar/parser/data/PhutilCalendarRecurrenceSet.php',
|
||||
'PhutilCalendarRecurrenceSource' => 'applications/calendar/parser/data/PhutilCalendarRecurrenceSource.php',
|
||||
'PhutilCalendarRecurrenceTestCase' => 'applications/calendar/parser/data/__tests__/PhutilCalendarRecurrenceTestCase.php',
|
||||
'PhutilCalendarRelativeDateTime' => 'applications/calendar/parser/data/PhutilCalendarRelativeDateTime.php',
|
||||
'PhutilCalendarRootNode' => 'applications/calendar/parser/data/PhutilCalendarRootNode.php',
|
||||
'PhutilCalendarUserNode' => 'applications/calendar/parser/data/PhutilCalendarUserNode.php',
|
||||
'PhutilCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilCodeSnippetContextFreeGrammar.php',
|
||||
'PhutilDisqusAuthAdapter' => 'applications/auth/adapter/PhutilDisqusAuthAdapter.php',
|
||||
'PhutilEmptyAuthAdapter' => 'applications/auth/adapter/PhutilEmptyAuthAdapter.php',
|
||||
'PhutilFacebookAuthAdapter' => 'applications/auth/adapter/PhutilFacebookAuthAdapter.php',
|
||||
'PhutilGitHubAuthAdapter' => 'applications/auth/adapter/PhutilGitHubAuthAdapter.php',
|
||||
'PhutilGoogleAuthAdapter' => 'applications/auth/adapter/PhutilGoogleAuthAdapter.php',
|
||||
'PhutilICSParser' => 'applications/calendar/parser/ics/PhutilICSParser.php',
|
||||
'PhutilICSParserException' => 'applications/calendar/parser/ics/PhutilICSParserException.php',
|
||||
'PhutilICSParserTestCase' => 'applications/calendar/parser/ics/__tests__/PhutilICSParserTestCase.php',
|
||||
'PhutilICSWriter' => 'applications/calendar/parser/ics/PhutilICSWriter.php',
|
||||
'PhutilICSWriterTestCase' => 'applications/calendar/parser/ics/__tests__/PhutilICSWriterTestCase.php',
|
||||
'PhutilJIRAAuthAdapter' => 'applications/auth/adapter/PhutilJIRAAuthAdapter.php',
|
||||
'PhutilJavaCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilJavaCodeSnippetContextFreeGrammar.php',
|
||||
'PhutilLDAPAuthAdapter' => 'applications/auth/adapter/PhutilLDAPAuthAdapter.php',
|
||||
'PhutilLipsumContextFreeGrammar' => 'infrastructure/lipsum/PhutilLipsumContextFreeGrammar.php',
|
||||
'PhutilOAuth1AuthAdapter' => 'applications/auth/adapter/PhutilOAuth1AuthAdapter.php',
|
||||
'PhutilOAuthAuthAdapter' => 'applications/auth/adapter/PhutilOAuthAuthAdapter.php',
|
||||
'PhutilPHPCodeSnippetContextFreeGrammar' => 'infrastructure/lipsum/code/PhutilPHPCodeSnippetContextFreeGrammar.php',
|
||||
'PhutilPhabricatorAuthAdapter' => 'applications/auth/adapter/PhutilPhabricatorAuthAdapter.php',
|
||||
'PhutilQsprintfInterface' => 'infrastructure/storage/xsprintf/PhutilQsprintfInterface.php',
|
||||
'PhutilQueryString' => 'infrastructure/storage/xsprintf/PhutilQueryString.php',
|
||||
'PhutilRealNameContextFreeGrammar' => 'infrastructure/lipsum/PhutilRealNameContextFreeGrammar.php',
|
||||
'PhutilRemarkupBlockInterpreter' => 'infrastructure/markup/blockrule/PhutilRemarkupBlockInterpreter.php',
|
||||
'PhutilRemarkupBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupBlockRule.php',
|
||||
'PhutilRemarkupBlockStorage' => 'infrastructure/markup/PhutilRemarkupBlockStorage.php',
|
||||
'PhutilRemarkupBoldRule' => 'infrastructure/markup/markuprule/PhutilRemarkupBoldRule.php',
|
||||
'PhutilRemarkupCodeBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupCodeBlockRule.php',
|
||||
'PhutilRemarkupDefaultBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupDefaultBlockRule.php',
|
||||
'PhutilRemarkupDelRule' => 'infrastructure/markup/markuprule/PhutilRemarkupDelRule.php',
|
||||
'PhutilRemarkupDocumentLinkRule' => 'infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php',
|
||||
'PhutilRemarkupEngine' => 'infrastructure/markup/remarkup/PhutilRemarkupEngine.php',
|
||||
'PhutilRemarkupEngineTestCase' => 'infrastructure/markup/remarkup/__tests__/PhutilRemarkupEngineTestCase.php',
|
||||
'PhutilRemarkupEscapeRemarkupRule' => 'infrastructure/markup/markuprule/PhutilRemarkupEscapeRemarkupRule.php',
|
||||
'PhutilRemarkupHeaderBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupHeaderBlockRule.php',
|
||||
'PhutilRemarkupHighlightRule' => 'infrastructure/markup/markuprule/PhutilRemarkupHighlightRule.php',
|
||||
'PhutilRemarkupHorizontalRuleBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php',
|
||||
'PhutilRemarkupHyperlinkEngineExtension' => 'infrastructure/markup/markuprule/PhutilRemarkupHyperlinkEngineExtension.php',
|
||||
'PhutilRemarkupHyperlinkRef' => 'infrastructure/markup/markuprule/PhutilRemarkupHyperlinkRef.php',
|
||||
'PhutilRemarkupHyperlinkRule' => 'infrastructure/markup/markuprule/PhutilRemarkupHyperlinkRule.php',
|
||||
'PhutilRemarkupInlineBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupInlineBlockRule.php',
|
||||
'PhutilRemarkupInterpreterBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupInterpreterBlockRule.php',
|
||||
'PhutilRemarkupItalicRule' => 'infrastructure/markup/markuprule/PhutilRemarkupItalicRule.php',
|
||||
'PhutilRemarkupLinebreaksRule' => 'infrastructure/markup/markuprule/PhutilRemarkupLinebreaksRule.php',
|
||||
'PhutilRemarkupListBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupListBlockRule.php',
|
||||
'PhutilRemarkupLiteralBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupLiteralBlockRule.php',
|
||||
'PhutilRemarkupMonospaceRule' => 'infrastructure/markup/markuprule/PhutilRemarkupMonospaceRule.php',
|
||||
'PhutilRemarkupNoteBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupNoteBlockRule.php',
|
||||
'PhutilRemarkupQuotedBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupQuotedBlockRule.php',
|
||||
'PhutilRemarkupQuotesBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupQuotesBlockRule.php',
|
||||
'PhutilRemarkupReplyBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupReplyBlockRule.php',
|
||||
'PhutilRemarkupRule' => 'infrastructure/markup/markuprule/PhutilRemarkupRule.php',
|
||||
'PhutilRemarkupSimpleTableBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupSimpleTableBlockRule.php',
|
||||
'PhutilRemarkupTableBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupTableBlockRule.php',
|
||||
'PhutilRemarkupTestInterpreterRule' => 'infrastructure/markup/blockrule/PhutilRemarkupTestInterpreterRule.php',
|
||||
'PhutilRemarkupUnderlineRule' => 'infrastructure/markup/markuprule/PhutilRemarkupUnderlineRule.php',
|
||||
'PhutilSlackAuthAdapter' => 'applications/auth/adapter/PhutilSlackAuthAdapter.php',
|
||||
'PhutilTwitchAuthAdapter' => 'applications/auth/adapter/PhutilTwitchAuthAdapter.php',
|
||||
'PhutilTwitterAuthAdapter' => 'applications/auth/adapter/PhutilTwitterAuthAdapter.php',
|
||||
'PhutilWordPressAuthAdapter' => 'applications/auth/adapter/PhutilWordPressAuthAdapter.php',
|
||||
'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php',
|
||||
'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php',
|
||||
'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php',
|
||||
|
@ -5587,6 +5701,7 @@ phutil_register_library_map(array(
|
|||
'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
|
||||
'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php',
|
||||
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
|
||||
'QueryFuture' => 'infrastructure/storage/future/QueryFuture.php',
|
||||
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
|
||||
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
|
||||
'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
|
||||
|
@ -5713,7 +5828,15 @@ phutil_register_library_map(array(
|
|||
'phid_get_subtype' => 'applications/phid/utils.php',
|
||||
'phid_get_type' => 'applications/phid/utils.php',
|
||||
'phid_group_by_type' => 'applications/phid/utils.php',
|
||||
'qsprintf' => 'infrastructure/storage/xsprintf/qsprintf.php',
|
||||
'qsprintf_check_scalar_type' => 'infrastructure/storage/xsprintf/qsprintf.php',
|
||||
'qsprintf_check_type' => 'infrastructure/storage/xsprintf/qsprintf.php',
|
||||
'queryfx' => 'infrastructure/storage/xsprintf/queryfx.php',
|
||||
'queryfx_all' => 'infrastructure/storage/xsprintf/queryfx.php',
|
||||
'queryfx_one' => 'infrastructure/storage/xsprintf/queryfx.php',
|
||||
'require_celerity_resource' => 'applications/celerity/api.php',
|
||||
'vqsprintf' => 'infrastructure/storage/xsprintf/qsprintf.php',
|
||||
'xsprintf_query' => 'infrastructure/storage/xsprintf/qsprintf.php',
|
||||
),
|
||||
'xmap' => array(
|
||||
'AlmanacAddress' => 'Phobject',
|
||||
|
@ -5937,18 +6060,35 @@ phutil_register_library_map(array(
|
|||
'Aphront400Response' => 'AphrontResponse',
|
||||
'Aphront403Response' => 'AphrontHTMLResponse',
|
||||
'Aphront404Response' => 'AphrontHTMLResponse',
|
||||
'AphrontAccessDeniedQueryException' => 'AphrontQueryException',
|
||||
'AphrontAjaxResponse' => 'AphrontResponse',
|
||||
'AphrontApplicationConfiguration' => 'Phobject',
|
||||
'AphrontBarView' => 'AphrontView',
|
||||
'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
|
||||
'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||
'AphrontCalendarEventView' => 'AphrontView',
|
||||
'AphrontCharacterSetQueryException' => 'AphrontQueryException',
|
||||
'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException',
|
||||
'AphrontConnectionQueryException' => 'AphrontQueryException',
|
||||
'AphrontController' => 'Phobject',
|
||||
'AphrontCountQueryException' => 'AphrontQueryException',
|
||||
'AphrontCursorPagerView' => 'AphrontView',
|
||||
'AphrontDatabaseConnection' => array(
|
||||
'Phobject',
|
||||
'PhutilQsprintfInterface',
|
||||
),
|
||||
'AphrontDatabaseTableRef' => array(
|
||||
'Phobject',
|
||||
'AphrontDatabaseTableRefInterface',
|
||||
),
|
||||
'AphrontDatabaseTransactionState' => 'Phobject',
|
||||
'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException',
|
||||
'AphrontDialogResponse' => 'AphrontResponse',
|
||||
'AphrontDialogView' => array(
|
||||
'AphrontView',
|
||||
'AphrontResponseProducerInterface',
|
||||
),
|
||||
'AphrontDuplicateKeyQueryException' => 'AphrontQueryException',
|
||||
'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||
'AphrontException' => 'Exception',
|
||||
'AphrontFileHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||
|
@ -5981,6 +6121,8 @@ phutil_register_library_map(array(
|
|||
'AphrontHTTPSink' => 'Phobject',
|
||||
'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
|
||||
'AphrontIntHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||
'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException',
|
||||
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
|
||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
||||
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
|
||||
'AphrontJSONResponse' => 'AphrontResponse',
|
||||
|
@ -5988,15 +6130,21 @@ phutil_register_library_map(array(
|
|||
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
|
||||
'AphrontListFilterView' => 'AphrontView',
|
||||
'AphrontListHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||
'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException',
|
||||
'AphrontMalformedRequestException' => 'AphrontException',
|
||||
'AphrontMoreView' => 'AphrontView',
|
||||
'AphrontMultiColumnView' => 'AphrontView',
|
||||
'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
|
||||
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
||||
'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
|
||||
'AphrontNotSupportedQueryException' => 'AphrontQueryException',
|
||||
'AphrontNullView' => 'AphrontView',
|
||||
'AphrontObjectMissingQueryException' => 'AphrontQueryException',
|
||||
'AphrontPHIDHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||
'AphrontPHIDListHTTPParameterType' => 'AphrontListHTTPParameterType',
|
||||
'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
|
||||
'AphrontPageView' => 'AphrontView',
|
||||
'AphrontParameterQueryException' => 'AphrontQueryException',
|
||||
'AphrontPlainTextResponse' => 'AphrontResponse',
|
||||
'AphrontProgressBarView' => 'AphrontBarView',
|
||||
'AphrontProjectListHTTPParameterType' => 'AphrontListHTTPParameterType',
|
||||
|
@ -6004,6 +6152,9 @@ phutil_register_library_map(array(
|
|||
'AphrontResponse',
|
||||
'AphrontResponseProducerInterface',
|
||||
),
|
||||
'AphrontQueryException' => 'Exception',
|
||||
'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException',
|
||||
'AphrontRecoverableQueryException' => 'AphrontQueryException',
|
||||
'AphrontRedirectResponse' => 'AphrontResponse',
|
||||
'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
|
||||
'AphrontReloadResponse' => 'AphrontRedirectResponse',
|
||||
|
@ -6013,6 +6164,7 @@ phutil_register_library_map(array(
|
|||
'AphrontResponse' => 'Phobject',
|
||||
'AphrontRoutingMap' => 'Phobject',
|
||||
'AphrontRoutingResult' => 'Phobject',
|
||||
'AphrontSchemaQueryException' => 'AphrontQueryException',
|
||||
'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||
'AphrontSideNavFilterView' => 'AphrontView',
|
||||
'AphrontSite' => 'Phobject',
|
||||
|
@ -6818,6 +6970,7 @@ phutil_register_library_map(array(
|
|||
'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow',
|
||||
'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
|
||||
'DiffusionServeController' => 'DiffusionController',
|
||||
'DiffusionServiceRef' => 'Phobject',
|
||||
'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
|
||||
'DiffusionSetupException' => 'Exception',
|
||||
'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension',
|
||||
|
@ -10530,6 +10683,7 @@ phutil_register_library_map(array(
|
|||
),
|
||||
'PhabricatorPolicyType' => 'PhabricatorPolicyConstants',
|
||||
'PhabricatorPonderApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorPreambleTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorPrimaryEmailUserLogType' => 'PhabricatorUserLogType',
|
||||
'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine',
|
||||
'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
|
@ -11095,9 +11249,10 @@ phutil_register_library_map(array(
|
|||
'PhabricatorSearchResultBucketGroup' => 'Phobject',
|
||||
'PhabricatorSearchResultView' => 'AphrontView',
|
||||
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
|
||||
'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting',
|
||||
'PhabricatorSearchScopeSetting' => 'PhabricatorSelectSetting',
|
||||
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
|
||||
'PhabricatorSearchService' => 'Phobject',
|
||||
'PhabricatorSearchSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
|
||||
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
|
||||
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
|
||||
'PhabricatorSearchTextField' => 'PhabricatorSearchField',
|
||||
|
@ -12169,6 +12324,92 @@ phutil_register_library_map(array(
|
|||
'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
|
||||
'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilAuthAdapter' => 'Phobject',
|
||||
'PhutilAuthConfigurationException' => 'PhutilAuthException',
|
||||
'PhutilAuthCredentialException' => 'PhutilAuthException',
|
||||
'PhutilAuthException' => 'Exception',
|
||||
'PhutilAuthUserAbortedException' => 'PhutilAuthException',
|
||||
'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter',
|
||||
'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',
|
||||
'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar',
|
||||
'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter',
|
||||
'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilICSParser' => 'Phobject',
|
||||
'PhutilICSParserException' => 'Exception',
|
||||
'PhutilICSParserTestCase' => 'PhutilTestCase',
|
||||
'PhutilICSWriter' => 'Phobject',
|
||||
'PhutilICSWriterTestCase' => 'PhutilTestCase',
|
||||
'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter',
|
||||
'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
|
||||
'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter',
|
||||
'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar',
|
||||
'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter',
|
||||
'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter',
|
||||
'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
|
||||
'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilQueryString' => 'Phobject',
|
||||
'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',
|
||||
'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
|
||||
'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter',
|
||||
'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType',
|
||||
'PonderAddAnswerView' => 'AphrontView',
|
||||
'PonderAnswer' => array(
|
||||
|
@ -12265,6 +12506,7 @@ phutil_register_library_map(array(
|
|||
'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
|
||||
'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
|
||||
'QueryFormattingTestCase' => 'PhabricatorTestCase',
|
||||
'QueryFuture' => 'Future',
|
||||
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
|
||||
'ReleephBranch' => array(
|
||||
'ReleephDAO',
|
||||
|
|
80
src/applications/auth/adapter/PhutilAmazonAuthAdapter.php
Normal file
80
src/applications/auth/adapter/PhutilAmazonAuthAdapter.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
86
src/applications/auth/adapter/PhutilAsanaAuthAdapter.php
Normal file
86
src/applications/auth/adapter/PhutilAsanaAuthAdapter.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?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();
|
||||
}
|
||||
|
||||
}
|
123
src/applications/auth/adapter/PhutilAuthAdapter.php
Normal file
123
src/applications/auth/adapter/PhutilAuthAdapter.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
73
src/applications/auth/adapter/PhutilBitbucketAuthAdapter.php
Normal file
73
src/applications/auth/adapter/PhutilBitbucketAuthAdapter.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
84
src/applications/auth/adapter/PhutilDisqusAuthAdapter.php
Normal file
84
src/applications/auth/adapter/PhutilDisqusAuthAdapter.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
42
src/applications/auth/adapter/PhutilEmptyAuthAdapter.php
Normal file
42
src/applications/auth/adapter/PhutilEmptyAuthAdapter.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
114
src/applications/auth/adapter/PhutilFacebookAuthAdapter.php
Normal file
114
src/applications/auth/adapter/PhutilFacebookAuthAdapter.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
72
src/applications/auth/adapter/PhutilGitHubAuthAdapter.php
Normal file
72
src/applications/auth/adapter/PhutilGitHubAuthAdapter.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
105
src/applications/auth/adapter/PhutilGoogleAuthAdapter.php
Normal file
105
src/applications/auth/adapter/PhutilGoogleAuthAdapter.php
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
164
src/applications/auth/adapter/PhutilJIRAAuthAdapter.php
Normal file
164
src/applications/auth/adapter/PhutilJIRAAuthAdapter.php
Normal file
|
@ -0,0 +1,164 @@
|
|||
<?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');
|
||||
}
|
||||
|
||||
}
|
505
src/applications/auth/adapter/PhutilLDAPAuthAdapter.php
Normal file
505
src/applications/auth/adapter/PhutilLDAPAuthAdapter.php
Normal file
|
@ -0,0 +1,505 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
}
|
211
src/applications/auth/adapter/PhutilOAuth1AuthAdapter.php
Normal file
211
src/applications/auth/adapter/PhutilOAuth1AuthAdapter.php
Normal file
|
@ -0,0 +1,211 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
228
src/applications/auth/adapter/PhutilOAuthAuthAdapter.php
Normal file
228
src/applications/auth/adapter/PhutilOAuthAuthAdapter.php
Normal file
|
@ -0,0 +1,228 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
}
|
102
src/applications/auth/adapter/PhutilPhabricatorAuthAdapter.php
Normal file
102
src/applications/auth/adapter/PhutilPhabricatorAuthAdapter.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?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, '/');
|
||||
}
|
||||
|
||||
}
|
61
src/applications/auth/adapter/PhutilSlackAuthAdapter.php
Normal file
61
src/applications/auth/adapter/PhutilSlackAuthAdapter.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?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();
|
||||
}
|
||||
|
||||
}
|
76
src/applications/auth/adapter/PhutilTwitchAuthAdapter.php
Normal file
76
src/applications/auth/adapter/PhutilTwitchAuthAdapter.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?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();
|
||||
}
|
||||
|
||||
}
|
75
src/applications/auth/adapter/PhutilTwitterAuthAdapter.php
Normal file
75
src/applications/auth/adapter/PhutilTwitterAuthAdapter.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
73
src/applications/auth/adapter/PhutilWordPressAuthAdapter.php
Normal file
73
src/applications/auth/adapter/PhutilWordPressAuthAdapter.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?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();
|
||||
}
|
||||
|
||||
}
|
|
@ -68,12 +68,42 @@ final class PhabricatorLogoutController
|
|||
->setURI('/auth/loggedout/');
|
||||
}
|
||||
|
||||
|
||||
if ($viewer->getPHID()) {
|
||||
return $this->newDialog()
|
||||
$dialog = $this->newDialog()
|
||||
->setTitle(pht('Log Out?'))
|
||||
->appendChild(pht('Are you sure you want to log out?'))
|
||||
->addSubmitButton(pht('Log Out'))
|
||||
->appendParagraph(pht('Are you sure you want to log out?'))
|
||||
->addCancelButton('/');
|
||||
|
||||
$configs = id(new PhabricatorAuthProviderConfigQuery())
|
||||
->setViewer(PhabricatorUser::getOmnipotentUser())
|
||||
->execute();
|
||||
if (!$configs) {
|
||||
$dialog
|
||||
->appendRemarkup(
|
||||
pht(
|
||||
'WARNING: You have not configured any authentication providers '.
|
||||
'yet, so your account has no login credentials. If you log out '.
|
||||
'now, you will not be able to log back in normally.'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'To enable the login flow, follow setup guidance and configure '.
|
||||
'at least one authentication provider, then associate '.
|
||||
'credentials with your account. After completing these steps, '.
|
||||
'you will be able to log out and log back in normally.'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'If you log out now, you can still regain access to your '.
|
||||
'account later by using the account recovery workflow. The '.
|
||||
'login screen will prompt you with recovery instructions.'));
|
||||
|
||||
$button = pht('Log Out Anyway');
|
||||
} else {
|
||||
$button = pht('Log Out');
|
||||
}
|
||||
|
||||
$dialog->addSubmitButton($button);
|
||||
return $dialog;
|
||||
}
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI('/');
|
||||
|
|
|
@ -64,7 +64,7 @@ final class PhabricatorAuthListController
|
|||
array(
|
||||
'href' => $this->getApplicationURI('config/new/'),
|
||||
),
|
||||
pht('Add Authentication Provider'))));
|
||||
pht('Add Provider'))));
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs();
|
||||
$crumbs->addTextCrumb(pht('Login and Registration'));
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication is not configured correctly.
|
||||
*/
|
||||
final class PhutilAuthConfigurationException extends PhutilAuthException {}
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The user provided invalid credentials.
|
||||
*/
|
||||
final class PhutilAuthCredentialException extends PhutilAuthException {}
|
7
src/applications/auth/exception/PhutilAuthException.php
Normal file
7
src/applications/auth/exception/PhutilAuthException.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Abstract exception class for errors encountered during authentication
|
||||
* workflows.
|
||||
*/
|
||||
abstract class PhutilAuthException extends Exception {}
|
|
@ -0,0 +1,14 @@
|
|||
<?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 {}
|
|
@ -0,0 +1,287 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?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();
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
final class PhutilCalendarDocumentNode
|
||||
extends PhutilCalendarContainerNode {
|
||||
|
||||
const NODETYPE = 'document';
|
||||
|
||||
public function getEvents() {
|
||||
return $this->getChildrenOfType(PhutilCalendarEventNode::NODETYPE);
|
||||
}
|
||||
|
||||
}
|
181
src/applications/calendar/parser/data/PhutilCalendarDuration.php
Normal file
181
src/applications/calendar/parser/data/PhutilCalendarDuration.php
Normal file
|
@ -0,0 +1,181 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
20
src/applications/calendar/parser/data/PhutilCalendarNode.php
Normal file
20
src/applications/calendar/parser/data/PhutilCalendarNode.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?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();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
final class PhutilCalendarRawNode
|
||||
extends PhutilCalendarContainerNode {
|
||||
|
||||
const NODETYPE = 'raw';
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,162 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?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);
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<?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());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
final class PhutilCalendarRootNode
|
||||
extends PhutilCalendarContainerNode {
|
||||
|
||||
const NODETYPE = 'root';
|
||||
|
||||
public function getDocuments() {
|
||||
return $this->getChildrenOfType(PhutilCalendarDocumentNode::NODETYPE);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?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());
|
||||
}
|
||||
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,196 @@
|
|||
<?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.'));
|
||||
}
|
||||
|
||||
}
|
919
src/applications/calendar/parser/ics/PhutilICSParser.php
Normal file
919
src/applications/calendar/parser/ics/PhutilICSParser.php
Normal file
|
@ -0,0 +1,919 @@
|
|||
<?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';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
final class PhutilICSParserException extends Exception {
|
||||
|
||||
private $parserFailureCode;
|
||||
|
||||
public function setParserFailureCode($code) {
|
||||
$this->parserFailureCode = $code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParserFailureCode() {
|
||||
return $this->parserFailureCode;
|
||||
}
|
||||
|
||||
}
|
387
src/applications/calendar/parser/ics/PhutilICSWriter.php
Normal file
387
src/applications/calendar/parser/ics/PhutilICSWriter.php
Normal file
|
@ -0,0 +1,387 @@
|
|||
<?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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
<?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));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20160719T095722Z
|
||||
DURATION:P1DT17H4M23S
|
||||
SUMMARY:Duration Event
|
||||
DESCRIPTION:This is an event with a complex duration.
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DATA;VALUE=BINARY;ENCODING=BASE64:<QUACK! QUACK!>
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DUCK;VALUE=BOOLEAN:QUACK
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DTSTART:quack
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DURATION:quack
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DTSTART:
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DURATION:
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1 @@
|
|||
END:VCALENDAR
|
|
@ -0,0 +1,2 @@
|
|||
BEGIN:VCALENDAR
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
A;B="C:D
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
A;B:C
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
PEANUTBUTTER&JELLY:sandwich
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DTSTART:20130101,20130101
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DURATION:P1W,P2W
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,2 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
TRIANGLE;color=red
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:A
|
||||
BEGIN:B
|
||||
END:A
|
||||
END:B
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=A,B:20160915T090000
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1 @@
|
|||
NAME:value
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
STORY:The duck coughed up an unescaped backslash: \
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
SQUARE;color=red"
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,8 @@
|
|||
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
|
|
@ -0,0 +1,5 @@
|
|||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
DUCK;VALUE=BOOLEAN:TRUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN:VCALENDAR
|
||||
END:VCALENDAR
|
||||
BEGIN:VCALENDAR
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,12 @@
|
|||
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
|
|
@ -0,0 +1,16 @@
|
|||
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
|
|
@ -0,0 +1,14 @@
|
|||
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
|
|
@ -0,0 +1,13 @@
|
|||
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
|
|
@ -0,0 +1,15 @@
|
|||
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
|
|
@ -0,0 +1,16 @@
|
|||
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
|
|
@ -0,0 +1,16 @@
|
|||
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
|
|
@ -0,0 +1,12 @@
|
|||
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
|
|
@ -140,11 +140,20 @@ final class PhabricatorConfigManagementSetWorkflow
|
|||
'Wrote configuration key "%s" to database storage.',
|
||||
$key);
|
||||
} else {
|
||||
$config_source = id(new PhabricatorConfigLocalSource())
|
||||
->setKeys(array($key => $value));
|
||||
$config_source = new PhabricatorConfigLocalSource();
|
||||
|
||||
$local_path = $config_source->getReadablePath();
|
||||
|
||||
try {
|
||||
$config_source->setKeys(array($key => $value));
|
||||
} catch (FilesystemException $ex) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Local path "%s" is not writable. This file must be writable '.
|
||||
'so that "bin/config" can store configuration.',
|
||||
Filesystem::readablePath($local_path)));
|
||||
}
|
||||
|
||||
$write_message = pht(
|
||||
'Wrote configuration key "%s" to local storage (in file "%s").',
|
||||
$key,
|
||||
|
|
48
src/applications/diffusion/ref/DiffusionServiceRef.php
Normal file
48
src/applications/diffusion/ref/DiffusionServiceRef.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
final class DiffusionServiceRef
|
||||
extends Phobject {
|
||||
|
||||
private $uri;
|
||||
private $protocol;
|
||||
private $isWritable;
|
||||
private $devicePHID;
|
||||
private $deviceName;
|
||||
|
||||
private function __construct() {
|
||||
return;
|
||||
}
|
||||
|
||||
public static function newFromDictionary(array $map) {
|
||||
$ref = new self();
|
||||
|
||||
$ref->uri = $map['uri'];
|
||||
$ref->isWritable = $map['writable'];
|
||||
$ref->devicePHID = $map['devicePHID'];
|
||||
$ref->protocol = $map['protocol'];
|
||||
$ref->deviceName = $map['device'];
|
||||
|
||||
return $ref;
|
||||
}
|
||||
|
||||
public function isWritable() {
|
||||
return $this->isWritable;
|
||||
}
|
||||
|
||||
public function getDevicePHID() {
|
||||
return $this->devicePHID;
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function getProtocol() {
|
||||
return $this->protocol;
|
||||
}
|
||||
|
||||
public function getDeviceName() {
|
||||
return $this->deviceName;
|
||||
}
|
||||
|
||||
}
|
|
@ -14,42 +14,33 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
|||
}
|
||||
|
||||
protected function executeRepositoryOperations() {
|
||||
// This is a write, and must have write access.
|
||||
$this->requireWriteAccess();
|
||||
|
||||
$is_proxy = $this->shouldProxy();
|
||||
if ($is_proxy) {
|
||||
return $this->executeRepositoryProxyOperations($for_write = true);
|
||||
}
|
||||
|
||||
$host_wait_start = microtime(true);
|
||||
|
||||
$repository = $this->getRepository();
|
||||
$viewer = $this->getSSHUser();
|
||||
$device = AlmanacKeys::getLiveDevice();
|
||||
|
||||
// This is a write, and must have write access.
|
||||
$this->requireWriteAccess();
|
||||
|
||||
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
|
||||
->setViewer($viewer)
|
||||
->setRepository($repository)
|
||||
->setLog($this);
|
||||
|
||||
$is_proxy = $this->shouldProxy();
|
||||
if ($is_proxy) {
|
||||
$command = $this->getProxyCommand(true);
|
||||
$did_write = false;
|
||||
$command = csprintf('git-receive-pack %s', $repository->getLocalPath());
|
||||
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
|
||||
|
||||
if ($device) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Push received by \"%s\", forwarding to cluster host.\n",
|
||||
$device->getName()));
|
||||
}
|
||||
} else {
|
||||
$command = csprintf('git-receive-pack %s', $repository->getLocalPath());
|
||||
$did_write = true;
|
||||
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
|
||||
|
||||
if ($device) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Ready to receive on cluster host \"%s\".\n",
|
||||
$device->getName()));
|
||||
}
|
||||
if ($device) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Ready to receive on cluster host \"%s\".\n",
|
||||
$device->getName()));
|
||||
}
|
||||
|
||||
$log = $this->newProtocolLog($is_proxy);
|
||||
|
@ -71,9 +62,7 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
|||
|
||||
// We've committed the write (or rejected it), so we can release the lock
|
||||
// without waiting for the client to receive the acknowledgement.
|
||||
if ($did_write) {
|
||||
$cluster_engine->synchronizeWorkingCopyAfterWrite();
|
||||
}
|
||||
$cluster_engine->synchronizeWorkingCopyAfterWrite();
|
||||
|
||||
if ($caught) {
|
||||
throw $caught;
|
||||
|
@ -85,18 +74,16 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
|||
// When a repository is clustered, we reach this cleanup code on both
|
||||
// the proxy and the actual final endpoint node. Don't do more cleanup
|
||||
// or logging than we need to.
|
||||
if ($did_write) {
|
||||
$repository->writeStatusMessage(
|
||||
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
|
||||
PhabricatorRepositoryStatusMessage::CODE_OKAY);
|
||||
$repository->writeStatusMessage(
|
||||
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
|
||||
PhabricatorRepositoryStatusMessage::CODE_OKAY);
|
||||
|
||||
$host_wait_end = microtime(true);
|
||||
$host_wait_end = microtime(true);
|
||||
|
||||
$this->updatePushLogWithTimingInformation(
|
||||
$this->getClusterEngineLogProperty('writeWait'),
|
||||
$this->getClusterEngineLogProperty('readWait'),
|
||||
($host_wait_end - $host_wait_start));
|
||||
}
|
||||
$this->updatePushLogWithTimingInformation(
|
||||
$this->getClusterEngineLogProperty('writeWait'),
|
||||
$this->getClusterEngineLogProperty('readWait'),
|
||||
($host_wait_end - $host_wait_start));
|
||||
}
|
||||
|
||||
return $err;
|
||||
|
|
|
@ -8,6 +8,10 @@ abstract class DiffusionGitSSHWorkflow
|
|||
private $protocolLog;
|
||||
|
||||
private $wireProtocol;
|
||||
private $ioBytesRead = 0;
|
||||
private $ioBytesWritten = 0;
|
||||
private $requestAttempts = 0;
|
||||
private $requestFailures = 0;
|
||||
|
||||
protected function writeError($message) {
|
||||
// Git assumes we'll add our own newlines.
|
||||
|
@ -98,6 +102,8 @@ abstract class DiffusionGitSSHWorkflow
|
|||
PhabricatorSSHPassthruCommand $command,
|
||||
$message) {
|
||||
|
||||
$this->ioBytesWritten += strlen($message);
|
||||
|
||||
$log = $this->getProtocolLog();
|
||||
if ($log) {
|
||||
$log->didWriteBytes($message);
|
||||
|
@ -125,7 +131,131 @@ abstract class DiffusionGitSSHWorkflow
|
|||
$message = $protocol->willReadBytes($message);
|
||||
}
|
||||
|
||||
// Note that bytes aren't counted until they're emittted by the protocol
|
||||
// layer. This means the underlying command might emit bytes, but if they
|
||||
// are buffered by the protocol layer they won't count as read bytes yet.
|
||||
|
||||
$this->ioBytesRead += strlen($message);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
final protected function getIOBytesRead() {
|
||||
return $this->ioBytesRead;
|
||||
}
|
||||
|
||||
final protected function getIOBytesWritten() {
|
||||
return $this->ioBytesWritten;
|
||||
}
|
||||
|
||||
final protected function executeRepositoryProxyOperations($for_write) {
|
||||
$device = AlmanacKeys::getLiveDevice();
|
||||
|
||||
$refs = $this->getAlmanacServiceRefs($for_write);
|
||||
$err = 1;
|
||||
|
||||
while (true) {
|
||||
$ref = head($refs);
|
||||
|
||||
$command = $this->getProxyCommandForServiceRef($ref);
|
||||
|
||||
if ($device) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Request received by \"%s\", forwarding to cluster ".
|
||||
"host \"%s\".\n",
|
||||
$device->getName(),
|
||||
$ref->getDeviceName()));
|
||||
}
|
||||
|
||||
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
|
||||
|
||||
$future = id(new ExecFuture('%C', $command))
|
||||
->setEnv($this->getEnvironment());
|
||||
|
||||
$this->didBeginRequest();
|
||||
|
||||
$err = $this->newPassthruCommand()
|
||||
->setIOChannel($this->getIOChannel())
|
||||
->setCommandChannelFromExecFuture($future)
|
||||
->execute();
|
||||
|
||||
// TODO: Currently, when proxying, we do not write an event log on the
|
||||
// proxy. Perhaps we should write a "proxy log". This is not very useful
|
||||
// for statistics or auditing, but could be useful for diagnostics.
|
||||
// Marking the proxy logs as proxied (and recording devicePHID on all
|
||||
// logs) would make differentiating between these use cases easier.
|
||||
|
||||
if (!$err) {
|
||||
$this->waitForGitClient();
|
||||
return $err;
|
||||
}
|
||||
|
||||
// Throw away this service: the request failed and we're treating the
|
||||
// failure as persistent, so we don't want to retry another request to
|
||||
// the same host.
|
||||
array_shift($refs);
|
||||
|
||||
$should_retry = $this->shouldRetryRequest($refs);
|
||||
if (!$should_retry) {
|
||||
return $err;
|
||||
}
|
||||
|
||||
// If we haven't bailed out yet, we'll retry the request with the next
|
||||
// service.
|
||||
}
|
||||
|
||||
throw new Exception(pht('Reached an unreachable place.'));
|
||||
}
|
||||
|
||||
private function didBeginRequest() {
|
||||
$this->requestAttempts++;
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function shouldRetryRequest(array $remaining_refs) {
|
||||
$this->requestFailures++;
|
||||
|
||||
if ($this->requestFailures > $this->requestAttempts) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
"Workflow has recorded more failures than attempts; there is a ".
|
||||
"missing call to \"didBeginRequest()\".\n"));
|
||||
}
|
||||
|
||||
if (!$remaining_refs) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# All available services failed to serve the request, ".
|
||||
"giving up.\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
$read_len = $this->getIOBytesRead();
|
||||
if ($read_len) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Client already read from service (%s bytes), unable to retry.\n",
|
||||
new PhutilNumber($read_len)));
|
||||
return false;
|
||||
}
|
||||
|
||||
$write_len = $this->getIOBytesWritten();
|
||||
if ($write_len) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Client already wrote to service (%s bytes), unable to retry.\n",
|
||||
new PhutilNumber($write_len)));
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Service request failed, retrying (making attempt %s of %s).\n",
|
||||
new PhutilNumber($this->requestAttempts + 1),
|
||||
new PhutilNumber($this->requestAttempts + count($remaining_refs))));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
||||
final class DiffusionGitUploadPackSSHWorkflow
|
||||
extends DiffusionGitSSHWorkflow {
|
||||
|
||||
protected function didConstruct() {
|
||||
$this->setName('git-upload-pack');
|
||||
|
@ -14,39 +15,33 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
|||
}
|
||||
|
||||
protected function executeRepositoryOperations() {
|
||||
$repository = $this->getRepository();
|
||||
$is_proxy = $this->shouldProxy();
|
||||
if ($is_proxy) {
|
||||
return $this->executeRepositoryProxyOperations($for_write = false);
|
||||
}
|
||||
|
||||
$viewer = $this->getSSHUser();
|
||||
$repository = $this->getRepository();
|
||||
$device = AlmanacKeys::getLiveDevice();
|
||||
|
||||
$skip_sync = $this->shouldSkipReadSynchronization();
|
||||
$is_proxy = $this->shouldProxy();
|
||||
|
||||
if ($is_proxy) {
|
||||
$command = $this->getProxyCommand(false);
|
||||
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
|
||||
if (!$skip_sync) {
|
||||
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
|
||||
->setViewer($viewer)
|
||||
->setRepository($repository)
|
||||
->setLog($this)
|
||||
->synchronizeWorkingCopyBeforeRead();
|
||||
|
||||
if ($device) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Fetch received by \"%s\", forwarding to cluster host.\n",
|
||||
"# Cleared to fetch on cluster host \"%s\".\n",
|
||||
$device->getName()));
|
||||
}
|
||||
} else {
|
||||
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
|
||||
if (!$skip_sync) {
|
||||
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
|
||||
->setViewer($viewer)
|
||||
->setRepository($repository)
|
||||
->setLog($this)
|
||||
->synchronizeWorkingCopyBeforeRead();
|
||||
|
||||
if ($device) {
|
||||
$this->writeClusterEngineLogMessage(
|
||||
pht(
|
||||
"# Cleared to fetch on cluster host \"%s\".\n",
|
||||
$device->getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
|
||||
|
||||
$pull_event = $this->newPullEvent();
|
||||
|
@ -60,14 +55,12 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
|||
$log->didStartSession($command);
|
||||
}
|
||||
|
||||
if (!$is_proxy) {
|
||||
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
|
||||
$protocol = new DiffusionGitUploadPackWireProtocol();
|
||||
if ($log) {
|
||||
$protocol->setProtocolLog($log);
|
||||
}
|
||||
$this->setWireProtocol($protocol);
|
||||
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
|
||||
$protocol = new DiffusionGitUploadPackWireProtocol();
|
||||
if ($log) {
|
||||
$protocol->setProtocolLog($log);
|
||||
}
|
||||
$this->setWireProtocol($protocol);
|
||||
}
|
||||
|
||||
$err = $this->newPassthruCommand()
|
||||
|
@ -89,15 +82,7 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
|||
->setResultCode(0);
|
||||
}
|
||||
|
||||
// TODO: Currently, when proxying, we do not write a log on the proxy.
|
||||
// Perhaps we should write a "proxy log". This is not very useful for
|
||||
// statistics or auditing, but could be useful for diagnostics. Marking
|
||||
// the proxy logs as proxied (and recording devicePHID on all logs) would
|
||||
// make differentiating between these use cases easier.
|
||||
|
||||
if (!$is_proxy) {
|
||||
$pull_event->save();
|
||||
}
|
||||
$pull_event->save();
|
||||
|
||||
if (!$err) {
|
||||
$this->waitForGitClient();
|
||||
|
|
|
@ -73,13 +73,13 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
|
|||
return $this->shouldProxy;
|
||||
}
|
||||
|
||||
protected function getProxyCommand($for_write) {
|
||||
final protected function getAlmanacServiceRefs($for_write) {
|
||||
$viewer = $this->getSSHUser();
|
||||
$repository = $this->getRepository();
|
||||
|
||||
$is_cluster_request = $this->getIsClusterRequest();
|
||||
|
||||
$uri = $repository->getAlmanacServiceURI(
|
||||
$refs = $repository->getAlmanacServiceRefs(
|
||||
$viewer,
|
||||
array(
|
||||
'neverProxy' => $is_cluster_request,
|
||||
|
@ -89,14 +89,28 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
|
|||
'writable' => $for_write,
|
||||
));
|
||||
|
||||
if (!$uri) {
|
||||
if (!$refs) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Failed to generate an intracluster proxy URI even though this '.
|
||||
'request was routed as a proxy request.'));
|
||||
}
|
||||
|
||||
$uri = new PhutilURI($uri);
|
||||
return $refs;
|
||||
}
|
||||
|
||||
final protected function getProxyCommand($for_write) {
|
||||
$refs = $this->getAlmanacServiceRefs($for_write);
|
||||
|
||||
$ref = head($refs);
|
||||
|
||||
return $this->getProxyCommandForServiceRef($ref);
|
||||
}
|
||||
|
||||
final protected function getProxyCommandForServiceRef(
|
||||
DiffusionServiceRef $ref) {
|
||||
|
||||
$uri = new PhutilURI($ref->getURI());
|
||||
|
||||
$username = AlmanacKeys::getClusterSSHUser();
|
||||
if ($username === null) {
|
||||
|
|
|
@ -1842,6 +1842,20 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
PhabricatorUser $viewer,
|
||||
array $options) {
|
||||
|
||||
$refs = $this->getAlmanacServiceRefs($viewer, $options);
|
||||
|
||||
if (!$refs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ref = head($refs);
|
||||
return $ref->getURI();
|
||||
}
|
||||
|
||||
public function getAlmanacServiceRefs(
|
||||
PhabricatorUser $viewer,
|
||||
array $options) {
|
||||
|
||||
PhutilTypeSpec::checkMap(
|
||||
$options,
|
||||
array(
|
||||
|
@ -1856,7 +1870,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
|
||||
$cache_key = $this->getAlmanacServiceCacheKey();
|
||||
if (!$cache_key) {
|
||||
return null;
|
||||
return array();
|
||||
}
|
||||
|
||||
$cache = PhabricatorCaches::getMutableStructureCache();
|
||||
|
@ -1869,7 +1883,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
}
|
||||
|
||||
if ($uris === null) {
|
||||
return null;
|
||||
return array();
|
||||
}
|
||||
|
||||
$local_device = AlmanacKeys::getDeviceID();
|
||||
|
@ -1893,7 +1907,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
|
||||
if ($local_device && $never_proxy) {
|
||||
if ($uri['device'] == $local_device) {
|
||||
return null;
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1954,15 +1968,20 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
}
|
||||
}
|
||||
|
||||
$refs = array();
|
||||
foreach ($results as $result) {
|
||||
$refs[] = DiffusionServiceRef::newFromDictionary($result);
|
||||
}
|
||||
|
||||
// If we require a writable device, remove URIs which aren't writable.
|
||||
if ($writable) {
|
||||
foreach ($results as $key => $uri) {
|
||||
if (!$uri['writable']) {
|
||||
foreach ($refs as $key => $ref) {
|
||||
if (!$ref->isWritable()) {
|
||||
unset($results[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$results) {
|
||||
if (!$refs) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This repository ("%s") is not writable with the given '.
|
||||
|
@ -1974,23 +1993,30 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
}
|
||||
|
||||
if ($writable) {
|
||||
$results = $this->sortWritableAlmanacServiceURIs($results);
|
||||
$refs = $this->sortWritableAlmanacServiceRefs($refs);
|
||||
} else {
|
||||
shuffle($results);
|
||||
$refs = $this->sortReadableAlmanacServiceRefs($refs);
|
||||
}
|
||||
|
||||
$result = head($results);
|
||||
return $result['uri'];
|
||||
return array_values($refs);
|
||||
}
|
||||
|
||||
private function sortWritableAlmanacServiceURIs(array $results) {
|
||||
private function sortReadableAlmanacServiceRefs(array $refs) {
|
||||
assert_instances_of($refs, 'DiffusionServiceRef');
|
||||
shuffle($refs);
|
||||
return $refs;
|
||||
}
|
||||
|
||||
private function sortWritableAlmanacServiceRefs(array $refs) {
|
||||
assert_instances_of($refs, 'DiffusionServiceRef');
|
||||
|
||||
// See T13109 for discussion of how this method routes requests.
|
||||
|
||||
// In the absence of other rules, we'll send traffic to devices randomly.
|
||||
// We also want to select randomly among nodes which are equally good
|
||||
// candidates to receive the write, and accomplish that by shuffling the
|
||||
// list up front.
|
||||
shuffle($results);
|
||||
shuffle($refs);
|
||||
|
||||
$order = array();
|
||||
|
||||
|
@ -2002,8 +2028,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
$this->getPHID());
|
||||
if ($writer) {
|
||||
$device_phid = $writer->getWriteProperty('devicePHID');
|
||||
foreach ($results as $key => $result) {
|
||||
if ($result['devicePHID'] === $device_phid) {
|
||||
foreach ($refs as $key => $ref) {
|
||||
if ($ref->getDevicePHID() === $device_phid) {
|
||||
$order[] = $key;
|
||||
}
|
||||
}
|
||||
|
@ -2025,8 +2051,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
}
|
||||
$max_devices = array_fuse($max_devices);
|
||||
|
||||
foreach ($results as $key => $result) {
|
||||
if (isset($max_devices[$result['devicePHID']])) {
|
||||
foreach ($refs as $key => $ref) {
|
||||
if (isset($max_devices[$ref->getDevicePHID()])) {
|
||||
$order[] = $key;
|
||||
}
|
||||
}
|
||||
|
@ -2034,9 +2060,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
|||
|
||||
// Reorder the results, putting any we've selected as preferred targets for
|
||||
// the write at the head of the list.
|
||||
$results = array_select_keys($results, $order) + $results;
|
||||
$refs = array_select_keys($refs, $order) + $refs;
|
||||
|
||||
return $results;
|
||||
return $refs;
|
||||
}
|
||||
|
||||
public function supportsSynchronization() {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorSearchSettingsPanel
|
||||
extends PhabricatorEditEngineSettingsPanel {
|
||||
|
||||
const PANELKEY = 'search';
|
||||
|
||||
public function getPanelName() {
|
||||
return pht('Search');
|
||||
}
|
||||
|
||||
public function getPanelMenuIcon() {
|
||||
return 'fa-search';
|
||||
}
|
||||
|
||||
public function getPanelGroupKey() {
|
||||
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
|
||||
}
|
||||
|
||||
public function isTemplatePanel() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isUserPanel() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorSearchScopeSetting
|
||||
extends PhabricatorInternalSetting {
|
||||
extends PhabricatorSelectSetting {
|
||||
|
||||
const SETTINGKEY = 'search-scope';
|
||||
|
||||
|
@ -9,8 +9,33 @@ final class PhabricatorSearchScopeSetting
|
|||
return pht('Search Scope');
|
||||
}
|
||||
|
||||
public function getSettingPanelKey() {
|
||||
return PhabricatorSearchSettingsPanel::PANELKEY;
|
||||
}
|
||||
|
||||
public function getSettingDefaultValue() {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
protected function getControlInstructions() {
|
||||
return pht(
|
||||
'Choose the default behavior of the global search in the main menu.');
|
||||
}
|
||||
|
||||
protected function getSelectOptions() {
|
||||
$scopes = PhabricatorMainMenuSearchView::getGlobalSearchScopeItems(
|
||||
$this->getViewer(),
|
||||
new PhabricatorSettingsApplication());
|
||||
|
||||
$scope_map = array();
|
||||
foreach ($scopes as $scope) {
|
||||
if (!isset($scope['value'])) {
|
||||
continue;
|
||||
}
|
||||
$scope_map[$scope['value']] = $scope['name'];
|
||||
}
|
||||
|
||||
return $scope_map;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -100,32 +100,34 @@ final class PhabricatorSystemActionEngine extends Phobject {
|
|||
|
||||
$actor_hashes = array();
|
||||
foreach ($actors as $actor) {
|
||||
$actor_hashes[] = PhabricatorHash::digestForIndex($actor);
|
||||
$digest = PhabricatorHash::digestForIndex($actor);
|
||||
$actor_hashes[$digest] = $actor;
|
||||
}
|
||||
|
||||
$log = new PhabricatorSystemActionLog();
|
||||
|
||||
$window = self::getWindow();
|
||||
|
||||
$conn_r = $log->establishConnection('r');
|
||||
$scores = queryfx_all(
|
||||
$conn_r,
|
||||
'SELECT actorIdentity, SUM(score) totalScore FROM %T
|
||||
$conn = $log->establishConnection('r');
|
||||
|
||||
$rows = queryfx_all(
|
||||
$conn,
|
||||
'SELECT actorHash, SUM(score) totalScore FROM %T
|
||||
WHERE action = %s AND actorHash IN (%Ls)
|
||||
AND epoch >= %d GROUP BY actorHash',
|
||||
$log->getTableName(),
|
||||
$action->getActionConstant(),
|
||||
$actor_hashes,
|
||||
(time() - $window));
|
||||
array_keys($actor_hashes),
|
||||
(PhabricatorTime::getNow() - $window));
|
||||
|
||||
$scores = ipull($scores, 'totalScore', 'actorIdentity');
|
||||
$rows = ipull($rows, 'totalScore', 'actorHash');
|
||||
|
||||
foreach ($scores as $key => $score) {
|
||||
$scores[$key] = $score / $window;
|
||||
$scores = array();
|
||||
foreach ($actor_hashes as $digest => $actor) {
|
||||
$score = idx($rows, $digest, 0);
|
||||
$scores[$actor] = ($score / $window);
|
||||
}
|
||||
|
||||
$scores = $scores + array_fill_keys($actors, 0);
|
||||
|
||||
return $scores;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,10 +15,9 @@ You can use a special preamble script to make arbitrary adjustments to the
|
|||
environment and some parts of Phabricator's configuration in order to fix these
|
||||
problems and set up the environment which Phabricator expects.
|
||||
|
||||
NOTE: This is an advanced feature. Most installs should not need to configure
|
||||
a preamble script.
|
||||
|
||||
= Creating a Preamble Script =
|
||||
Creating a Preamble Script
|
||||
==========================
|
||||
|
||||
To create a preamble script, write a file to:
|
||||
|
||||
|
@ -37,6 +36,7 @@ If present, this script will be executed at the very beginning of each web
|
|||
request, allowing you to adjust the environment. For common adjustments and
|
||||
examples, see the next sections.
|
||||
|
||||
|
||||
Adjusting Client IPs
|
||||
====================
|
||||
|
||||
|
@ -44,9 +44,15 @@ If your install is behind a load balancer, Phabricator may incorrectly detect
|
|||
all requests as originating from the load balancer, rather than from the
|
||||
correct client IPs.
|
||||
|
||||
If this is the case and some other header (like `X-Forwarded-For`) is known to
|
||||
be trustworthy, you can read the header and overwrite the `REMOTE_ADDR` value
|
||||
so Phabricator can figure out the client IP correctly.
|
||||
In common cases where networks are configured like this, the `X-Forwarded-For`
|
||||
header will have trustworthy information about the real client IP. You
|
||||
can use the function `preamble_trust_x_forwarded_for_header()` in your
|
||||
preamble to tell Phabricator that you expect to receive requests from a
|
||||
load balancer or proxy which modifies this header:
|
||||
|
||||
```name="Trust X-Forwarded-For Header", lang=php
|
||||
preamble_trust_x_forwarded_for_header();
|
||||
```
|
||||
|
||||
You should do this //only// if the `X-Forwarded-For` header is known to be
|
||||
trustworthy. In particular, if users can make requests to the web server
|
||||
|
@ -54,30 +60,29 @@ directly, they can provide an arbitrary `X-Forwarded-For` header, and thereby
|
|||
spoof an arbitrary client IP.
|
||||
|
||||
The `X-Forwarded-For` header may also contain a list of addresses if a request
|
||||
has been forwarded through multiple loadbalancers. Using a snippet like this
|
||||
will usually handle most situations correctly:
|
||||
has been forwarded through multiple load balancers. If you know that requests
|
||||
on your network are routed through `N` trustworthy devices, you can specify
|
||||
that `N` to tell the function how many layers of `X-Forwarded-For` to discard:
|
||||
|
||||
```name="Trust X-Forwarded-For Header, Multiple Layers", lang=php
|
||||
preamble_trust_x_forwarded_for_header(3);
|
||||
```
|
||||
name=Overwrite REMOTE_ADDR with X-Forwarded-For
|
||||
<?php
|
||||
|
||||
// Overwrite REMOTE_ADDR with the value in the "X-Forwarded-For" HTTP header.
|
||||
If you have an unusual network configuration (for example, the number of
|
||||
trustworthy devices depends on the network path) you can also implement your
|
||||
own logic.
|
||||
|
||||
// Only do this if you're certain the request is coming from a loadbalancer!
|
||||
// If the request came directly from a client, doing this will allow them to
|
||||
// them spoof any remote address.
|
||||
Note that this is very odd, advanced, and easy to get wrong. If you get it
|
||||
wrong, users will most likely be able to spoof any client address.
|
||||
|
||||
// The header may contain a list of IPs, like "1.2.3.4, 4.5.6.7", if the
|
||||
// request the load balancer received also had this header.
|
||||
```name="Custom X-Forwarded-For Handling", lang=php
|
||||
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$forwarded_for = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
if ($forwarded_for) {
|
||||
$forwarded_for = explode(',', $forwarded_for);
|
||||
$forwarded_for = end($forwarded_for);
|
||||
$forwarded_for = trim($forwarded_for);
|
||||
$_SERVER['REMOTE_ADDR'] = $forwarded_for;
|
||||
}
|
||||
$raw_header = $_SERVER['X_FORWARDED_FOR'];
|
||||
|
||||
$real_address = your_custom_parsing_function($raw_header);
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] = $raw_header;
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -221,6 +221,9 @@ final class PhabricatorDatabaseRef
|
|||
return $this->replicaRefs;
|
||||
}
|
||||
|
||||
public function getDisplayName() {
|
||||
return $this->getRefKey();
|
||||
}
|
||||
|
||||
public function getRefKey() {
|
||||
$host = $this->getHost();
|
||||
|
|
19
src/infrastructure/env/PhabricatorEnv.php
vendored
19
src/infrastructure/env/PhabricatorEnv.php
vendored
|
@ -135,6 +135,11 @@ final class PhabricatorEnv extends Phobject {
|
|||
// TODO: Add a "locale.default" config option once we have some reasonable
|
||||
// defaults which aren't silly nonsense.
|
||||
self::setLocaleCode('en_US');
|
||||
|
||||
// Load the preamble utility library if we haven't already. On web
|
||||
// requests this loaded earlier, but we want to load it for non-web
|
||||
// requests so that unit tests can call these functions.
|
||||
require_once $phabricator_path.'/support/startup/preamble-utils.php';
|
||||
}
|
||||
|
||||
public static function beginScopedLocale($locale_code) {
|
||||
|
@ -249,9 +254,17 @@ final class PhabricatorEnv extends Phobject {
|
|||
}
|
||||
|
||||
try {
|
||||
$stack->pushSource(
|
||||
id(new PhabricatorConfigDatabaseSource('default'))
|
||||
->setName(pht('Database')));
|
||||
// See T13403. If we're starting up in "config optional" mode, suppress
|
||||
// messages about connection retries.
|
||||
if ($config_optional) {
|
||||
$database_source = @new PhabricatorConfigDatabaseSource('default');
|
||||
} else {
|
||||
$database_source = new PhabricatorConfigDatabaseSource('default');
|
||||
}
|
||||
|
||||
$database_source->setName(pht('Database'));
|
||||
|
||||
$stack->pushSource($database_source);
|
||||
} catch (AphrontSchemaQueryException $exception) {
|
||||
// If the database is not available, just skip this configuration
|
||||
// source. This happens during `bin/storage upgrade`, `bin/conf` before
|
||||
|
|
107
src/infrastructure/lipsum/PhutilLipsumContextFreeGrammar.php
Normal file
107
src/infrastructure/lipsum/PhutilLipsumContextFreeGrammar.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?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',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
155
src/infrastructure/lipsum/PhutilRealNameContextFreeGrammar.php
Normal file
155
src/infrastructure/lipsum/PhutilRealNameContextFreeGrammar.php
Normal file
|
@ -0,0 +1,155 @@
|
|||
<?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',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
<?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(''));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
<?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]',
|
||||
));
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue