2016-10-13 08:14:18 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
*
|
|
|
|
* @file
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace MediaWiki\Linter;
|
|
|
|
|
|
|
|
use FormatJson;
|
2017-11-23 07:05:04 +00:00
|
|
|
use MediaWiki\Logger\LoggerFactory;
|
2016-10-13 08:14:18 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Database logic
|
|
|
|
*/
|
2016-10-29 01:06:08 +00:00
|
|
|
class Database {
|
2016-11-24 03:47:04 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Maximum number of errors to save per category,
|
|
|
|
* for a page, the rest are just dropped
|
|
|
|
*/
|
2020-09-19 17:06:39 +00:00
|
|
|
public const MAX_PER_CAT = 20;
|
2021-04-07 19:33:16 +00:00
|
|
|
public const MAX_ACCURATE_COUNT = 20;
|
2016-11-24 03:47:04 +00:00
|
|
|
|
2016-10-13 08:14:18 +00:00
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $pageId;
|
|
|
|
|
2016-12-08 07:41:48 +00:00
|
|
|
/**
|
|
|
|
* @var CategoryManager
|
|
|
|
*/
|
|
|
|
private $categoryManager;
|
|
|
|
|
2016-10-13 08:14:18 +00:00
|
|
|
/**
|
|
|
|
* @param int $pageId
|
|
|
|
*/
|
|
|
|
public function __construct( $pageId ) {
|
|
|
|
$this->pageId = $pageId;
|
2016-12-08 07:41:48 +00:00
|
|
|
$this->categoryManager = new CategoryManager();
|
2016-10-13 08:14:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a specific LintError by id
|
|
|
|
*
|
|
|
|
* @param int $id linter_id
|
|
|
|
* @return bool|LintError
|
|
|
|
*/
|
2016-10-29 01:06:08 +00:00
|
|
|
public function getFromId( $id ) {
|
|
|
|
$row = wfGetDB( DB_REPLICA )->selectRow(
|
2016-10-13 08:14:18 +00:00
|
|
|
'linter',
|
2016-12-01 02:47:05 +00:00
|
|
|
[ 'linter_cat', 'linter_params', 'linter_start', 'linter_end' ],
|
2016-10-13 08:14:18 +00:00
|
|
|
[ 'linter_id' => $id, 'linter_page' => $this->pageId ],
|
2016-10-29 01:06:08 +00:00
|
|
|
__METHOD__
|
2016-10-13 08:14:18 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if ( $row ) {
|
|
|
|
$row->linter_id = $id;
|
|
|
|
return $this->makeLintError( $row );
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Turn a database row into a LintError object
|
|
|
|
*
|
|
|
|
* @param \stdClass $row
|
2017-10-31 17:42:07 +00:00
|
|
|
* @return LintError|bool false on error
|
2016-10-13 08:14:18 +00:00
|
|
|
*/
|
|
|
|
public static function makeLintError( $row ) {
|
2017-10-31 17:42:07 +00:00
|
|
|
try {
|
|
|
|
$name = ( new CategoryManager() )->getCategoryName( $row->linter_cat );
|
|
|
|
} catch ( MissingCategoryException $e ) {
|
2017-11-23 07:05:04 +00:00
|
|
|
LoggerFactory::getInstance( 'Linter' )->error(
|
|
|
|
'Could not find name for id: {linter_cat}',
|
|
|
|
[ 'linter_cat' => $row->linter_cat ]
|
|
|
|
);
|
2017-10-31 17:42:07 +00:00
|
|
|
return false;
|
|
|
|
}
|
2016-10-13 08:14:18 +00:00
|
|
|
return new LintError(
|
2017-10-31 17:42:07 +00:00
|
|
|
$name,
|
2016-12-01 02:47:05 +00:00
|
|
|
[ (int)$row->linter_start, (int)$row->linter_end ],
|
2016-10-13 08:14:18 +00:00
|
|
|
$row->linter_params,
|
2017-12-07 22:30:14 +00:00
|
|
|
$row->linter_cat,
|
2016-10-13 08:14:18 +00:00
|
|
|
(int)$row->linter_id
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all the lint errors for a page
|
|
|
|
*
|
|
|
|
* @return LintError[]
|
|
|
|
*/
|
2016-10-29 01:06:08 +00:00
|
|
|
public function getForPage() {
|
|
|
|
$rows = wfGetDB( DB_REPLICA )->select(
|
2016-10-13 08:14:18 +00:00
|
|
|
'linter',
|
2016-12-01 02:47:05 +00:00
|
|
|
[
|
|
|
|
'linter_id', 'linter_cat', 'linter_start',
|
|
|
|
'linter_end', 'linter_params'
|
|
|
|
],
|
2016-10-13 08:14:18 +00:00
|
|
|
[ 'linter_page' => $this->pageId ],
|
2016-10-29 01:06:08 +00:00
|
|
|
__METHOD__
|
2016-10-13 08:14:18 +00:00
|
|
|
);
|
|
|
|
$result = [];
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
$error = $this->makeLintError( $row );
|
2017-10-31 17:42:07 +00:00
|
|
|
if ( !$error ) {
|
|
|
|
continue;
|
|
|
|
}
|
2016-10-13 08:14:18 +00:00
|
|
|
$result[$error->id()] = $error;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert a LintError object into an array for
|
|
|
|
* inserting/querying in the database
|
|
|
|
*
|
|
|
|
* @param LintError $error
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function serializeError( LintError $error ) {
|
2016-10-29 01:06:08 +00:00
|
|
|
return [
|
|
|
|
'linter_page' => $this->pageId,
|
2017-12-07 22:30:14 +00:00
|
|
|
'linter_cat' => $this->categoryManager->getCategoryId( $error->category, $error->catId ),
|
2016-10-29 01:06:08 +00:00
|
|
|
'linter_params' => FormatJson::encode( $error->params, false, FormatJson::ALL_OK ),
|
2016-12-01 02:47:05 +00:00
|
|
|
'linter_start' => $error->location[0],
|
|
|
|
'linter_end' => $error->location[1],
|
2016-10-29 01:06:08 +00:00
|
|
|
];
|
2016-10-13 08:14:18 +00:00
|
|
|
}
|
|
|
|
|
2017-04-28 20:04:45 +00:00
|
|
|
/**
|
|
|
|
* @param LintError[] $errors
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function countByCat( array $errors ) {
|
|
|
|
$count = [];
|
|
|
|
foreach ( $errors as $error ) {
|
|
|
|
if ( !isset( $count[$error->category] ) ) {
|
|
|
|
$count[$error->category] = 1;
|
|
|
|
} else {
|
|
|
|
$count[$error->category] += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $count;
|
|
|
|
}
|
|
|
|
|
2016-10-13 08:14:18 +00:00
|
|
|
/**
|
|
|
|
* Save the specified lint errors in the
|
|
|
|
* database
|
|
|
|
*
|
|
|
|
* @param LintError[] $errors
|
2017-04-28 20:04:45 +00:00
|
|
|
* @return array [ 'deleted' => [ cat => count ], 'added' => [ cat => count ] ]
|
2016-10-13 08:14:18 +00:00
|
|
|
*/
|
|
|
|
public function setForPage( $errors ) {
|
2016-10-29 01:06:08 +00:00
|
|
|
$previous = $this->getForPage();
|
2021-05-04 07:58:08 +00:00
|
|
|
$dbw = wfGetDB( DB_PRIMARY );
|
2016-10-13 08:14:18 +00:00
|
|
|
if ( !$previous && !$errors ) {
|
2017-04-28 20:04:45 +00:00
|
|
|
return [ 'deleted' => [], 'added' => [] ];
|
2016-10-13 08:14:18 +00:00
|
|
|
} elseif ( !$previous && $errors ) {
|
2016-12-01 02:17:36 +00:00
|
|
|
$toInsert = array_values( $errors );
|
2016-10-13 08:14:18 +00:00
|
|
|
$toDelete = [];
|
|
|
|
} elseif ( $previous && !$errors ) {
|
|
|
|
$dbw->delete(
|
|
|
|
'linter',
|
|
|
|
[ 'linter_page' => $this->pageId ],
|
|
|
|
__METHOD__
|
|
|
|
);
|
2017-04-28 20:04:45 +00:00
|
|
|
return [ 'deleted' => $this->countByCat( $previous ), 'added' => [] ];
|
2016-10-13 08:14:18 +00:00
|
|
|
} else {
|
|
|
|
$toInsert = [];
|
|
|
|
$toDelete = $previous;
|
|
|
|
// Diff previous and errors
|
|
|
|
foreach ( $errors as $error ) {
|
|
|
|
$uniqueId = $error->id();
|
|
|
|
if ( isset( $previous[$uniqueId] ) ) {
|
|
|
|
unset( $toDelete[$uniqueId] );
|
|
|
|
} else {
|
|
|
|
$toInsert[] = $error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $toDelete ) {
|
|
|
|
$ids = [];
|
|
|
|
foreach ( $toDelete as $lintError ) {
|
|
|
|
if ( $lintError->lintId ) {
|
|
|
|
$ids[] = $lintError->lintId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$dbw->delete(
|
|
|
|
'linter',
|
|
|
|
[ 'linter_id' => $ids ],
|
|
|
|
__METHOD__
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $toInsert ) {
|
2016-12-02 23:54:02 +00:00
|
|
|
// Insert into db, ignoring any duplicate key errors
|
|
|
|
// since they're the same lint error
|
2016-10-13 08:14:18 +00:00
|
|
|
$dbw->insert(
|
|
|
|
'linter',
|
|
|
|
array_map( [ $this, 'serializeError' ], $toInsert ),
|
2016-12-02 23:54:02 +00:00
|
|
|
__METHOD__,
|
|
|
|
[ 'IGNORE' ]
|
2016-10-13 08:14:18 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return [
|
2017-04-28 20:04:45 +00:00
|
|
|
'deleted' => $this->countByCat( $toDelete ),
|
|
|
|
'added' => $this->countByCat( $toInsert ),
|
2016-10-13 08:14:18 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2016-12-08 07:41:48 +00:00
|
|
|
/**
|
|
|
|
* @return int[]
|
|
|
|
*/
|
2016-12-15 03:58:18 +00:00
|
|
|
public function getTotalsForPage() {
|
2018-02-01 20:59:08 +00:00
|
|
|
return $this->getTotalsAccurate( [ 'linter_page' => $this->pageId ] );
|
2016-12-15 03:58:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-02-01 20:59:08 +00:00
|
|
|
* Get an estimate of how many rows are there for the
|
|
|
|
* specified category with EXPLAIN SELECT COUNT(*).
|
|
|
|
* If the category actually has no rows, then 0 will
|
|
|
|
* be returned.
|
|
|
|
*
|
|
|
|
* @param int $catId
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
private function getTotalsEstimate( $catId ) {
|
|
|
|
$dbr = wfGetDB( DB_REPLICA );
|
2021-04-07 19:33:16 +00:00
|
|
|
// First see if there are no rows, or a moderate number
|
|
|
|
// within the limit specified by the MAX_ACCURATE_COUNT.
|
|
|
|
// The distinction between 0, a few and a lot is important
|
|
|
|
// to determine first, as estimateRowCount seem to never
|
|
|
|
// return 0 or accurate low error counts.
|
2018-02-01 20:59:08 +00:00
|
|
|
$rows = $dbr->selectRowCount(
|
|
|
|
'linter',
|
|
|
|
'*',
|
|
|
|
[ 'linter_cat' => $catId ],
|
|
|
|
__METHOD__,
|
2021-08-06 19:32:07 +00:00
|
|
|
// Select 1 more so we can see if we're over the max limit
|
|
|
|
[ 'LIMIT' => self::MAX_ACCURATE_COUNT + 1 ]
|
2018-02-01 20:59:08 +00:00
|
|
|
);
|
2021-04-07 19:33:16 +00:00
|
|
|
// Return an accurate count if the number of errors is
|
|
|
|
// below the maximum accurate count limit
|
2021-08-06 19:32:07 +00:00
|
|
|
if ( $rows <= self::MAX_ACCURATE_COUNT ) {
|
2021-04-07 19:33:16 +00:00
|
|
|
return $rows;
|
2018-02-01 20:59:08 +00:00
|
|
|
}
|
2021-04-07 19:33:16 +00:00
|
|
|
// Now we can just estimate if the maximum accurate count limit
|
|
|
|
// was returned, which isn't the actual count but the limit reached
|
2018-02-01 20:59:08 +00:00
|
|
|
return $dbr->estimateRowCount(
|
|
|
|
'linter',
|
|
|
|
'*',
|
|
|
|
[ 'linter_cat' => $catId ],
|
|
|
|
__METHOD__
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This uses COUNT(*), which is accurate, but can be significantly
|
|
|
|
* slower depending upon how many rows are in the database.
|
|
|
|
*
|
|
|
|
* @param array $conds
|
|
|
|
*
|
2016-12-15 03:58:18 +00:00
|
|
|
* @return int[]
|
|
|
|
*/
|
2018-02-01 20:59:08 +00:00
|
|
|
private function getTotalsAccurate( $conds = [] ) {
|
2017-09-24 03:51:18 +00:00
|
|
|
$rows = wfGetDB( DB_REPLICA )->select(
|
2016-12-08 07:41:48 +00:00
|
|
|
'linter',
|
|
|
|
[ 'linter_cat', 'COUNT(*) AS count' ],
|
2016-12-15 03:58:18 +00:00
|
|
|
$conds,
|
2016-12-08 07:41:48 +00:00
|
|
|
__METHOD__,
|
|
|
|
[ 'GROUP BY' => 'linter_cat' ]
|
|
|
|
);
|
|
|
|
|
|
|
|
// Initialize zero values
|
|
|
|
$ret = array_fill_keys( $this->categoryManager->getVisibleCategories(), 0 );
|
|
|
|
foreach ( $rows as $row ) {
|
2017-10-31 17:42:07 +00:00
|
|
|
try {
|
|
|
|
$catName = $this->categoryManager->getCategoryName( $row->linter_cat );
|
|
|
|
} catch ( MissingCategoryException $e ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$ret[$catName] = (int)$row->count;
|
2016-12-08 07:41:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
2018-02-01 20:59:08 +00:00
|
|
|
/**
|
|
|
|
* @return int[]
|
|
|
|
*/
|
|
|
|
public function getTotals() {
|
|
|
|
$ret = [];
|
|
|
|
foreach ( $this->categoryManager->getVisibleCategories() as $cat ) {
|
|
|
|
$id = $this->categoryManager->getCategoryId( $cat );
|
|
|
|
$ret[$cat] = $this->getTotalsEstimate( $id );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
2016-10-13 08:14:18 +00:00
|
|
|
}
|