1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21/**
22 * DeferredUpdates helper class for managing DeferrableUpdate::doUpdate() nesting levels
23 * caused by nested calls to DeferredUpdates::doUpdates()
24 *
25 * @see DeferredUpdates
26 * @see DeferredUpdatesScopeStack
27 * @internal For use by DeferredUpdates and DeferredUpdatesScopeStack only
28 * @since 1.36
29 */
30class DeferredUpdatesScope {
31	/** @var DeferredUpdatesScope|null Parent scope (root scope as none) */
32	private $parentScope;
33	/** @var DeferrableUpdate|null Deferred update that owns this scope (root scope has none) */
34	private $activeUpdate;
35	/** @var int|null Active processing stage in DeferredUpdates::STAGES (if any) */
36	private $activeStage;
37	/** @var DeferrableUpdate[][] Stage-ordered (stage => merge class or position => update) map */
38	private $queueByStage;
39
40	/**
41	 * @param int|null $activeStage One of DeferredUpdates::STAGES or null
42	 * @param DeferrableUpdate|null $update
43	 * @param DeferredUpdatesScope|null $parentScope
44	 */
45	private function __construct(
46		$activeStage,
47		?DeferrableUpdate $update,
48		?DeferredUpdatesScope $parentScope
49	) {
50		$this->activeStage = $activeStage;
51		$this->activeUpdate = $update;
52		$this->parentScope = $parentScope;
53		$this->queueByStage = array_fill_keys( DeferredUpdates::STAGES, [] );
54	}
55
56	/**
57	 * @return DeferredUpdatesScope Scope for the case of no in-progress deferred update
58	 */
59	public static function newRootScope() {
60		return new self( null, null, null );
61	}
62
63	/**
64	 * @param int $activeStage The in-progress stage; one of DeferredUpdates::STAGES
65	 * @param DeferrableUpdate $update The deferred update that owns this scope
66	 * @param DeferredUpdatesScope $parentScope The parent scope of this scope
67	 * @return DeferredUpdatesScope Scope for the case of an in-progress deferred update
68	 */
69	public static function newChildScope(
70		$activeStage,
71		DeferrableUpdate $update,
72		DeferredUpdatesScope $parentScope
73	) {
74		return new self( $activeStage, $update, $parentScope );
75	}
76
77	/**
78	 * Get the deferred update that owns this scope (root scope has none)
79	 *
80	 * @return DeferrableUpdate|null
81	 */
82	public function getActiveUpdate() {
83		return $this->activeUpdate;
84	}
85
86	/**
87	 * Enqueue a deferred update within this scope using the specified "defer until" time
88	 *
89	 * @param DeferrableUpdate $update
90	 * @param int $stage One of DeferredUpdates::STAGES
91	 */
92	public function addUpdate( DeferrableUpdate $update, $stage ) {
93		// Handle the case where the the specified stage must have already passed
94		$stageEffective = max( $stage, $this->activeStage );
95
96		$queue =& $this->queueByStage[$stageEffective];
97
98		if ( $update instanceof MergeableUpdate ) {
99			$class = get_class( $update ); // fully-qualified class
100			if ( isset( $queue[$class] ) ) {
101				/** @var MergeableUpdate $existingUpdate */
102				$existingUpdate = $queue[$class];
103				'@phan-var MergeableUpdate $existingUpdate';
104				$existingUpdate->merge( $update );
105				// Move the update to the end to handle things like mergeable purge
106				// updates that might depend on the prior updates in the queue running
107				unset( $queue[$class] );
108				$queue[$class] = $existingUpdate;
109			} else {
110				$queue[$class] = $update;
111			}
112		} else {
113			$queue[] = $update;
114		}
115	}
116
117	/**
118	 * Get the number of pending updates within this scope
119	 *
120	 * @return int
121	 */
122	public function pendingUpdatesCount() {
123		return array_sum( array_map( 'count', $this->queueByStage ) );
124	}
125
126	/**
127	 * Get pending updates within this scope with the given "defer until" stage
128	 *
129	 * @param int $stage One of DeferredUpdates::STAGES or DeferredUpdates::ALL
130	 * @return DeferrableUpdate[]
131	 */
132	public function getPendingUpdates( $stage ) {
133		$matchingQueues = [];
134		foreach ( $this->queueByStage as $queueStage => $queue ) {
135			if ( $stage === DeferredUpdates::ALL || $stage === $queueStage ) {
136				$matchingQueues[] = $queue;
137			}
138		}
139
140		return array_merge( ...$matchingQueues );
141	}
142
143	/**
144	 * Cancel all pending updates within this scope
145	 */
146	public function clearPendingUpdates() {
147		$this->queueByStage = array_fill_keys( array_keys( $this->queueByStage ), [] );
148	}
149
150	/**
151	 * Remove pending updates of the specified stage/class and pass them to a callback
152	 *
153	 * @param int $stage One of DeferredUpdates::STAGES or DeferredUpdates::ALL
154	 * @param string $class Only take updates of this fully qualified class/interface name
155	 * @param callable $callback Callback that takes DeferrableUpdate
156	 */
157	public function consumeMatchingUpdates( $stage, $class, callable $callback ) {
158		// T268840: defensively claim the pending updates in case of recursion
159		$claimedUpdates = [];
160		foreach ( $this->queueByStage as $queueStage => $queue ) {
161			if ( $stage === DeferredUpdates::ALL || $stage === $queueStage ) {
162				foreach ( $queue as $k => $update ) {
163					if ( $update instanceof $class ) {
164						$claimedUpdates[] = $update;
165						unset( $this->queueByStage[$queueStage][$k] );
166					}
167				}
168			}
169		}
170		// Execute the callback for each update
171		foreach ( $claimedUpdates as $update ) {
172			$callback( $update );
173		}
174	}
175
176	/**
177	 * Iteratively, reassign unready pending updates to the parent scope (if applicable) and
178	 * process the ready pending updates in stage-order with the callback, repeating the process
179	 * until there is nothing left to do
180	 *
181	 * @param int $stage One of DeferredUpdates::STAGES or DeferredUpdates::ALL
182	 * @param callable $callback Processing function with arguments (update, effective stage)
183	 */
184	public function processUpdates( $stage, callable $callback ) {
185		if ( $stage === DeferredUpdates::ALL ) {
186			// Do everything, all the way to the last "defer until" stage
187			$activeStage = DeferredUpdates::STAGES[count( DeferredUpdates::STAGES ) - 1];
188		} else {
189			// Handle the case where the the specified stage must have already passed
190			$activeStage = max( $stage, $this->activeStage );
191		}
192
193		do {
194			$processed = $this->upmergeUnreadyUpdates( $activeStage );
195			foreach ( range( DeferredUpdates::STAGES[0], $activeStage ) as $queueStage ) {
196				$processed += $this->processStageQueue( $queueStage, $activeStage, $callback );
197			}
198		} while ( $processed > 0 );
199	}
200
201	/**
202	 * If this is a child scope, then reassign unready pending updates to the parent scope:
203	 *   - MergeableUpdate instances will be reassigned to the parent scope on account of their
204	 *     de-duplication/melding semantics. They are normally only processed in the root scope.
205	 *   - DeferrableUpdate instances with a "defer until" stage later than the specified stage
206	 *     will be reassigned to the parent scope since they are not ready.
207	 *
208	 * @param int $activeStage One of DeferredUpdates::STAGES
209	 * @return int Number of updates moved
210	 */
211	private function upmergeUnreadyUpdates( $activeStage ) {
212		$reassigned = 0;
213
214		if ( !$this->parentScope ) {
215			return $reassigned;
216		}
217
218		foreach ( $this->queueByStage as $queueStage => $queue ) {
219			foreach ( $queue as $k => $update ) {
220				if ( $update instanceof MergeableUpdate || $queueStage > $activeStage ) {
221					unset( $this->queueByStage[$queueStage][$k] );
222					$this->parentScope->addUpdate( $update, $queueStage );
223					++$reassigned;
224				}
225			}
226		}
227
228		return $reassigned;
229	}
230
231	/**
232	 * @param int $stage One of DeferredUpdates::STAGES
233	 * @param int $activeStage One of DeferredUpdates::STAGES
234	 * @param callable $callback Processing function with arguments (update, effective stage)
235	 * @return int Number of updates processed
236	 */
237	private function processStageQueue( $stage, $activeStage, callable $callback ) {
238		$processed = 0;
239
240		// Defensively claim the pending updates in case of recursion
241		$claimedUpdates = $this->queueByStage[$stage];
242		$this->queueByStage[$stage] = [];
243
244		// Keep doing rounds of updates until none get enqueued...
245		while ( $claimedUpdates ) {
246			// Segregate the updates into one for DataUpdate and one for everything else.
247			// This is done for historical reasons; DataUpdate used to have its own static
248			// method for running DataUpdate instances and was called first in DeferredUpdates.
249			// Before that, page updater code directly ran that static method.
250			// @TODO: remove this logic given the existence of RefreshSecondaryDataUpdate
251			$claimedDataUpdates = [];
252			$claimedGenericUpdates = [];
253			foreach ( $claimedUpdates as $claimedUpdate ) {
254				if ( $claimedUpdate instanceof DataUpdate ) {
255					$claimedDataUpdates[] = $claimedUpdate;
256				} else {
257					$claimedGenericUpdates[] = $claimedUpdate;
258				}
259				++$processed;
260			}
261
262			// Execute the DataUpdate queue followed by the DeferrableUpdate queue...
263			foreach ( $claimedDataUpdates as $claimedDataUpdate ) {
264				$callback( $claimedDataUpdate, $activeStage );
265			}
266			foreach ( $claimedGenericUpdates as $claimedGenericUpdate ) {
267				$callback( $claimedGenericUpdate, $activeStage );
268			}
269
270			// Check for new entries;  defensively claim the pending updates in case of recursion
271			$claimedUpdates = $this->queueByStage[$stage];
272			$this->queueByStage[$stage] = [];
273		}
274
275		return $processed;
276	}
277}
278