Index: trunk/phase3/maintenance/edit.php |
— | — | @@ -58,15 +58,20 @@ |
59 | 59 | |
60 | 60 | # Do the edit |
61 | 61 | print "Saving... "; |
62 | | -$success = $wgArticle->doEdit( $text, $summary, |
| 62 | +$status = $wgArticle->doEdit( $text, $summary, |
63 | 63 | ( $minor ? EDIT_MINOR : 0 ) | |
64 | 64 | ( $bot ? EDIT_FORCE_BOT : 0 ) | |
65 | 65 | ( $autoSummary ? EDIT_AUTOSUMMARY : 0 ) | |
66 | 66 | ( $noRC ? EDIT_SUPPRESS_RC : 0 ) ); |
67 | | -if ( $success ) { |
| 67 | +if ( $status->isOK() ) { |
68 | 68 | print "done\n"; |
| 69 | + $exit = 0; |
69 | 70 | } else { |
70 | 71 | print "failed\n"; |
71 | | - exit( 1 ); |
| 72 | + $exit = 1; |
72 | 73 | } |
| 74 | +if ( !$status->isGood() ) { |
| 75 | + print $status->getWikiText() . "\n"; |
| 76 | +} |
| 77 | +exit( $exit ); |
73 | 78 | |
Index: trunk/phase3/includes/Article.php |
— | — | @@ -1125,7 +1125,7 @@ |
1126 | 1126 | * Best if all done inside a transaction. |
1127 | 1127 | * |
1128 | 1128 | * @param Database $dbw |
1129 | | - * @return int The newly created page_id key |
| 1129 | + * @return int The newly created page_id key, or false if the title already existed |
1130 | 1130 | * @private |
1131 | 1131 | */ |
1132 | 1132 | function insertOn( $dbw ) { |
— | — | @@ -1144,13 +1144,15 @@ |
1145 | 1145 | 'page_touched' => $dbw->timestamp(), |
1146 | 1146 | 'page_latest' => 0, # Fill this in shortly... |
1147 | 1147 | 'page_len' => 0, # Fill this in shortly... |
1148 | | - ), __METHOD__ ); |
1149 | | - $newid = $dbw->insertId(); |
| 1148 | + ), __METHOD__, 'IGNORE' ); |
1150 | 1149 | |
1151 | | - $this->mTitle->resetArticleId( $newid ); |
1152 | | - |
| 1150 | + $affected = $dbw->affectedRows(); |
| 1151 | + if ( $affected ) { |
| 1152 | + $newid = $dbw->insertId(); |
| 1153 | + $this->mTitle->resetArticleId( $newid ); |
| 1154 | + } |
1153 | 1155 | wfProfileOut( __METHOD__ ); |
1154 | | - return $newid; |
| 1156 | + return $affected ? $newid : false; |
1155 | 1157 | } |
1156 | 1158 | |
1157 | 1159 | /** |
— | — | @@ -1363,29 +1365,31 @@ |
1364 | 1366 | ( $minor ? EDIT_MINOR : 0 ) | |
1365 | 1367 | ( $forceBot ? EDIT_FORCE_BOT : 0 ); |
1366 | 1368 | |
1367 | | - $good = $this->doEdit( $text, $summary, $flags ); |
1368 | | - if ( $good ) { |
1369 | | - $dbw = wfGetDB( DB_MASTER ); |
1370 | | - if ($watchthis) { |
1371 | | - if (!$this->mTitle->userIsWatching()) { |
1372 | | - $dbw->begin(); |
1373 | | - $this->doWatch(); |
1374 | | - $dbw->commit(); |
1375 | | - } |
1376 | | - } else { |
1377 | | - if ( $this->mTitle->userIsWatching() ) { |
1378 | | - $dbw->begin(); |
1379 | | - $this->doUnwatch(); |
1380 | | - $dbw->commit(); |
1381 | | - } |
| 1369 | + $status = $this->doEdit( $text, $summary, $flags ); |
| 1370 | + if ( !$status->isOK() ) { |
| 1371 | + return false; |
| 1372 | + } |
| 1373 | + |
| 1374 | + $dbw = wfGetDB( DB_MASTER ); |
| 1375 | + if ($watchthis) { |
| 1376 | + if (!$this->mTitle->userIsWatching()) { |
| 1377 | + $dbw->begin(); |
| 1378 | + $this->doWatch(); |
| 1379 | + $dbw->commit(); |
1382 | 1380 | } |
| 1381 | + } else { |
| 1382 | + if ( $this->mTitle->userIsWatching() ) { |
| 1383 | + $dbw->begin(); |
| 1384 | + $this->doUnwatch(); |
| 1385 | + $dbw->commit(); |
| 1386 | + } |
| 1387 | + } |
1383 | 1388 | |
1384 | | - $extraQuery = ''; // Give extensions a chance to modify URL query on update |
1385 | | - wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) ); |
| 1389 | + $extraQuery = ''; // Give extensions a chance to modify URL query on update |
| 1390 | + wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) ); |
1386 | 1391 | |
1387 | | - $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery ); |
1388 | | - } |
1389 | | - return $good; |
| 1392 | + $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery ); |
| 1393 | + return true; |
1390 | 1394 | } |
1391 | 1395 | |
1392 | 1396 | /** |
— | — | @@ -1415,13 +1419,27 @@ |
1416 | 1420 | * Fill in blank summaries with generated text where possible |
1417 | 1421 | * |
1418 | 1422 | * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected. |
1419 | | - * If EDIT_UPDATE is specified and the article doesn't exist, the function will return false. If |
1420 | | - * EDIT_NEW is specified and the article does exist, a duplicate key error will cause an exception |
1421 | | - * to be thrown from the Database. These two conditions are also possible with auto-detection due |
1422 | | - * to MediaWiki's performance-optimised locking strategy. |
| 1423 | + * If EDIT_UPDATE is specified and the article doesn't exist, the function will an |
| 1424 | + * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an |
| 1425 | + * edit-already-exists error will be returned. These two conditions are also possible with |
| 1426 | + * auto-detection due to MediaWiki's performance-optimised locking strategy. |
| 1427 | + * |
1423 | 1428 | * @param $baseRevId, the revision ID this edit was based off, if any |
1424 | 1429 | * |
1425 | | - * @return int Current revision ID after this edit |
| 1430 | + * @return Status object. Possible errors: |
| 1431 | + * edit-hook-aborted: The ArticleSave hook aborted the edit but didn't set the fatal flag of $status |
| 1432 | + * edit-gone-missing: In update mode, but the article didn't exist |
| 1433 | + * edit-conflict: In update mode, the article changed unexpectedly |
| 1434 | + * edit-no-change: Warning that the text was the same as before |
| 1435 | + * edit-already-exists: In creation mode, but the article already exists |
| 1436 | + * |
| 1437 | + * Extensions may define additional errors. |
| 1438 | + * |
| 1439 | + * $return->value will contain an associative array with members as follows: |
| 1440 | + * new: Boolean indicating if the function attempted to create a new article |
| 1441 | + * revision: The revision object for the inserted revision, or null |
| 1442 | + * |
| 1443 | + * Compatibility note: this function previously returned a boolean value indicating success/failure |
1426 | 1444 | */ |
1427 | 1445 | function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { |
1428 | 1446 | global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries; |
— | — | @@ -1436,10 +1454,13 @@ |
1437 | 1455 | if ($user == null) { |
1438 | 1456 | $user = $wgUser; |
1439 | 1457 | } |
1440 | | - $good = true; |
| 1458 | + $status = Status::newGood( array() ); |
1441 | 1459 | |
| 1460 | + # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already |
| 1461 | + $this->loadPageData(); |
| 1462 | + |
1442 | 1463 | if ( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) { |
1443 | | - $aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE ); |
| 1464 | + $aid = $this->mTitle->getArticleID(); |
1444 | 1465 | if ( $aid ) { |
1445 | 1466 | $flags |= EDIT_UPDATE; |
1446 | 1467 | } else { |
— | — | @@ -1449,11 +1470,14 @@ |
1450 | 1471 | |
1451 | 1472 | if( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, |
1452 | 1473 | &$summary, $flags & EDIT_MINOR, |
1453 | | - null, null, &$flags ) ) ) |
| 1474 | + null, null, &$flags, &$status ) ) ) |
1454 | 1475 | { |
1455 | 1476 | wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" ); |
1456 | 1477 | wfProfileOut( __METHOD__ ); |
1457 | | - return false; |
| 1478 | + if ( $status->isOK() ) { |
| 1479 | + $status->fatal( 'edit-hook-aborted'); |
| 1480 | + } |
| 1481 | + return $status; |
1458 | 1482 | } |
1459 | 1483 | |
1460 | 1484 | # Silently ignore EDIT_MINOR if not allowed |
— | — | @@ -1477,13 +1501,13 @@ |
1478 | 1502 | |
1479 | 1503 | if ( $flags & EDIT_UPDATE ) { |
1480 | 1504 | # Update article, but only if changed. |
| 1505 | + $status->value['new'] = false; |
1481 | 1506 | |
1482 | 1507 | # Make sure the revision is either completely inserted or not inserted at all |
1483 | 1508 | if( !$wgDBtransactions ) { |
1484 | 1509 | $userAbort = ignore_user_abort( true ); |
1485 | 1510 | } |
1486 | 1511 | |
1487 | | - $lastRevision = 0; |
1488 | 1512 | $revisionId = 0; |
1489 | 1513 | |
1490 | 1514 | $changed = ( strcmp( $text, $oldtext ) != 0 ); |
— | — | @@ -1493,14 +1517,12 @@ |
1494 | 1518 | - (int)$this->isCountable( $oldtext ); |
1495 | 1519 | $this->mTotalAdjustment = 0; |
1496 | 1520 | |
1497 | | - $lastRevision = $dbw->selectField( |
1498 | | - 'page', 'page_latest', array( 'page_id' => $this->getId() ) ); |
1499 | | - |
1500 | | - if ( !$lastRevision ) { |
| 1521 | + if ( !$this->mLatest ) { |
1501 | 1522 | # Article gone missing |
1502 | 1523 | wfDebug( __METHOD__.": EDIT_UPDATE specified but article doesn't exist\n" ); |
| 1524 | + $status->fatal( 'edit-gone-missing' ); |
1503 | 1525 | wfProfileOut( __METHOD__ ); |
1504 | | - return false; |
| 1526 | + return $status; |
1505 | 1527 | } |
1506 | 1528 | |
1507 | 1529 | $revision = new Revision( array( |
— | — | @@ -1508,7 +1530,7 @@ |
1509 | 1531 | 'comment' => $summary, |
1510 | 1532 | 'minor_edit' => $isminor, |
1511 | 1533 | 'text' => $text, |
1512 | | - 'parent_id' => $lastRevision, |
| 1534 | + 'parent_id' => $this->mLatest, |
1513 | 1535 | 'user' => $user->getId(), |
1514 | 1536 | 'user_text' => $user->getName(), |
1515 | 1537 | ) ); |
— | — | @@ -1517,11 +1539,21 @@ |
1518 | 1540 | $revisionId = $revision->insertOn( $dbw ); |
1519 | 1541 | |
1520 | 1542 | # Update page |
1521 | | - $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision ); |
| 1543 | + # |
| 1544 | + # Note that we use $this->mLatest instead of fetching a value from the master DB |
| 1545 | + # during the course of this function. This makes sure that EditPage can detect |
| 1546 | + # edit conflicts reliably, either by $ok here, or by $article->getTimestamp() |
| 1547 | + # before this function is called. A previous function used a separate query, this |
| 1548 | + # creates a window where concurrent edits can cause an ignored edit conflict. |
| 1549 | + $ok = $this->updateRevisionOn( $dbw, $revision, $this->mLatest ); |
1522 | 1550 | |
1523 | 1551 | if( !$ok ) { |
1524 | 1552 | /* Belated edit conflict! Run away!! */ |
1525 | | - $good = false; |
| 1553 | + $status->fatal( 'edit-conflict' ); |
| 1554 | + # Delete the invalid revision if the DB is not transactional |
| 1555 | + if ( !$wgDBtransactions ) { |
| 1556 | + $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ ); |
| 1557 | + } |
1526 | 1558 | $revisionId = 0; |
1527 | 1559 | $dbw->rollback(); |
1528 | 1560 | } else { |
— | — | @@ -1530,7 +1562,7 @@ |
1531 | 1563 | # Update recentchanges |
1532 | 1564 | if( !( $flags & EDIT_SUPPRESS_RC ) ) { |
1533 | 1565 | $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, |
1534 | | - $lastRevision, $this->getTimestamp(), $bot, '', $oldsize, $newsize, |
| 1566 | + $this->mLatest, $this->getTimestamp(), $bot, '', $oldsize, $newsize, |
1535 | 1567 | $revisionId ); |
1536 | 1568 | |
1537 | 1569 | # Mark as patrolled if the user can do so |
— | — | @@ -1542,6 +1574,7 @@ |
1543 | 1575 | $dbw->commit(); |
1544 | 1576 | } |
1545 | 1577 | } else { |
| 1578 | + $status->warning( 'edit-no-change' ); |
1546 | 1579 | $revision = null; |
1547 | 1580 | // Keep the same revision ID, but do some updates on it |
1548 | 1581 | $revisionId = $this->getRevIdFetched(); |
— | — | @@ -1553,16 +1586,20 @@ |
1554 | 1587 | if( !$wgDBtransactions ) { |
1555 | 1588 | ignore_user_abort( $userAbort ); |
1556 | 1589 | } |
| 1590 | + // Now that ignore_user_abort is restored, we can respond to fatal errors |
| 1591 | + if ( !$status->isOK() ) { |
| 1592 | + wfProfileOut( __METHOD__ ); |
| 1593 | + return $status; |
| 1594 | + } |
1557 | 1595 | |
1558 | | - if ( $good ) { |
1559 | | - # Invalidate cache of this article and all pages using this article |
1560 | | - # as a template. Partly deferred. |
1561 | | - Article::onArticleEdit( $this->mTitle, false ); // leave templatelinks for editUpdates() |
1562 | | - # Update links tables, site stats, etc. |
1563 | | - $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); |
1564 | | - } |
| 1596 | + # Invalidate cache of this article and all pages using this article |
| 1597 | + # as a template. Partly deferred. |
| 1598 | + Article::onArticleEdit( $this->mTitle, false ); // leave templatelinks for editUpdates() |
| 1599 | + # Update links tables, site stats, etc. |
| 1600 | + $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); |
1565 | 1601 | } else { |
1566 | 1602 | # Create new article |
| 1603 | + $status->value['new'] = true; |
1567 | 1604 | |
1568 | 1605 | # Set statistics members |
1569 | 1606 | # We work out if it's countable after PST to avoid counter drift |
— | — | @@ -1571,10 +1608,18 @@ |
1572 | 1609 | $this->mTotalAdjustment = 1; |
1573 | 1610 | |
1574 | 1611 | $dbw->begin(); |
| 1612 | + |
1575 | 1613 | # Add the page record; stake our claim on this title! |
1576 | | - # This will fail with a database query exception if the article already exists |
| 1614 | + # This will return false if the article already exists |
1577 | 1615 | $newid = $this->insertOn( $dbw ); |
1578 | 1616 | |
| 1617 | + if ( $newid === false ) { |
| 1618 | + $dbw->rollback(); |
| 1619 | + $status->fatal( 'edit-already-exists' ); |
| 1620 | + wfProfileOut( __METHOD__ ); |
| 1621 | + return $status; |
| 1622 | + } |
| 1623 | + |
1579 | 1624 | # Save the revision text... |
1580 | 1625 | $revision = new Revision( array( |
1581 | 1626 | 'page' => $newid, |
— | — | @@ -1614,17 +1659,19 @@ |
1615 | 1660 | $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); |
1616 | 1661 | } |
1617 | 1662 | |
1618 | | - if ( $good && !( $flags & EDIT_DEFER_UPDATES ) ) { |
| 1663 | + # Do updates right now unless deferral was requested |
| 1664 | + if ( !( $flags & EDIT_DEFER_UPDATES ) ) { |
1619 | 1665 | wfDoUpdates(); |
1620 | 1666 | } |
1621 | 1667 | |
1622 | | - if ( $good ) { |
1623 | | - wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, |
1624 | | - $flags & EDIT_MINOR, null, null, &$flags, $revision ) ); |
1625 | | - } |
| 1668 | + // Return the new revision (or null) to the caller |
| 1669 | + $status->value['revision'] = $revision; |
1626 | 1670 | |
| 1671 | + wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, |
| 1672 | + $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status ) ); |
| 1673 | + |
1627 | 1674 | wfProfileOut( __METHOD__ ); |
1628 | | - return $revisionId; |
| 1675 | + return $status; |
1629 | 1676 | } |
1630 | 1677 | |
1631 | 1678 | /** |
— | — | @@ -2590,7 +2637,12 @@ |
2591 | 2638 | if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) ) |
2592 | 2639 | $flags |= EDIT_FORCE_BOT; |
2593 | 2640 | # Actually store the edit |
2594 | | - $revId = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() ); |
| 2641 | + $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() ); |
| 2642 | + if ( !empty( $status->value['revision'] ) ) { |
| 2643 | + $revId = $status->value['revision']->getId(); |
| 2644 | + } else { |
| 2645 | + $revId = false; |
| 2646 | + } |
2595 | 2647 | |
2596 | 2648 | wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) ); |
2597 | 2649 | |
Index: trunk/phase3/languages/messages/MessagesEn.php |
— | — | @@ -1190,6 +1190,11 @@ |
1191 | 1191 | 'deleted-notice' => 'This page has been deleted. |
1192 | 1192 | The deletion log for the page is provided below for reference.', |
1193 | 1193 | 'deletelog-fulllog' => 'View full log', |
| 1194 | +'edit-hook-aborted' => 'Edit aborted by hook, it gave no explanation.', |
| 1195 | +'edit-gone-missing' => 'Couldn\'t update the article, it appears to have been deleted.', |
| 1196 | +'edit-conflict' => 'Edit conflict.', |
| 1197 | +'edit-no-change' => 'Your edit was ignored, because no change was made to the text.', |
| 1198 | +'edit-already-exists' => 'Couldn\'t create a new article, it already exists.', |
1194 | 1199 | |
1195 | 1200 | # Parser/template warnings |
1196 | 1201 | 'expensive-parserfunction-warning' => 'Warning: This page contains too many expensive parser function calls. |