Управление документами ODT с помощью PHP (базовый поиск и замена)

С помощью LibreOffice я разработал и написал текстовый документ (формат ODT). Теперь я хочу программно найти определенные заполнители и заменить их текстом из базы данных.

Я знаю, что есть некоторые библиотеки ODT для PHP, но поскольку файлы ODT — это просто ZIP-файлы, содержащие XML-файлы (среди прочего), я думаю, что это должно быть возможно с базовым PHP и без каких-либо библиотек, не так ли?

Поэтому я написал короткий скрипт, который распаковывает ODT-файл, изменяет content.xml, а затем снова архивирует папку. Вы можете увидеть полный код ниже.

Хотя я могу распаковать, заменить, заархивировать вручную, это не работает, когда я позволяю приведенному ниже PHP-скрипту выполнять эту работу. LibreOffice сообщит мне, что не может открыть документ и может попытаться восстановить его (что тоже не работает).

Есть ли какие-то особые требования, на которые мне нужно обратить внимание? Нужно ли мне изменять какие-либо метафайлы, кроме content.xml?

if (unzipFolder('Template.odt', 'temp')) {
    $source = file_get_contents('temp'.DIRECTORY_SEPARATOR.'content.xml');
    $source = str_replace('XXXplaceholder1XXX', 'Example Value #1', $source);
    $source = str_replace('XXXplaceholder2XXX', 'Example Value #2', $source);
    file_put_contents('temp'.DIRECTORY_SEPARATOR.'content.xml', $source);

    zipFolder('temp', 'output/Document.odt');
}

function unzipFolder($zipInputFile, $outputFolder) {
    $zip = new ZipArchive;
    $res = $zip->open($zipInputFile);
    if ($res === true) {
        $zip->extractTo($outputFolder);
        $zip->close();
        return true;
    }
    else {
        return false;
    }
}

function zipFolder($inputFolder, $zipOutputFile) {
    if (!extension_loaded('zip') || !file_exists($inputFolder)) {
        return false;
    }

    $zip = new ZipArchive();
    if (!$zip->open($zipOutputFile, ZIPARCHIVE::CREATE)) {
        return false;
    }

    $inputFolder = str_replace('\\', DIRECTORY_SEPARATOR, realpath($inputFolder));

    if (is_dir($inputFolder) === true) {
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($inputFolder), RecursiveIteratorIterator::SELF_FIRST);

        foreach ($files as $file) {
            $file = str_replace('\\', DIRECTORY_SEPARATOR, $file);

            if (in_array(substr($file, strrpos($file, '/')+1), array('.', '..'))) {
                continue;
            }

            $file = realpath($file);

            if (is_dir($file) === true) {
                $dirName = str_replace($inputFolder.DIRECTORY_SEPARATOR, '', $file.DIRECTORY_SEPARATOR);
                $zip->addEmptyDir($dirName);
            }
            else if (is_file($file) === true) {
                $fileName = str_replace($inputFolder.DIRECTORY_SEPARATOR, '', $file);
                $zip->addFromString($fileName, file_get_contents($file));
            }
        }
    }
    else if (is_file($inputFolder) === true) {
        $zip->addFromString(basename($inputFolder), file_get_contents($inputFolder));
    }

    return $zip->close();
}

Правка №1: приведенный выше код не работает даже в том случае, если вы просто разархивируете и повторно заархивируете содержимое файла ODT, т. е. если вы раскомментируете все манипуляции с данными. Что-то не так с форматом вывода PHP ZipArchive?

Правка №2. Точнее, метод zipFolder(...) все ломает. Вы можете позволить PHP выполнить распаковку, манипуляции со строками также работают нормально (str_replace(...)), но когда функция zipFolder(...) создает архив, его нельзя открыть, в то время как он работает нормально, если вы создаете архив вручную (с помощью 7-Zip, например ).

Правка №3: я даже заработал, просто заменив часть повторного архивирования в PHP вызовом 7-Zip через exec(...). Таким образом, проблема определенно заключается в создании правильного ZIP-архива. Для лучшей переносимости и меньшего количества зависимостей, было бы лучше, конечно, чтобы решение с PHP'овским ZipArchive работало и нам не нужен был 7-Zip.


person caw    schedule 16.12.2013    source источник
comment
или вы можете просто использовать phpexcel, который может читать/записывать odf просто отлично   -  person Dave    schedule 16.12.2013
comment
Не обращайтесь с XML-файлами как с обычным текстовым файлом. Выполнение текстового поиска и замены в XML-файле открывает уязвимости для внедрения.   -  person Lie Ryan    schedule 16.12.2013
comment
@LieRyan: Конечно, ты прав. В общем, не следует этого делать. Но я использую контент только из своей личной базы данных и никогда не приму пользовательский ввод. И даже если бы у него были слабые места в безопасности, он действительно должен работать.   -  person caw    schedule 16.12.2013
comment
@Dave: Я только что видел, что проблема даже не в чтении и записи, а в том, что распаковка/архивация не работает. Если я разархивирую все с помощью 7-Zip в Windows и снова заархивирую, это сработает. Если я позволю PHP сделать это, он не сработает.   -  person caw    schedule 16.12.2013
comment
Просто скопировал весь ваш код, создал фиктивный файл .odt и протестировал его. Все работает нормально, я могу открыть заархивированный файл Document.odt. Исправлено только $zip->extractTo('./'.$outputFolder);, чтобы он извлекал .odt в относительную временную папку.   -  person CrazySabbath    schedule 19.12.2013
comment
@CrazySabbath: Спасибо за тестирование и указание на это! Что ж, я тестировал это только под XAMPP в Windows 7, потому что не ожидал, что это будет проблема, связанная с платформой. Но после прочтения вашего комментария я попробовал скрипт на сервере Ubuntu, и он сработал, как и у вас. Но, к сожалению, он по-прежнему не работает на моем компьютере с Windows 7 с XAMPP. Это скорее проблема Windows или ошибка в XAMPP?   -  person caw    schedule 20.12.2013
comment
@МаркоВ. Я только что проверил ваш код на файле .odt. Выведенный .odt отлично работает в MS Word, но не открывается в LibreOffice. Однако продукты MS нередко стараются изо всех сил, даже если файл или формат поврежден (подумайте, что старый IE прощает небрежный HTML-код).   -  person user555    schedule 20.12.2013


Ответы (1)


Есть ряд проблем с вашей функцией zipFolder(), из-за которых файл .odt не работает. Загрузчик файлов, используемый в LibreOffice, не очень щадящий, это может относиться и к OpenOffice, поскольку первый является ответвлением второго.

Благодаря отчету об ошибке PHP #48763 мне удалось сузить круг проблемы. Этот отчет об ошибке в основном касается проблемы с ZipArchive::addFromString(), которая была исправлена, начиная с PHP 5.2.11. Однако пользователь "Ларс" дает представление об ограничении в файле LibreOffice. погрузчик.

«При использовании разделителей файловой системы Windows zip-архив .ods не работает, хотя извлечение архива работает».

1. "." и ".." до сих пор находятся в архиве

У вас есть статус if, который выглядит следующим образом:

if (in_array(substr($file, strrpos($file, '/')+1), array('.', '..'))) {
    continue;
}

Я не знаю, было ли намерение отфильтровать . и .., во всяком случае, это не помогает. Поскольку вы включаете .., который вместе с realpath() переводится в родительский каталог, вы нарушаете файл .odt.

2. Все разделители каталогов должны быть косой чертой (в стиле unix).

В Windows разделители каталогов имеют тип обратной косой черты (\). Это объясняет, почему ваш скрипт работает на Linux (как тестировал пользователь CrazySabbath), но не на Windows (XAMPP). Согласно отчету об ошибке, о котором я упоминал в начале, вы должны использовать косую черту (/) в качестве разделителей каталогов для LibreOffice, чтобы открывать ваши файлы.

Также обратите внимание, что realpath() в Windows изменит пути в стиле unix на стиль Windows.

стандарт файла ZIP гласит, что все косые черты ДОЛЖНЫ быть прямыми косыми чертами, однако кажется, что ZipArchive давайте проигнорируем стандарт, не сделав преобразование за вас.

4.4.17.1 Имя файла с необязательным относительным путем. Сохраненный путь НЕ ДОЛЖЕН содержать букву диска или устройства или косую черту в начале. Все косые черты ДОЛЖНЫ быть прямыми косыми чертами «/», а не обратными косыми чертами «\» для совместимости с файловыми системами Amiga и UNIX и т. д.

3. DIRECTORY_SEPARATOR не нужен

Не проблема с вашим кодом, просто общий совет. Нет необходимости использовать константу DIRECTORY_SEPARATOR, просто используйте косую черту (/), и она будет работать как в системах *nix, так и в Windows.

Тем не менее, DIRECTORY_SEPARATOR по-прежнему полезен для таких вещей, как расчленение или замена пути.

person user555    schedule 20.12.2013
comment
Большое тебе спасибо! Этот отчет об ошибке, безусловно, та же проблема, что и у меня. Я попробую ваши решения (или отдельные намеки на них) и сообщу здесь. Однако DIRECTORY_SEPARATOR был необходим, вот почему я включил его: мое первое решение было только с косой чертой, но затем Windows включила полную структуру каталогов (начиная с имени диска) в полученный ZIP. Это потому, что $dirName = str_replace(...) и $fileName = str_replace(...) в противном случае ничего не заменят. - person caw; 20.12.2013
comment
@МаркоВ. Я бы сделал $inputFolder = str_replace('\\', '/', realpath($inputFolder));, а затем использовал FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS в качестве флагов для конструктора RecursiveDirectoryIterator. Это даст вам косую черту для каждого пути, если вы воздержитесь от повторного использования realpath(). Тогда вам также не нужен DIRECTORY_SEPARATOR в str_replace(). - person user555; 20.12.2013
comment
Отличный ответ и полезный комментарий! Теперь все работает нормально. Большое тебе спасибо! - person caw; 21.12.2013