commit 565302911ecca339e457950ea7b1afd31b375e0b
Author: Bertrand Dunogier <bd@ez.no>
Date:   Thu May 29 08:10:16 2014 +0200

    Fix EZP-22913: prevent exponential growth in number of queries when removing image alias attributes
    
    Conflicts:
    	kernel/classes/datatypes/ezimage/ezimagealiashandler.php
    (cherry picked from commit d759414796b858f1b7222c420d46459ddc0ed4b7)
    
    Conflicts:
    	kernel/classes/datatypes/ezimage/ezimagealiashandler.php
    
    Also includes regression fixes:
    
    Fixed regression from EZP-22913
    
    Missing method in backport + missing fetched db field in a method.
    (cherry picked from commit d573b71e7fab078b483066ff5ae3cba3933b926c)
    
    Fix EZP-22615: Not possible to remove an image from an object
    (cherry picked from commit 4fa0c1a588dd5dd7c20fe38f64b4c7e7035fa1dd)
    (cherry picked from commit 86a50fda7809aec8f506bce32b4255f057c6c727)
    (cherry picked from commit 5b32eb894a8f87ca1c99599a55667031b32e640e)
    
    Conflicts:
    	kernel/classes/datatypes/ezimage/ezimagealiashandler.php

diff --git a/kernel/classes/datatypes/ezimage/ezimagealiashandler.php b/kernel/classes/datatypes/ezimage/ezimagealiashandler.php
index e3943fb..0d67019 100644
--- a/kernel/classes/datatypes/ezimage/ezimagealiashandler.php
+++ b/kernel/classes/datatypes/ezimage/ezimagealiashandler.php
@@ -713,123 +713,90 @@ class eZImageAliasHandler
     }
 
     /**
-     * Removes all the image aliases and their information.
-     * The stored images will also be removed if the attribute is the owner
-     * of the images.
+     * Removes aliases from the attribute.
      *
-     * After the images are removed the attribute will containe an internal
-     * structure with empty data
+     * Files, as well as their references in ezimagefile, are only removed if this attribute/version was
+     * the last one referencing it. The attribute's XML is then reset to reference no image.
+     */
+    function removeAliases()
+    {
+        foreach ( $this->aliasList() as $alias )
+        {
+            if ( $alias['is_valid'] )
+                $this->removeAliasFile( $alias['url'] );
+        }
+
+        $this->generateXMLData();
+    }
+
+    /**
+     * Removes $aliasFile and references to it for the attribute.
      *
-     * @param eZContentObjectAttribute $contentObjectAttribute
-     *        Content object attribute to remove aliases for
+     * The eZImageFile entry as well as the file are only removed if this attribute was the last reference.
      *
-     * @return void
+     * @param string $aliasFile
+     *
+     * @return array bool true if something was actually done, false otherwise
      */
-    function removeAliases( $contentObjectAttribute )
+    public function removeAliasFile( $aliasFile )
     {
-        $aliasList = $this->aliasList();
-        $alternativeText = false;
+        if ( $aliasFile == '' )
+            throw new InvalidArgumentException( "Expecting image file path" );
 
-        $contentObjectAttributeVersion = $this->ContentObjectAttributeData['version'];
-        $contentObjectAttributeID = $this->ContentObjectAttributeData['id'];
+        $filePathIsReferencedByOtherAttributes = false;
 
-        $isImageOwner = $this->isImageOwner();
-
-        // We loop over each image alias, and look up the file in ezcontentobject_attribute
-        // Only images referenced by one version will be removed
-        foreach ( $aliasList as $aliasName => $alias )
+        // we must check all eZImageFile referencing the alias file
+        $imageFilesReferencingFilePath = eZImageFile::fetchListByFilepath( $aliasFile );
+        foreach ( $imageFilesReferencingFilePath as $imageFileData )
         {
-            $dirpath = $alias['dirpath'];
-            $doNotDelete = false; // Do not delete files from storage
-
-            if ( $aliasName == 'original' )
-                $alternativeText = $alias['alternative_text'];
-            if ( $alias['is_valid'] )
+            if ( $imageFileData['contentobject_attribute_id'] != $this->ContentObjectAttributeData['id'] )
             {
-                $filepath = $alias['url'];
-
-                /**
-                 * If there are no ezimagefile references to this file with this content object attribute id,
-                 * we are dealing with an attribute that was copied from another content.
-                 * The image doesn't belong to us, and we just don't do anything.
-                 * See http://jira.ez.no/browse/EZP-21324
-                 */
-                if ( !eZImageFile::fetchByFilepath( $contentObjectAttributeID, $filepath ) )
-                    continue;
+                $filePathIsReferencedByOtherAttributes = true;
+            }
 
-                // Fetch ezimage attributes that use $filepath
-                // Always returns current attribute (array of $contentObjectAttributeID and $contentObjectAttributeVersion)
-                $dbResult = eZImageFile::fetchImageAttributesByFilepath( $filepath, $contentObjectAttributeID );
-                $dbResultCount = count( $dbResult );
-                // Check if there are the attributes.
-                if ( $dbResultCount > 0 )
-                {
-                    $doNotDelete = true;
-                    foreach ( $dbResult as $res )
-                    {
-                        // We only look results where the version matches
-                        if ( $res['version'] == $contentObjectAttributeVersion )
-                        {
-                            // If more than one result has been returned, it means
-                            // that another version is using the same image,
-                            // and we should not delete this file
-                            if ( $dbResultCount > 1 )
-                            {
-                                continue;
-                            }
-                            // Only one result means that the current attribute
-                            // & version are the only ones using this image,
-                            // and it can be removed
-                            else
-                            {
-                                $doNotDelete = false;
-                            }
-                        }
+            $imageFileIsReferencedByOtherAttributes = false;
 
-                        eZImageFile::appendFilepath( $res['id'], $filepath, true );
-                    }
-                }
+            // we skip eZImageFile entries that reference another contentobject attribute or another version/language
+            foreach ( eZImageFile::fetchImageAttributesByFilepath( $aliasFile, $imageFileData['contentobject_attribute_id'] ) as $attribute )
+            {
+                if ( $attribute['id'] != $this->ContentObjectAttributeData['id'] )
+                    continue;
 
-                if ( !$doNotDelete )
+                if (
+                    $attribute['version'] != $this->ContentObjectAttributeData['version'] ||
+                    $attribute['language_code'] != $this->ContentObjectAttributeData['language_code']
+                )
                 {
-                    $file = eZClusterFileHandler::instance( $filepath );
-                    if ( $file->exists() )
-                    {
-                        $file->delete();
-                        eZImageFile::removeFilepath( $contentObjectAttributeID, $filepath );
-                        eZDir::cleanupEmptyDirectories( $dirpath );
-                    }
-                    else
-                    {
-                        eZDebug::writeError( "Image file $filepath for alias $aliasName does not exist, could not remove from disk", __METHOD__ );
-                    }
+                    $filePathIsReferencedByOtherAttributes = true;
+                    $imageFileIsReferencedByOtherAttributes = true;
+                    break;
                 }
             }
-        }
 
-        $doc = new DOMDocument( '1.0', 'utf-8' );
-        $imageNode = $doc->createElement( "ezimage" );
-        $doc->appendChild( $imageNode );
-
-        $imageNode->setAttribute( 'serial_number', false );
-        $imageNode->setAttribute( 'is_valid', false );
-        $imageNode->setAttribute( 'filename', false );
-        $imageNode->setAttribute( 'suffix', false );
-        $imageNode->setAttribute( 'basename', false );
-        $imageNode->setAttribute( 'dirpath', false );
-        $imageNode->setAttribute( 'url', false );
-        $imageNode->setAttribute( 'original_filename', false );
-        $imageNode->setAttribute( 'mime_type', false );
-        $imageNode->setAttribute( 'width', false );
-        $imageNode->setAttribute( 'height', false );
-        $imageNode->setAttribute( 'alternative_text', $alternativeText );
-        $imageNode->setAttribute( 'alias_key', false );
-        $imageNode->setAttribute( 'timestamp', false );
+            if ( $imageFileIsReferencedByOtherAttributes )
+                continue;
 
-        $this->ContentObjectAttributeData['DataTypeCustom']['dom_tree'] = $doc;
-        unset( $this->ContentObjectAttributeData['DataTypeCustom']['alias_list'] );
+            // we remove all rows since we can have duplicates
+            eZImageFile::removeObject(
+                eZImageFile::definition(),
+                array(
+                    'contentobject_attribute_id' => $this->ContentObjectAttributeData['id'],
+                    'filepath' => $aliasFile
+                )
+            );
+        }
 
-        $this->storeDOMTree( $doc, true, $contentObjectAttribute );
+        // remove file, if applicable
+        if ( $filePathIsReferencedByOtherAttributes )
+            return;
+        $file = eZClusterFileHandler::instance( $aliasFile );
+        if ( !$file->exists() )
+        {
+            eZDebug::writeError( "Image file {$aliasFile} does not exist, could not remove from disk", __METHOD__ );
+            return;
+        }
+        $file->delete();
+        eZDir::cleanupEmptyDirectories( dirname( $aliasFile ) );
     }
 
     /*!
diff --git a/kernel/classes/datatypes/ezimage/ezimagefile.php b/kernel/classes/datatypes/ezimage/ezimagefile.php
index 4aa6386..c35b2fb 100644
--- a/kernel/classes/datatypes/ezimage/ezimagefile.php
+++ b/kernel/classes/datatypes/ezimage/ezimagefile.php
@@ -117,7 +117,7 @@ class eZImageFile extends eZPersistentObject
         );
         // Escape _ in like to avoid it to act as a wildcard !
         $filepath = addcslashes( $filepath, "_" );
-        $query = "SELECT id, version
+        $query = "SELECT id, version, language_code
                   FROM   ezcontentobject_attribute
                   WHERE  contentobject_id = $contentObjectID and
                          contentclassattribute_id = $contentClassAttributeID and
@@ -142,6 +142,25 @@ class eZImageFile extends eZPersistentObject
                                                 $asObject );
     }
 
+    /**
+     * Fetches unique eZImageFile data (as an array) for $filePath
+     *
+     * @param string $filePath
+     *
+     * @return array array of hash. Keys: contentobject_attribute_id, filepath
+     */
+    static function fetchListByFilePath( $filePath )
+    {
+        return eZPersistentObject::fetchObjectList(
+            eZImageFile::definition(),
+            array( 'contentobject_attribute_id', 'filepath' ),
+            array( 'filepath' => $filePath ),
+            null,
+            null,
+            false
+        );
+    }
+
     static function moveFilepath( $contentObjectAttributeID, $oldFilepath, $newFilepath )
     {
         $db = eZDB::instance();
@@ -191,6 +210,69 @@ class eZImageFile extends eZPersistentObject
         eZPersistentObject::removeObject( eZImageFile::definition(), array( 'contentobject_attribute_id' => $contentObjectAttributeID ) );
     }
 
+    /**
+     * Tests if $filepath is referenced by content attributes of the same $attributeId in different $attributeVersion or $languageCode
+     *
+     * @param string $filepath
+     * @param int $attributeId Content object attribute ID
+     * @param int $attributeVersion Content object attribute version
+     * @param string $languageCode Attribute language code
+     * @return bool true if $filepath is referenced by other attributes, false otherwise
+     */
+    public static function isReferencedByOtherAttributes( $filepath, $attributeId, $attributeVersion, $languageCode )
+    {
+        $db = eZDB::instance();
+        $filepath = $db->escapeString( $filepath );
+
+        // Check eZImageFile and eZContentobjectAttribute for references to the alias file, in another version/language
+        $rows = $db->arrayQuery(
+            sprintf(
+                "
+                SELECT count(*) as count FROM ezcontentobject_attribute
+                INNER JOIN ezimagefile ON ezcontentobject_attribute.id=ezimagefile.contentobject_attribute_id
+                WHERE ezimagefile.filepath='%s'
+                AND ezcontentobject_attribute.data_text LIKE '%%url=\"%s\"%%'
+                AND ezcontentobject_attribute.id = %d
+                AND ( ezcontentobject_attribute.version != %d OR ezcontentobject_attribute.language_code != '%s' )
+                ",
+                $filepath, $filepath,
+                $attributeId,
+                $attributeVersion,
+                $languageCode
+            )
+        );
+
+        return (int)$rows[0]['count'] > 0;
+    }
+
+    /**
+     * Tests if there are ezimagefile rows with a different $attributeId that reference $filepath
+     *
+     * @param string $filepath
+     * @param int $attributeId
+     * @return bool
+     */
+    public static function isReferencedByOtherImageFiles( $filepath, $attributeId )
+    {
+        $db = eZDB::instance();
+        $filepath = $db->escapeString( $filepath );
+
+        $rows = $db->arrayQuery(
+            sprintf(
+                "
+                SELECT count(*) as count FROM ezcontentobject_attribute
+                INNER JOIN ezimagefile ON ezcontentobject_attribute.id=ezimagefile.contentobject_attribute_id
+                WHERE ezimagefile.filepath='%s'
+                AND ezcontentobject_attribute.id != %d
+                ",
+                $filepath,
+                $attributeId
+            )
+        );
+
+        return (int)$rows[0]['count'] > 0;
+    }
+
 
     /// \privatesection
     public $ID;
diff --git a/kernel/classes/datatypes/ezimage/ezimagetype.php b/kernel/classes/datatypes/ezimage/ezimagetype.php
index 68a91b8..b329cc1 100644
--- a/kernel/classes/datatypes/ezimage/ezimagetype.php
+++ b/kernel/classes/datatypes/ezimage/ezimagetype.php
@@ -304,11 +304,9 @@ class eZImageType extends eZDataType
 
         }
 
-        if ( $content )
+        if ( $result && $hasImageAltText )
         {
-            if ( $hasImageAltText )
-                $content->setAttribute( 'alternative_text', $imageAltText );
-            $result = true;
+            $content->setAttribute( 'alternative_text', $imageAltText );
         }
 
         return $result;
