diff --git a/doc/changelogs/6.0/6.0.15.md b/doc/changelogs/6.0/6.0.15.md index 4fc0f203b0..35bc20646e 100644 --- a/doc/changelogs/6.0/6.0.15.md +++ b/doc/changelogs/6.0/6.0.15.md @@ -2102,3 +2102,14 @@ of the repin commit in the range. Each package's own release notes are in the the user out, the audit record of the sign-out asked `currentUser()` who acts, which signed out again, without end. While the sign-out is recorded, `currentUser()` now answers with the user being signed out (`eZUser::$recordingLogout`, reset in a `finally` block and by Velocity between requests). Test: `eZUserDisabledCurrentUserLiveTest` (live). + +## 7 October 2026: the dialogs of the online editor and pasted styles + +### Fixed + +- The dialogs of the online editor (`ezoe/dialog`, `ezoe/relations`) open for whoever may edit the version being + edited, as uploads and custom tags already did, not only for whoever may read the object: the editors of a draft of + an object that was never published, which no one can read yet, could not insert a link, an image or an embedded + object. `Exponential\View\Extension\Ezoe\Ezoe\Dialog::mayOpen()`. Test: `expOEDialogAccessTest`. +- A pasted style declaration without a colon (`style="color; text-align: right"`) raised two PHP warnings; it is left + out now, and a value keeps the colons it holds (an address). Test: `expOEPastedStylesTest`. diff --git a/extension/ezoe/classes/runnable/views/ezoe/dialog.php b/extension/ezoe/classes/runnable/views/ezoe/dialog.php index c0f4b47064..1fc1da33a0 100644 --- a/extension/ezoe/classes/runnable/views/ezoe/dialog.php +++ b/extension/ezoe/classes/runnable/views/ezoe/dialog.php @@ -9,6 +9,30 @@ class Dialog extends \Exponential\Runnable\ModuleView { + /** + * Whether the dialogs of the editor open for $object: for whoever may read the object, and for whoever may edit + * the version being edited. Someone who edits a draft of an object that was never published can not read the + * object yet (it has no location), and an extension may let others edit a version (filter content/edit/access, + * as for uploads and custom tags); both need the dialogs of the editor they work in. + * + * @param \eZContentObject|null $object + * @param int $versionNumber The version being edited + * @return bool + */ + public static function mayOpen( $object, $versionNumber ) + { + if ( !$object instanceof \eZContentObject ) + { + return false; + } + if ( $object->canRead() ) + { + return true; + } + $version = $object->version( (int)$versionNumber ); + return $version instanceof \eZContentObjectVersion && (bool)$object->editAccess( $version ); + } + public function run( array $scope ) { // the including function's variables ($Params, $Module, $cli, ...) @@ -28,7 +52,7 @@ public function run( array $scope ) } $object = \eZContentObject::fetch( $objectID ); - if ( !$object instanceof \eZContentObject || !$object->canRead() ) + if ( !self::mayOpen( $object, $objectVersion ) ) { echo \ezpI18n::tr( 'design/standard/ezoe', 'Invalid parameter: %parameter = %value', null, array( '%parameter' => 'ObjectId', '%value' => $objectID ) ); \eZExecution::cleanExit(); diff --git a/extension/ezoe/classes/runnable/views/ezoe/relations.php b/extension/ezoe/classes/runnable/views/ezoe/relations.php index 28baf26acb..7aaee4b3dc 100644 --- a/extension/ezoe/classes/runnable/views/ezoe/relations.php +++ b/extension/ezoe/classes/runnable/views/ezoe/relations.php @@ -64,7 +64,8 @@ public function run( array $scope ) $imageIni = \eZINI::instance( 'image.ini' ); $params = array('loadImages' => true, 'imagePreGenerateSizes' => array('small', 'original') ); - if ( !$object instanceof \eZContentObject || !$object->canRead() ) + // Read access to the object, or edit access to the version being edited (Dialog::mayOpen()) + if ( !Dialog::mayOpen( $object, $objectVersion ) ) { echo \ezpI18n::tr( 'design/standard/ezoe', 'Invalid parameter: %parameter = %value', null, array( '%parameter' => 'ObjectId', '%value' => $objectID ) ); \eZExecution::cleanExit(); diff --git a/extension/ezoe/ezxmltext/handlers/input/ezoeinputparser.php b/extension/ezoe/ezxmltext/handlers/input/ezoeinputparser.php index d8a077e8dd..c0da65e4f9 100644 --- a/extension/ezoe/ezxmltext/handlers/input/ezoeinputparser.php +++ b/extension/ezoe/ezxmltext/handlers/input/ezoeinputparser.php @@ -1515,12 +1515,14 @@ protected function elementStylesToAttribute( DOMElement $element ) $styleArray = explode( ';', $styleString ); foreach( $styleArray as $style ) { - if ( !$style ) + // A declaration without a colon (left over from pasted content) gives no attribute; the value may + // hold colons itself (an address) + $parts = explode( ':', $style, 2 ); + if ( count( $parts ) < 2 ) continue; - list( $name, $value ) = explode( ':', $style ); - $name = strtolower( trim( $name ) ); - $value = trim( $value ); + $name = strtolower( trim( $parts[0] ) ); + $value = trim( $parts[1] ); if ( $name === 'float' || $name === 'text-align' ) $name = 'align'; diff --git a/share/filelist.md5 b/share/filelist.md5 index 8c45dbe437..a198175d62 100644 --- a/share/filelist.md5 +++ b/share/filelist.md5 @@ -2992,7 +2992,7 @@ b2661906876c559f0a805cd1378d55b7 doc/changelogs/4.4/unstable/CHANGELOG-4.4.0bet d7bfaf80f8066a99e0d74160ffcf8f20 doc/changelogs/6.0/6.0.12.md 09ae95fd34fca4003ad48cb5fb98e168 doc/changelogs/6.0/6.0.13.md 32049118b749decc1e1d0493b6bebcf8 doc/changelogs/6.0/6.0.14.md -a12077dd0db3dcfe4b893277013a4824 doc/changelogs/6.0/6.0.15.md +9d05407567676b2522443d77eeabdba3 doc/changelogs/6.0/6.0.15.md 4f263894a87973f99e99761658ca0d20 doc/changelogs/6.0/6.0.2.md f6a43e9644b79e39fb5c671954aaecd8 doc/changelogs/6.0/6.0.3.md a9409f4e49a880bc3f66229f097d00b6 doc/changelogs/6.0/6.0.4.md @@ -4169,10 +4169,10 @@ a816b6bff7b91f94b91681927dd330eb extension/ezoe/classes/expoeurlexception.php 53ac72fee1421196ea7adbfd9b001b1a extension/ezoe/classes/expoeurlfetcher.php 4c23a9e19cae5d4d9a92c33f630045c9 extension/ezoe/classes/ezoeserverfunctions.php d8765d028ea543e378067d65c0eac1bd extension/ezoe/classes/runnable/views/ezoe/atd_rpc.php -42d8f20d22cb4dc369aa2198035e619c extension/ezoe/classes/runnable/views/ezoe/dialog.php +462c3fd3414f8a067b7a19f08026ebae extension/ezoe/classes/runnable/views/ezoe/dialog.php 0473e9fbd4329be2c8e9ba010b2f1925 extension/ezoe/classes/runnable/views/ezoe/embed_view.php 75dd302bd7c6f2f9e57a7da7a479cb22 extension/ezoe/classes/runnable/views/ezoe/load.php -7e00654ceec83e801f365cdd4095a9bf extension/ezoe/classes/runnable/views/ezoe/relations.php +3376f3eaf3425c054fe35496c3e4d90b extension/ezoe/classes/runnable/views/ezoe/relations.php 3cb7ff865bd748517082efa50d4c8d8b extension/ezoe/classes/runnable/views/ezoe/spellcheck_rpc.php dca390bb9761c002daf96504a1aa2472 extension/ezoe/classes/runnable/views/ezoe/tags.php 4a83fd59cd77bc7207a48deab6c26538 extension/ezoe/classes/runnable/views/ezoe/upload.php @@ -4618,7 +4618,7 @@ e957ad0751e2c0d271450a92c8ab1e64 extension/ezoe/design/standard/templates/ezoe/ 432508f8b8c2b01e6ced699e524f4abe extension/ezoe/doc/oe5_intro.pdf 70322a77a52a65c55b7f33427a6c6eb4 extension/ezoe/doc/path_concept.gif b9fd60ca35c9e3786ffd14bb18c1ed1f extension/ezoe/extension.xml -760b73fe00413afcc4ad32140a4c8d9e extension/ezoe/ezxmltext/handlers/input/ezoeinputparser.php +f801e88a86700884026d37c019eba428 extension/ezoe/ezxmltext/handlers/input/ezoeinputparser.php 0bf0f15f65f71fef5f7ac8851292386b extension/ezoe/ezxmltext/handlers/input/ezoexmlinput.php ec9d0894a9b5155ca5bfa598488a5854 extension/ezoe/modules/ezoe/atd_rpc.php 84be03f87c6a63ead6d791281c09bda5 extension/ezoe/modules/ezoe/classes/EnchantSpell.php @@ -7013,10 +7013,12 @@ b977ef5a409ff4c1d2e4c734c839d6e3 tests/tests/extension/expservices/users/expUse 02bf8c6b6d73f53dcd28b4b50eda7195 tests/tests/extension/expservices/users/expUsersDeclarationsTest.php c2d1d9f4d1d3bfd48c427cac7ccaace7 tests/tests/extension/expservices/users/expUsersTestCase.php d5ef0a34d06298d2da7f951273c12e1d tests/tests/extension/ezjscore/ezjscoreCallThrowableTest.php +5973ccb32a7ebadd3900bee754ed946a tests/tests/extension/ezoe/expOEDialogAccessTest.php f6cf185c8c7b6205b83513395a280767 tests/tests/extension/ezoe/expOEEditorRegistryTest.php ec537531a37d13e0a1a90a8f3a112d4d tests/tests/extension/ezoe/expOEEditorServicesTest.php 1f431d2787ac5ae352d9bc4e7c3e5ef0 tests/tests/extension/ezoe/expOEEngineConfigTest.php 8788b408ac3034b0742e9ca87316bd77 tests/tests/extension/ezoe/expOEEngineViewTest.php +9a7f16efa0e2944384ae92ca524a9ede tests/tests/extension/ezoe/expOEPastedStylesTest.php 12e337e5d6b9e90a74cd6889c0936ef8 tests/tests/extension/ezoe/expOERoundTripTest.php 10becba9add2f221837f6d4eb8cdef5e tests/tests/extension/ezoe/expOETestCase.php 8693268e9c25820e2d7b65e59d997ba8 tests/tests/extension/ezoe/expOEUploadRulesTest.php diff --git a/tests/tests/extension/ezoe/expOEDialogAccessTest.php b/tests/tests/extension/ezoe/expOEDialogAccessTest.php new file mode 100644 index 0000000000..abe5a0f940 --- /dev/null +++ b/tests/tests/extension/ezoe/expOEDialogAccessTest.php @@ -0,0 +1,135 @@ +roles as $role ) + { + $role->removeThis(); + } + foreach ( $this->userIDs as $userID ) + { + $object = eZContentObject::fetch( $userID ); + if ( $object instanceof eZContentObject ) + { + $object->purge(); + } + if ( eZUser::fetch( $userID ) ) + { + eZUser::removeUser( $userID ); + } + eZUser::purgeUserCacheByUserId( $userID ); + } + unset( $GLOBALS['ezpolicylimitation_list'] ); + eZRole::expireCache(); + eZContentObject::clearCache(); + $this->roles = $this->userIDs = array(); + parent::tearDown(); + } + + /** + * Logs in a user that was never published (no groups, so only the policies given apply). + * + * @param array $policies array( array( module, function ), ... ) without limitations + */ + private function logInUserWith( array $policies ) + { + $this->loginAdmin(); + $adminID = (int)eZUser::currentUserID(); + $login = 'x1-ezoe-dialog-' . bin2hex( random_bytes( 4 ) ); + $userObject = eZContentClass::fetchByIdentifier( 'user' )->instantiate( $adminID ); + $userID = (int)$userObject->attribute( 'id' ); + $this->userIDs[] = $userID; + + $user = eZUser::fetch( $userID ) ?: eZUser::create( $userID ); + $user->setAttribute( 'login', $login ); + $user->setAttribute( 'email', $login . '@' . self::ADDRESS_DOMAIN ); + $user->setAttribute( 'password_hash', eZUser::createHash( $login, bin2hex( random_bytes( 12 ) ), eZUser::site(), eZUser::hashType() ) ); + $user->setAttribute( 'password_hash_type', eZUser::hashType() ); + $user->store(); + + $role = eZRole::create( 'X1 ezoe dialog ' . $login ); + $role->store(); + $this->roles[] = $role; + foreach ( $policies as $policy ) + { + $role->appendPolicy( $policy[0], $policy[1] ); + } + $role->assignToUser( $userID ); + eZRole::expireCache(); + eZUser::purgeUserCacheByUserId( $userID ); + unset( $GLOBALS['ezpolicylimitation_list'] ); + eZContentObject::clearCache(); + + eZUser::setCurrentlyLoggedInUser( eZUser::fetch( $userID ), $userID, eZUser::NO_SESSION_REGENERATE ); + $this->assertSame( $userID, (int)eZUser::currentUserID() ); + } + + /** The object of the content root and its current version */ + private function rootObject() + { + $node = eZContentObjectTreeNode::fetch( 2 ); + $this->assertInstanceOf( 'eZContentObjectTreeNode', $node ); + $object = eZContentObject::fetch( (int)$node->attribute( 'contentobject_id' ) ); + return array( $object, (int)$object->attribute( 'current_version' ) ); + } + + public function testWhoMayReadTheObjectOpensTheDialogs() + { + $this->logInUserWith( array( array( 'content', 'read' ) ) ); + list( $object, $version ) = $this->rootObject(); + + $this->assertTrue( $object->canRead() ); + $this->assertTrue( Dialog::mayOpen( $object, $version ) ); + } + + public function testWhoMayEditTheVersionButNotReadTheObjectOpensTheDialogs() + { + $this->logInUserWith( array( array( 'content', 'edit' ) ) ); + list( $object, $version ) = $this->rootObject(); + + $this->assertFalse( (bool)$object->canRead(), 'the user may not read the object' ); + $this->assertTrue( (bool)$object->editAccess( $object->version( $version ) ), 'the user may edit the version' ); + $this->assertTrue( Dialog::mayOpen( $object, $version ) ); + } + + public function testWhoMayNeitherReadNorEditDoesNotOpenThem() + { + $this->logInUserWith( array( array( 'content', 'create' ) ) ); + list( $object, $version ) = $this->rootObject(); + + $this->assertFalse( Dialog::mayOpen( $object, $version ) ); + } + + public function testAVersionThatDoesNotExistOrNoObjectOpensNothing() + { + $this->logInUserWith( array( array( 'content', 'edit' ) ) ); + list( $object, $version ) = $this->rootObject(); + + $this->assertFalse( Dialog::mayOpen( $object, 999999 ) ); + $this->assertFalse( Dialog::mayOpen( null, $version ) ); + } +} diff --git a/tests/tests/extension/ezoe/expOEPastedStylesTest.php b/tests/tests/extension/ezoe/expOEPastedStylesTest.php new file mode 100644 index 0000000000..566608ce43 --- /dev/null +++ b/tests/tests/extension/ezoe/expOEPastedStylesTest.php @@ -0,0 +1,68 @@ +process( $html ); + } + finally + { + restore_error_handler(); + } + $this->assertSame( array(), $warnings, 'no PHP warning for: ' . $html ); + $this->assertNotNull( $doc, 'the parser gave a document for: ' . $html ); + $this->assertTrue( $parser->isValid(), 'the input stays valid: ' . $html ); + $xml = eZXMLTextType::domString( $doc ); + $check = new DOMDocument(); + $this->assertTrue( $check->loadXML( $xml ), 'the stored XML loads again' ); + return preg_replace( '/^<\?xml[^>]*\?>\s*]*>|<\/section>\s*$/', '', $xml ); + } + + public function testADeclarationWithoutColonIsLeftOut() + { + $this->assertSame( 'a', $this->xml( '

a

' ) ); + $this->assertSame( 'a', $this->xml( '

a

' ) ); + } + + public function testAValueKeepsItsColons() + { + $this->assertSame( 'a', + $this->xml( '

a

' ) ); + } + + public function testNamesThatCanNotBeAttributesAreLeftOut() + { + $this->assertSame( 'a', + $this->xml( '

a

' ) ); + } + + public function testCharactersXmlDoesNotAllowNeverReachTheStoredXml() + { + $this->assertSame( 'a b', $this->xml( "

a\x0Bb

" ) ); + } +}