Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5989853
ContentModelHandler.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
8 KB
Referenced Files
None
Subscribers
None
ContentModelHandler.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
Wikimedia\Parsoid\Wikitext
;
use
Wikimedia\Parsoid\Config\Env
;
use
Wikimedia\Parsoid\Core\ContentModelHandler
as
IContentModelHandler
;
use
Wikimedia\Parsoid\Core\SelectiveUpdateData
;
use
Wikimedia\Parsoid\DOM\Document
;
use
Wikimedia\Parsoid\Ext\DOMProcessor
as
ExtDOMProcessor
;
use
Wikimedia\Parsoid\Ext\ParsoidExtensionAPI
;
use
Wikimedia\Parsoid\Html2Wt\RemoveRedLinks
;
use
Wikimedia\Parsoid\Html2Wt\SelectiveSerializer
;
use
Wikimedia\Parsoid\Html2Wt\WikitextSerializer
;
use
Wikimedia\Parsoid\Utils\ContentUtils
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
use
Wikimedia\Parsoid\Utils\DOMDataUtils
;
use
Wikimedia\Parsoid\Utils\Timing
;
class
ContentModelHandler
extends
IContentModelHandler
{
/** @var Env */
private
$env
;
/**
* Sneak an environment in here since it's not exposed as part of the
* ParsoidExtensionAPI
*
* @param Env $env
*/
public
function
__construct
(
Env
$env
)
{
$this
->
env
=
$env
;
}
/**
* Bring DOM to expected canonical form
*/
private
function
canonicalizeDOM
(
Env
$env
,
Document
$doc
,
bool
$isSelectiveUpdate
):
void
{
$body
=
DOMCompat
::
getBody
(
$doc
);
// Convert DOM to internal canonical form
DOMDataUtils
::
visitAndLoadDataAttribs
(
$body
,
[
'markNew'
=>
!
$isSelectiveUpdate
,
]
);
// Update DSR offsets if necessary.
ContentUtils
::
convertOffsets
(
$env
,
$doc
,
$env
->
getRequestOffsetType
(),
'byte'
);
// Strip <section> and mw:FallbackId <span> tags, if present,
// as well as extended annotation wrappers.
// This ensures that we can accept HTML from CX / VE
// and other clients that might have stripped them.
ContentUtils
::
stripUnnecessaryWrappersAndSyntheticNodes
(
$body
);
$redLinkRemover
=
new
RemoveRedLinks
(
$this
->
env
);
$redLinkRemover
->
run
(
$body
);
}
/**
* Fetch prior DOM for selser.
*
* @param ParsoidExtensionAPI $extApi
* @param SelectiveUpdateData $selserData
*/
private
function
setupSelser
(
ParsoidExtensionAPI
$extApi
,
SelectiveUpdateData
$selserData
)
{
$env
=
$this
->
env
;
// Why is it safe to use a reparsed dom for dom diff'ing?
// (Since that's the only use of `env.page.dom`)
//
// There are two types of non-determinism to discuss:
//
// * The first is from parsoid generated ids. At this point,
// data-attributes have already been applied so there's no chance
// that variability in the ids used to associate data-attributes
// will lead to data being applied to the wrong nodes.
//
// Further, although about ids will differ, they belong to the set
// of ignorable attributes in the dom differ.
//
// * Templates, and encapsulated content in general, are the second.
// Since that content can change in between parses, the resulting
// dom might not be the same. However, because dom diffing on
// on those regions only uses data-mw for comparision (which will
// remain constant between parses), this also shouldn't be an
// issue.
//
// There is one caveat. Because encapsulated content isn't
// guaranteed to be "balanced", the template affected regions
// may change between parses. This should be rare.
//
// We therefore consider this safe since it won't corrupt the page
// and, at worst, mixed up diff'ing annotations can end up with an
// unfaithful serialization of the edit.
//
// However, in cases where original content is not returned by the
// client / RESTBase, selective serialization cannot proceed and
// we're forced to fallback to normalizing the entire page. This has
// proved unacceptable to editors as is and, as we lean heavier on
// selser, will only get worse over time.
//
// So, we're forced to trade off the correctness for usability.
if
(
$selserData
->
revHTML
===
null
)
{
$env
->
log
(
"warn/html2wt"
,
"Missing selserData->revHTML. Regenerating."
);
// FIXME(T266838): Create a new Env for this parse? Something is
// needed to avoid this rigmarole.
$topLevelDoc
=
$env
->
topLevelDoc
;
$env
->
setupTopLevelDoc
();
// This effectively parses $selserData->revText for us because
// $selserData->revText = $env->getPageconfig()->getPageMainContent()
$doc
=
$this
->
toDOM
(
$extApi
);
$env
->
topLevelDoc
=
$topLevelDoc
;
}
else
{
$doc
=
ContentUtils
::
createDocument
(
$selserData
->
revHTML
,
true
);
}
$this
->
canonicalizeDOM
(
$env
,
$doc
,
false
);
$selserData
->
revDOM
=
$doc
;
}
private
function
processIndicators
(
Document
$doc
,
ParsoidExtensionAPI
$extApi
):
void
{
// Erroneous indicators without names will be <span>s
$indicators
=
DOMCompat
::
querySelectorAll
(
$doc
,
'meta[typeof~="mw:Extension/indicator"]'
);
$iData
=
[];
// https://www.mediawiki.org/wiki/Help:Page_status_indicators#Adding_page_status_indicators
// says that last one wins. But, that may just be documentation of the
// implementation vs. being a deliberate strategy.
//
// The indicators are ordered by depth-first pre-order DOM traversal.
// This ensures that the indicators are in document textual order.
// Given that, the for-loop below implements "last-one-wins" semantics
// for indicators that use the same name key.
foreach
(
$indicators
as
$meta
)
{
// Since the DOM is in "stored" state, we have to reparse data-mw here.
$codec
=
DOMDataUtils
::
getCodec
(
$doc
);
$dataMwAttr
=
DOMCompat
::
getAttribute
(
$meta
,
'data-mw'
);
$dmw
=
$dataMwAttr
===
null
?
null
:
$codec
->
newFromJsonString
(
$dataMwAttr
,
DOMDataUtils
::
getCodecHints
()[
'data-mw'
]
);
$name
=
$dmw
->
attrs
->
name
;
$iData
[
$name
]
=
$dmw
->
html
;
}
// set indicator metadata for unique keys
foreach
(
$iData
as
$name
=>
$html
)
{
$extApi
->
getMetadata
()->
setIndicator
(
(
string
)
$name
,
$html
);
}
}
/**
* @inheritDoc
*/
public
function
toDOM
(
ParsoidExtensionAPI
$extApi
,
?
SelectiveUpdateData
$selectiveUpdateData
=
null
):
Document
{
$env
=
$this
->
env
;
$pipelineFactory
=
$env
->
getPipelineFactory
();
if
(
$selectiveUpdateData
)
{
$doc
=
ContentUtils
::
createDocument
(
$selectiveUpdateData
->
revHTML
,
true
);
$env
->
setupTopLevelDoc
(
$doc
);
$this
->
canonicalizeDOM
(
$env
,
$env
->
topLevelDoc
,
true
);
$selectiveUpdateData
->
revDOM
=
$doc
;
$doc
=
$pipelineFactory
->
selectiveDOMUpdate
(
$selectiveUpdateData
);
}
else
{
$doc
=
$pipelineFactory
->
parse
(
// @phan-suppress-next-line PhanDeprecatedFunction not ready for topFrame yet
$env
->
getPageConfig
()->
getPageMainContent
()
);
}
// Hardcoded support for indicators
// TODO: Eventually we'll want to apply this to selective updates as well
if
(
!
$selectiveUpdateData
)
{
$this
->
processIndicators
(
$doc
,
$extApi
);
}
return
$doc
;
}
/**
* Preprocess the edited DOM as required before attempting to convert it to wikitext
* 1. The edited DOM (represented by body) might not be in canonical form
* because Parsoid might be providing server-side management of global state
* for extensions. To address this and bring the DOM back to canonical form,
* we run extension-provided handlers. The original DOM isn't subject to this problem.
* FIXME: But, this is not the only reason an extension might register a preprocessor.
* How do we know when to run a preprocessor on both original & edited DOMs?
* 2. We need to do this after all data attributes have been loaded.
* 3. We need to do this before we run dom-diffs to eliminate spurious diffs.
*
* @param Env $env
* @param Document $doc
*/
private
function
preprocessEditedDOM
(
Env
$env
,
Document
$doc
):
void
{
$siteConfig
=
$env
->
getSiteConfig
();
// Run any registered DOM preprocessors
foreach
(
$siteConfig
->
getExtDOMProcessors
()
as
$extName
=>
$domProcs
)
{
foreach
(
$domProcs
as
$i
=>
$classNameOrSpec
)
{
$c
=
$siteConfig
->
getObjectFactory
()->
createObject
(
$classNameOrSpec
,
[
'allowClassName'
=>
true
,
'assertClass'
=>
ExtDOMProcessor
::
class
,
]
);
$c
->
htmlPreprocess
(
new
ParsoidExtensionAPI
(
$env
),
DOMCompat
::
getBody
(
$doc
)
);
}
}
}
/**
* @inheritDoc
*/
public
function
fromDOM
(
ParsoidExtensionAPI
$extApi
,
?
SelectiveUpdateData
$selserData
=
null
):
string
{
$env
=
$this
->
env
;
$siteConfig
=
$env
->
getSiteConfig
();
$setupTiming
=
Timing
::
start
(
$siteConfig
);
$this
->
canonicalizeDOM
(
$env
,
$env
->
topLevelDoc
,
false
);
$serializerOpts
=
[
'selserData'
=>
$selserData
];
if
(
$selserData
)
{
$serializer
=
new
SelectiveSerializer
(
$env
,
$serializerOpts
);
$this
->
setupSelser
(
$extApi
,
$selserData
);
$wtsType
=
'selser'
;
}
else
{
// Fallback
$serializer
=
new
WikitextSerializer
(
$env
,
$serializerOpts
);
$wtsType
=
'noselser'
;
}
$setupTiming
->
end
(
'html2wt.setup'
,
'html2wt_setup_seconds'
,
[]
);
$preprocTiming
=
Timing
::
start
(
$siteConfig
);
$this
->
preprocessEditedDOM
(
$env
,
$env
->
topLevelDoc
);
$preprocTiming
->
end
(
'html2wt.preprocess'
,
'html2wt_preprocess_seconds'
,
[]
);
$serializeTiming
=
Timing
::
start
(
$siteConfig
);
$res
=
$serializer
->
serializeDOM
(
$env
->
topLevelDoc
);
$serializeTiming
->
end
(
"html2wt.{$wtsType}.serialize"
,
"html2wt_serialize_seconds"
,
[
'wts'
=>
$wtsType
]
);
return
$res
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Oct 4, 01:16 (3 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
f2/83/1750f438b35eb1e132f314a4c376
Default Alt Text
ContentModelHandler.php (8 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment