1<?php
2/**
3 * Object for storing information about the effects of an edit.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 *
22 * @author Ostrzyciel
23 */
24
25namespace MediaWiki\Storage;
26
27use JsonSerializable;
28
29/**
30 * Object for storing information about the effects of an edit.
31 *
32 * This object should be constructed by an EditResultBuilder with relevant information filled in
33 * during the process of saving the revision by the PageUpdater. You can use it to extract
34 * information about whether the edit was a revert and which edits were reverted.
35 *
36 * @since 1.35
37 */
38class EditResult implements JsonSerializable {
39
40	// revert methods
41	public const REVERT_UNDO = 1;
42	public const REVERT_ROLLBACK = 2;
43	public const REVERT_MANUAL = 3;
44
45	private const SERIALIZATION_FORMAT_VERSION = '1';
46
47	/** @var bool */
48	private $isNew;
49
50	/** @var bool|int */
51	private $originalRevisionId;
52
53	/** @var int|null */
54	private $revertMethod;
55
56	/** @var int|null */
57	private $newestRevertedRevId;
58
59	/** @var int|null */
60	private $oldestRevertedRevId;
61
62	/** @var bool */
63	private $isExactRevert;
64
65	/** @var bool */
66	private $isNullEdit;
67
68	/** @var string[] */
69	private $revertTags;
70
71	/**
72	 * EditResult constructor.
73	 *
74	 * @param bool $isNew
75	 * @param bool|int $originalRevisionId
76	 * @param int|null $revertMethod
77	 * @param int|null $oldestReverted
78	 * @param int|null $newestReverted
79	 * @param bool $isExactRevert
80	 * @param bool $isNullEdit
81	 * @param string[] $revertTags
82	 *
83	 * @internal Use EditResultBuilder for constructing EditResults.
84	 */
85	public function __construct(
86		bool $isNew,
87		$originalRevisionId,
88		?int $revertMethod,
89		?int $oldestReverted,
90		?int $newestReverted,
91		bool $isExactRevert,
92		bool $isNullEdit,
93		array $revertTags
94	) {
95		$this->isNew = $isNew;
96		$this->originalRevisionId = $originalRevisionId;
97		$this->revertMethod = $revertMethod;
98		$this->oldestRevertedRevId = $oldestReverted;
99		$this->newestRevertedRevId = $newestReverted;
100		$this->isExactRevert = $isExactRevert;
101		$this->isNullEdit = $isNullEdit;
102		$this->revertTags = $revertTags;
103	}
104
105	/**
106	 * Recreate the EditResult object from its array representation.
107	 *
108	 * This must ONLY be used for deserializing EditResult objects serialized using
109	 * EditResult::jsonSerialize(). The structure of the array may change without prior
110	 * notice.
111	 *
112	 * Any changes to the format are guaranteed to be backwards-compatible, so this
113	 * method will work fine with old serialized EditResults.
114	 *
115	 * For constructing EditResult objects from scratch use EditResultBuilder.
116	 *
117	 * @see EditResult::jsonSerialize()
118	 *
119	 * @param array $a
120	 * @phpcs:ignore Generic.Files.LineLength
121	 * @phan-param array{isNew:bool,originalRevisionId:bool|int,revertMethod:int|null,newestRevertedRevId:int|null,oldestRevertedRevId:int|null,isExactRevert:bool,isNullEdit:bool,revertTags:string[],version:string} $a
122	 *
123	 * @return EditResult
124	 *
125	 * @since 1.36
126	 */
127	public static function newFromArray( array $a ) {
128		return new self(
129			$a['isNew'],
130			$a['originalRevisionId'],
131			$a['revertMethod'],
132			$a['oldestRevertedRevId'],
133			$a['newestRevertedRevId'],
134			$a['isExactRevert'],
135			$a['isNullEdit'],
136			$a['revertTags']
137		);
138	}
139
140	/**
141	 * Returns the ID of the most recent revision that was reverted by this edit.
142	 * The same as getOldestRevertedRevisionId if only a single revision was
143	 * reverted. Returns null if the edit was not a revert.
144	 *
145	 * @see EditResult::isRevert() for information on how a revert is defined
146	 *
147	 * @return int|null
148	 */
149	public function getNewestRevertedRevisionId() : ?int {
150		return $this->newestRevertedRevId;
151	}
152
153	/**
154	 * Returns the ID of the oldest revision that was reverted by this edit.
155	 * The same as getOldestRevertedRevisionId if only a single revision was
156	 * reverted. Returns null if the edit was not a revert.
157	 *
158	 * @see EditResult::isRevert() for information on how a revert is defined
159	 *
160	 * @return int|null
161	 */
162	public function getOldestRevertedRevisionId() : ?int {
163		return $this->oldestRevertedRevId;
164	}
165
166	/**
167	 * If the edit was an undo, returns the oldest revision that was undone.
168	 * Method kept for compatibility reasons.
169	 *
170	 * @return int
171	 */
172	public function getUndidRevId() : int {
173		if ( $this->getRevertMethod() !== self::REVERT_UNDO ) {
174			return 0;
175		}
176		return $this->getOldestRevertedRevisionId() ?? 0;
177	}
178
179	/**
180	 * Returns the ID of an earlier revision that is being repeated or restored.
181	 *
182	 * The original revision's content should match the new revision exactly.
183	 *
184	 * @return bool|int The original revision id, or false if no earlier revision is known to be
185	 * repeated or restored.
186	 * The old PageUpdater::getOriginalRevisionId() returned false in such cases. This value would
187	 * be then passed on to extensions through hooks, so it may be wise to keep compatibility with
188	 * the old behavior.
189	 */
190	public function getOriginalRevisionId() {
191		return $this->originalRevisionId;
192	}
193
194	/**
195	 * Whether the edit created a new page
196	 *
197	 * @return bool
198	 */
199	public function isNew() : bool {
200		return $this->isNew;
201	}
202
203	/**
204	 * Whether the edit was a revert, not necessarily exact.
205	 *
206	 * An edit is considered a revert if it either:
207	 * - Restores the page to an exact previous state (rollbacks, manual reverts and some undos).
208	 *   E.g. for edits A B C D, edits C and D are reverted.
209	 * - Undoes some edits made previously, not necessarily restoring the page to an exact
210	 *   previous state (undo). It is guaranteed that the revert was a "clean" result of a
211	 *   three-way merge and no additional changes were made by the reverting user.
212	 *   E.g. for edits A B C D, edits B and C are reverted.
213	 *
214	 * To check whether the edit was an exact revert, please use the isExactRevert() method.
215	 * The getRevertMethod() will provide additional information about which kind of revert
216	 * was made.
217	 *
218	 * @return bool
219	 */
220	public function isRevert() : bool {
221		return !$this->isNew() && $this->getOldestRevertedRevisionId();
222	}
223
224	/**
225	 * Returns the revert method that was used to perform the edit, if any changes were reverted.
226	 * Returns null if the edit was not a revert.
227	 *
228	 * Possible values: REVERT_UNDO, REVERT_ROLLBACK, REVERT_MANUAL
229	 *
230	 * @see EditResult::isRevert()
231	 *
232	 * @return int|null
233	 */
234	public function getRevertMethod() : ?int {
235		return $this->revertMethod;
236	}
237
238	/**
239	 * Whether the edit was an exact revert,
240	 * i.e. the contents of the revert revision and restored revision match
241	 *
242	 * @return bool
243	 */
244	public function isExactRevert() : bool {
245		return $this->isExactRevert;
246	}
247
248	/**
249	 * An edit is a null edit if the original revision is equal to the parent revision,
250	 * i.e. no changes were made.
251	 *
252	 * @return bool
253	 */
254	public function isNullEdit() : bool {
255		return $this->isNullEdit;
256	}
257
258	/**
259	 * Returns an array of revert-related tags that were applied automatically to this edit.
260	 *
261	 * @return string[]
262	 */
263	public function getRevertTags() : array {
264		return $this->revertTags;
265	}
266
267	/**
268	 * Returns an array representing the EditResult object.
269	 *
270	 * @see EditResult::newFromArray()
271	 *
272	 * @return array
273	 * @phpcs:ignore Generic.Files.LineLength
274	 * @phan-return array{isNew:bool,originalRevisionId:bool|int,revertMethod:int|null,newestRevertedRevId:int|null,oldestRevertedRevId:int|null,isExactRevert:bool,isNullEdit:bool,revertTags:string[],version:string}
275	 *
276	 * @since 1.36
277	 */
278	public function jsonSerialize() {
279		return [
280			'isNew' => $this->isNew,
281			'originalRevisionId' => $this->originalRevisionId,
282			'revertMethod' => $this->revertMethod,
283			'newestRevertedRevId' => $this->newestRevertedRevId,
284			'oldestRevertedRevId' => $this->oldestRevertedRevId,
285			'isExactRevert' => $this->isExactRevert,
286			'isNullEdit' => $this->isNullEdit,
287			'revertTags' => $this->revertTags,
288			'version' => self::SERIALIZATION_FORMAT_VERSION
289		];
290	}
291}
292