2019-08-26 13:01:09 +00:00
|
|
|
<?php
|
|
|
|
|
2020-10-16 22:10:37 +00:00
|
|
|
use MediaWiki\Config\ServiceOptions;
|
2020-09-29 14:52:05 +00:00
|
|
|
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
|
2020-12-18 14:05:33 +00:00
|
|
|
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutor;
|
|
|
|
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesLookup;
|
2020-10-11 21:17:41 +00:00
|
|
|
use MediaWiki\Extension\AbuseFilter\Filter\Filter;
|
|
|
|
use MediaWiki\Extension\AbuseFilter\FilterLookup;
|
2021-01-02 13:41:31 +00:00
|
|
|
use MediaWiki\Extension\AbuseFilter\Variables\AbuseFilterVariableHolder;
|
2019-08-26 13:01:09 +00:00
|
|
|
use MediaWiki\Revision\MutableRevisionRecord;
|
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
2020-10-16 22:10:37 +00:00
|
|
|
use Psr\Log\NullLogger;
|
2019-08-26 13:01:09 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generic tests for utility functions in AbuseFilter that require DB access
|
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*
|
|
|
|
* @license GPL-2.0-or-later
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @group Test
|
|
|
|
* @group AbuseFilter
|
|
|
|
* @group AbuseFilterGeneric
|
|
|
|
* @group Database
|
|
|
|
*/
|
|
|
|
class AbuseFilterDBTest extends MediaWikiTestCase {
|
|
|
|
/**
|
|
|
|
* @var array These tables will be deleted in parent::tearDown.
|
|
|
|
* We need it to happen to make tests on fresh pages.
|
|
|
|
*/
|
|
|
|
protected $tablesUsed = [
|
|
|
|
'abuse_filter',
|
|
|
|
'abuse_filter_history',
|
2020-04-06 23:06:03 +00:00
|
|
|
'abuse_filter_log'
|
2019-08-26 13:01:09 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Test storing and loading the var dump. See also AbuseFilterConsequencesTest::testVarDump
|
|
|
|
*
|
2021-01-02 13:41:31 +00:00
|
|
|
* @param array $variables Map of [ name => value ] to build a variable holder with
|
Rewrite the VariableHolder code to translate deprecated variables
The current code was more of a subpar, temporary solution. However, we
need a stable solution in case more variables will be deprecated in the
future (T213006 fixes the problem for the past deprecation round). So,
instead of setting a hacky property, directly translate all variables
when loading the var dump. This is not only stable, but has a couple
micro-performance advantages:
- Calling getDeprecatedVariables happens only once when loading the
dump, and not every time a variable is accessed
- No checks are needed when retrieving a variable,
because names can always assumed to be new
Some simple benchmarks reveals a runtime reduction of 8-15% compared to
the old code (8% when it had varsVersion = 2, 15% for varsVersion = 1),
which comes at no cost together with increased readability and
stability. It ain't much, but it's honest work.
Change-Id: Ib32a92c4ad939790633aa63eb3ef8d4629488bea
2020-09-21 11:54:23 +00:00
|
|
|
* @param ?array $expectedValues Null to use $variables
|
2021-01-02 13:41:31 +00:00
|
|
|
* @covers \MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore
|
|
|
|
* @covers \MediaWiki\Extension\AbuseFilter\Variables\VariablesManager::dumpAllVars
|
2019-08-26 13:01:09 +00:00
|
|
|
* @dataProvider provideVariables
|
|
|
|
*/
|
Rewrite the VariableHolder code to translate deprecated variables
The current code was more of a subpar, temporary solution. However, we
need a stable solution in case more variables will be deprecated in the
future (T213006 fixes the problem for the past deprecation round). So,
instead of setting a hacky property, directly translate all variables
when loading the var dump. This is not only stable, but has a couple
micro-performance advantages:
- Calling getDeprecatedVariables happens only once when loading the
dump, and not every time a variable is accessed
- No checks are needed when retrieving a variable,
because names can always assumed to be new
Some simple benchmarks reveals a runtime reduction of 8-15% compared to
the old code (8% when it had varsVersion = 2, 15% for varsVersion = 1),
which comes at no cost together with increased readability and
stability. It ain't much, but it's honest work.
Change-Id: Ib32a92c4ad939790633aa63eb3ef8d4629488bea
2020-09-21 11:54:23 +00:00
|
|
|
public function testVarDump( array $variables, array $expectedValues = null ) {
|
2020-09-29 14:52:05 +00:00
|
|
|
$varBlobStore = AbuseFilterServices::getVariablesBlobStore();
|
2019-01-06 14:20:10 +00:00
|
|
|
$holder = AbuseFilterVariableHolder::newFromArray( $variables );
|
2019-08-26 13:01:09 +00:00
|
|
|
|
2020-09-29 14:52:05 +00:00
|
|
|
$insertID = $varBlobStore->storeVarDump( $holder );
|
|
|
|
$dump = $varBlobStore->loadVarDump( $insertID );
|
Rewrite the VariableHolder code to translate deprecated variables
The current code was more of a subpar, temporary solution. However, we
need a stable solution in case more variables will be deprecated in the
future (T213006 fixes the problem for the past deprecation round). So,
instead of setting a hacky property, directly translate all variables
when loading the var dump. This is not only stable, but has a couple
micro-performance advantages:
- Calling getDeprecatedVariables happens only once when loading the
dump, and not every time a variable is accessed
- No checks are needed when retrieving a variable,
because names can always assumed to be new
Some simple benchmarks reveals a runtime reduction of 8-15% compared to
the old code (8% when it had varsVersion = 2, 15% for varsVersion = 1),
which comes at no cost together with increased readability and
stability. It ain't much, but it's honest work.
Change-Id: Ib32a92c4ad939790633aa63eb3ef8d4629488bea
2020-09-21 11:54:23 +00:00
|
|
|
$expected = $expectedValues ? AbuseFilterVariableHolder::newFromArray( $expectedValues ) : $holder;
|
|
|
|
$this->assertEquals( $expected, $dump, 'The var dump is not saved correctly' );
|
2019-08-26 13:01:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Data provider for testVarDump
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function provideVariables() {
|
|
|
|
return [
|
|
|
|
'Only basic variables' => [
|
|
|
|
[
|
2019-01-06 14:20:10 +00:00
|
|
|
'action' => 'edit',
|
2019-08-26 13:01:09 +00:00
|
|
|
'old_wikitext' => 'Old text',
|
|
|
|
'new_wikitext' => 'New text'
|
|
|
|
]
|
|
|
|
],
|
2019-01-06 14:20:10 +00:00
|
|
|
'Normal case' => [
|
2019-08-26 13:01:09 +00:00
|
|
|
[
|
2019-01-06 14:20:10 +00:00
|
|
|
'action' => 'edit',
|
2019-08-26 13:01:09 +00:00
|
|
|
'old_wikitext' => 'Old text',
|
|
|
|
'new_wikitext' => 'New text',
|
|
|
|
'user_editcount' => 15,
|
|
|
|
'added_lines' => [ 'Foo', '', 'Bar' ]
|
|
|
|
]
|
|
|
|
],
|
|
|
|
'Deprecated variables' => [
|
|
|
|
[
|
2019-01-06 14:20:10 +00:00
|
|
|
'action' => 'edit',
|
2019-08-26 13:01:09 +00:00
|
|
|
'old_wikitext' => 'Old text',
|
|
|
|
'new_wikitext' => 'New text',
|
|
|
|
'article_articleid' => 11745,
|
|
|
|
'article_first_contributor' => 'Good guy'
|
Rewrite the VariableHolder code to translate deprecated variables
The current code was more of a subpar, temporary solution. However, we
need a stable solution in case more variables will be deprecated in the
future (T213006 fixes the problem for the past deprecation round). So,
instead of setting a hacky property, directly translate all variables
when loading the var dump. This is not only stable, but has a couple
micro-performance advantages:
- Calling getDeprecatedVariables happens only once when loading the
dump, and not every time a variable is accessed
- No checks are needed when retrieving a variable,
because names can always assumed to be new
Some simple benchmarks reveals a runtime reduction of 8-15% compared to
the old code (8% when it had varsVersion = 2, 15% for varsVersion = 1),
which comes at no cost together with increased readability and
stability. It ain't much, but it's honest work.
Change-Id: Ib32a92c4ad939790633aa63eb3ef8d4629488bea
2020-09-21 11:54:23 +00:00
|
|
|
],
|
|
|
|
[
|
|
|
|
'action' => 'edit',
|
|
|
|
'old_wikitext' => 'Old text',
|
|
|
|
'new_wikitext' => 'New text',
|
|
|
|
'page_id' => 11745,
|
|
|
|
'page_first_contributor' => 'Good guy'
|
2019-08-26 13:01:09 +00:00
|
|
|
]
|
|
|
|
],
|
2019-01-06 14:20:10 +00:00
|
|
|
'Move action' => [
|
2019-08-26 13:01:09 +00:00
|
|
|
[
|
2019-01-06 14:20:10 +00:00
|
|
|
'action' => 'move',
|
2019-08-26 13:01:09 +00:00
|
|
|
'old_wikitext' => 'Old text',
|
|
|
|
'new_wikitext' => 'New text',
|
|
|
|
'all_links' => [ 'https://en.wikipedia.org' ],
|
|
|
|
'moved_to_id' => 156,
|
|
|
|
'moved_to_prefixedtitle' => 'MediaWiki:Foobar.js',
|
|
|
|
'new_content_model' => CONTENT_MODEL_JAVASCRIPT
|
|
|
|
]
|
|
|
|
],
|
2019-01-06 14:20:10 +00:00
|
|
|
'Delete action' => [
|
2019-08-26 13:01:09 +00:00
|
|
|
[
|
|
|
|
'old_wikitext' => 'Old text',
|
|
|
|
'new_wikitext' => 'New text',
|
|
|
|
'timestamp' => 1546000295,
|
|
|
|
'action' => 'delete',
|
|
|
|
'page_namespace' => 114
|
|
|
|
]
|
|
|
|
],
|
|
|
|
'Disabled vars' => [
|
|
|
|
[
|
2019-01-06 14:20:10 +00:00
|
|
|
'action' => 'edit',
|
2019-08-26 13:01:09 +00:00
|
|
|
'old_wikitext' => 'Old text',
|
|
|
|
'new_wikitext' => 'New text',
|
|
|
|
'old_html' => 'Foo <small>bar</small> <s>lol</s>.',
|
|
|
|
'old_text' => 'Foobar'
|
|
|
|
]
|
2019-01-06 14:20:10 +00:00
|
|
|
],
|
|
|
|
'Account creation' => [
|
|
|
|
[
|
|
|
|
'action' => 'createaccount',
|
|
|
|
'accountname' => 'XXX'
|
|
|
|
]
|
2019-08-26 13:01:09 +00:00
|
|
|
]
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2019-12-17 15:56:36 +00:00
|
|
|
/**
|
|
|
|
* Test for the wiki_name variable.
|
|
|
|
*
|
2021-01-02 13:41:31 +00:00
|
|
|
* @covers \MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer::compute
|
2019-12-17 15:56:36 +00:00
|
|
|
*/
|
|
|
|
public function testWikiNameVar() {
|
|
|
|
$name = 'foo';
|
|
|
|
$prefix = 'bar';
|
|
|
|
$this->setMwGlobals( [
|
|
|
|
'wgDBname' => $name,
|
|
|
|
'wgDBprefix' => $prefix
|
|
|
|
] );
|
|
|
|
|
|
|
|
$vars = new AbuseFilterVariableHolder();
|
|
|
|
$vars->setLazyLoadVar( 'wiki_name', 'get-wiki-name', [] );
|
2020-10-18 22:25:05 +00:00
|
|
|
$manager = AbuseFilterServices::getVariablesManager();
|
2019-12-17 15:56:36 +00:00
|
|
|
$this->assertSame(
|
|
|
|
"$name-$prefix",
|
2020-10-18 22:25:05 +00:00
|
|
|
$manager->getVar( $vars, 'wiki_name' )->toNative()
|
2019-12-17 15:56:36 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Test for the wiki_language variable.
|
|
|
|
*
|
2021-01-02 13:41:31 +00:00
|
|
|
* @covers \MediaWiki\Extension\AbuseFilter\Variables\LazyVariableComputer::compute
|
2019-12-17 15:56:36 +00:00
|
|
|
*/
|
|
|
|
public function testWikiLanguageVar() {
|
|
|
|
$fakeCode = 'foobar';
|
|
|
|
$fakeLang = $this->getMockBuilder( Language::class )
|
|
|
|
->setMethods( [ 'getCode' ] )
|
|
|
|
->getMock();
|
|
|
|
$fakeLang->method( 'getCode' )->willReturn( $fakeCode );
|
|
|
|
$this->setService( 'ContentLanguage', $fakeLang );
|
|
|
|
|
|
|
|
$vars = new AbuseFilterVariableHolder();
|
|
|
|
$vars->setLazyLoadVar( 'wiki_language', 'get-wiki-language', [] );
|
2020-10-18 22:25:05 +00:00
|
|
|
$manager = AbuseFilterServices::getVariablesManager();
|
2019-12-17 15:56:36 +00:00
|
|
|
$this->assertSame(
|
|
|
|
$fakeCode,
|
2020-10-18 22:25:05 +00:00
|
|
|
$manager->getVar( $vars, 'wiki_language' )->toNative()
|
2019-12-17 15:56:36 +00:00
|
|
|
);
|
|
|
|
}
|
2020-01-08 16:46:24 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param RevisionRecord $revRec
|
|
|
|
* @param bool $privileged
|
|
|
|
* @param bool $expected
|
|
|
|
* @dataProvider provideUserCanViewRev
|
|
|
|
* @covers AbuseFilter::userCanViewRev
|
|
|
|
*/
|
|
|
|
public function testUserCanViewRev( RevisionRecord $revRec, bool $privileged, bool $expected ) {
|
|
|
|
$user = $privileged
|
|
|
|
? $this->getTestUser( 'suppress' )->getUser()
|
|
|
|
: $this->getTestUser()->getUser();
|
|
|
|
$this->assertSame( $expected, AbuseFilter::userCanViewRev( $revRec, $user ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Generator|array
|
|
|
|
*/
|
|
|
|
public function provideUserCanViewRev() {
|
|
|
|
$title = Title::newFromText( __METHOD__ );
|
|
|
|
|
|
|
|
$visible = new MutableRevisionRecord( $title );
|
|
|
|
yield 'Visible, not privileged' => [ $visible, false, true ];
|
|
|
|
yield 'Visible, privileged' => [ $visible, true, true ];
|
|
|
|
|
|
|
|
$userSup = new MutableRevisionRecord( $title );
|
|
|
|
$userSup->setVisibility( RevisionRecord::SUPPRESSED_USER );
|
|
|
|
yield 'User suppressed, not privileged' => [ $userSup, false, false ];
|
|
|
|
yield 'User suppressed, privileged' => [ $userSup, true, true ];
|
|
|
|
|
|
|
|
$allSupp = new MutableRevisionRecord( $title );
|
|
|
|
$allSupp->setVisibility( RevisionRecord::SUPPRESSED_ALL );
|
|
|
|
yield 'All suppressed, not privileged' => [ $allSupp, false, false ];
|
|
|
|
yield 'All suppressed, privileged' => [ $allSupp, true, true ];
|
|
|
|
}
|
2018-09-26 11:38:51 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $rawConsequences A raw, unfiltered list of consequences
|
2020-10-11 21:17:41 +00:00
|
|
|
* @param array $expectedKeys
|
2018-09-26 11:38:51 +00:00
|
|
|
* @param Title $title
|
2020-12-18 14:05:33 +00:00
|
|
|
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutor
|
2018-09-26 11:38:51 +00:00
|
|
|
* @dataProvider provideConsequences
|
|
|
|
*/
|
2020-10-11 21:17:41 +00:00
|
|
|
public function testGetFilteredConsequences( $rawConsequences, $expectedKeys, Title $title ) {
|
2020-10-16 22:10:37 +00:00
|
|
|
$locallyDisabledActions = [
|
|
|
|
'flag' => false,
|
|
|
|
'throttle' => false,
|
|
|
|
'warn' => false,
|
|
|
|
'disallow' => false,
|
|
|
|
'blockautopromote' => true,
|
|
|
|
'block' => true,
|
|
|
|
'rangeblock' => true,
|
|
|
|
'degroup' => true,
|
|
|
|
'tag' => false
|
|
|
|
];
|
|
|
|
$options = $this->createMock( ServiceOptions::class );
|
|
|
|
$options->method( 'get' )
|
|
|
|
->with( 'AbuseFilterLocallyDisabledGlobalActions' )
|
|
|
|
->willReturn( $locallyDisabledActions );
|
2020-10-11 21:17:41 +00:00
|
|
|
$fakeFilter = $this->createMock( Filter::class );
|
|
|
|
$fakeFilter->method( 'getName' )->willReturn( 'unused name' );
|
2020-12-19 13:36:58 +00:00
|
|
|
$fakeFilter->method( 'getID' )->willReturn( 1 );
|
2020-10-11 21:17:41 +00:00
|
|
|
$fakeLookup = $this->createMock( FilterLookup::class );
|
|
|
|
$fakeLookup->method( 'getFilter' )->willReturn( $fakeFilter );
|
|
|
|
$this->setService( FilterLookup::SERVICE_NAME, $fakeLookup );
|
2018-09-26 11:38:51 +00:00
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
$vars = AbuseFilterVariableHolder::newFromArray( [ 'action' => 'edit' ] );
|
2020-10-16 22:10:37 +00:00
|
|
|
$executor = new ConsequencesExecutor(
|
|
|
|
$this->createMock( ConsequencesLookup::class ),
|
|
|
|
AbuseFilterServices::getConsequencesFactory(),
|
|
|
|
AbuseFilterServices::getConsequencesRegistry(),
|
|
|
|
$fakeLookup,
|
|
|
|
new NullLogger,
|
|
|
|
$options,
|
|
|
|
$user,
|
|
|
|
$title,
|
|
|
|
$vars
|
|
|
|
);
|
|
|
|
$actual = $executor->getFilteredConsequences(
|
|
|
|
$executor->replaceArraysWithConsequences( $rawConsequences ) );
|
2018-09-26 11:38:51 +00:00
|
|
|
|
2020-10-11 21:17:41 +00:00
|
|
|
$actualKeys = [];
|
|
|
|
foreach ( $actual as $filter => $actions ) {
|
|
|
|
$actualKeys[$filter] = array_keys( $actions );
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->assertEquals( $expectedKeys, $actualKeys );
|
2018-09-26 11:38:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Data provider for testGetFilteredConsequences
|
|
|
|
* @todo Split these
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function provideConsequences() {
|
2020-10-23 14:56:21 +00:00
|
|
|
$pageName = 'TestFilteredConsequences';
|
2018-09-26 11:38:51 +00:00
|
|
|
$title = $this->createMock( Title::class );
|
|
|
|
$title->method( 'getPrefixedText' )->willReturn( $pageName );
|
|
|
|
|
|
|
|
return [
|
|
|
|
'warn and throttle exclude other actions' => [
|
|
|
|
[
|
|
|
|
2 => [
|
|
|
|
'warn' => [
|
|
|
|
'abusefilter-warning'
|
|
|
|
],
|
|
|
|
'tag' => [
|
|
|
|
'some tag'
|
|
|
|
]
|
|
|
|
],
|
|
|
|
13 => [
|
|
|
|
'throttle' => [
|
|
|
|
'13',
|
|
|
|
'14,15',
|
|
|
|
'user'
|
|
|
|
],
|
|
|
|
'disallow' => []
|
|
|
|
],
|
|
|
|
168 => [
|
|
|
|
'degroup' => []
|
|
|
|
]
|
|
|
|
],
|
|
|
|
[
|
2020-10-11 21:17:41 +00:00
|
|
|
2 => [ 'warn' ],
|
|
|
|
13 => [ 'throttle' ],
|
|
|
|
168 => [ 'degroup' ]
|
2018-09-26 11:38:51 +00:00
|
|
|
],
|
|
|
|
$title
|
|
|
|
],
|
|
|
|
'warn excludes other actions, block excludes disallow' => [
|
|
|
|
[
|
|
|
|
3 => [
|
|
|
|
'tag' => [
|
|
|
|
'some tag'
|
|
|
|
]
|
|
|
|
],
|
|
|
|
'global-2' => [
|
|
|
|
'warn' => [
|
|
|
|
'abusefilter-beautiful-warning'
|
|
|
|
],
|
|
|
|
'degroup' => []
|
|
|
|
],
|
|
|
|
4 => [
|
|
|
|
'disallow' => [],
|
|
|
|
'block' => [
|
|
|
|
'blocktalk',
|
|
|
|
'15 minutes',
|
|
|
|
'indefinite'
|
|
|
|
]
|
|
|
|
]
|
|
|
|
],
|
|
|
|
[
|
2020-10-11 21:17:41 +00:00
|
|
|
3 => [ 'tag' ],
|
|
|
|
'global-2' => [ 'warn' ],
|
|
|
|
4 => [ 'block' ]
|
2018-09-26 11:38:51 +00:00
|
|
|
],
|
|
|
|
$title
|
|
|
|
],
|
|
|
|
'some global actions are disabled locally, the longest block is chosen' => [
|
|
|
|
[
|
|
|
|
'global-1' => [
|
|
|
|
'blockautopromote' => [],
|
|
|
|
'block' => [
|
|
|
|
'blocktalk',
|
|
|
|
'indefinite',
|
|
|
|
'indefinite'
|
|
|
|
]
|
|
|
|
],
|
|
|
|
1 => [
|
|
|
|
'block' => [
|
|
|
|
'blocktalk',
|
|
|
|
'4 hours',
|
|
|
|
'4 hours'
|
|
|
|
]
|
|
|
|
],
|
|
|
|
2 => [
|
|
|
|
'degroup' => [],
|
|
|
|
'block' => [
|
|
|
|
'blocktalk',
|
|
|
|
'infinity',
|
|
|
|
'never'
|
|
|
|
]
|
|
|
|
]
|
|
|
|
],
|
|
|
|
[
|
|
|
|
'global-1' => [],
|
|
|
|
1 => [],
|
2020-10-11 21:17:41 +00:00
|
|
|
2 => [ 'degroup', 'block' ]
|
2018-09-26 11:38:51 +00:00
|
|
|
],
|
|
|
|
$title
|
|
|
|
],
|
|
|
|
];
|
|
|
|
}
|
2019-08-26 13:01:09 +00:00
|
|
|
}
|