public function unmerge() { global $wgUser; $nonFamilyPages = array(); // contains revid, next_revid, title $familyPages = array(); // ditto $manualPages = array(); // ditto $unchangedPages = array(); // ditto $dbw =& wfGetDB(DB_MASTER); // break into different arrays $seenTitles = array(); foreach ($this->merges as $merge) { $fields = explode('|', $merge); $role = $fields[0]; $revidSets = explode('#', $fields[1]); foreach ($revidSets as $revidSet) { if ($revidSet) { $revids = explode('/', $revidSet); foreach ($revids as $revid) { if ($revid) { // get two following revisions $rows = $dbw->query('SELECT r2.rev_page, r2.rev_id, r2.rev_comment FROM revision AS r1, revision AS r2' . ' WHERE r1.rev_id = ' . $revid . ' AND r1.rev_page = r2.rev_page AND r2.rev_id > ' . $revid . ' ORDER BY r2.rev_id LIMIT 2'); $cnt = 0; $cmt = ''; $nextRevid = ''; $pageId = ''; while ($row = $dbw->fetchObject($rows)) { $cnt++; if ($cnt == 1) { $pageId = $row->rev_page; $nextRevid = $row->rev_id; $cmt = $row->rev_comment; } } $dbw->freeResult($rows); if ($cnt == 0) { $revision = Revision::newFromId($revid); if ($revision) { $title = $revision->getTitle(); } else { // page must have been deleted $title = null; } } else { $title = Title::newFromId($pageId); } // TODO if title doesn't exist, then unmerged pages could have red links; oh well if ($title && !@$seenTitles[$title->getPrefixedText()]) { $seenTitles[$title->getPrefixedText()] = 1; $entry = array('revid' => $revid, 'next_revid' => $nextRevid, 'title' => $title); if ($cnt == 0 || strpos($cmt, '[[Special:ReviewMerge/' . $this->mergeId . '|') === false) { $unchangedPages[] = $entry; // page was not edited in the merge } else { if ($cnt > 1) { $manualPages[] = $entry; } else { if ($role == 'Family') { PropagationManager::addBlacklistPage($title); $familyPages[] = $entry; } else { PropagationManager::addBlacklistPage($title); $nonFamilyPages[] = $entry; } } } } } } } } } // update nonFamilyPages foreach ($nonFamilyPages as $page) { $this->revert($page, false); } // update familyPages foreach ($familyPages as $page) { $this->revert($page, true); } // update mergelog $dbw->update('mergelog', array('ml_unmerge_user' => $wgUser->getID(), 'ml_unmerge_timestamp' => $dbw->timestamp(wfTimestampNow())), array('ml_id' => $this->mergeId), 'ReviewForm::unmerge'); // add log and RC $mergeComment = 'Unmerge [[' . $this->mergeTitle->getPrefixedText() . ']]' . ($this->comment ? ' - ' . $this->comment : ''); $log = new LogPage('merge', false); $t = Title::makeTitle(NS_SPECIAL, "ReviewMerge/{$this->mergeId}"); $log->addEntry('unmerge', $t, $mergeComment); RecentChange::notifyLog(wfTimestampNow(), $t, $wgUser, $mergeComment, '', 'merge', 'unmerge', $t->getPrefixedText(), $mergeComment, '', 0, 0); // list pages $output = ''; $skin =& $wgUser->getSkin(); if (count($manualPages) == 0) { $output .= "<h2>Unmerge successful</h2>\n"; } else { $output .= "<h2>Unmerge partially completed</h2><p><b><font color=\"red\">The following page(s) must still be unmerged</font></b></p><ul>"; foreach ($manualPages as $page) { $output .= "<li>" . htmlspecialchars($page['title']->getPrefixedText()) . ' <b>' . $skin->makeKnownLinkObj($page['title'], 'changes to undo', 'diff=' . $page['next_revid'] . '&oldid=' . $page['revid']) . '</b> => <b>' . $skin->makeKnownLinkObj($page['title'], 'edit', 'action=edit') . "</b></li>\n"; error_log("Unmerge may be needed: {$page['title']->getPrefixedText()} id={$this->mergeId}"); } $output .= <<<END </ul> <p>For each page listed above, click on the <b>changes to undo</b> link to see what changes need to be manually undone: <ol> <li>For each item that was removed in the merge (listed on the left side of the screen in yellow), you need to add that item back to the page.</li> <li>For each item that was added in the merge (listed on the right side of the screen in green), you need to remove that item from the page.</li> </ol></p> <p>Once you know which items need to be added/removed, click on the <b>edit</b> link to edit the page and add/remove the items.</p> <p>Additional notes: <ul> <li>Some of the items may have been added/removed already.</li> <li>You can ignore place changes that simply fill in missing place levels.</li> <li>When reviewing changes to Family pages, you can ignore changes to personal information (birth, death, etc.) associated with the family members: <i>husband</i>, <i>wife</i>, or <i>child</i>. Just re-add family members that were removed in the merge, and remove ones that were added.</li> </ul></p> <p>If you have questions, send an email to <b>solveig@quass.org</b> containing the titles of the pages listed above and the URL appearing at the top of your browser window and we will finish the unmerge for you.</p> <p> </p> END; } if (count($familyPages) + count($nonFamilyPages) + count($unchangedPages) > 0) { $output .= "<p><b>The following pages have been successfully unmerged:</b></p><ul>\n"; foreach ($familyPages as $page) { $output .= "<li>" . $skin->makeKnownLinkObj($page['title']) . "</li>\n"; } foreach ($nonFamilyPages as $page) { $output .= "<li>" . $skin->makeKnownLinkObj($page['title']) . "</li>\n"; } // $output .= '</ul>'; // } // if (count($unchangedPages) > 0) { // $output .= "<p><b>The following pages were not merged and so did not need to be unmerged:</b></p><ul>\n"; foreach ($unchangedPages as $page) { $output .= "<li>" . $skin->makeKnownLinkObj($page['title']) . "</li>\n"; } $output .= "</ul>"; } return $output; }
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; }
/** * Run a refreshLinks job * @return boolean success */ function run() { global $wgUser, $wgTitle, $wrIsTreeDeletion; $status = FTE_SUCCESS; $user = $this->params['user']; $wgUser = User::newFromName($user); $treeId = $this->params['tree_id']; if ($treeId == 9662) { return false; } $delPages = $this->params['delete_pages'] == 1; $wgTitle = $this->title; // FakeTitle (the default) generates errors when accessed, and sometimes I log wgTitle, so set it to something else $dbw =& wfGetDB(DB_MASTER); $dbw->begin(); $dbw->ignoreErrors(true); if ($delPages) { // Delete the page if it is in this tree // and is in one of the 4 deletable namespaces // and nobody else is watching the page // and it is not in another of the users trees // Keep this query in sync with SpecialTreeDeletionImpact.php $sql = 'SELECT fp_namespace, fp_title FROM familytree_page AS fp1' . ' WHERE fp_tree_id=' . $dbw->addQuotes($treeId) . ' and fp_namespace in (' . NS_IMAGE . ',' . NS_PERSON . ',' . NS_FAMILY . ',' . NS_MYSOURCE . ')' . ' and not exists (SELECT 1 FROM watchlist WHERE wl_namespace = fp_namespace and wl_title = fp_title and wl_user <> fp_user_id)' . ' and not exists (SELECT 1 FROM familytree_page AS fp2 WHERE fp2.fp_namespace = fp1.fp_namespace and fp2.fp_title = fp1.fp_title and fp2.fp_user_id = fp1.fp_user_id and fp2.fp_tree_id <> fp1.fp_tree_id)'; $rows = $dbw->query($sql); $errno = $dbw->lastErrno(); if ($errno > 0) { $status = FTE_DB_ERROR; } else { if ($rows !== false) { $titles = array(); while ($row = $dbw->fetchObject($rows)) { $title = Title::makeTitle($row->fp_namespace, $row->fp_title); $talkTitle = $title->getTalkPage(); if ($title->exists()) { $titles[] = $title; PropagationManager::addBlacklistPage($title); } if ($talkTitle->exists()) { $titles[] = $talkTitle; } } $dbw->freeResult($rows); $wrIsTreeDeletion = true; foreach ($titles as $title) { $status = ftDelPage($title, false); if ($status == FTE_NOT_AUTHORIZED) { $this->error = "While deleting {$treeId} tried to delete a page not authorized to delete: " . $title->getPrefixedText() . "\n"; } if ($status != FTE_SUCCESS) { break; } } } } // remove remaining pages from watchlist if ($status == FTE_SUCCESS) { $sql = 'SELECT fp_namespace, fp_title FROM familytree_page AS fp1' . ' WHERE fp_tree_id=' . $dbw->addQuotes($treeId) . ' and not exists (SELECT 1 FROM familytree_page AS fp2 WHERE fp2.fp_namespace = fp1.fp_namespace and fp2.fp_title = fp1.fp_title and fp2.fp_user_id = fp1.fp_user_id and fp2.fp_tree_id <> fp1.fp_tree_id)'; $rows = $dbw->query($sql); $errno = $dbw->lastErrno(); if ($errno > 0) { $status = FTE_DB_ERROR; } else { if ($rows !== false) { while ($row = $dbw->fetchObject($rows)) { $title = Title::makeTitle($row->fp_namespace, $row->fp_title); $wgUser->removeWatch($title); } $dbw->freeResult($rows); } } } // remove familytree_page's // If we delete pages that are unwatched by others but in someone else's tree, then this code won't delete them from the others' trees // We need to ensure that all pages in trees are watched. if ($status == FTE_SUCCESS) { $dbw->delete('familytree_page', array('fp_tree_id' => $treeId)); $errno = $dbw->lastErrno(); if ($errno > 0) { $status = FTE_DB_ERROR; } } if ($status == FTE_SUCCESS) { // remove familytree_data's $dbw->delete('familytree_data', array('fd_tree_id' => $treeId)); $errno = $dbw->lastErrno(); if ($errno > 0) { $status = FTE_DB_ERROR; } // keep familytree_gedcom in case we want to look at it later } if ($status == FTE_SUCCESS) { $dbw->commit(); return true; } else { $dbw->rollback(); if (!$this->error) { $this->error = "Error deleting tree; status={$status}\n"; } return false; } } }