public function testChoosePrioritizedRow()
 {
     // Make sure we can see all languages
     ezpINIHelper::setINISetting('site.ini', 'RegionalSettings', 'ShowUntranslatedObjects', 'enabled');
     $action = "eznode:" . mt_rand();
     $name = __FUNCTION__ . mt_rand();
     $engGB = eZContentLanguage::fetchByLocale('eng-GB');
     $norNO = eZContentLanguage::fetchByLocale('nor-NO');
     // Create an english entry
     $url1 = eZURLAliasML::create($name . " en", $action, 0, $engGB->attribute('id'));
     $url1->store();
     // Create a norwegian entry
     $url2 = eZURLAliasML::create($name . " no", $action, 0, $norNO->attribute('id'));
     $url2->store();
     // Fetch the created entries. choosePrioritizedRow() wants rows from the
     // database so our eZURLAliasML objects wont work.
     $db = eZDB::instance();
     $rows = $db->arrayQuery("SELECT * FROM ezurlalias_ml where action = '{$action}'");
     // -------------------------------------------------------------------
     // TEST PART 1 - NORMAL PRIORITIZATION -------------------------------
     // The order of the language array also determines the prioritization.
     // In this case 'eng-GB' should be prioritized before 'nor-NO'.
     $languageList = array("eng-GB", "nor-NO");
     ezpINIHelper::setINISetting('site.ini', 'RegionalSettings', 'SiteLanguageList', $languageList);
     eZContentLanguage::clearPrioritizedLanguages();
     $row = eZURLAliasML::choosePrioritizedRow($rows);
     // The prioritzed language should be 'eng-GB'
     self::assertEquals($engGB->attribute('id'), $row["lang_mask"]);
     // -------------------------------------------------------------------
     // TEST PART 2 - REVERSED PRIORITIZATION -----------------------------
     // Reverse the order of the specified languages, this will also
     // reverse the priority.
     $languageList = array_reverse($languageList);
     ezpINIHelper::setINISetting('site.ini', 'RegionalSettings', 'SiteLanguageList', $languageList);
     eZContentLanguage::clearPrioritizedLanguages();
     $row = eZURLAliasML::choosePrioritizedRow($rows);
     // The prioritzed language should be 'nor-NO'
     self::assertEquals($norNO->attribute('id'), $row["lang_mask"]);
     // -------------------------------------------------------------------
     // TEST TEAR DOWN ----------------------------------------------------
     ezpINIHelper::restoreINISettings();
     // -------------------------------------------------------------------
 }
 static function storePath($path, $action, $languageName = false, $linkID = false, $alwaysAvailable = false, $rootID = false, $cleanupElements = true, $autoAdjustName = false, $reportErrors = true, $aliasRedirects = true)
 {
     $path = eZURLAliasML::cleanURL($path);
     if ($languageName === false) {
         $languageName = eZContentLanguage::topPriorityLanguage();
     }
     if (is_object($languageName)) {
         $languageObj = $languageName;
         $languageID = (int) $languageName->attribute('id');
         $languageName = $languageName->attribute('locale');
     } else {
         $languageObj = eZContentLanguage::fetchByLocale($languageName);
         $languageID = (int) $languageObj->attribute('id');
     }
     $languageMask = $languageID;
     if ($alwaysAvailable) {
         $languageMask |= 1;
     }
     $path = eZURLAliasML::cleanURL($path);
     $elements = explode('/', $path);
     $db = eZDB::instance();
     $parentID = 0;
     // If the root ID is specified we will start the parent search from that
     if ($rootID !== false) {
         $parentID = $rootID;
     }
     $i = 0;
     // Top element is handled separately.
     $topElement = array_pop($elements);
     // Find correct parent, and create missing ones if necessary
     $createdPath = array();
     foreach ($elements as $element) {
         $actionStr = $db->escapeString($action);
         if ($cleanupElements) {
             $element = eZURLAliasML::convertToAlias($element, 'noname' . (count($createdPath) + 1));
         }
         $elementStr = $db->escapeString(eZURLAliasML::strtolower($element));
         $query = "SELECT * FROM ezurlalias_ml WHERE text_md5 = " . eZURLAliasML::md5($db, $elementStr, false) . " AND parent = {$parentID}";
         $rows = $db->arrayQuery($query);
         if (count($rows) == 0) {
             // Create a fake element to ensure we have a parent
             $elementObj = eZURLAliasML::create($element, "nop:", $parentID, 1);
             $elementObj->store();
             $parentID = (int) $elementObj->attribute('id');
         } else {
             $parentID = (int) $rows[0]['link'];
         }
         $createdPath[] = $element;
         ++$i;
     }
     if ($parentID != 0) {
         $sql = "SELECT text, parent FROM ezurlalias_ml WHERE id = {$parentID}";
         $rows = $db->arrayQuery($sql);
         if (count($rows) > 0) {
             // A special case. If the special entry with empty text is used as parent
             // the parent must be adjust to 0 (ie. real top level).
             if (strlen($rows[0]['text']) == 0 && $rows[0]['parent'] == 0) {
                 $createdPath = array();
                 $parentID = 0;
             }
         }
     }
     if (!preg_match("#^(.+):(.+)\$#", $action, $matches)) {
         return array('status' => self::ACTION_INVALID, 'error_message' => "The action value " . var_export($action, true) . " is invalid", 'error_number' => self::ACTION_INVALID, 'path' => null, 'element' => null);
     }
     $actionName = $matches[1];
     $actionValue = $matches[2];
     $existingElementID = null;
     $alwaysMask = $alwaysAvailable ? 1 : 0;
     $actionStr = $db->escapeString($action);
     $actionTypeStr = $db->escapeString($actionName);
     $createdElement = null;
     if ($linkID === false) {
         if ($cleanupElements) {
             $topElement = eZURLAliasML::convertToAlias($topElement, 'noname' . (count($createdPath) + 1));
         }
         $adjustName = false;
         $curElementID = null;
         $newElementID = null;
         $newText = $topElement;
         $uniqueCounter = 0;
         // Loop until we a valid entry point, which means:
         // 1. The entry does not exist yet, so create a new one
         // 2. The entry exists but is re-usable (e.g. nop or same action)
         // 3. The entry exists and cannot be re-used, instead the name is adjusted to be unique.
         while (true) {
             $newText = $topElement;
             if ($uniqueCounter > 0) {
                 $newText .= $uniqueCounter + 1;
             }
             $textMD5 = eZURLAliasML::md5($db, $newText);
             $query = "SELECT * FROM ezurlalias_ml WHERE parent = {$parentID} AND text_md5 = {$textMD5}";
             $rows = $db->arrayQuery($query);
             if (count($rows) == 0) {
                 // No such entry, create a new one
                 break;
             }
             $row = $rows[0];
             $curID = (int) $row['id'];
             $curAction = $row['action'];
             if ($curAction == 'nop:' || $curAction == $action || $row['is_original'] == 0) {
                 // We can reuse the element so record the ID
                 $curElementID = $curID;
                 $newElementID = $curID;
                 break;
             }
             if (!$autoAdjustName) {
                 if ($reportErrors) {
                     eZDebug::writeError("Tried to store path '{$path}' but the path already exists (ID: {$curID}) but with action '{$curAction}', the new action was '{$action}'");
                 }
                 return array('status' => self::LINK_ALREADY_TAKEN, 'path' => $path, 'element' => null);
             }
             // Need to adjust name, re-iterate
             ++$uniqueCounter;
         }
         $textEsc = $db->escapeString($newText);
         // See if there is already a node in the same level with the same action
         if ($newElementID === null) {
             $query = "SELECT * FROM ezurlalias_ml " . "WHERE parent = {$parentID} AND action = '{$actionStr}' AND is_original = 1 AND is_alias = 0";
             $rows = $db->arrayQuery($query);
             if (count($rows) > 0) {
                 $newElementID = (int) $rows[0]['id'];
             }
         }
         // Create or update the element
         if ($curElementID !== null) {
             // Check if an already existing entry at the same level exists, with a different id
             // if so the id must be updated.
             $query = "SELECT * FROM ezurlalias_ml " . "WHERE parent = {$parentID} AND action = '{$actionStr}' AND is_original = 1 AND is_alias = 0";
             $rows = $db->arrayQuery($query);
             if (count($rows) > 0) {
                 $existingEntryId = (int) $rows[0]['id'];
                 if ($existingEntryId != $curElementID) {
                     // move history entry to the same id
                     $query = "UPDATE ezurlalias_ml SET id = {$existingEntryId} " . "WHERE parent = {$parentID} AND text_md5 = {$textMD5}";
                     $res = $db->query($query);
                     if (!$res) {
                         return eZURLAliasML::dbError($db);
                     }
                     $curElementID = $existingEntryId;
                 }
             }
             $bitOr = $db->bitOr($db->bitAnd('lang_mask', ~1), $languageMask);
             // Note: The `text` field is updated too, this ensures case-changes are stored.
             $query = "UPDATE ezurlalias_ml SET link = id, lang_mask = {$bitOr}, text = '{$textEsc}', action = '{$actionStr}', action_type = '{$actionTypeStr}', is_alias = 0, is_original = 1 " . "WHERE parent = {$parentID} AND text_md5 = {$textMD5}";
             $res = $db->query($query);
             if (!$res) {
                 return eZURLAliasML::dbError($db);
             }
             $newElementID = $curElementID;
         } else {
             $element = new eZURLAliasML(array('id' => $newElementID, 'link' => null, 'parent' => $parentID, 'text' => $newText, 'lang_mask' => $languageID | $alwaysMask, 'action' => $action));
             $element->store();
             $newElementID = (int) $element->attribute('id');
             $createdElement = $element;
         }
         $createdPath[] = $newText;
         // OMS-urlalias-fix: We want to retain the lang_mask of url entries, but mark others as history elements is_original = 0
         // Furthermore this change is not performed on custom alias entries.
         $bitAnd = $db->bitAnd('lang_mask', $languageID);
         // First we look at the entries to mark as history entries, if an entry comprise more languages, it must not be set as history element.
         $query = "SELECT * FROM ezurlalias_ml " . "WHERE action = '{$actionStr}' AND ({$bitAnd} > 0) AND is_original = 1 AND is_alias = 0 AND (parent != {$parentID} OR text_md5 != {$textMD5})";
         $toBeUpdated = $db->arrayQuery($query);
         // 0. Check if the entry to be updated represents multiple languages:
         // IF YES:
         //  1. "Downgrade" existing entry, by removing the active translation's language id from the language_mask.
         // IF NO:
         //  1. Mark entry as a history entry
         if (count($toBeUpdated) > 0) {
             $languageMask = $toBeUpdated[0]['lang_mask'];
             if (($languageMask & ~($languageID | 1)) != 0) {
                 // "Composite entry", downgrade current entry
                 $currentEntry = new eZURLAliasML($toBeUpdated[0]);
                 $currentEntry->LangMask = (int) $currentEntry->LangMask & ~$languageID;
                 $currentEntry->store();
             } else {
                 // Mark as history element.
                 $query = "UPDATE ezurlalias_ml SET is_original = 0 " . "WHERE action = '{$actionStr}' AND ({$bitAnd} > 0) AND is_original = 1 AND is_alias = 0 AND (parent != {$parentID} OR text_md5 != {$textMD5})";
                 $res = $db->query($query);
                 if (!$res) {
                     return eZURLAliasML::dbError($db);
                 }
             }
         }
         // OMS-urlalias-fix: instead entries without language we look at history elements with same action (and language)
         // Look for other nodes with the same action and language
         // if found make then link to the new entry
         $bitAnd = $db->bitAnd('lang_mask', $languageID);
         $query = "SELECT * FROM ezurlalias_ml " . "WHERE action = '{$actionStr}' AND ({$bitAnd} > 0) AND is_original = 0 AND (parent != {$parentID} OR text_md5 != {$textMD5})";
         $rows = $db->arrayQuery($query);
         foreach ($rows as $row) {
             $idtmp = (int) $row['id'];
             if ($idtmp == $newElementID) {
                 $idtmp = self::getNewID();
             }
             $parentIDTmp = (int) $row['parent'];
             $textMD5Tmp = eZURLAliasML::md5($db, $row['text']);
             // OMS-urlalias-fix: We do not touch the lang_mask here
             $res = $db->query("UPDATE ezurlalias_ml SET id = {$idtmp}, link = {$newElementID}, is_alias = 0, is_original = 0 " . "WHERE parent = {$parentIDTmp} AND text_md5 = {$textMD5Tmp}");
             if (!$res) {
                 return eZURLAliasML::dbError($db);
             }
         }
         $res = $db->query($query);
         if (!$res) {
             return eZURLAliasML::dbError($db);
         }
         // Look for other nodes which is a link for the current action
         // if found make then link to the new entry
         // OMS-urlalias-fix: We only want to update the links of entries within the same language.
         // Also, only to be applied on normal entries, not custom aliases
         $bitAnd = $db->bitAnd('lang_mask', $languageID);
         $query = "UPDATE ezurlalias_ml SET link = {$newElementID}, is_alias = 0, is_original = 0 " . "WHERE action = '{$actionStr}' AND is_original = 0 AND is_alias = 0 AND ({$bitAnd} > 0) AND (parent != {$parentID} OR text_md5 != {$textMD5})";
         $res = $db->query($query);
         if (!$res) {
             return eZURLAliasML::dbError($db);
         }
         // Move children from old node to the new node
         // Conflicts:
         // New       |       Old |  Action
         // -------------------------------
         // Element   | Link      | Delete old
         // Element   | Element   | Will not happen, if so delete old
         // Element   | Other     | Reparent with new name
         // Element   | nop       | Delete old
         // Link      | Link      | Delete old
         // Link      | Element   | Delete new, reparent
         // Link      | Other     | Delete new, reparent
         // Link      | nop       | Delete old
         // nop       | Link      | Delete new, reparent
         // nop       | Element   | Delete new, reparent
         // nop       | nop       | Delete old
         // TODO: Handle all conflict cases, for now only the `Delete old, reparent` action is done
         // OMS-urlalias-fix: We are only updating child nodes within the same language,
         // and only for real system-generated url aliases. Custom aliases are left alone.
         $bitAnd = $db->bitAnd('lang_mask', $languageID);
         $query = "SELECT id FROM ezurlalias_ml " . "WHERE action = '{$actionStr}' AND is_alias = 0 AND (parent != {$parentID} OR text_md5 != {$textMD5})";
         $rows = $db->arrayQuery($query);
         foreach ($rows as $row) {
             $oldParentID = (int) $row['id'];
             $query = "UPDATE ezurlalias_ml SET parent = {$newElementID} " . "WHERE parent = {$oldParentID} AND ({$bitAnd} > 0)";
             $res = $db->query($query);
             if (!$res) {
                 return eZURLAliasML::dbError($db);
             }
         }
     } else {
         // Check the link ID
         if ($linkID !== true) {
             $linkID = (int) $linkID;
             // Step 1, find existing ID
             $query = "SELECT * FROM ezurlalias_ml WHERE id = '{$linkID}'";
             $rows = $db->arrayQuery($query);
             // Some sanity checking
             if (count($rows) == 0) {
                 if ($reportErrors) {
                     eZDebug::writeError("The link ID {$linkID} does not exist, cannot create the link", __METHOD__);
                 }
                 return array('status' => eZURLAliasML::LINK_ID_NOT_FOUND);
             }
             if ($rows[0]['action'] != $action) {
                 if ($reportErrors) {
                     eZDebug::writeError("The link ID {$linkID} uses a different action ({$rows[0]['action']}) than the requested action ({$action}) for the link, cannot create the link", __METHOD__);
                 }
                 return array('status' => eZURLAliasML::LINK_ID_WRONG_ACTION);
             }
             // If the element which is pointed to is a link, then grab the link id from that instead
             if ($rows[0]['link'] != $rows[0]['id']) {
                 $linkID = (int) $rows[0]['link'];
             }
         } else {
             $linkID = null;
         }
         if ($cleanupElements) {
             $topElement = eZURLAliasML::convertToAlias($topElement, 'noname' . (count($createdPath) + 1));
         }
         $adjustName = false;
         $curElementID = null;
         $newText = $topElement;
         $uniqueCounter = 0;
         $rows = null;
         // Will be filled in by the while loop
         // Loop until we a valid entry point, which means:
         // 1. The entry does not exist yet, so create a new one
         // 2. The entry exists but is re-usable (e.g. nop or same action)
         // 3. The entry exists and cannot be re-used, instead the name is adjusted to be unique.
         while (true) {
             $newText = $topElement;
             if ($uniqueCounter > 0) {
                 $newText .= $uniqueCounter + 1;
             }
             $textMD5 = eZURLAliasML::md5($db, $newText);
             $query = "SELECT * FROM ezurlalias_ml WHERE parent = {$parentID} AND text_md5 = {$textMD5}";
             $rows = $db->arrayQuery($query);
             if (count($rows) == 0) {
                 // No such entry, create a new one
                 break;
             }
             $row = $rows[0];
             $curID = (int) $row['id'];
             $curLink = (int) $row['link'];
             $curAction = $row['action'];
             if ($curAction == $action) {
                 // If the current node is the same action and is not a link we
                 // cannot replace it with a link node.
                 if ($curID != $curLink) {
                     // We can reuse the element so record the ID
                     $curElementID = $curID;
                     break;
                 }
             } else {
                 if ($curAction == 'nop:' || $row['is_original'] == 0) {
                     // We can reuse the element so record the ID
                     $curElementID = $curID;
                     break;
                 }
             }
             if (!$autoAdjustName) {
                 if ($reportErrors) {
                     eZDebug::writeError("Tried to store path '{$path}' but the path already exists (ID: {$curID}) but with action '{$curAction}', the new action was '{$action}'");
                 }
                 return array('status' => self::LINK_ALREADY_TAKEN, 'path' => $path, 'element' => null);
             }
             // Need to adjust name, re-iterate
             ++$uniqueCounter;
         }
         $textEsc = $db->escapeString($newText);
         // Create or update the element
         if ($curElementID !== null) {
             $element = new eZURLAliasML($rows[0]);
             // $rows is from the while loop
             $element->LangMask |= $languageID | $alwaysMask;
             $element->IsAlias = 1;
             $element->Action = $action;
             // Note: The `text` field is updated too, this ensures case-changes are stored.
             $element->Text = $newText;
             $element->TextMD5 = null;
             $element->ActionType = null;
             $element->Link = null;
         } else {
             $element = new eZURLAliasML(array('id' => null, 'link' => null, 'parent' => $parentID, 'text' => $newText, 'lang_mask' => $languageID | $alwaysMask, 'action' => $action, 'is_alias' => 1));
         }
         $element->AliasRedirects = $aliasRedirects ? 1 : 0;
         $element->store();
         $createdPath[] = $topElement;
         $createdElement = $element;
     }
     return array('status' => true, 'path' => join("/", $createdPath), 'element' => $createdElement);
 }
 public function testChoosePrioritizedRow()
 {
     // TEST SETUP --------------------------------------------------------
     $ini = eZINI::instance();
     // Make sure to preserve ini settings in case other tests depend on them
     $orgShowUntranslatedObjects = $ini->variable('RegionalSettings', 'ShowUntranslatedObjects');
     $orgSiteLanguageList = $ini->variable('RegionalSettings', 'SiteLanguageList');
     // Make sure we can see all languages
     $ini->setVariable('RegionalSettings', 'ShowUntranslatedObjects', 'enabled');
     $action = "eznode:" . mt_rand();
     $name = __FUNCTION__ . mt_rand();
     // Create an english entry
     $url1 = eZURLAliasML::create($name . " en", $action, 0, 2);
     $url1->store();
     // Create a norwegian entry
     $url2 = eZURLAliasML::create($name . " no", $action, 0, 4);
     $url2->store();
     // Fetch the created entries. choosePrioritizedRow() wants rows from the
     // database so our eZURLAliasML objects wont work.
     $db = eZDB::instance();
     $rows = $db->arrayQuery("SELECT * FROM ezurlalias_ml where action = '{$action}'");
     // -------------------------------------------------------------------
     // TEST PART 1 - NORMAL PRIORITIZATION -------------------------------
     // The order of the language array also determines the prioritization.
     // In this case 'eng-GB' should be prioritized before 'nor-NO'.
     $languageList = array("eng-GB", "nor-NO");
     $ini->setVariable('RegionalSettings', 'SiteLanguageList', $languageList);
     eZContentLanguage::clearPrioritizedLanguages();
     $row = eZURLAliasML::choosePrioritizedRow($rows);
     // The prioritzed language should be 'eng-GB' (lang_mask = 2)
     self::assertEquals(2, $row["lang_mask"]);
     // -------------------------------------------------------------------
     // TEST PART 2 - REVERSED PRIORITIZATION -----------------------------
     // Reverse the order of the specified languages, this will also
     // reverse the priority.
     $languageList = array_reverse($languageList);
     $ini->setVariable('RegionalSettings', 'SiteLanguageList', $languageList);
     eZContentLanguage::clearPrioritizedLanguages();
     $row = eZURLAliasML::choosePrioritizedRow($rows);
     // The prioritzed language should be 'nor-NO' (lang_mask = 4)
     self::assertEquals(4, $row["lang_mask"]);
     // -------------------------------------------------------------------
     // TEST TEAR DOWN ----------------------------------------------------
     $ini->setVariable('RegionalSettings', 'ShowUntranslatedObjects', $orgShowUntranslatedObjects);
     $ini->setVariable('RegionalSettings', 'SiteLanguageList', $orgSiteLanguageList);
     // -------------------------------------------------------------------
 }