Page MenuHomeWickedGov Phorge

Campaign.php
No OneTemporary

Size
49 KB
Referenced Files
None
Subscribers
None

Campaign.php

<?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

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)

Event Timeline