public function doMerge() { global $wgOut, $wgUser; $skin =& $wgUser->getSkin(); if ($this->isGedcom()) { $editFlags = EDIT_UPDATE; $mergeText = 'updated'; } else { // create a mergelog record $mergeScore = $this->getMergeScore(); $isTrustedUser = CompareForm::isTrustedMerger($wgUser, false); $isTrustedMerge = MergeForm::isTrustedMerge($mergeScore, $isTrustedUser); $mergeLogId = $this->logMerge($mergeScore, $isTrustedMerge); wfDebug("MERGESCORE mergeLogId={$mergeLogId} total score={$mergeScore}\n"); if (!$isTrustedUser && $mergeScore < self::$LOW_MATCH_THRESHOLD) { error_log("WARNING suspect merge: id={$mergeLogId} user={$wgUser->getName()} score={$mergeScore}"); } $editFlags = EDIT_UPDATE | EDIT_FORCE_BOT; $mergeText = 'merged'; } // get merging people and families // add merging people and families to blacklist so propagation doesn't also try to update them $mergingPeople = array(); $mergingFamilies = array(); for ($m = 0; $m < count($this->data); $m++) { for ($p = 0; $p < count($this->data[$m]); $p++) { $titleString = $this->data[$m][$p]['title']; if ($this->namespace == 'Family' && $m == 0) { if ($p > 0) { $mergingFamilies[$titleString] = $this->data[$m][0]['title']; } $ns = NS_FAMILY; } else { if ($p > 0) { $mergingPeople[$titleString] = $this->data[$m][0]['title']; } $ns = NS_PERSON; } if (!GedcomUtil::isGedcomTitle($titleString)) { $title = Title::newFromText($titleString, $ns); PropagationManager::addBlacklistPage($title); } } } $output = "<H2>Pages {$mergeText} successfully</H2>"; $output .= $this->getWarnings(); $output .= '<ul>'; $outputRows = array(); $emptyRequest = new FauxRequest(array(), true); $mergeCmtSuffix = $this->isGedcom() ? '' : " - [[Special:ReviewMerge/{$mergeLogId}|review/undo]]"; if ($this->namespace == 'Family' && !$this->isGedcom()) { $t = Title::newFromText($this->data[0][0]['title'], NS_FAMILY); $mergeCmtFamily = $this->namespace == 'Family' ? " in merge of [[{$t->getPrefixedText()}]]" : ''; } else { $mergeCmtFamily = ''; } // backwards, because you must merge family last, so that propagated person data in family xml is correct // and so that mergeCmtFamily can be cleared at the end and mergeSummary and mergeTargetTitle are correct after the for loop for ($m = count($this->data) - 1; $m >= 0; $m--) { $requestData = array(); $contents = ''; $talkContents = ''; $outputRow = ''; $mainOutput = ''; $talkOutput = ''; $nameCount = $eventCount = $sourceCount = $imageCount = $noteCount = $husbandCount = $wifeCount = $childrenCount = $parentFamilyCount = $spouseFamilyCount = 0; $primaryNameFound = $primaryImageFound = $birthFound = $christeningFound = $deathFound = $burialFound = $marriageFound = false; if ($this->namespace == 'Family' && $m == 0) { $mergeTargetNs = NS_FAMILY; $mergeTargetTalkNs = NS_FAMILY_TALK; $mergeCmtFamily = ''; } else { $mergeTargetNs = NS_PERSON; $mergeTargetTalkNs = NS_PERSON_TALK; } $mergeTargetTitle = Title::newFromText($this->data[$m][0]['title'], $mergeTargetNs); if ($mergeTargetTitle->getNamespace() != $mergeTargetNs) { error_log("Merge glitch:{$mergeTargetNs}:{$this->data[$m][0]['title']}:{$mergeTargetTitle->getNamespace()}:"); } $mergeTargetTalkTitle = Title::newFromText($this->data[$m][0]['title'], $mergeTargetTalkNs); $mergeSummary = ''; $talkMergeSummary = ''; $keepKeys = $this->generateKeepKeys($this->data[$m], $this->add[$m], $this->key[$m]); $notesMap = array(); $noteAdoptions = array(); $this->generateMapAdoptions('notes', 'N', $mergeTargetNs, $this->data[$m], $this->add[$m], $this->key[$m], $keepKeys, $notesMap, $noteAdoptions); $sourcesMap = array(); $sourceAdoptions = array(); $this->generateMapAdoptions('sources', 'S', $mergeTargetNs, $this->data[$m], $this->add[$m], $this->key[$m], $keepKeys, $sourcesMap, $sourceAdoptions); $imagesMap = array(); $imageAdoptions = array(); $this->generateMapAdoptions('images', 'I', $mergeTargetNs, $this->data[$m], $this->add[$m], $this->key[$m], $keepKeys, $imagesMap, $imageAdoptions); // get request data for merge target for ($p = 0; $p < count($this->data[$m]); $p++) { if ($this->isMergeable($this->data[$m][$p])) { $this->addNotesToRequestData($requestData, $keepKeys[$p], $noteCount, $notesMap[$p], $this->data[$m][$p]['notes']); $this->addImagesToRequestData($requestData, $keepKeys[$p], $imageCount, $imagesMap[$p], $primaryImageFound, $this->data[$m][$p]['images']); $this->addSourcesToRequestData($requestData, $keepKeys[$p], $sourceCount, $sourcesMap[$p], $notesMap[$p], $imagesMap[$p], $this->data[$m][$p]['sources'], $noteAdoptions, $imageAdoptions); $this->addEventsToRequestData($requestData, $keepKeys[$p], $eventCount, $mergeTargetNs == NS_PERSON ? Person::$STD_EVENT_TYPES : Family::$STD_EVENT_TYPES, $birthFound, $christeningFound, $deathFound, $burialFound, $marriageFound, $notesMap[$p], $imagesMap[$p], $sourcesMap[$p], $this->data[$m][$p]['events'], $noteAdoptions, $sourceAdoptions, $imageAdoptions); if ($mergeTargetNs == NS_PERSON) { $this->addNamesToRequestData($requestData, $keepKeys[$p], $nameCount, $primaryNameFound, $notesMap[$p], $sourcesMap[$p], $this->data[$m][$p]['names'], $noteAdoptions, $sourceAdoptions); $this->addFamilyToRequestData($requestData, $mergingFamilies, 'child_of_family', $parentFamilyCount, $this->data[$m][$p]['child_of_families']); $this->addFamilyToRequestData($requestData, $mergingFamilies, 'spouse_of_family', $spouseFamilyCount, $this->data[$m][$p]['spouse_of_families']); } else { $this->addFamilyMembersToRequestData($requestData, $mergingPeople, 'husband', $husbandCount, $this->data[$m][$p]['husbands']); $this->addFamilyMembersToRequestData($requestData, $mergingPeople, 'wife', $wifeCount, $this->data[$m][$p]['wives']); $this->addFamilyMembersToRequestData($requestData, $mergingPeople, 'child', $childrenCount, $this->data[$m][$p]['children']); } $pageContents = $this->mapContents($sourcesMap[$p], $imagesMap[$p], $notesMap[$p], $this->data[$m][$p]['contents']); $this->addContents($contents, $keepKeys[$p], $pageContents); if ($p > 0) { if ($mergeSummary) { $mergeSummary .= ', '; } if ($mainOutput) { $mainOutput .= ', '; } if ($this->data[$m][$p]['gedcom']) { $mergeSummary .= 'gedcom'; $mainOutput .= htmlspecialchars(($mergeTargetNs == NS_FAMILY ? 'Family:' : 'Person:') . $this->data[$m][$p]['title']); } else { $title = Title::newFromText($this->data[$m][$p]['title'], $mergeTargetNs); $mergeSummary .= "[[" . $title->getPrefixedText() . "]]"; $mainOutput .= $skin->makeKnownLinkObj($title, htmlspecialchars($title->getPrefixedText()), 'redirect=no'); } } } } // redirect other pages to merge target $redir = "#REDIRECT [[" . $mergeTargetTitle->getPrefixedText() . "]]"; $talkRedir = "#REDIRECT [[" . $mergeTargetTalkTitle->getPrefixedText() . "]]"; for ($p = 1; $p < count($this->data[$m]); $p++) { if (!$this->data[$m][$p]['gedcom']) { $obj = $this->data[$m][$p]['object']; $comment = $this->makeComment($this->userComment, "merge into [[" . $mergeTargetTitle->getPrefixedText() . "]]" . $mergeCmtFamily, $mergeCmtSuffix); $obj->editPage($emptyRequest, $redir, $comment, $editFlags, false); // redir talk page as well if ($this->data[$m][$p]['talkrevid']) { // if talk page exists $talkTitle = Title::newFromText($this->data[$m][$p]['title'], $mergeTargetTalkNs); $article = new Article($talkTitle, 0); if ($article) { $this->addTalkContents($talkContents, $talkTitle, $article->fetchContent()); if ($talkMergeSummary) { $talkMergeSummary .= ', '; } if ($talkOutput) { $talkOutput .= ', '; } $talkMergeSummary .= "[[" . $talkTitle->getPrefixedText() . "]]"; $talkOutput .= $skin->makeKnownLinkObj($talkTitle, htmlspecialchars($talkTitle->getPrefixedText()), 'redirect=no'); $comment = $this->makeComment($this->userComment, "merge into [[" . $mergeTargetTalkTitle->getPrefixedText() . "]]" . $mergeCmtFamily, $mergeCmtSuffix); $article->doEdit($talkRedir, $comment, $editFlags); } } } } // update merge target talk if ($talkContents) { $article = new Article($mergeTargetTalkTitle, 0); if ($article) { $mergeTargetTalkContents = $article->fetchContent(); if ($mergeTargetTalkContents) { $mergeTargetTalkContents = rtrim($mergeTargetTalkContents) . "\n\n"; } $comment = $this->makeComment($this->userComment, 'merged with ' . $talkMergeSummary . $mergeCmtFamily, $mergeCmtSuffix); $article->doEdit($mergeTargetTalkContents . $talkContents, $comment, $editFlags); if ($this->addWatches) { StructuredData::addWatch($wgUser, $article, true); } } $outputRow .= '<li>Merged ' . $talkOutput . ' into ' . $skin->makeKnownLinkObj($mergeTargetTalkTitle, htmlspecialchars($mergeTargetTalkTitle->getPrefixedText())) . "</li>"; } $obj = $this->data[$m][0]['object']; if ($mergeTargetNs == NS_PERSON) { Person::addGenderToRequestData($requestData, $this->data[$m][0]['gender']); } else { // family $obj->isMerging(true); // to read propagated data from person pages, not from prev family revision } // update merge target $req = new FauxRequest($requestData, true); $comment = $this->makeComment($this->userComment, ($mergeSummary == 'gedcom' ? 'Add data from gedcom' : 'merged with ' . $mergeSummary) . $mergeCmtFamily, $mergeCmtSuffix); $obj->editPage($req, $contents, $comment, $editFlags, $this->addWatches); $outputRow .= '<li>Merged ' . $mainOutput . ' into ' . $skin->makeKnownLinkObj($mergeTargetTitle, htmlspecialchars($mergeTargetTitle->getPrefixedText())) . "</li>"; $outputRows[] = $outputRow; } // add log and recent changes if (!$this->isGedcom()) { if (!$mergeSummary) { $mergeSummary = 'members of other families'; } $mergeComment = 'Merge [[' . $mergeTargetTitle->getPrefixedText() . ']] and ' . $mergeSummary; $log = new LogPage('merge', false); $t = Title::makeTitle(NS_SPECIAL, "ReviewMerge/{$mergeLogId}"); $log->addEntry('merge', $t, $mergeComment); RecentChange::notifyLog(wfTimestampNow(), $t, $wgUser, $mergeComment, '', 'merge', 'merge', $t->getPrefixedText(), $mergeComment, '', $isTrustedMerge, 0); } $nonmergedPages = $this->getNonmergedPages(); $output .= join("\n", array_reverse($outputRows)) . '</ul>' . ($nonmergedPages ? '<p>In addition to the people listed above, the following have also been included in the target family' . ($this->isGedcom() ? '<br/>(GEDCOM people listed will be added when the GEDCOM is imported)' : '') . $nonmergedPages . "</p>\n" : '') . ($this->isGedcom() ? '' : '<p>' . $skin->makeKnownLinkObj(Title::makeTitle(NS_SPECIAL, 'ReviewMerge/' . $mergeLogId), htmlspecialchars("Review/undo merge")) . '<br>' . $skin->makeKnownLinkObj(Title::makeTitle(NS_SPECIAL, 'ShowDuplicates'), htmlspecialchars("Show more duplicates")) . '</p>'); return $output; }
function writeMerge($userid, &$userMerge) { $foundFamilyTitle = null; $foundPeopleCount = 0; $familyCount = 0; $mergeCount = count($userMerge->merges); $preMergeTimestamp = wfTimestamp(TS_MW, wfTimestamp(TS_UNIX, $userMerge->firstTimestamp) - 2); foreach ($userMerge->merges as $merge) { $mergeNs = $merge->target->title->getNamespace(); if ($mergeNs != NS_PERSON && $mergeNs != NS_FAMILY && $mergeNs != NS_PERSON_TALK && $mergeNs != NS_FAMILY_TALK) { return "invalid target for merge: title={$merge->target->title->getPrefixedText()}"; } if ($foundFamilyTitle && $mergeNs == NS_FAMILY) { return "invalid merge sequence - {$merge->target->title->getPrefixedText()} after family: {$foundFamilyTitle->getPrefixedText()}"; } if ($mergeNs == NS_FAMILY) { $familyCount = count($merge->sources) + 1; $foundFamilyTitle = $merge->target->title; } else { if ($mergeNs == NS_PERSON) { $foundPeopleCount++; } } // get target preRevid $revid = StructuredData::getRevidForTimestamp($merge->target->title, $preMergeTimestamp); // its ok if revid was created during the merge for talk pages if ($revid == 0 || $revid > $userMerge->lastRevid || $revid >= $userMerge->firstRevid && $mergeNs != NS_PERSON_TALK && $mergeNs != NS_FAMILY_TALK) { return "revision not found for target={$merge->target->title->getPrefixedText()} revid={$revid} firstRevid={$userMerge->firstRevid} timestamp={$preMergeTimestamp}"; } $merge->target->preRevid = $revid >= $userMerge->firstRevid ? 0 : $revid; // 0 if created during the merge $revision = Revision::newFromId($revid); $merge->target->revPage = $revision->getPage(); // get target postRevid and comment list($revid, $comment) = $this->getNextRevidComment($merge->target->revPage, $merge->target->preRevid); if ($revid != 0 && $revid <= $userMerge->lastRevid) { // if target was edited in the merge $merge->target->postRevid = $revid; $merge->target->comment = $comment; } // get source preRevid's and titles foreach ($merge->sources as $source) { $revid = $this->getPrevRevid($source->revPage, $source->postRevid); if (!$revid) { return "previous revision not found for source={$source->postRevid}"; } $source->preRevid = $revid; $revision = Revision::newFromId($revid); $title = $revision->getTitle(); if (!$title) { return "title not found for source={$source->postRevid}"; } $source->title = $title; if ($title->getNamespace() != $mergeNs) { return "invalid source={$title->getPrefixedText()} target={$merge->target->title->getPrefixedText()}"; } } } if ($foundPeopleCount > 1 && !$foundFamilyTitle) { return "invalid merge sequence: multiple merges without a family: count={$mergeCount}"; } if (!$foundFamilyTitle && $foundPeopleCount == 0) { return "invalid merge sequence: no people or families count={$mergeCount}"; } // put merge revids into their proper rows and columns, and add roles $mergeRows = array(); for ($m = 0; $m < count($userMerge->merges); $m++) { $mergeRows[$m] = (object) array('role' => '', 'revids' => array()); if ($foundFamilyTitle && $userMerge->merges[$m]->target->title->getNamespace() == NS_PERSON) { for ($p = 0; $p < count($familyCount); $p++) { $mergeRows[$m]->revids[$p] = array(); } } } $mainTitle = null; $totalScore = 0; $totalCount = 0; $seenRevids = array(); for ($m = 0; $m < count($userMerge->merges); $m++) { $merge =& $userMerge->merges[$m]; $ns = $merge->target->title->getNamespace(); // read baseData and data, calc match score if ($ns == NS_PERSON || $ns == NS_FAMILY) { $baseData = $this->readData($merge->target->title, $merge->target->preRevid); if (!$baseData) { return "unable to read data for {$merge->target->title->getPrefixedText()} revid={$merge->target->preRevid}"; } if ($ns == NS_FAMILY) { $baseStdData = MergeForm::standardizeFamilyData($baseData); } else { $baseStdData = MergeForm::standardizePersonData($baseData); } $data = array(); for ($p = 0; $p < count($merge->sources); $p++) { $data[$p] = $this->readData($merge->sources[$p]->title, $merge->sources[$p]->preRevid); if (!$data[$p]) { return "unable to read data for {$merge->sources[$p]->title->getPrefixedText()} revid={$merge->sources[$p]->preRevid}"; } if ($ns == NS_FAMILY) { $stdData = MergeForm::standardizeFamilyData($data[$p]); } else { $stdData = MergeForm::standardizePersonData($data[$p]); } $totalScore += MergeForm::calcMatchScore($baseStdData, $stdData); $totalCount++; } } if ($foundFamilyTitle && $ns == NS_FAMILY) { // use pre-merge family revisions to get roles, row numbers $this->recordFamilyMembers($baseData['husbands'], 'husband', 0, $userMerge->merges, $preMergeTimestamp, $familyCount, $mergeRows, $seenRevids); $this->recordFamilyMembers($baseData['wives'], 'wife', 0, $userMerge->merges, $preMergeTimestamp, $familyCount, $mergeRows, $seenRevids); $this->recordFamilyMembers($baseData['children'], 'child', 0, $userMerge->merges, $preMergeTimestamp, $familyCount, $mergeRows, $seenRevids); for ($p = 0; $p < count($data); $p++) { $this->recordFamilyMembers($data[$p]['husbands'], 'husband', $p + 1, $userMerge->merges, $preMergeTimestamp, $familyCount, $mergeRows, $seenRevids); $this->recordFamilyMembers($data[$p]['wives'], 'wife', $p + 1, $userMerge->merges, $preMergeTimestamp, $familyCount, $mergeRows, $seenRevids); $this->recordFamilyMembers($data[$p]['children'], 'child', $p + 1, $userMerge->merges, $preMergeTimestamp, $familyCount, $mergeRows, $seenRevids); } } if (!$foundFamilyTitle || $ns != NS_PERSON) { // handle people in families above $mergeRows[$m]->revids[0] = array(); $mergeRows[$m]->revids[0][] = $merge->target->preRevid; for ($p = 0; $p < count($merge->sources); $p++) { $mergeRows[$m]->revids[$p + 1] = array(); $mergeRows[$m]->revids[$p + 1][] = $merge->sources[$p]->preRevid; } if ($ns == NS_PERSON_TALK || $ns == NS_FAMILY_TALK) { $mergeRows[$m]->role = 'talk'; } else { if ($ns == NS_PERSON) { $mergeRows[$m]->role = 'Person'; } else { if ($ns == NS_FAMILY) { $mergeRows[$m]->role = 'Family'; } } } } if ($ns == NS_FAMILY || $ns == NS_PERSON && !$foundFamilyTitle) { $mainTitle = $merge->target->title; } } if ($foundFamilyTitle && $mainTitle->getPrefixedText() != $foundFamilyTitle->getPrefixedText()) { return "family titles not equal: {$mainTitle->getPrefixedText()} and {$foundFamilyTitle->getPrefixedText()}"; } if ($foundFamilyTitle) { // verify that all merging people have been recorded in mergeRows somewhere foreach ($userMerge->merges as &$merge) { if ($merge->target->title->getNamespace() == NS_PERSON) { if (!@$seenRevids[$merge->target->preRevid]) { return "target person not found: {$merge->target->title->getPrefixedText()}"; } foreach ($merge->sources as &$source) { if (!@$seenRevids[$source->preRevid]) { return "source person not found: {$source->title->getPrefixedText()}"; } } } } } // write mergelog record $mergeScore = $totalCount > 0 ? $totalScore / $totalCount : 0; $isTrustedUser = CompareForm::isTrustedMerger(User::newFromName(User::whois($userid))); $isTrustedMerge = MergeForm::isTrustedMerge($mergeScore, $isTrustedUser); $mergeId = $this->writeMergelog($userid, $userMerge, $mergeScore, $isTrustedMerge, $mainTitle, $mergeRows); // update post-merge comments with ml_id $this->updateMergeComments($userMerge, $mainTitle, $mergeId); $this->log("INFO", "writeMerge id={$mergeId} score={$mergeScore} isTrusted={$isTrustedMerge} merges={$mergeCount} title={$mainTitle->getPrefixedText()}", $userid, $userMerge->firstTimestamp); return ''; }