Index: kernel/classes/ezcontentcachemanager.php
===================================================================
--- kernel/classes/ezcontentcachemanager.php	(revision 21525)
+++ kernel/classes/ezcontentcachemanager.php	(working copy)
@@ -397,6 +397,29 @@
         return $info;
     }
 
+    /*
+     Can be used to debug the \a $handledObjectList parameter of nodeListForObject()
+    */
+    static function writeDebugBits( $handledObjectList, $highestBit )
+    {
+        $bitPadLength = (int)( pow( $highestBit, 0.5 ) + 1 );
+        //$bitPadLength = strlen( decbin( $highestBit ) );
+
+        $objectIDList = array_keys( $handledObjectList );
+        $maxObjectID = max( $objectIDList );
+        $padLength = strlen( $maxObjectID ) + 2;
+
+        $msg = '';
+        foreach ( $handledObjectList as $objectID => $clearCacheType )
+        {
+            $bitString = decbin( $clearCacheType );
+            $msg .= str_pad( $objectID, $padLength, ' ', STR_PAD_RIGHT ) . str_pad( $bitString, $bitPadLength, '0', STR_PAD_LEFT );
+            $msg .= "\r\n";
+        }
+
+        eZDebug::writeDebug( $msg, 'eZContentCacheManager::writeDebugBits()' );
+    }
+
     /*!
      \static
      Use \a $clearCacheType to include different kind of nodes( parent, relating, etc ).
@@ -414,11 +437,27 @@
                             - self::CLEAR_ALL_CACHE - Enables all of the above
                             - self::CLEAR_NO_CACHE - Do not clear cache for current object.
      \param[out] $nodeList An array with node IDs that are affected by the current object change.
+     \param[out] $handledObjectList An associative array with object IDs and the cache types that were handled for these objects already.
+                                    Used to avoid infinite recursion.
 
      \note This function is recursive.
     */
-    static function nodeListForObject( $contentObject, $versionNum, $clearCacheType, &$nodeList )
+    static function nodeListForObject( $contentObject, $versionNum, $clearCacheType, &$nodeList, &$handledObjectList )
     {
+        $contentObjectID = $contentObject->attribute( 'id' );
+
+        if ( isset( $handledObjectList[$contentObjectID] ) )
+        {
+            $handledObjectList[$contentObjectID] |= $clearCacheType;
+        }
+        else
+        {
+            $handledObjectList[$contentObjectID] = $clearCacheType;
+        }
+
+        $handledObjectList[$contentObjectID] |= $clearCacheType;
+        //self::writeDebugBits( $handledObjectList, self::CLEAR_SIBLINGS_CACHE );
+
         $assignedNodes = $contentObject->assignedNodes();
 
         // determine if $contentObject has dependent objects for which cache should be cleared too.
@@ -429,6 +468,9 @@
         if ( $dependentClassInfo['clear_cache_type'] === self::CLEAR_NO_CACHE )
         {
             $clearCacheType = $dependentClassInfo['clear_cache_type'];
+            // when recursing we will never have to handle this object again for other cache types
+            // because types of caches to clear will always be set to none
+            $handledObjectList[$contentObjectID] = self::CLEAR_ALL_CACHE;
         }
 
         if ( $clearCacheType & self::CLEAR_NODE_CACHE )
@@ -458,7 +500,11 @@
 
         if ( $dependentClassInfo['clear_cache_type'] & self::CLEAR_SIBLINGS_CACHE )
         {
-            eZContentCacheManager::appendSiblingsNodeIDs( $assignedNodes, $nodeList );
+            if ( !( $clearCacheType & self::CLEAR_SIBLINGS_CACHE ) )
+            {
+                eZContentCacheManager::appendSiblingsNodeIDs( $assignedNodes, $nodeList );
+                $handledObjectList[$contentObjectID] |= self::CLEAR_SIBLINGS_CACHE;
+            }
 
             // drop 'siblings' bit and process parent nodes.
             // since 'sibling' mode is affected to the current object
@@ -469,9 +515,18 @@
         {
             foreach( $dependentClassInfo['additional_objects'] as $objectID )
             {
+                // skip if cache type is already handled for this object
+                if ( isset( $handledObjectList[$objectID] ) && $handledObjectList[$objectID] & self::CLEAR_NODE_CACHE )
+                {
+                    continue;
+                }
+
                 $object = eZContentObject::fetch( $objectID );
                 if ( $object )
-                    eZContentCacheManager::nodeListForObject( $object, true, self::CLEAR_NODE_CACHE, $nodeList );
+                {
+                    //eZDebug::writeDebug( 'adding additional object ' . $objectID, 'eZContentCacheManager::nodeListForObject() for object ' . $contentObjectID );
+                    eZContentCacheManager::nodeListForObject( $object, true, self::CLEAR_NODE_CACHE, $nodeList, $handledObjectList );
+                }
             }
         }
 
@@ -488,18 +543,9 @@
             {
                 $step = 0;
 
-                // getting class identifier and node ID for each node in the $nodePath.
-                $nodeInfoList = eZContentObjectTreeNode::fetchClassIdentifierListByPathString( $nodePath, false );
+                // getting class identifier and node ID for each node in the $nodePath, up to $maxParents
+                $nodeInfoList = eZContentObjectTreeNode::fetchClassIdentifierListByPathString( $nodePath, false, $maxParents );
 
-                if ( $maxParents > 0 )
-                {
-                    // need to reverse $nodeInfoList if $maxParents is used.
-                    // if offset is zero then we will loop through all elements in $nodeInfoList. So,
-                    // array_reverse is not needed.
-
-                    $nodeInfoList = array_reverse( $nodeInfoList );
-                }
-
                 // for each node in $nodeInfoList determine if this node belongs to $dependentClassIdentifiers. If
                 // so then clear cache for this node.
                 foreach ( $nodeInfoList as $item )
@@ -507,32 +553,39 @@
                     if ( in_array( $item['class_identifier'], $dependentClassIdentifiers ) )
                     {
                         $object = eZContentObject::fetchByNodeID( $item['node_id'] );
+                        $objectID = $object->attribute( 'id' );
 
+                        if ( isset( $handledObjectList[$objectID] ) )
+                        {
+                            // remove cache types that were already handled
+                            $smartClearType &= ~$handledObjectList[$objectID];
+
+                            // if there are no cache types remaining, then skip
+                            if ( $smartClearType == self::CLEAR_NO_CACHE )
+                            {
+                                continue;
+                            }
+                        }
+
                         if ( count( $dependentClassInfo['object_filter'] ) > 0 )
                         {
-                            foreach ( $dependentClassInfo['object_filter'] as $objectIDFilter )
+                            if ( in_array( $objectID, $dependentClassInfo['object_filter'] ) )
                             {
-                                if ( $objectIDFilter == $object->attribute( 'id' ) )
-                                {
-                                    eZContentCacheManager::nodeListForObject( $object, true, $smartClearType, $nodeList );
-                                    break;
-                                }
+                                //eZDebug::writeDebug( 'adding parent ' . $objectID, 'eZContentCacheManager::nodeListForObject() for object ' . $contentObjectID );
+                                eZContentCacheManager::nodeListForObject( $object, true, $smartClearType, $nodeList, $handledObjectList );
                             }
                         }
                         else
                         {
-                            eZContentCacheManager::nodeListForObject( $object, true, $smartClearType, $nodeList );
+                            //eZDebug::writeDebug( 'adding parent ' . $objectID, 'eZContentCacheManager::nodeListForObject() for object ' . $contentObjectID );
+                            eZContentCacheManager::nodeListForObject( $object, true, $smartClearType, $nodeList, $handledObjectList );
                         }
                     }
-
-                    // if we reached $maxParents then break
-                    if ( ++$step == $maxParents )
-                    {
-                        break;
-                    }
                 }
             }
         }
+
+        //self::writeDebugBits( $handledObjectList, self::CLEAR_SIBLINGS_CACHE );
     }
 
     /*!
@@ -559,7 +612,7 @@
             return false;
         }
 
-        eZContentCacheManager::nodeListForObject( $object, $versionNum, self::CLEAR_DEFAULT, $nodeList );
+        eZContentCacheManager::nodeListForObject( $object, $versionNum, self::CLEAR_DEFAULT, $nodeList, $handledObjectList );
 
         return $nodeList;
     }
@@ -586,6 +639,8 @@
     */
     static function clearObjectViewCache( $objectID, $versionNum = true, $additionalNodeList = false )
     {
+        eZDebug::accumulatorStart( 'node_cleanup_list', '', 'Node cleanup list' );
+
         $nodeList = eZContentCacheManager::nodeList( $objectID, $versionNum );
 
         if ( $nodeList === false and !is_array( $additionalNodeList ) )
@@ -598,6 +653,8 @@
 
         $nodeList = array_unique( $nodeList );
 
+        eZDebug::accumulatorStop( 'node_cleanup_list' );
+
         eZDebugSetting::writeDebug( 'kernel-content-edit', count( $nodeList ), "count in nodeList" );
 
         $ini = eZINI::instance();
@@ -801,7 +858,7 @@
                 $useURLAlias = $ini->variable( 'URLTranslator', 'Translation' ) == 'enabled';
             }
 
-            eZContentCacheManager::nodeListForObject( $object, true, self::CLEAR_DEFAULT, $nodes );
+            eZContentCacheManager::nodeListForObject( $object, true, self::CLEAR_DEFAULT, $nodes, $handledObjectList );
 
             // If no nodes returns it means that ClearCacheMethod = self::CLEAR_NO_CACHE
             if ( count( $nodes ) )
Index: kernel/classes/ezcontentobjecttreenode.php
===================================================================
--- kernel/classes/ezcontentobjecttreenode.php	(revision 21546)
+++ kernel/classes/ezcontentobjecttreenode.php	(working copy)
@@ -1641,28 +1641,40 @@
 
     /*!
         \a static
+
+        \param $limit maximum number of nodes in the path to use, starting from last node
     */
-    static function createNodesConditionSQLStringFromPath( $nodePath, $includingLastNodeInThePath )
+    static function createNodesConditionSQLStringFromPath( $nodePath, $includingLastNodeInThePath, $limit = false )
     {
         $pathString = false;
-        $pathArray  = explode( '/', trim($nodePath,'/') );
+        $pathArray  = explode( '/', trim( $nodePath, '/' ) );
 
-        if ( $includingLastNodeInThePath == false )
-            $pathArray = array_slice( $pathArray, 0, count($pathArray)-1 );
+        $pathArrayCount = count( $pathArray );
 
-        if ( count( $pathArray ) > 0 )
+        if ( $limit && $includingLastNodeInThePath == false )
         {
-            foreach ( $pathArray as $node )
-            {
-                $pathString .= 'or node_id = ' . $node . ' ';
+            $limit++;
+        }
 
-            }
-            if ( strlen( $pathString) > 0 )
-            {
-                $pathString = '('. substr( $pathString, 2 ) . ') and ';
-            }
+        $sliceOffset = $limit && $pathArrayCount > $limit ? $pathArrayCount - $limit : 0;
+        $sliceLength = $includingLastNodeInThePath ? $pathArrayCount - $sliceOffset : $pathArrayCount - ( $sliceOffset + 1 );
+
+        // only take a slice when necessary
+        if ( ( $sliceOffset + $sliceLength ) < $pathArrayCount )
+        {
+            $pathArray = array_slice( $pathArray, $sliceOffset, $sliceLength );
         }
 
+        if ( $sliceLength == 1 )
+        {
+            $pathString = ' node_id = ' . implode( '', $pathArray ) . ' and ';
+        }
+        else if ( $sliceLength > 0 )
+        {
+            $db = eZDB::instance();
+            $pathString = ' ' . $db->generateSQLInStatement( $pathArray, 'node_id' ) . ' and ';
+        }
+
         return $pathString;
     }
 
@@ -3480,10 +3492,10 @@
                           The last node is the node which the path was fetched from.
      \param $asObjects If \c true then return PHP objects, if not return raw row data.
     */
-    static function fetchNodesByPathString( $nodePath, $withLastNode = false, $asObjects = true )
+    static function fetchNodesByPathString( $nodePath, $withLastNode = false, $asObjects = true, $limit = false )
     {
         $nodesListArray = array();
-        $pathString = eZContentObjectTreeNode::createNodesConditionSQLStringFromPath( $nodePath, $withLastNode );
+        $pathString = eZContentObjectTreeNode::createNodesConditionSQLStringFromPath( $nodePath, $withLastNode, $limit );
 
         if ( $pathString  )
         {
@@ -3535,10 +3547,10 @@
      $list = fetchClassIdentifierListByPathString( '/2/10/', false );
      \endcode
     */
-    static function fetchClassIdentifierListByPathString( $nodePath, $withLastNode )
+    static function fetchClassIdentifierListByPathString( $nodePath, $withLastNode, $limit = false )
     {
         $itemList = array();
-        $nodes = eZContentObjectTreeNode::fetchNodesByPathString( $nodePath, $withLastNode, false );
+        $nodes = eZContentObjectTreeNode::fetchNodesByPathString( $nodePath, $withLastNode, false, $limit );
 
         foreach ( $nodes as $node )
         {
