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); }