Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F2753029
Campaign.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
49 KB
Referenced Files
None
Subscribers
None
Campaign.php
View Options
<?php
use
MediaWiki\Json\FormatJson
;
use
MediaWiki\MediaWikiServices
;
use
MediaWiki\SpecialPage\SpecialPage
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\User
;
use
MediaWiki\Utils\MWTimestamp
;
use
Wikimedia\Rdbms\SelectQueryBuilder
;
class
Campaign
{
/** @var int|null */
private
$id
=
null
;
/** @var string|null */
private
$name
=
null
;
/** @var MWTimestamp Start datetime of campaign */
private
$start
=
null
;
/** @var MWTimestamp End datetime of campaign */
private
$end
=
null
;
/** @var int Priority level of the campaign, higher is more important */
private
$priority
=
null
;
/** @var bool True if the campaign is enabled for showing */
private
$enabled
=
null
;
/** @var bool True if the campaign is currently non editable */
private
$locked
=
null
;
/** @var bool True if the campaign has been moved to the archive */
private
$archived
=
null
;
/** @var bool True if there is geo-targeting data for ths campaign */
private
$geotargeted
=
null
;
/** @var int The number of buckets in this campaign */
private
$buckets
=
null
;
/** @var int */
private
$throttle
=
null
;
/**
* Construct a lazily loaded CentralNotice campaign object
*
* @param string|int $campaignIdentifier Either an ID or name for the campaign
*/
public
function
__construct
(
$campaignIdentifier
)
{
if
(
is_int
(
$campaignIdentifier
)
)
{
$this
->
id
=
$campaignIdentifier
;
}
else
{
$this
->
name
=
$campaignIdentifier
;
}
}
/**
* Get the unique numerical ID for this campaign
*
* @throws CampaignExistenceException If lazy loading failed.
* @return int
*/
public
function
getId
()
{
if
(
$this
->
id
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
id
;
}
/**
* Get the unique name for this campaign
*
* @throws CampaignExistenceException If lazy loading failed.
* @return string
*/
public
function
getName
()
{
if
(
$this
->
name
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
name
;
}
/**
* Get the start time for the campaign. Only applicable if the campaign is enabled.
*
* @throws CampaignExistenceException If lazy loading failed.
* @return MWTimestamp
*/
public
function
getStartTime
()
{
if
(
$this
->
start
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
start
;
}
/**
* Get the end time for the campaign. Only applicable if the campaign is enabled.
*
* @throws CampaignExistenceException If lazy loading failed.
* @return MWTimestamp
*/
public
function
getEndTime
()
{
if
(
$this
->
end
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
end
;
}
/**
* Get the priority level for this campaign. The larger this is the higher the priority is.
*
* @throws CampaignExistenceException If lazy loading failed.
* @return int
*/
public
function
getPriority
()
{
if
(
$this
->
priority
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
priority
;
}
/**
* Returns the enabled/disabled status of the campaign.
*
* If a campaign is enabled it is eligible to be shown to users.
*
* @throws CampaignExistenceException If lazy loading failed.
* @return bool
*/
public
function
isEnabled
()
{
if
(
$this
->
enabled
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
enabled
;
}
/**
* Returns the locked/unlocked status of the campaign. A locked campaign is not able to be
* edited until unlocked.
*
* @throws CampaignExistenceException If lazy loading failed.
* @return bool
*/
public
function
isLocked
()
{
if
(
$this
->
locked
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
locked
;
}
/**
* Returns the archival status of the campaign. An archived campaign is not allowed to be
* edited.
*
* @throws CampaignExistenceException If lazy loading failed.
* @return bool
*/
public
function
isArchived
()
{
if
(
$this
->
archived
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
archived
;
}
/**
* Returned the geotargeted status of this campaign. Will be true if GeoIP information should
* be used to determine user eligibility.
*
* @throws CampaignExistenceException If lazy loading failed.
* @return bool
*/
public
function
isGeotargeted
()
{
if
(
$this
->
geotargeted
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
geotargeted
;
}
/**
* Get the number of buckets in this campaign.
*
* @throws CampaignExistenceException If lazy loading failed.
* @return int
*/
public
function
getBuckets
()
{
if
(
$this
->
buckets
===
null
)
{
$this
->
loadBasicSettings
();
}
return
$this
->
buckets
;
}
/**
* Load basic campaign settings from the database table cn_notices
*
* @throws CampaignExistenceException If the campaign doesn't exist
*/
private
function
loadBasicSettings
()
{
$db
=
CNDatabase
::
getDb
();
// What selector are we using?
if
(
$this
->
id
!==
null
)
{
$selector
=
[
'not_id'
=>
$this
->
id
];
}
elseif
(
$this
->
name
!==
null
)
{
$selector
=
[
'not_name'
=>
$this
->
name
];
}
else
{
throw
new
CampaignExistenceException
(
"No valid database key available for campaign."
);
}
// Get campaign info from database
$row
=
$db
->
newSelectQueryBuilder
()
->
select
(
[
'not_id'
,
'not_name'
,
'not_start'
,
'not_end'
,
'not_enabled'
,
'not_preferred'
,
'not_locked'
,
'not_archived'
,
'not_geo'
,
'not_buckets'
,
'not_throttle'
,
]
)
->
from
(
'cn_notices'
)
->
where
(
$selector
)
->
caller
(
__METHOD__
)
->
fetchRow
();
if
(
$row
)
{
$this
->
id
=
$row
->
not_id
;
$this
->
name
=
$row
->
not_name
;
$this
->
start
=
new
MWTimestamp
(
$row
->
not_start
);
$this
->
end
=
new
MWTimestamp
(
$row
->
not_end
);
$this
->
enabled
=
(
bool
)
$row
->
not_enabled
;
$this
->
priority
=
(
int
)
$row
->
not_preferred
;
$this
->
locked
=
(
bool
)
$row
->
not_locked
;
$this
->
archived
=
(
bool
)
$row
->
not_archived
;
$this
->
geotargeted
=
(
bool
)
$row
->
not_geo
;
$this
->
buckets
=
(
int
)
$row
->
not_buckets
;
$this
->
throttle
=
(
int
)
$row
->
not_throttle
;
}
else
{
throw
new
CampaignExistenceException
(
"Campaign could not be retrieved from database "
.
"with id '{$this->id}' or name '{$this->name}'"
);
}
}
/**
* See if a given campaign exists in the database
*
* @param string $campaignName
*
* @return bool
*/
public
static
function
campaignExists
(
$campaignName
)
{
$dbr
=
CNDatabase
::
getDb
();
return
(
bool
)
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_name'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$campaignName
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
}
/**
* Get a list of active/active-and-future campaigns and associated banners.
*
* @param bool $includeFuture Include campaigns that haven't started yet, too.
*
* @return array An array of campaigns, whose elements are arrays with campaign name,
* an array of associated banners, and campaign start and end times.
*/
public
static
function
getActiveCampaignsAndBanners
(
$includeFuture
=
false
)
{
$dbr
=
CNDatabase
::
getDb
(
DB_REPLICA
);
$time
=
$dbr
->
timestamp
();
$conds
=
[
$dbr
->
expr
(
'notices.not_end'
,
'>='
,
$dbr
->
timestamp
(
$time
)
),
'notices.not_enabled'
=>
1
,
'notices.not_archived'
=>
0
];
if
(
!
$includeFuture
)
{
$conds
[]
=
$dbr
->
expr
(
'notices.not_start'
,
'<='
,
$dbr
->
timestamp
(
$time
)
);
}
// Query campaigns and banners at once
$dbRows
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
[
'notices.not_id'
,
'notices.not_name'
,
'notices.not_start'
,
'notices.not_end'
,
'templates.tmp_name'
]
)
->
from
(
'cn_notices'
,
'notices'
)
->
leftJoin
(
'cn_assignments'
,
'assignments'
,
'notices.not_id = assignments.not_id'
)
->
leftJoin
(
'cn_templates'
,
'templates'
,
'assignments.tmp_id = templates.tmp_id'
)
->
where
(
$conds
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
$campaigns
=
[];
foreach
(
$dbRows
as
$dbRow
)
{
$campaignId
=
$dbRow
->
not_id
;
// The first time we see any campaign, create the corresponding outer K/V
// entry. Note that these keys don't make it into data structure we return.
if
(
!
isset
(
$campaigns
[
$campaignId
]
)
)
{
$campaigns
[
$campaignId
]
=
[
'name'
=>
$dbRow
->
not_name
,
'start'
=>
$dbRow
->
not_start
,
'end'
=>
$dbRow
->
not_end
,
];
}
$bannerName
=
$dbRow
->
tmp_name
;
// Automagically PHP creates the inner array as needed
if
(
$bannerName
)
{
$campaigns
[
$campaignId
][
'banners'
][]
=
$bannerName
;
}
}
return
array_values
(
$campaigns
);
}
/**
* Return settings for a campaign
*
* @param string $campaignName The name of the campaign
*
* @return array|bool an array of settings or false if the campaign does not exist
*/
public
static
function
getCampaignSettings
(
$campaignName
)
{
$dbr
=
CNDatabase
::
getDb
();
// Get campaign info from database
$row
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
[
'not_id'
,
'not_start'
,
'not_end'
,
'not_enabled'
,
'not_preferred'
,
'not_locked'
,
'not_archived'
,
'not_geo'
,
'not_buckets'
,
'not_throttle'
,
'not_type'
,
]
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$campaignName
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
if
(
$row
)
{
$campaign
=
[
'start'
=>
$row
->
not_start
,
'end'
=>
$row
->
not_end
,
'enabled'
=>
$row
->
not_enabled
,
'preferred'
=>
$row
->
not_preferred
,
'locked'
=>
$row
->
not_locked
,
'archived'
=>
$row
->
not_archived
,
'geo'
=>
$row
->
not_geo
,
'buckets'
=>
$row
->
not_buckets
,
'throttle'
=>
$row
->
not_throttle
,
'type'
=>
$row
->
not_type
];
}
else
{
return
false
;
}
$projects
=
self
::
getNoticeProjects
(
$campaignName
);
$languages
=
self
::
getNoticeLanguages
(
$campaignName
);
$geo_countries
=
self
::
getNoticeCountries
(
$campaignName
);
$geo_regions
=
self
::
getNoticeRegions
(
$campaignName
);
$campaign
[
'projects'
]
=
implode
(
", "
,
$projects
);
$campaign
[
'languages'
]
=
implode
(
", "
,
$languages
);
$campaign
[
'countries'
]
=
implode
(
", "
,
$geo_countries
);
$campaign
[
'regions'
]
=
implode
(
", "
,
$geo_regions
);
$bannersIn
=
Banner
::
getCampaignBanners
(
$row
->
not_id
);
$bannersOut
=
[];
// All we want are the banner names, weights, and buckets
foreach
(
$bannersIn
as
$row
)
{
$outKey
=
$row
[
'name'
];
$bannersOut
[
$outKey
][
'weight'
]
=
$row
[
'weight'
];
$bannersOut
[
$outKey
][
'bucket'
]
=
$row
[
'bucket'
];
}
// Encode into a JSON string for storage
$campaign
[
'banners'
]
=
FormatJson
::
encode
(
$bannersOut
);
$campaign
[
'mixins'
]
=
FormatJson
::
encode
(
self
::
getCampaignMixins
(
$campaignName
,
true
)
);
return
$campaign
;
}
/**
* Get all campaign configurations as of timestamp $ts
*
* @param int $ts
* @return array of settings structs having the following properties:
* id
* name
* enabled
* projects: array of sister project names
* languages: array of language codes
* countries: array of country codes
* regions: array of region codes
* preferred: campaign priority
* geo: is geolocated?
* buckets: number of buckets
* banners: array of banner objects, as returned by getHistoricalBanner,
* plus the following information from the parent campaign:
* campaign: name of the campaign
* campaign_z_index
* campaign_num_buckets
* campaign_throttle
*/
public
static
function
getHistoricalCampaigns
(
$ts
)
{
$dbr
=
CNDatabase
::
getDb
();
$res
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
[
"log_id"
=>
"MAX(notlog_id)"
,
]
)
->
from
(
'cn_notice_log'
)
->
where
(
[
$dbr
->
expr
(
'notlog_timestamp'
,
'<='
,
$dbr
->
timestamp
(
$ts
)
)
]
)
->
groupBy
(
'notlog_not_id'
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
$campaigns
=
[];
foreach
(
$res
as
$row
)
{
$campaignRow
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
[
"id"
=>
"notlog_not_id"
,
"name"
=>
"notlog_not_name"
,
"enabled"
=>
"notlog_end_enabled"
,
"projects"
=>
"notlog_end_projects"
,
"languages"
=>
"notlog_end_languages"
,
"countries"
=>
"notlog_end_countries"
,
"regions"
=>
"notlog_end_regions"
,
"preferred"
=>
"notlog_end_preferred"
,
"geotargeted"
=>
"notlog_end_geo"
,
"banners"
=>
"notlog_end_banners"
,
"bucket_count"
=>
"notlog_end_buckets"
,
"throttle"
=>
"notlog_end_throttle"
,
]
)
->
from
(
'cn_notice_log'
)
->
where
(
[
"notlog_id"
=>
$row
->
log_id
,
$dbr
->
expr
(
'notlog_end_start'
,
'<='
,
$dbr
->
timestamp
(
$ts
)
),
$dbr
->
expr
(
'notlog_end_end'
,
'>='
,
$dbr
->
timestamp
(
$ts
)
),
'notlog_end_enabled'
=>
1
,
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
if
(
!
$campaignRow
)
{
continue
;
}
$campaign
=
(
array
)
$campaignRow
;
$campaign
[
'projects'
]
=
explode
(
", "
,
$campaign
[
'projects'
]
);
$campaign
[
'languages'
]
=
explode
(
", "
,
$campaign
[
'languages'
]
);
$campaign
[
'countries'
]
=
explode
(
", "
,
$campaign
[
'countries'
]
);
$campaign
[
'regions'
]
=
explode
(
", "
,
$campaign
[
'regions'
]
);
if
(
$campaign
[
'banners'
]
===
null
)
{
$campaign
[
'banners'
]
=
[];
}
else
{
$campaign
[
'banners'
]
=
FormatJson
::
decode
(
$campaign
[
'banners'
],
true
);
if
(
!
is_array
(
current
(
$campaign
[
'banners'
]
)
)
)
{
// Old log format; only had weight
foreach
(
$campaign
[
'banners'
]
as
$key
=>
&
$value
)
{
$value
=
[
'weight'
=>
$value
,
'bucket'
=>
0
];
}
}
}
if
(
$campaign
[
'bucket_count'
]
===
null
)
{
// Fix for legacy logs before bucketing
$campaign
[
'bucket_count'
]
=
1
;
}
foreach
(
$campaign
[
'banners'
]
as
$name
=>
&
$banner
)
{
$historical_banner
=
Banner
::
getHistoricalBanner
(
$name
,
$ts
);
if
(
$historical_banner
===
null
)
{
// FIXME: crazy hacks
$historical_banner
=
Banner
::
getBannerSettings
(
$name
);
$historical_banner
[
'label'
]
=
wfMessage
(
'centralnotice-damaged-log'
,
$name
)->
text
();
$historical_banner
[
'display_anon'
]
=
$historical_banner
[
'anon'
];
$historical_banner
[
'display_account'
]
=
$historical_banner
[
'account'
];
$historical_banner
[
'devices'
]
=
[
'desktop'
];
}
$banner
[
'name'
]
=
$name
;
$banner
[
'label'
]
=
$name
;
$campaign_info
=
[
'campaign'
=>
$campaign
[
'name'
],
'campaign_z_index'
=>
$campaign
[
'preferred'
],
'campaign_num_buckets'
=>
$campaign
[
'bucket_count'
],
'campaign_throttle'
=>
$campaign
[
'throttle'
],
];
$banner
=
array_merge
(
$banner
,
$campaign_info
,
$historical_banner
);
}
$campaigns
[]
=
$campaign
;
}
return
$campaigns
;
}
/**
* Retrieve campaign mixins settings for this campaign.
*
* If $compact is true, retrieve only enabled mixins, and return a compact
* data structure in which keys are mixin names and values are parameter
* settings.
*
* If $compact is false, mixins that were once enabled for this campaign but
* are now disabled will be included, showing their last parameter settings.
* The data structure will be an array whose keys are mixin names and whose
* values are arrays with 'enabled' and 'parameters' keys. Note that mixins
* that were never enabled for this campaign will be omitted.
*
* @param string $campaignName
* @param bool $compact
* @return array
*/
public
static
function
getCampaignMixins
(
$campaignName
,
$compact
=
false
)
{
global
$wgCentralNoticeCampaignMixins
;
$dbr
=
CNDatabase
::
getDb
();
// Prepare query conditions
$conds
=
[
'notices.not_name'
=>
$campaignName
];
if
(
$compact
)
{
$conds
[
'notice_mixins.nmxn_enabled'
]
=
1
;
}
$dbRows
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
[
'notice_mixins.nmxn_mixin_name'
,
'notice_mixins.nmxn_enabled'
,
'notice_mixin_params.nmxnp_param_name'
,
'notice_mixin_params.nmxnp_param_value'
]
)
->
from
(
'cn_notices'
,
'notices'
)
->
join
(
'cn_notice_mixins'
,
'notice_mixins'
,
'notices.not_id = notice_mixins.nmxn_not_id'
)
->
leftJoin
(
'cn_notice_mixin_params'
,
'notice_mixin_params'
,
'notice_mixins.nmxn_id = notice_mixin_params.nmxnp_notice_mixin_id'
)
->
where
(
$conds
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
// Build up the results
// We expect a row for every parameter name-value pair for every mixin,
// and maybe some with null name-value pairs (for mixins with no
// parameters).
$campaignMixins
=
[];
foreach
(
$dbRows
as
$dbRow
)
{
$mixinName
=
$dbRow
->
nmxn_mixin_name
;
// A mixin may have been removed from the code but may still
// leave stuff in the database. In that case, skip it!
if
(
!
isset
(
$wgCentralNoticeCampaignMixins
[
$mixinName
]
)
)
{
continue
;
}
// First time we have a result row for this mixin?
if
(
!
isset
(
$campaignMixins
[
$mixinName
]
)
)
{
// Data structure depends on $compact
if
(
$compact
)
{
$campaignMixins
[
$mixinName
]
=
[];
}
else
{
$campaignMixins
[
$mixinName
]
=
[
'enabled'
=>
(
bool
)
$dbRow
->
nmxn_enabled
,
'parameters'
=>
[]
];
}
}
// If there are mixin params in this row, add them in
if
(
$dbRow
->
nmxnp_param_name
!==
null
)
{
$paramName
=
$dbRow
->
nmxnp_param_name
;
$mixinDef
=
$wgCentralNoticeCampaignMixins
[
$mixinName
];
// Handle mixin parameters being removed, too
if
(
!
isset
(
$mixinDef
[
'parameters'
][
$paramName
]
)
)
{
continue
;
}
$paramType
=
$mixinDef
[
'parameters'
][
$paramName
][
'type'
];
switch
(
$paramType
)
{
case
'string'
:
$paramVal
=
$dbRow
->
nmxnp_param_value
;
break
;
case
'integer'
:
$paramVal
=
intval
(
$dbRow
->
nmxnp_param_value
);
break
;
case
'float'
:
$paramVal
=
floatval
(
$dbRow
->
nmxnp_param_value
);
break
;
case
'boolean'
:
$paramVal
=
(
$dbRow
->
nmxnp_param_value
===
'true'
);
break
;
case
'json'
:
$paramVal
=
json_decode
(
$dbRow
->
nmxnp_param_value
);
if
(
$paramVal
===
null
)
{
wfLogWarning
(
'Couldn
\'
t decode json param '
.
$paramName
.
' for mixin '
.
$mixinName
.
' in campaign '
.
$campaignName
.
'.'
);
// In this case, it's fine to emit a null value for the
// parameter. Both Admin UI and subscribing client-side
// code should handle it gracefully and warn in the console.
// TODO Handle this better, server-side.
}
break
;
default
:
throw
new
DomainException
(
'Unknown parameter type '
.
$paramType
);
}
// Again, data structure depends on $compact
if
(
$compact
)
{
$campaignMixins
[
$mixinName
][
$paramName
]
=
$paramVal
;
}
else
{
$campaignMixins
[
$mixinName
][
'parameters'
][
$paramName
]
=
$paramVal
;
}
}
}
// Ensure consistent ordering, since it's needed for
// CNChoiceDataResourceLoaderModule (which gets this data via
// ChoiceDataProvider) for consistent RL module hashes.
array_walk
(
$campaignMixins
,
static
function
(
&
$campaignMixin
)
{
ksort
(
$campaignMixin
);
}
);
ksort
(
$campaignMixins
);
return
$campaignMixins
;
}
/**
* Update enabled or disabled status and parameters for a campaign mixin,
* for a given campaign.
*
* @param string $campaignName
* @param string $mixinName
* @param bool $enable
* @param array|null $params For mixins with no parameters, set to an empty array.
*/
public
static
function
updateCampaignMixins
(
$campaignName
,
$mixinName
,
$enable
,
$params
=
null
)
{
global
$wgCentralNoticeCampaignMixins
;
// TODO Error handling!
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
// Get the campaign ID
// Note: the need to fetch the ID here highlights the need for some
// kind of ORM.
$noticeId
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$campaignName
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
if
(
$enable
)
{
if
(
$params
===
null
)
{
throw
new
InvalidArgumentException
(
'Paremeters info required to enable mixin '
.
$mixinName
.
' for campaign '
.
$campaignName
);
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_mixins'
)
->
row
(
[
'nmxn_not_id'
=>
$noticeId
,
'nmxn_mixin_name'
=>
$mixinName
,
'nmxn_enabled'
=>
1
]
)
->
onDuplicateKeyUpdate
()
->
uniqueIndexFields
(
[
'nmxn_not_id'
,
'nmxn_mixin_name'
]
)
->
set
(
[
'nmxn_enabled'
=>
1
]
)
->
caller
(
__METHOD__
)
->
execute
();
$noticeMixinId
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'nmxn_id'
)
->
from
(
'cn_notice_mixins'
)
->
where
(
[
'nmxn_not_id'
=>
$noticeId
,
'nmxn_mixin_name'
=>
$mixinName
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
foreach
(
$params
as
$paramName
=>
$paramVal
)
{
$mixinDef
=
$wgCentralNoticeCampaignMixins
[
$mixinName
];
// Handle an undefined parameter. Not likely to happen, maybe
// in the middle of a deploy that removes a parameter.
if
(
!
isset
(
$mixinDef
[
'parameters'
][
$paramName
]
)
)
{
wfLogWarning
(
'No definition found for the parameter '
.
$paramName
.
' for the campaign mixn '
.
$mixinName
.
'.'
);
continue
;
}
// Munge boolean params for database storage. (Other types
// should end up as strings, which will be fine.)
if
(
$mixinDef
[
'parameters'
][
$paramName
][
'type'
]
===
'boolean'
)
{
$paramVal
=
(
$paramVal
?
'true'
:
'false'
);
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_mixin_params'
)
->
row
(
[
'nmxnp_notice_mixin_id'
=>
$noticeMixinId
,
'nmxnp_param_name'
=>
$paramName
,
'nmxnp_param_value'
=>
$paramVal
]
)
->
onDuplicateKeyUpdate
()
->
uniqueIndexFields
(
[
'nmxnp_notice_mixin_id'
,
'nmxnp_param_name'
]
)
->
set
(
[
'nmxnp_param_value'
=>
$paramVal
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
}
else
{
// When we disable a mixin, just set enabled to false; since we keep
// the old parameter values in case the mixin is re-enabled, we also
// keep the row in this table, since the id is used in the param
// table.
$dbw
->
newUpdateQueryBuilder
()
->
update
(
'cn_notice_mixins'
)
->
set
(
[
'nmxn_enabled'
=>
0
]
)
->
where
(
[
'nmxn_not_id'
=>
$noticeId
,
'nmxn_mixin_name'
=>
$mixinName
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
}
/**
* Get all the campaigns in the database
*
* @return array an array of campaign names
*/
public
static
function
getAllCampaignNames
()
{
$dbr
=
CNDatabase
::
getDb
();
return
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_name'
)
->
from
(
'cn_notices'
)
->
caller
(
__METHOD__
)
->
fetchFieldValues
();
}
/**
* Add a new campaign to the database
*
* @param string $noticeName Name of the campaign
* @param bool $enabled Boolean setting, true or false
* @param string $startTs Campaign start in UTC
* @param array $projects Targeted project types (wikipedia, wikibooks, etc.)
* @param array $project_languages Targeted project languages (en, de, etc.)
* @param bool $geotargeted Boolean setting, true or false
* @param array $geo_countries Targeted countries
* @param array $geo_regions Targeted regions in format CountryCode_RegionCode
* @param int $throttle limit allocations, 0 - 100
* @param int $priority priority level, LOW_PRIORITY - EMERGENCY_PRIORITY
* @param User $user User adding the campaign
* @param string|null $type Type of campaign
* @param string|null $summary Change summary provided by the user
* @return int|string noticeId on success, or message key for error
*/
public
static
function
addCampaign
(
$noticeName
,
$enabled
,
$startTs
,
$projects
,
$project_languages
,
$geotargeted
,
$geo_countries
,
$geo_regions
,
$throttle
,
$priority
,
$user
,
$type
,
$summary
=
null
)
{
$noticeName
=
trim
(
$noticeName
);
if
(
self
::
campaignExists
(
$noticeName
)
)
{
return
'centralnotice-notice-exists'
;
}
elseif
(
!
$projects
)
{
return
'centralnotice-no-project'
;
}
elseif
(
!
$project_languages
)
{
return
'centralnotice-no-language'
;
}
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
startAtomic
(
__METHOD__
);
$endTime
=
strtotime
(
'+1 hour'
,
(
int
)
wfTimestamp
(
TS_UNIX
,
$startTs
)
);
$endTs
=
wfTimestamp
(
TS_MW
,
$endTime
);
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notices'
)
->
row
(
[
'not_name'
=>
$noticeName
,
'not_enabled'
=>
(
int
)
$enabled
,
'not_start'
=>
$dbw
->
timestamp
(
$startTs
),
'not_end'
=>
$dbw
->
timestamp
(
$endTs
),
'not_geo'
=>
(
int
)
$geotargeted
,
'not_throttle'
=>
$throttle
,
'not_preferred'
=>
$priority
,
'not_type'
=>
$type
]
)
->
caller
(
__METHOD__
)
->
execute
();
$not_id
=
$dbw
->
insertId
();
if
(
$not_id
)
{
// Do multi-row insert for campaign projects
$insertArray
=
[];
foreach
(
$projects
as
$project
)
{
$insertArray
[]
=
[
'np_notice_id'
=>
$not_id
,
'np_project'
=>
$project
];
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_projects'
)
->
ignore
()
->
rows
(
$insertArray
)
->
caller
(
__METHOD__
)
->
execute
();
// Do multi-row insert for campaign languages
$insertArray
=
[];
foreach
(
$project_languages
as
$code
)
{
$insertArray
[]
=
[
'nl_notice_id'
=>
$not_id
,
'nl_language'
=>
$code
];
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_languages'
)
->
ignore
()
->
rows
(
$insertArray
)
->
caller
(
__METHOD__
)
->
execute
();
if
(
$geotargeted
)
{
// Do multi-row insert for campaign countries
if
(
$geo_countries
)
{
$insertArray
=
[];
foreach
(
$geo_countries
as
$code
)
{
$insertArray
[]
=
[
'nc_notice_id'
=>
$not_id
,
'nc_country'
=>
$code
];
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_countries'
)
->
ignore
()
->
rows
(
$insertArray
)
->
caller
(
__METHOD__
)
->
execute
();
}
// Do multi-row insert for campaign regions
if
(
$geo_regions
)
{
$insertArray
=
[];
foreach
(
$geo_regions
as
$code
)
{
$insertArray
[]
=
[
'nr_notice_id'
=>
$not_id
,
'nr_region'
=>
$code
];
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_regions'
)
->
ignore
()
->
rows
(
$insertArray
)
->
caller
(
__METHOD__
)
->
execute
();
}
}
$dbw
->
endAtomic
(
__METHOD__
);
// Log the creation of the campaign
$beginSettings
=
[];
$endSettings
=
[
'projects'
=>
implode
(
", "
,
$projects
),
'languages'
=>
implode
(
", "
,
$project_languages
),
'countries'
=>
implode
(
", "
,
$geo_countries
),
'regions'
=>
implode
(
", "
,
$geo_regions
),
'start'
=>
$dbw
->
timestamp
(
$startTs
),
'end'
=>
$dbw
->
timestamp
(
$endTs
),
'enabled'
=>
(
int
)
$enabled
,
'preferred'
=>
0
,
'locked'
=>
0
,
'archived'
=>
0
,
'geo'
=>
(
int
)
$geotargeted
,
'throttle'
=>
$throttle
,
'type'
=>
$type
];
self
::
processAfterCampaignChange
(
'created'
,
$not_id
,
$noticeName
,
$user
,
$beginSettings
,
$endSettings
,
$summary
);
return
$not_id
;
}
throw
new
RuntimeException
(
'insertId() did not return a value.'
);
}
/**
* Remove a campaign from the database
*
* @param string $campaignName Name of the campaign
* @param User $user User removing the campaign
*
* @return bool|string True on success, string with message key for error
*/
public
static
function
removeCampaign
(
$campaignName
,
$user
)
{
// TODO This method is never used?
$dbr
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$res
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_locked'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$campaignName
]
)
->
caller
(
__METHOD__
)
->
fetchField
();
if
(
$res
===
false
)
{
return
'centralnotice-remove-notice-doesnt-exist'
;
}
if
(
$res
)
{
return
'centralnotice-notice-is-locked'
;
}
self
::
removeCampaignByName
(
$campaignName
,
$user
);
return
true
;
}
private
static
function
removeCampaignByName
(
$campaignName
,
$user
)
{
// Log the removal of the campaign
$campaignId
=
self
::
getNoticeId
(
$campaignName
);
self
::
processAfterCampaignChange
(
'removed'
,
$campaignId
,
$campaignName
,
$user
);
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
startAtomic
(
__METHOD__
);
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_assignments'
)
->
where
(
[
'not_id'
=>
$campaignId
]
)
->
caller
(
__METHOD__
)
->
execute
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$campaignName
]
)
->
caller
(
__METHOD__
)
->
execute
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notice_languages'
)
->
where
(
[
'nl_notice_id'
=>
$campaignId
]
)
->
caller
(
__METHOD__
)
->
execute
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notice_projects'
)
->
where
(
[
'np_notice_id'
=>
$campaignId
]
)
->
caller
(
__METHOD__
)
->
execute
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notice_countries'
)
->
where
(
[
'nc_notice_id'
=>
$campaignId
]
)
->
caller
(
__METHOD__
)
->
execute
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notice_regions'
)
->
where
(
[
'nr_notice_id'
=>
$campaignId
]
)
->
caller
(
__METHOD__
)
->
execute
();
$dbw
->
endAtomic
(
__METHOD__
);
}
/**
* Assign a banner to a campaign at a certain weight
* @param string $noticeName
* @param string $templateName
* @param int $weight
* @param int $bucket
* @return bool|string True on success, string with message key for error
*/
public
static
function
addTemplateTo
(
$noticeName
,
$templateName
,
$weight
,
$bucket
=
0
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$noticeId
=
self
::
getNoticeId
(
$noticeName
);
$templateId
=
Banner
::
fromName
(
$templateName
)->
getId
();
$res
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'asn_id'
)
->
from
(
'cn_assignments'
)
->
where
(
[
'tmp_id'
=>
$templateId
,
'not_id'
=>
$noticeId
]
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
if
(
$res
->
numRows
()
>
0
)
{
return
'centralnotice-template-already-exists'
;
}
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_assignments'
)
->
row
(
[
'tmp_id'
=>
$templateId
,
'tmp_weight'
=>
$weight
,
'not_id'
=>
$noticeId
,
'asn_bucket'
=>
$bucket
,
]
)
->
caller
(
__METHOD__
)
->
execute
();
return
true
;
}
/**
* Remove a banner assignment from a campaign
* @param string $noticeName
* @param string $templateName
*/
public
static
function
removeTemplateFor
(
$noticeName
,
$templateName
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$noticeId
=
self
::
getNoticeId
(
$noticeName
);
$templateId
=
Banner
::
fromName
(
$templateName
)->
getId
();
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_assignments'
)
->
where
(
[
'tmp_id'
=>
$templateId
,
'not_id'
=>
$noticeId
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
/**
* Lookup the ID for a campaign based on the campaign name
* @param string $noticeName
* @return int|null
*/
public
static
function
getNoticeId
(
$noticeName
)
{
$dbr
=
CNDatabase
::
getDb
();
$row
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$noticeName
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
if
(
$row
)
{
return
$row
->
not_id
;
}
else
{
return
null
;
}
}
/**
* Lookup the name of a campaign based on the campaign ID
* @param int $noticeId
* @return null|string
*/
public
static
function
getNoticeName
(
$noticeId
)
{
$dbr
=
CNDatabase
::
getDb
();
if
(
is_numeric
(
$noticeId
)
)
{
$row
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_name'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_id'
=>
$noticeId
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
if
(
$row
)
{
return
$row
->
not_name
;
}
}
return
null
;
}
public
static
function
getNoticeProjects
(
$noticeName
)
{
$dbr
=
CNDatabase
::
getDb
();
$row
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$noticeName
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
$projects
=
[];
if
(
$row
)
{
$projects
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'np_project'
)
->
from
(
'cn_notice_projects'
)
->
where
(
[
'np_notice_id'
=>
$row
->
not_id
]
)
->
caller
(
__METHOD__
)
->
fetchFieldValues
();
}
sort
(
$projects
);
return
$projects
;
}
public
static
function
getNoticeLanguages
(
$noticeName
)
{
$dbr
=
CNDatabase
::
getDb
();
$row
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$noticeName
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
$languages
=
[];
if
(
$row
)
{
$languages
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'nl_language'
)
->
from
(
'cn_notice_languages'
)
->
where
(
[
'nl_notice_id'
=>
$row
->
not_id
]
)
->
caller
(
__METHOD__
)
->
fetchFieldValues
();
}
sort
(
$languages
);
return
$languages
;
}
/**
* @param string $noticeName
*
* @return string[]
*/
public
static
function
getNoticeCountries
(
$noticeName
)
{
$dbr
=
CNDatabase
::
getDb
();
$row
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$noticeName
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
$countries
=
[];
if
(
$row
)
{
$countries
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'nc_country'
)
->
from
(
'cn_notice_countries'
)
->
where
(
[
'nc_notice_id'
=>
$row
->
not_id
]
)
->
caller
(
__METHOD__
)
->
fetchFieldValues
();
}
sort
(
$countries
);
return
$countries
;
}
/**
* @param string $noticeName
*
* @return string[]
*/
public
static
function
getNoticeRegions
(
$noticeName
)
{
$dbr
=
CNDatabase
::
getDb
();
$row
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$noticeName
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
$regions
=
[];
if
(
$row
)
{
$regions
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'nr_region'
)
->
from
(
'cn_notice_regions'
)
->
where
(
[
'nr_notice_id'
=>
$row
->
not_id
]
)
->
caller
(
__METHOD__
)
->
fetchFieldValues
();
}
sort
(
$regions
);
return
$regions
;
}
/**
* Returns a Title object to use in obtaining the URL of a campaign.
* @return Title
*/
public
static
function
getTitleForURL
()
{
return
SpecialPage
::
getTitleFor
(
'CentralNotice'
);
}
/**
* Returns an array with key/value pairs for a query string, to use in obtaining the
* URL of the campaign with the specified name.
*
* @param string $campaignName
* @return string[]
*/
public
static
function
getQueryForURL
(
$campaignName
)
{
return
[
'subaction'
=>
'noticeDetail'
,
'notice'
=>
$campaignName
];
}
/**
* Returns the canonical URL for campaign with the specified name (as returned by
* Title::getCanonicalURL()).
*
* Usage note: This method should be considered part of CentralNotice's public API.
* It's called from outside the extension in EventBus::onCentralNoticeCampaignChange().
*
* @param string $campaignName
* @return string
*/
public
static
function
getCanonicalURL
(
$campaignName
)
{
return
self
::
getTitleForURL
()->
getCanonicalURL
(
self
::
getQueryForURL
(
$campaignName
)
);
}
/**
* @param string $noticeName
* @param string $start Date
* @param string $end Date
* @return bool|string True on success, string with message key for error
*/
public
static
function
updateNoticeDate
(
$noticeName
,
$start
,
$end
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
// Start/end don't line up
if
(
$start
>
$end
||
$end
<
$start
)
{
return
'centralnotice-invalid-date-range'
;
}
// Invalid campaign name
if
(
!
self
::
campaignExists
(
$noticeName
)
)
{
return
'centralnotice-notice-doesnt-exist'
;
}
// Overlap over a date within the same project and language
$startDate
=
$dbw
->
timestamp
(
$start
);
$endDate
=
$dbw
->
timestamp
(
$end
);
$dbw
->
newUpdateQueryBuilder
()
->
update
(
'cn_notices'
)
->
set
(
[
'not_start'
=>
$startDate
,
'not_end'
=>
$endDate
]
)
->
where
(
[
'not_name'
=>
$noticeName
]
)
->
caller
(
__METHOD__
)
->
execute
();
return
true
;
}
/**
* Update a boolean setting on a campaign
*
* @param string $noticeName Name of the campaign
* @param string $settingName Name of a boolean setting (enabled, locked, or geo)
* @param bool $settingValue Value to use for the setting, true or false
*/
public
static
function
setBooleanCampaignSetting
(
$noticeName
,
$settingName
,
$settingValue
)
{
if
(
!
self
::
campaignExists
(
$noticeName
)
)
{
// Exit quietly since campaign may have been deleted at the same time.
return
;
}
else
{
$settingName
=
strtolower
(
$settingName
);
if
(
!
self
::
settingNameIsValid
(
$settingName
)
)
{
throw
new
InvalidArgumentException
(
"Invalid setting name"
);
}
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
newUpdateQueryBuilder
()
->
update
(
'cn_notices'
)
->
set
(
[
'not_'
.
$settingName
=>
(
int
)
$settingValue
]
)
->
where
(
[
'not_name'
=>
$noticeName
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
}
/**
* Updates a numeric setting on a campaign
*
* @param string $noticeName Name of the campaign
* @param string $settingName Name of a numeric setting (preferred)
* @param int $settingValue Value to use
* @param int $max The max that the value can take, default 1
* @param int $min The min that the value can take, default 0
* @throws InvalidArgumentException|RangeException
*/
public
static
function
setNumericCampaignSetting
(
$noticeName
,
$settingName
,
$settingValue
,
$max
=
1
,
$min
=
0
)
{
if
(
$max
<=
$min
)
{
throw
new
RangeException
(
'Max must be greater than min.'
);
}
if
(
!
is_numeric
(
$settingValue
)
)
{
throw
new
InvalidArgumentException
(
'Setting value must be numeric.'
);
}
if
(
$settingValue
>
$max
)
{
$settingValue
=
$max
;
}
if
(
$settingValue
<
$min
)
{
$settingValue
=
$min
;
}
if
(
!
self
::
campaignExists
(
$noticeName
)
)
{
// Exit quietly since campaign may have been deleted at the same time.
return
;
}
else
{
$settingName
=
strtolower
(
$settingName
);
if
(
!
self
::
settingNameIsValid
(
$settingName
)
)
{
throw
new
InvalidArgumentException
(
"Invalid setting name"
);
}
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
newUpdateQueryBuilder
()
->
update
(
'cn_notices'
)
->
set
(
[
'not_'
.
$settingName
=>
$settingValue
]
)
->
where
(
[
'not_name'
=>
$noticeName
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
}
/**
* Updates the weight of a banner in a campaign.
*
* @param string $noticeName Name of the campaign to update
* @param int $templateId ID of the banner in the campaign
* @param int $weight New banner weight
*/
public
static
function
updateWeight
(
$noticeName
,
$templateId
,
$weight
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$noticeId
=
self
::
getNoticeId
(
$noticeName
);
$dbw
->
newUpdateQueryBuilder
()
->
update
(
'cn_assignments'
)
->
set
(
[
'tmp_weight'
=>
$weight
]
)
->
where
(
[
'tmp_id'
=>
$templateId
,
'not_id'
=>
$noticeId
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
/**
* Updates the bucket of a banner in a campaign. Buckets alter what is shown to the end user
* which can affect the relative weight of the banner in a campaign.
*
* @param string $noticeName Name of the campaign to update
* @param int $templateId ID of the banner in the campaign
* @param int $bucket New bucket number
*/
public
static
function
updateBucket
(
$noticeName
,
$templateId
,
$bucket
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$noticeId
=
self
::
getNoticeId
(
$noticeName
);
$dbw
->
newUpdateQueryBuilder
()
->
update
(
'cn_assignments'
)
->
set
(
[
'asn_bucket'
=>
$bucket
]
)
->
where
(
[
'tmp_id'
=>
$templateId
,
'not_id'
=>
$noticeId
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
public
static
function
updateProjects
(
$notice
,
$newProjects
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
startAtomic
(
__METHOD__
);
// Get the previously assigned projects
$oldProjects
=
self
::
getNoticeProjects
(
$notice
);
// Get the notice id
$row
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$notice
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
// Add newly assigned projects
$addProjects
=
array_diff
(
$newProjects
,
$oldProjects
);
$insertArray
=
[];
foreach
(
$addProjects
as
$project
)
{
$insertArray
[]
=
[
'np_notice_id'
=>
$row
->
not_id
,
'np_project'
=>
$project
];
}
if
(
$insertArray
)
{
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_projects'
)
->
ignore
()
->
rows
(
$insertArray
)
->
caller
(
__METHOD__
)
->
execute
();
}
// Remove disassociated projects
$removeProjects
=
array_diff
(
$oldProjects
,
$newProjects
);
if
(
$removeProjects
)
{
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notice_projects'
)
->
where
(
[
'np_notice_id'
=>
$row
->
not_id
,
'np_project'
=>
$removeProjects
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
$dbw
->
endAtomic
(
__METHOD__
);
}
public
static
function
updateProjectLanguages
(
$notice
,
$newLanguages
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
startAtomic
(
__METHOD__
);
// Get the previously assigned languages
$oldLanguages
=
self
::
getNoticeLanguages
(
$notice
);
// Get the notice id
$row
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$notice
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
// Add newly assigned languages
$addLanguages
=
array_diff
(
$newLanguages
,
$oldLanguages
);
$insertArray
=
[];
foreach
(
$addLanguages
as
$code
)
{
$insertArray
[]
=
[
'nl_notice_id'
=>
$row
->
not_id
,
'nl_language'
=>
$code
];
}
if
(
$insertArray
)
{
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_languages'
)
->
ignore
()
->
rows
(
$insertArray
)
->
caller
(
__METHOD__
)
->
execute
();
}
// Remove disassociated languages
$removeLanguages
=
array_diff
(
$oldLanguages
,
$newLanguages
);
if
(
$removeLanguages
)
{
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notice_languages'
)
->
where
(
[
'nl_notice_id'
=>
$row
->
not_id
,
'nl_language'
=>
$removeLanguages
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
$dbw
->
endAtomic
(
__METHOD__
);
}
/**
* Update countries targeted for a campaign
* @param string $notice
* @param array $newCountries
*/
public
static
function
updateCountries
(
$notice
,
$newCountries
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
startAtomic
(
__METHOD__
);
// Get the previously assigned countries
$oldCountries
=
self
::
getNoticeCountries
(
$notice
);
// Get the notice id
$row
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$notice
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
// Add newly assigned countries
$addCountries
=
array_diff
(
$newCountries
,
$oldCountries
);
$insertArray
=
[];
foreach
(
$addCountries
as
$code
)
{
$insertArray
[]
=
[
'nc_notice_id'
=>
$row
->
not_id
,
'nc_country'
=>
$code
];
}
if
(
$insertArray
)
{
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_countries'
)
->
ignore
()
->
rows
(
$insertArray
)
->
caller
(
__METHOD__
)
->
execute
();
}
// Remove disassociated countries
$removeCountries
=
array_diff
(
$oldCountries
,
$newCountries
);
if
(
$removeCountries
)
{
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notice_countries'
)
->
where
(
[
'nc_notice_id'
=>
$row
->
not_id
,
'nc_country'
=>
$removeCountries
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
$dbw
->
endAtomic
(
__METHOD__
);
}
/**
* Update regions targeted for a campaign
* @param string $notice
* @param array $newRegions in format CountryCode_RegionCode
*/
public
static
function
updateRegions
(
$notice
,
$newRegions
)
{
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
startAtomic
(
__METHOD__
);
// Get the previously assigned regions
$oldRegions
=
self
::
getNoticeRegions
(
$notice
);
// Get the notice id
$row
=
$dbw
->
newSelectQueryBuilder
()
->
select
(
'not_id'
)
->
from
(
'cn_notices'
)
->
where
(
[
'not_name'
=>
$notice
]
)
->
caller
(
__METHOD__
)
->
fetchRow
();
// Add newly assigned regions
$addRegions
=
array_diff
(
$newRegions
,
$oldRegions
);
$insertArray
=
[];
foreach
(
$addRegions
as
$code
)
{
$insertArray
[]
=
[
'nr_notice_id'
=>
$row
->
not_id
,
'nr_region'
=>
$code
];
}
if
(
$insertArray
)
{
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_regions'
)
->
ignore
()
->
rows
(
$insertArray
)
->
caller
(
__METHOD__
)
->
execute
();
}
// Remove disassociated regions
$removeRegions
=
array_diff
(
$oldRegions
,
$newRegions
);
if
(
$removeRegions
)
{
$dbw
->
newDeleteQueryBuilder
()
->
deleteFrom
(
'cn_notice_regions'
)
->
where
(
[
'nr_notice_id'
=>
$row
->
not_id
,
'nr_region'
=>
$removeRegions
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
$dbw
->
endAtomic
(
__METHOD__
);
}
/**
* Log any changes related to a campaign
*
* @param string $action 'created', 'modified', or 'removed'
* @param int $campaignId ID of the campaign
* @param string $campaignName Name of the campaign
* @param User $user User causing the change
* @param array $beginSettings array of campaign settings before changes (optional).
* If provided, it should include at least start, end, enabled and archived.
* @param array $endSettings array of campaign settings after changes (optional).
* If provided, it should include at least start, end, enabled and archived.
* @param string|null $summary Change summary provided by the user
*/
public
static
function
processAfterCampaignChange
(
$action
,
$campaignId
,
$campaignName
,
$user
,
$beginSettings
=
[],
$endSettings
=
[],
$summary
=
null
)
{
ChoiceDataProvider
::
invalidateCache
();
// Summary shouldn't actually come in null, but just in case...
if
(
$summary
===
null
)
{
$summary
=
''
;
}
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$time
=
$dbw
->
timestamp
();
$log
=
[
'notlog_timestamp'
=>
$time
,
'notlog_user_id'
=>
$user
->
getId
(),
'notlog_action'
=>
$action
,
'notlog_not_id'
=>
$campaignId
,
'notlog_not_name'
=>
$campaignName
,
'notlog_comment'
=>
$summary
,
];
foreach
(
$beginSettings
as
$key
=>
$value
)
{
if
(
!
self
::
settingNameIsValid
(
$key
)
)
{
throw
new
InvalidArgumentException
(
"Invalid setting name"
);
}
$log
[
'notlog_begin_'
.
$key
]
=
$value
;
}
foreach
(
$endSettings
as
$key
=>
$value
)
{
if
(
!
self
::
settingNameIsValid
(
$key
)
)
{
throw
new
InvalidArgumentException
(
"Invalid setting name"
);
}
$log
[
'notlog_end_'
.
$key
]
=
$value
;
}
(
new
CentralNoticeHookRunner
(
MediaWikiServices
::
getInstance
()->
getHookContainer
()
)
)
->
onCentralNoticeCampaignChange
(
$action
,
$time
,
$campaignName
,
$user
,
self
::
processSettingsForHook
(
$beginSettings
),
self
::
processSettingsForHook
(
$endSettings
),
$summary
);
// Only log the change if it is done by an actual user (rather than a testing script)
// FIXME There must be a cleaner way to do this?
if
(
$user
->
getId
()
>
0
)
{
// User::getID returns 0 for anonymous or non-existant users
$dbw
->
newInsertQueryBuilder
()
->
insertInto
(
'cn_notice_log'
)
->
row
(
$log
)
->
caller
(
__METHOD__
)
->
execute
();
}
}
/**
* Prepare campaign settings to be sent to the CampaignChange hook. This is necessary
* since the settings provided to processAfterCampaignChange() are in a format
* that is appropriate for the cn_notice_log table, but not for the hook.
*
* @param array $settings
* @return array|null
*/
private
static
function
processSettingsForHook
(
$settings
)
{
if
(
!
$settings
)
{
return
null
;
}
if
(
isset
(
$settings
[
'banners'
]
)
)
{
$banners
=
json_decode
(
$settings
[
'banners'
]
);
// This should never happen, since the string should just have been json-encoded
// in getCampaignSettings().
if
(
$banners
===
null
)
{
throw
new
UnexpectedValueException
(
'Json decoding error for banner settings'
);
}
// Names of banners are object properties
$banners
=
array_keys
(
(
array
)
$banners
);
}
else
{
$banners
=
[];
}
return
[
'start'
=>
$settings
[
'start'
],
'end'
=>
$settings
[
'end'
],
'enabled'
=>
(
bool
)
$settings
[
'enabled'
],
'archived'
=>
(
bool
)
$settings
[
'archived'
],
'banners'
=>
$banners
,
];
}
/**
* Check that a string is a valid setting name.
* @param string $settingName
* @return bool
*/
private
static
function
settingNameIsValid
(
$settingName
)
{
return
(
preg_match
(
'/^[a-z_]*$/'
,
$settingName
)
===
1
);
}
public
static
function
setType
(
$campaignName
,
$type
)
{
// Following pattern from setNumericaCampaignSettings() and exiting with no
// error if the campaign doesn't exist. TODO Is this right?
if
(
!
self
::
campaignExists
(
$campaignName
)
)
{
return
;
}
$dbw
=
CNDatabase
::
getDb
(
DB_PRIMARY
);
$dbw
->
newUpdateQueryBuilder
()
->
update
(
'cn_notices'
)
->
set
(
[
'not_type'
=>
$type
]
)
->
where
(
[
'not_name'
=>
$campaignName
]
)
->
caller
(
__METHOD__
)
->
execute
();
}
public
static
function
campaignLogs
(
$campaign
=
false
,
$username
=
false
,
$start
=
false
,
$end
=
false
,
$limit
=
50
,
$offset
=
0
)
{
// Read from the primary database to avoid concurrency problems
$dbr
=
CNDatabase
::
getDb
();
$conds
=
[];
if
(
$start
)
{
$conds
[]
=
$dbr
->
expr
(
'notlog_timestamp'
,
'>='
,
$start
);
}
if
(
$end
)
{
$conds
[]
=
$dbr
->
expr
(
'notlog_timestamp'
,
'<'
,
$end
);
}
if
(
$campaign
)
{
// This used to be a LIKE, but that was undocumented,
// and filters prevented the % and \ character from being
// used. The one character _ wildcard could have been used
// from the api, but that was completely undocumented.
// This was sketchy security wise, so the LIKE was removed.
$conds
[
"notlog_not_name"
]
=
$campaign
;
}
if
(
$username
)
{
$user
=
User
::
newFromName
(
$username
);
if
(
$user
)
{
$conds
[
"notlog_user_id"
]
=
$user
->
getId
();
}
}
$res
=
$dbr
->
newSelectQueryBuilder
()
->
select
(
'*'
)
->
from
(
'cn_notice_log'
)
->
where
(
$conds
)
->
orderBy
(
'notlog_timestamp'
,
SelectQueryBuilder
::
SORT_DESC
)
->
limit
(
$limit
)
->
offset
(
$offset
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
$logs
=
[];
foreach
(
$res
as
$row
)
{
$entry
=
new
CampaignLog
(
$row
);
$logs
[]
=
array_merge
(
get_object_vars
(
$entry
),
$entry
->
changes
()
);
}
return
$logs
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Jul 3, 20:40 (1 d, 10 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ab/7b/ab5f771597dd469d26416175d668
Default Alt Text
Campaign.php (49 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment