* @author Jeroen De Dauw < jeroendedauw@gmail.com > */ class UploadWizardCampaign { /** * The campaign configuration. * * @since 1.2 * @var array */ protected $config = []; /** * The campaign configuration, after wikitext properties have been parsed. * * @since 1.2 * @var array|null */ protected $parsedConfig = null; /** * Array of templates used in this campaign. * Each item is an array with ( namespace, template_title ) * Stored without deduplication * * @since 1.2 * @var array */ protected $templates = []; /** * The Title representing the current campaign * * @since 1.4 * @var Title|null */ protected $title = null; /** * The RequestContext to use for operations performed from this object * * @since 1.4 * @var RequestContext|null */ protected $context = null; public static function newFromName( $name ) { $campaignTitle = Title::makeTitleSafe( NS_CAMPAIGN, $name ); if ( $campaignTitle === null || !$campaignTitle->exists() ) { return false; } return new UploadWizardCampaign( $campaignTitle ); } function __construct( $title, $config = null, $context = null ) { $this->title = $title; if ( $config === null ) { $content = WikiPage::factory( $title )->getContent(); if ( $content->getModel() !== 'Campaign' ) { throw new MWException( 'Wrong content model' ); } $this->config = $content->getJsonData(); } else { $this->config = $config; } if ( $context === null ) { $this->context = RequestContext::getMain(); } else { $this->context = $context; } } /** * Returns true if current campaign is enabled * * @since 1.4 * * @return bool */ public function getIsEnabled() { return $this->config['enabled']; } /** * Returns name of current campaign * * @since 1.4 * * @return string */ public function getName() { return $this->title->getDBkey(); } public function getTitle() { return $this->title; } public function getTrackingCategory() { $trackingCats = UploadWizardConfig::getSetting( 'trackingCategory' ); return Title::makeTitleSafe( NS_CATEGORY, str_replace( '$1', $this->getName(), $trackingCats['campaign'] ) ); } public function getUploadedMediaCount() { return Category::newFromTitle( $this->getTrackingCategory() )->getFileCount(); } public function getTotalContributorsCount() { global $wgMemc; $key = wfMemcKey( 'uploadwizard', 'campaign', $this->getName(), 'contributors-count' ); $data = $wgMemc->get( $key ); if ( $data === false ) { wfDebug( __METHOD__ . ' cache miss for key ' . $key ); $dbr = wfGetDB( DB_REPLICA ); if ( class_exists( ActorMigration::class ) ) { $actorQuery = ActorMigration::newMigration()->getJoin( 'img_user' ); } else { $actorQuery = [ 'tables' => [], 'fields' => [ 'img_user' => 'img_user' ], 'joins' => [], ]; } $result = $dbr->select( [ 'categorylinks', 'page', 'image' ] + $actorQuery['tables'], [ 'count' => 'COUNT(DISTINCT ' . $actorQuery['fields']['img_user'] . ')' ], [ 'cl_to' => $this->getTrackingCategory()->getDBKey(), 'cl_type' => 'file' ], __METHOD__, [ 'USE INDEX' => [ 'categorylinks' => 'cl_timestamp' ] ], [ 'page' => [ 'INNER JOIN', 'cl_from=page_id' ], 'image' => [ 'INNER JOIN', 'page_title=img_name' ] ] + $actorQuery['joins'] ); $data = $result->current()->count; $wgMemc->set( $key, $data, UploadWizardConfig::getSetting( 'campaignStatsMaxAge' ) ); } return $data; } public function getUploadedMedia( $limit = 24 ) { $dbr = wfGetDB( DB_REPLICA ); $result = $dbr->select( [ 'categorylinks', 'page' ], [ 'cl_from', 'page_namespace', 'page_title' ], [ 'cl_to' => $this->getTrackingCategory()->getDBKey(), 'cl_type' => 'file' ], __METHOD__, [ 'ORDER BY' => 'cl_timestamp DESC', 'LIMIT' => $limit, 'USE INDEX' => [ 'categorylinks' => 'cl_timestamp' ] ], [ 'page' => [ 'INNER JOIN', 'cl_from=page_id' ] ] ); $images = []; foreach ( $result as $row ) { $images[] = Title::makeTitle( $row->page_namespace, $row->page_title ); } return $images; } /** * Returns all set config properties. * Property name => property value * * @since 1.2 * * @return array */ public function getRawConfig() { return $this->config; } /** * Update internal list of templates used in parsing this campaign * * @param ParserOutput $parserOutput */ private function updateTemplates( ParserOutput $parserOutput ) { $templateIds = $parserOutput->getTemplateIds(); foreach ( $parserOutput->getTemplates() as $ns => $templates ) { foreach ( $templates as $dbk => $id ) { $this->templates[$ns][$dbk] = [ $id, $templateIds[$ns][$dbk] ]; } } } /** * Wrapper around OutputPage::parseInline * * @param string $value Wikitext to parse * @param Language $lang * * @since 1.3 * * @return String parsed wikitext */ private function parseValue( $value, Language $lang ) { global $wgParser; $parserOptions = ParserOptions::newFromContext( $this->context ); if ( !defined( 'ParserOutput::SUPPORTS_STATELESS_TRANSFORMS' ) ) { $parserOptions->setEditSection( false ); } $parserOptions->setInterfaceMessage( true ); $parserOptions->setUserLang( $lang ); $parserOptions->setTargetLanguage( $lang ); $parserOptions->setTidy( true ); $output = $wgParser->parse( $value, $this->getTitle(), $parserOptions ); $parsed = $output->getText( [ 'enableSectionEditLinks' => false, ] ); // Strip out the surrounding

tags $m = []; if ( preg_match( '/^

(.*)\n?<\/p>\n?/sU', $parsed, $m ) ) { $parsed = $m[1]; } $this->updateTemplates( $output ); return $parsed; } /** * Parses the values in an assoc array as wikitext * * @param array $array * @param array $forKeys Array of keys whose values should be parsed * * @since 1.3 * * @return array */ private function parseArrayValues( $array, $lang, $forKeys = null ) { $parsed = []; foreach ( $array as $key => $value ) { if ( $forKeys !== null ) { if ( in_array( $key, $forKeys ) ) { if ( is_array( $value ) ) { $parsed[$key] = $this->parseArrayValues( $value, $lang ); } else { $parsed[$key] = $this->parseValue( $value, $lang ); } } else { $parsed[$key] = $value; } } else { $parsed[$key] = $this->parseValue( $value, $lang ); } } return $parsed; } /** * Returns all config parameters, after parsing the wikitext based ones * * @since 1.3 * * @param Language|null $lang * @return array */ public function getParsedConfig( $lang = null ) { if ( $lang === null ) { $lang = $this->context->getLanguage(); } // We check if the parsed config for this campaign is cached. If it is available in cache, // we then check to make sure that it is the latest version - by verifying that its // timestamp is greater than or equal to the timestamp of the last time an invalidate was // issued. $cache = ObjectCache::getMainWANInstance(); $memKey = wfMemcKey( 'uploadwizard', 'campaign', $this->getName(), 'parsed-config', $lang->getCode() ); $depKeys = [ $this->makeInvalidateTimestampKey() ]; $curTTL = null; $memValue = $cache->get( $memKey, $curTTL, $depKeys ); if ( is_array( $memValue ) && $curTTL > 0 ) { $this->parsedConfig = $memValue['config']; } if ( $this->parsedConfig === null ) { $parsedConfig = []; foreach ( $this->config as $key => $value ) { switch ( $key ) { case "title": case "description": $parsedConfig[$key] = $this->parseValue( $value, $lang ); break; case "display": foreach ( $value as $option => $optionValue ) { if ( is_array( $optionValue ) ) { $parsedConfig['display'][$option] = $this->parseArrayValues( $optionValue, $lang, [ 'label' ] ); } else { $parsedConfig['display'][$option] = $this->parseValue( $optionValue, $lang ); } } break; case "fields": $parsedConfig['fields'] = []; foreach ( $value as $field ) { $parsedConfig['fields'][] = $this->parseArrayValues( $field, $lang, [ 'label', 'options' ] ); } break; case "whileActive": case "afterActive": case "beforeActive": if ( array_key_exists( 'display', $value ) ) { $value['display'] = $this->parseArrayValues( $value['display'], $lang ); } $parsedConfig[$key] = $value; break; default: $parsedConfig[$key] = $value; break; } } $this->parsedConfig = $parsedConfig; $cache->set( $memKey, [ 'timestamp' => time(), 'config' => $parsedConfig ] ); } $uwDefaults = UploadWizardConfig::getSetting( 'defaults' ); if ( array_key_exists( 'objref', $uwDefaults ) ) { $this->applyObjectReferenceToButtons( $uwDefaults['objref'] ); } $this->modifyIfNecessary(); return $this->parsedConfig; } /** * Modifies the parsed config if there are time-based modifiers that are active. */ protected function modifyIfNecessary() { foreach ( $this->parsedConfig as $cnf => $modifiers ) { if ( $cnf === 'whileActive' && $this->isActive() ) { $activeModifiers = $modifiers; } elseif ( $cnf === 'afterActive' && $this->wasActive() ) { $activeModifiers = $modifiers; } elseif ( $cnf === 'beforeActive' ) { $activeModifiers = $modifiers; } } if ( isset( $activeModifiers ) ) { foreach ( $activeModifiers as $cnf => $modifier ) { switch ( $cnf ) { case "autoAdd": case "display": if ( !array_key_exists( $cnf, $this->parsedConfig ) ) { $this->parsedConfig[$cnf] = []; } $this->parsedConfig[$cnf] = array_merge( $this->parsedConfig[$cnf], $modifier ); break; } } } } /** * Returns the templates used in this Campaign's config * * @return array [ns => [ dbk => [page_id, rev_id ] ] ] */ public function getTemplates() { if ( $this->parsedConfig === null ) { $this->getParsedConfig(); } return $this->templates; } /** * Invalidate the cache for this campaign, in all languages * * Does so by simply writing a new invalidate timestamp to memcached. * Since this invalidate timestamp is checked on every read, the cached entries * for the campaign will be regenerated the next time there is a read. */ public function invalidateCache() { $cache = ObjectCache::getMainWANInstance(); $cache->touchCheckKey( $this->makeInvalidateTimestampKey() ); } /** * Returns key used to store the last time the cache for a particular campaign was invalidated * * @return String */ private function makeInvalidateTimestampKey() { return wfMemcKey( 'uploadwizard', 'campaign', $this->getName(), 'parsed-config', 'invalidate-timestamp' ); } /** * Checks the current date against the configured start and end dates to determine * whether the campaign is currently active. */ private function isActive() { $today = strtotime( date( "Y-m-d" ) ); $start = array_key_exists( 'start', $this->parsedConfig ) ? strtotime( $this->parsedConfig['start'] ) : null; $end = array_key_exists( 'end', $this->parsedConfig ) ? strtotime( $this->parsedConfig['end'] ) : null; return ( $start === null || $start <= $today ) && ( $end === null || $end > $today ); } /** * Checks the current date against the configured start and end dates to determine * whether the campaign is currently active. */ private function wasActive() { $today = strtotime( date( "Y-m-d" ) ); $start = array_key_exists( 'start', $this->parsedConfig ) ? strtotime( $this->parsedConfig['start'] ) : null; return $start === null || $start <= $today; } /** * Generate the URL out of the object reference * * @param string $objRef * @return bool|string */ private function getButtonHrefByObjectReference( $objRef ) { $arrObjRef = explode( '|', $objRef ); if ( count( $arrObjRef ) > 1 ) { list( $wiki, $title ) = $arrObjRef; if ( Interwiki::isValidInterwiki( $wiki ) ) { return str_replace( '$1', $title, Interwiki::fetch( $wiki )->getURL() ); } } return false; } /** * Apply given object reference to buttons configured to use it as href * * @param string $objRef */ private function applyObjectReferenceToButtons( $objRef ) { $customizableButtons = [ 'homeButton', 'beginButton' ]; foreach ( $customizableButtons as $button ) { if ( isset( $this->parsedConfig['display'][$button]['target'] ) && $this->parsedConfig['display'][$button]['target'] === 'useObjref' ) { $validUrl = $this->getButtonHrefByObjectReference( $objRef ); if ( $validUrl ) { $this->parsedConfig['display'][$button]['target'] = $validUrl; } else { unset( $this->parsedConfig['display'][$button] ); } } } } }