This site has been archived and you can no longer log in or post new messages. For up-to-date community resources please visit

eZ Community » Blogs » Harry Oosterveen » Restore a sub tree from the trash in...


Restore a sub tree from the trash in eZ Publish

Thursday 16 February 2012 7:42:55 pm

  • Currently 5 out of 5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

The trash is great: it just happens that you will accidentally delete an item, and you can simply recover and place it in the original location. This works well if you deleted a single item, but If the item you deleted has many sub items, this becomes cumbersome. A long time ago  this issue had been raised, but so far no solution has been created.

As it recently happened that I had to restore a large number of items from a sub tree from the trash, I created this script. Given a node ID, it restores the node itself and all items from the trash that are sub items (to any depth) from this node.

The code to restore a single item from Trash is in the file kernel/content/restore.php, so this script is based on that code.

Collecting all items

You have to restore the items in the right order, top down, as you can not restore an item if its parent has not been restored yet. One way to collect all items is to start with the given node, restore it, and find all children that are in the trash. Repeat this recursively for each child: restore, and find its children.
I have taken a slightly more simple approach, find all items in the trash that are in the sub tree, based on the path_string; sorting either by path_string or depth will ensure that a parent node is always restored before each of it child nodes.

This way, it may be possible that you try to restore a node, whose parent node does not exist anymore, not even in the trash; so this item cannot be restored. A simple check will allow us to skip those items.

Changing node ID’s

After recovering an item, it has been restored at the original location (i.e. with the same parent node), but with a different node ID. As we may have to restore also sub items from this item, it has to have the original node ID again. The function changeNodeID takes care of this.

Also some other attributes of the original node have to be restored to the new node ('is_hidden', 'is_invisible', 'priority', 'sort_field', 'sort_order').

Top node not in trash?

It may happen that the top node is not in the trash. For example, if an object has additional locations, after removing one node, the object will not be removed but simply has one location less. The children of that node are all removed, and can be in the trash--but can not be restored directly to their original location as the top node does not exist anymore.

To fix this situation, manually create a new node, and give the node ID as an extra parameter: replace-id. The node ID of the  new node will be changed to the original top node ID, and recovery of the sub items can continue.


php bin/php/ezrestoresubtree.php -s <siteaccess> --node-id=<nodeID>


 php bin/php/ezrestoresubtree.php -s <siteaccess> --node-id=<nodeID> --replace-id=<replaceID>


I suggest to save the script as bin/php/ezrestoresubtree.php.

#!/usr/bin/env php
// SOFTWARE LICENSE: GNU General Public License v2.0
// NOTICE: >
//   This program is free software; you can redistribute it and/or
//   modify it under the terms of version 2.0  of the GNU General
//   Public License as published by the Free Software Foundation.
//   This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//   GNU General Public License for more details.
//   You should have received a copy of version 2.0 of the GNU General
//   Public License along with this program; if not, write to the Free
//   Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
//   MA 02110-1301, USA.
// Subtree ReStore Script
// file  bin/php/ezrestoresubtree.php
// This script restores all items under a given subtree node ID in the trash to their original location
// If original top node does not exist anymore (e.g., deleted as additional location), 
//   create a new node manually and provide the nodeID as replace-id
//  Code based on kernel/content/restore.php
// script initializing
require 'autoload.php';
$cli = eZCLI::instance();
$script = eZScript::instance( array( 'description' => ( "\n" .
                                                         "This script will restore content object subtrees from trash.\n" ),
                                      'use-session' => false,
                                      'use-modules' => true,
                                      'use-extensions' => true ) );
$scriptOptions = $script->getOptions( "[node-id:][replace-id:]",
                                      array( 'node-id' => "Subtree node ID",
                                        'replace-id' => "Replaced node ID, this existing node ID will be changed to node-id"
                                      false );
$srcNodeID  = $scriptOptions[ 'node-id' ] ? trim( $scriptOptions[ 'node-id' ] ) : false;
$replNodeID  = $scriptOptions[ 'replace-id' ] ? trim( $scriptOptions[ 'replace-id' ] ) : false;
$db = eZDB::instance();
if ( !$srcNodeID )
    $cli->error( "Subtree restore Error!\nCannot get subtree node. Please check node-id argument and try again." );
    $script->shutdown( 1 );
// Check if top node is in trash
$query = sprintf( 'SELECT path_string FROM ezcontentobject_trash WHERE node_id= "%d"', $nodeID );
$rows = $db->arrayQuery( $query );
if( count( $rows ) > 0 ) 
    $pathString = $rows[0]['path_string'];
    $cli->output( 'Restoring top node from trash' );
    // Must be an existing node, or replace an existing node
    if ( $replNodeID )
        changeNodeID( $replNodeID, $srcNodeID );
    $topNode = eZContentObjectTreeNode::fetch( $srcNodeID );
    if( is_object( $topNode ))
        $pathString = $topNode->PathString;
        $cli->error( 'Top node could not be found or restored' );
        $script->shutdown( 1 );
// Get items to restore
// Ordered by depth, so parent nodes will be restored before their children
$query = sprintf( 'SELECT * FROM ezcontentobject_trash WHERE path_string LIKE "%s%%" ORDER BY depth', $pathString );
$trashList = $db->arrayQuery( $query );
$cli->output( sprintf( "Found %d nodes to restore", count( $trashList )));
$restoreAttributes = array( 'is_hidden', 'is_invisible', 'priority', 'sort_field', 'sort_order' );
$checkedParents = array();
foreach( $trashList as $trashItem ) 
    $objectID     = $trashItem['contentobject_id'];
    $parentNodeID = $trashItem['parent_node_id'];
    $orgNodeID    = $trashItem['node_id'];
    // Check if object exists
    $object = eZContentObject::fetch( $objectID );
    if ( !is_object( $object ) ) 
        $cli->error( sprintf( 'Object %d does not exist', $objectID ));
    $cli->output( sprintf( 'Restoring object %d, "%s"', $objectID, $object->Name ));
    // Check whether object is archived indeed
    if ( $object->attribute( 'status' ) != eZContentObject::STATUS_ARCHIVED )
        $cli->error( sprintf( 'Object %d is not archived', $objectID ));
    // Check if parent node exists
    if( !array_key_exists( $parentNodeID, $checkedParents ))
        $parentNode = eZContentObjectTreeNode::fetch( $parentNodeID );
        $checkedParents[$parentNodeID] = is_object( $parentNode );
    if( !$checkedParents[$parentNodeID] ) 
        $cli->error( sprintf( 'Parent node for object %d does not exist', $objectID ));
    $version = $object->attribute( 'current' );
    $location = eZNodeAssignment::fetch( $object->ID, $version->Version, $parentNodeID );
    $opCode = $location->attribute( 'op_code' );
    $opCode &= ~1;
    // We only include assignments which create or nops.
    if ( !$opCode == eZNodeAssignment::OP_CODE_CREATE_NOP && !$opCode == eZNodeAssignment::OP_CODE_NOP ) {
        $cli->error( sprintf( 'Object %d can not be restored', $object->ID ));
    $selectedNodeID = $location->attribute( 'parent_node' );
    // Remove all existing assignments, only our new ones should be present.
    foreach ( $version->attribute( 'node_assignments' ) as $assignment )
    $version->assignToNode( $parentNodeID, true );
    $object->setAttribute( 'status', eZContentObject::STATUS_DRAFT );
    $version->setAttribute( 'status', eZContentObjectVersion::STATUS_DRAFT );
    $user = eZUser::fetch( $version->CreatorID );
    $operationResult = eZOperationHandler::execute( 'content', 'publish', array( 'object_id' => $objectID,
                                                                                 'version' => $version->attribute( 'version' ) ) );
    $objectID = $object->attribute( 'id' );
    $object = eZContentObject::fetch( $objectID );
    $mainNodeID = $object->attribute( 'main_node_id' );
    // Restore original node number
    changeNodeID( $mainNodeID, $orgNodeID );
    // Restore other attributes
    $node = eZContentObjectTreeNode::fetch( $orgNodeID );
    foreach( $restoreAttributes as $attr )
        $node->setAttribute( $attr, $trashItem[ $attr ] );
    eZContentObjectTrashNode::purgeForObject( $objectID  );
    if ( $object->attribute( 'contentclass_id' ) == $userClassID )
        eZUser::purgeUserCacheByUserId( $object->attribute( 'id' ) );
    eZContentObject::fixReverseRelations( $objectID, 'restore' );
    $cli->output( sprintf( 'Restored at node %d', $orgNodeID ));
$cli->output( "Done." );
function changeNodeID( $fromID, $toID )
    global $db;
    // Restore original node ID
    $query = sprintf( 'UPDATE `ezcontentobject_tree` 
                        SET `node_id`=%d, 
                            `path_string`= REPLACE( `path_string`, "/%d/", "/%d/" )
                        WHERE `node_id`=%d',
                    $toID, $fromID, $toID, $fromID );
    $db->query( $query );
    // Update main node IDs
    $query = sprintf( 'UPDATE `ezcontentobject_tree` SET `main_node_id`=%d WHERE `main_node_id`=%d',
                    $toID, $fromID );
    $db->query( $query );
Proudly Developed with from