July 12, 2015

MS Word OLE Automation

В очередной раз победил сложную проблему, три дня не выходил из гостиницы, питаясь бесплатными завтраками и сухарями с чаем, аки пустынник. Сейчас всё заработало. Уфф.
Пишу для себя будущего, быстро и непонятно, если кому-то вдруг поможет - велкам.

Итак. У нас есть вебсайт на неком движке (у меня самописный велосипед, но может быть что угодно - апач, cgi, php, etc). Требуется на лету генерировать MsWord документы и давать скачать пользователям (в моём случае - представления ЦПЛС экипажу). Аналогичную задачу для Excel я решил уже несколько лет назад, и всё отлично работает. Word с наскоку подключить не удалось (и теперь я понял, почему), так что когда возникала необходимость засунуть на сайт автозаполнялку ворда я рисовал аналогичный документ в екселе и вуаля. Но сейчас были час та натхнення, и доколе. Вот.

Классический способ работы с Ms Office документами на лету - OLE Automation.
Перед тем, как загнать некий код на сервер, тестируешь его на личном ПК. Всё работает отлично, ексель создаётся, заполняется, сохраняется. С сервера - не хочет.

С екселем был следующий камень преткновения. Как правило, весь серверный софт работает из под учетной записи "NETWORK SERVICE". У этой учетки по умолчанию отсутствуют права на запуск объектов DCOM, через которые мы и получаем доступ к API MSOffice. Эти права надо дать, следующим образом.

На сервере запускаем dcomcnfg.exe, идём Службы компонентов, Компьютеры, Мой Компьютер, настройка DCOM, ищем наши компоненты.
Для екселя это Microsoft Excel Application, для ворда - Microsoft Word Document (я здесь засомневался, по логике должно быть тоже Application, но CLSID совпадает со справочником, всё верно).

Дальше Свойства, Безопасность, Настроить - Изменить для всех трёх, добавить учетку "NETWORK SERVICE", дать ей права.

Кнопки "настроить" могут быть серыми, это значит в реестре HKEY_CLASSES_ROOT\CLSID\{нужный-вам-CLSID} дать права к управлению веткой. CLSID можно увидеть в свойствах компонента.

Права могут не сохраняться, судя по форумам, это связано с владельцем ветки реестра. Я уже слишком опытный чукча, чтобы лезть в реестр сервера менять владельцев системных веток, поэтому просто экспортировал правильные права с соседнего объекта, подменил в reg-файле CLSID и импортировал.

reg файл получился такой:

[HKEY_CLASSES_ROOT\CLSID\{000209FF-0000-0000-C000-000000000046}]
@="Microsoft Word Application"
"AppID"="{00020906-0000-0000-C000-000000000046}"
"AccessPermission"=hex:01,00,04,80,58,00,00,00,68,00,00,00,00,00,00,00,14,00,\
00,00,02,00,44,00,03,00,00,00,00,00,14,00,07,00,00,00,01,01,00,00,00,00,00,\
05,14,00,00,00,00,00,14,00,07,00,00,00,01,01,00,00,00,00,00,05,0a,00,00,00,\
00,00,14,00,03,00,00,00,01,01,00,00,00,00,00,05,12,00,00,00,01,02,00,00,00,\
00,00,05,20,00,00,00,20,02,00,00,01,02,00,00,00,00,00,05,20,00,00,00,20,02,\
00,00
"LaunchPermission"=hex:01,00,04,80,70,00,00,00,8c,00,00,00,00,00,00,00,14,00,\
00,00,02,00,5c,00,04,00,00,00,00,00,14,00,1f,00,00,00,01,01,00,00,00,00,00,\
05,14,00,00,00,00,00,14,00,1f,00,00,00,01,01,00,00,00,00,00,05,12,00,00,00,\
00,00,18,00,1f,00,00,00,01,02,00,00,00,00,00,05,20,00,00,00,20,02,00,00,00,\
00,14,00,1f,00,00,00,01,01,00,00,00,00,00,05,04,00,00,00,01,05,00,00,00,00,\
00,05,15,00,00,00,a0,5f,84,1f,5e,2e,6b,49,ce,12,03,03,f4,01,00,00,01,05,00,\
00,00,00,00,05,15,00,00,00,a0,5f,84,1f,5e,2e,6b,49,ce,12,03,03,f4,01,00,00

Далее, у "NETWORK SERVICE" скорее всего не будет прав доступа к папкам на диске, где мы собираемся работать с файлами. Их надо аккуратно назначить, и на доступ к папке, и на создание новых объектов.

Для екселя этого достаточно, OLE на сервере заработает. Для ворда идём далее.

Ворд зараза такая хочет иметь доступ к шаблонам.
Туда, где они лежат, прав доступа у сетевого сервиса быть не может.
Сначала смотрим, какие пути у него в настройках (спасибо wheelibin из stackoverflow):
word.Options.DefaultFilePath(Microsoft.Office.Interop.Word.WdDefaultFilePath.wdUserTemplatesPath)
или в переводе на c++ builder:
W.OlePropertyGet("Options").OlePropertyGet("DefaultFilePath",2)
(значение константы wdUserTemplatesPath смотрим в типе WdDefaultFilePath):

typedef enum WdDefaultFilePath
{
wdDocumentsPath = 0,
wdPicturesPath = 1,
wdUserTemplatesPath = 2,
wdWorkgroupTemplatesPath = 3,
wdUserOptionsPath = 4,
wdAutoRecoverPath = 5,
wdToolsPath = 6,
wdTutorialPath = 7,
wdStartupPath = 8,
wdProgramPath = 9,
wdGraphicsFiltersPath = 10,
wdTextConvertersPath = 11,
wdProofingToolsPath = 12,
wdTempFilePath = 13,
wdCurrentFolderPath = 14,
wdStyleGalleryPath = 15,
wdBorderArtPath = 19
} WdDefaultFilePath;

Меняем её на нашу рабочую папку, при создании файла методом Documents.Add там будет появляться normal.dot

Для Add и SaveAs этого уже достаточно. Но чтобы заработал метод Open, DCOM ворда должен выполняться от админской учетки. (Догадываюсь, что для открытия файла мелкомягкие индусы использовали высокоуровневое API, которому нужна работа в интерактивном режиме). Ок, снова запускаем dcomcnfg.exe, идем в компонент, меняем учетку (на форумах этого нет! сам догадался).

Ура. То есть, уфф.

Тестовый код, который успешно запустился у меня на сервере, вот:

Variant W;
try{
AnsiString fname = "d:\\skydbattach\\1.doc";
W = CreateOleObject("Word.Application");
W.OlePropertyGet("Options").OlePropertySet("CreateBackup",False);
W.OlePropertyGet("Options").OlePropertySet("DefaultFilePath",2,"D:\\SkydbAttach\\");
// W.OlePropertyGet("Documents").OleProcedure("Add");
W.OlePropertyGet("Documents").OleFunction("Open",StringToOleStr(fname),False);
WR(W.OlePropertyGet("Documents").OlePropertyGet("Count")+BR);
W.OlePropertyGet("Selection").OleFunction("TypeText", "Podoroges was here!");
WR("FN:"+W.OlePropertyGet("ActiveDocument").OlePropertyGet("FullName")+BR);
WR("RO:"+W.OlePropertyGet("ActiveDocument").OlePropertyGet("ReadOnly")+BR);
WR("W:"+W.OlePropertyGet("Options").OlePropertyGet("WarnBeforeSavingPrintingSendingMarkup")+BR);
WR("W:"+W.OlePropertyGet("DisplayAlerts")+BR);
W.OlePropertyGet("ActiveDocument").OleProcedure("SaveAs",StringToOleStr(fname2),0);
W.OlePropertyGet("ActiveDocument").OleProcedure("Close",0);
WR("Closed"+BR);
W.OleFunction("Quit");
}
catch(EOleSysError &e){
WR("Exception: "+e.ClassName()+" "+e.Message+" "+e.ErrorCode);
}
W = Unassigned;

В минуты душевной слабости на середине пути начинал смотреть в сторону Microsoft Office XML format, он прикольный и его понимают все офисы начиная с 2003, и работать с ним можно как с Ansi-файлом. Но при работе с кириллицей начинается свистопляска, связанная с юникодом, mime-типами, кодировками и crossbrowser compatibility. Вобщем, я рад, что удалось добить OLE.

Комменты традиционно отключены, в холодильнике сыр и портвейн, поздравляю себя с именинами :)

No comments: