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": [
|
"exclude": [
|
||||||
"(^externals/)",
|
"(^externals/)",
|
||||||
"(^webroot/rsrc/externals/(?!javelin/))"
|
"(^webroot/rsrc/externals/(?!javelin/))",
|
||||||
|
"(/__tests__/data/)"
|
||||||
],
|
],
|
||||||
"linters": {
|
"linters": {
|
||||||
"chmod": {
|
"chmod": {
|
||||||
|
|
|
@ -176,15 +176,27 @@ phutil_register_library_map(array(
|
||||||
'Aphront400Response' => 'aphront/response/Aphront400Response.php',
|
'Aphront400Response' => 'aphront/response/Aphront400Response.php',
|
||||||
'Aphront403Response' => 'aphront/response/Aphront403Response.php',
|
'Aphront403Response' => 'aphront/response/Aphront403Response.php',
|
||||||
'Aphront404Response' => 'aphront/response/Aphront404Response.php',
|
'Aphront404Response' => 'aphront/response/Aphront404Response.php',
|
||||||
|
'AphrontAccessDeniedQueryException' => 'infrastructure/storage/exception/AphrontAccessDeniedQueryException.php',
|
||||||
'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php',
|
'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php',
|
||||||
'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php',
|
'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php',
|
||||||
'AphrontBarView' => 'view/widget/bars/AphrontBarView.php',
|
'AphrontBarView' => 'view/widget/bars/AphrontBarView.php',
|
||||||
|
'AphrontBaseMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php',
|
||||||
'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php',
|
'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php',
|
||||||
'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.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',
|
'AphrontController' => 'aphront/AphrontController.php',
|
||||||
|
'AphrontCountQueryException' => 'infrastructure/storage/exception/AphrontCountQueryException.php',
|
||||||
'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.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',
|
'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
|
||||||
'AphrontDialogView' => 'view/AphrontDialogView.php',
|
'AphrontDialogView' => 'view/AphrontDialogView.php',
|
||||||
|
'AphrontDuplicateKeyQueryException' => 'infrastructure/storage/exception/AphrontDuplicateKeyQueryException.php',
|
||||||
'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php',
|
'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php',
|
||||||
'AphrontException' => 'aphront/exception/AphrontException.php',
|
'AphrontException' => 'aphront/exception/AphrontException.php',
|
||||||
'AphrontFileHTTPParameterType' => 'aphront/httpparametertype/AphrontFileHTTPParameterType.php',
|
'AphrontFileHTTPParameterType' => 'aphront/httpparametertype/AphrontFileHTTPParameterType.php',
|
||||||
|
@ -217,6 +229,8 @@ phutil_register_library_map(array(
|
||||||
'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
|
'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
|
||||||
'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
|
'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
|
||||||
'AphrontIntHTTPParameterType' => 'aphront/httpparametertype/AphrontIntHTTPParameterType.php',
|
'AphrontIntHTTPParameterType' => 'aphront/httpparametertype/AphrontIntHTTPParameterType.php',
|
||||||
|
'AphrontInvalidCredentialsQueryException' => 'infrastructure/storage/exception/AphrontInvalidCredentialsQueryException.php',
|
||||||
|
'AphrontIsolatedDatabaseConnection' => 'infrastructure/storage/connection/AphrontIsolatedDatabaseConnection.php',
|
||||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php',
|
'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php',
|
||||||
'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php',
|
'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php',
|
||||||
'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php',
|
'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php',
|
||||||
|
@ -224,19 +238,28 @@ phutil_register_library_map(array(
|
||||||
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php',
|
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php',
|
||||||
'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php',
|
'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php',
|
||||||
'AphrontListHTTPParameterType' => 'aphront/httpparametertype/AphrontListHTTPParameterType.php',
|
'AphrontListHTTPParameterType' => 'aphront/httpparametertype/AphrontListHTTPParameterType.php',
|
||||||
|
'AphrontLockTimeoutQueryException' => 'infrastructure/storage/exception/AphrontLockTimeoutQueryException.php',
|
||||||
'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php',
|
'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php',
|
||||||
'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
|
'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
|
||||||
'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
|
'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
|
||||||
|
'AphrontMySQLDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
|
||||||
'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
|
'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
|
||||||
|
'AphrontMySQLiDatabaseConnection' => 'infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
|
||||||
|
'AphrontNotSupportedQueryException' => 'infrastructure/storage/exception/AphrontNotSupportedQueryException.php',
|
||||||
'AphrontNullView' => 'view/AphrontNullView.php',
|
'AphrontNullView' => 'view/AphrontNullView.php',
|
||||||
|
'AphrontObjectMissingQueryException' => 'infrastructure/storage/exception/AphrontObjectMissingQueryException.php',
|
||||||
'AphrontPHIDHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDHTTPParameterType.php',
|
'AphrontPHIDHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDHTTPParameterType.php',
|
||||||
'AphrontPHIDListHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php',
|
'AphrontPHIDListHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php',
|
||||||
'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
|
'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
|
||||||
'AphrontPageView' => 'view/page/AphrontPageView.php',
|
'AphrontPageView' => 'view/page/AphrontPageView.php',
|
||||||
|
'AphrontParameterQueryException' => 'infrastructure/storage/exception/AphrontParameterQueryException.php',
|
||||||
'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
|
'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
|
||||||
'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
|
'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
|
||||||
'AphrontProjectListHTTPParameterType' => 'aphront/httpparametertype/AphrontProjectListHTTPParameterType.php',
|
'AphrontProjectListHTTPParameterType' => 'aphront/httpparametertype/AphrontProjectListHTTPParameterType.php',
|
||||||
'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.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',
|
'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php',
|
||||||
'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
|
'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
|
||||||
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
|
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
|
||||||
|
@ -247,6 +270,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
|
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
|
||||||
'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
|
'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
|
||||||
'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
|
'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
|
||||||
|
'AphrontSchemaQueryException' => 'infrastructure/storage/exception/AphrontSchemaQueryException.php',
|
||||||
'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
|
'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
|
||||||
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
|
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
|
||||||
'AphrontSite' => 'aphront/site/AphrontSite.php',
|
'AphrontSite' => 'aphront/site/AphrontSite.php',
|
||||||
|
@ -997,6 +1021,7 @@ phutil_register_library_map(array(
|
||||||
'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php',
|
'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php',
|
||||||
'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php',
|
'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php',
|
||||||
'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
|
'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
|
||||||
|
'DiffusionServiceRef' => 'applications/diffusion/ref/DiffusionServiceRef.php',
|
||||||
'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
|
'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
|
||||||
'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
|
'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
|
||||||
'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php',
|
'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php',
|
||||||
|
@ -4176,6 +4201,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
|
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
|
||||||
'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
|
'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
|
||||||
'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php',
|
'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php',
|
||||||
|
'PhabricatorPreambleTestCase' => 'infrastructure/util/__tests__/PhabricatorPreambleTestCase.php',
|
||||||
'PhabricatorPrimaryEmailUserLogType' => 'applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php',
|
'PhabricatorPrimaryEmailUserLogType' => 'applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php',
|
||||||
'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php',
|
'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php',
|
||||||
'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php',
|
'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php',
|
||||||
|
@ -4629,6 +4655,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
|
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
|
||||||
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
|
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
|
||||||
'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php',
|
'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php',
|
||||||
|
'PhabricatorSearchSettingsPanel' => 'applications/settings/panel/PhabricatorSearchSettingsPanel.php',
|
||||||
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
|
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
|
||||||
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
|
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
|
||||||
'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
|
'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
|
||||||
|
@ -5512,6 +5539,93 @@ phutil_register_library_map(array(
|
||||||
'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php',
|
'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php',
|
||||||
'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php',
|
'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php',
|
||||||
'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.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',
|
'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php',
|
||||||
'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php',
|
'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php',
|
||||||
'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php',
|
'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php',
|
||||||
|
@ -5587,6 +5701,7 @@ phutil_register_library_map(array(
|
||||||
'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
|
'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
|
||||||
'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php',
|
'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php',
|
||||||
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
|
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
|
||||||
|
'QueryFuture' => 'infrastructure/storage/future/QueryFuture.php',
|
||||||
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
|
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
|
||||||
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
|
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
|
||||||
'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.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_subtype' => 'applications/phid/utils.php',
|
||||||
'phid_get_type' => 'applications/phid/utils.php',
|
'phid_get_type' => 'applications/phid/utils.php',
|
||||||
'phid_group_by_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',
|
'require_celerity_resource' => 'applications/celerity/api.php',
|
||||||
|
'vqsprintf' => 'infrastructure/storage/xsprintf/qsprintf.php',
|
||||||
|
'xsprintf_query' => 'infrastructure/storage/xsprintf/qsprintf.php',
|
||||||
),
|
),
|
||||||
'xmap' => array(
|
'xmap' => array(
|
||||||
'AlmanacAddress' => 'Phobject',
|
'AlmanacAddress' => 'Phobject',
|
||||||
|
@ -5937,18 +6060,35 @@ phutil_register_library_map(array(
|
||||||
'Aphront400Response' => 'AphrontResponse',
|
'Aphront400Response' => 'AphrontResponse',
|
||||||
'Aphront403Response' => 'AphrontHTMLResponse',
|
'Aphront403Response' => 'AphrontHTMLResponse',
|
||||||
'Aphront404Response' => 'AphrontHTMLResponse',
|
'Aphront404Response' => 'AphrontHTMLResponse',
|
||||||
|
'AphrontAccessDeniedQueryException' => 'AphrontQueryException',
|
||||||
'AphrontAjaxResponse' => 'AphrontResponse',
|
'AphrontAjaxResponse' => 'AphrontResponse',
|
||||||
'AphrontApplicationConfiguration' => 'Phobject',
|
'AphrontApplicationConfiguration' => 'Phobject',
|
||||||
'AphrontBarView' => 'AphrontView',
|
'AphrontBarView' => 'AphrontView',
|
||||||
|
'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
|
||||||
'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType',
|
'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||||
'AphrontCalendarEventView' => 'AphrontView',
|
'AphrontCalendarEventView' => 'AphrontView',
|
||||||
|
'AphrontCharacterSetQueryException' => 'AphrontQueryException',
|
||||||
|
'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException',
|
||||||
|
'AphrontConnectionQueryException' => 'AphrontQueryException',
|
||||||
'AphrontController' => 'Phobject',
|
'AphrontController' => 'Phobject',
|
||||||
|
'AphrontCountQueryException' => 'AphrontQueryException',
|
||||||
'AphrontCursorPagerView' => 'AphrontView',
|
'AphrontCursorPagerView' => 'AphrontView',
|
||||||
|
'AphrontDatabaseConnection' => array(
|
||||||
|
'Phobject',
|
||||||
|
'PhutilQsprintfInterface',
|
||||||
|
),
|
||||||
|
'AphrontDatabaseTableRef' => array(
|
||||||
|
'Phobject',
|
||||||
|
'AphrontDatabaseTableRefInterface',
|
||||||
|
),
|
||||||
|
'AphrontDatabaseTransactionState' => 'Phobject',
|
||||||
|
'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException',
|
||||||
'AphrontDialogResponse' => 'AphrontResponse',
|
'AphrontDialogResponse' => 'AphrontResponse',
|
||||||
'AphrontDialogView' => array(
|
'AphrontDialogView' => array(
|
||||||
'AphrontView',
|
'AphrontView',
|
||||||
'AphrontResponseProducerInterface',
|
'AphrontResponseProducerInterface',
|
||||||
),
|
),
|
||||||
|
'AphrontDuplicateKeyQueryException' => 'AphrontQueryException',
|
||||||
'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType',
|
'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||||
'AphrontException' => 'Exception',
|
'AphrontException' => 'Exception',
|
||||||
'AphrontFileHTTPParameterType' => 'AphrontHTTPParameterType',
|
'AphrontFileHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||||
|
@ -5981,6 +6121,8 @@ phutil_register_library_map(array(
|
||||||
'AphrontHTTPSink' => 'Phobject',
|
'AphrontHTTPSink' => 'Phobject',
|
||||||
'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
|
'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
|
||||||
'AphrontIntHTTPParameterType' => 'AphrontHTTPParameterType',
|
'AphrontIntHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||||
|
'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException',
|
||||||
|
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
|
||||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
||||||
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
|
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
|
||||||
'AphrontJSONResponse' => 'AphrontResponse',
|
'AphrontJSONResponse' => 'AphrontResponse',
|
||||||
|
@ -5988,15 +6130,21 @@ phutil_register_library_map(array(
|
||||||
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
|
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
|
||||||
'AphrontListFilterView' => 'AphrontView',
|
'AphrontListFilterView' => 'AphrontView',
|
||||||
'AphrontListHTTPParameterType' => 'AphrontHTTPParameterType',
|
'AphrontListHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||||
|
'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException',
|
||||||
'AphrontMalformedRequestException' => 'AphrontException',
|
'AphrontMalformedRequestException' => 'AphrontException',
|
||||||
'AphrontMoreView' => 'AphrontView',
|
'AphrontMoreView' => 'AphrontView',
|
||||||
'AphrontMultiColumnView' => 'AphrontView',
|
'AphrontMultiColumnView' => 'AphrontView',
|
||||||
|
'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
|
||||||
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
||||||
|
'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
|
||||||
|
'AphrontNotSupportedQueryException' => 'AphrontQueryException',
|
||||||
'AphrontNullView' => 'AphrontView',
|
'AphrontNullView' => 'AphrontView',
|
||||||
|
'AphrontObjectMissingQueryException' => 'AphrontQueryException',
|
||||||
'AphrontPHIDHTTPParameterType' => 'AphrontHTTPParameterType',
|
'AphrontPHIDHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||||
'AphrontPHIDListHTTPParameterType' => 'AphrontListHTTPParameterType',
|
'AphrontPHIDListHTTPParameterType' => 'AphrontListHTTPParameterType',
|
||||||
'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
|
'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
|
||||||
'AphrontPageView' => 'AphrontView',
|
'AphrontPageView' => 'AphrontView',
|
||||||
|
'AphrontParameterQueryException' => 'AphrontQueryException',
|
||||||
'AphrontPlainTextResponse' => 'AphrontResponse',
|
'AphrontPlainTextResponse' => 'AphrontResponse',
|
||||||
'AphrontProgressBarView' => 'AphrontBarView',
|
'AphrontProgressBarView' => 'AphrontBarView',
|
||||||
'AphrontProjectListHTTPParameterType' => 'AphrontListHTTPParameterType',
|
'AphrontProjectListHTTPParameterType' => 'AphrontListHTTPParameterType',
|
||||||
|
@ -6004,6 +6152,9 @@ phutil_register_library_map(array(
|
||||||
'AphrontResponse',
|
'AphrontResponse',
|
||||||
'AphrontResponseProducerInterface',
|
'AphrontResponseProducerInterface',
|
||||||
),
|
),
|
||||||
|
'AphrontQueryException' => 'Exception',
|
||||||
|
'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException',
|
||||||
|
'AphrontRecoverableQueryException' => 'AphrontQueryException',
|
||||||
'AphrontRedirectResponse' => 'AphrontResponse',
|
'AphrontRedirectResponse' => 'AphrontResponse',
|
||||||
'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
|
'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
|
||||||
'AphrontReloadResponse' => 'AphrontRedirectResponse',
|
'AphrontReloadResponse' => 'AphrontRedirectResponse',
|
||||||
|
@ -6013,6 +6164,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontResponse' => 'Phobject',
|
'AphrontResponse' => 'Phobject',
|
||||||
'AphrontRoutingMap' => 'Phobject',
|
'AphrontRoutingMap' => 'Phobject',
|
||||||
'AphrontRoutingResult' => 'Phobject',
|
'AphrontRoutingResult' => 'Phobject',
|
||||||
|
'AphrontSchemaQueryException' => 'AphrontQueryException',
|
||||||
'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
|
'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
|
||||||
'AphrontSideNavFilterView' => 'AphrontView',
|
'AphrontSideNavFilterView' => 'AphrontView',
|
||||||
'AphrontSite' => 'Phobject',
|
'AphrontSite' => 'Phobject',
|
||||||
|
@ -6818,6 +6970,7 @@ phutil_register_library_map(array(
|
||||||
'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow',
|
'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow',
|
||||||
'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
|
'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
|
||||||
'DiffusionServeController' => 'DiffusionController',
|
'DiffusionServeController' => 'DiffusionController',
|
||||||
|
'DiffusionServiceRef' => 'Phobject',
|
||||||
'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
|
'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
|
||||||
'DiffusionSetupException' => 'Exception',
|
'DiffusionSetupException' => 'Exception',
|
||||||
'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension',
|
'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension',
|
||||||
|
@ -10530,6 +10683,7 @@ phutil_register_library_map(array(
|
||||||
),
|
),
|
||||||
'PhabricatorPolicyType' => 'PhabricatorPolicyConstants',
|
'PhabricatorPolicyType' => 'PhabricatorPolicyConstants',
|
||||||
'PhabricatorPonderApplication' => 'PhabricatorApplication',
|
'PhabricatorPonderApplication' => 'PhabricatorApplication',
|
||||||
|
'PhabricatorPreambleTestCase' => 'PhabricatorTestCase',
|
||||||
'PhabricatorPrimaryEmailUserLogType' => 'PhabricatorUserLogType',
|
'PhabricatorPrimaryEmailUserLogType' => 'PhabricatorUserLogType',
|
||||||
'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine',
|
'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine',
|
||||||
'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor',
|
'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||||
|
@ -11095,9 +11249,10 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorSearchResultBucketGroup' => 'Phobject',
|
'PhabricatorSearchResultBucketGroup' => 'Phobject',
|
||||||
'PhabricatorSearchResultView' => 'AphrontView',
|
'PhabricatorSearchResultView' => 'AphrontView',
|
||||||
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
|
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
|
||||||
'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting',
|
'PhabricatorSearchScopeSetting' => 'PhabricatorSelectSetting',
|
||||||
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
|
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
|
||||||
'PhabricatorSearchService' => 'Phobject',
|
'PhabricatorSearchService' => 'Phobject',
|
||||||
|
'PhabricatorSearchSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
|
||||||
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
|
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
|
||||||
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
|
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
|
||||||
'PhabricatorSearchTextField' => 'PhabricatorSearchField',
|
'PhabricatorSearchTextField' => 'PhabricatorSearchField',
|
||||||
|
@ -12169,6 +12324,92 @@ phutil_register_library_map(array(
|
||||||
'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
|
'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
|
||||||
'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||||
'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
'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',
|
'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType',
|
||||||
'PonderAddAnswerView' => 'AphrontView',
|
'PonderAddAnswerView' => 'AphrontView',
|
||||||
'PonderAnswer' => array(
|
'PonderAnswer' => array(
|
||||||
|
@ -12265,6 +12506,7 @@ phutil_register_library_map(array(
|
||||||
'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
|
'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
|
||||||
'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
|
'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
|
||||||
'QueryFormattingTestCase' => 'PhabricatorTestCase',
|
'QueryFormattingTestCase' => 'PhabricatorTestCase',
|
||||||
|
'QueryFuture' => 'Future',
|
||||||
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
|
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
|
||||||
'ReleephBranch' => array(
|
'ReleephBranch' => array(
|
||||||
'ReleephDAO',
|
'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/');
|
->setURI('/auth/loggedout/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($viewer->getPHID()) {
|
if ($viewer->getPHID()) {
|
||||||
return $this->newDialog()
|
$dialog = $this->newDialog()
|
||||||
->setTitle(pht('Log Out?'))
|
->setTitle(pht('Log Out?'))
|
||||||
->appendChild(pht('Are you sure you want to log out?'))
|
->appendParagraph(pht('Are you sure you want to log out?'))
|
||||||
->addSubmitButton(pht('Log Out'))
|
|
||||||
->addCancelButton('/');
|
->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('/');
|
return id(new AphrontRedirectResponse())->setURI('/');
|
||||||
|
|
|
@ -64,7 +64,7 @@ final class PhabricatorAuthListController
|
||||||
array(
|
array(
|
||||||
'href' => $this->getApplicationURI('config/new/'),
|
'href' => $this->getApplicationURI('config/new/'),
|
||||||
),
|
),
|
||||||
pht('Add Authentication Provider'))));
|
pht('Add Provider'))));
|
||||||
|
|
||||||
$crumbs = $this->buildApplicationCrumbs();
|
$crumbs = $this->buildApplicationCrumbs();
|
||||||
$crumbs->addTextCrumb(pht('Login and Registration'));
|
$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.',
|
'Wrote configuration key "%s" to database storage.',
|
||||||
$key);
|
$key);
|
||||||
} else {
|
} else {
|
||||||
$config_source = id(new PhabricatorConfigLocalSource())
|
$config_source = new PhabricatorConfigLocalSource();
|
||||||
->setKeys(array($key => $value));
|
|
||||||
|
|
||||||
$local_path = $config_source->getReadablePath();
|
$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(
|
$write_message = pht(
|
||||||
'Wrote configuration key "%s" to local storage (in file "%s").',
|
'Wrote configuration key "%s" to local storage (in file "%s").',
|
||||||
$key,
|
$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() {
|
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);
|
$host_wait_start = microtime(true);
|
||||||
|
|
||||||
$repository = $this->getRepository();
|
$repository = $this->getRepository();
|
||||||
$viewer = $this->getSSHUser();
|
$viewer = $this->getSSHUser();
|
||||||
$device = AlmanacKeys::getLiveDevice();
|
$device = AlmanacKeys::getLiveDevice();
|
||||||
|
|
||||||
// This is a write, and must have write access.
|
|
||||||
$this->requireWriteAccess();
|
|
||||||
|
|
||||||
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
|
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
->setRepository($repository)
|
->setRepository($repository)
|
||||||
->setLog($this);
|
->setLog($this);
|
||||||
|
|
||||||
$is_proxy = $this->shouldProxy();
|
$command = csprintf('git-receive-pack %s', $repository->getLocalPath());
|
||||||
if ($is_proxy) {
|
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
|
||||||
$command = $this->getProxyCommand(true);
|
|
||||||
$did_write = false;
|
|
||||||
|
|
||||||
if ($device) {
|
if ($device) {
|
||||||
$this->writeClusterEngineLogMessage(
|
$this->writeClusterEngineLogMessage(
|
||||||
pht(
|
pht(
|
||||||
"# Push received by \"%s\", forwarding to cluster host.\n",
|
"# Ready to receive on cluster host \"%s\".\n",
|
||||||
$device->getName()));
|
$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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$log = $this->newProtocolLog($is_proxy);
|
$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
|
// We've committed the write (or rejected it), so we can release the lock
|
||||||
// without waiting for the client to receive the acknowledgement.
|
// without waiting for the client to receive the acknowledgement.
|
||||||
if ($did_write) {
|
$cluster_engine->synchronizeWorkingCopyAfterWrite();
|
||||||
$cluster_engine->synchronizeWorkingCopyAfterWrite();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($caught) {
|
if ($caught) {
|
||||||
throw $caught;
|
throw $caught;
|
||||||
|
@ -85,18 +74,16 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
||||||
// When a repository is clustered, we reach this cleanup code on both
|
// 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
|
// the proxy and the actual final endpoint node. Don't do more cleanup
|
||||||
// or logging than we need to.
|
// or logging than we need to.
|
||||||
if ($did_write) {
|
$repository->writeStatusMessage(
|
||||||
$repository->writeStatusMessage(
|
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
|
||||||
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
|
PhabricatorRepositoryStatusMessage::CODE_OKAY);
|
||||||
PhabricatorRepositoryStatusMessage::CODE_OKAY);
|
|
||||||
|
|
||||||
$host_wait_end = microtime(true);
|
$host_wait_end = microtime(true);
|
||||||
|
|
||||||
$this->updatePushLogWithTimingInformation(
|
$this->updatePushLogWithTimingInformation(
|
||||||
$this->getClusterEngineLogProperty('writeWait'),
|
$this->getClusterEngineLogProperty('writeWait'),
|
||||||
$this->getClusterEngineLogProperty('readWait'),
|
$this->getClusterEngineLogProperty('readWait'),
|
||||||
($host_wait_end - $host_wait_start));
|
($host_wait_end - $host_wait_start));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $err;
|
return $err;
|
||||||
|
|
|
@ -8,6 +8,10 @@ abstract class DiffusionGitSSHWorkflow
|
||||||
private $protocolLog;
|
private $protocolLog;
|
||||||
|
|
||||||
private $wireProtocol;
|
private $wireProtocol;
|
||||||
|
private $ioBytesRead = 0;
|
||||||
|
private $ioBytesWritten = 0;
|
||||||
|
private $requestAttempts = 0;
|
||||||
|
private $requestFailures = 0;
|
||||||
|
|
||||||
protected function writeError($message) {
|
protected function writeError($message) {
|
||||||
// Git assumes we'll add our own newlines.
|
// Git assumes we'll add our own newlines.
|
||||||
|
@ -98,6 +102,8 @@ abstract class DiffusionGitSSHWorkflow
|
||||||
PhabricatorSSHPassthruCommand $command,
|
PhabricatorSSHPassthruCommand $command,
|
||||||
$message) {
|
$message) {
|
||||||
|
|
||||||
|
$this->ioBytesWritten += strlen($message);
|
||||||
|
|
||||||
$log = $this->getProtocolLog();
|
$log = $this->getProtocolLog();
|
||||||
if ($log) {
|
if ($log) {
|
||||||
$log->didWriteBytes($message);
|
$log->didWriteBytes($message);
|
||||||
|
@ -125,7 +131,131 @@ abstract class DiffusionGitSSHWorkflow
|
||||||
$message = $protocol->willReadBytes($message);
|
$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;
|
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
|
<?php
|
||||||
|
|
||||||
final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
final class DiffusionGitUploadPackSSHWorkflow
|
||||||
|
extends DiffusionGitSSHWorkflow {
|
||||||
|
|
||||||
protected function didConstruct() {
|
protected function didConstruct() {
|
||||||
$this->setName('git-upload-pack');
|
$this->setName('git-upload-pack');
|
||||||
|
@ -14,39 +15,33 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function executeRepositoryOperations() {
|
protected function executeRepositoryOperations() {
|
||||||
$repository = $this->getRepository();
|
$is_proxy = $this->shouldProxy();
|
||||||
|
if ($is_proxy) {
|
||||||
|
return $this->executeRepositoryProxyOperations($for_write = false);
|
||||||
|
}
|
||||||
|
|
||||||
$viewer = $this->getSSHUser();
|
$viewer = $this->getSSHUser();
|
||||||
|
$repository = $this->getRepository();
|
||||||
$device = AlmanacKeys::getLiveDevice();
|
$device = AlmanacKeys::getLiveDevice();
|
||||||
|
|
||||||
$skip_sync = $this->shouldSkipReadSynchronization();
|
$skip_sync = $this->shouldSkipReadSynchronization();
|
||||||
$is_proxy = $this->shouldProxy();
|
|
||||||
|
|
||||||
if ($is_proxy) {
|
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
|
||||||
$command = $this->getProxyCommand(false);
|
if (!$skip_sync) {
|
||||||
|
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->setRepository($repository)
|
||||||
|
->setLog($this)
|
||||||
|
->synchronizeWorkingCopyBeforeRead();
|
||||||
|
|
||||||
if ($device) {
|
if ($device) {
|
||||||
$this->writeClusterEngineLogMessage(
|
$this->writeClusterEngineLogMessage(
|
||||||
pht(
|
pht(
|
||||||
"# Fetch received by \"%s\", forwarding to cluster host.\n",
|
"# Cleared to fetch on cluster host \"%s\".\n",
|
||||||
$device->getName()));
|
$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);
|
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
|
||||||
|
|
||||||
$pull_event = $this->newPullEvent();
|
$pull_event = $this->newPullEvent();
|
||||||
|
@ -60,14 +55,12 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
||||||
$log->didStartSession($command);
|
$log->didStartSession($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$is_proxy) {
|
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
|
||||||
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
|
$protocol = new DiffusionGitUploadPackWireProtocol();
|
||||||
$protocol = new DiffusionGitUploadPackWireProtocol();
|
if ($log) {
|
||||||
if ($log) {
|
$protocol->setProtocolLog($log);
|
||||||
$protocol->setProtocolLog($log);
|
|
||||||
}
|
|
||||||
$this->setWireProtocol($protocol);
|
|
||||||
}
|
}
|
||||||
|
$this->setWireProtocol($protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
$err = $this->newPassthruCommand()
|
$err = $this->newPassthruCommand()
|
||||||
|
@ -89,15 +82,7 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
|
||||||
->setResultCode(0);
|
->setResultCode(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Currently, when proxying, we do not write a log on the proxy.
|
$pull_event->save();
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$err) {
|
if (!$err) {
|
||||||
$this->waitForGitClient();
|
$this->waitForGitClient();
|
||||||
|
|
|
@ -73,13 +73,13 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
|
||||||
return $this->shouldProxy;
|
return $this->shouldProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getProxyCommand($for_write) {
|
final protected function getAlmanacServiceRefs($for_write) {
|
||||||
$viewer = $this->getSSHUser();
|
$viewer = $this->getSSHUser();
|
||||||
$repository = $this->getRepository();
|
$repository = $this->getRepository();
|
||||||
|
|
||||||
$is_cluster_request = $this->getIsClusterRequest();
|
$is_cluster_request = $this->getIsClusterRequest();
|
||||||
|
|
||||||
$uri = $repository->getAlmanacServiceURI(
|
$refs = $repository->getAlmanacServiceRefs(
|
||||||
$viewer,
|
$viewer,
|
||||||
array(
|
array(
|
||||||
'neverProxy' => $is_cluster_request,
|
'neverProxy' => $is_cluster_request,
|
||||||
|
@ -89,14 +89,28 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
|
||||||
'writable' => $for_write,
|
'writable' => $for_write,
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!$uri) {
|
if (!$refs) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
pht(
|
pht(
|
||||||
'Failed to generate an intracluster proxy URI even though this '.
|
'Failed to generate an intracluster proxy URI even though this '.
|
||||||
'request was routed as a proxy request.'));
|
'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();
|
$username = AlmanacKeys::getClusterSSHUser();
|
||||||
if ($username === null) {
|
if ($username === null) {
|
||||||
|
|
|
@ -1842,6 +1842,20 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
PhabricatorUser $viewer,
|
PhabricatorUser $viewer,
|
||||||
array $options) {
|
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(
|
PhutilTypeSpec::checkMap(
|
||||||
$options,
|
$options,
|
||||||
array(
|
array(
|
||||||
|
@ -1856,7 +1870,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
|
|
||||||
$cache_key = $this->getAlmanacServiceCacheKey();
|
$cache_key = $this->getAlmanacServiceCacheKey();
|
||||||
if (!$cache_key) {
|
if (!$cache_key) {
|
||||||
return null;
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
$cache = PhabricatorCaches::getMutableStructureCache();
|
$cache = PhabricatorCaches::getMutableStructureCache();
|
||||||
|
@ -1869,7 +1883,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($uris === null) {
|
if ($uris === null) {
|
||||||
return null;
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
$local_device = AlmanacKeys::getDeviceID();
|
$local_device = AlmanacKeys::getDeviceID();
|
||||||
|
@ -1893,7 +1907,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
|
|
||||||
if ($local_device && $never_proxy) {
|
if ($local_device && $never_proxy) {
|
||||||
if ($uri['device'] == $local_device) {
|
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 we require a writable device, remove URIs which aren't writable.
|
||||||
if ($writable) {
|
if ($writable) {
|
||||||
foreach ($results as $key => $uri) {
|
foreach ($refs as $key => $ref) {
|
||||||
if (!$uri['writable']) {
|
if (!$ref->isWritable()) {
|
||||||
unset($results[$key]);
|
unset($results[$key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$results) {
|
if (!$refs) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
pht(
|
pht(
|
||||||
'This repository ("%s") is not writable with the given '.
|
'This repository ("%s") is not writable with the given '.
|
||||||
|
@ -1974,23 +1993,30 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($writable) {
|
if ($writable) {
|
||||||
$results = $this->sortWritableAlmanacServiceURIs($results);
|
$refs = $this->sortWritableAlmanacServiceRefs($refs);
|
||||||
} else {
|
} else {
|
||||||
shuffle($results);
|
$refs = $this->sortReadableAlmanacServiceRefs($refs);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = head($results);
|
return array_values($refs);
|
||||||
return $result['uri'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// See T13109 for discussion of how this method routes requests.
|
||||||
|
|
||||||
// In the absence of other rules, we'll send traffic to devices randomly.
|
// 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
|
// We also want to select randomly among nodes which are equally good
|
||||||
// candidates to receive the write, and accomplish that by shuffling the
|
// candidates to receive the write, and accomplish that by shuffling the
|
||||||
// list up front.
|
// list up front.
|
||||||
shuffle($results);
|
shuffle($refs);
|
||||||
|
|
||||||
$order = array();
|
$order = array();
|
||||||
|
|
||||||
|
@ -2002,8 +2028,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
$this->getPHID());
|
$this->getPHID());
|
||||||
if ($writer) {
|
if ($writer) {
|
||||||
$device_phid = $writer->getWriteProperty('devicePHID');
|
$device_phid = $writer->getWriteProperty('devicePHID');
|
||||||
foreach ($results as $key => $result) {
|
foreach ($refs as $key => $ref) {
|
||||||
if ($result['devicePHID'] === $device_phid) {
|
if ($ref->getDevicePHID() === $device_phid) {
|
||||||
$order[] = $key;
|
$order[] = $key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2025,8 +2051,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
}
|
}
|
||||||
$max_devices = array_fuse($max_devices);
|
$max_devices = array_fuse($max_devices);
|
||||||
|
|
||||||
foreach ($results as $key => $result) {
|
foreach ($refs as $key => $ref) {
|
||||||
if (isset($max_devices[$result['devicePHID']])) {
|
if (isset($max_devices[$ref->getDevicePHID()])) {
|
||||||
$order[] = $key;
|
$order[] = $key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2034,9 +2060,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
|
||||||
|
|
||||||
// Reorder the results, putting any we've selected as preferred targets for
|
// Reorder the results, putting any we've selected as preferred targets for
|
||||||
// the write at the head of the list.
|
// 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() {
|
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
|
<?php
|
||||||
|
|
||||||
final class PhabricatorSearchScopeSetting
|
final class PhabricatorSearchScopeSetting
|
||||||
extends PhabricatorInternalSetting {
|
extends PhabricatorSelectSetting {
|
||||||
|
|
||||||
const SETTINGKEY = 'search-scope';
|
const SETTINGKEY = 'search-scope';
|
||||||
|
|
||||||
|
@ -9,8 +9,33 @@ final class PhabricatorSearchScopeSetting
|
||||||
return pht('Search Scope');
|
return pht('Search Scope');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSettingPanelKey() {
|
||||||
|
return PhabricatorSearchSettingsPanel::PANELKEY;
|
||||||
|
}
|
||||||
|
|
||||||
public function getSettingDefaultValue() {
|
public function getSettingDefaultValue() {
|
||||||
return 'all';
|
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();
|
$actor_hashes = array();
|
||||||
foreach ($actors as $actor) {
|
foreach ($actors as $actor) {
|
||||||
$actor_hashes[] = PhabricatorHash::digestForIndex($actor);
|
$digest = PhabricatorHash::digestForIndex($actor);
|
||||||
|
$actor_hashes[$digest] = $actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
$log = new PhabricatorSystemActionLog();
|
$log = new PhabricatorSystemActionLog();
|
||||||
|
|
||||||
$window = self::getWindow();
|
$window = self::getWindow();
|
||||||
|
|
||||||
$conn_r = $log->establishConnection('r');
|
$conn = $log->establishConnection('r');
|
||||||
$scores = queryfx_all(
|
|
||||||
$conn_r,
|
$rows = queryfx_all(
|
||||||
'SELECT actorIdentity, SUM(score) totalScore FROM %T
|
$conn,
|
||||||
|
'SELECT actorHash, SUM(score) totalScore FROM %T
|
||||||
WHERE action = %s AND actorHash IN (%Ls)
|
WHERE action = %s AND actorHash IN (%Ls)
|
||||||
AND epoch >= %d GROUP BY actorHash',
|
AND epoch >= %d GROUP BY actorHash',
|
||||||
$log->getTableName(),
|
$log->getTableName(),
|
||||||
$action->getActionConstant(),
|
$action->getActionConstant(),
|
||||||
$actor_hashes,
|
array_keys($actor_hashes),
|
||||||
(time() - $window));
|
(PhabricatorTime::getNow() - $window));
|
||||||
|
|
||||||
$scores = ipull($scores, 'totalScore', 'actorIdentity');
|
$rows = ipull($rows, 'totalScore', 'actorHash');
|
||||||
|
|
||||||
foreach ($scores as $key => $score) {
|
$scores = array();
|
||||||
$scores[$key] = $score / $window;
|
foreach ($actor_hashes as $digest => $actor) {
|
||||||
|
$score = idx($rows, $digest, 0);
|
||||||
|
$scores[$actor] = ($score / $window);
|
||||||
}
|
}
|
||||||
|
|
||||||
$scores = $scores + array_fill_keys($actors, 0);
|
|
||||||
|
|
||||||
return $scores;
|
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
|
environment and some parts of Phabricator's configuration in order to fix these
|
||||||
problems and set up the environment which Phabricator expects.
|
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:
|
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
|
request, allowing you to adjust the environment. For common adjustments and
|
||||||
examples, see the next sections.
|
examples, see the next sections.
|
||||||
|
|
||||||
|
|
||||||
Adjusting Client IPs
|
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
|
all requests as originating from the load balancer, rather than from the
|
||||||
correct client IPs.
|
correct client IPs.
|
||||||
|
|
||||||
If this is the case and some other header (like `X-Forwarded-For`) is known to
|
In common cases where networks are configured like this, the `X-Forwarded-For`
|
||||||
be trustworthy, you can read the header and overwrite the `REMOTE_ADDR` value
|
header will have trustworthy information about the real client IP. You
|
||||||
so Phabricator can figure out the client IP correctly.
|
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
|
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
|
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.
|
spoof an arbitrary client IP.
|
||||||
|
|
||||||
The `X-Forwarded-For` header may also contain a list of addresses if a request
|
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
|
has been forwarded through multiple load balancers. If you know that requests
|
||||||
will usually handle most situations correctly:
|
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!
|
Note that this is very odd, advanced, and easy to get wrong. If you get it
|
||||||
// If the request came directly from a client, doing this will allow them to
|
wrong, users will most likely be able to spoof any client address.
|
||||||
// them spoof any remote address.
|
|
||||||
|
|
||||||
// The header may contain a list of IPs, like "1.2.3.4, 4.5.6.7", if the
|
```name="Custom X-Forwarded-For Handling", lang=php
|
||||||
// request the load balancer received also had this header.
|
|
||||||
|
|
||||||
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||||
$forwarded_for = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
$raw_header = $_SERVER['X_FORWARDED_FOR'];
|
||||||
if ($forwarded_for) {
|
|
||||||
$forwarded_for = explode(',', $forwarded_for);
|
$real_address = your_custom_parsing_function($raw_header);
|
||||||
$forwarded_for = end($forwarded_for);
|
|
||||||
$forwarded_for = trim($forwarded_for);
|
$_SERVER['REMOTE_ADDR'] = $raw_header;
|
||||||
$_SERVER['REMOTE_ADDR'] = $forwarded_for;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -221,6 +221,9 @@ final class PhabricatorDatabaseRef
|
||||||
return $this->replicaRefs;
|
return $this->replicaRefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDisplayName() {
|
||||||
|
return $this->getRefKey();
|
||||||
|
}
|
||||||
|
|
||||||
public function getRefKey() {
|
public function getRefKey() {
|
||||||
$host = $this->getHost();
|
$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
|
// TODO: Add a "locale.default" config option once we have some reasonable
|
||||||
// defaults which aren't silly nonsense.
|
// defaults which aren't silly nonsense.
|
||||||
self::setLocaleCode('en_US');
|
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) {
|
public static function beginScopedLocale($locale_code) {
|
||||||
|
@ -249,9 +254,17 @@ final class PhabricatorEnv extends Phobject {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stack->pushSource(
|
// See T13403. If we're starting up in "config optional" mode, suppress
|
||||||
id(new PhabricatorConfigDatabaseSource('default'))
|
// messages about connection retries.
|
||||||
->setName(pht('Database')));
|
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) {
|
} catch (AphrontSchemaQueryException $exception) {
|
||||||
// If the database is not available, just skip this configuration
|
// If the database is not available, just skip this configuration
|
||||||
// source. This happens during `bin/storage upgrade`, `bin/conf` before
|
// 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