Skip to content

The documents this library writes are valid OOXML, and a gate keeps them that way - #43

Closed
fabeat wants to merge 13 commits into
mainfrom
feat/phpword-ooxml-patch
Closed

fabeat wants to merge 13 commits into
mainfrom
feat/phpword-ooxml-patch

Conversation

@fabeat

@fabeat fabeat commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

What this is

The issue it carries is PHPOffice/PHPWord#2963,
"The documents this library writes are not valid OOXML", and the five pull requests that
fix it upstream are
#2964,
#2965,
#2966,
#2968 and
#2969.

The documents this library writes are not valid Office Open XML, and nothing in the
suite could see it: the files open, because Word is lenient where the schema is not.
A reader that walks the schema — a validator, a converter, an accessibility tool —
stops at the first child it did not expect.

Microsoft's Open XML SDK validator — the one the Open XML SDK Productivity Tool
uses — reports 1002 errors over the twenty-nine documents examples/build.php
writes. This pull request brings that to 0, and adds the gate that keeps it
there.

It carries three things, and it supersedes
#41 and
#42, which are in it.

The measure

errors kinds
main today 1002 24
with the patch, the templates and #41 0 0

1. The gate

tools/ooxml-validator/ is a small console program over the Open XML SDK: it
opens a document, validates it, and exits non-zero on any error. The SDK version
is pinned, because the validator is the oracle the gate is measured against and a
gate whose oracle moves is not a gate.

The test runs it over the whole corpus — the twenty-nine documents the build
writes — rather than over documents the test writes itself, because a gate over a
selection is a gate over the selection. It is a gate and not a report: the test
fails on a document the validator rejects, and a second test asks the validator
about a document with an error in it and insists it is found. Without that, a
validator that said "no errors" about everything would pass the gate.

On a machine without the .NET SDK the test skips itself and says what is missing.
A job of its own installs the SDK, so the gate cannot be skipped in CI — and the
jobs that run the rest of the suite leave it out, so it is one red cross rather
than five.

2. The patch

PHPWord 1.4.0 writes the children of w:rPr, of w:pPr, of w:tblPr, of
w:sectPr and of w:style in an order of its own, writes a page size in twips
as a fraction, and writes a few elements where the format does not allow them.
Every document this library writes is a document PHPWord writes.

The fixes are in five pull requests upstream. Until they are released the library
carries them as a patch — the same way it has carried PHPPresentation's fixes
until now.

The project that installs this one has to allow cweagans/composer-patches. The
plugin applies the patch, and Composer runs it only when the project allows it:

{
    "config": {
        "allow-plugins": {
            "cweagans/composer-patches": true
        }
    }
}

The README says so, and markdown-presentation says it the same way.

The patch is against the release, not against the branch. A patch of
1.4.0 → master would carry everything upstream has done since 1.4.0 to those
files, and one of those changes is a feature rather than a fix: upstream writes a
style of its own for a title depth used without addTitleStyle(), and its style
id collides with the one this library registers for its own headings. Two styles
with one id is a document Word repairs, and it breaks the reader here. So each
file is merged instead — the released file with our change applied on top of
upstream's master as the common ancestor — and the result is a patch of 1,270
lines over eighteen files that carries the fixes and one piece of upstream's own: the
outline level of a heading style, which upstream moved to the end of a paragraph's
properties at the same time as the fixes, and which cannot be carried without it.

It applies to the released 1.4.0 with no conflict and no fuzz.

3. The templates

invoice.dotx, meridian-qbr.dotx and plain-letterhead.dotx were written by
PHPWord before the fixes, so they carried four of its defects inside them: a page
size in twips with a fraction, a style whose properties were in the wrong order,
the settings in the wrong order, and an inline picture in a header. Every document
rendered from a template inherited them — 44 of the 1002 errors were in the
templates
and not in the code that writes them.

They are written by the fixed library now, and they score 0 errors each.

4. The link's run properties

src/Writer/HyperlinkPass.php writes the w:rPr of a hyperlink's run by hand,
because PHPWord's Link element holds one plain string and cannot carry a label
with emphasis in it. The children were appended in the order the properties came
to mind — w:b, w:i, w:strike, w:u, w:color, w:sz, w:rFonts — and
w:rPr is a sequence in ECMA-376 and not a set, so the typeface was written last
where the schema names it second, and the underline was written before the colour
where the schema names it after the size. 19 errors, one for every styled
link.

The order is the schema's now: w:rFonts, w:b, w:i, w:strike, w:color,
w:sz, w:u.

This is #41, with the
two comments that said a document with the order wrong is one Word refuses to open
corrected: Word opens it, because Word is lenient where the schema is not.

The audit that came with it

Every place in the repository that builds a w:rPr or a w:pPr by hand was
looked at — BuiltInHeadingPass, WordFields, TocPass, NumberingMerger,
ContentControl and the example build — and every one of them is right.

The checks

composer check is green: the manifest, the audit, smoke.php, stress.php
and 3953 tests, the gate among them.

What the release needs

This library's release carries the patch, so the fix reaches the people who
install the package rather than only the people who check the repository out. The
patch comes out when upstream ships the fixes.

fabeat added 12 commits October 30, 2026 16:57
The properties of a hyperlink's run were written by hand, because PHPWord's
Link element holds one plain string and cannot carry a label with emphasis
in it. They were written in the order they came to mind: the weight, the
slant, the strike, the underline, the colour, the size and the typeface.

w:rPr is the sequence EG_RPrBase in ECMA-376's wml.xsd and not a set, so a
reader walks the children in the order they are written and stops at the
first one it did not expect. The typeface comes before the weight, and the
underline comes after the size, and a document that gets this wrong is one
Word refuses to open.

Microsoft's own validator reported "The element has unexpected child
element 'w:color'" nineteen times across the example documents, every one
of them a link whose label was styled. There are none of them now.
Two of them named a position in the schema's sequence, which is a number
that goes stale the moment the schema is read again, and one of them said
a run can hold every property, which is a claim about the schema rather
than about this library.
A `.docx` is Office Open XML, and a document that breaks the schema is a
document that is not what it claims to be. Word opens it anyway, because Word
is lenient where the schema is not, and nothing else in the suite could see it.

The validator is Microsoft's own — the one the Open XML SDK Productivity Tool
uses — and it is run over the examples: the whole corpus of documents this
library writes, built by `examples/build.php`. A gate over a selection is a
gate over the selection.

The test skips itself where the .NET SDK is not, and says what is missing; the
job that installs the SDK runs it where it is, so the gate cannot be skipped
there. A second test asks the validator about a document with an error in it,
which is what proves that the gate can fail.
Drawing a preview of each document's first page takes longer than everything
else the build does, and the gate that reads the documents wants the documents.

The previews are still emptied with the rest of `out/`: a preview of a
document that is no longer built is a picture of nothing, which is the same
thing the directory is emptied for.
The gate is red when it lands: over the twenty-nine documents the build
writes, Microsoft's validator reports a thousand and two errors, and with the
four fixes upstream in PHPWord's writer it reports a hundred and fifty-one, of
which forty-four belong to the three committed templates and eighty-eight to
PHPWord itself.
…e suite

The runner image ships a .NET SDK, so the test does not skip itself there and
the gate would have failed the test matrix over a document that is nobody's
business but the gate's — and the analysis job needs the test job to have
passed. The gate has a job of its own, with the SDK version the validator is
built for, and the other jobs leave the suite out.
@sonarqubecloud

Copy link
Copy Markdown

@fabeat

fabeat commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

Combined into #49, which carries this and the rest of the campaign in one pull request. Nothing is lost: the commits are in it, and the release notes are folded into the 0.3.1 section.

@fabeat fabeat closed this Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant