From fb59325d2a11d016a1bbe906b4381d33ff2c52d6 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 17 May 2022 16:08:41 -0100 Subject: [PATCH] first draft Signed-off-by: Maxence Lange --- .php_cs.cache | 1 + appinfo/info.xml | 2 +- appinfo/routes.php | 7 +- drafts/federated_sync.md | 493 ++++++++++ lib/CircleSharesManager.php | 242 +++++ lib/CirclesManager.php | 52 +- lib/Command/CirclesDebug.php | 894 ++++++++++++++++++ lib/Controller/RemoteController.php | 49 + lib/Db/CoreQueryBuilder.php | 26 +- lib/Db/CoreRequestBuilder.php | 40 +- lib/Db/DebugRequest.php | 100 ++ lib/Db/DebugRequestBuilder.php | 116 +++ lib/Db/SyncedItemLockRequest.php | 61 ++ lib/Db/SyncedItemLockRequestBuilder.php | 117 +++ lib/Db/SyncedItemRequest.php | 100 ++ lib/Db/SyncedItemRequestBuilder.php | 116 +++ ...LockRequest.php => SyncedShareRequest.php} | 37 +- ...lder.php => SyncedShareRequestBuilder.php} | 54 +- .../CircleSharesManagerException.php | 36 + ...ception.php => DebugNotFoundException.php} | 2 +- .../FederatedSyncConflictException.php | 36 + .../FederatedSyncManagerNotFoundException.php | 36 + .../SyncedItemNotFoundException.php | 34 + ...n.php => SyncedShareNotFoundException.php} | 2 +- ... => SyncedSharedAlreadyExistException.php} | 2 +- lib/FederatedItems/CircleSettings.php | 85 -- .../FederatedSync/ShareCreation.php | 150 +++ lib/FederatedItems/ItemLock.php | 130 --- lib/ICircleSharesManager.php | 101 ++ ...dSync.php => IFederatedItemSyncedItem.php} | 22 +- lib/IFederatedSyncManager.php | 293 ++++++ lib/IReferencedObject.php | 34 + .../Version0025Date20220510104622.php | 243 +++++ lib/Model/Circle.php | 7 +- lib/Model/Debug.php | 286 ++++++ lib/Model/Federated/FederatedEvent.php | 48 +- lib/Model/Federated/RemoteInstance.php | 75 +- lib/Model/Member.php | 5 +- lib/Model/SyncedItem.php | 307 ++++++ lib/Model/SyncedItemLock.php | 238 +++++ lib/Model/SyncedShare.php | 157 +++ lib/Service/ConfigService.php | 3 + lib/Service/DebugService.php | 229 +++++ lib/Service/FederatedEventService.php | 76 +- lib/Service/FederatedShareService.php | 117 --- lib/Service/FederatedSyncItemService.php | 163 ++++ lib/Service/FederatedSyncService.php | 138 +++ lib/Service/FederatedSyncShareService.php | 230 +++++ lib/Service/RemoteStreamService.php | 4 + lib/Tools/ISignedModel.php | 58 ++ lib/Tools/Model/ReferencedDataStore.php | 473 +++++++++ lib/Tools/Traits/TNCSignatory.php | 30 + 52 files changed, 5878 insertions(+), 479 deletions(-) create mode 100644 .php_cs.cache create mode 100644 drafts/federated_sync.md create mode 100644 lib/CircleSharesManager.php create mode 100644 lib/Command/CirclesDebug.php create mode 100644 lib/Db/DebugRequest.php create mode 100644 lib/Db/DebugRequestBuilder.php create mode 100644 lib/Db/SyncedItemLockRequest.php create mode 100644 lib/Db/SyncedItemLockRequestBuilder.php create mode 100644 lib/Db/SyncedItemRequest.php create mode 100644 lib/Db/SyncedItemRequestBuilder.php rename lib/Db/{ShareLockRequest.php => SyncedShareRequest.php} (61%) rename lib/Db/{ShareLockRequestBuilder.php => SyncedShareRequestBuilder.php} (58%) create mode 100644 lib/Exceptions/CircleSharesManagerException.php rename lib/Exceptions/{FederatedShareBelongingException.php => DebugNotFoundException.php} (94%) create mode 100644 lib/Exceptions/FederatedSyncConflictException.php create mode 100644 lib/Exceptions/FederatedSyncManagerNotFoundException.php create mode 100644 lib/Exceptions/SyncedItemNotFoundException.php rename lib/Exceptions/{FederatedShareNotFoundException.php => SyncedShareNotFoundException.php} (94%) rename lib/Exceptions/{FederatedShareAlreadyLockedException.php => SyncedSharedAlreadyExistException.php} (94%) delete mode 100644 lib/FederatedItems/CircleSettings.php create mode 100644 lib/FederatedItems/FederatedSync/ShareCreation.php delete mode 100644 lib/FederatedItems/ItemLock.php create mode 100644 lib/ICircleSharesManager.php rename lib/{IFederatedSync.php => IFederatedItemSyncedItem.php} (73%) create mode 100644 lib/IFederatedSyncManager.php create mode 100644 lib/IReferencedObject.php create mode 100644 lib/Migration/Version0025Date20220510104622.php create mode 100644 lib/Model/Debug.php create mode 100644 lib/Model/SyncedItem.php create mode 100644 lib/Model/SyncedItemLock.php create mode 100644 lib/Model/SyncedShare.php create mode 100644 lib/Service/DebugService.php delete mode 100644 lib/Service/FederatedShareService.php create mode 100644 lib/Service/FederatedSyncItemService.php create mode 100644 lib/Service/FederatedSyncService.php create mode 100644 lib/Service/FederatedSyncShareService.php create mode 100644 lib/Tools/ISignedModel.php create mode 100644 lib/Tools/Model/ReferencedDataStore.php diff --git a/.php_cs.cache b/.php_cs.cache new file mode 100644 index 000000000..e9a2bde1e --- /dev/null +++ b/.php_cs.cache @@ -0,0 +1 @@ +{"php":"7.4.28","version":"2.19.3:v2.19.3#75ac86f33fab4714ea5a39a396784d83ae3b5ed8","indent":"\t","lineEnding":"\n","rules":{"encoding":true,"full_opening_tag":true,"blank_line_after_namespace":true,"braces":{"position_after_anonymous_constructs":"same","position_after_control_structures":"same","position_after_functions_and_oop_constructs":"same"},"class_definition":true,"constant_case":true,"elseif":true,"function_declaration":{"closure_function_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"no_break_comment":true,"no_closing_tag":true,"no_spaces_after_function_name":true,"no_spaces_inside_parenthesis":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":true,"single_import_per_statement":true,"single_line_after_imports":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"visibility_required":{"elements":["property","method","const"]},"align_multiline_comment":true,"array_indentation":true,"array_syntax":{"syntax":"short"},"binary_operator_spaces":{"default":"single_space"},"blank_line_after_opening_tag":true,"list_syntax":{"syntax":"short"},"no_unused_imports":true},"hashes":{"files\/list.php":3098306110,"templates\/files\/list.php":3952870293,"tests\/TestSuiteListener.php":2864865747,"tests\/unit\/lib\/Controller\/AdminControllerTest.php":3976347962,"tests\/unit\/lib\/Controller\/LocalControllerTest.php":1858681571,"tests\/unit\/lib\/Api\/CirclesTest.php":3757281238,"tests\/bootstrap.php":3458017747,"lib\/StatusCode.php":1517508866,"lib\/ShareByCircleProvider.php":2660302009,"lib\/Cron\/Maintenance.php":2165319515,"lib\/Cron\/GlobalSync.php":3259630900,"lib\/Cron\/ContactsExistingShares.php":1436358626,"lib\/Listeners\/UserCreated.php":2356192654,"lib\/Listeners\/GroupMemberAdded.php":1309590894,"lib\/Listeners\/GroupDeleted.php":643084160,"lib\/Listeners\/Examples\/ExampleAddingCircleMember.php":2358706464,"lib\/Listeners\/Examples\/ExampleMembershipsRemoved.php":1140636880,"lib\/Listeners\/Examples\/ExampleMembershipsCreated.php":2742947017,"lib\/Listeners\/Examples\/ExampleRequestingCircleMember.php":543434179,"lib\/Listeners\/Notifications\/RequestingMember.php":669805256,"lib\/Listeners\/GroupCreated.php":979770048,"lib\/Listeners\/Files\/AddingMemberSendMail.php":2320983638,"lib\/Listeners\/Files\/MemberAddedSendMail.php":2569100200,"lib\/Listeners\/Files\/CreatingShareSendMail.php":4004922421,"lib\/Listeners\/Files\/RemovingMember.php":1651816727,"lib\/Listeners\/Files\/PreparingMemberSendMail.php":2944165511,"lib\/Listeners\/Files\/ShareCreatedSendMail.php":1984205067,"lib\/Listeners\/Files\/PreparingShareSendMail.php":687968187,"lib\/Listeners\/Files\/DestroyingCircle.php":2272468684,"lib\/Listeners\/Files\/MembershipsRemoved.php":4232695309,"lib\/Listeners\/DeprecatedListener.php":2161897895,"lib\/Listeners\/GroupMemberRemoved.php":1275126808,"lib\/Listeners\/UserDeleted.php":1317516900,"lib\/IFederatedItemHighSeverity.php":1758644050,"lib\/GlobalScale\/MemberJoin.php":1883633825,"lib\/GlobalScale\/MemberRemove.php":284845080,"lib\/GlobalScale\/CircleDestroy.php":331971873,"lib\/GlobalScale\/GlobalSync.php":403207439,"lib\/GlobalScale\/AGlobalScaleEvent.php":1523577386,"lib\/GlobalScale\/CircleCreate.php":518670645,"lib\/GlobalScale\/Test.php":245699567,"lib\/GlobalScale\/CircleUpdate.php":3794135126,"lib\/GlobalScale\/MemberLeave.php":75784237,"lib\/GlobalScale\/MemberUpdate.php":677108294,"lib\/GlobalScale\/UserDeleted.php":1725910468,"lib\/GlobalScale\/FileUnshare.php":1548441625,"lib\/GlobalScale\/FileShare.php":3953187827,"lib\/GlobalScale\/MemberAdd.php":2607375077,"lib\/GlobalScale\/CircleStatus.php":3271681906,"lib\/GlobalScale\/GSMount\/MountManager.php":1320335712,"lib\/GlobalScale\/GSMount\/MountProvider.php":3304612192,"lib\/GlobalScale\/GSMount\/Mount.php":3718785463,"lib\/Service\/SearchService.php":3839171914,"lib\/Service\/EventService.php":2058103507,"lib\/Service\/RemoteDownstreamService.php":1551878007,"lib\/Service\/GSUpstreamService.php":3035168612,"lib\/Service\/RemoteUpstreamService.php":2856176656,"lib\/Service\/EventWrapperService.php":1147557857,"lib\/Service\/OutputService.php":4058852349,"lib\/Service\/GSDownstreamService.php":3698549562,"lib\/Service\/CircleService.php":2569416279,"lib\/Service\/SendMailService.php":3628515164,"lib\/Service\/MemberService.php":2202443321,"lib\/Service\/TimezoneService.php":436192332,"lib\/Service\/GlobalScaleService.php":3633015323,"lib\/Service\/FederatedUserService.php":2865268909,"lib\/Service\/ShareWrapperService.php":3800367712,"lib\/Service\/ContactService.php":1517469205,"lib\/Service\/InterfaceService.php":386071894,"lib\/Service\/ConfigService.php":519972026,"lib\/Service\/FederatedEventService.php":3756840608,"lib\/Service\/RemoteStreamService.php":2304067225,"lib\/Service\/NotificationService.php":3839471957,"lib\/Service\/MigrationService.php":376900923,"lib\/Service\/EventsService.php":1717181588,"lib\/Service\/ShareService.php":3232740508,"lib\/Service\/MembershipService.php":522334436,"lib\/Service\/PermissionService.php":1722876776,"lib\/Service\/MembersService.php":2747321365,"lib\/Service\/MaintenanceService.php":2008565585,"lib\/Service\/MiscService.php":1021072651,"lib\/Service\/FederatedShareService.php":2041740728,"lib\/Service\/GroupsService.php":2974167051,"lib\/Service\/SyncService.php":3373494320,"lib\/Service\/RemoteService.php":3830722518,"lib\/Service\/DavService.php":2842988121,"lib\/Service\/CirclesService.php":272950919,"lib\/Service\/ShareTokenService.php":3839540439,"lib\/Db\/EventWrapperRequestBuilder.php":1086415118,"lib\/Db\/AccountsRequest.php":3482160173,"lib\/Db\/FederatedLinksRequestBuilder.php":2656064863,"lib\/Db\/RemoteRequestBuilder.php":227744767,"lib\/Db\/CoreQueryBuilder.php":2313020751,"lib\/Db\/CircleRequestBuilder.php":1106369364,"lib\/Db\/ShareTokenRequest.php":4192031480,"lib\/Db\/CircleRequest.php":3039949294,"lib\/Db\/CircleProviderRequest.php":3817630890,"lib\/Db\/EventWrapperRequest.php":2884795734,"lib\/Db\/ShareLockRequest.php":4201457145,"lib\/Db\/CircleProviderRequestBuilder.php":2804195879,"lib\/Db\/DeprecatedRequestBuilder.php":1990711328,"lib\/Db\/FileSharesRequest.php":2138500987,"lib\/Db\/AccountsRequestBuilder.php":2398896036,"lib\/Db\/DeprecatedMembersRequestBuilder.php":3344575336,"lib\/Db\/GSSharesRequestBuilder.php":1836370644,"lib\/Db\/DeprecatedMembersRequest.php":3503766299,"lib\/Db\/MembershipRequest.php":439722937,"lib\/Db\/DeprecatedCirclesRequest.php":1164055518,"lib\/Db\/MemberRequest.php":3563956248,"lib\/Db\/MountRequest.php":1099753953,"lib\/Db\/GSSharesRequest.php":3658195235,"lib\/Db\/MemberRequestBuilder.php":3291166456,"lib\/Db\/ShareLockRequestBuilder.php":1174499547,"lib\/Db\/TokensRequestBuilder.php":2249346642,"lib\/Db\/MembershipRequestBuilder.php":4086277549,"lib\/Db\/CoreRequestBuilder.php":1970316195,"lib\/Db\/RemoteRequest.php":642979665,"lib\/Db\/FederatedLinksRequest.php":1198900323,"lib\/Db\/FileSharesRequestBuilder.php":3340254850,"lib\/Db\/DeprecatedCirclesRequestBuilder.php":2658765513,"lib\/Db\/MountRequestBuilder.php":2890867244,"lib\/Db\/ShareWrapperRequest.php":360367138,"lib\/Db\/ShareWrapperRequestBuilder.php":2948379969,"lib\/Db\/ShareTokenRequestBuilder.php":2954299403,"lib\/Db\/TokensRequest.php":2165524722,"lib\/Exceptions\/MigrationException.php":3135728899,"lib\/Exceptions\/FederatedRemoteCircleDoesNotExistException.php":3724491253,"lib\/Exceptions\/FederatedEventException.php":4268947022,"lib\/Exceptions\/FrontendException.php":3477799826,"lib\/Exceptions\/ShareTokenAlreadyExistException.php":3311853497,"lib\/Exceptions\/FakeException.php":1228679011,"lib\/Exceptions\/FederatedItemConflictException.php":1642080270,"lib\/Exceptions\/TokenDoesNotExistException.php":3986469986,"lib\/Exceptions\/FederatedShareNotFoundException.php":3306044363,"lib\/Exceptions\/FederatedItemRemoteException.php":4133160510,"lib\/Exceptions\/SingleCircleNotFoundException.php":439715431,"lib\/Exceptions\/UnknownFederatedItemException.php":1800942895,"lib\/Exceptions\/FederatedLinkDoesNotExistException.php":950685170,"lib\/Exceptions\/MemberHelperException.php":3308150237,"lib\/Exceptions\/CircleTypeNotValidException.php":3747441818,"lib\/Exceptions\/ConfigNoCircleAvailableException.php":3761163339,"lib\/Exceptions\/RemoteAlreadyExistsException.php":1662956393,"lib\/Exceptions\/FileCacheNotFoundException.php":3921852036,"lib\/Exceptions\/MountPointConstructionException.php":3646808514,"lib\/Exceptions\/MemberLevelException.php":380716406,"lib\/Exceptions\/MembersLimitException.php":2936076078,"lib\/Exceptions\/FederatedRemoteIsDownException.php":2677480615,"lib\/Exceptions\/RemoteCircleException.php":1231134117,"lib\/Exceptions\/ShareWrapperNotFoundException.php":4195344203,"lib\/Exceptions\/FederatedEventDSyncException.php":4032770513,"lib\/Exceptions\/FederatedItemBadRequestException.php":161143109,"lib\/Exceptions\/CircleAlreadyExistsException.php":4093257493,"lib\/Exceptions\/FederatedItemUnauthorizedException.php":1117239920,"lib\/Exceptions\/ShareTokenNotFoundException.php":517851422,"lib\/Exceptions\/MemberAlreadyExistsException.php":1519904708,"lib\/Exceptions\/FederatedCircleStatusUpdateException.php":3495978386,"lib\/Exceptions\/ParseMemberLevelException.php":626527872,"lib\/Exceptions\/JsonException.php":4050075292,"lib\/Exceptions\/CircleNameTooShortException.php":3504410197,"lib\/Exceptions\/FederatedItemException.php":3009723389,"lib\/Exceptions\/OwnerNotFoundException.php":1041683449,"lib\/Exceptions\/BroadcasterIsNotCompatibleException.php":1868665268,"lib\/Exceptions\/FederatedCircleLinkFormatException.php":2559586115,"lib\/Exceptions\/EventWrapperNotFoundException.php":2006798244,"lib\/Exceptions\/NotContactAddressException.php":3882857942,"lib\/Exceptions\/InvalidModelException.php":4248268090,"lib\/Exceptions\/RemoteResourceNotFoundException.php":1419686696,"lib\/Exceptions\/RemoteUidException.php":1906531318,"lib\/Exceptions\/EmailAccountInvalidFormatException.php":205206406,"lib\/Exceptions\/UnknownInterfaceException.php":1710879839,"lib\/Exceptions\/RequestBuilderException.php":3436614428,"lib\/Exceptions\/PayloadDeliveryException.php":1523296073,"lib\/Exceptions\/FederatedItemNotFoundException.php":2519165628,"lib\/Exceptions\/MemberNotFoundException.php":3494615888,"lib\/Exceptions\/ModeratorIsNotHighEnoughException.php":1600152488,"lib\/Exceptions\/MemberIsNotOwnerException.php":279323812,"lib\/Exceptions\/RemoteInstanceException.php":1007113206,"lib\/Exceptions\/FederatedUserException.php":1016160639,"lib\/Exceptions\/CircleTypeDisabledException.php":2509021574,"lib\/Exceptions\/MembershipNotFoundException.php":1478290916,"lib\/Exceptions\/ContactFormatException.php":900862783,"lib\/Exceptions\/CircleTypeIsEmptyException.php":198438215,"lib\/Exceptions\/FederatedRemoteDoesNotAllowException.php":3266751647,"lib\/Exceptions\/JsonNotRequestedException.php":1670599906,"lib\/Exceptions\/FederatedUserNotFoundException.php":1823543263,"lib\/Exceptions\/FederatedLinkCreationException.php":3553406001,"lib\/Exceptions\/SuperSessionException.php":4199586510,"lib\/Exceptions\/NotLocalMemberException.php":3409966422,"lib\/Exceptions\/SharingFrameSourceCannotBeAppCirclesException.php":3162211457,"lib\/Exceptions\/SharingFrameAlreadyExistException.php":1973362236,"lib\/Exceptions\/ApiVersionIncompatibleException.php":466252244,"lib\/Exceptions\/GSKeyException.php":363258347,"lib\/Exceptions\/InitiatorNotConfirmedException.php":2213761202,"lib\/Exceptions\/ContactNotFoundException.php":225495792,"lib\/Exceptions\/ModelException.php":3502810125,"lib\/Exceptions\/MemberCantJoinCircleException.php":1453191618,"lib\/Exceptions\/MountNotFoundException.php":163443323,"lib\/Exceptions\/CircleNotFoundException.php":2091462126,"lib\/Exceptions\/FederatedLinkCircleNotFoundException.php":3106945948,"lib\/Exceptions\/LinkedGroupNotAllowedException.php":530078929,"lib\/Exceptions\/FederatedShareBelongingException.php":154025487,"lib\/Exceptions\/InsufficientPermissionException.php":2624917449,"lib\/Exceptions\/GroupNotFoundException.php":695595798,"lib\/Exceptions\/GlobalScaleDSyncException.php":37698037,"lib\/Exceptions\/GroupCannotBeOwnerException.php":3873200201,"lib\/Exceptions\/UserTypeNotFoundException.php":507301615,"lib\/Exceptions\/GSStatusException.php":3782067941,"lib\/Exceptions\/MemberIsOwnerException.php":26260482,"lib\/Exceptions\/MemberIsNotAdminException.php":1789519602,"lib\/Exceptions\/MemberIsNotModeratorException.php":3142953152,"lib\/Exceptions\/InvalidIdException.php":326696496,"lib\/Exceptions\/FederatedShareAlreadyLockedException.php":2200365589,"lib\/Exceptions\/GroupDoesNotExistException.php":2813605874,"lib\/Exceptions\/CommandMissingArgumentException.php":888054115,"lib\/Exceptions\/UnknownRemoteException.php":906713777,"lib\/Exceptions\/InitiatorNotFoundException.php":3982863268,"lib\/Exceptions\/MemberDoesNotExistException.php":1306729406,"lib\/Exceptions\/MemberIsBlockedException.php":3825208092,"lib\/Exceptions\/FederatedCircleNotAllowedException.php":3825954080,"lib\/Exceptions\/CircleDoesNotExistException.php":4128272286,"lib\/Exceptions\/GlobalScaleEventException.php":3605272841,"lib\/Exceptions\/FederatedLinkUpdateException.php":110716268,"lib\/Exceptions\/RemoteNotFoundException.php":4061282988,"lib\/Exceptions\/FederatedItemServerException.php":3273933160,"lib\/Exceptions\/MissingKeyInArrayException.php":4090709890,"lib\/Exceptions\/SharingFrameDoesNotExistException.php":122635795,"lib\/Exceptions\/FederatedItemForbiddenException.php":4292004348,"lib\/Exceptions\/ContactAddressBookNotFoundException.php":3577466788,"lib\/Exceptions\/MemberTypeCantEditLevelException.php":4230209291,"lib\/Exceptions\/MaintenanceException.php":3331759326,"lib\/Exceptions\/SharingFrameAlreadyDeliveredException.php":1071708694,"lib\/Exceptions\/CircleNameFirstCharException.php":2069577355,"lib\/IFederatedItemMustBeInitializedLocally.php":2985817742,"lib\/IFederatedItemLoopbackTest.php":3603051499,"lib\/Command\/CirclesConfig.php":2488709766,"lib\/Command\/CirclesReport.php":2364648697,"lib\/Command\/CirclesJoin.php":3121495449,"lib\/Command\/CirclesSync.php":2046514547,"lib\/Command\/FixUniqueId.php":296260002,"lib\/Command\/SyncContact.php":344240517,"lib\/Command\/SharesFiles.php":1897893678,"lib\/Command\/CirclesDetails.php":971987540,"lib\/Command\/MembersSearch.php":3614851548,"lib\/Command\/MembersAdd.php":2007173529,"lib\/Command\/CirclesRemote.php":3281019039,"lib\/Command\/CirclesDestroy.php":254004564,"lib\/Command\/CirclesCreate.php":4182130016,"lib\/Command\/MembersList.php":490163638,"lib\/Command\/CirclesLeave.php":3232797054,"lib\/Command\/CirclesTest.php":653579852,"lib\/Command\/MembersRemove.php":2552408237,"lib\/Command\/CirclesList.php":1111882427,"lib\/Command\/MembersDetails.php":3900946282,"lib\/Command\/Groups.php":855176257,"lib\/Command\/CirclesMemberships.php":3289350223,"lib\/Command\/CirclesEdit.php":3117317224,"lib\/Command\/CirclesCheck.php":3164036220,"lib\/Command\/CirclesSetting.php":993767465,"lib\/Command\/CirclesMaintenance.php":4058568107,"lib\/Command\/MembersLevel.php":2854814447,"lib\/IFederatedItemMemberOptional.php":405335139,"lib\/Activity\/ProviderSubjectMember.php":3836737211,"lib\/Activity\/ProviderSubjectGroup.php":71629136,"lib\/Activity\/ProviderParser.php":1325018371,"lib\/Activity\/ProviderSubjectCircle.php":4197460670,"lib\/Activity\/SettingAsNonMember.php":38353465,"lib\/Activity\/ProviderSubjectLink.php":246975354,"lib\/Activity\/SettingAsMember.php":1239138452,"lib\/Activity\/Provider.php":1587460034,"lib\/Activity\/Filter.php":3360568612,"lib\/Activity\/SettingAsModerator.php":1229660981,"lib\/IFederatedSync.php":2126007987,"lib\/IFederatedItemSharedItem.php":3906925967,"lib\/IFederatedItemShareManagement.php":1785366796,"lib\/CirclesManager.php":160574126,"lib\/IFederatedItemInitiatorMembershipNotRequired.php":1517055476,"lib\/ShareByCircleProviderDeprecated.php":2244310573,"lib\/IFederatedItemMemberRequired.php":1936688799,"lib\/Handlers\/WebfingerHandler.php":4056953625,"lib\/Search\/FederatedUsers.php":2283220058,"lib\/Search\/GlobalScaleUsers.php":3287167603,"lib\/Search\/LocalUsers.php":2672740033,"lib\/Search\/Contacts.php":2657873953,"lib\/Search\/LocalGroups.php":655714057,"lib\/IFederatedItemInitiatorCheckNotRequired.php":2323721215,"lib\/IFederatedItemCircleCheckNotRequired.php":2686907469,"lib\/MountManager\/CircleMountManager.php":42382,"lib\/MountManager\/CircleMount.php":3297487901,"lib\/MountManager\/CircleMountProvider.php":3100107888,"lib\/IFederatedItemDataRequestOnly.php":2615199436,"lib\/Circles\/FileSharingBroadcaster.php":3376763357,"lib\/IFederatedUser.php":2664730507,"lib\/IFederatedItemLimitedToInstanceWithMembership.php":790702277,"lib\/UnifiedSearch\/UnifiedSearchResult.php":2647724377,"lib\/UnifiedSearch\/UnifiedSearchProvider.php":887662080,"lib\/AppInfo\/Capabilities.php":1677809034,"lib\/AppInfo\/Application.php":2416266824,"lib\/Collaboration\/v2\/CollaboratorSearchPlugin.php":1989445649,"lib\/IFederatedModel.php":4251218432,"lib\/Migration\/Migration.php":3153592806,"lib\/Migration\/ImportOwncloudCustomGroups.php":1942453221,"lib\/Migration\/Version0022Date20220526111723.php":664705595,"lib\/Migration\/Version0024Date20220203123902.php":415138860,"lib\/Migration\/Version0023Date20211216113101.php":937534026,"lib\/Migration\/Version0024Date20220203123901.php":3038501296,"lib\/Migration\/Version0024Date20220317190331.php":1939137606,"lib\/Migration\/Version0022Date20220703115023.php":2337341183,"lib\/Migration\/Version0022Date20220526113601.php":1773317857,"lib\/FederatedItems\/MemberLevel.php":1233608704,"lib\/FederatedItems\/MemberDisplayName.php":2407238499,"lib\/FederatedItems\/CircleSetting.php":22919417,"lib\/FederatedItems\/CircleConfig.php":1233529358,"lib\/FederatedItems\/CircleEdit.php":1868024238,"lib\/FederatedItems\/MemberRemove.php":3156308056,"lib\/FederatedItems\/CircleDestroy.php":2846866196,"lib\/FederatedItems\/SharedItemsSync.php":4190894639,"lib\/FederatedItems\/MassiveMemberAdd.php":1679750120,"lib\/FederatedItems\/ItemLock.php":2015208856,"lib\/FederatedItems\/Files\/FileUnshare.php":1522956073,"lib\/FederatedItems\/Files\/FileShare.php":2487680888,"lib\/FederatedItems\/CircleCreate.php":2450296473,"lib\/FederatedItems\/CircleLeave.php":306486166,"lib\/FederatedItems\/CircleJoin.php":3611832830,"lib\/FederatedItems\/CircleSettings.php":2388414692,"lib\/FederatedItems\/LoopbackTest.php":304425936,"lib\/FederatedItems\/SingleMemberAdd.php":850252658,"lib\/ISearch.php":120915058,"lib\/Controller\/AdminController.php":2931854213,"lib\/Controller\/RemoteController.php":3729341881,"lib\/Controller\/EventWrapperController.php":853491259,"lib\/Controller\/LocalController.php":3155140627,"lib\/IEntity.php":3181412256,"lib\/IFederatedItem.php":3976108387,"lib\/Notification\/Notifier.php":2608559573,"lib\/Api\/v1\/Circles.php":852660927,"lib\/IFederatedItemMemberCheckNotRequired.php":3452777671,"lib\/Tools\/Db\/ExtendedQueryBuilder.php":2515334429,"lib\/Tools\/Db\/IQueryRow.php":2184964271,"lib\/Tools\/Exceptions\/RequestServerException.php":1035169003,"lib\/Tools\/Exceptions\/MalformedArrayException.php":3718624740,"lib\/Tools\/Exceptions\/InvalidOriginException.php":4000904630,"lib\/Tools\/Exceptions\/RequestContentException.php":1746252892,"lib\/Tools\/Exceptions\/SignatoryException.php":339962206,"lib\/Tools\/Exceptions\/RequestResultSizeException.php":1058757206,"lib\/Tools\/Exceptions\/RowNotFoundException.php":3492704175,"lib\/Tools\/Exceptions\/ItemNotFoundException.php":15984198,"lib\/Tools\/Exceptions\/ArrayNotFoundException.php":119469321,"lib\/Tools\/Exceptions\/RequestResultNotJsonException.php":1819604022,"lib\/Tools\/Exceptions\/SignatureException.php":4135972970,"lib\/Tools\/Exceptions\/InvalidItemException.php":2697814382,"lib\/Tools\/Exceptions\/WellKnownLinkNotFoundException.php":4159121169,"lib\/Tools\/Exceptions\/RequestNetworkException.php":453246520,"lib\/Tools\/Exceptions\/UnknownTypeException.php":3776373339,"lib\/Tools\/Exceptions\/DateTimeException.php":3819558445,"lib\/Tools\/Traits\/TNCSetup.php":2087128303,"lib\/Tools\/Traits\/TDeserialize.php":40344778,"lib\/Tools\/Traits\/TStringTools.php":195491532,"lib\/Tools\/Traits\/TConsoleTree.php":1953045507,"lib\/Tools\/Traits\/TArrayTools.php":107065102,"lib\/Tools\/Traits\/TAsync.php":2150747497,"lib\/Tools\/Traits\/TNCRequest.php":2685477491,"lib\/Tools\/Traits\/TNCWellKnown.php":682537172,"lib\/Tools\/Traits\/TNCLocalSignatory.php":1369452634,"lib\/Tools\/Traits\/TNCLogger.php":3438905615,"lib\/Tools\/Traits\/TNCSignatory.php":3204722111,"lib\/Tools\/IDeserializable.php":1840963260,"lib\/Tools\/ActivityPub\/NCSignature.php":3412759612,"lib\/Tools\/Model\/NCRequest.php":2836813437,"lib\/Tools\/Model\/NCRequestResult.php":4161785829,"lib\/Tools\/Model\/NCWebfinger.php":4043179298,"lib\/Tools\/Model\/NCSignatory.php":460204014,"lib\/Tools\/Model\/NCWellKnownLink.php":1203534144,"lib\/Tools\/Model\/SimpleDataStore.php":3794015914,"lib\/Tools\/Model\/Request.php":574047815,"lib\/Tools\/Model\/NCSignedRequest.php":3755998710,"lib\/Tools\/Model\/TreeNode.php":407985192,"lib\/IFederatedItemAsyncProcess.php":1768763470,"lib\/Events\/RequestingCircleMemberEvent.php":3995250,"lib\/Events\/AddingCircleMemberEvent.php":2383152349,"lib\/Events\/EditingCircleEvent.php":244866671,"lib\/Events\/MembershipsCreatedEvent.php":3448510229,"lib\/Events\/PreparingCircleMemberEvent.php":3019540765,"lib\/Events\/SharedItemsSyncRequestedEvent.php":1101995147,"lib\/Events\/CircleResultGenericEvent.php":1473683824,"lib\/Events\/CircleMemberGenericEvent.php":415119142,"lib\/Events\/CircleDestroyedEvent.php":3277551699,"lib\/Events\/CircleMemberEditedEvent.php":40903193,"lib\/Events\/CircleEditedEvent.php":3066634986,"lib\/Events\/CircleMemberRequestedEvent.php":1924785239,"lib\/Events\/DestroyingCircleEvent.php":3735477557,"lib\/Events\/MembershipsEditedEvent.php":2299609965,"lib\/Events\/MembershipsRemovedEvent.php":2651659751,"lib\/Events\/Files\/CreatingFileShareEvent.php":254806223,"lib\/Events\/Files\/FileShareCreatedEvent.php":1470935004,"lib\/Events\/Files\/PreparingFileShareEvent.php":2169101426,"lib\/Events\/CircleCreatedEvent.php":1117733505,"lib\/Events\/CircleGenericEvent.php":1559155969,"lib\/Events\/CreatingCircleEvent.php":2306768285,"lib\/Events\/CircleMemberRemovedEvent.php":133490551,"lib\/Events\/CircleMemberAddedEvent.php":2658217316,"lib\/Events\/RemovingCircleMemberEvent.php":3385682802,"lib\/Events\/EditingCircleMemberEvent.php":1543325352,"lib\/IQueryProbe.php":2865868626,"lib\/CirclesQueryHelper.php":2098331520,"lib\/IBroadcaster.php":2546838599,"lib\/IFederatedItemMemberEmpty.php":213782023,"lib\/Model\/GlobalScale\/GSShareMountpoint.php":3315247971,"lib\/Model\/GlobalScale\/GSEvent.php":137224992,"lib\/Model\/GlobalScale\/GSWrapper.php":2719296814,"lib\/Model\/GlobalScale\/GSShare.php":4200473692,"lib\/Model\/Federated\/EventWrapper.php":1467861897,"lib\/Model\/Federated\/FederatedEvent.php":3711061173,"lib\/Model\/Federated\/RemoteInstance.php":482160017,"lib\/Model\/Federated\/FederatedShare.php":2811421953,"lib\/Model\/SharesToken.php":2861357059,"lib\/Model\/Probes\/MemberProbe.php":3768197157,"lib\/Model\/Probes\/CircleProbe.php":339995689,"lib\/Model\/Probes\/BasicProbe.php":2973275655,"lib\/Model\/Member.php":1252239317,"lib\/Model\/Helpers\/MemberHelper.php":3324556941,"lib\/Model\/DavCard.php":2581584539,"lib\/Model\/SharingFrame.php":1594937260,"lib\/Model\/FederatedLink.php":1909955777,"lib\/Model\/Report.php":3444277527,"lib\/Model\/Circle.php":1299456529,"lib\/Model\/FileCacheWrapper.php":1035533403,"lib\/Model\/DeprecatedMember.php":2374518110,"lib\/Model\/Mountpoint.php":3948252563,"lib\/Model\/ShareToken.php":1946010418,"lib\/Model\/SearchResult.php":3551878101,"lib\/Model\/BaseCircle.php":32534729,"lib\/Model\/ModelManager.php":3164044928,"lib\/Model\/ManagedModel.php":1462064976,"lib\/Model\/ShareWrapper.php":683879466,"lib\/Model\/BaseMember.php":1026462325,"lib\/Model\/Mount.php":40054916,"lib\/Model\/FederatedUser.php":1206269253,"lib\/Model\/Membership.php":2863837345,"lib\/Model\/DeprecatedCircle.php":3434937094,"appinfo\/routes.php":2739162420,"lib\/Cron\/MaintenanceHeavy.php":806985314}} \ No newline at end of file diff --git a/appinfo/info.xml b/appinfo/info.xml index ac19d5fbf..632124894 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -66,7 +66,7 @@ Those groups of users (or "circles") can then be used by any other app for shari OCA\Circles\Command\MembersDetails OCA\Circles\Command\MembersLevel OCA\Circles\Command\MembersRemove - + OCA\Circles\Command\CirclesDebug diff --git a/appinfo/routes.php b/appinfo/routes.php index 9b4b6d21e..7597b8409 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -112,6 +112,11 @@ ['name' => 'Remote#members', 'url' => '/members/{circleId}/', 'verb' => 'GET'], ['name' => 'Remote#member', 'url' => '/member/{type}/{userId}/', 'verb' => 'GET'], ['name' => 'Remote#inherited', 'url' => '/inherited/{circleId}/', 'verb' => 'GET'], - ['name' => 'Remote#memberships', 'url' => '/memberships/{circleId}/', 'verb' => 'GET'] + ['name' => 'Remote#memberships', 'url' => '/memberships/{circleId}/', 'verb' => 'GET'], + + ['name' => 'Remote#syncItem', 'url' => '/sync/item', 'verb' => 'POST'], + ['name' => 'Remote#syncShare', 'url' => '/sync/share', 'verb' => 'POST'], + ['name' => 'Remote#debugDaemon', 'url' => '/debug', 'verb' => 'POST'] + ] ]; diff --git a/drafts/federated_sync.md b/drafts/federated_sync.md new file mode 100644 index 000000000..1315bd4ca --- /dev/null +++ b/drafts/federated_sync.md @@ -0,0 +1,493 @@ +**Version 1.0.0 - 08/05/22 13:29** + +- comments on this version: + * it might not be necessary to have a random key string generated as `itemId`; `itemId` can be + auto-incremented, + * a use-case where data are de-sync between `Circles` and an app using this feature (ie. app failing + to fulfill the destruction of a remote share when requested) is fixed by adding a `deleted` + field/entry to `circles_item`. When an `item` is in the process of deletion, the entry related + to `itemId` in `circles_item` is flag as `deleted` and will only be deleted from the table when + the `IFederatedSyncManager` returns `ItemNotFoundException` on `serializeItem(itemId)`. No action + can be initiated on an `item` flag as `deleted` + +# Federated Sync + +The concept of **Federated Sync** is to provide enough tools for an app to share its own content over +multiple instances of Nextcloud. + +We call `item` a chunk of data identified by a unique id which represent an entry that can be shared. + +> If we take the Deck App as example, an `item` is a board with attached cards and comments. A user can +> create a board and share it. Recipients to the share have access to the board, cards and comments. + +The data of a shared `item` is copied in the database of every instances that host at least one `User` +whom is a member of the circles the `item` is shared to. + +To implements **Federated Sync**, the app must comply to some prerequisites: + +- Create a class that implements [`IFederatedSyncManager`](../lib/IFederatedSyncManager.php), +- Use [`CircleSharesManager`](../lib/ICircleSharesManager.php) to notify `Circles` when creating or + altering a share or an item. + +_Note:_ the current sync does not support `partial update`, which can be implemented in the future with +a `IFederatedPartialSyncManager` that will add few methods to the `IFederatedSyncManager`. + +### Known limits + +`Circles` allow the creating of circles over instances not necessarily available to each others as long +as the master instance of the circle (the instance the owner of the circle belongs to) can reach every +other instances. +However, sharing will require direct exchange with other instances from the instance the `item` have been +created. + +**A shared item will only be available between 2 instances if both instances are known to each other** ( +cf. `./occ circles:remote`) + +_In case of a circle grouping internal and external instances, the only solution will come from the +users: the shared item must be created on the same instance the owner of the circle belongs to._ + +# Implementing in an app + +The app need to register a _Federated Sync Manager_: + +``` +class Application extends App implements IBootstrap { + public function boot(IBootContext $context): void { + /** @var CirclesManager $circleManager */ + $circleManager = $context->getAppContainer()->get(CirclesManager::class); + $circleManager->getShareManager() + ->registerFederatedSyncManager(TestFederatedSync::class); + } +} +``` + +`TestFederatedSync` is a local class that implements `IFederatedSyncManager` which will be used +by `Circles` to communicate with the app + +### Full Support or Lazy Implementation ? + +As we will see, the concept of **Federated Sync** requires the app to register a `IFederatedSyncManager` +able to: + +- `syncItem(itemId, serializeData);` create/update item based on serializedData +- `serializeItem(itemId): array;` serialize an item based on itemId +- `deleteItem(itemId);` delete item based on itemId +- `isShareCreatable(itemId, circleId, extraData, membership): bool;` confirm a share can be generated by + this member +- `onShareCreation(itemId, circleId, extraData, membership)` create a new share +- `isShareModifiable(itemId, circleId, &extraData, membership)` confirm a share can be edited by this + member +- `onShareModification(itemId, circleId, extraData, membership)` update a share +- `isShareDeletable(itemId, circleId, extraData, membership): bool;` confirm a share can be deleted by + this member +- `onShareDeletion(itemId, circleId, memberships)` delete a share +- `isItemUpdatable(itemId, &serializedData, federatedUser): bool;` confirm an item is modifiable by this + user +- `updateItem(itemId, extraData, federatedUser)` + and the app to use `ICircleSharesManager` when: + +- a share is created using `createShare(itemId, circleId, extraData)` +- a share is updated using `updateShare(itemId, circleId, extraData)` +- a share is deleted using `deleteShare(itemId, circleId, extraData)` +- an item is updated using `updateItem(itemId, extraDataData)` +- an item is deleted using `deleteItem(itemId)` + +If is possible that the code used when doing most of those action might already exist in the external +app, and this code is called directly by the `Controller` as direct interaction with the front-end. + +We can assume that the code is structured as: + +- first, _verification of the feasibility of the action_, +- second, _proceed to the requested action_. + +When implementing **Federated Sync**, it is considered **FullSupport** to move all the code into the +generated `IFederatedSyncManager` and call the right methods from `ICircleSharesManager` at the right +time. Each process will run the same on every instance of the circle. + +The **LazyImplementation** is to keep the current code, and only notify `Circles` after the legacy code +of the app is done In that case, some method from `IFederatedSyncManager` related to the action to be +performed will not be run on the instance that initiate the action. + +> For a better understanding, and with the example of the Deck App, this is a quick overview of creating a new share +> In FullSupport: +> * user create a new share, +> * Deck's controller calls `createShare()` from `Circles` +> * `Circles` run `isShareCreatable()` +> * `Circles run `onShareCreation()` on every instance +> +> In LazyImplementation: +> * user create a new share, +> * Deck's controller confirm creation and fill the database or throw Exception if not possible +> * Deck calls `createShare()` from `Circles` +> * Circles run `isShareCreatable()` +> * Circles run `onShareCreation()` on every instance but the one that initiate the process + +While it is fully advice to use the **FullSupport**, both implementation should safely work on Single +Instance and GlobalScale. + +# Database, tables + +`circles_item`: + +- **id** +- **single_id** +- **instance** +- **app_id** +- **item_type** +- **item_id** +- **checksum** + +- `single_id` and `item_id` are each unique. +- `checksum` is a checksum of the current content of the shared item. It is used to compare version on + sync. +- `item_id` is `varchar(32)` and must be filled with a random string generated by the app. The app must + also store this `item_id` in the related table. + +`circles_share`: + +- **id** +- **single_id** +- **circle_id** + +- [`single_id`, `circle_id`] is a unique key pair + +`circles_lock`: + +- **id** +- **single_id** +- **update_type** +- **update_type_id** +- **time** + +- [`single_id`, `update_type`, `update_type_id`] is a unique key pair + +# Basic Knowledge about the Circles App + +quick reminder on the technology used by `Circles` that will be used in the process + +### IEntity and SingleId + +`Circles` allows to identify an entity from the instance by a _unique single id_, making sharing simpler +as the same table and code can be used for a share to a user or to a grouping of accounts. + +### Signed request + +When `Circles` from `instance1` is requesting `instance2`, the payload is signed using a private/public +key pairs, available through `/.well-known/webfinger`. Meaning that: + +- `instance2` can confirm the origin of the request as `instance1`, +- `instance1` can request a proof of ownership on the key pair when requesting `/.well-known/webfinger`. + +### IFederatedItem + +`IFederatedItem` is an existing process to exchange data related to a circle, making the master instance +of the circle (the one the owner of the circle belongs to) to be requested when running any operation on +a circle (example: add, edit, remove members). Once an operation is confirmed by master instance, every +other instances from the circle will be requested to run the exact same creation/modification process. + +This technology will be used by **Federated Sync** to broadcast some event to every instance of a circle. +However, the data itself will only be exchanged by direct request to the instance that host the +original `item` from each other instances. + +# Proof of concept, Analysis + +Draft of how **Federated Sync** should handle each sharing action while providing safe exchange of the +data: + +- `instanceX` describe the Circles App on Nextcloud number X, +- `userXY` is a user on Nextcloud number X, +- `circleX` is a circle owned by a user from Nextcloud number X, +- `appX` is the app on Nextcloud number X, + +In this draft, the step-by-step description of each process will be displayed in a table with 3 columns +that represent 3 different instances of Nextcloud named `Nextcloud 1`, `Nextcloud 2`,`Nextcloud 3`. The +flow of the process must be read from top to bottom. + +### New item, new share. + +This process describe: + +- the process of sharing a new item, +- the process of sharing an existing item already shared to another circle. +- the process of resharing a local item. + +Nextcloud 1 | Nextcloud 2 | Nextcloud 3 +---|---|--- +| | `user2` is owner of `circle2` | +| `user1` is member of `circle2` | | `user3` is member of `circle2` +| | +| `user1` share `item1` | +| `app1` calls `createShare(itemId, circleId, extraData): void;` | +| `instance1` verify `itemId` already exists in `circles_item`. If it does, compare with `appId`, `itemType`, extract `itemSingleId` and see if `instance` is local. (see `Action: Sharing a non-local item`) +| if `itemSingleId` is known, search for existing shares in `circles_share` based on `itemSingleId`, `circleId`. +| `instance1` get membership based on current session `FederatedUser`, `circleId` and request `app1` to verify share can be created using `isShareCreatable(itemId, circleId, extraData, Membership): bool;` +|`app1` confirms the share is creatable. +| if `itemSingleId` is not known, `instance1` generate a new one and created tne entry in `circles_item` using `itemSingleId`, `instance` (local), `appId`, `itemType`, `itemId` +| `instance1` creates an entry in `circles_share` using `itemSingleId` and `circleId` +| if `fullSupport`, `instance1` uses `onShareCreation(itemId, circleId, extraData, Membership): void;`and `app1` generate entries in its own shares table. +| `instance1` create a `IFederatedItem` using `circleId` containing `itemSingleId`, `appId`, `itemType`, `itemId` and request the master instance for `circle2` +| | +| _Async process at this point._ +| | +| | as master instance for `circle2`, `instance2` receive the `IFederatedItem`. +| | `instance2` verify `itemSingleId` exists in `circles_item`. If it does, compare with `appId`, `itemType`, `itemId` and confirm `instance`=`instance1`. +| | if `itemSingleId` is not known, create a new entry in `circles_item`. +| | `instance2` request all instances available in `circle2` and broadcast the `IFederatedItem` +| `instance1` will ignore the request | `instance2` will run the exact same process than `instance3` | `instance3` verify `itemSingleId` exists in `circles_item`. If it does, compare with `appId`, `itemType`, `itemId` and confirm `instance`=`instance1`. +| | | `instance3` send a signed request to `instance1` to retrieve content of the shared item, based on `itemSingleId`, `appId`, `itemType`, `itemId`, `circleId`. +| `instance1` confirms at least one user from `instance3` belongs to one of the circles `itemSingleId` is shared to using `circles_share` +| `instance1` returns serialized data of the item using `serializeItem(itemId)`, and its checksum. +| | | if `itemSingleId` was not known, and after confirmation from `instance1`, store data in `circles_item`. +| | | if `itemSingleId` was known, `instance3` compare `checksum`. If different to the one stored in `circles_item`, use `syncItem(itemId, serializedData): void;` and update `checksum` +| | | `app3` get serialized data and store it in its own table. +| | | `instance3` search for existing shares in `circles_share` based on `itemSingleId`, `circleId`. exit process if found. +| | | `instance3` store data in `circles_share` +| | | `instance3` communicate the new share to `app3` using `onShareCreation(itemId, circleId, extraData)` +| | | `app3` generate/update an entry in its own shares table + +### Sharing a non-local item + +This process describe: + +- the process of sharing a remote item, +- the process of sharing a local item + +We can assume that re-sharing is creating a share on an `item` that does not belong to the `initiator` of +the request. + +Nextcloud 1 | Nextcloud 2 | Nextcloud 3 +---|---|--- +| | `user2` is owner of `circle2` | +| `user1` is member of `circle2` | | `user3` is member of `circle2` +| | `user2` is owner of `circle2b` +| `user1` is member of `circle2b` | | `user3` is member of `circle2b` +| `user1` share `item1` to `circle2` with `permissions` +| | | `user3` share `item1` to `circle2b` +| | | `app3` calls `createShare(itemId, circleId, extraData): void;` +| | | `instance3` get `itemSingleId` from `circles_item`, based on `appId`, `itemType`, `itemId` see that `instance` is not local. (for local, see `Action: New item, new share`) +| | | `instance3` search for existing shares in `circles_share` based on `itemSingleId`, `circleId`. +| | | `instance3` request `instance1` about the new share using `itemSingleId`, `circleId`, `extraData`, `FederatedUser` (current session) +| `instance1` confirm `itemSingleId` exists in `circles_item` and `instance` is `instance1`. get `appId`, `itemType`, `itemId` +| `instance1` confirms `FederatedUser` is known and `instance` is `instance3` and belongs to one of the circle `itemSingleId` is shared to in `circles_share`. +| `instance1` get membership based on `FederatedUser`, `circleId` and request `app1` to verify share can be created using `isShareCreatable(itemId, circleId, extraData, Membership): bool;` +|`app1` confirms the share is creatable, based on other memberships of `initiator` that can be extract from `Membership` and its `FederatedUser` to verify permissions +| `instance1` creates an entry in `circles_share` using `itemSingleId` and `circleId` +| `instance1` uses `onShareCreation(itemId, circleId, extraData, Membership): void;` and `app1` generate entries in its own shares table. +| `instance1` create a `IFederatedItem` using `circleId` containing `itemSingleId`, `appId`, `itemType`, `itemId` and request the master instance for `circle2b` +| | +| _Async process at this point._ +| | | `instance1` confirmed the creation of the share and `instance3` will create an entry in `circles_share` and if no conflict run `onShareCreation(itemId, circleId, extraData)`. +| | +| | as master instance for `circle2b`, `instance2` receive the `IFederatedItem`. +| | `instance2` verify `itemSingleId` exists in `circles_item`. If it does, compare with `appId`, `itemType`, `itemId` and confirm `instance`=`instance1`. +| | if `itemSingleId` is not known, create a new entry in `circles_item`. +| | `instance2` request all instances available in `circle2b` and broadcast the `IFederatedItem` +| `instance1` will ignore the request | `instance2` verify `itemSingleId` exists in `circles_item`. If it does, compare with `appId`, `itemType`, `itemId` and confirm `instance`=`instance1`. | `instance3` will realise that share already exist in `circles_share` +| | `instance2` send a signed request to `instance1` to retrieve content of the shared item, based on `itemSingleId`, `appId`, `itemType`, `itemId`, `circleId`. +| `instance1` confirms at least one user from `instance3` belongs to one of the circles `itemSingleId` is shared to using `circles_share` +| `instance1` returns serialized data of the item using `serializeItem(itemId)`, and its checksum. +| | if `itemSingleId` was not known, and after confirmation from `instance1`, store data in `circles_item`. +| | if `itemSingleId` was known, `instance2` compare `checksum`. If different to the one stored in `circles_item`, use `syncItem(itemId, serializedData): void;` and update `checksum` +| | `app2` get serialized data and store it in its own table. +| | `instance2` search for existing shares in `circles_share` based on `itemSingleId`, `circleId`. exit process if found. +| | `instance2` store data in `circles_share` +| | `instance2` communicate the new share to `app2` using `onShareCreation(itemId, circleId, extraData)` +| | `app2` generate/update an entry in its own shares table + +### Edit shares permissions + +This process describe: + +- the process of update a local share, +- the process of update a remote share. + +Nextcloud 1 | Nextcloud 2 | Nextcloud 3 +---|---|--- +| | `user2` is owner of `circle2` | +| `user1` is member of `circle2` | | `user3` is member of `circle2` +| | +| `user1` share `item1` | +| | | `user3` update the share of `item1` to `circle2` +| | | `app3` uses `$circleManager->getShareManager(appId, itemType)->updateShare(itemId, circleId, extraData): void;` +| | | `instance3` get `itemSingleId` from `circles_item` and get remote host from `instance`. +| | | (in case the process was initiated from `Nextcloud1` the `instance would be local and the process goes directly to step `(b)`) +| | | `instance3` request `instance1` about the update using `itemSingleId`, `circleId`, `extraData`, `FederatedUser` (current session) +|`instance1` confirms `itemSingleId` exist in `circles_item` and `instance` is local +| `instance1` confirms at least one user from `instance3` belongs to one of the circle to which the item have been shared to using `circles_share` +|`instance1` confirms `FederatedUser` exist, is from `instance3` and belongs to one of the circle. +|`instance1` ask `app1` if share can be edited using `isShareModifiable(itemId, circleId, extraData, FederatedUser): bool;` +|`app1` confirm permission and rights based on the memberships of `FederatedUser`. +| `instance1` communicate the updated share to `app1` using `onShareModification(itemId, circleId, extraData)` +| `instance1` create a FederatedItem using `circleId` containing `shareSindleId`, `extraData` +| | +| _Async process at this point._ +| | | if update was initiated from `instance3`, `instance1` confirmed the update and `instance3` run `onShareModification(itemId, circleId, extraData)` +| | +| | as master instance for `circle2`, `instance2` receive the `IFederatedItem`. +| | `instance2` verify `itemSingleId` exists in `circles_item`. If it does, compare with `appId`, `itemType`, `itemId` and confirm `instance`=`instance1`. +| | if `itemSingleId` is not known, create a new entry in `circles_item`. +| | `instance2` request all instances available in `circle2` and broadcast the `IFederatedItem` +| `instance1` will ignore the request | `instance2` verify `itemSingleId` exists in `circles_item`. If it does, compare with `appId`, `itemType`, `itemId` and confirm `instance`=`instance1`. | if update was initiated from `instance3`, `instance3` will ignore the request. If not, will run exactly like `instance2` +| | `instance2` confirm the share exist in `circles_share`. +| | `instance2` use onShareModification(itemId, circleId, extraData) +| | `app2` update the share in its own table. + +### Updating an item + +This process describe: + +- updating a local item or one of its components, +- updating a non-local item or one of its components. + +**UpdateLock** + +To avoid race condition, `UpdateLock` allows the app to lock any further update on an item during a +previous update process. The app have the possibility to lock specific entry of the item. + +The `UpdateLock` will identify the locked part using `updateType` and `updateTypeId` + +> In case of the Deck App, it is possible to lock the action of moving a card (`updateType='card'`, `updateTypeId=cardId`) while not locking +> the edition of another card (`updateType='card'`, `updateTypeId=anotherCardId`) + +The `UpdateLock` also allow the app to enforce that the `item` (stored on the instance a user initiated +the update) is the last known version. + +Now, because most of the locks will only be kept few 1/10s, any parallel request will cycle for few +seconds, waiting for an available slot, before returning a lock error. +A `fifo` can be implemented in the future if needed. + +**Updating an item** + +Nextcloud 1 | Nextcloud 2 | Nextcloud 3 +---|---|--- +| `user1` is owner of `circle1` | +| | | `user3` is member of `circle1` +| | `user2` is owner of `circle2` +| `user1` is member of `circle2` +| | +| `user1` shares `item1` to `circle1` +| `user1` shares `item1` to `circle2` +| | | `user3` update `item1`. Based on the edit, `app3` generate an `updateLock` based on `updateType`, `updateTypeId` and if `checksum` needs to be up-to-date. +| | | `app3` calls `updateItem(itemId, extraData, updateLock): void;` +| | | `instance3` get `itemSingleId`, `checksum` from `circles_item` linked to `appId`, `itemType`, `itemId` and get remote host from `instance` +| | | `instance3` request `instance1` about the update using `itemSingleId`, `extraData`, `updateLock`, `FederatedUser` (current session), `checksum` +| `instance1` confirms `itemSingleId` exist and get `checksum` in `circles_item` and `instance` is local +| `instance1` confirms at least one user from `instance3` belongs to one of the circle to which the item have been shared to using `circles_share` +| `instance1` confirm `FederatedUser` exist in local database, is from `instance3` and belongs to one of the circle. +| `(a)` if `updateLock.verifyChecksum` is `true`, `instance1` confirm `checksum` from `circles_item` is identical to the one sent by `instance3` +| `instance1` search in `circles_lock` for a lock using `updateLock.updateType`, `updateLock.updateTypeId` +| if the `UpdateLock` is older than a minute, the entry is ignored and deleted. | if an `UpdateLock` exists, pause for a second. Retrieve `checksum` from `circles_item` and return to the previous step `(a)` +| `instance1` will wait for the `item` to `unlock` for few seconds and returns a SyncException if the lock is still up. +| `instance1` use `isItemUpdatable(itemId, extraData, federatedUser): bool;` +| `app1` confirm permissions to update based on `extraData`, `federatedUser`. +| Async process at this point with the confirmation (or not) that `app1` accepted the update. If accepted, the current `checksum` is also returned +| `instance1` generate an entry in `circles_lock` using `updateLock.updateType`, `updateLock.updateTypeId` and set `time` to now | | if `checksum` from `circles_item` is still the same and `updateLock.verifyChecksum=false`, or `checksum` is the one returned by `instance1`, then `instance3` initiate the update process locally using `onItemUpdate(itemId, extraData, federatedUser);` +| `instance1` initiate the updating process using `onItemUpdate(itemId, extraData, federatedUser);` | | `app3` update the item in its table. +| `app1` update the item in its table. | | `instance1` get serializedData from `app1` using `serializeItem(itemId)` and update `checksum` in `circles_item` +| `instance1` remove `UpdateLock` from `circles_lock` +| `instance1` get serializedData from `app1` using `serializeItem(itemId)` and update `checksum` in `circles_item` +| based on `circles_share`, `instance1` create one `IFederatedItem` per `circleId`, containing `itemSindleId`, `checksum`: +| _(need to verify if process can be run without a new async)_ +| `instance1` will manage `circle1` the same way `instance2` handle `circle2` | `instance2` get `itemId` from `circles_item` linked to `itemSingleId` in `circles_item` and confirm that `instance` is `instance1` and `circleId` is in `circles_share` +| | `instance2` broadcast the FederatedItem to all instances available in the circle +| `instance1` will exit process | | `instance2` will run the exact same process than `instance3` | `instance3` get `itemId` from linked to `itemSingleId` in `circles_item` and `circleId` from `circles_share` +| | | `instance3` verify `checksum` and if the same exit process | | | `instance3` send a signed request to `instance1` to retrieve content of the shared item, based on `itemSingleId` +| `instance1` confirms at least one user from `instance3` belongs to one of the circle to which the item have been shared to using `circles_share` +| `instance1` get serialized data of the item using `serializeItem(itemId): array;` +| `app1` returns serialized data based on `itemId`. +| `instance1` generate a `checksum`, update the one stored in `circles_item` if needed, and returns `serializedData` and `checksum` +| | | `instance3` compare `checksum` with the one in `circles_item`. If different, run `syncItem(itemId, serializedData)`; +| | | `app3` update the item in its table. + +### Delete share + +not documented yet but will work like `Edit shares permissions` + +### Remove item + +not documented yet, but will work more or less like `Update Item` + +### temp + +[comment]: <> (| `instance1` communicate the updated item to `app1`) + +[comment]: <> (using `IFederatedSyncManager::syncItem(itemId, serializeData)`) + +[comment]: <> (-------------------------) + +[comment]: <> (------------------------------) + +[comment]: <> (| `instance1` verify `itemId` already exists in `circles_item`. If it does, compare with `appId`) + +[comment]: <> (, `itemType`, extract `itemSingleId` and see if `instance` is local. () + +[comment]: <> (see `Action: Resharing a non-local item`)) + +[comment]: <> (| if `itemSingleId` is known, search for existing shares in `circles_share` based on `itemSingleId`) + +[comment]: <> (, `circleId`. | `instance1` get membership based on current session `FederatedUser`, `circleId` and) + +[comment]: <> (request `app1` to verify share can be created) + +[comment]: <> (using `isShareCreatable(itemId, circleId, extraData, Membership): bool;`) + +[comment]: <> (|`app1` confirms the share is creatable. | if `itemSingleId` is not known, `instance1` generate a new one) + +[comment]: <> (and created tne entry in `circles_item` using `itemSingleId`, `instance` (local), `appId`, `itemType`) + +[comment]: <> (, `itemId`) + +[comment]: <> (| `instance1` creates an entry in `circles_share` using `itemSingleId` and `circleId`) + +[comment]: <> (| if `fullSupport`, `instance1` uses `onShareCreation(itemId, circleId, extraData, Membership): void;`) + +[comment]: <> (and `app1` generate entries in its own shares table. | `instance1` create a `IFederatedItem`) + +[comment]: <> (using `circleId` containing `itemSingleId`, `appId`, `itemType`, `itemId` and request the master instance) + +[comment]: <> (for `circle2`) + +[comment]: <> (| | | _Async process at this point._) + +[comment]: <> (| | | | as master instance for `circle2`, `instance2` receive the `IFederatedItem`. | | `instance2`) + +[comment]: <> (verify `itemSingleId` exists in `circles_item`. If it does, compare with `appId`, `itemType`, `itemId`) + +[comment]: <> (and confirm `instance`=`instance1`. | | if `itemSingleId` is not known, create a new entry) + +[comment]: <> (in `circles_item`. | | `instance2` request **all instances** available in `circle2` to send and broadcast) + +[comment]: <> (the `IFederatedItem`) + +[comment]: <> (| `instance1` will ignore the request | `instance2` will run the exact same process than `instance3`) + +[comment]: <> (| `instance3` verify `itemSingleId` exists in `circles_item`. If it does, compare with `appId`) + +[comment]: <> (, `itemType`, `itemId` and confirm `instance`=`instance1`. | | | `instance3` send a signed request) + +[comment]: <> (to `instance1` to retrieve content of the shared item, based on `itemSingleId`, `appId`, `itemType`) + +[comment]: <> (, `itemId`, `circleId`. | `instance1` confirms at least one user from `instance3` belongs to one of the) + +[comment]: <> (circles `itemSingleId` is shared to using `circles_share`) + +[comment]: <> (| `instance1` returns serialized data of the item using `serializeItem(itemId)`, and its checksum. | | |) + +[comment]: <> (if `itemSingleId` was not known, and after confirmation from `instance1`, store data in `circles_item`. |) + +[comment]: <> (| | if `itemSingleId` was known, `instance3` compare `checksum`. If different to the one stored) + +[comment]: <> (in `circles_item`, use `syncItem(itemId, serializedData): void;` and update `checksum`) + +[comment]: <> (| | | `app3` get serialized data and store it in its own table. | | | `instance3` search for existing) + +[comment]: <> (shares in `circles_share` based on `itemSingleId`, `circleId`. exit process if found. | | | `instance3`) + +[comment]: <> (store data in `circles_share`) + +[comment]: <> (| | | `instance3` communicate the new share to `app3`) + +[comment]: <> (using `onShareCreation(itemId, circleId, extraData)`) + +[comment]: <> (| | | `app3` generate/update an entry in its own shares table) + diff --git a/lib/CircleSharesManager.php b/lib/CircleSharesManager.php new file mode 100644 index 000000000..b113e880f --- /dev/null +++ b/lib/CircleSharesManager.php @@ -0,0 +1,242 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles; + +use Exception; +use OCA\Circles\Exceptions\CircleSharesManagerException; +use OCA\Circles\Model\Probes\CircleProbe; +use OCA\Circles\Service\CircleService; +use OCA\Circles\Service\ConfigService; +use OCA\Circles\Service\DebugService; +use OCA\Circles\Service\FederatedSyncItemService; +use OCA\Circles\Service\FederatedSyncService; +use OCA\Circles\Service\FederatedSyncShareService; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +/** + * Class CircleSharesManager + * + * @package OCA\Circles + */ +class CircleSharesManager implements ICircleSharesManager { + + + private CircleService $circleService; + private FederatedSyncService $federatedSyncService; + private FederatedSyncItemService $federatedSyncItemService; + private FederatedSyncShareService $federatedSyncShareService; + private ConfigService $configService; + private DebugService $debugService; + + private string $originAppId = ''; + private string $originItemType = ''; + + + /** + * @param CircleService $circleService + * @param FederatedSyncItemService $federatedSyncItemService + * @param FederatedSyncShareService $federatedSyncShareService + * @param ConfigService $configService + */ + public function __construct( + CircleService $circleService, + FederatedSyncService $federatedSyncService, + FederatedSyncItemService $federatedSyncItemService, + FederatedSyncShareService $federatedSyncShareService, + ConfigService $configService, + DebugService $debugService + ) { + $this->circleService = $circleService; + $this->federatedSyncService = $federatedSyncService; + $this->federatedSyncItemService = $federatedSyncItemService; + $this->federatedSyncShareService = $federatedSyncShareService; + $this->configService = $configService; + $this->debugService = $debugService; + } + + + /** + * @param string $syncManager + * + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function registerFederatedSyncManager(string $syncManager): void { + if ($this->originAppId !== '' || $this->originItemType !== '') { + return; + } + + $federatedSyncManager = \OC::$server->get($syncManager); + if (!($federatedSyncManager instanceof IFederatedSyncManager)) { + // log something + return; + } + + $this->federatedSyncService->addFederatedSyncManager($federatedSyncManager); + } + + + /** + * @param string $itemId + * @param string $circleId + * @param array $extraData + * + * @throws CircleSharesManagerException + * @throws Exceptions\CircleNotFoundException + * @throws Exceptions\FederatedSyncConflictException + * @throws Exceptions\FederatedSyncManagerNotFoundException + * @throws Exceptions\InitiatorNotFoundException + * @throws Exceptions\RequestBuilderException + * @throws Exceptions\SyncedSharedAlreadyExistException + */ + public function createShare( + string $itemId, + string $circleId, + array $extraData = [] + ): void { + $this->debugService->setDebugType('federated_sync'); + $this->debugService->info('New request to create a share', $circleId, [ + 'appId' => $this->originAppId, + 'itemType' => $this->originItemType, + 'itemId' => $itemId, + 'extraData' => $extraData + ]); + + try { + $this->mustHaveOrigin(); + + // TODO: verify rules that apply when sharing to a circle + $probe = new CircleProbe(); + $probe->includeSystemCircles() + ->mustBeMember(); + + $circle = $this->circleService->getCircle($circleId, $probe); + + // get valid SyncedItem based on appId, itemType, itemId + $syncedItem = $this->federatedSyncItemService->getSyncedItem( + $this->originAppId, + $this->originItemType, + $itemId + ); + + $this->debugService->info( + 'Initiating the process of sharing {syncedItem.singleId} to {circle.id}', + $circleId, [ + 'circle' => $circle, + 'syncedItem' => $syncedItem, + 'extraData' => $extraData + ] + ); + + // confirm item is local + if (!$syncedItem->isLocal()) { + // TODO: sharing a remote item + return; + } + + $this->federatedSyncShareService->createShare($syncedItem, $circle, $extraData); + } catch (Exception $e) { + $this->debugService->exception($e, $circleId); + throw $e; + } +// this->$this->federatedItemService->getSharedItem + } + + /** + * @param string $itemId + * @param string $circleId + * @param array $extraData + * + * @throws CircleSharesManagerException + */ + public function updateShare( + string $itemId, + string $circleId, + array $extraData = [] + ): void { + $this->mustHaveOrigin(); + } + + /** + * @param string $itemId + * @param string $circleId + * + * @throws CircleSharesManagerException + */ + public function deleteShare(string $itemId, string $circleId): void { + $this->mustHaveOrigin(); + } + + /** + * @param string $itemId + * @param array $serializedData + */ + public function updateItem( + string $itemId, + array $serializedData + ): void { + $this->mustHaveOrigin(); + } + + /** + * @param string $itemId + * + * @throws CircleSharesManagerException + */ + public function deleteItem(string $itemId): void { + $this->mustHaveOrigin(); + } + + + /** + * @param string $appId + * @param string $itemType + */ + public function setOrigin(string $appId, string $itemType) { + $this->originAppId = $appId; + $this->originItemType = $itemType; + } + + /** + * @throws CircleSharesManagerException + */ + private function mustHaveOrigin(): void { + if ($this->originAppId !== '' && $this->originItemType !== '') { + return; + } + + throw new CircleSharesManagerException( + 'ICirclesManager::getShareManager(appId, itemType) used empty params' + ); + } +} diff --git a/lib/CirclesManager.php b/lib/CirclesManager.php index 32da68c60..e375dd8c1 100644 --- a/lib/CirclesManager.php +++ b/lib/CirclesManager.php @@ -64,36 +64,22 @@ use OCA\Circles\Service\MembershipService; use OCA\Circles\Tools\Exceptions\InvalidItemException; -/** - * Class CirclesManager - * - * @package OCA\Circles - */ -class CirclesManager { - - /** @var FederatedUserService */ - private $federatedUserService; - - /** @var CircleService */ - private $circleService; - - /** @var MemberService */ - private $memberService; - - /** @var MembershipService */ - private $membershipService; - - /** @var ConfigService */ - private $configService; +class CirclesManager { - /** @var CirclesQueryHelper */ - private $circlesQueryHelper; + private CircleSharesManager $circleSharesManager; + private FederatedUserService $federatedUserService; + private CircleService $circleService; + private MemberService $memberService; + private MembershipService $membershipService; + private ConfigService $configService; + private CirclesQueryHelper $circlesQueryHelper; /** * CirclesManager constructor. * + * @param CircleSharesManager $circleSharesManager * @param FederatedUserService $federatedUserService * @param CircleService $circleService * @param MemberService $memberService @@ -102,6 +88,7 @@ class CirclesManager { * @param CirclesQueryHelper $circlesQueryHelper */ public function __construct( + CircleSharesManager $circleSharesManager, FederatedUserService $federatedUserService, CircleService $circleService, MemberService $memberService, @@ -109,6 +96,7 @@ public function __construct( ConfigService $configService, CirclesQueryHelper $circlesQueryHelper ) { + $this->circleSharesManager = $circleSharesManager; $this->federatedUserService = $federatedUserService; $this->circleService = $circleService; $this->memberService = $memberService; @@ -118,6 +106,24 @@ public function __construct( } + /** + * @param string $appId + * @param string $itemType + * + * @return CircleSharesManager + */ + public function getShareManager(string $appId = '', string $itemType = ''): ICircleSharesManager { + if ($appId === '') { + return $this->circleSharesManager; + } + + $clone = clone $this->circleSharesManager; + $clone->setOrigin($appId, $itemType); + + return $clone; + } + + /** * @param string $federatedId * @param int $type diff --git a/lib/Command/CirclesDebug.php b/lib/Command/CirclesDebug.php new file mode 100644 index 000000000..1254f4fdd --- /dev/null +++ b/lib/Command/CirclesDebug.php @@ -0,0 +1,894 @@ + + * @copyright 2017 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Command; + +use Exception; +use JetBrains\PhpStorm\Pure; +use OC\Core\Command\Base; +use OCA\Circles\Model\Debug; +use OCA\Circles\Service\ConfigService; +use OCA\Circles\Service\DebugService; +use OCA\Circles\Tools\Model\ReferencedDataStore; +use OCA\Circles\Tools\Traits\TArrayTools; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Terminal; + +/** + * Class CirclesCheck + * + * @package OCA\Circles\Command + */ +class CirclesDebug extends Base { + use TArrayTools; + + public const REFRESH = 50000; + + private DebugService $debugService; + private ConfigService $configService; + + private TopPanel $topPanel; + private BottomLeftPanel $bottomLeftPanel; + private BottomRightPanel $bottomRightPanel; + private ProgressBar $display; + + /** @var Panel[] $panels */ + private array $panels = []; + /** @var Debug[] $debugs */ + private array $debugs = []; + private bool $refresh = false; + private int $lastId = 0; + + /** + * @param DebugService $debugService + */ + public function __construct(DebugService $debugService, ConfigService $configService) { + parent::__construct(); + + $this->debugService = $debugService; + $this->configService = $configService; + } + + protected function configure() { + parent::configure(); + $this->setName('circles:debug') + ->setDescription('Debug!') + ->addOption('circle', '', InputOption::VALUE_REQUIRED, 'filter circle', '') + ->addOption('history', '', InputOption::VALUE_REQUIRED, 'last history', '15') + ->addOption('size', '', InputOption::VALUE_REQUIRED, 'height', '0') + ->addOption('ping', '', InputOption::VALUE_NONE, 'ping debug daemon') + ->addOption('instance', '', InputOption::VALUE_REQUIRED, 'filter instance', ''); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * + * @return int + * @throws Exception + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + if ($input->getOption('ping')) { + $this->debugService->info('ping.'); + + return 0; + } + + $this->init(); + $this->initTerminal((int)$input->getOption('size')); + $this->initPanel($output); + $this->initHistory((int)$input->getOption('history')); + + while (true) { + try { + $this->keyPressed(); + } catch (QuitException $e) { + break; + } + + $this->live(); + $this->refresh(); + + usleep(self::REFRESH); + } + +// $this->displayDebugs($debugs); + + return 0; + } + + + private function init(): void { + stream_set_blocking(STDIN, false); + readline_callback_handler_install( + '', + function () { + } + ); + + $this->panels[] = $this->topPanel = new TopPanel( + [ + 'currentLine' => 'topPanelCurrentLine', + ] + ); + $this->panels[] = $this->bottomLeftPanel = new BottomLeftPanel( + [ + 'currentLine' => 'bottomLeftPanelCurrentLine', + ] + ); + $this->panels[] = $this->bottomRightPanel = new BottomRightPanel( + [ + 'currentLine' => 'bottomRightPanelCurrentLine', + ] + ); + } + + + /** + * @param int $height + */ + private function initTerminal(int $height = 0): void { + if ($height === 0) { + $height = (new Terminal())->getHeight() - 1; + } + + $this->topPanel->setHeight((int)floor($height / 2)); + $this->bottomLeftPanel->setHeight($height - $this->topPanel->getHeight()); + $this->bottomRightPanel->setHeight($height - $this->topPanel->getHeight()); + + $this->topPanel->setMaxLines($this->topPanel->getHeight() - 2); + $this->bottomLeftPanel->setMaxLines($this->bottomLeftPanel->getHeight() - 7); + $this->bottomRightPanel->setMaxLines($this->bottomRightPanel->getHeight() - 2); + } + + + /** + * @param OutputInterface $output + */ + private function initPanel(OutputInterface $output): void { + $this->display = new ProgressBar($output); + $this->initPanelMessage(); + $this->display->setOverwrite(true); + + $lines = []; + $lines[] = '┌─%top%──────────────────────────────────────────────────────'; + + for ($i = 0; $i < $this->topPanel->getMaxLines(); $i++) { + $lines[] = '│' + . $this->incrementString($i, 'topPanelCurrentLine', '%') + . $this->incrementstring($i, 'lineT', '%'); + + $this->update($this->incrementString($i, 'topPanelCurrentLine'), ' '); + $this->update($this->incrementString($i, 'lineT'), ' '); + } + + $lines[] = '└─────'; + $lines[] = + '┌────────────────────────────────────────────┬─────────────%test%─────────────────────────'; + + for ($i = 0; $i < $this->bottomLeftPanel->getMaxLines(); $i++) { + $lines[] = '│' + . $this->incrementString($i, 'bottomLeftPanelCurrentLine', '%') . ' ' + . $this->incrementString($i, 'lineBL', ':42s%') . ' ' + . '│ ' + . $this->incrementString($i, 'lineBR', '%'); + + $this->update($this->incrementString($i, 'bottomLeftPanelCurrentLine'), ''); + $this->update($this->incrementString($i, 'lineBL'), ''); + $this->update($this->incrementString($i, 'lineBR'), ''); + } + + $more = [' Thread', ' Type', 'CircleId', 'Instance', ' Time']; + for ($j = 0; $j < count($more); $j++) { + $lines[] = '│ ' . strtolower($more[$j]) . ': %curr' + . trim($more[$j]) . ':-32s% │ ' + . $this->incrementString($i + $j, 'lineBR', '%'); + $this->update($this->incrementString($i + $j, 'lineBR'), ''); + } + $lines[] = '└────────────────────────────────────────────┘'; + + $this->setCurr(); + +// $this->display->clear(); + $this->display->setFormat(implode("\n", $lines) . "\n"); + $this->display->start(); + } + + + /** + * @param int $history + */ + private function initHistory(int $history): void { + $debugs = $this->debugService->getHistory(max($history, 1)); + $this->lastId = empty($debugs) ? 0 : $debugs[0]->getId(); + + if ($history < 1) { + return; + } + + $this->debugs = array_reverse($debugs, false); + $this->topPanel->setCurrentPage(max(count($this->debugs) - 3, 0)); + + $this->refreshHistory(); + } + + /** + * + */ + private function live(): void { + $debugs = $this->debugService->getSince($this->lastId); + if (empty($debugs)) { + return; + } + + $this->lastId = $debugs[0]->getId(); + + foreach (array_reverse($debugs, false) as $debug) { + $this->debugs[] = $debug; + } + + $this->refreshHistory(); + } + + + private function refreshHistory(): void { + $selectableLines = 0; + for ($i = 0; $i < $this->topPanel->getMaxLines(); $i++) { + $k = $this->topPanel->getCurrentPage() + $i; + if (!array_key_exists($k, $this->debugs)) { + $this->update($this->incrementString($i, 'lineT'), ''); + } else { + $item = $this->debugs[$k]; + $debug = $item->getDebug(); + + $instance = ($this->configService->isLocalInstance($item->getInstance())) ? + 'local' : $item->getInstance(); + + $instanceColor = $this->configService->getAppValue('debug_instance.' . $instance); + if ($instanceColor === '') { + $instanceColor = 'white'; + } + + $line = '' . $instance . ' - '; + $action = $debug->g(DebugService::ACTION); + + preg_match_all('/{((?:[^{}]*|(?R))*)}/x', $action, $match); + foreach ($match[1] as $entry) { + $flag = substr($entry, 0, 1); + if ($flag === '!') { + $path = substr($entry, 1); + $color = 'yellow'; + } else if ($flag === '?') { + $path = substr($entry, 1); + $color = 'red'; + } else { + $path = $entry; + $color = 'green'; + } + + $value = $this->get($path, $debug->jsonSerialize()); + $action = + str_replace('{' . $entry . '}', '' . $value . '', $action); + } + + $selectableLines++; + + $line .= $action; + $this->update($this->incrementString($i, 'lineT'), $line); + } + } + + $this->topPanel->setSelectableLines($selectableLines); + } + + /** + * + */ + private function initPanelMessage(): void { + $this->updates([ + 'test' => '', + ]); + } + + + /** + * @param array $data + */ + private function updates(array $data): void { + foreach ($data as $k => $v) { + $this->update($k, (string)$v); + } + } + + /** + * @param string $key + * @param string $value + */ + private function update(string $key, string $value = ''): void { + $this->display->setMessage($value, $key); + $this->forceRefresh(); + } + + + /** + * @throws QuitException + */ + public function keyPressed(): void { + $n = fread(STDIN, 16); + if ($n !== '') { + if (substr($n, 1, 1) === '[') { + $this->keyActionTopPanel(strtolower(substr($n, 2, 1))); + } + + $c = str_split($n, 1); + foreach ($c as $a) { + $this->onKeyPressed(strtolower($a)); + } + } + } + + /** + * @param string $key + * + * @throws QuitException + */ + public function onKeyPressed(string $key): void { + switch ($key) { + case 'q': + throw new QuitException(); + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + $this->keyActionLeftPanel((int)$key); + break; + + case 'w': + case 's': + case 'a': + case 'd': + $this->keyActionBottomRightPanel($key); + break; + + default: + return; + } + } + + + /** + * + */ + private function forceRefresh(): void { + $this->refresh = true; + } + + /** + * + */ + private function refresh(): void { + if (!$this->isRefreshNeeded()) { + return; + } + + $this->display->display(); + $this->cleanRefresh(); + } + + /** + * @return bool + */ + #[Pure] + private function isRefreshNeeded(): bool { + if ($this->refresh) { + return true; + } + + foreach ($this->panels as $panel) { + if ($panel->isModified()) { + return true; + } + } + + return false; + } + + private function cleanRefresh(): void { + $this->refresh = false; + foreach ($this->panels as $panel) { + $panel->setModified(false); + } + } + + + /** + * @return Debug + */ + #[Pure] + private function getSelectedEntry(): Debug { + return $this->debugs[$this->topPanel->getCurrentPage() + $this->topPanel->getCurrentLine()]; + } + + + /** + * @param string $key + */ + public function keyActionTopPanel(string $key): void { + switch ($key) { + case 'a': + $done = $this->topPanel->keyUp(); + break; + case 'b': + $done = $this->topPanel->keyDown(); + break; + case 'c': + $done = $this->topPanel->keyRight(); + $this->refreshHistory(); + break; + case 'd': + $done = $this->topPanel->keyLeft(); + $this->refreshHistory(); + break; + + default: + return; + } + + if ($done) { + $this->displayTopPanelCurrentLine(); + $this->displayBottomLeftPanelItems(); + + $this->bottomRightPanel->setLines([]); + $this->bottomRightPanel->setSelectableLines(count($this->bottomRightPanel->getLines())); + $this->displayBottomRightPanelItems(); + } + } + + /** + * @param string $key + */ + public function keyActionBottomRightPanel(string $key): void { + switch ($key) { + case 'w': + $this->bottomRightPanel->keyUp(); + break; + case 's': + $this->bottomRightPanel->keyDown(); + break; +// case 'a': +// $this->bottomRightPanel->keyLeft(); +// break; +// case 'd': +// $this->bottomRightPanel->keyRight(); +// break; + + default: + return; + } + + $this->displayBottomRightPanelItems(); + } + + /** + * + */ + private function displayTopPanelCurrentLine(): void { + for ($i = 0; $i < $this->topPanel->getMaxLines(); $i++) { + if ($this->topPanel->getCurrentLine() === $i) { + $this->update( + $this->incrementString($i, $this->get('currentLine', $this->topPanel->getInternal())), +// '·' + '>' + ); + } else { + $this->update( + $this->incrementString($i, $this->get('currentLine', $this->topPanel->getInternal())), + ' ' + ); + } + } + } + + + /** + * + */ + private function displayBottomLeftPanelItems(): void { + $this->setCurr($this->getSelectedEntry()); + $debug = $this->getSelectedEntry()->getDebug(); + + + $references = $debug->getAllReferences(); + + $items = []; + $this->bottomLeftPanel->setSelectableLines(count($references)); + for ($i = 0; $i < $this->bottomLeftPanel->getMaxLines(); $i++) { + $entry = array_shift($references); + if (is_null($entry)) { + $this->update($this->incrementString($i, 'lineBL'), ''); + } else { + $items[] = $name = $this->get(ReferencedDataStore::KEY_NAME, $entry); + + $type = $this->get(ReferencedDataStore::KEY_TYPE, $entry); + if ($type === ReferencedDataStore::OBJECT) { + $path = $this->get(ReferencedDataStore::KEY_CLASS, $entry); + $type = implode('\\', array_slice(explode('\\', $path), -2)); + } + + $this->update( + $this->incrementString($i, 'lineBL'), + ($i + 1) . '. ' . $name . ' (' . $type . ')' + ); + } + } + + $this->bottomLeftPanel->setItems($items); + $this->bottomLeftPanel->setCurrentLine(-1); + + $this->displayBottomLeftPanelCurrentLine(); + } + + + private function displayBottomLeftPanelCurrentLine(): void { + for ($i = 0; $i < $this->bottomLeftPanel->getMaxLines(); $i++) { + if ($this->bottomLeftPanel->getCurrentLine() === $i) { + $this->update( + $this->incrementString( + $i, + $this->get('currentLine', $this->bottomLeftPanel->getInternal()) + ), + '' + ); + } else { + $this->update( + $this->incrementString( + $i, + $this->get('currentLine', $this->bottomLeftPanel->getInternal()) + ), + '' + ); + } + } + } + + + private function displayBottomRightPanelItems(): void { + + $this->update($this->incrementString(0, 'lineBR')); +// $this->bottomLeftPanel->setSelectableLines(count($references)); + + $lines = $this->bottomRightPanel->getLines(); + for ($i = 0; $i < $this->bottomRightPanel->getMaxLines(); $i++) { + $c = $i + $this->bottomRightPanel->getCurrentLine(); + if ($this->bottomRightPanel->getSelectableLines() < $c || empty($lines)) { + $this->update($this->incrementString($i, 'lineBR'), ''); + } else { + $this->update($this->incrementString($i, 'lineBR'), (string)$lines[$c]); + } +// $type = $this->get(ReferencedDataStore::KEY_TYPE, $entry); +// if ($type === ReferencedDataStore::OBJECT) { +// $path = $this->get(ReferencedDataStore::KEY_CLASS, $entry); +// $type = implode('\\', array_slice(explode('\\', $path), -2)); +// } +// +// $this->update( +// $this->incrementString($i, 'lineBL'), +// 'circle (' . $type . ')' +// ); + } + + } + + + public function test(int $b) { + $this->update('top', (string)$b); + } + + + private function keyActionLeftPanel(int $item): void { + $this->bottomLeftPanel->onKeyItem($item); + + $this->displayBottomLeftPanelCurrentLine(); + + + $items = $this->bottomLeftPanel->getItems(); + $debug = $this->getSelectedEntry()->getDebug(); + + if ($this->bottomLeftPanel->getCurrentLine() < count($items)) { + $this->bottomRightPanel->setLines( + explode( + "\n", + trim( + json_encode( + $debug->gAll()[$items[$this->bottomLeftPanel->getCurrentLine()]], + JSON_PRETTY_PRINT + ) + ) + ) + ); + } + $this->bottomRightPanel->setSelectableLines(count($this->bottomRightPanel->getLines())); + $this->bottomRightPanel->setCurrentLine(0); + + $this->displayBottomRightPanelItems(); + } + + + private function incrementString(int $number, string $prefix = '', string $wrapper = ''): string { + $str = sprintf('%03d', $number); + $chars = 'ABCDEFGHIJ'; + $result = ''; + for ($i = 0; $i < 3; $i++) { + $result .= $chars[(int)$str[$i]]; + } + + if ($wrapper !== '') { + $prefix = '%' . $prefix; + } + + return $prefix . $result . $wrapper; + } + + private function setCurr(?Debug $debug = null) { + $this->update('currThread', $debug?->getThread() ?? ''); + $this->update('currType', $debug?->getType() ?? ''); + $this->update('currCircleId', $debug?->getCircleId() ?? ''); + $this->update('currInstance', $debug?->getInstance() ?? ''); + $this->update('currTime', (string)$debug?->getTime() ?? ''); + } + +} + + +class Panel { + private array $lines = []; + private int $maxLines = 0; + private int $selectableLines = 0; + private int $currentLine = -1; + private int $height = 0; + private bool $modified = true; + private array $internal; + private int $currentPage; + + public function __construct(array $internal = []) { + $this->internal = $internal; + } + + /** + * @param array $lines + */ + public function setLines(array $lines): void { + $this->lines = $lines; + } + + /** + * @param int $page + * + * @return array + */ + public function getLines(int $page = -1): array { + return $this->lines; + } + + + /** + * @param int $maxLines + */ + public function setMaxLines(int $maxLines): void { + $this->maxLines = $maxLines; + } + + /** + * @return int + */ + public function getMaxLines(): int { + return $this->maxLines; + } + + /** + * @param int $selectableLines + */ + public function setSelectableLines(int $selectableLines): void { + $this->selectableLines = $selectableLines; + } + + /** + * @return int + */ + public function getSelectableLines(): int { + return $this->selectableLines; + } + + + /** + * @param int $currentLine + */ + public function setCurrentLine(int $currentLine): void { + if ($this->currentLine !== $currentLine) { + $this->setModified(true); + } + + $this->currentLine = $currentLine; + } + + /** + * @return int + */ + public function getCurrentLine(): int { + return $this->currentLine; + } + + + /** + * @param int $currentPage + */ + public function setCurrentPage(int $currentPage): void { + $this->currentPage = $currentPage; + } + + /** + * @return int + */ + public function getCurrentPage(): int { + return $this->currentPage; + } + + /** + * @param int $height + */ + public function setHeight(int $height): void { + $this->height = $height; + } + + /** + * @return int + */ + public function getHeight(): int { + return $this->height; + } + + /** + * @return array + */ + public function getInternal(): array { + return $this->internal; + } + + public function keyUp(): bool { + $curr = $this->getCurrentLine() - 1; + if ($curr >= 0) { + $this->setCurrentLine($curr); + + return true; + } + + return false; + } + + public function keyDown(): bool { + $curr = $this->getCurrentLine() + 1; + if ($curr < $this->getSelectableLines()) { + $this->setCurrentLine($curr); + + return true; + } + + return false; + } + + public function keyLeft(): bool { + $curr = max($this->getCurrentPage() - $this->getMaxLines(), 0); + if ($curr !== $this->getCurrentPage()) { + $this->setCurrentPage($curr); + + return true; + } + + return false; + } + + public function keyRight(): bool { +// $curr = $this->getCurrentLine() + 1; + $curr = $this->getCurrentPage() + $this->getMaxLines(); + if ($curr !== $this->getCurrentPage()) { + $this->setCurrentPage($curr); + + return true; + } + + return false; + } + + + /** + * @param bool $modified + */ + public function setModified(bool $modified): void { + $this->modified = $modified; + } + + /** + * @return bool + */ + public function isModified(): bool { + return $this->modified; + } +} + + +/** + * + */ +class TopPanel extends Panel { +} + + +/** + * + */ +class BottomLeftPanel extends Panel { + private array $items = []; + + public function onKeyItem(int $item) { + if (--$item < $this->getSelectableLines()) { + $this->setCurrentLine($item); + } + } + + /** + * @param array $items + */ + public function setItems(array $items): void { + $this->items = $items; + } + + /** + * @return array + */ + public function getItems(): array { + return $this->items; + } +} + +class BottomRightPanel extends Panel { +} + +class QuitException extends Exception { +} diff --git a/lib/Controller/RemoteController.php b/lib/Controller/RemoteController.php index 990bb3476..b9e0c9111 100644 --- a/lib/Controller/RemoteController.php +++ b/lib/Controller/RemoteController.php @@ -40,6 +40,7 @@ use OCA\Circles\Exceptions\JsonNotRequestedException; use OCA\Circles\Exceptions\UnknownInterfaceException; use OCA\Circles\Model\Circle; +use OCA\Circles\Model\Debug; use OCA\Circles\Model\Federated\FederatedEvent; use OCA\Circles\Model\Federated\RemoteInstance; use OCA\Circles\Model\FederatedUser; @@ -48,6 +49,7 @@ use OCA\Circles\Model\Probes\CircleProbe; use OCA\Circles\Service\CircleService; use OCA\Circles\Service\ConfigService; +use OCA\Circles\Service\DebugService; use OCA\Circles\Service\FederatedUserService; use OCA\Circles\Service\InterfaceService; use OCA\Circles\Service\MemberService; @@ -105,6 +107,8 @@ class RemoteController extends Controller { /** @var InterfaceService */ private $interfaceService; + private DebugService $debugService; + /** @var ConfigService */ private $configService; @@ -137,6 +141,7 @@ public function __construct( MemberService $memberService, MembershipService $membershipService, InterfaceService $interfaceService, + DebugService $debugService, ConfigService $configService, IUserSession $userSession ) { @@ -149,6 +154,7 @@ public function __construct( $this->memberService = $memberService; $this->membershipService = $membershipService; $this->interfaceService = $interfaceService; + $this->debugService = $debugService; $this->configService = $configService; $this->userSession = $userSession; @@ -195,6 +201,12 @@ public function event(): DataResponse { return $this->exceptionResponse($e, Http::STATUS_UNAUTHORIZED); } + $this->debugService->info( + 'new event requested from', + ($event->hasCircle()) ? $event->getCircle()->getSingleId() : '', + ['event' => $event] + ); + try { $this->remoteDownstreamService->requestedEvent($event); @@ -422,6 +434,43 @@ public function memberships(string $circleId): DataResponse { } + /** + * @PublicPage + * @NoCSRFRequired + * + * @return DataResponse + */ + public function debugDaemon(): DataResponse { + try { + if ($this->configService->getAppValue(ConfigService::DEBUG) !== DebugService::DEBUG_DAEMON) { + throw new Exception(); + } + + $signed = $this->remoteStreamService->incomingSignedRequest(); + $this->confirmRemoteInstance($signed); + + /** @var Debug $debug */ + $debug = $this->deserialize(json_decode($signed->getBody(), true), Debug::class); + $debug->setInstance($signed->getOrigin()); + $this->debugService->save($debug); + + return new DataResponse([]); + } catch (Exception $e) { + return $this->exceptionResponse($e, Http::STATUS_UNAUTHORIZED); + } + +// try { +// $this->remoteDownstreamService->requestedEvent($event); +// +// return new DataResponse($event->getOutcome()); +// } catch (Exception $e) { +// $this->e($e, ['event' => $event]); +// +// return $this->exceptionResponse($e); +// } + } + + /** * @return FederatedEvent * @throws InvalidItemException diff --git a/lib/Db/CoreQueryBuilder.php b/lib/Db/CoreQueryBuilder.php index f54e94aa3..6e6db07f2 100644 --- a/lib/Db/CoreQueryBuilder.php +++ b/lib/Db/CoreQueryBuilder.php @@ -79,6 +79,10 @@ class CoreQueryBuilder extends ExtendedQueryBuilder { public const TOKEN = 'tk'; public const OPTIONS = 'pt'; public const HELPER = 'hp'; + public const SYNC_ITEM = 'si'; + public const SYNC_SHARE = 'ss'; + public const SYNC_LOCK = 'sl'; + public const DEBUG = 'bg'; public static $SQL_PATH = [ @@ -227,7 +231,11 @@ class CoreQueryBuilder extends ExtendedQueryBuilder { self::BASED_ON ] ] - ] + ], + self::SYNC_ITEM => [], + self::SYNC_SHARE => [], + self::SYNC_LOCK => [], + self::DEBUG => [] ]; @@ -320,6 +328,22 @@ public function limitToSingleId(string $singleId): void { } + /** + * @param string $appId + */ + public function limitToAppId(string $appId): void { + $this->limit('app_id', $appId, '', true); + } + + + /** + * @param string $itemType + */ + public function limitToItemType(string $itemType): void { + $this->limit('item_type', $itemType, '', true); + } + + /** * @param string $itemId */ diff --git a/lib/Db/CoreRequestBuilder.php b/lib/Db/CoreRequestBuilder.php index 41a5cea71..6101dbc1c 100644 --- a/lib/Db/CoreRequestBuilder.php +++ b/lib/Db/CoreRequestBuilder.php @@ -57,8 +57,12 @@ class CoreRequestBuilder { public const TABLE_MOUNT = 'circles_mount'; public const TABLE_MOUNTPOINT = 'circles_mountpoint'; - // wip - public const TABLE_SHARE_LOCK = 'circles_share_lock'; + public const TABLE_SYNC_ITEM = 'circles_item'; + public const TABLE_SYNC_SHARE = 'circles_share'; + public const TABLE_SYNC_LOCK = 'circles_lock'; + + public const TABLE_DEBUG = 'circles_debug'; + public const TABLE_TOKEN = 'circles_token'; public const TABLE_GSSHARES = 'circle_gsshares'; // rename ? @@ -139,7 +143,37 @@ class CoreRequestBuilder { 'mountpoint_hash' ], self::TABLE_MOUNTPOINT => [], - self::TABLE_SHARE_LOCK => [], + self::TABLE_SYNC_ITEM => [ + 'id', + 'single_id', + 'instance', + 'app_id', + 'item_type', + 'item_id', + 'checksum', + 'deleted' + ], + self::TABLE_SYNC_SHARE => [ + 'id', + 'single_id', + 'circle_id' + ], + self::TABLE_SYNC_LOCK => [ + 'id', + 'single_id', + 'update_type', + 'update_type_id', + 'time' + ], + self::TABLE_DEBUG => [ + 'id', + 'thread', + 'type', + 'circle_id', + 'instance', + 'debug', + 'time' + ], self::TABLE_TOKEN => [ 'id', 'share_id', diff --git a/lib/Db/DebugRequest.php b/lib/Db/DebugRequest.php new file mode 100644 index 000000000..0a06492b9 --- /dev/null +++ b/lib/Db/DebugRequest.php @@ -0,0 +1,100 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Db; + +use OCA\Circles\Model\Debug; + +/** + * Class ShareRequest + * + * @package OCA\Circles\Db + */ +class DebugRequest extends DebugRequestBuilder { + + + /** + * @param Debug $debug + */ + public function save(Debug $debug): void { + $qb = $this->getDebugInsertSql(); + $qb->setValue('thread', $qb->createNamedParameter($debug->getThread())) + ->setValue('type', $qb->createNamedParameter($debug->getType())) + ->setValue('circle_id', $qb->createNamedParameter($debug->getCircleId())) + ->setValue('instance', $qb->createNamedParameter($debug->getInstance())) + ->setValue('debug', $qb->createNamedParameter(json_encode($debug->getDebug()))) + ->setValue('time', $qb->createNamedParameter($debug->getTime())); + + $qb->execute(); + } + + + /** + * @param int $id + * + * @return array + */ + public function since(int $id): array { + $qb = $this->getDebugSelectSql(); + $qb->gt('id', $id); + + return $this->getItemsFromRequest($qb); + } + + + /** + * @param int $history + * + * @return Debug[] + */ + public function getHistory(int $history): array { + $qb = $this->getDebugSelectSql(); + + $qb->orderBy('id', 'desc'); + $qb->paginate($history); + + return $this->getItemsFromRequest($qb); + } + + /** + * @param int $lastId + * + * @return Debug[] + */ + public function getSince(int $lastId): array { + $qb = $this->getDebugSelectSql(); + + $qb->orderBy('id', 'desc'); + $qb->gt('id', $lastId); + + return $this->getItemsFromRequest($qb); + } +} diff --git a/lib/Db/DebugRequestBuilder.php b/lib/Db/DebugRequestBuilder.php new file mode 100644 index 000000000..d824c2d84 --- /dev/null +++ b/lib/Db/DebugRequestBuilder.php @@ -0,0 +1,116 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Db; + +use OCA\Circles\Exceptions\DebugNotFoundException; +use OCA\Circles\Model\Debug; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\Exceptions\RowNotFoundException; + +class DebugRequestBuilder extends CoreRequestBuilder { + + + /** + * @return CoreQueryBuilder + */ + protected function getDebugInsertSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->insert(self::TABLE_DEBUG); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getDebugSelectSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->generateSelect( + self::TABLE_DEBUG, + self::$tables[self::TABLE_DEBUG], + CoreQueryBuilder::DEBUG + ); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getDebugUpdateSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->update(self::TABLE_DEBUG); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getDebugDeleteSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->delete(self::TABLE_DEBUG); + + return $qb; + } + + + /** + * @param CoreQueryBuilder $qb + * + * @return Debug + * @throws DebugNotFoundException + */ + public function getItemFromRequest(CoreQueryBuilder $qb): Debug { + /** @var Debug $debug */ + try { + $debug = $qb->asItem(Debug::class); + } catch (InvalidItemException | RowNotFoundException $e) { + throw new DebugNotFoundException(get_class($e)); + } + + return $debug; + } + + /** + * @param CoreQueryBuilder $qb + * + * @return Debug[] + */ + public function getItemsFromRequest(CoreQueryBuilder $qb): array { + /** @var Debug[] $result */ + return $qb->asItems(Debug::class); + } +} diff --git a/lib/Db/SyncedItemLockRequest.php b/lib/Db/SyncedItemLockRequest.php new file mode 100644 index 000000000..19a8ae595 --- /dev/null +++ b/lib/Db/SyncedItemLockRequest.php @@ -0,0 +1,61 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Db; + +use OCA\Circles\Exceptions\InvalidIdException; +use OCA\Circles\Model\SyncedItemLock; + +/** + * Class SyncedItemLockRequest + * + * @package OCA\Circles\Db + */ +class SyncedItemLockRequest extends SyncedItemLockRequestBuilder { + + + /** + * @param SyncedItemLock $lock + * + * @throws InvalidIdException + */ + public function save(SyncedItemLock $lock): void { + $this->confirmValidIds([$lock->getSingleId()]); + + $qb = $this->getSyncedItemLockInsertSql(); + $qb->setValue('single_id', $qb->createNamedParameter($lock->getSingleId())) + ->setValue('update_type', $qb->createNamedParameter($lock->getUpdateType())) + ->setValue('update_type_id', $qb->createNamedParameter($lock->getUpdateTypeId())) + ->setValue('time', $qb->createNamedParameter($lock->getTime())); + + $qb->execute(); + } +} diff --git a/lib/Db/SyncedItemLockRequestBuilder.php b/lib/Db/SyncedItemLockRequestBuilder.php new file mode 100644 index 000000000..308c7d371 --- /dev/null +++ b/lib/Db/SyncedItemLockRequestBuilder.php @@ -0,0 +1,117 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Db; + +use OCA\Circles\Exceptions\SyncedItemNotFoundException; +use OCA\Circles\Model\SyncedItemLock; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\Exceptions\RowNotFoundException; + +class SyncedItemLockRequestBuilder extends CoreRequestBuilder { + + + /** + * @return CoreQueryBuilder + */ + protected function getSyncedItemLockInsertSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->insert(self::TABLE_SYNC_LOCK); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getSyncedItemLockSelectSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->generateSelect( + self::TABLE_SYNC_LOCK, + self::$tables[self::TABLE_SYNC_LOCK], + CoreQueryBuilder::SYNC_LOCK + ); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getSyncedItemLockUpdateSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->update(self::TABLE_SYNC_LOCK); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getSyncedItemLockDeleteSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->delete(self::TABLE_SYNC_LOCK); + + return $qb; + } + + + /** + * @param CoreQueryBuilder $qb + * + * @return SyncedItemLock + * @throws InvalidItemException + * @throws SyncedItemNotFoundException + */ + public function getItemFromRequest(CoreQueryBuilder $qb): SyncedItemLock { + /** @var SyncedItemLock $lock */ + try { + $lock = $qb->asItem(SyncedItemLock::class); + } catch (RowNotFoundException $e) { + throw new SyncedItemNotFoundException(); + } + + return $lock; + } + + /** + * @param CoreQueryBuilder $qb + * + * @return SyncedItemLock[] + */ + public function getItemsFromRequest(CoreQueryBuilder $qb): array { + /** @var SyncedItemLock[] $result */ + return $qb->asItems(SyncedItemLock::class); + } +} diff --git a/lib/Db/SyncedItemRequest.php b/lib/Db/SyncedItemRequest.php new file mode 100644 index 000000000..f3782e3ce --- /dev/null +++ b/lib/Db/SyncedItemRequest.php @@ -0,0 +1,100 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Db; + +use OCA\Circles\Exceptions\InvalidIdException; +use OCA\Circles\Exceptions\SyncedItemNotFoundException; +use OCA\Circles\Model\SyncedItem; +use OCA\Circles\Tools\Exceptions\InvalidItemException; + +/** + * Class ShareRequest + * + * @package OCA\Circles\Db + */ +class SyncedItemRequest extends SyncedItemRequestBuilder { + + + /** + * @param SyncedItem $item + * + * @throws InvalidIdException + */ + public function save(SyncedItem $item): void { + $this->confirmValidId($item->getSingleId()); + + $qb = $this->getSyncedItemInsertSql(); + $qb->setValue('single_id', $qb->createNamedParameter($item->getSingleId())) + ->setValue('instance', $qb->createNamedParameter($qb->getInstance($item))) + ->setValue('app_id', $qb->createNamedParameter($item->getAppId())) + ->setValue('item_type', $qb->createNamedParameter($item->getItemType())) + ->setValue('item_id', $qb->createNamedParameter($item->getItemId())) + ->setValue('checksum', $qb->createNamedParameter($item->getChecksum())) + ->setValue('deleted', $qb->createNamedParameter($item->isDeleted())); + + $qb->execute(); + } + + + /** + * @param string $singleId + * + * @return SyncedItem + * @throws SyncedItemNotFoundException + */ + public function getSyncedItemFromSingleId(string $singleId): SyncedItem { + $qb = $this->getSyncedItemSelectSql(); + + $qb->limitToSingleId($singleId); + + return $this->getItemFromRequest($qb); + } + + + /** + * @param string $appId + * @param string $itemType + * @param string $itemId + * + * @return SyncedItem + * @throws SyncedItemNotFoundException + */ + public function getSyncedItem(string $appId, string $itemType, string $itemId): SyncedItem { + $qb = $this->getSyncedItemSelectSql(); + + $qb->limitToAppId($appId); + $qb->limitToItemType($itemType); + $qb->limitToItemId($itemId); + + return $this->getItemFromRequest($qb); + } +} diff --git a/lib/Db/SyncedItemRequestBuilder.php b/lib/Db/SyncedItemRequestBuilder.php new file mode 100644 index 000000000..c5e5e88c2 --- /dev/null +++ b/lib/Db/SyncedItemRequestBuilder.php @@ -0,0 +1,116 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Db; + +use OCA\Circles\Exceptions\SyncedItemNotFoundException; +use OCA\Circles\Model\SyncedItem; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\Exceptions\RowNotFoundException; + +class SyncedItemRequestBuilder extends CoreRequestBuilder { + + + /** + * @return CoreQueryBuilder + */ + protected function getSyncedItemInsertSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->insert(self::TABLE_SYNC_ITEM); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getSyncedItemSelectSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->generateSelect( + self::TABLE_SYNC_ITEM, + self::$tables[self::TABLE_SYNC_ITEM], + CoreQueryBuilder::SYNC_ITEM + ); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getSyncedItemUpdateSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->update(self::TABLE_SYNC_ITEM); + + return $qb; + } + + + /** + * @return CoreQueryBuilder + */ + protected function getSyncedItemDeleteSql(): CoreQueryBuilder { + $qb = $this->getQueryBuilder(); + $qb->delete(self::TABLE_SYNC_ITEM); + + return $qb; + } + + + /** + * @param CoreQueryBuilder $qb + * + * @return SyncedItem + * @throws SyncedItemNotFoundException + */ + public function getItemFromRequest(CoreQueryBuilder $qb): SyncedItem { + /** @var SyncedItem $item */ + try { + $item = $qb->asItem(SyncedItem::class); + } catch (InvalidItemException | RowNotFoundException $e) { + throw new SyncedItemNotFoundException(get_class($e)); + } + + return $item; + } + + /** + * @param CoreQueryBuilder $qb + * + * @return SyncedItem[] + */ + public function getItemsFromRequest(CoreQueryBuilder $qb): array { + /** @var SyncedItem[] $result */ + return $qb->asItems(SyncedItem::class); + } +} diff --git a/lib/Db/ShareLockRequest.php b/lib/Db/SyncedShareRequest.php similarity index 61% rename from lib/Db/ShareLockRequest.php rename to lib/Db/SyncedShareRequest.php index d1301da56..ea47a2e57 100644 --- a/lib/Db/ShareLockRequest.php +++ b/lib/Db/SyncedShareRequest.php @@ -31,49 +31,46 @@ namespace OCA\Circles\Db; -use OCA\Circles\Exceptions\FederatedShareNotFoundException; use OCA\Circles\Exceptions\InvalidIdException; -use OCA\Circles\Model\Federated\FederatedShare; +use OCA\Circles\Exceptions\SyncedShareNotFoundException; +use OCA\Circles\Model\SyncedShare; /** * Class ShareRequest * * @package OCA\Circles\Db */ -class ShareLockRequest extends ShareLockRequestBuilder { +class SyncedShareRequest extends SyncedShareRequestBuilder { /** - * @param FederatedShare $share + * @param SyncedShare $share * * @throws InvalidIdException */ - public function save(FederatedShare $share): void { - $this->confirmValidIds([$share->getItemId()]); + public function save(SyncedShare $share): void { + $this->confirmValidIds([$share->getSingleId(), $share->getCircleId()]); - $qb = $this->getShareLockInsertSql(); - $qb->setValue('item_id', $qb->createNamedParameter($share->getItemId())) - ->setValue('circle_id', $qb->createNamedParameter($share->getCircleId())) - ->setValue('instance', $qb->createNamedParameter($qb->getInstance($share))); + $qb = $this->getSyncedShareInsertSql(); + $qb->setValue('single_id', $qb->createNamedParameter($share->getSingleId())) + ->setValue('circle_id', $qb->createNamedParameter($share->getCircleId())); - $qb->execute(); +// $qb->execute(); } /** - * @param string $itemId + * @param string $itemSingleId * @param string $circleId * - * @return FederatedShare - * @throws FederatedShareNotFoundException + * @return SyncedShare + * @throws SyncedShareNotFoundException */ - public function getShare(string $itemId, string $circleId = ''): FederatedShare { - $qb = $this->getShareLockSelectSql(); + public function getShare(string $itemSingleId, string $circleId): SyncedShare { + $qb = $this->getSyncedShareSelectSql(); - $qb->limitToItemId($itemId); - if ($circleId !== '') { - $qb->limitToCircleId($circleId); - } + $qb->limitToSingleId($itemSingleId); + $qb->limitToCircleId($circleId); return $this->getItemFromRequest($qb); } diff --git a/lib/Db/ShareLockRequestBuilder.php b/lib/Db/SyncedShareRequestBuilder.php similarity index 58% rename from lib/Db/ShareLockRequestBuilder.php rename to lib/Db/SyncedShareRequestBuilder.php index ae9a90c75..d11a7d108 100644 --- a/lib/Db/ShareLockRequestBuilder.php +++ b/lib/Db/SyncedShareRequestBuilder.php @@ -10,7 +10,7 @@ * later. See the COPYING file. * * @author Maxence Lange - * @copyright 2021 + * @copyright 2022 * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify @@ -31,24 +31,25 @@ namespace OCA\Circles\Db; +use OCA\Circles\Exceptions\SyncedShareNotFoundException; +use OCA\Circles\Model\SyncedShare; +use OCA\Circles\Tools\Exceptions\InvalidItemException; use OCA\Circles\Tools\Exceptions\RowNotFoundException; -use OCA\Circles\Exceptions\FederatedShareNotFoundException; -use OCA\Circles\Model\Federated\FederatedShare; /** * Class ShareRequestBuilder * * @package OCA\Circles\Db */ -class ShareLockRequestBuilder extends CoreRequestBuilder { +class SyncedShareRequestBuilder extends CoreRequestBuilder { /** * @return CoreQueryBuilder */ - protected function getShareLockInsertSql(): CoreQueryBuilder { + protected function getSyncedShareInsertSql(): CoreQueryBuilder { $qb = $this->getQueryBuilder(); - $qb->insert(self::TABLE_SHARE_LOCK); + $qb->insert(self::TABLE_SYNC_SHARE); return $qb; } @@ -57,12 +58,13 @@ protected function getShareLockInsertSql(): CoreQueryBuilder { /** * @return CoreQueryBuilder */ - protected function getShareLockSelectSql(): CoreQueryBuilder { + protected function getSyncedShareSelectSql(): CoreQueryBuilder { $qb = $this->getQueryBuilder(); - - $qb->select('s.id', 's.item_id', 's.circle_id', 's.instance') - ->from(self::TABLE_SHARE_LOCK, 's') - ->setDefaultSelectAlias('s'); + $qb->generateSelect( + self::TABLE_SYNC_SHARE, + self::$tables[self::TABLE_SYNC_SHARE], + CoreQueryBuilder::SYNC_SHARE + ); return $qb; } @@ -71,9 +73,9 @@ protected function getShareLockSelectSql(): CoreQueryBuilder { /** * @return CoreQueryBuilder */ - protected function getShareLockUpdateSql(): CoreQueryBuilder { + protected function getSyncedShareUpdateSql(): CoreQueryBuilder { $qb = $this->getQueryBuilder(); - $qb->update(self::TABLE_SHARE_LOCK); + $qb->update(self::TABLE_SYNC_SHARE); return $qb; } @@ -82,9 +84,9 @@ protected function getShareLockUpdateSql(): CoreQueryBuilder { /** * @return CoreQueryBuilder */ - protected function getShareDeleteSql(): CoreQueryBuilder { + protected function getSyncedShareDeleteSql(): CoreQueryBuilder { $qb = $this->getQueryBuilder(); - $qb->delete(self::TABLE_SHARE_LOCK); + $qb->delete(self::TABLE_SYNC_SHARE); return $qb; } @@ -93,27 +95,27 @@ protected function getShareDeleteSql(): CoreQueryBuilder { /** * @param CoreQueryBuilder $qb * - * @return FederatedShare - * @throws FederatedShareNotFoundException + * @return SyncedShare + * @throws SyncedShareNotFoundException */ - public function getItemFromRequest(CoreQueryBuilder $qb): FederatedShare { - /** @var FederatedShare $circle */ + public function getItemFromRequest(CoreQueryBuilder $qb): SyncedShare { + /** @var SyncedShare $lock */ try { - $circle = $qb->asItem(FederatedShare::class); - } catch (RowNotFoundException $e) { - throw new FederatedShareNotFoundException(); + $lock = $qb->asItem(SyncedShare::class); + } catch (RowNotFoundException | InvalidItemException $e) { + throw new SyncedShareNotFoundException(); } - return $circle; + return $lock; } /** * @param CoreQueryBuilder $qb * - * @return FederatedShare[] + * @return SyncedShare[] */ public function getItemsFromRequest(CoreQueryBuilder $qb): array { - /** @var FederatedShare[] $result */ - return $qb->asItems(FederatedShare::class); + /** @var SyncedShare[] $result */ + return $qb->asItems(SyncedShare::class); } } diff --git a/lib/Exceptions/CircleSharesManagerException.php b/lib/Exceptions/CircleSharesManagerException.php new file mode 100644 index 000000000..1595df54c --- /dev/null +++ b/lib/Exceptions/CircleSharesManagerException.php @@ -0,0 +1,36 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Exceptions; + +use Exception; + +class CircleSharesManagerException extends Exception { +} diff --git a/lib/Exceptions/FederatedShareBelongingException.php b/lib/Exceptions/DebugNotFoundException.php similarity index 94% rename from lib/Exceptions/FederatedShareBelongingException.php rename to lib/Exceptions/DebugNotFoundException.php index 42f7e0b1d..80b3dd3bc 100644 --- a/lib/Exceptions/FederatedShareBelongingException.php +++ b/lib/Exceptions/DebugNotFoundException.php @@ -30,5 +30,5 @@ namespace OCA\Circles\Exceptions; -class FederatedShareBelongingException extends FederatedItemException { +class DebugNotFoundException extends FederatedItemBadRequestException { } diff --git a/lib/Exceptions/FederatedSyncConflictException.php b/lib/Exceptions/FederatedSyncConflictException.php new file mode 100644 index 000000000..99a3412d3 --- /dev/null +++ b/lib/Exceptions/FederatedSyncConflictException.php @@ -0,0 +1,36 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Exceptions; + +use Exception; + +class FederatedSyncConflictException extends Exception { +} diff --git a/lib/Exceptions/FederatedSyncManagerNotFoundException.php b/lib/Exceptions/FederatedSyncManagerNotFoundException.php new file mode 100644 index 000000000..5e0714481 --- /dev/null +++ b/lib/Exceptions/FederatedSyncManagerNotFoundException.php @@ -0,0 +1,36 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Exceptions; + +use Exception; + +class FederatedSyncManagerNotFoundException extends Exception { +} diff --git a/lib/Exceptions/SyncedItemNotFoundException.php b/lib/Exceptions/SyncedItemNotFoundException.php new file mode 100644 index 000000000..719cfaa91 --- /dev/null +++ b/lib/Exceptions/SyncedItemNotFoundException.php @@ -0,0 +1,34 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Exceptions; + +class SyncedItemNotFoundException extends FederatedItemBadRequestException { +} diff --git a/lib/Exceptions/FederatedShareNotFoundException.php b/lib/Exceptions/SyncedShareNotFoundException.php similarity index 94% rename from lib/Exceptions/FederatedShareNotFoundException.php rename to lib/Exceptions/SyncedShareNotFoundException.php index 9113fdfab..e9725eea9 100644 --- a/lib/Exceptions/FederatedShareNotFoundException.php +++ b/lib/Exceptions/SyncedShareNotFoundException.php @@ -32,5 +32,5 @@ use Exception; -class FederatedShareNotFoundException extends Exception { +class SyncedShareNotFoundException extends Exception { } diff --git a/lib/Exceptions/FederatedShareAlreadyLockedException.php b/lib/Exceptions/SyncedSharedAlreadyExistException.php similarity index 94% rename from lib/Exceptions/FederatedShareAlreadyLockedException.php rename to lib/Exceptions/SyncedSharedAlreadyExistException.php index 993339c88..747f8c58f 100644 --- a/lib/Exceptions/FederatedShareAlreadyLockedException.php +++ b/lib/Exceptions/SyncedSharedAlreadyExistException.php @@ -32,5 +32,5 @@ use Exception; -class FederatedShareAlreadyLockedException extends Exception { +class SyncedSharedAlreadyExistException extends Exception { } diff --git a/lib/FederatedItems/CircleSettings.php b/lib/FederatedItems/CircleSettings.php deleted file mode 100644 index 0521c23f5..000000000 --- a/lib/FederatedItems/CircleSettings.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @copyright 2021 - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - - -namespace OCA\Circles\FederatedItems; - -use OCA\Circles\Tools\Traits\TDeserialize; -use OCA\Circles\Db\CircleRequest; -use OCA\Circles\IFederatedItem; -use OCA\Circles\Model\Federated\FederatedEvent; - -/** - * Class CircleSettings - * - * @package OCA\Circles\FederatedItems - */ -class CircleSettings implements IFederatedItem { - use TDeserialize; - - - /** @var CircleRequest */ - private $circleRequest; - - - /** - * CircleSettings constructor. - * - * @param CircleRequest $circleRequest - */ - public function __construct(CircleRequest $circleRequest) { - $this->circleRequest = $circleRequest; - } - - - /** - * @param FederatedEvent $event - */ - public function verify(FederatedEvent $event): void { - $circle = $event->getCircle(); - $new = clone $circle; - $event->setOutcome($this->serialize($new)); - } - - - /** - * @param FederatedEvent $event - */ - public function manage(FederatedEvent $event): void { - } - - - /** - * @param FederatedEvent $event - * @param array $results - */ - public function result(FederatedEvent $event, array $results): void { - } -} diff --git a/lib/FederatedItems/FederatedSync/ShareCreation.php b/lib/FederatedItems/FederatedSync/ShareCreation.php new file mode 100644 index 000000000..2f5871561 --- /dev/null +++ b/lib/FederatedItems/FederatedSync/ShareCreation.php @@ -0,0 +1,150 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\FederatedItems\FederatedSync; + +use OCA\Circles\Db\SyncedItemRequest; +use OCA\Circles\Exceptions\FederatedSyncConflictException; +use OCA\Circles\Exceptions\RequestBuilderException; +use OCA\Circles\Exceptions\SyncedItemNotFoundException; +use OCA\Circles\IFederatedItem; +use OCA\Circles\IFederatedItemHighSeverity; +use OCA\Circles\IFederatedItemSyncedItem; +use OCA\Circles\Model\Federated\FederatedEvent; +use OCA\Circles\Service\ConfigService; +use OCA\Circles\Service\DebugService; +use OCA\Circles\Tools\Traits\TDeserialize; + + +class ShareCreation implements + IFederatedItem, + IFederatedItemHighSeverity, + IFederatedItemSyncedItem { + use TDeserialize; + + private SyncedItemRequest $syncedItemRequest; + private ConfigService $configService; + private DebugService $debugService; + + + /** + * @param SyncedItemRequest $syncedItemRequest + * @param ConfigService $configService + * @param DebugService $debugService + */ + public function __construct( + SyncedItemRequest $syncedItemRequest, + ConfigService $configService, + DebugService $debugService + ) { + $this->syncedItemRequest = $syncedItemRequest; + $this->configService = $configService; + $this->debugService = $debugService; + } + + + /** + * @param FederatedEvent $event + * + * @throws FederatedSyncConflictException + */ + public function verify(FederatedEvent $event): void { + $circle = $event->getCircle(); + $syncedItem = $event->getSyncedItem(); +// $initiator = $circle->getInitiator(); + + $this->debugService->info( + 'verify', $circle->getSingleId(), + [ + 'event' => $event, + 'circle' => $circle, + 'syncedItem' => $syncedItem + ] + ); + + $syncedItem->setInstance($event->getOrigin()); + + try { + $knownItem = $this->syncedItemRequest->getSyncedItemFromSingleId($syncedItem->getSingleId()); + if ($knownItem->getAppId() !== $syncedItem->getAppId() + || $knownItem->getItemType() !== $syncedItem->getItemType() + || $knownItem->getInstance() !== $syncedItem->getInstance() + || $knownItem->isDeleted()) { + throw new FederatedSyncConflictException(); + } + } catch (SyncedItemNotFoundException $e) { + } + } + + + /** + * @param FederatedEvent $event + * + * @throws RequestBuilderException + */ + public function manage(FederatedEvent $event): void { + if ($this->configService->isLocalInstance($event->getOrigin())) { + return; + } + + $circle = $event->getCircle(); + $syncedItem = $event->getSyncedItem(); + + try { + $knownItem = $this->syncedItemRequest->getSyncedItemFromSingleId($syncedItem->getSingleId()); + if ($knownItem->getAppId() !== $syncedItem->getAppId() + || $knownItem->getItemType() !== $syncedItem->getItemType() + || $knownItem->getInstance() !== $event->getOrigin() + || $knownItem->isDeleted()) { + // TODO: manage dsync on item + return; + } + } catch (SyncedItemNotFoundException $e) { +// $this- + } + +// instance3 verify itemSingleId exists in circles_item. +// If it does, compare with appId, itemType, itemId and confirm instance=instance1. +// instance3 send a signed request to instance1 to retrieve content of the shared item, based on itemSingleId, appId, itemType, itemId, circleId. + + echo '___ ' . json_encode($syncedItem); + + + } + + + /** + * @param FederatedEvent $event + * @param array $results + */ + public function result(FederatedEvent $event, array $results): void { + } +} diff --git a/lib/FederatedItems/ItemLock.php b/lib/FederatedItems/ItemLock.php deleted file mode 100644 index f3254a66d..000000000 --- a/lib/FederatedItems/ItemLock.php +++ /dev/null @@ -1,130 +0,0 @@ - - * @copyright 2017 - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - - -namespace OCA\Circles\FederatedItems; - -use OCA\Circles\Tools\Traits\TStringTools; -use OCA\Circles\Db\ShareLockRequest; -use OCA\Circles\Exceptions\FederatedShareNotFoundException; -use OCA\Circles\Exceptions\InvalidIdException; -use OCA\Circles\IFederatedItem; -use OCA\Circles\IFederatedItemDataRequestOnly; -use OCA\Circles\Model\Federated\FederatedEvent; -use OCA\Circles\Model\Federated\FederatedShare; - -/** - * Class ItemLock - * - * @package OCA\Circles\FederatedItems - */ -class ItemLock implements - IFederatedItem, - IFederatedItemDataRequestOnly { - use TStringTools; - - - public const STATUS_LOCKED = 'locked'; - public const STATUS_ALREADY_LOCKED = 'already_locked'; - public const STATUS_INSTANCE_LOCKED = 'instance_locked'; - - - /** @var ShareLockRequest */ - private $shareLockRequest; - - - /** - * ItemLock constructor. - * - * @param ShareLockRequest $shareLockRequest - */ - public function __construct(ShareLockRequest $shareLockRequest) { - $this->shareLockRequest = $shareLockRequest; - } - - - /** - * create lock in db if the lock does not exist for this circle. - * will fail if the lock already exist for anothr instance, even for another circle - * - * @param FederatedEvent $event - * - * @throws InvalidIdException - * @throws FederatedShareNotFoundException - */ - public function verify(FederatedEvent $event): void { - $itemId = $event->getParams()->g('itemId'); - $this->shareLockRequest->confirmValidId($itemId); - - $status = ''; - try { - $known = $this->shareLockRequest->getShare($itemId); - - if ($known->getInstance() === $event->getSender()) { - $status = self::STATUS_ALREADY_LOCKED; - $known = $this->shareLockRequest->getShare($itemId, $event->getCircle()->getSingleId()); - } else { - $status = self::STATUS_INSTANCE_LOCKED; - } - } catch (FederatedShareNotFoundException $e) { - $share = new FederatedShare(); - $share->setItemId($itemId); - $share->setCircleId($event->getCircle()->getSingleId()); - $share->setInstance($event->getSender()); - - $this->shareLockRequest->save($share); - $known = $this->shareLockRequest->getShare($itemId); - if ($status === '') { - $status = self::STATUS_LOCKED; - } - } - - $known->setLockStatus($status); - $event->setOutcome(['federatedShare' => $known]); - } - - - /** - * @param FederatedEvent $event - */ - public function manage(FederatedEvent $event): void { -// $this->circleEventService->onSharedItemsSyncRequested($event); -// -// $event->setResult(new SimpleDataStore(['shares' => 'ok'])); - } - - - /** - * @param FederatedEvent $event - * @param array $results - */ - public function result(FederatedEvent $event, array $results): void { - } -} diff --git a/lib/ICircleSharesManager.php b/lib/ICircleSharesManager.php new file mode 100644 index 000000000..2785343a9 --- /dev/null +++ b/lib/ICircleSharesManager.php @@ -0,0 +1,101 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles; + +/** + * Interface IShareManager + * + * @package OCA\Circles + */ +interface ICircleSharesManager { + + /** + * Register a IFederatedSyncManager + * + * This is the first step to set up in `Application.php` of any app willing to use the + * FederatedItem feature: + * + * public function boot(IBootContext $context): void { + * $circleManager = $context->getAppContainer()->get(CirclesManager::class); + * $circleManager->getShareManager() + * ->registerFederatedSyncManager(TestFederatedSync::class); + * } + * + * @param string $syncManager the class that implemented IFederatedSyncManager + */ + public function registerFederatedSyncManager(string $syncManager): void; + + /** + * Initiate a share of an item to a circle. + * Your app can add some extraData about the share that can be needed during the process + * + * @param string $itemId + * @param string $circleId + * @param array $extraData + */ + public function createShare(string $itemId, string $circleId, array $extraData = []): void; + + /** + * Initiate an update on a share. (ie. permissions) + * Your app can add some extraData about the share that can be needed during the process + * + * @param string $itemId + * @param string $circleId + * @param array $extraData + */ + public function updateShare(string $itemId, string $circleId, array $extraData = []): void; + + /** + * Initiate the deletion of a share + * + * @param string $itemId + * @param string $circleId + */ + public function deleteShare(string $itemId, string $circleId): void; + + /** + * Initiate the update of a share + * $serializedData contains the serialized data of the item that will be used during the process by your + * app to update that content in its table + * + * @param string $itemId + * @param array $extraData + */ + public function updateItem(string $itemId, array $extraData): void; + + /** + * Initiate the deletion of an Item + * + * @param string $itemId + */ + public function deleteItem(string $itemId): void; +} diff --git a/lib/IFederatedSync.php b/lib/IFederatedItemSyncedItem.php similarity index 73% rename from lib/IFederatedSync.php rename to lib/IFederatedItemSyncedItem.php index 25683e685..4ae1cdb2f 100644 --- a/lib/IFederatedSync.php +++ b/lib/IFederatedItemSyncedItem.php @@ -10,7 +10,7 @@ * later. See the COPYING file. * * @author Maxence Lange - * @copyright 2021 + * @copyright 2022 * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify @@ -31,24 +31,12 @@ namespace OCA\Circles; -use OCA\Circles\Tools\Model\SimpleDataStore; - /** - * Interface IFederatedSync + * SyncedItem from IFederatedItem will be check: * - * @package OCA\Circles + * - SyncedItem needs to be present + * - SyncedItem needs to come from the same instance that the request */ -interface IFederatedSync { - - /** - * @param string $circleId - * - * @return SimpleDataStore - */ - public function export(string $circleId): SimpleDataStore; +interface IFederatedItemSyncedItem { - /** - * @param SimpleDataStore $data - */ - public function import(SimpleDataStore $data): void; } diff --git a/lib/IFederatedSyncManager.php b/lib/IFederatedSyncManager.php new file mode 100644 index 000000000..c10ec0998 --- /dev/null +++ b/lib/IFederatedSyncManager.php @@ -0,0 +1,293 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles; + +use OCA\Circles\Exceptions\SyncedItemNotFoundException; +use OCA\Circles\Model\FederatedUser; +use OCA\Circles\Model\Membership; + +/** + * Interface IFederatedSyncManager + * + * @package OCA\Circles + */ +interface IFederatedSyncManager { + + + /** + * The string that id the app, must be unique. + * + * @return string + */ + public function getAppId(): string; + + /** + * The same app can manage FederatedItem on multiple types of items. + * If this is the case, your app will need to register as many IFederatedSyncManager than types + * of items to be managed. + * + * Each IFederatedSyncManager will use a different string to identify each type of items. + * + * @return string + */ + public function getItemType(): string; + + /** + * because there can be exchange between different version of your app you can keep trace of + * used version + * + * @return int + */ + public function getApiVersion(): int; + + /** + * limit to exchange only with Api that are equal or above this value + * + * @return int + */ + public function getApiLowerBackCompatibility(): int; + + /** + * return true if FullSupport + * + * @return bool + */ + public function isFullSupport(): bool; + + + /** + * The method is called during data synchronisation, to serialize an item. + * Your app need to return the item as an array based on itemId + * + * @param string $itemId + * + * @return array + * @throws SyncedItemNotFoundException + */ + public function serializeItem(string $itemId): array; + + + /** + * This method is called when data with different checksum is received from the instance that created the + * shared item. + * $serializedData is the array returned by serializeItem(itemId). + * + * Your app needs to update the information related to the item identified by itemId in its own + * table. + * + * IMPORTANT: If itemId is contained within the serialized data of an item, your app needs to + * compare $itemId with the itemId stored within the $serializedData + * + * @param string $itemId + * @param array $serializedData + */ + public function syncItem(string $itemId, array $serializedData): void; + + + /** + * Your app returns details about a share. + * Method will be called to re-sync a share on a remote instance + * + * @param string $itemId + * @param string $circleId + * + * @return array + */ + public function getShareDetails(string $itemId, string $circleId): array; + + /** + * Force an update of the share, based on the $extraData returned by getShareDetails() + * + * @param string $itemId + * @param string $circleId + * @param array $extraData + */ + public function syncShare(string $itemId, string $circleId, array $extraData): void; + + + /** + * Your app returns if the share is creatable at that point. + * Method is only called on the instance that owns the shared item + * + * @param string $itemId + * @param string $circleId + * @param array $extraData + * @param FederatedUser $federatedUser + * + * @return bool + */ + public function isShareCreatable( + string $itemId, + string $circleId, + array $extraData, + FederatedUser $federatedUser + ): bool; + + + /** + * Is called when the share looks valid. + * Method is called on every instance. + * + * In this method, your app needs to create its own entries in its table regarding the new share + * + * $extraData is the array sent when your app initiated the process with + * ICircleSharesManager::createShare(itemId, circleId, extraData); + * + * Note: In case of isFullSupport() returns false, it will not be executed on the instance that owns the + * item. + * + * $membership contains enough data about the author of the share for your app to generate its + * own event (activity, mail, ...) + * + * @param string $itemId + * @param string $circleId + * @param array $extraData + * @param FederatedUser $federatedUser + */ + public function onShareCreation( + string $itemId, + string $circleId, + array $extraData, + FederatedUser $federatedUser + ): void; + + + /** + * Your app returns if the share is modifiable at that point. + * Method is only called on the instance that owns the shared item + * + * @param string $itemId + * @param string $circleId + * @param array $extraData + * @param Membership $membership + * + * @return bool + */ + public function isShareModifiable( + string $itemId, + string $circleId, + array $extraData, + Membership $membership + ): bool; + + + /** + * Is called when the share looks valid. + * Method is called on every instance. + * + * In this method, your app needs to update its own entries in its table the modified share + * + * $extraData is the array sent when your app initiated the process with + * ICircleSharesManager::updateShare(itemId, circleId, extraData); + * + * Note: In case of $fullSupport===false when registering the IFederateShareManager, it will not be + * executed on the instance that owns the item. + * + * $membership contains enough data about the author of the share for your app to generate its + * own event (activity, mail, ...) + * + * @param string $itemId + * @param string $circleId + * @param array $extraData + * @param Membership $membership + */ + public function onShareModification( + string $itemId, + string $circleId, + array $extraData, + Membership $membership + ): void; + + + /** + * Your app returns if the share is deletable at that point. + * Method is only called on the instance that owns the shared item + * + * @param string $itemId + * @param string $circleId + * @param Membership $membership + * + * @return bool + */ + public function isShareDeletable( + string $itemId, + string $circleId, + Membership $membership + ): bool; + + + /** + * Is called when the share is supposed to be deleted. + * Method is called on every instance. + * + * In this method, your app needs to delete its own entries in its table about the deleted share + * + * Note: In case of $fullSupport===false when registering the IFederateShareManager, it will not be + * executed on the instance that owns the item. + * + * $membership contains enough data about the author of the share for your app to generate its + * own event (activity, mail, ...) + * + * @param string $itemId + * @param string $circleId + * @param Membership $membership + */ + public function onShareDeletion( + string $itemId, + string $circleId, + Membership $membership + ): void; + + + /** + * Your app returns if the item can be updated by $federatedUser. + * + * Membership of $federatedUser can go through multiple paths as the same item can be shared to different + * circles $federatedUser is a member. Meaning multiple permissions needs to be checked. + * + * $serializedData can be modified/fixed within the method before being stored. Maybe some data cannot be + * edited based on permissions + * + * Method is only called on the instance that owns the shared item + * + * @param string $itemId + * @param array $extraData + * @param FederatedUser $federatedUser + * + * @return bool + */ + public function isItemUpdatable( + string $itemId, + array $extraData, + FederatedUser $federatedUser + ): bool; +} diff --git a/lib/IReferencedObject.php b/lib/IReferencedObject.php new file mode 100644 index 000000000..985c2b5d7 --- /dev/null +++ b/lib/IReferencedObject.php @@ -0,0 +1,34 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Tools; + +interface IReferencedObject extends IDeserializable { +} diff --git a/lib/Migration/Version0025Date20220510104622.php b/lib/Migration/Version0025Date20220510104622.php new file mode 100644 index 000000000..f68192939 --- /dev/null +++ b/lib/Migration/Version0025Date20220510104622.php @@ -0,0 +1,243 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Migration; + +use Closure; +use Doctrine\DBAL\Schema\SchemaException; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version0025Date20220510104622 extends SimpleMigrationStep { + + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + } + + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * + * @return null|ISchemaWrapper + * @throws SchemaException + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('circles_item')) { + $table = $schema->createTable('circles_item'); + $table->addColumn( + 'id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'single_id', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + $table->addColumn( + 'instance', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ] + ); + $table->addColumn( + 'app_id', Types::STRING, [ + 'length' => 255, + 'notnull' => false + ] + ); + $table->addColumn( + 'item_type', Types::STRING, [ + 'length' => 127, + 'notnull' => false + ] + ); + $table->addColumn( + 'item_id', Types::STRING, [ + 'length' => 63, + 'notnull' => false + ] + ); + $table->addColumn( + 'checksum', Types::STRING, [ + 'length' => 127, + 'notnull' => false + ] + ); + $table->addColumn( + 'deleted', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false + ] + ); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['single_id']); + $table->addIndex(['app_id', 'item_type', 'item_id'], 'c_aiitii'); + } + + if (!$schema->hasTable('circles_share')) { + $table = $schema->createTable('circles_share'); + $table->addColumn( + 'id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'single_id', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + $table->addColumn( + 'circle_id', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['single_id', 'circle_id'], 'c_sici'); + } + + if (!$schema->hasTable('circles_lock')) { + $table = $schema->createTable('circles_lock'); + $table->addColumn( + 'id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'single_id', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + $table->addColumn( + 'update_type', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + $table->addColumn( + 'update_type_id', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + $table->addColumn( + 'time', Types::INTEGER, [ + 'notnull' => false, + 'length' => 7, + 'unsigned' => true + ] + ); + + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['single_id', 'update_type', 'update_type_id'], 'c_siututi'); + } + + if (!$schema->hasTable('circles_debug')) { + $table = $schema->createTable('circles_debug'); + $table->addColumn( + 'id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 14, + 'unsigned' => true, + ] + ); + $table->addColumn( + 'thread', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + $table->addColumn( + 'type', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + $table->addColumn( + 'circle_id', Types::STRING, [ + 'notnull' => false, + 'length' => 31, + ] + ); + $table->addColumn( + 'instance', Types::STRING, [ + 'notnull' => false, + 'length' => 127, + ] + ); + $table->addColumn( + 'debug', Types::TEXT, [ + 'notnull' => false + ] + ); + $table->addColumn( + 'time', Types::INTEGER, [ + 'notnull' => false, + 'length' => 7, + 'unsigned' => true + ] + ); + + $table->setPrimaryKey(['id']); + $table->addIndex(['circle_id', 'instance'], 'circles_debug_cii'); + $table->addIndex(['time']); + } + + return $schema; + } +} diff --git a/lib/Model/Circle.php b/lib/Model/Circle.php index 03c54e394..e2c22ce48 100644 --- a/lib/Model/Circle.php +++ b/lib/Model/Circle.php @@ -50,6 +50,7 @@ use OCA\Circles\Tools\Db\IQueryRow; use OCA\Circles\Tools\Exceptions\InvalidItemException; use OCA\Circles\Tools\IDeserializable; +use OCA\Circles\Tools\IReferencedObject; use OCA\Circles\Tools\Traits\TArrayTools; use OCA\Circles\Tools\Traits\TDeserialize; use OCP\Security\IHasher; @@ -82,7 +83,11 @@ * * @package OCA\Circles\Model */ -class Circle extends ManagedModel implements IEntity, IDeserializable, IQueryRow, JsonSerializable { +class Circle extends ManagedModel implements IEntity, + IReferencedObject, + IQueryRow, + JsonSerializable { + use TArrayTools; use TDeserialize; diff --git a/lib/Model/Debug.php b/lib/Model/Debug.php new file mode 100644 index 000000000..d5110fdc0 --- /dev/null +++ b/lib/Model/Debug.php @@ -0,0 +1,286 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Model; + +use JetBrains\PhpStorm\ArrayShape; +use JetBrains\PhpStorm\Pure; +use JsonSerializable; +use OCA\Circles\Tools\Db\IQueryRow; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\IDeserializable; +use OCA\Circles\Tools\IReferencedObject; +use OCA\Circles\Tools\Model\ReferencedDataStore; +use OCA\Circles\Tools\Traits\TArrayTools; +use OCA\Circles\Tools\Traits\TDeserialize; + +class Debug implements + IReferencedObject, + IQueryRow, + JsonSerializable { + + use TArrayTools; + use TDeserialize; + + private int $id; + private string $thread; + private string $type; + private string $circleId; + private string $instance = ''; + private ReferencedDataStore $debug; + private int $time = 0; + + + /** + * @param ReferencedDataStore|null $data + * @param string $circleId + */ + public function __construct( + ?ReferencedDataStore $data = null, + string $circleId = '', + string $thread = '', + string $type = '' + ) { + if (!is_null($data)) { + $this->setDebug($data); + } + + $this->setThread($thread); + $this->setCircleId($circleId); + $this->setType($type); + } + + + /** + * @param int $id + * + * @return Debug + */ + public function setId(int $id): self { + $this->id = $id; + + return $this; + } + + /** + * @return int + */ + public function getId(): int { + return $this->id; + } + + + /** + * @param string $thread + * + * @return Debug + */ + public function setThread(string $thread): self { + $this->thread = $thread; + + return $this; + } + + /** + * @return string + */ + public function getThread(): string { + return $this->thread; + } + + + /** + * @param string $type + * + * @return Debug + */ + public function setType(string $type): self { + $this->type = $type; + + return $this; + } + + /** + * @return string + */ + public function getType(): string { + return $this->type; + } + + + /** + * @param string $circleId + *-> + * + * @return Debug + */ + public function setCircleId(string $circleId): self { + $this->circleId = $circleId; + + return $this; + } + + /** + * @return string + */ + public function getCircleId(): string { + return $this->circleId; + } + + + /** + * @param string $instance + * + * @return Debug + */ + public function setInstance(string $instance): self { + $this->instance = $instance; + + return $this; + } + + /** + * @return string + */ + public function getInstance(): string { + return $this->instance; + } + + + /** + * @param ReferencedDataStore $debug + * + * @return Debug + */ + public function setDebug(ReferencedDataStore $debug): self { + $this->debug = $debug; + + return $this; + } + + /** + * @return ReferencedDataStore + */ + public function getDebug(): ReferencedDataStore { + return $this->debug; + } + + + /** + * @param int $time + * + * @return Debug + */ + public function setTime(int $time): self { + $this->time = $time; + + return $this; + } + + /** + * @return int + */ + public function getTime(): int { + return $this->time; + } + + + /** + * @param array $data + * + * @return IDeserializable + * @throws InvalidItemException + */ + public function import(array $data): IDeserializable { + $this->setThread($this->get('thread', $data)); + $this->setType($this->get('type', $data)); + $this->setCircleId($this->get('circleId', $data)); + $this->setInstance($this->get('instance', $data)); + $this->setTime($this->getInt('time', $data)); + + /** @var ReferencedDataStore $store */ + $store = $this->deserialize($this->getArray('debug', $data), ReferencedDataStore::class); + $this->setDebug($store); + + return $this; + } + + + /** + * @param array $data + * @param string $prefix + * + * @return IQueryRow + * @throws InvalidItemException + */ + public function importFromDatabase(array $data, string $prefix = ''): IQueryRow { + if (empty($this->getArray($prefix . 'debug', $data))) { + throw new InvalidItemException(); + } + + $this->setId($this->getInt($prefix . 'id', $data)); + $this->setThread($this->get($prefix . 'thread', $data)); + $this->setType($this->get($prefix . 'type', $data)); + $this->setCircleId($this->get($prefix . 'circle_id', $data)); + $this->setInstance($this->get($prefix . 'instance', $data)); + $this->setTime($this->getInt($prefix . 'time', $data)); + + /** @var ReferencedDataStore $store */ + $store = $this->deserialize($this->getArray('debug', $data), ReferencedDataStore::class); + $this->setDebug($store); + + return $this; + } + + + /** + * @return array + */ + #[Pure] + #[ArrayShape([ + 'thread' => 'string', + 'type' => 'string', + 'circleId' => 'string', + 'instance' => 'string', + 'debug' => '\OCA\Circles\Tools\Model\ReferencedDataStore', + 'time' => 'int' + ])] + public function jsonSerialize(): array { + return [ + 'thread' => $this->getThread(), + 'type' => $this->getType(), + 'circleId' => $this->getCircleId(), + 'instance' => $this->getInstance(), + 'debug' => $this->getDebug(), + 'time' => $this->getTime() + ]; + } +} diff --git a/lib/Model/Federated/FederatedEvent.php b/lib/Model/Federated/FederatedEvent.php index 8443f67c9..903256661 100644 --- a/lib/Model/Federated/FederatedEvent.php +++ b/lib/Model/Federated/FederatedEvent.php @@ -31,12 +31,13 @@ namespace OCA\Circles\Model\Federated; -use OCA\Circles\Tools\Exceptions\InvalidItemException; -use OCA\Circles\Tools\Model\SimpleDataStore; -use OCA\Circles\Tools\Traits\TArrayTools; use JsonSerializable; use OCA\Circles\Model\Circle; use OCA\Circles\Model\Member; +use OCA\Circles\Model\SyncedItem; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\Model\SimpleDataStore; +use OCA\Circles\Tools\Traits\TArrayTools; /** * Class FederatedEvent @@ -65,6 +66,8 @@ class FederatedEvent implements JsonSerializable { /** @var Circle */ private $circle; + private ?SyncedItem $syncedItem = null; + /** @var string */ private $itemId = ''; @@ -293,6 +296,31 @@ public function getCircle(): Circle { } + /** + * @param SyncedItem $syncedItem + * + * @return FederatedEvent + */ + public function setSyncedItem(SyncedItem $syncedItem): self { + $this->syncedItem = $syncedItem; + + return $this; + } + + /** + * @return SyncedItem + */ + public function getSyncedItem(): SyncedItem { + return $this->syncedItem; + } + + /** + * @return bool + */ + public function hasSyncedItem(): bool { + return !is_null($this->syncedItem); + } + /** * @param string $itemId * @@ -556,9 +584,7 @@ public function addResultEntry(string $key, array $result): self { * @return FederatedEvent */ public function bypass(int $flag): self { - if (!$this->canBypass($flag)) { - $this->bypass += $flag; - } + $this->bypass |= $flag; return $this; } @@ -602,6 +628,13 @@ public function import(array $data): self { $this->setMember($member); } + try { + $syncedItem = new SyncedItem(); + $syncedItem->import($this->getArray('syncedItem', $data)); + $this->setSyncedItem($syncedItem); + } catch (InvalidItemException $e) { + } + $members = []; foreach ($this->getArray('members', $data) as $item) { $member = new Member(); @@ -637,6 +670,9 @@ public function jsonSerialize(): array { if ($this->hasMember()) { $arr['member'] = $this->getMember(); } + if ($this->hasSyncedItem()) { + $arr['syncedItem'] = $this->getSyncedItem(); + } return $arr; } diff --git a/lib/Model/Federated/RemoteInstance.php b/lib/Model/Federated/RemoteInstance.php index 34eab92c1..d651ccb4d 100644 --- a/lib/Model/Federated/RemoteInstance.php +++ b/lib/Model/Federated/RemoteInstance.php @@ -31,12 +31,12 @@ namespace OCA\Circles\Model\Federated; -use OCA\Circles\Tools\Db\IQueryRow; -use OCA\Circles\Tools\Model\NCSignatory; -use OCA\Circles\Tools\Traits\TArrayTools; use JsonSerializable; use OCA\Circles\Exceptions\RemoteNotFoundException; use OCA\Circles\Exceptions\RemoteUidException; +use OCA\Circles\Tools\Db\IQueryRow; +use OCA\Circles\Tools\Model\NCSignatory; +use OCA\Circles\Tools\Traits\TArrayTools; /** * Class AppService @@ -73,6 +73,9 @@ class RemoteInstance extends NCSignatory implements IQueryRow, JsonSerializable public const INHERITED = 'inherited'; public const UID = 'uid'; public const AUTH_SIGNED = 'auth-signed'; + public const DEBUG = 'debug'; + public const SYNC_ITEM = 'syncItem'; + public const SYNC_SHARE = 'syncShare'; /** @var int */ private $dbId = 0; @@ -125,6 +128,10 @@ class RemoteInstance extends NCSignatory implements IQueryRow, JsonSerializable /** @var bool */ private $identityAuthed = false; + private string $syncItem = ''; + private string $syncShare = ''; + private string $debug = ''; + /** * @param int $dbId @@ -277,7 +284,6 @@ public function getTest(): string { return $this->test; } - /** * @return string */ @@ -473,6 +479,59 @@ public function mustBeIdentityAuthed(): void { } + /** + * @param string $syncItem + */ + public function setSyncItem(string $syncItem): self { + $this->syncItem = $syncItem; + + return $this; + } + + /** + * @return string + */ + public function getSyncItem(): string { + return $this->syncItem; + } + + + /** + * @param string $syncShare + */ + public function setSyncShare(string $syncShare): self { + $this->syncShare = $syncShare; + + return $this; + } + + /** + * @return string + */ + public function getSyncShare(): string { + return $this->syncShare; + } + + + /** + * @param string $debug + * + * @return RemoteInstance + */ + public function setDebug(string $debug): self { + $this->debug = $debug; + + return $this; + } + + /** + * @return string + */ + public function getDebug(): string { + return $this->debug; + } + + /** * @param array $data * @@ -492,6 +551,9 @@ public function import(array $data): NCSignatory { ->setMember($this->get(self::MEMBER, $data)) ->setInherited($this->get(self::INHERITED, $data)) ->setMemberships($this->get(self::MEMBERSHIPS, $data)) + ->setDebug($this->get(self::DEBUG, $data)) + ->setSyncItem($this->get(self::SYNC_ITEM, $data)) + ->setSyncShare($this->get(self::SYNC_SHARE, $data)) ->setUid($this->get(self::UID, $data)); $algo = ''; @@ -522,7 +584,10 @@ public function jsonSerialize(): array { self::MEMBERS => $this->getMembers(), self::MEMBER => $this->getMember(), self::INHERITED => $this->getInherited(), - self::MEMBERSHIPS => $this->getMemberships() + self::MEMBERSHIPS => $this->getMemberships(), + self::SYNC_ITEM => $this->getSyncItem(), + self::SYNC_SHARE => $this->getSyncShare(), + self::DEBUG => $this->getDebug(), ]; if ($this->getAuthSigned() !== '') { diff --git a/lib/Model/Member.php b/lib/Model/Member.php index 4dd420b83..f43fe9ff9 100644 --- a/lib/Model/Member.php +++ b/lib/Model/Member.php @@ -46,6 +46,7 @@ use OCA\Circles\Tools\Db\IQueryRow; use OCA\Circles\Tools\Exceptions\InvalidItemException; use OCA\Circles\Tools\IDeserializable; +use OCA\Circles\Tools\IReferencedObject; use OCA\Circles\Tools\Traits\TArrayTools; use OCA\Circles\Tools\Traits\TDeserialize; @@ -57,7 +58,7 @@ class Member extends ManagedModel implements IEntity, IFederatedUser, - IDeserializable, + IReferencedObject, IQueryRow, JsonSerializable { use TArrayTools; @@ -741,7 +742,7 @@ public function getLink(string $singleId, bool $detailed = false): Membership { if ($singleId !== '') { $this->getManager()->getLink($this, $singleId, $detailed); } - + throw new MembershipNotFoundException(); } diff --git a/lib/Model/SyncedItem.php b/lib/Model/SyncedItem.php new file mode 100644 index 000000000..5ef9ff242 --- /dev/null +++ b/lib/Model/SyncedItem.php @@ -0,0 +1,307 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Model; + +use JetBrains\PhpStorm\ArrayShape; +use JetBrains\PhpStorm\Pure; +use JsonSerializable; +use OCA\Circles\Exceptions\SyncedItemNotFoundException; +use OCA\Circles\IFederatedModel; +use OCA\Circles\Tools\Db\IQueryRow; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\IDeserializable; +use OCA\Circles\Tools\Traits\TArrayTools; + +class SyncedItem extends ManagedModel implements IFederatedModel, IDeserializable, IQueryRow, JsonSerializable { + use TArrayTools; + + private int $id; + private string $singleId; + private string $instance = ''; + private string $appId; + private string $itemType; + private string $itemId; + private string $checksum = ''; + private bool $deleted = false; + + public function __construct() { + } + + + /** + * @param int $id + * + * @return SyncedItem + */ + public function setId(int $id): self { + $this->id = $id; + + return $this; + } + + /** + * @return int + */ + public function getId(): int { + return $this->id; + } + + + /** + * @param string $singleId + * + * @return SyncedItem + */ + public function setSingleId(string $singleId): self { + $this->singleId = $singleId; + + return $this; + } + + /** + * @return string + */ + public function getSingleId(): string { + return $this->singleId; + } + + + /** + * @param string $instance + * + * @return SyncedItem + */ + public function setInstance(string $instance): self { + $this->instance = $instance; + + return $this; + } + + /** + * @return string + */ + public function getInstance(): string { + return $this->instance; + } + + /** + * @return bool + */ + public function isLocal(): bool { + return $this->getManager()->isLocalInstance($this->getInstance()); + } + + + /** + * @param string $appId + * + * @return SyncedItem + */ + public function setAppId(string $appId): self { + $this->appId = $appId; + + return $this; + } + + /** + * @return string + */ + public function getAppId(): string { + return $this->appId; + } + + + /** + * @param string $itemType + * + * @return SyncedItem + */ + public function setItemType(string $itemType): self { + $this->itemType = $itemType; + + return $this; + } + + /** + * @return string + */ + public function getItemType(): string { + return $this->itemType; + } + + + /** + * @param string $itemId + * + * @return SyncedItem + */ + public function setItemId(string $itemId): self { + $this->itemId = $itemId; + + return $this; + } + + /** + * @param int $itemId + * + * @return SyncedItem + */ + public function setItemIdAsInt(int $itemId): self { + $this->itemId = (string)$itemId; + + return $this; + } + + /** + * @return string + */ + public function getItemId(): string { + return $this->itemId; + } + + /** + * @return int + */ + public function getItemIdAsInt(): int { + return (int)$this->itemId; + } + + /** + * @param string $checksum + * + * @return SyncedItem + */ + public function setChecksum(string $checksum): self { + $this->checksum = $checksum; + + return $this; + } + + /** + * @return string + */ + public function getChecksum(): string { + return $this->checksum; + } + + + /** + * @param bool $deleted + */ + public function setDeleted(bool $deleted): void { + $this->deleted = $deleted; + } + + /** + * @return bool + */ + public function isDeleted(): bool { + return $this->deleted; + } + + + /** + * @param array $data + * + * @return ShareToken + * @throws InvalidItemException + */ + public function import(array $data): IDeserializable { + if ($this->get('singleId', $data) === 0) { + throw new InvalidItemException(); + } + + $this->setSingleId($this->get('singleId', $data)); + $this->setInstance($this->get('instance', $data)); + $this->setAppId($this->get('appId', $data)); + $this->setItemType($this->get('itemType', $data)); + $this->setItemId($this->get('itemId', $data)); + $this->setChecksum($this->get('checksum', $data)); + $this->setDeleted($this->getBool('deleted', $data)); + + return $this; + } + + + /** + * @param array $data + * @param string $prefix + * + * @return IQueryRow + * @throws SyncedItemNotFoundException + */ + public function importFromDatabase(array $data, string $prefix = ''): IQueryRow { + if ($this->get($prefix . 'single_id', $data) === '') { + throw new SyncedItemNotFoundException(); + } + + $this->setSingleId($this->get($prefix . 'single_id', $data)); + $this->setInstance($this->get($prefix . 'instance', $data)); + $this->setInstance($this->get($prefix . 'instance', $data)); + $this->setAppId($this->get($prefix . 'app_id', $data)); + $this->setItemType($this->get($prefix . 'item_type', $data)); + $this->setItemId($this->get($prefix . 'item_id', $data)); + $this->setChecksum($this->get($prefix . 'checksum', $data)); + $this->setDeleted($this->getBool($prefix . 'deleted', $data)); + + if ($this->getInstance() === '') { + $this->setInstance($this->getManager()->getLocalInstance()); + } + + return $this; + } + + /** + * @return array + */ + #[Pure] + #[ArrayShape([ + 'singleId' => 'string', + 'instance' => 'string', + 'appId' => 'string', + 'itemType' => 'string', + 'itemId' => 'string', + 'checksum' => 'string', + 'deleted' => 'bool' + ])] + public function jsonSerialize(): array { + return [ + 'singleId' => $this->getSingleId(), + 'instance' => $this->getInstance(), + 'appId' => $this->getAppId(), + 'itemType' => $this->getItemType(), + 'itemId' => $this->getItemId(), + 'checksum' => $this->getChecksum(), + 'deleted' => $this->isDeleted() + ]; + } +} diff --git a/lib/Model/SyncedItemLock.php b/lib/Model/SyncedItemLock.php new file mode 100644 index 000000000..230a34cca --- /dev/null +++ b/lib/Model/SyncedItemLock.php @@ -0,0 +1,238 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Model; + +use JetBrains\PhpStorm\ArrayShape; +use JetBrains\PhpStorm\Pure; +use JsonSerializable; +use OCA\Circles\Exceptions\ShareTokenNotFoundException; +use OCA\Circles\Tools\Db\IQueryRow; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\IDeserializable; +use OCA\Circles\Tools\Traits\TArrayTools; + +class SyncedItemLock implements IDeserializable, IQueryRow, JsonSerializable { + use TArrayTools; + + private int $id; + private string $singleId; + private string $updateType; + private string $updateTypeId; + private int $time; + private bool $verifyChecksum; + + + public function __construct( + string $updateType = '', + string $updateTypeId = '', + bool $verifyChecksum = false + ) { + $this->updateType = $updateType; + $this->updateTypeId = $updateTypeId; + $this->verifyChecksum = $verifyChecksum; + } + + + /** + * @param int $id + * + * @return SyncedItemLock + */ + public function setId(int $id): self { + $this->id = $id; + + return $this; + } + + /** + * @return int + */ + public function getId(): int { + return $this->id; + } + + + /** + * @param string $singleId + * + * @return SyncedItemLock + */ + public function setSingleId(string $singleId): self { + $this->singleId = $singleId; + + return $this; + } + + /** + * @return string + */ + public function getSingleId(): string { + return $this->singleId; + } + + /** + * @param string $updateType + * + * @return SyncedItemLock + */ + public function setUpdateType(string $updateType): self { + $this->updateType = $updateType; + + return $this; + } + + /** + * @return string + */ + public function getUpdateType(): string { + return $this->updateType; + } + + + /** + * @param string $updateTypeId + * + * @return SyncedItemLock + */ + public function setUpdateTypeId(string $updateTypeId): self { + $this->updateTypeId = $updateTypeId; + + return $this; + } + + /** + * @return string + */ + public function getUpdateTypeId(): string { + return $this->updateTypeId; + } + + + /** + * @param int $time + * + * @return SyncedItemLock + */ + public function setTime(int $time): self { + $this->time = $time; + + return $this; + } + + /** + * @return int + */ + public function getTime(): int { + return $this->time; + } + + + /** + * @param bool $verifyChecksum + * + * @return SyncedItemLock + */ + public function setVerifyChecksum(bool $verifyChecksum): self { + $this->verifyChecksum = $verifyChecksum; + + return $this; + } + + /** + * @return bool + */ + public function isVerifyChecksum(): bool { + return $this->verifyChecksum; + } + + + /** + * @param array $data + * + * @return ShareToken + * @throws InvalidItemException + */ + public function import(array $data): IDeserializable { + if ($this->getInt('singleId', $data) === 0) { + throw new InvalidItemException(); + } + + $this->setSingleId($this->get('singleId', $data)); + $this->setUpdateType($this->get('updateType', $data)); + $this->setUpdateTypeId($this->get('updateTypeId', $data)); + $this->setTime($this->getInt('time', $data)); + + return $this; + } + + + /** + * @param array $data + * @param string $prefix + * + * @return IQueryRow + * @throws ShareTokenNotFoundException + */ + public function importFromDatabase(array $data, string $prefix = ''): IQueryRow { + if ($this->get($prefix . 'token', $data) === '') { + throw new ShareTokenNotFoundException(); + } + + $this->setSingleId($this->get($prefix . 'single_id', $data)); + $this->setUpdateType($this->get($prefix . 'update_type', $data)); + $this->setUpdateTypeId($this->get($prefix . 'update_type_id', $data)); + $this->setTime($this->getInt($prefix . 'time', $data)); + + return $this; + } + + /** + * @return array + */ + #[ArrayShape([ + 'singleId' => 'string', + 'updateType' => 'string', + 'updateTypeId' => 'string', + 'time' => 'int', + 'verifyChecksum' => 'bool', + 'limitToVersion' => 'bool' + ])] #[Pure] + public function jsonSerialize(): array { + return [ + 'singleId' => $this->getSingleId(), + 'updateType' => $this->getUpdateType(), + 'updateTypeId' => $this->getUpdateTypeId(), + 'time' => $this->getTime(), + 'verifyChecksum' => $this->isVerifyChecksum() + ]; + } +} diff --git a/lib/Model/SyncedShare.php b/lib/Model/SyncedShare.php new file mode 100644 index 000000000..451cff407 --- /dev/null +++ b/lib/Model/SyncedShare.php @@ -0,0 +1,157 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Model; + +use JetBrains\PhpStorm\ArrayShape; +use JetBrains\PhpStorm\Pure; +use JsonSerializable; +use OCA\Circles\Exceptions\SyncedShareNotFoundException; +use OCA\Circles\Tools\Db\IQueryRow; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\IDeserializable; +use OCA\Circles\Tools\Traits\TArrayTools; + +class SyncedShare implements IDeserializable, IQueryRow, JsonSerializable { + use TArrayTools; + + private int $id; + private string $singleId; + private string $circleId; + + public function __construct() { + } + + + /** + * @param int $id + * + * @return SyncedShare + */ + public function setId(int $id): self { + $this->id = $id; + + return $this; + } + + /** + * @return int + */ + public function getId(): int { + return $this->id; + } + + + /** + * @param string $singleId + * + * @return SyncedShare + */ + public function setSingleId(string $singleId): self { + $this->singleId = $singleId; + + return $this; + } + + /** + * @return string + */ + public function getSingleId(): string { + return $this->singleId; + } + + /** + * @param string $circleId + * + * @return SyncedShare + */ + public function setCircleId(string $circleId): self { + $this->circleId = $circleId; + + return $this; + } + + /** + * @return string + */ + public function getCircleId(): string { + return $this->circleId; + } + + + /** + * @param array $data + * + * @return ShareToken + * @throws InvalidItemException + */ + public function import(array $data): IDeserializable { + if ($this->getInt('singleId', $data) === 0) { + throw new InvalidItemException(); + } + + $this->setSingleId($this->get('singleId', $data)); + $this->setCircleId($this->get('circleId', $data)); + + return $this; + } + + + /** + * @param array $data + * @param string $prefix + * + * @return IQueryRow + * @throws SyncedShareNotFoundException + */ + public function importFromDatabase(array $data, string $prefix = ''): IQueryRow { + if ($this->get($prefix . 'single_id', $data) === '') { + throw new SyncedShareNotFoundException(); + } + + $this->setCircleId($this->get($prefix . 'circle_id', $data)); + $this->setSingleId($this->get($prefix . 'single_id', $data)); + + return $this; + } + + /** + * @return array + */ + #[Pure] + #[ArrayShape(['singleId' => 'string', 'circleId' => 'string'])] + public function jsonSerialize(): array { + return [ + 'singleId' => $this->getSingleId(), + 'circleId' => $this->getCircleId() + ]; + } +} diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 649c4c305..5947d711e 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -118,6 +118,7 @@ class ConfigService { public const MAINTENANCE_UPDATE = 'maintenance_update'; public const MAINTENANCE_RUN = 'maintenance_run'; + public const DEBUG = 'debug'; public const GS_MODE = 'mode'; public const GS_KEY = 'key'; @@ -198,6 +199,8 @@ class ConfigService { self::MAINTENANCE_UPDATE => '[]', self::MAINTENANCE_RUN => '0', + self::DEBUG => '', + self::FORCE_NC_BASE => '', self::TEST_NC_BASE => '', self::CIRCLES_CONTACT_BACKEND => '0', diff --git a/lib/Service/DebugService.php b/lib/Service/DebugService.php new file mode 100644 index 000000000..a2e0cf802 --- /dev/null +++ b/lib/Service/DebugService.php @@ -0,0 +1,229 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Service; + +use Exception; +use JsonSerializable; +use OCA\Circles\Db\DebugRequest; +use OCA\Circles\Exceptions\FederatedItemException; +use OCA\Circles\Exceptions\RemoteInstanceException; +use OCA\Circles\Exceptions\RemoteNotFoundException; +use OCA\Circles\Exceptions\RemoteResourceNotFoundException; +use OCA\Circles\Exceptions\UnknownRemoteException; +use OCA\Circles\Model\Debug; +use OCA\Circles\Model\Federated\RemoteInstance; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\IReferencedObject; +use OCA\Circles\Tools\Model\ReferencedDataStore; +use OCA\Circles\Tools\Model\Request; +use OCA\Circles\Tools\Traits\TStringTools; +use Psr\Log\LoggerInterface; + +class DebugService { + use TStringTools; + + public const ACTION = '_action'; + public const EXCEPTION = '_exception'; + public const E_CLASS = '_class'; + public const E_TRACE = '_trace'; + + const DEBUG_LOCAL = 'local'; + const DEBUG_DAEMON = 'daemon'; + + + private LoggerInterface $loggerInterface; + private DebugRequest $debugRequest; + private RemoteStreamService $remoteStreamService; + private ConfigService $configService; + + private string $debugType; + private string $processToken; + + /** + * @param DebugRequest $debugRequest + * @param RemoteStreamService $remoteStreamService + * @param ConfigService $configService + */ + public function __construct( + LoggerInterface $loggerInterface, + DebugRequest $debugRequest, + RemoteStreamService $remoteStreamService, + ConfigService $configService + ) { + $this->loggerInterface = $loggerInterface; + $this->debugRequest = $debugRequest; + $this->remoteStreamService = $remoteStreamService; + $this->configService = $configService; + + $this->processToken = $this->token(7); + $this->debugType = ''; + } + + + public function setDebugType(string $type): void { + $this->debugType = $type; + } + + /** + * @param int $history + * + * @return Debug[] + */ + public function getHistory(int $history): array { + return $this->debugRequest->getHistory($history); + } + + /** + * @param int $lastId + * + * @return Debug[] + */ + public function getSince(int $lastId): array { + return $this->debugRequest->getSince($lastId); + } + + + /** + * @param string $action + * @param string $circleId + * @param array $objects + */ + public function info(string $action, string $circleId = '', array $objects = []): void { + if (!$this->isDebugEnabled()) { + return; + } + + $store = new ReferencedDataStore(); + $store->s(self::ACTION, $action); + + try { + $this->store($store, $circleId, $objects); + } catch (Exception $e) { + //$this->logger-> + } + } + + + /** + * @param Exception $e + * @param string $circleId + * @param array $objects + */ + public function exception(Exception $e, string $circleId = '', array $objects = []): void { + if (!$this->isDebugEnabled()) { + return; + } + + $msg = $e->getMessage(); + $store = new ReferencedDataStore(); + $store->s(self::ACTION, '{?' . self::E_CLASS . '}' . (($msg !== '') ? ' (' . $msg . ')' : '')); + $store->s(self::E_CLASS, get_class($e)); + $store->s(self::EXCEPTION, $e->getMessage()); + $store->sArray(self::E_TRACE, debug_backtrace()); + + try { + $this->store($store, $circleId, $objects); + } catch (Exception $e) { + //$this->logger-> + } + } + + + /** + * @param ReferencedDataStore $store + * @param string $circleId + * @param array $objects + * + * @throws FederatedItemException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws UnknownRemoteException + */ + private function store(ReferencedDataStore $store, string $circleId, array $objects = []): void { + foreach ($objects as $k => $obj) { + try { + $store->sMixed((string)$k, $obj); + } catch (InvalidItemException $e) { + } + } + + $debug = new Debug($store, $circleId, $this->processToken, $this->debugType); + if ($this->isDebugLocal()) { + $this->save($debug); + + return; + } + + $this->remote($debug); + } + + /** + * @param Debug $debug + */ + public function save(Debug $debug): void { + $this->debugRequest->save($debug); + } + + /** + * @param Debug $debug + * + * @throws FederatedItemException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws UnknownRemoteException + */ + private function remote(Debug $debug): void { + $this->remoteStreamService->resultRequestRemoteInstance( + $this->getDebugInstance(), + RemoteInstance::DEBUG, + Request::TYPE_POST, + $debug, + ); + } + + + private function isDebugEnabled(): bool { + return ($this->configService->getAppValue(ConfigService::DEBUG) !== ''); + } + + private function isDebugLocal(): bool { + return ($this->configService->getAppValue(ConfigService::DEBUG) === self::DEBUG_LOCAL + || $this->configService->getAppValue(ConfigService::DEBUG) === self::DEBUG_DAEMON); + } + + private function getDebugInstance(): string { + return $this->configService->getAppValue(ConfigService::DEBUG); + } + +} diff --git a/lib/Service/FederatedEventService.php b/lib/Service/FederatedEventService.php index 9759da74c..5776d1f51 100644 --- a/lib/Service/FederatedEventService.php +++ b/lib/Service/FederatedEventService.php @@ -34,11 +34,8 @@ use OCA\Circles\Db\EventWrapperRequest; use OCA\Circles\Db\MemberRequest; use OCA\Circles\Db\RemoteRequest; -use OCA\Circles\Db\ShareLockRequest; use OCA\Circles\Exceptions\FederatedEventException; use OCA\Circles\Exceptions\FederatedItemException; -use OCA\Circles\Exceptions\FederatedShareBelongingException; -use OCA\Circles\Exceptions\FederatedShareNotFoundException; use OCA\Circles\Exceptions\InitiatorNotConfirmedException; use OCA\Circles\Exceptions\OwnerNotFoundException; use OCA\Circles\Exceptions\RemoteInstanceException; @@ -60,7 +57,7 @@ use OCA\Circles\IFederatedItemMemberOptional; use OCA\Circles\IFederatedItemMemberRequired; use OCA\Circles\IFederatedItemMustBeInitializedLocally; -use OCA\Circles\IFederatedItemSharedItem; +use OCA\Circles\IFederatedItemSyncedItem; use OCA\Circles\Model\Circle; use OCA\Circles\Model\Federated\EventWrapper; use OCA\Circles\Model\Federated\FederatedEvent; @@ -91,9 +88,6 @@ class FederatedEventService extends NCSignature { /** @var RemoteRequest */ private $remoteRequest; - /** @var ShareLockRequest */ - private $shareLockRequest; - /** @var MemberRequest */ private $memberRequest; @@ -106,6 +100,8 @@ class FederatedEventService extends NCSignature { /** @var ConfigService */ private $configService; + private DebugService $debugService; + /** * FederatedEventService constructor. @@ -113,27 +109,27 @@ class FederatedEventService extends NCSignature { * @param EventWrapperRequest $eventWrapperRequest * @param RemoteRequest $remoteRequest * @param MemberRequest $memberRequest - * @param ShareLockRequest $shareLockRequest * @param RemoteUpstreamService $remoteUpstreamService * @param InterfaceService $interfaceService * @param ConfigService $configService + * @param DebugService $debugService */ public function __construct( EventWrapperRequest $eventWrapperRequest, RemoteRequest $remoteRequest, MemberRequest $memberRequest, - ShareLockRequest $shareLockRequest, RemoteUpstreamService $remoteUpstreamService, InterfaceService $interfaceService, - ConfigService $configService + ConfigService $configService, + DebugService $debugService ) { $this->eventWrapperRequest = $eventWrapperRequest; $this->remoteRequest = $remoteRequest; - $this->shareLockRequest = $shareLockRequest; $this->memberRequest = $memberRequest; $this->remoteUpstreamService = $remoteUpstreamService; $this->interfaceService = $interfaceService; $this->configService = $configService; + $this->debugService = $debugService; } @@ -168,6 +164,17 @@ public function newEvent(FederatedEvent $event): array { } if ($this->configService->isLocalInstance($instance)) { + + $this->debugService->info( + 'New local event ', + ($event->hasCircle()) ? $event->getCircle()?->getSingleId() : '', + [ + 'event' => $event, + 'federatedItem' => $federatedItem, + 'instance' => $instance + ] + ); + $event->setSender($instance); $federatedItem->verify($event); @@ -181,6 +188,24 @@ public function newEvent(FederatedEvent $event): array { $this->initBroadcast($event); } else { +// $this->debugService->info( +// 'Requesting {event.instance} to confirm IFederatedEvent {event.getClass}', +// $circle->getSingleId(), +// [ +// 'event' => $event +// ] +// ); + + $this->debugService->info( + 'New event to be confirmed by {instance}', + ($event->hasCircle()) ? $event->getCircle()?->getSingleId() : '', + [ + 'event' => $event, + 'federatedItem' => $federatedItem, + 'instance' => $instance + ] + ); + $this->remoteUpstreamService->confirmEvent($event); if ($event->isDataRequestOnly()) { return $event->getOutcome(); @@ -266,8 +291,6 @@ public function getFederatedItem(FederatedEvent $event, bool $checkLocalOnly = t $this->confirmRequiredCondition($event, $item, $checkLocalOnly); $this->configureEvent($event, $item); -// $this->confirmSharedItem($event, $item); - return $item; } @@ -334,32 +357,9 @@ private function confirmRequiredCondition( if ($item instanceof IFederatedItemMustBeInitializedLocally && $checkLocalOnly) { throw new FederatedEventException('FederatedItem must be executed locally'); } - } - - /** - * @param FederatedEvent $event - * @param IFederatedItem $item - * - * @throws FederatedEventException - * @throws FederatedShareBelongingException - * @throws FederatedShareNotFoundException - * @throws OwnerNotFoundException - */ - private function confirmSharedItem(FederatedEvent $event, IFederatedItem $item): void { - if (!$item instanceof IFederatedItemSharedItem) { - return; - } - - if ($event->getItemId() === '') { - throw new FederatedEventException('FederatedItem must contains ItemId'); - } - - if ($this->configService->isLocalInstance($event->getCircle()->getInstance())) { - $shareLock = $this->shareLockRequest->getShare($event->getItemId()); - if ($shareLock->getInstance() !== $event->getSender()) { - throw new FederatedShareBelongingException('ShareLock belongs to another instance'); - } + if ($item instanceof IFederatedItemSyncedItem && !$event->hasSyncedItem()) { + throw new FederatedEventException('FederatedItem must contains a valid SyncedItem'); } } diff --git a/lib/Service/FederatedShareService.php b/lib/Service/FederatedShareService.php deleted file mode 100644 index 5ed5afa2e..000000000 --- a/lib/Service/FederatedShareService.php +++ /dev/null @@ -1,117 +0,0 @@ - - * @copyright 2021 - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OCA\Circles\Service; - -use OCA\Circles\Exceptions\CircleNotFoundException; -use OCA\Circles\Exceptions\FederatedEventDSyncException; -use OCA\Circles\Exceptions\FederatedEventException; -use OCA\Circles\Exceptions\FederatedItemException; -use OCA\Circles\Exceptions\FederatedShareAlreadyLockedException; -use OCA\Circles\Exceptions\InitiatorNotConfirmedException; -use OCA\Circles\Exceptions\InitiatorNotFoundException; -use OCA\Circles\Exceptions\OwnerNotFoundException; -use OCA\Circles\Exceptions\RemoteNotFoundException; -use OCA\Circles\Exceptions\RemoteResourceNotFoundException; -use OCA\Circles\Exceptions\UnknownRemoteException; -use OCA\Circles\FederatedItems\ItemLock; -use OCA\Circles\Model\Federated\FederatedEvent; -use OCA\Circles\Model\Federated\FederatedShare; -use OCA\Circles\Tools\ActivityPub\NCSignature; -use OCA\Circles\Tools\Exceptions\InvalidItemException; -use OCA\Circles\Tools\Exceptions\RequestNetworkException; -use OCA\Circles\Tools\Exceptions\SignatoryException; -use OCA\Circles\Tools\Exceptions\UnknownTypeException; - -/** - * Class FederatedShareService - * - * @package OCA\Circles\Service - */ -class FederatedShareService extends NCSignature { - - - /** @var FederatedEventService */ - private $federatedEventService; - - /** @var CircleService */ - private $circleService; - - - /** - * FederatedEventService constructor. - * - * @param FederatedEventService $federatedEventService - * @param CircleService $circleService - */ - public function __construct(FederatedEventService $federatedEventService, CircleService $circleService) { - $this->federatedEventService = $federatedEventService; - $this->circleService = $circleService; - } - - - /** - * @param string $circleId - * @param string $itemId - * - * @return FederatedShare - * @throws FederatedEventDSyncException - * @throws FederatedEventException - * @throws FederatedItemException - * @throws FederatedShareAlreadyLockedException - * @throws InitiatorNotConfirmedException - * @throws InvalidItemException - * @throws OwnerNotFoundException - * @throws RemoteNotFoundException - * @throws RemoteResourceNotFoundException - * @throws RequestNetworkException - * @throws SignatoryException - * @throws UnknownRemoteException - * @throws UnknownTypeException - * @throws CircleNotFoundException - * @throws InitiatorNotFoundException - */ - public function lockItem(string $circleId, string $itemId): FederatedShare { - $circle = $this->circleService->getCircle($circleId); - - $event = new FederatedEvent(ItemLock::class); - $event->setCircle($circle); - $event->getData()->s('itemId', $itemId); - $data = $this->federatedEventService->newEvent($event); - - /** @var FederatedShare $share */ - $share = $data->gObj('federatedShare', FederatedShare::class); - if ($share->getLockStatus() === ItemLock::STATUS_INSTANCE_LOCKED) { - throw new FederatedShareAlreadyLockedException('item already locked by ' . $share->getInstance()); - } - - return $share; - } -} diff --git a/lib/Service/FederatedSyncItemService.php b/lib/Service/FederatedSyncItemService.php new file mode 100644 index 000000000..e8a866633 --- /dev/null +++ b/lib/Service/FederatedSyncItemService.php @@ -0,0 +1,163 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Service; + +use Exception; +use OCA\Circles\Db\RemoteRequest; +use OCA\Circles\Db\SyncedItemRequest; +use OCA\Circles\Db\SyncedShareRequest; +use OCA\Circles\Exceptions\FederatedSyncConflictException; +use OCA\Circles\Exceptions\FederatedSyncManagerNotFoundException; +use OCA\Circles\Exceptions\SyncedItemNotFoundException; +use OCA\Circles\Model\SyncedItem; +use OCA\Circles\Tools\ActivityPub\NCSignature; +use OCA\Circles\Tools\Traits\TStringTools; + +class FederatedSyncItemService extends NCSignature { + use TStringTools; + + private SyncedItemRequest $syncedItemRequest; + private SyncedShareRequest $syncedShareRequest; + private RemoteRequest $remoteRequest; + private FederatedSyncService $federatedSyncService; + private FederatedEventService $federatedEventService; + private RemoteStreamService $remoteStreamService; + private InterfaceService $interfaceService; + private DebugService $debugService; + + + /** + * @param SyncedItemRequest $syncedItemRequest + * @param SyncedShareRequest $syncedShareRequest + * @param RemoteRequest $remoteRequest + * @param FederatedSyncService $federatedSyncService + * @param FederatedEventService $federatedEventService + * @param RemoteStreamService $remoteStreamService + * @param InterfaceService $interfaceService + * @param DebugService $debugService + */ + public function __construct( + SyncedItemRequest $syncedItemRequest, + SyncedShareRequest $syncedShareRequest, + RemoteRequest $remoteRequest, + FederatedSyncService $federatedSyncService, + FederatedEventService $federatedEventService, + RemoteStreamService $remoteStreamService, + InterfaceService $interfaceService, + DebugService $debugService + ) { + $this->syncedItemRequest = $syncedItemRequest; + $this->syncedShareRequest = $syncedShareRequest; + $this->remoteRequest = $remoteRequest; + $this->federatedSyncService = $federatedSyncService; + $this->federatedEventService = $federatedEventService; + $this->remoteStreamService = $remoteStreamService; + $this->interfaceService = $interfaceService; + $this->debugService = $debugService; + } + + + /** + * get existing SyncedItem based appId, itemType and itemId. + * + * throws FederatedSyncConflictException if sync conflict: + * - item is marked as deleted, but app have not confirmed its deletion, + * - item is not local and have no known origin. + * + * if SyncedItem does not exist, app is called to confirm itemId exist locally. If item exists, we create + * a new SyncedItem. + * + * @param string $appId + * @param string $itemType + * @param string $itemId + * + * @return SyncedItem + * @throws FederatedSyncConflictException + * @throws FederatedSyncManagerNotFoundException + */ + public function getSyncedItem(string $appId, string $itemType, string $itemId): SyncedItem { + + // verify existing SyncedItem is not flag as deleted + try { + $syncedItem = $this->syncedItemRequest->getSyncedItem($appId, $itemType, $itemId); + if ($syncedItem->isDeleted()) { + throw new FederatedSyncConflictException("SyncedItem $appId.$itemType.$itemId is deprecated"); + } + + $this->debugService->info('Found {syncedItem.singleId} in database', '', [ + 'syncedItem' => $syncedItem + ]); + + return $syncedItem; + } catch (SyncedItemNotFoundException $e) { + } + + // verify itemId is local + $syncManager = $this->federatedSyncService->getSyncManager($appId, $itemType); + try { + $syncManager->serializeItem($itemId); + } catch (Exception $e) { + throw new FederatedSyncConflictException( + 'SyncedItem not found in database and does not appears to be local' + ); + } + + // create entry + return $this->createSyncedItem($appId, $itemType, $itemId); + } + + + /** + * create a new (local) SyncedItem based on appId, itemType, itemId. + * + * @param string $appId + * @param string $itemType + * @param string $itemId + * + * @return SyncedItem + */ + private function createSyncedItem(string $appId, string $itemType, string $itemId): SyncedItem { + $syncedItem = new SyncedItem(); + $syncedItem->setSingleId($this->token(31)) + ->setAppId($appId) + ->setItemType($itemType) + ->setItemId($itemId); + + $this->debugService->info( + 'Generating new SyncedItem for {syncedItem.appId}.{syncedItem.itemType}.{syncedItem.itemId}', + '', + ['syncedItem' => $syncedItem] + ); + + return $syncedItem; + } + +} diff --git a/lib/Service/FederatedSyncService.php b/lib/Service/FederatedSyncService.php new file mode 100644 index 000000000..77bdf0c25 --- /dev/null +++ b/lib/Service/FederatedSyncService.php @@ -0,0 +1,138 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Service; + +use OCA\Circles\Db\RemoteRequest; +use OCA\Circles\Db\SyncedItemRequest; +use OCA\Circles\Db\SyncedShareRequest; +use OCA\Circles\Exceptions\FederatedSyncManagerNotFoundException; +use OCA\Circles\IFederatedSyncManager; +use OCA\Circles\Model\Federated\RemoteInstance; +use OCA\Circles\Model\SyncedItem; +use OCA\Circles\Tools\ActivityPub\NCSignature; +use OCA\Circles\Tools\Model\Request; +use OCA\Circles\Tools\Traits\TStringTools; + +class FederatedSyncService extends NCSignature { + use TStringTools; + + private SyncedItemRequest $syncedItemRequest; + private SyncedShareRequest $syncedShareRequest; + private RemoteRequest $remoteRequest; + private FederatedEventService $federatedEventService; + private RemoteStreamService $remoteStreamService; + private InterfaceService $interfaceService; + + /** @var IFederatedSyncManager[] */ + private array $syncManager = []; + + + /** + * @param SyncedItemRequest $syncedItemRequest + * @param SyncedShareRequest $syncedShareRequest + * @param RemoteRequest $remoteRequest + * @param FederatedEventService $federatedEventService + * @param RemoteStreamService $remoteStreamService + * @param InterfaceService $interfaceService + */ + public function __construct( + SyncedItemRequest $syncedItemRequest, + SyncedShareRequest $syncedShareRequest, + RemoteRequest $remoteRequest, + FederatedEventService $federatedEventService, + RemoteStreamService $remoteStreamService, + InterfaceService $interfaceService + ) { + $this->syncedItemRequest = $syncedItemRequest; + $this->syncedShareRequest = $syncedShareRequest; + $this->remoteRequest = $remoteRequest; + $this->federatedEventService = $federatedEventService; + $this->remoteStreamService = $remoteStreamService; + $this->interfaceService = $interfaceService; + } + + + /** + * @param IFederatedSyncManager $federatedSyncManager + */ + public function addFederatedSyncManager(IFederatedSyncManager $federatedSyncManager): void { + $this->syncManager[] = $federatedSyncManager; + } + + + /** + * @param string $appId + * @param string $itemType + * + * @return IFederatedSyncManager + * @throws FederatedSyncManagerNotFoundException + */ + public function getSyncManager(string $appId, string $itemType): IFederatedSyncManager { + foreach ($this->syncManager as $federatedSyncManager) { + if ($federatedSyncManager->getAppId() === $appId + && $federatedSyncManager->getItemType() === $itemType) { + return $federatedSyncManager; + } + } + + throw new FederatedSyncManagerNotFoundException(); + } + + + /** + * @param SyncedItem $syncedItem + * + * @return IFederatedSyncManager + * @throws FederatedSyncManagerNotFoundException + */ + public function initSyncManager(SyncedItem $syncedItem): IFederatedSyncManager { + return $this->getSyncManager($syncedItem->getAppId(), $syncedItem->getItemType()); + } + + + // + public function reachInstance(string $instance) { +// $remoteInstance = $this->remoteRequest->getFromInstance($instance); + $syncedItem = new SyncedItem(); + $syncedItem->setSingleId('toto'); + +// $this->interfaceService->setCurrentInterface($remoteInstance->getInterface()); + $data = $this->remoteStreamService->resultRequestRemoteInstance( + $instance, + RemoteInstance::SYNC_ITEM, + Request::TYPE_GET, + $syncedItem + ); + + echo 'reached !? ' . json_encode($data) . "\n"; + } + +} diff --git a/lib/Service/FederatedSyncShareService.php b/lib/Service/FederatedSyncShareService.php new file mode 100644 index 000000000..3d643c030 --- /dev/null +++ b/lib/Service/FederatedSyncShareService.php @@ -0,0 +1,230 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Service; + +use OCA\Circles\Db\RemoteRequest; +use OCA\Circles\Db\SyncedItemRequest; +use OCA\Circles\Db\SyncedShareRequest; +use OCA\Circles\Exceptions\FederatedEventException; +use OCA\Circles\Exceptions\FederatedItemException; +use OCA\Circles\Exceptions\FederatedSyncManagerNotFoundException; +use OCA\Circles\Exceptions\InitiatorNotConfirmedException; +use OCA\Circles\Exceptions\OwnerNotFoundException; +use OCA\Circles\Exceptions\RemoteInstanceException; +use OCA\Circles\Exceptions\RemoteNotFoundException; +use OCA\Circles\Exceptions\RemoteResourceNotFoundException; +use OCA\Circles\Exceptions\RequestBuilderException; +use OCA\Circles\Exceptions\SyncedItemNotFoundException; +use OCA\Circles\Exceptions\SyncedSharedAlreadyExistException; +use OCA\Circles\Exceptions\SyncedShareNotFoundException; +use OCA\Circles\Exceptions\UnknownRemoteException; +use OCA\Circles\FederatedItems\FederatedSync\ShareCreation; +use OCA\Circles\Model\Circle; +use OCA\Circles\Model\Federated\FederatedEvent; +use OCA\Circles\Model\SyncedItem; +use OCA\Circles\Model\SyncedShare; +use OCA\Circles\Tools\ActivityPub\NCSignature; +use OCA\Circles\Tools\Traits\TStringTools; + +class FederatedSyncShareService extends NCSignature { + use TStringTools; + + private SyncedItemRequest $syncedItemRequest; + private SyncedShareRequest $syncedShareRequest; + private RemoteRequest $remoteRequest; + private FederatedSyncService $federatedSyncService; + private FederatedEventService $federatedEventService; + private RemoteStreamService $remoteStreamService; + private InterfaceService $interfaceService; + private DebugService $debugService; + + + /** + * @param SyncedItemRequest $syncedItemRequest + * @param SyncedShareRequest $syncedShareRequest + * @param RemoteRequest $remoteRequest + * @param FederatedSyncService $federatedSyncService + * @param FederatedEventService $federatedEventService + * @param RemoteStreamService $remoteStreamService + * @param InterfaceService $interfaceService + * @param DebugService $debugService + */ + public function __construct( + SyncedItemRequest $syncedItemRequest, + SyncedShareRequest $syncedShareRequest, + RemoteRequest $remoteRequest, + FederatedSyncService $federatedSyncService, + FederatedEventService $federatedEventService, + RemoteStreamService $remoteStreamService, + InterfaceService $interfaceService, + DebugService $debugService + ) { + $this->syncedItemRequest = $syncedItemRequest; + $this->syncedShareRequest = $syncedShareRequest; + $this->remoteRequest = $remoteRequest; + $this->federatedSyncService = $federatedSyncService; + $this->federatedEventService = $federatedEventService; + $this->remoteStreamService = $remoteStreamService; + $this->interfaceService = $interfaceService; + $this->debugService = $debugService; + } + + /** + * search for existing shares in circles_share based on itemSingleId, circleId. + * + * @param SyncedItem $syncedItem + * @param Circle $circle + * @param array $extraData + * + * @throws SyncedSharedAlreadyExistException + * @throws FederatedSyncManagerNotFoundException + */ + public function createShare(SyncedItem $syncedItem, Circle $circle, array $extraData = []) { + if (!$this->isShareCreatable($syncedItem, $circle, $extraData)) { + $this->debugService->info( + 'share of {syncedItem.singleId} to {circle.id} is set as not creatable by {!syncedItem.appId}', + $circle->getSingleId(), + [ + 'syncedItem' => $syncedItem, + 'circle' => $circle, + 'extraData' => $extraData + ] + ); + } + + try { + $this->syncedItemRequest->getSyncedItemFromSingleId($syncedItem->getSingleId()); + } catch (SyncedItemNotFoundException $e) { + $this->debugService->info( + 'storing SyncedItem {syncedItem.singleId} in database', '', + ['syncedItem' => $syncedItem] + ); + + $this->syncedItemRequest->save($syncedItem); + } + + $syncedShare = new SyncedShare(); + $syncedShare->setSingleId($syncedItem->getSingleId()) + ->setCircleId($circle->getSingleId()); + + $this->syncedShareRequest->save($syncedShare); + $this->debugService->info( + 'storing SyncedShared {syncedShare.singleId} to {syncedShare.circleId} in database', + $circle->getSingleId(), ['syncedShare' => $syncedShare] + ); + + + $this->debugService->info( + 'calling onShareCreation() on IFederatedSyncManager from {syncedItem.appId}.{syncedItem.itemType}', + $syncedShare->getCircleId(), + [ + 'syncedItem' => $syncedItem, + 'syncedShare' => $syncedShare, + 'extraData' => $extraData, + 'initiator' => $circle->getInitiator()->getInheritedBy() + ] + ); + + $this->federatedSyncService->initSyncManager($syncedItem)->onShareCreation( + $syncedItem->getItemId(), + $syncedShare->getCircleId(), + $extraData, + $circle->getInitiator()->getInheritedBy() + ); + + $this->broadcastShareCreation($syncedItem, $circle); + } + + + /** + * @param Circle $circle + * @param SyncedItem $syncedItem + * + * @throws FederatedEventException + * @throws FederatedItemException + * @throws InitiatorNotConfirmedException + * @throws OwnerNotFoundException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws RequestBuilderException + * @throws UnknownRemoteException + */ + private function broadcastShareCreation(SyncedItem $syncedItem, Circle $circle): void { + $event = new FederatedEvent(ShareCreation::class); + $event->setCircle($circle) + ->setSyncedItem($syncedItem); + + $this->debugService->info( + 'Generation of IFederatedEvent {event.getClass}', + $circle->getSingleId(), + [ + 'event' => $event, + 'syncedItem' => $syncedItem, + 'circle' => $circle + ] + ); + + $this->federatedEventService->newEvent($event); + } + + + /** + * verify that: + * + * - SyncedShare is not already known + * - app agree on creating this share + * + * @param SyncedItem $syncedItem + * @param Circle $circle + * @param array $extraData + * + * @return bool + * @throws FederatedSyncManagerNotFoundException + * @throws SyncedSharedAlreadyExistException + */ + private function isShareCreatable(SyncedItem $syncedItem, Circle $circle, array $extraData = []): bool { + try { + $this->syncedShareRequest->getShare($syncedItem->getSingleId(), $circle->getsingleId()); + throw new SyncedSharedAlreadyExistException('share already exists'); + } catch (SyncedShareNotFoundException $e) { + } + + return $this->federatedSyncService->initSyncManager($syncedItem) + ->isShareCreatable( + $syncedItem->getItemId(), + $circle->getSingleId(), + $extraData, + $circle->getInitiator()->getInheritedBy() + ); + } + +} diff --git a/lib/Service/RemoteStreamService.php b/lib/Service/RemoteStreamService.php index 8ff9232dd..07ffb262f 100644 --- a/lib/Service/RemoteStreamService.php +++ b/lib/Service/RemoteStreamService.php @@ -177,6 +177,10 @@ public function getAppSignatory(bool $generate = true, string $confirmKey = ''): ) ); + $app->setSyncItem($this->interfaceService->getCloudPath('circles.Remote.syncItem')); + $app->setSyncShare($this->interfaceService->getCloudPath('circles.Remote.syncShare')); + $app->setDebug($this->interfaceService->getCloudPath('circles.Remote.debugDaemon')); + if ($this->interfaceService->isCurrentInterfaceInternal()) { $app->setAliases(array_values(array_filter($this->interfaceService->getInterfaces(false)))); } diff --git a/lib/Tools/ISignedModel.php b/lib/Tools/ISignedModel.php new file mode 100644 index 000000000..67dcdc0a1 --- /dev/null +++ b/lib/Tools/ISignedModel.php @@ -0,0 +1,58 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Tools; + +interface ISignedModel { + + /** + * @param string $signature + * + * @return $this + */ + public function setSignature(string $signature): self; + + /** + * @return string + */ + public function getSignature(): string; + + + /** + * returns array/data to be signed to identify the model + * + * @return array + */ + public function signedData(): array; + +} + diff --git a/lib/Tools/Model/ReferencedDataStore.php b/lib/Tools/Model/ReferencedDataStore.php new file mode 100644 index 000000000..0dc2ab7a0 --- /dev/null +++ b/lib/Tools/Model/ReferencedDataStore.php @@ -0,0 +1,473 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Tools\Model; + +use JetBrains\PhpStorm\Pure; +use JsonSerializable; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\Exceptions\ItemNotFoundException; +use OCA\Circles\Tools\IDeserializable; +use OCA\Circles\Tools\IReferencedObject; +use OCA\Circles\Tools\Traits\TArrayTools; +use OCA\Circles\Tools\Traits\TDeserialize; +use ReflectionClass; +use ReflectionException; + +class ReferencedDataStore implements IDeserializable, JsonSerializable { + use TArrayTools; + use TDeserialize; + + public const STRING = 'string'; + public const INTEGER = 'integer'; + public const BOOLEAN = 'boolean'; + public const ARRAY = 'array'; + public const OBJECT = 'object'; + + public const KEY_NAME = 'name'; + public const KEY_TYPE = 'type'; + public const KEY_CLASS = 'class'; + + public const _THIS = '__this'; + public const _REFERENCE = '__reference'; + + private array $ref = []; + private array $data = []; + private string $lock = IReferencedObject::class; + + public function __construct(array $data = []) { + $this->data = $data; + } + + + /** + * @param string $key + * @param string $value + * + * @return ReferencedDataStore + */ + public function s(string $key, string $value): self { + $this->data[$key] = $value; + $this->ref($key, self::STRING); + + return $this; + } + + /** + * @param string $key + * + * @return string + * @throws InvalidItemException + */ + public function g(string $key): string { + $this->confirmRef($key, self::STRING); + + return $this->get($key, $this->data); + } + + + /** + * @param string $key + * + * @return ReferencedDataStore + */ + public function u(string $key): self { + if ($this->hasKey($key)) { + unset($this->data[$key]); + } + + return $this; + } + + + /** + * @param string $key + * @param int $value + * + * @return ReferencedDataStore + */ + public function sInt(string $key, int $value): self { + $this->data[$key] = $value; + $this->ref($key, self::INTEGER); + + return $this; + } + + /** + * @param string $key + * + * @return int + * @throws InvalidItemException + */ + public function gInt(string $key): int { + $this->confirmRef($key, self::INTEGER); + + return $this->getInt($key, $this->data); + } + + + /** + * @param string $key + * @param bool $value + * + * @return ReferencedDataStore + */ + public function sBool(string $key, bool $value): self { + $this->data[$key] = $value; + $this->ref($key, self::BOOLEAN); + + return $this; + } + + /** + * @param string $key + * + * @return bool + * @throws InvalidItemException + */ + public function gBool(string $key): bool { + $this->confirmRef($key, self::BOOLEAN); + + return $this->getBool($key, $this->data); + } + + + /** + * @param string $key + * @param array $value + * + * @return ReferencedDataStore + */ + public function sArray(string $key, array $value): self { + $this->data[$key] = $value; + $this->ref($key, self::ARRAY); + + return $this; + } + + /** + * @param string $key + * + * @return array + * @throws InvalidItemException + */ + public function gArray(string $key): array { + $this->confirmRef($key, self::ARRAY); + + return $this->getArray($key, $this->data); + } + + + /** + * @param string $key + * @param JsonSerializable $value + * + * @return ReferencedDataStore + */ + public function sObj(string $key, JsonSerializable $value): self { + $this->data[$key] = $value; + $this->ref($key, self::OBJECT, $value); + + return $this; + } + + + /** + * @param string $key + * @param string $class + * + * @return JsonSerializable[] + */ +// public function gObjs(string $key, string $class = ''): array { +// $list = $this->gArray($key); +// $result = []; +// foreach ($list as $item) { +// $data = new SimpleDataStore([$key => $item]); +// $result[] = $data->gObj($key, $class); +// } +// +// return array_filter($result); +// } + + + /** + * @param string $key + * + * @return null|JsonSerializable + * @throws InvalidItemException + */ + public function gObj(string $key): ?IDeserializable { + $this->confirmRef($key, self::OBJECT); + $class = $this->getRef($key, self::KEY_CLASS); + + $item = $this->data[$key]; + if ($item instanceof IDeserializable) { + return $item; + } + + try { + $reflection = new ReflectionClass($class); + } catch (ReflectionException $e) { + throw new InvalidItemException('reflection issue with ' . $class); + } + + if (!$reflection->implementsInterface(IDeserializable::class)) { + throw new InvalidItemException('object does not implements IDeserializable'); + } + + if ($this->locked() !== '' && !$reflection->implementsInterface($this->locked())) { + throw new InvalidItemException('model is locked'); + } + + return $this->deserialize($item, $class); + } + + + /** + * @param string $key + * + * @return mixed + * @throws ItemNotFoundException + */ +// public function gItem(string $key) { +// if (!array_key_exists($key, $this->data)) { +// throw new ItemNotFoundException(); +// } +// +// return $this->data[$key]; +// } + + + /** + * @param string $k + * @param mixed $obj + * + * @return ReferencedDataStore + * @throws InvalidItemException + */ + public function sMixed(string $k, mixed $obj): self { + if ($obj instanceof JsonSerializable) { + return $this->sObj($k, $obj); + } + if (is_array($obj)) { + return $this->sArray($k, $obj); + } + if (is_integer($obj)) { + return $this->sInt($k, $obj); + } + if (is_string($obj)) { + return $this->s($k, $obj); + } + if (is_bool($obj)) { + return $this->sBool($k, $obj); + } + + throw new InvalidItemException(); + } + + + /** + * @param string $json + * + * @return $this + * @throws InvalidItemException + */ + public function json(string $json): self { + $this->import(json_decode($json, true)); + + return $this; + } + + + public function keys(): array { + return array_keys($this->data); + } + + /** + * @param string $key + * + * @return bool + */ + public function hasKey(string $key): bool { + return (array_key_exists($key, $this->data)); + } + + + /** + * @param array $keys + * + * @return bool + */ + #[Pure] + public function hasKeys(array $keys): bool { + foreach ($keys as $key) { + if (!$this->hasKey($key)) { + return false; + } + } + + return true; + } + + + /** + * @param string $lock + * + * @return $this + */ + public function lock(string $lock): self { + $this->lock = $lock; + + return $this; + } + + /** + * @return $this + */ + public function unlock(): self { + $this->lock = ''; + + return $this; + } + + /** + * @return string + */ + public function locked(): string { + return $this->lock; + } + + + /** + * @return array + */ + public function gAll(): array { + return $this->data; + } + + + /** + * @param string $key + * @param string $type + * @param JsonSerializable|null $object + * + * @return $this + */ + private function ref(string $key, string $type, ?JsonSerializable $object = null): self { + $ref = [self::KEY_TYPE => $type]; + + if ($key !== self::_THIS) { + $ref[self::KEY_NAME] = $key; + } + + if (!is_null($object)) { + $ref[self::KEY_CLASS] = get_class($object); + } + + $this->ref[$key] = $ref; + + return $this; + } + + /** + * @param string $key + * @param string $ref + * + * @return string + */ + private function getRef(string $key, string $ref): string { + return $this->get($key . '.' . $ref, $this->ref); + } + + /** + * @param string $key + * @param string $type + * + * @throws InvalidItemException + */ + private function confirmRef(string $key, string $type): void { + if ($this->getRef($key, self::KEY_TYPE) === $type) { + return; + } + + throw new InvalidItemException(); + } + + public function getType(string $key): string { + return $this->getRef($key, self::KEY_TYPE); + } + + + /** + * @return array + */ + public function getAllReferences(): array { + $ref = $this->ref; + unset($ref[self::_THIS]); + + return $ref; + } + + /** + * @param array $data + * + * @return IDeserializable + * @throws InvalidItemException + */ + public function import(array $data): IDeserializable { + $this->ref = $this->getArray(self::_REFERENCE, $data); + + if ($this->getRef(self::_THIS, self::KEY_TYPE) !== self::OBJECT + || $this->getRef(self::_THIS, self::KEY_CLASS) !== get_class($this)) { + throw new InvalidItemException(); + } + + unset($data[self::_REFERENCE]); + $this->data = $data; + + return $this; + } + + + /** + * @return array + */ + public function jsonSerialize(): array { + $this->ref(self::_THIS, self::OBJECT, $this); + + return array_merge( + [ + '__reference' => $this->ref, + ], + $this->data + ); + } +} diff --git a/lib/Tools/Traits/TNCSignatory.php b/lib/Tools/Traits/TNCSignatory.php index 0d28fedf1..deeed009b 100644 --- a/lib/Tools/Traits/TNCSignatory.php +++ b/lib/Tools/Traits/TNCSignatory.php @@ -35,6 +35,7 @@ use OCA\Circles\Tools\Exceptions\RequestNetworkException; use OCA\Circles\Tools\Exceptions\SignatoryException; use OCA\Circles\Tools\Exceptions\SignatureException; +use OCA\Circles\Tools\ISignedModel; use OCA\Circles\Tools\Model\NCRequest; use OCA\Circles\Tools\Model\NCSignatory; @@ -189,6 +190,19 @@ public function signString(string $clear, NCSignatory $signatory): string { } + /** + * @param ISignedModel $model + * @param NCSignatory $signatory + * + * @throws SignatoryException + */ + public function signModel(ISignedModel $model, NCSignatory $signatory): void { + $string = json_encode($model->signedData()); + $signature = $this->signString($string, $signatory); + $model->setSignature($signature); + } + + /** * @param string $clear * @param string $signed @@ -204,4 +218,20 @@ public function verifyString( throw new SignatureException('signature issue'); } } + + /** + * @param ISignedModel $model + * @param string $publicKey + * @param string $algo + * + * @throws SignatureException + */ + public function verifyModel( + ISignedModel $model, + string $publicKey, + string $algo = NCSignatory::SHA256 + ): void { + $string = json_encode($model->signedData()); + $this->verifyString($string, $model->getSignature(), $publicKey, $algo); + } }