Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/changelogs/6.0/6.0.15.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
26 changes: 25 additions & 1 deletion extension/ezoe/classes/runnable/views/ezoe/dialog.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...)
Expand All @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion extension/ezoe/classes/runnable/views/ezoe/relations.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 6 additions & 4 deletions extension/ezoe/ezxmltext/handlers/input/ezoeinputparser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 6 additions & 4 deletions share/filelist.md5
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
135 changes: 135 additions & 0 deletions tests/tests/extension/ezoe/expOEDialogAccessTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php
/**
* The dialogs of the editor (ezoe/dialog, ezoe/relations) open for whoever may read the object and for whoever may
* edit the version being edited: Exponential\View\Extension\Ezoe\Ezoe\Dialog::mayOpen(). Someone who edits a draft of
* an object that was never published can not read the object yet, and an extension may let others edit a version
* (filter content/edit/access, as for uploads and custom tags).
*
* Each test creates its own user (never published, address at x1.example.invalid) with a role of its own and removes
* both again; the object is the one of the content root, which is not changed.
*
* @copyright Copyright (C) 1998 - 2026 7x & Exponential Foundation. All rights reserved.
* @license GNU General Public License v2.0 (or any later version)
* @package tests
*/

require_once __DIR__ . '/expOETestCase.php';

use Exponential\View\Extension\Ezoe\Ezoe\Dialog;

class expOEDialogAccessTest extends expOETestCase
{
const ADDRESS_DOMAIN = 'x1.example.invalid';

private $roles = array();
private $userIDs = array();

public function tearDown(): void
{
foreach ( $this->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 ) );
}
}
68 changes: 68 additions & 0 deletions tests/tests/extension/ezoe/expOEPastedStylesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
/**
* Styles pasted into the editor (from a word processor, from another site) are turned into attributes where they can
* be, and left out where they can not, without a PHP warning and without making the input invalid:
* eZOEInputParser::elementStylesToAttribute().
*
* @copyright Copyright (C) 1998 - 2026 7x & Exponential Foundation. All rights reserved.
* @license GNU General Public License v2.0 (or any later version)
* @package tests
*/

require_once __DIR__ . '/expOETestCase.php';

class expOEPastedStylesTest extends expOETestCase
{
/**
* Parses $html and returns the stored XML without its declaration and section element; every PHP warning or
* notice raised meanwhile fails the test.
*/
protected function xml( $html )
{
$warnings = array();
set_error_handler( function ( $number, $message ) use ( &$warnings )
{
$warnings[] = $message;
return true;
} );
try
{
$parser = new eZOEInputParser();
$doc = $parser->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[^>]*>|<\/section>\s*$/', '', $xml );
}

public function testADeclarationWithoutColonIsLeftOut()
{
$this->assertSame( '<paragraph align="right">a</paragraph>', $this->xml( '<p style="color; text-align: right">a</p>' ) );
$this->assertSame( '<paragraph align="center">a</paragraph>', $this->xml( '<p style="text-align: center; mso-hide">a</p>' ) );
}

public function testAValueKeepsItsColons()
{
$this->assertSame( '<paragraph align="left">a</paragraph>',
$this->xml( '<p style="background: url(http://example.invalid/a.png); text-align: left">a</p>' ) );
}

public function testNamesThatCanNotBeAttributesAreLeftOut()
{
$this->assertSame( '<paragraph align="left">a</paragraph>',
$this->xml( '<p style="@font-face: x; -webkit-text-size-adjust: 100%; text-align: left">a</p>' ) );
}

public function testCharactersXmlDoesNotAllowNeverReachTheStoredXml()
{
$this->assertSame( '<paragraph align="center">a b</paragraph>', $this->xml( "<p style=\"text-align: center\x0B\">a\x0Bb</p>" ) );
}
}
Loading