Lab: extending, testing, experimenting, hacking

Class-attribute-based button sets in new MCE Online Editor

Tuesday, 24 June 2008, 02:58
Categories: Lab
Tags: eZ MCE Online Editor, class attribute, eZOE, XML editor, hack

One of my primary postulates towards the new MCE Online Editor is that it supports different button sets, depending on explicit call or preset settings. Actually, presets would stand a much better approach, so let's forget about template declaration for a while. I decided to find out how much work it would require to make the preset approach run.

Ini structure modification

First of all, we have to store the settings we will be using later on. Current button declaration in ezoe.ini looks as follows:

[EditorSettings]
Buttons[]
Buttons[]=formatselect
Buttons[]=bold
Buttons[]=italic
Buttons[]=underline
...

We would need multi-dimentional array with multiple preset-like named blocks instead:

[EditorSettings]
ButtonPresets[]
ButtonPresets[]=full
ButtonPresets[]=mini
#...
[ButtonPreset-full]
Buttons[]
Buttons[]=formatselect
Buttons[]=bold
Buttons[]=italic
Buttons[]=underline
Buttons[]=|
Buttons[]=bullist
Buttons[]=numlist
Buttons[]=indent
Buttons[]=outdent
Buttons[]=|
Buttons[]=undo
Buttons[]=redo
#...
[ButtonPreset-mini]
Buttons[]
Buttons[]=formatselect
Buttons[]=bold
Buttons[]=italic
Buttons[]=underline
Buttons[]=|
Buttons[]=bullist
Buttons[]=numlist
Buttons[]=indent
Buttons[]=outdent

XMLBlock datatype modification

Once we're done with our preset configuration, we have to make the XML Block datatype actually display and store the preset values. First of all, we need to modify the datatype itself (kernel/classes/datatypes/ezxmltext/ezxmltexttype.php):

// Class constants declaration
const BUTTONS_FIELD = 'data_text2';
const BUTTONS_VARIABLE = '_ezxmltext_buttons_';
// Fetch and store post data - method modification
function fetchClassAttributeHTTPInput( $http, $base, $classAttribute )
{
$column = $base . self::COLS_VARIABLE . $classAttribute->attribute( 'id' );
$buttons = $base . self::BUTTONS_VARIABLE . $classAttribute->attribute( 'id' );
if ( $http->hasPostVariable( $column ) )
{
$columnValue = $http->postVariable( $column );
$classAttribute->setAttribute( self::COLS_FIELD, $columnValue );
$buttonsValue = $http->postVariable( $buttons );
$classAttribute->setAttribute( self::BUTTONS_FIELD, $buttonsValue );
return true;
}
return false;
}

The example above is not complete, there are other methods to modify, like initialization or (un)serialization ones. This should be enough to run the test, though.

We still have to modify the datatype templates. Again, we'll do the minimum: modify the datatype's class attribute template (design/standard/templates/class/datatype/edit/ezxmltext.tpl) by adding the following code:

<div class="block">
<label>{'Button preset'|i18n( 'design/standard/class/datatype' )}:</label>
<select name="ContentClass_ezxmltext_buttons_{$class_attribute.id}">
{def $preset_list=ezini( 'EditorSettings', 'ButtonPresets', 'ezoe.ini' )}
{foreach $preset_list as $preset}
<option value="{$preset|wash()}"{if eq( $preset, $class_attribute.data_text2 )} selected="selected"{/if}>{$preset|wash()}</option>
{/foreach}
</select>
</div>

Now, all pieces are in their place for the final cuts.

MCE Online Editor modification

First of all, we have to modify the method responsible for collecting the button settings from the configuration files. We locate the eZOEXMLInput handler class and extend the proper method:

function getEditorButtonList()
{
if ( $this->editorButtonList === null )
{
$contentClassAttributeID = $this->ContentObjectAttribute->ContentClassAttributeID;
$contentClassAttribute = eZContentClassAttribute::fetch( $contentClassAttributeID );
$buttonPreset = $contentClassAttribute->DataText2;

$oeini = eZINI::instance( 'ezoe.ini' );
$buttonPresets = $oeini->variable( 'EditorSettings', 'ButtonPresets' );

if( !in_array( $buttonPreset, $buttonPresets ) )
{
$buttonPreset = $buttonPresets[0];
}
$buttonList = $oeini->variable( 'ButtonPreset-' . $buttonPreset , 'Buttons' );

$contentini = eZINI::instance( 'content.ini' );
$tags = $contentini->variable('CustomTagSettings', 'AvailableCustomTags' );
$hideButtons = array();
$showButtons = array();

// filter out underline if custom underline tag is not enabled
if ( !in_array('underline', $tags ) )
$hideButtons[] = 'underline';

// filter out pagebreak if custom pagebreak tag is not enabled
if ( !in_array('pagebreak', $tags ) )
$hideButtons[] = 'pagebreak';

// filter out relations buttons if user dosn't have access to relations
if ( !eZOEXMLInput::currentUserHasAccess( 'relations' ) )
{
$hideButtons[] = 'image';
$hideButtons[] = 'objects';
}
foreach( $buttonList as $button )
{
if ( !in_array( $button, $hideButtons ) )
$showButtons[] = $button;
}
$this->editorButtonList = $showButtons;
}
return $this->editorButtonList;
}

All this modification does is choose a proper button preset instead of a general one. It could be more warning-secured, this is the minimum.

This seems to be all, but it is not. The main MCE init template uses run-once operator to make sure that OE init is only run once. This is going to be a problem since OE button configuration is part of the init. We have to remove the run-once operator and allow multiple initializations. Now, I'm not sure at the moment if this is JavaScript-safe, but seems to work fine at first glance. Edit ezxmltext_ezoe.tpl template file and comment out run-once operators:

{*run-once*}
...
{*/run-once*}

This should be it.

Summary

As this example shows, a number of kernel-located files have to be modified in order to achieve this functionality. This takes just a couple of minutes once you know what you're doing, but kernel modification won't likely be accepted for premium support, for example. This is why I really hope this modification makes it to the eZ Publish 4.1.0 release, with some 4.0.x backward compatibility mode.

Also, note that this is hardly a substitute for server-side validation of what user is allowed to do within the OE (which ideally should automatically control what a user can do and I hope for that in eZ 4.2+), but stands a great transitional presentation-layer functionality that can be backed up with proper access control. Many projects will suffer if this is not in place...

Comments (2)

Persistent variables - checked out

Wednesday, 21 May 2008, 18:50
Categories: Lab
Tags: node, cache, cache-block, persistent variables, persistent var, var, full view, objects, serialize, hash, array

Well, I gave up my variable caching ideas, even if just for a while. I wanted to master the standard caching techniques (there had always been an excuse, some extension to write...). And I am very glad now because it starts to feel like having real control over my eZ implementations ;)

Among things that I've discovered were persistent variables, recommended by Gaetano in response to my var cache struggling, and completely unknown to me before. Even if not a complete substitute, still a very nice solution, one of those that you ask yourself how you could have lived without...

So what's so great about persistent variables without going into much detail?

Control

They don't require any additional control. Persistent variables are compiled as part of viewcache, which means that they only expire when the viewcache expires, and they naturally follow the smart viewcache clear rules as well. What more control needed than that?

Availability

They seem to be available at all times, if not cached before, calculated upon request for the full view that stores them.

This also means that expiry times of cache-blocks that depend on that data do not have to be synchronized in any way (not that it is even possible...). Gaetano mentioned the relationship between viewcache and cache-block clear being an important issue, but I haven't been able to spot any unwanted or problematic link, yet.

Cost

Persistent variables can indeed store a variety of data useful for further generating parts of the pagelayout. Even if the singular cost of viewcache generation goes slightly up, this is usually benefitial: once generated, the variables are simply there as long as needed. And in most cases all I need is some $node-related operations.

Even if some more expensive data is to be fetched, once you're through the effort of composing you persistent hash, it's there!

Some minor disadvantages.

Since the variables are physically serialized, it is impossible to pass complex objects, such as $node directly. You have to precisely choose which simple data you want (numbers, strings, arrays, hashes...). Luckily, arrays do just fine in most cases, so that's probably as much as one may need before going straight to a fetch outside of a full view.

Then, some includes seem to be able to damage a persistent var that had been generated few lines before, so you have to carefully test if the persistent var set is available in all the full views required. Haven't found an exact rule, yet.

If you're interested if a very nice use example, look here in the comments.

Comments (0)

Variable cache layer... Var-block - wouldn't that be something?

Tuesday, 22 April 2008, 19:49
Categories: Lab
Tags: variable cache, var cache, cache control, cache layer, viewcache, cache-block, var-block

It took me fairly long time to figure out what particular tool available from PHP in my custom software and website implementations was missing from eZ Publish... but I finally got it. It's the ability to dynamically, flexibly cache variables, understood as operation or logic results, stored in a reusable form. Naturally, there is no significant need of caching simple variables defined directly within the pagelayout, just like these:

{def $my_var=345}
{def $my_other_var=hash( 'a', '4023' )}

However, it gets worse not being able to cheaply store an array of ten values, whose fetching/generating cost was over fifty or one hundred SQL queries, several files accessed in the file system, etc.

Problem

The problem seems quite straightforward - out of many caching techniques and layers in eZ Publish, only two are universally useful: the viewcache and the cache-blocks. Unfortunately, both of them store presentation layer results rather than data, and both are quite independent. As a result:

  1. Whenever you crave for variables that will be used by several cache-blocks in the pagelayout, they must be placed outside of cache-blocks themselves. Don't get fooled by the top cache-block apparently holding the variables used further on - it's a coincidence. This may only work if all expiry times are equal for all the blocks and no subtree expiry is ever used (or you've used bugged eZ 4.0.0 for half-a-year, where subtree expiry is simply broken and it's easy to take it as the default behavior), provided that the blocks never got desynchronized. The point is: variables must be kept outside cache-blocks and they will not be cached.
  2. The module result gets generated before the pagelayout, so there's little reusability between their vars.
So what's missing?

My idea is a cache layer halfway between logic/data and the presentation layer. It could be a variable-dedicated cache-block equivalent (maybe a var-block?).

Example: Imagine a website that for each of its node views should be able to access both current node data (data map) as well as root node data in order to make some decisions, calculations, etc. Further, the data could be required by at least three of its cache blocks (with different expiry times, expiry rules and "uncomfortable" locations within the pagelayout). Today that sort of combination requires a substantial...

The var-block as I imagine it would have expiry settings similar to cache-blocks: subtree expiry, expiry ignore, expiry time and a flexible key management. In order to prevent frequent file system access, var-block could serialize variable collections rather than just singles. An additional "collection name" parameter could help organize the blocks within the pagelayout.

Please let me know what you think.

Here's the prototype:
http://ez.no/developer/contribs/template_plugins/self_var_cache

Comments (2)

"Require flag" on the object attribute level

Sunday, 10 February 2008, 20:02
Categories: Lab
Tags: datatype, require flag, content object attribute, attribute, content class, validation, content model

Lately we've been having this discussion with André about different suggestions regarding ways to improve eZ content model:
http://ez.no/developer/forum/suggestions/in...

It was started by my own suggestion to introduce an additional flag that would sort of require objects attributes to be present (or, in other words, impossible to remove from the presentation/XHTML layer). And that actually resulted from my early eZ days questions on how does eZ Publish actually deal with missing attributes...

Back to the additional "require attribute flag"
  1. "Required" flag, that we have today, suggests by semantics that the attribute is required, however, that is not so. I hope I was the only user not to go in depth with this issue, however I expect otherwise. Today's "required" should in my opinion be called "Value required" or "Require value", because that's what the checked flag actually causes.
  2. Why isn't the default behavior of attribute validation such that all attributes are required to be present?
  3. Doesn't lack of all/any of the solution ideas mentioned above (or other) make the editing process vulnerable to any manipulation of the presentation layer? Isn't that comparatively easy to simply omit all uncomfortable attributes, for example datatype-based CAPTCHA, limits, identifiers, etc.?
  4. "Require all required" as a class attribute/flag actually doesn't make much sense with current "Required" meaning. What would it mean? Require all required values? This is why I suggest that there should be a separation of "required flags". We could leave today's required as "Require value" flag, as I suggested above. Then, there should be an additional flag that would decide whether the attribute itself is required to be present in the editing process. This would be much more flexible than earlier "require all required" and at the same time seems to make that idea useless.
Discovery

Well, I finally found some time to look at it again, especially into the eZ Publish kernel. I followed the path that an attribute takes from the edit view all the way to the datatype itself. Actually, I found out that no matter what you do, no matter how you manipulate the presentation layer, the attributes of a content class always reach input validation function in their datatype. So it is up to the given datatype to take further steps, but it's already a good news: it is possible to force attributes without any kernel/lib modifications! Here's an example (ezstring datatype):

function validateObjectAttributeHTTPInput( $http, $base, $contentObjectAttribute )
{
if ( $http->hasPostVariable( $base . '_ezstring_data_text_' . $contentObjectAttribute->attribute( 'id' ) ) )
{
// THE REST OF THE CODE
}
$contentObjectAttribute->setValidationError( ezi18n( 'kernel/classes/datatypes', 'Attribute missing in the presentation layer!' ) );
return eZInputValidator::STATE_INVALID;
}

A quick update on my four points above: I still believe the new flag would be a useful solution. I still believe there's space for both "required flags". I'm still not sure why checking an attributes presence is not default behavior in most (or all) out-of-the-box datatypes. But at least now I have it under control! ;)

And here's how the new flag could work:

function validateObjectAttributeHTTPInput( $http, $base, $contentObjectAttribute )
{
if ( $http->hasPostVariable( $base . '_ezstring_data_text_' . $contentObjectAttribute->attribute( 'id' ) ) )
{
// THE REST OF THE CODE
// Including use of: $contentObjectAttribute->validateIsRequired()...
}
elseif( $contentObjectAttribute->attributeIsRequired() )
{
$contentObjectAttribute->setValidationError( ezi18n( 'kernel/classes/datatypes', 'Attribute missing in the presentation layer!' ) );
return eZInputValidator::STATE_INVALID;
}
}

What do you think?

Comments (0)

Custom top level nodes - is that possible? Round I - exploration...

Saturday, 29 December 2007, 23:39
Categories: Lab
Tags: top level nodes, node, navigation

Are custom top level nodes possible? It is especially thrilling when one has encountered top level node problems ;) I forgot about this issue for quite some time, but now it's back. As I'm typing this post, I know that it won't provide an ultimate answer tonight, and some quick NO answers are within reach. Still, I want to give it a try. Round I - no particular goals, just exploration.

I run a fresh installation of eZ Publish 4.0.0 with just English language, URL addressing. When ready, I manually clean all the cache, just in case.

So what top level nodes do we have? Out of five available, there are three that seem to have much in common (all deal with content in a similar fashion), and just one of them - the media tree root - has a two-digit node ID (43) that will be easier to look for, than content's '2' ;)

Let's look for some clues in the application. I start with /settings directory as I know that it is where the top level nodes are actually declared. But just in case, I search through all the *.ini files. Only two files contain relevant 43 string: menu.ini in this context is only used for navigation part and tab configuration, no key information there, so let's skip it for now; content.ini contains the very declaration:

[NodeSettings]
# The node ID of the normal content tree
RootNode=2
# The node ID of the user tree
UserRootNode=5
# The node ID for the media tree
MediaRootNode=43
# The node ID for the setup tree
SetupRootNode=48
# The node ID for the design tree
DesignRootNode=58

The above code doesn't seem to suggest that extending top level was ever planned, but let's move on.

I log into the administration interface and create a folder called 'Custom', just under eZ Publish content tree root node. I memorize the new IDs: 59 as node ID and 57 as content object ID (the IDs you get may be different). I will need those as soon as I figure out what to destroy in the database ;) Basically, what I will try to do is to move this #59 node folder to the top level.

Let's look for some clues in the database now. I run a database search for any 43-like values in the entire table set (assuming that 43 will be the only way of referencing parts of the database that have something in common with the media root node). I get around 15 table hits, 6 of which seem to be relevant. Now, by analysing the tables, I come up with the following decisions:

  1. ezuser_role - role configuration, 43 was there because of default editor role's limitations, I can skip this one
  2. ezurlalias_ml - URL aliases (multilanguage?), I update my 59 node just in case, md5 has to be calculated for lowercase 'custom' (probably a more complex rule behind this, but lowercase will do in this case):
    UPDATE ezurlalias_ml SET text='Custom', text_md5='8b9035807842a4e4dbe009f3f1478127' WHERE action='eznode:59'
  3. ezurlalias - URL aliases, I add a missing record, same md5 hash:
    INSERT INTO ezurlalias VALUES ('content/view/full/59', 0, NULL, 1, 1, 0, '8b9035807842a4e4dbe009f3f1478127', 'custom')
  4. ezsearch_object_word_link - search engine, can be reindexed, so let's skip this one...
  5. eznode_assignment - I have absolutely no idea what this table's for, so I just take a guess and change depth:
    UPDATE eznode_assignment SET parent_node=1 WHERE id=36
  6. ezcontentobject_tree - actual tree, I modify path string, parent node, identification string and depth - all for my folder:
    UPDATE ezcontentobject_tree SET path_string='/1/59/', parent_node_id=1, path_identification_string='custom', depth=1 WHERE path_string='/1/2/59/'

Just before I touch my eZ Publish interfaces, I clear all the cache, just in case again...

And here we go. From this moment on I have a sixth top level folder node. I don't have a tab and navigation part for it, yet, but I can access it by system URL: /content/view/full/59... Not only that... it seems to properly hold newly created child nodes. I can navigate through this new, hidden structure without visible problems, but...

...something must be missing, it's obvious when you search the kernel and libraries for 'MediaRootNode', 'RootNode', or 'UserRootNode', and see this:

// /kernel/classes/ezcontentbrowse.php
static function nodeAliasID( $nodeName )
{
if ( is_numeric( $nodeName ) )
return $nodeName;
$browseINI = eZINI::instance( 'browse.ini' );
$aliasList = $browseINI->variable( 'BrowseSettings', 'AliasList' );
if ( isset( $aliasList[$nodeName] ) )
return $aliasList[$nodeName];
$contentINI = eZINI::instance( 'content.ini' );
if ( $nodeName == 'content' )
return $contentINI->variable( 'NodeSettings', 'RootNode' );
else if ( $nodeName == 'users' )
return $contentINI->variable( 'NodeSettings', 'UserRootNode' );
else if ( $nodeName == 'media' )
return $contentINI->variable( 'NodeSettings', 'MediaRootNode' );
else if ( $nodeName == 'setup' )
return $contentINI->variable( 'NodeSettings', 'SetupRootNode' );
else
return false;
}

But that's for another round...

Comments (2)