summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/UploadWizard/includes/UploadWizardCampaign.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/UploadWizard/includes/UploadWizardCampaign.php')
-rw-r--r--www/wiki/extensions/UploadWizard/includes/UploadWizardCampaign.php507
1 files changed, 507 insertions, 0 deletions
diff --git a/www/wiki/extensions/UploadWizard/includes/UploadWizardCampaign.php b/www/wiki/extensions/UploadWizard/includes/UploadWizardCampaign.php
new file mode 100644
index 00000000..ace937d9
--- /dev/null
+++ b/www/wiki/extensions/UploadWizard/includes/UploadWizardCampaign.php
@@ -0,0 +1,507 @@
+<?php
+
+/**
+ * Class that represents a single upload campaign.
+ * An upload campaign is stored as a row in the uw_campaigns table,
+ * and its configuration is stored in the Campaign: namespace
+ *
+ * This class is 'readonly' - to modify the campaigns, please
+ * edit the appropriate Campaign: namespace page
+ *
+ * @file
+ * @ingroup Upload
+ *
+ * @since 1.2
+ *
+ * @license GPL-2.0-or-later
+ * @author Yuvi Panda <yuvipanda@gmail.com>
+ * @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 <p> tags
+ $m = [];
+ if ( preg_match( '/^<p>(.*)\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] );
+ }
+ }
+ }
+ }
+}