PhpIrbis

Форк
0
/
PhpIrbis.php 
5958 строк · 170.3 Кб
1
<?php
2

3
/** @noinspection PhpUnused */
4
/** @noinspection PhpUnnecessaryLocalVariableInspection */
5
/** @noinspection PhpFullyQualifiedNameUsageInspection */
6

7
namespace Irbis;
8

9
//
10
// Простой клиент для АБИС ИРБИС64.
11
// Требует PHP 5.4 или выше.
12
// Работает с сервером ИРБИС64 2014 и выше.
13
//
14

15
// Кодировки
16
//
17
// ВАЖНО! Предполагается, что внутренняя кодировка символов в PHP -- UTF-8
18
// И строковые литералы в PHP-файлах хранятся также в кодировке UTF-8
19
// Если это не так, возможны проблемы
20
//
21

22
// Статус записи
23

24
const LOGICALLY_DELETED  = 1;  ///< Запись логически удалена
25
const PHYSICALLY_DELETED = 2;  ///< Запись физически удалена
26
const ABSENT             = 4;  ///< Запись отсутствует
27
const NON_ACTUALIZED     = 8;  ///< Запись не актуализирована
28
const LAST_VERSION       = 32; ///< Последняя версия записи
29
const LOCKED_RECORD      = 64; ///< Запись заблокирована на ввод
30

31
// Распространённые форматы
32

33
const ALL_FORMAT       = "&uf('+0')";  ///< Полные данные по полям
34
const BRIEF_FORMAT     = '@brief';     ///< Краткое библиографическое описание
35
const IBIS_FORMAT      = '@ibiskw_h';  ///< Формат IBIS (старый)
36
const INFO_FORMAT      = '@info_w';    ///< Информационный формат
37
const OPTIMIZED_FORMAT = '@';          ///< Оптимизированный формат
38

39
// Распространённые поиски
40

41
const KEYWORD_PREFIX    = 'K=';  ///< Ключевые слова
42
const AUTHOR_PREFIX     = 'A=';  ///< Индивидуальный автор, редактор, составитель
43
const COLLECTIVE_PREFIX = 'M=';  ///< Коллектив или мероприятие
44
const TITLE_PREFIX      = 'T=';  ///< Заглавие
45
const INVENTORY_PREFIX  = 'IN='; ///< Инвентарный номер, штрих-код или радиометка
46
const INDEX_PREFIX      = 'I=';  ///< Шифр документа в базе
47

48
// Логические операторы для поиска
49

50
const LOGIC_OR                = 0; ///< Только ИЛИ
51
const LOGIC_OR_AND            = 1; ///< ИЛИ и И
52
const LOGIC_OR_AND_NOT        = 2; ///< ИЛИ, И, НЕТ (по умолчанию)
53
const LOGIC_OR_AND_NOT_FIELD  = 3; ///< ИЛИ, И, НЕТ, И (в поле)
54
const LOGIC_OR_AND_NOT_PHRASE = 4; ///< ИЛИ, И, НЕТ, И (в поле), И (фраза)
55

56
// Коды АРМ
57

58
const ADMINISTRATOR = 'A'; ///< Адмнистратор
59
const CATALOGER     = 'C'; ///< Каталогизатор
60
const ACQUSITIONS   = 'M'; ///< Комплектатор
61
const READER        = 'R'; ///< Читатель
62
const CIRCULATION   = 'B'; ///< Книговыдача
63
const BOOKLAND      = 'B'; ///< Книговыдача
64
const PROVISION     = 'K'; ///< Книгообеспеченность
65

66
// Команды глобальной корректировки
67

68
const ADD_FIELD        = 'ADD';    ///< добавление нового повторения поля или подполя в заданное существующее поле
69
const DELETE_FIELD     = 'DEL';    ///< удаляет поле или подполе в поле
70
const REPLACE_FIELD    = 'REP';    ///< замена целиком поля или подполя
71
const CHANGE_FIELD     = 'CHA';    ///< замена данных в поле или в подполе
72
const CHANGE_WITH_CASE = 'CHAC';   ///< замена данных в поле или в подполе с учетом регистра символов
73
const DELETE_RECORD    = 'DELR';   ///< удаляет записи, поданные на корректировку
74
const UNDELETE_RECORD  = 'UNDELR'; ///< восстанавливает записи
75
const CORRECT_RECORD   = 'CORREC'; ///< вызывает на корректировку другие записи, отобранные по поисковым терминам  из текущей или другой, доступной в системе, базы данных
76
const CREATE_RECORD    = 'NEWMFN'; ///< создание новой записи в текущей или другой базе данных
77
const EMPTY_RECORD     = 'EMPTY';  ///< очищает (опустошает) текущую запись
78
const UNDO_RECORD      = 'UNDOR';  ///< переход к одной из предыдущих копий записи (откат)
79
const GBL_END          = 'END';    ///< закрывающая операторная скобка
80
const GBL_IF           = 'IF';     ///< логическое ветвление
81
const GBL_FI           = 'FI';     ///< закрывающий оператор для ветвления
82
const GBL_ALL          = 'ALL';    ///< дополняет записи всеми полями текущей записи
83
const GBL_REPEAT       = 'REPEAT'; ///< цикл из группы операторов
84
const GBL_UNTIL        = 'UNTIL';  ///< закрывающий оператор для цикла
85
const PUTLOG           = 'PUTLOG'; ///< формирование пользовательского протокола
86

87
//Работа через WebToIrbisServer
88
const IRBIS_START_REQUEST = 'IRBIS_START_REQUEST'; //служебное слово для обозначения начала ответа/запроса
89
const IRBIS_END_REQUEST = 'IRBIS_END_REQUEST'; //служебное слово для обозначения начала ответа/запроса
90

91
/**
92
 * @brief Разделитель строк в ИРБИС.
93
 */
94
const IRBIS_DELIMITER = "\x1F\x1E";
95

96
/**
97
 * @brief Короткие версии разделителя строк ИРБИС.
98
 */
99
const SHORT_DELIMITER = "\x1E";
100
const ALT_DELIMITER   = "\x1F";
101

102
/**
103
 * @brief Пустая ли данная строка?
104
 *
105
 * @param string $text Строка для изучения.
106
 * @return bool
107
 */
108
function is_null_or_empty($text)
109
{
110
    return (!isset($text) || !$text || !trim($text));
111
} // function is_null_or_empty
112

113
/**
114
 * @brief Строки совпадают с точностью до регистра символов?
115
 *
116
 * @param string $str1 Первая строка.
117
 * @param string $str2 Вторая строка.
118
 * @return bool
119
 */
120
function same_string($str1, $str2)
121
{
122
    return strcasecmp($str1, $str2) === 0;
123
} // function same_string
124

125
/**
126
 * @brief Безопасное получение элемента массива по индексу.
127
 *
128
 * @param array $a Массив.
129
 * @param int $ofs Индекс.
130
 * @return mixed|null
131
 */
132
function safe_get(array $a, $ofs)
133
{
134
    if (isset($a[$ofs])) {
135
        return $a[$ofs];
136
    }
137
    return null;
138
} // function safe_get
139

140
/**
141
 * Проверяет, начинается ли строка с указанного фрагмента.
142
 * @param string $haystack Строка, подлежащая проверке.
143
 * @param string $needle Искомый фрагмент.
144
 * @return bool Результат проверки.
145
 */
146
function startsWith($haystack, $needle) {
147
    $length = strlen ($needle);
148
    return substr ($haystack, 0, $length) === $needle;
149
} // function startsWith
150

151
/**
152
 * Проверяет, заканчивается ли строка указанным фрагментом.
153
 * @param string $haystack Строка, подлежащая проверке.
154
 * @param string $needle Искомый фрагмент.
155
 * @return bool Результат проверки.
156
 */
157
function endsWith($haystack, $needle) {
158
    $length = strlen($needle);
159
    if(!$length) {
160
        return true;
161
    }
162
    return substr($haystack, -$length) === $needle;
163
} // function endsWith
164

165
/**
166
 * @brief Таблица ручной перекодировки из CP1251 в UTF8.
167
 * @return array
168
 */
169
function getAnsiTable()
170
{
171
    static $_ansiTable = array
172
    (
173
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
174
        19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
175
        35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
176
        51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66,
177
        67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82,
178
        83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98,
179
        99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
180
        112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
181
        125, 126, 127, 1026, 1027, 8218, 1107, 8222, 8230, 8224, 8225,
182
        8364, 8240, 1033, 8249, 1034, 1036, 1035, 1039, 1106, 8216, 8217,
183
        8220, 8221, 8226, 8211, 8212, 152, 8482, 1113, 8250, 1114, 1116,
184
        1115, 1119, 160, 1038, 1118, 1032, 164, 1168, 166, 167, 1025, 169,
185
        1028, 171, 172, 173, 174, 1031, 176, 177, 1030, 1110, 1169, 181,
186
        182, 183, 1105, 8470, 1108, 187, 1112, 1029, 1109, 1111, 1040,
187
        1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051,
188
        1052, 1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062,
189
        1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073,
190
        1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084,
191
        1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095,
192
        1096, 1097, 1098, 1099, 1100, 1101, 1102, 1103
193
    );
194

195
    return $_ansiTable;
196
}
197

198
/**
199
 * @brief Ручная перекодировка текста из CP1251 в UTF-8.
200
 *
201
 */
202
function ansiToUtf ($text)
203
{
204
    $table = getAnsiTable();
205
    $length = strlen ($text);
206
    $result = '';
207

208
    for ($i = 0; $i < $length; $i++) {
209
        $chr = ord ($text[$i]);
210
        $chr = $table[$chr];
211
        if ($chr < 128) {
212
            $result .= chr ($chr);
213
        }
214
        else {
215
            $result .= chr (($chr >> 6) | 0xC0);
216
            $result .= chr (($chr & 0x3F) | 0x80);
217
        }
218
    }
219

220
    return $result;
221
} // function ansiToUtf
222

223
/**
224
 * @brief Ручная перекодировка текста из CP1251 в UTF-8.
225
 *
226
 */
227
function utfToAnsi ($text)
228
{
229
    $table = getAnsiTable();
230

231
    $length = strlen ($text);
232
    $result = '';
233
    $offset = 0;
234

235
    while ($offset < $length) {
236
        $chr = ord ($text[$offset++]);
237
        if ($chr < 128) {
238
            $result .= chr ($chr);
239
        }
240
        else {
241
            if (($chr & 0xE0) === 0xC0) {
242
                $wide =  ($chr & 0x1F) << 6;
243
                $wide |= (ord ($text[$offset++]) & 0x3F);
244
                $chr = '?';
245
                for ($i = 128; $i < 256; $i++) {
246
                    // начинать с 0 нет смысла, сэкономим такты процессора
247
                    if ($table[$i] === $wide) {
248
                        $chr = chr ($i);
249
                        break;
250
                    }
251
                }
252

253
                $result .= $chr;
254
            }
255
            else if (($chr & 0xF0) === 0xE0) {
256
                $result .= '?';
257
                $offset++;
258
                $offset++;
259
            }
260
            else {
261
                $result .= '?';
262
                $offset++;
263
                $offset++;
264
                $offset++;
265
            }
266
        }
267

268
    }
269

270
    return $result;
271
} // function utfToAnsi
272

273
/**
274
 * @brief Замена обычных переводов строки на ИРБИСные.
275
 *
276
 * @param string $text Текст для замены.
277
 * @return string Текст с замененными переводами строки.
278
 */
279
function dos_to_irbis($text)
280
{
281
    return str_replace(array("\r\n", "\r", "\n"), array(IRBIS_DELIMITER, "0x1F", "0x1E"), $text);
282
} // function dos_to_irbis
283

284
/**
285
 * @brief Замена переводов строки с ИРБИСных на обычные.
286
 *
287
 * @param string $text Текст для замены.
288
 * @return string Текст с замененными переводами строки.
289
 */
290
function irbis_to_dos($text)
291
{
292
    return str_replace(IRBIS_DELIMITER, "\n", $text);
293
} // function irbis_to_dos
294

295
/**
296
 * @brief Разбивка текста на строки по ИРБИСным разделителям.
297
 *
298
 * @param string $text Текст для разбиения.
299
 * @return array Массив строк.
300
 */
301
function irbis_to_lines($text)
302
{
303
    return explode(IRBIS_DELIMITER, $text);
304
} // function irbis_to_lines
305

306
/**
307
 * @brief Удаление комментариев из строки.
308
 *
309
 * @param string $text Текст для удаления комментариев.
310
 * @return string Очищенный текст.
311
 */
312
function remove_comments($text)
313
{
314
    if (is_null_or_empty($text)) {
315
        return $text;
316
    }
317

318
    if (strpos($text, '/*') === false) {
319
        return $text;
320
    }
321

322
    $result = '';
323
    $state = '';
324
    $index = 0;
325
    $length = strlen($text);
326

327
    while ($index < $length) {
328
        $c = $text[$index];
329

330
        switch ($state) {
331
            case "'":
332
            case '"':
333
            case '|':
334
                if ($c === $state) {
335
                    $state = '';
336
                }
337

338
                $result .= $c;
339
                break;
340

341
            default:
342
                if ($c === '/') {
343
                    if ($index + 1 < $length && $text[$index + 1] === '*') {
344
                        while ($index < $length) {
345
                            $c = $text[$index];
346
                            if ($c === "\r" || $c === "\n") {
347
                                $result .= $c;
348
                                break;
349
                            }
350

351
                            $index++;
352
                        }
353
                    } else {
354
                        $result .= $c;
355
                    }
356
                } else if ($c === "'" || $c === '"' || $c === '|') {
357
                    $state = $c;
358
                    $result .= $c;
359
                } else {
360
                    $result .= $c;
361
                }
362
                break;
363
        }
364

365
        $index++;
366
    }
367

368
    return $result;
369
} // function remove_comments
370

371
/**
372
 * @brief Подготовка динамического формата для передачи на сервер.
373
 *
374
 * В формате должны отсутствовать комментарии и служебные символы (например, перевод строки или табуляция).
375
 *
376
 * @param string $text Текст для обработки.
377
 * @return string Обработанный текст.
378
 */
379
function prepare_format($text)
380
{
381
    $text = remove_comments($text);
382
    $length = strlen($text);
383
    if (!$length) {
384
        return $text;
385
    }
386

387
    $flag = false;
388
    for ($i = 0; $i < $length; $i++) {
389
        if ($text[$i] < ' ') {
390
            $flag = true;
391
            break;
392
        }
393
    }
394

395
    if (!$flag) {
396
        return $text;
397
    }
398

399
    $result = '';
400
    for ($i = 0; $i < $length; $i++) {
401
        $c = $text[$i];
402
        if ($c >= ' ') {
403
            $result .= $c;
404
        }
405
    }
406

407
    return $result;
408
} // function prepare_format
409

410
/**
411
 * @brief Получение описания по коду ошибки, возвращенному сервером.
412
 *
413
 * @param int $code Код ошибки.
414
 * @return string Словесное описание ошибки.
415
 */
416
function describe_error($code)
417
{
418
    if ($code >= 0) {
419
        return 'Нет ошибки';
420
    }
421

422
    $errors = array(
423
        -100 => 'Заданный MFN вне пределов БД',
424
        -101 => 'Ошибочный размер полки',
425
        -102 => 'Ошибочный номер полки',
426
        -140 => 'MFN вне пределов БД',
427
        -141 => 'Ошибка чтения',
428
        -200 => 'Указанное поле отсутствует',
429
        -201 => 'Предыдущая версия записи отсутствует',
430
        -202 => 'Заданный термин не найден (термин не существует)',
431
        -203 => 'Последний термин в списке',
432
        -204 => 'Первый термин в списке',
433
        -300 => 'База данных монопольно заблокирована',
434
        -301 => 'База данных монопольно заблокирована',
435
        -400 => 'Ошибка при открытии файлов MST или XRF (ошибка файла данных)',
436
        -401 => 'Ошибка при открытии файлов IFP (ошибка файла индекса)',
437
        -402 => 'Ошибка при записи',
438
        -403 => 'Ошибка при актуализации',
439
        -600 => 'Запись логически удалена',
440
        -601 => 'Запись физически удалена',
441
        -602 => 'Запись заблокирована на ввод',
442
        -603 => 'Запись логически удалена',
443
        -605 => 'Запись физически удалена',
444
        -607 => 'Ошибка autoin.gbl',
445
        -608 => 'Ошибка версии записи',
446
        -700 => 'Ошибка создания резервной копии',
447
        -701 => 'Ошибка восстановления из резервной копии',
448
        -702 => 'Ошибка сортировки',
449
        -703 => 'Ошибочный термин',
450
        -704 => 'Ошибка создания словаря',
451
        -705 => 'Ошибка загрузки словаря',
452
        -800 => 'Ошибка в параметрах глобальной корректировки',
453
        -801 => 'ERR_GBL_REP',
454
        -802 => 'ERR_GBL_MET',
455
        -1111 => 'Ошибка исполнения сервера (SERVER_EXECUTE_ERROR)',
456
        -2222 => 'Ошибка в протоколе (WRONG_PROTOCOL)',
457
        -3333 => 'Незарегистрированный клиент (ошибка входа на сервер) (клиент не в списке)',
458
        -3334 => 'Клиент не выполнил вход на сервер (клиент не используется)',
459
        -3335 => 'Неправильный уникальный идентификатор клиента',
460
        -3336 => 'Нет доступа к командам АРМ',
461
        -3337 => 'Клиент уже зарегистрирован',
462
        -3338 => 'Недопустимый клиент',
463
        -4444 => 'Неверный пароль',
464
        -5555 => 'Файл не существует',
465
        -6666 => 'Сервер перегружен. Достигнуто максимальное число потоков обработки',
466
        -7777 => 'Не удалось запустить/прервать поток администратора (ошибка процесса)',
467
        -8888 => 'Общая ошибка',
468
        -100001 => 'Ошибка создания сокета',
469
        -100002 => 'Сбой сети',
470
        -100003 => 'Не подключен к серверу'
471
    );
472

473
    return $errors[$code] ?: 'Неизвестная ошибка';
474
} // function describe_error
475

476
/**
477
 * @brief "Хорошие" коды для readRecord.
478
 *
479
 * @return array "Хорошие" коды для readRecord.
480
 */
481
function codes_for_read_record()
482
{
483
    return array(-201, -600, -602, -603);
484
} // function codes_for_read_record
485

486
/**
487
 * @brief "Хорошие" коды для readTerms.
488
 *
489
 * @return array "Хорошие" коды для readTerms.
490
 */
491
function codes_for_read_terms()
492
{
493
    return array(-202, -203, -204);
494
} // function codes_for_read_terms
495

496
/**
497
 * @brief Специфичное для ИРБИС исключение.
498
 */
499
final class IrbisException extends \Exception
500
{
501
    /**
502
     * @brief Конструктор.
503
     *
504
     * @param string $message Сообщение об ошибке.
505
     * @param int $code Код ошибки.
506
     * @param mixed $previous Вложенное исключение.
507
     */
508
    public function __construct($message = "",
509
                                $code = 0,
510
                                $previous = null)
511
    {
512
        parent::__construct($message, $code, $previous);
513
    } // function __construct
514

515
    /**
516
     * @return string Текстовое представление исключения.
517
     */
518
    public function __toString()
519
    {
520
        return __CLASS__ . ": [$this->code]: $this->message\n";
521
    } // function __toString
522
} // class IrbisException
523

524
/**
525
 * @brief Подполе записи. Состоит из кода и значения.
526
 */
527
final class SubField
528
{
529
    /**
530
     * @var string Код подполя.
531
     */
532
    public $code;
533

534
    /**
535
     * @var string Значение подполя.
536
     */
537
    public $value;
538

539
    /**
540
     * @brief Конструктор подполя.
541
     *
542
     * @param string $code Код подполя.
543
     * @param string $value Значение подполя.
544
     */
545
    public function __construct($code = '', $value = '')
546
    {
547
        $this->code = $code;
548
        $this->value = $value;
549
    } // function __construct
550

551
    /**
552
     * @brief Клонирование подполя.
553
     */
554
    public function __clone()
555
    {
556
        $this->value = str_repeat($this->value, 1);
557
    } // function __clone
558

559
    /**
560
     * @brief Декодирование подполя из протокольного представления.
561
     *
562
     * @param string $line
563
     */
564
    public function decode($line)
565
    {
566
        $this->code = $line[0];
567
        $this->value = substr($line, 1);
568
    } // function decode
569

570
    /**
571
     * @brief Верификация подполя.
572
     *
573
     * @param bool $throw Бросать ли исключение при ошибке?
574
     * @return bool Результат верификации.
575
     * @throws IrbisException
576
     */
577
    public function verify($throw = true)
578
    {
579
        $result = $this->code && $this->value;
580
        if (!$result && $throw) {
581
            throw new IrbisException();
582
        }
583

584
        return $result;
585
    } // function verify
586

587
    public function __toString()
588
    {
589
        return '^' . $this->code . $this->value;
590
    } // function __toString
591
} // class SubField
592

593
/**
594
 * @brief Поле записи. Состоит из метки и (опционального) значения.
595
 *
596
 * Может содержать произвольное количество подполей.
597
 */
598
final class RecordField
599
{
600
    /**
601
     * @var int Метка поля.
602
     */
603
    public $tag;
604

605
    /**
606
     * @var string Значение поля до первого разделителя.
607
     */
608
    public $value;
609

610
    /**
611
     * @var array Массив подполей.
612
     */
613
    public $subfields = array();
614

615
    /**
616
     * @brief Конструктор поля.
617
     *
618
     * @param int $tag Метка поля.
619
     * @param string $value Значение поля.
620
     */
621
    public function __construct($tag = 0, $value = '')
622
    {
623
        $this->tag = $tag;
624
        $this->value = $value;
625
    } // function __construct
626

627
    /**
628
     * @brief Клонирование поля.
629
     */
630
    public function __clone()
631
    {
632
        $this->value = str_repeat($this->value, 1);
633
        $new = array();
634
        foreach ($this->subfields as $i => $subfield) {
635
            $new[$i] = clone $subfield;
636
        }
637
        $this->subfields = $new;
638
    } // function __clone
639

640
    /**
641
     * @brief Добавление подполя с указанными кодом и значением.
642
     *
643
     * @param string $code Код подполя.
644
     * @param string $value Значение подполя.
645
     * @return $this
646
     */
647
    public function add($code, $value)
648
    {
649
        $subfield = new SubField();
650
        $subfield->code = $code;
651
        $subfield->value = $value;
652
        $this->subfields[] = $subfield;
653

654
        return $this;
655
    } // function add
656

657
    /**
658
     * @brief Очищает поле (удаляет значение и все подполя).
659
     *
660
     * @return $this
661
     */
662
    public function clear()
663
    {
664
        $this->value = '';
665
        $this->subfields = array();
666

667
        return $this;
668
    } // function clear
669

670
    /**
671
     * @brief Декодирование поля из протокольного представления.
672
     *
673
     * @param string $line
674
     */
675
    public function decode($line)
676
    {
677
        $this->tag = (int) strtok($line, "#");
678
        $body = strtok('');
679

680
        if ($body[0] === '^') {
681
            $this->value = '';
682
            $all = explode('^', $body);
683
        } else {
684
            $this->value = strtok($body, '^');
685
            $all = explode('^', strtok(''));
686
        }
687

688
        foreach ($all as $one) {
689
            if (!empty($one)) {
690
                $sf = new SubField();
691
                $sf->decode($one);
692
                $this->subfields[] = $sf;
693
            }
694
        }
695
    } // function decode
696

697
    /**
698
     * @brief Получает массив встроенных полей из данного поля.
699
     *
700
     * @return array Встроенные поля.
701
     */
702
    public function getEmbeddedFields()
703
    {
704
        $result = array();
705
        $found = null;
706
        foreach ($this->subfields as $subfield) {
707
            if ($subfield->code === '1') {
708
                if ($found) {
709
                    if (count($found->subfields) || $found->value) {
710
                        $result[] = $found;
711
                    }
712
                    $found = null;
713
                }
714
                $value = $subfield->value;
715
                if (!$value)
716
                    continue;
717

718
                $tag = intval(substr($value, 0, 3));
719
                $found = new RecordField($tag);
720
                if ($tag < 10) {
721
                    $found->value = substr($value, 3);
722
                }
723
            } else {
724
                if ($found) {
725
                    $found->subfields[] = $subfield;
726
                }
727
            }
728
        }
729

730
        if ($found) {
731
            if (count($found->subfields) || $found->value) {
732
                $result[] = $found;
733
            }
734
        }
735

736
        return $result;
737
    } // function getEmbeddedFields
738

739
    /**
740
     * @brief Возвращает первое вхождение подполя с указанным кодом.
741
     *
742
     * @param string $code Код искомого подполя.
743
     * @return SubField|null Найденное подполе.
744
     */
745
    public function getFirstSubfield($code)
746
    {
747
        foreach ($this->subfields as $subfield) {
748
            if (same_string($subfield->code, $code)) {
749
                return $subfield;
750
            }
751
        }
752

753
        return null;
754
    } // function getFirstSubfield
755

756
    /**
757
     * @brief Возвращает значение первого вхождения подполя с указанным кодом.
758
     *
759
     * @param string $code Код искомого подполя.
760
     * @return string Значение найденного подполя либо пустая строка.
761
     */
762
    public function getFirstSubfieldValue($code)
763
    {
764
        foreach ($this->subfields as $subfield) {
765
            if (same_string($subfield->code, $code)) {
766
                return $subfield->value;
767
            }
768
        }
769

770
        return '';
771
    } // function getFirstSubfieldValue
772

773
    /**
774
     * @brief Вставляет подполе по указанному индексу.
775
     *
776
     * @param int $index Позиция для вставки.
777
     * @param SubField $subfield Подполе.
778
     */
779
    public function insertAt($index, SubField $subfield)
780
    {
781
        array_splice($this->subfields, $index, 0, $subfield);
782
    } // function insertAt
783

784
    /**
785
     * @brief Удаляет подполе по указанному индексу.
786
     *
787
     * @param int $index Индекс для удаления.
788
     */
789
    public function removeAt($index)
790
    {
791
        unset($this->subfields[$index]);
792
        $this->subfields = array_values($this->subfields);
793
    } // function removeAt
794

795
    /**
796
     * @brief Удаляет все подполя с указанным кодом.
797
     *
798
     * @param string $code Искомый код подполя.
799
     */
800
    public function removeSubfield($code)
801
    {
802
        $flag = false;
803
        $len = count($this->subfields);
804
        for ($i = 0; $i < $len; ++$i) {
805
            $sub = $this->subfields[$i];
806
            if (same_string($sub->code, $code)) {
807
                unset($this->subfields[$i]);
808
                $flag = true;
809
            }
810
        }
811
        if ($flag) {
812
            $this->subfields = array_values($this->subfields);
813
        }
814
    } // function removeSubfield
815

816
    /**
817
     * @brief Устанавливает значение подполя с указанным кодом.
818
     *
819
     * @param string $code Искомый код подполя.
820
     * @param string $value Новое значение подполя.
821
     * @return $this
822
     */
823
    public function setSubfield($code, $value)
824
    {
825
        if (!$value) {
826
            $this->removeSubfield($code);
827
        }
828
        else {
829
            $subfield = $this->getFirstSubfield($code);
830
            if (!$subfield) {
831
                $this->add($code, $value);
832
                $subfield = $this->getFirstSubfield($code);
833
            }
834

835
            $subfield->value = $value;
836
        }
837

838
        return $this;
839
    } // function setSubfield
840

841
    /**
842
     * @brief Верификация поля.
843
     *
844
     * @param bool $throw Бросать ли исключение при ошибке?
845
     * @return bool Результат верификации.
846
     * @throws IrbisException Ошибка в структуре поля.
847
     */
848
    public function verify($throw = true)
849
    {
850
        $result = $this->tag && ($this->value || count($this->subfields));
851
        if ($result && $this->subfields) {
852
            foreach ($this->subfields as $subfield) {
853
                $result = $subfield->verify($throw);
854
                if (!$result) {
855
                    break;
856
                }
857
            }
858
        }
859

860
        if (!$result && $throw) {
861
            throw new IrbisException();
862
        }
863

864
        return $result;
865
    } // function verify
866

867
    public function __toString()
868
    {
869
        $result = $this->tag . '#' . $this->value;
870

871
        foreach ($this->subfields as $sf) {
872
            $result .= $sf;
873
        }
874

875
        return $result;
876
    } // function __toString
877
} // class RecordField
878

879
/**
880
 * @brief Запись. Состоит из произвольного количества полей.
881
 */
882
final class MarcRecord
883
{
884
    /**
885
     * @var string Имя базы данных, в которой хранится запись.
886
     */
887
    public $database = '';
888

889
    /**
890
     * @var int MFN записи.
891
     */
892
    public $mfn = 0;
893

894
    /**
895
     * @var int Версия записи.
896
     */
897
    public $version = 0;
898

899
    /**
900
     * @var int Статус записи.
901
     */
902
    public $status = 0;
903

904
    /**
905
     * @var array Массив полей.
906
     */
907
    public $fields = array();
908

909
    public function __clone()
910
    {
911
        $this->database = str_repeat($this->database, 1);
912
        $new = array();
913
        foreach ($this->fields as $i => $field) {
914
            $new[$i] = clone $field;
915
        }
916
        $this->fields = $new;
917
    } // function __clone
918

919
    /**
920
     * Добавление поля в запись.
921
     *
922
     * @param int $tag Метка поля.
923
     * @param string $value Значение поля до первого разделителя.
924
     * @return RecordField Созданное поле.
925
     */
926
    public function add($tag, $value = '')
927
    {
928
        $field = new RecordField();
929
        $field->tag = $tag;
930
        $field->value = $value;
931
        $this->fields[] = $field;
932

933
        return $field;
934
    } // function add
935

936
    /**
937
     * Очистка записи (удаление всех полей).
938
     *
939
     * @return $this
940
     */
941
    public function clear()
942
    {
943
        $this->fields = array();
944

945
        return $this;
946
    } // function clear
947

948
    /**
949
     * Декодирование ответа сервера.
950
     *
951
     * @param array $lines Массив строк
952
     * с клиентским представлением записи.
953
     */
954
    public function decode(array $lines)
955
    {
956
        if (empty($lines) || count($lines) < 2) {
957
            return;
958
        }
959

960
        // mfn and status of the record
961
        $firstLine = explode('#', $lines[0]);
962
        $this->mfn = intval($firstLine[0]);
963
        $this->status = intval(safe_get($firstLine, 1));
964

965
        // version of the record
966
        $secondLine = explode('#', $lines[1]);
967
        $this->version = intval(safe_get($secondLine, 1));
968
        $lines = array_slice($lines, 2);
969

970
        // fields
971
        foreach ($lines as $line) {
972
            if ($line) {
973
                $field = new RecordField();
974
                $field->decode($line);
975
                $this->fields[] = $field;
976
            }
977
        }
978
    } // function decode
979

980
    /**
981
     * Кодирование записи в протокольное представление.
982
     *
983
     * @param string $delimiter Разделитель строк.
984
     * В зависимости от ситуации ИРБИСный или обычный.
985
     * @return string
986
     */
987
    public function encode($delimiter = IRBIS_DELIMITER)
988
    {
989
        $result = $this->mfn . '#' . $this->status . $delimiter
990
            . '0#' . $this->version . $delimiter;
991

992
        foreach ($this->fields as $field) {
993
            $result .= ($field . $delimiter);
994
        }
995

996
        return $result;
997
    } // function encode
998

999
    /**
1000
     * Получение значения поля (или подполя)
1001
     * с указанной меткой (и указанным кодом).
1002
     *
1003
     * @param int $tag Метка поля
1004
     * @param string $code Код подполя
1005
     * @return string|null
1006
     */
1007
    public function fm($tag, $code = '')
1008
    {
1009
        foreach ($this->fields as $field) {
1010
            if ($field->tag === $tag) {
1011
                if ($code) {
1012
                    foreach ($field->subfields as $subfield) {
1013
                        if (strcasecmp($subfield->code, $code) === 0) {
1014
                            return $subfield->value;
1015
                        }
1016
                    }
1017
                } else {
1018
                    return $field->value;
1019
                }
1020
            }
1021
        }
1022

1023
        return null;
1024
    } // function fm
1025

1026
    /**
1027
     * Получение массива значений поля (или подполя)
1028
     * с указанной меткой (и указанным кодом).
1029
     *
1030
     * @param int $tag Искомая метка поля.
1031
     * @param string $code Код подполя.
1032
     * @return array
1033
     */
1034
    public function fma($tag, $code = '')
1035
    {
1036
        $result = array();
1037
        foreach ($this->fields as $field) {
1038
            if ($field->tag === $tag) {
1039
                if ($code) {
1040
                    foreach ($field->subfields as $subfield) {
1041
                        if (strcasecmp($subfield->code, $code) === 0) {
1042
                            if ($subfield->value) {
1043
                                $result[] = $subfield->value;
1044
                            }
1045
                        }
1046
                    }
1047
                } else {
1048
                    if ($field->value) {
1049
                        $result[] = $field->value;
1050
                    }
1051
                }
1052
            }
1053
        }
1054

1055
        return $result;
1056
    } // function fma
1057

1058
    /**
1059
     * Получение указанного поля (с учётом повторения).
1060
     *
1061
     * @param int $tag Метка поля.
1062
     * @param int $occurrence Номер повторения.
1063
     * @return RecordField|null
1064
     */
1065
    public function getField($tag, $occurrence = 0)
1066
    {
1067
        foreach ($this->fields as $field) {
1068
            if ($field->tag === $tag) {
1069
                if (!$occurrence) {
1070
                    return $field;
1071
                }
1072

1073
                $occurrence--;
1074
            }
1075
        }
1076

1077
        return null;
1078
    } // function getField
1079

1080
    /**
1081
     * Получение массива полей с указанной меткой.
1082
     *
1083
     * @param int $tag Искомая метка поля.
1084
     * @return array
1085
     */
1086
    public function getFields($tag)
1087
    {
1088
        $result = array();
1089
        foreach ($this->fields as $field) {
1090
            if ($field->tag === $tag) {
1091
                $result[] = $field;
1092
            }
1093
        }
1094

1095
        return $result;
1096
    } // function getFields
1097

1098
    /**
1099
     * Определяет, удалена ли запись?
1100
     *
1101
     * @return bool Запись удалена
1102
     * (неважно - логически или физически)?
1103
     */
1104
    public function isDeleted()
1105
    {
1106
        return ($this->status & 3) !== 0;
1107
    } // function is_deleted
1108

1109
    /**
1110
     * Вставляет поле по указанному индексу.
1111
     *
1112
     * @param int $index Позиция для вставки.
1113
     * @param RecordField $field Поле.
1114
     */
1115
    public function insertAt($index, RecordField $field)
1116
    {
1117
        array_splice($this->fields, $index, 0, $field);
1118
    } // function insertAt
1119

1120
    /**
1121
     * Удаляет поле по указанному индексу.
1122
     *
1123
     * @param int $index Индекс для удаления.
1124
     */
1125
    public function removeAt($index)
1126
    {
1127
        unset($this->fields[$index]);
1128
        $this->fields = array_values($this->fields);
1129
    } // function removeAt
1130

1131
    /**
1132
     * Удаляет все поля с указанной меткой.
1133
     *
1134
     * @param int $tag Индекс для удаления.
1135
     */
1136
    public function removeField($tag)
1137
    {
1138
        $flag = false;
1139
        $len = count($this->fields);
1140
        for ($i = 0; $i < $len; ++$i) {
1141
            $field = $this->fields[$i];
1142
            if ($field->tag === $tag) {
1143
                unset($this->fields[$i]);
1144
                $flag = true;
1145
            }
1146
        }
1147
        if ($flag) {
1148
            $this->fields = array_values($this->fields);
1149
        }
1150
    } // function removeField
1151

1152
    /**
1153
     * Сброс состояния записи, отвязка её от базы данных.
1154
     * Поля данных остаются при этом нетронутыми.
1155
     *
1156
     * @return $this
1157
     */
1158
    public function reset()
1159
    {
1160
        $this->mfn = 0;
1161
        $this->status = 0;
1162
        $this->version = 0;
1163
        $this->database = '';
1164

1165
        return $this;
1166
    } // function reset
1167

1168
    /**
1169
     * @brief Установка значения поля до первого разделителя.
1170
     *
1171
     * @param int $tag Искомая метка поля.
1172
     * @param string $value Новое значение до первого разделителя.
1173
     * @return $this
1174
     */
1175
    public function setValue($tag, $value)
1176
    {
1177
        if (!$value) {
1178
            $this->removeField($tag);
1179
        }
1180
        else {
1181
            $field = $this->getField($tag);
1182
            if (!$field) {
1183
                $field = $this->add($tag);
1184
            }
1185

1186
            $field->value = $value;
1187
        }
1188

1189
        return $this;
1190
    } // function setValue
1191

1192
    /**
1193
     * @brief Установка значения подполя.
1194
     *
1195
     * @param int $tag Метка поля.
1196
     * @param string $code Искомый код подполя.
1197
     * @param string $value Новое значение подполя.
1198
     * @return $this
1199
     */
1200
    public function setSubfield($tag, $code, $value)
1201
    {
1202
        $field = $this->getField($tag);
1203
        if (!$value) {
1204
            if ($field) {
1205
                $field->removeSubfield($code);
1206
            }
1207
        }
1208
        else {
1209
            if (!$field) {
1210
                $field = $this->add($tag);
1211
            }
1212

1213
            $field->setSubfield($code, $value);
1214
        }
1215

1216
        return $this;
1217
    } // function setSubfield
1218

1219
    /**
1220
     * Верификация записи.
1221
     *
1222
     * @param bool $throw Бросать ли исключение при ошибке?
1223
     * @return bool Результат верификации.
1224
     */
1225
    public function verify($throw = true)
1226
    {
1227
        $result = false;
1228
        foreach ($this->fields as $field) {
1229
            $result = $field->verify($throw);
1230
            if (!$result) {
1231
                break;
1232
            }
1233
        }
1234

1235
        return $result;
1236
    } // function verify
1237

1238
    public function __toString()
1239
    {
1240
        return $this->encode();
1241
    } // function __toStirng
1242
} // class MarcRecord
1243

1244
/**
1245
 * @brief Запись в "сыром" ("неразобранном") виде.
1246
 */
1247
final class RawRecord
1248
{
1249
    /**
1250
     * @var string Имя базы данных.
1251
     */
1252
    public $database = '';
1253

1254
    /**
1255
     * @var string MFN.
1256
     */
1257
    public $mfn = '';
1258

1259
    /**
1260
     * @var string Статус.
1261
     */
1262
    public $status = '';
1263

1264
    /**
1265
     * @var string Версия.
1266
     */
1267
    public $version = '';
1268

1269
    /**
1270
     * @var array Поля записи.
1271
     */
1272
    public $fields = array();
1273

1274
    /**
1275
     * Декодирование ответа сервера.
1276
     *
1277
     * @param array $lines Массив строк
1278
     * с клиентским представлением записи.
1279
     */
1280
    public function decode(array $lines)
1281
    {
1282
        if (empty($lines) || count($lines) < 2) {
1283
            return;
1284
        }
1285

1286
        // mfn and status of the record
1287
        $firstLine = explode('#', $lines[0]);
1288
        $this->mfn = intval($firstLine[0]);
1289
        $this->status = intval(safe_get($firstLine, 1));
1290

1291
        // version of the record
1292
        $secondLine = explode('#', $lines[1]);
1293
        $this->version = intval(safe_get($secondLine, 1));
1294
        $this->fields = array_slice($lines, 2);
1295
    } // function decode
1296

1297
    /**
1298
     * Кодирование записи в протокольное представление.
1299
     *
1300
     * @param string $delimiter Разделитель строк.
1301
     * В зависимости от ситуации ИРБИСный или обычный.
1302
     * @return string
1303
     */
1304
    public function encode($delimiter = IRBIS_DELIMITER)
1305
    {
1306
        $result = $this->mfn . '#' . $this->status . $delimiter
1307
            . '0#' . $this->version . $delimiter;
1308

1309
        foreach ($this->fields as $field) {
1310
            $result .= ($field . $delimiter);
1311
        }
1312

1313
        return $result;
1314
    } // function encode
1315
} // class RawRecord
1316

1317
/**
1318
 * @brief Строка найденной записи в ответе сервера.
1319
 */
1320
final class FoundLine
1321
{
1322
    /**
1323
     * @var bool Материализована?
1324
     */
1325
    public $materialized = false;
1326

1327
    /**
1328
     * @var int Порядковый номер.
1329
     */
1330
    public $serialNumber = 0;
1331

1332
    /**
1333
     * @var int MFN.
1334
     */
1335
    public $mfn = 0;
1336

1337
    /**
1338
     * @var null Иконка.
1339
     */
1340
    public $icon = null;
1341

1342
    /**
1343
     * @var bool Выбрана (помечена).
1344
     */
1345
    public $selected = false;
1346

1347
    /**
1348
     * @var string Библиографическое описание.
1349
     */
1350
    public $description = '';
1351

1352
    /**
1353
     * @var string Ключ для сортировки.
1354
     */
1355
    public $sort = '';
1356

1357
    /**
1358
     * Разбор ответа сервера.
1359
     *
1360
     * @param array $lines Строки ответа сервера.
1361
     * @return array Массив найденных записей с MFN
1362
     * и биб. описанием (опционально).
1363
     */
1364
    public static function parse(array $lines)
1365
    {
1366
        $result = array();
1367
        foreach ($lines as $line) {
1368
            $parts = explode('#', $line, 2);
1369
            $item = new FoundLine();
1370
            $item->mfn = intval($parts[0]);
1371
            $item->description = safe_get($parts, 1);
1372
            $result[] = $item;
1373
        }
1374

1375
        return $result;
1376
    }
1377

1378
    /**
1379
     * Разбор ответа сервера.
1380
     *
1381
     * @param array $lines Строки ответа сервера.
1382
     * @return array Массив MFN найденных записей.
1383
     */
1384
    public static function parseMfn(array $lines)
1385
    {
1386
        $result = array();
1387
        foreach ($lines as $line) {
1388
            $parts = explode('#', $line, 2);
1389
            $mfn = intval($parts[0]);
1390
            $result[] = $mfn;
1391
        }
1392

1393
        return $result;
1394
    }
1395

1396
    /**
1397
     * Преобразование в массив библиографических описаний.
1398
     *
1399
     * @param array $found Найденные записи.
1400
     * @return array Массив описаний.
1401
     */
1402
    public static function toDescription(array $found)
1403
    {
1404
        $result = array();
1405
        foreach ($found as $item) {
1406
            $result[] = $item->description;
1407
        }
1408

1409
        return $result;
1410
    }
1411

1412
    /**
1413
     * Преобразование в массив MFN.
1414
     *
1415
     * @param array $found Найденные записи.
1416
     * @return array Массив MFN.
1417
     */
1418
    public static function toMfn(array $found)
1419
    {
1420
        $result = array();
1421
        foreach ($found as $item) {
1422
            $result[] = $item->mfn;
1423
        }
1424

1425
        return $result;
1426
    }
1427

1428
    public function __toString()
1429
    {
1430
        return $this->description
1431
            ? $this->mfn . '#' . $this->description
1432
            : strval($this->mfn);
1433
    }
1434
} // class FoundLine
1435

1436
/**
1437
 * @brief Пара строк в меню.
1438
 */
1439
final class MenuEntry
1440
{
1441
    /**
1442
     * @var string Код -- первая строка в меню.
1443
     */
1444
    public $code;
1445

1446
    /**
1447
     * @var string Соответствующее коду значение -- вторая строка в меню.
1448
     */
1449
    public $comment;
1450

1451
    public function __toString()
1452
    {
1453
        return $this->code . ' - ' . $this->comment;
1454
    }
1455
} // class MenuEntry
1456

1457
/**
1458
 * @brief Файл меню. Состоит из пар строк (см. MenuEntry).
1459
 */
1460
final class MenuFile
1461
{
1462
    /**
1463
     * @var array Массив пар строк.
1464
     */
1465
    public $entries = array();
1466

1467
    /**
1468
     * Добавление элемента.
1469
     *
1470
     * @param string $code Код элемента.
1471
     * @param string $comment Комментарий.
1472
     * @return $this
1473
     */
1474
    public function add($code, $comment)
1475
    {
1476
        $entry = new MenuEntry();
1477
        $entry->code = $code;
1478
        $entry->comment = $comment;
1479
        $this->entries[] = $entry;
1480

1481
        return $this;
1482
    }
1483

1484
    /**
1485
     * Отыскивает запись, соответствующую данному коду.
1486
     *
1487
     * @param string $code
1488
     * @return mixed|null
1489
     */
1490
    public function getEntry($code)
1491
    {
1492
        foreach ($this->entries as $entry) {
1493
            if (strcasecmp($entry->code, $code) === 0) {
1494
                return $entry;
1495
            }
1496
        }
1497

1498
        $code = trim($code);
1499
        foreach ($this->entries as $entry) {
1500
            if (strcasecmp($entry->code, $code) === 0) {
1501
                return $entry;
1502
            }
1503
        }
1504

1505
        $code = self::trimCode($code);
1506
        foreach ($this->entries as $entry) {
1507
            if (strcasecmp($entry->code, $code) === 0) {
1508
                return $entry;
1509
            }
1510
        }
1511

1512
        return null;
1513
    }
1514

1515
    /**
1516
     * Выдает значение, соответствующее коду.
1517
     *
1518
     * @param $code
1519
     * @param string $defaultValue
1520
     * @return string
1521
     */
1522
    public function getValue($code, $defaultValue = '')
1523
    {
1524
        $entry = $this->getEntry($code);
1525
        if (!$entry) {
1526
            return $defaultValue;
1527
        }
1528

1529
        return $entry->comment;
1530
    }
1531

1532
    /**
1533
     * Разбор серверного представления MNU-файла.
1534
     *
1535
     * @param array $lines Массив строк.
1536
     */
1537
    public function parse(array $lines)
1538
    {
1539
        $length = count($lines);
1540
        for ($i = 0; $i < $length; $i += 2) {
1541
            $code = $lines[$i];
1542
            if (!$code || substr($code, 5) === '*****') {
1543
                break;
1544
            }
1545

1546
            $comment = $lines[$i + 1];
1547
            $entry = new MenuEntry();
1548
            $entry->code = $code;
1549
            $entry->comment = $comment;
1550
            $this->entries[] = $entry;
1551
        }
1552
    }
1553

1554
    /**
1555
     * Отрезание лишних символов в коде.
1556
     *
1557
     * @param string $code Код.
1558
     * @return string Очищенный код.
1559
     */
1560
    public static function trimCode($code)
1561
    {
1562
        return trim($code, '-=:');
1563
    }
1564

1565
    public function __toString()
1566
    {
1567
        $result = '';
1568

1569
        foreach ($this->entries as $entry) {
1570
            $result .= ($entry . PHP_EOL);
1571
        }
1572
        $result .= "*****\n";
1573

1574
        return $result;
1575
    }
1576
} // class MenuFile
1577

1578
/**
1579
 * @brief Строка INI-файла. Состоит из ключа
1580
 * и (опционального) значения.
1581
 */
1582
final class IniLine
1583
{
1584
    /**
1585
     * @var string Ключ.
1586
     */
1587
    public $key;
1588

1589
    /**
1590
     * @var string Значение.
1591
     */
1592
    public $value;
1593

1594
    public function __toString()
1595
    {
1596
        return $this->key . ' = ' . $this->value;
1597
    }
1598
} // class IniLine
1599

1600
/**
1601
 * @brief Секция INI-файла. Состоит из строк
1602
 * (см. IniLine).
1603
 */
1604
final class IniSection
1605
{
1606
    /**
1607
     * @var string Имя секции.
1608
     */
1609
    public $name = '';
1610

1611
    /**
1612
     * @var array Строки 'ключ=значение'.
1613
     */
1614
    public $lines = array();
1615

1616
    /**
1617
     * Поиск строки с указанным ключом.
1618
     *
1619
     * @param string $key Имя ключа.
1620
     * @return IniLine|null
1621
     */
1622
    public function find($key)
1623
    {
1624
        foreach ($this->lines as $line) {
1625
            if (same_string($line->key, $key)) {
1626
                return $line;
1627
            }
1628
        }
1629

1630
        return null;
1631
    }
1632

1633
    /**
1634
     * Получение значения для указанного ключа.
1635
     *
1636
     * @param string $key Имя ключа.
1637
     * @param string $defaultValue Значение по умолчанию.
1638
     * @return string Найденное значение или значение
1639
     * по умолчанию.
1640
     */
1641
    public function getValue($key, $defaultValue = '')
1642
    {
1643
        $found = $this->find($key);
1644
        return $found ? $found->value : $defaultValue;
1645
    }
1646

1647
    /**
1648
     * Удаление элемента с указанным ключом.
1649
     *
1650
     * @param string $key Имя ключа.
1651
     * @return IniSection
1652
     */
1653
    public function remove($key)
1654
    {
1655
        $length = count($this->lines);
1656
        for ($i = 0; $i < $length; $i++) {
1657
            if (same_string($this->lines[$i]->key, $key)) {
1658
                unset($this->lines[$i]);
1659
                break;
1660
            }
1661
        }
1662

1663
        return $this;
1664
    }
1665

1666
    /**
1667
     * Установка значения.
1668
     *
1669
     * @param string $key
1670
     * @param string $value
1671
     */
1672
    public function setValue($key, $value)
1673
    {
1674
        if (!$value) {
1675
            $this->remove($key);
1676
        } else {
1677
            $item = $this->find($key);
1678
            if ($item) {
1679
                $item->value = $value;
1680
            } else {
1681
                $item = new IniLine();
1682
                $item->key = $key;
1683
                $item->value = $value;
1684
                $this->lines[] = $item;
1685
            }
1686
        }
1687
    }
1688

1689
    public function __toString()
1690
    {
1691
        $result = '[' . $this->name . ']' . PHP_EOL;
1692

1693
        foreach ($this->lines as $line) {
1694
            $result .= ($line . PHP_EOL);
1695
        }
1696

1697
        return $result;
1698
    }
1699
} // class IniSection
1700

1701
/**
1702
 * @brief INI-файл. Состоит из секций (см. IniSection).
1703
 */
1704
final class IniFile
1705
{
1706
    /**
1707
     * @var array Секции INI-файла.
1708
     */
1709
    public $sections = array();
1710

1711
    /**
1712
     * Поиск секции с указанным именем.
1713
     *
1714
     * @param string $name Имя секции.
1715
     * @return mixed|null
1716
     */
1717
    public function findSection($name)
1718
    {
1719
        foreach ($this->sections as $section) {
1720
            if (same_string($section->name, $name)) {
1721
                return $section;
1722
            }
1723
        }
1724

1725
        return null;
1726
    }
1727

1728
    /**
1729
     * Поиск секции с указанным именем или создание
1730
     * в случае её отсутствия.
1731
     *
1732
     * @param string $name Имя секции.
1733
     * @return IniSection
1734
     */
1735
    public function getOrCreateSection($name)
1736
    {
1737
        $result = $this->findSection($name);
1738
        if (!$result) {
1739
            $result = new IniSection();
1740
            $result->name = $name;
1741
            $this->sections[] = $result;
1742
        }
1743

1744
        return $result;
1745
    }
1746

1747
    /**
1748
     * Получение значения (из одной из секций).
1749
     *
1750
     * @param string $sectionName Имя секции.
1751
     * @param string $key Ключ искомого элемента.
1752
     * @param string $defaultValue Значение по умолчанию.
1753
     * @return string Значение найденного элемента
1754
     * или значение по умолчанию.
1755
     */
1756
    public function getValue($sectionName, $key, $defaultValue = '')
1757
    {
1758
        $section = $this->findSection($sectionName);
1759
        if ($section) {
1760
            return $section->getValue($key, $defaultValue);
1761
        }
1762

1763
        return $defaultValue;
1764
    }
1765

1766
    /**
1767
     * Разбор текстового представления INI-файла.
1768
     *
1769
     * @param array $lines Строки INI-файла.
1770
     */
1771
    public function parse(array $lines)
1772
    {
1773
        $section = null;
1774

1775
        foreach ($lines as $line) {
1776
            $trimmed = trim($line);
1777
            if (is_null_or_empty($trimmed)) {
1778
                continue;
1779
            }
1780

1781
            if ($trimmed[0] === '[') {
1782
                $name = substr($trimmed, 1, -1);
1783
                $section = $this->getOrCreateSection($name);
1784
            } else if ($section) {
1785
                $parts = explode('=', $trimmed, 2);
1786
                $key = $parts[0];
1787
                $value = safe_get($parts, 1);
1788
                $item = new IniLine();
1789
                $item->key = $key;
1790
                $item->value = $value;
1791
                $section->lines[] = $item;
1792
            }
1793
        }
1794
    }
1795

1796
    /**
1797
     * Установка значения элемента (в одной из секций).
1798
     *
1799
     * @param string $sectionName Имя секции.
1800
     * @param string $key Ключ элемента.
1801
     * @param string $value Значение элемента.
1802
     * @return $this
1803
     */
1804
    public function setValue($sectionName, $key, $value)
1805
    {
1806
        $section = $this->getOrCreateSection($sectionName);
1807
        $section->setValue($key, $value);
1808

1809
        return $this;
1810
    }
1811

1812
    public function __toString()
1813
    {
1814
        $result = '';
1815
        $first = true;
1816

1817
        foreach ($this->sections as $section) {
1818
            if (!$first) {
1819
                $result .= PHP_EOL;
1820
            }
1821

1822
            $result .= $section;
1823

1824
            $first = false;
1825
        }
1826

1827
        return $result;
1828
    }
1829
} // class IniFile
1830

1831
/**
1832
 * @brief Узел дерева TRE-файла.
1833
 */
1834
final class TreeNode
1835
{
1836
    /**
1837
     * @var array Дочерние узлы.
1838
     */
1839
    public $children = array();
1840

1841
    /**
1842
     * @var string Значение, хранящееся в узле.
1843
     */
1844
    public $value = '';
1845

1846
    /**
1847
     * @var int Уровень вложенности узла.
1848
     */
1849
    public $level = 0;
1850

1851
    /**
1852
     * TreeNode constructor.
1853
     * @param string $value
1854
     */
1855
    public function __construct($value = '')
1856
    {
1857
        $this->value = $value;
1858
    }
1859

1860
    /**
1861
     * Добавление дочернего узла с указанным значением.
1862
     *
1863
     * @param $value
1864
     * @return $this
1865
     */
1866
    public function add($value)
1867
    {
1868
        $child = new TreeNode($value);
1869
        $this->children[] = $child;
1870

1871
        return $this;
1872
    }
1873

1874
    public function __toString()
1875
    {
1876
        return $this->value;
1877
    }
1878
} // class TreeNode
1879

1880
/**
1881
 * @brief Дерево, хранящееся в TRE-файле.
1882
 */
1883
final class TreeFile
1884
{
1885
    /**
1886
     * @var array Корни дерева.
1887
     */
1888
    public $roots = array();
1889

1890
    private static function arrange1(array $list, $level)
1891
    {
1892
        $count = count($list);
1893
        $index = 0;
1894

1895
        while ($index < $count) {
1896
            $next = self::arrange2($list, $level, $index, $count);
1897
            $index = $next;
1898
        }
1899
    }
1900

1901
    private static function arrange2(array $list, $level, $index, $count)
1902
    {
1903
        $next = $index + 1;
1904
        $level2 = $level + 1;
1905

1906
        $parent = $list[$index];
1907
        while ($next < $count) {
1908
            $child = $list[$next];
1909
            if ($child->level < $level) {
1910
                break;
1911
            }
1912

1913
            if ($child->level === $level2) {
1914
                $parent->children[] = $child;
1915
            }
1916

1917
            $next++;
1918
        }
1919

1920
        return $next;
1921
    }
1922

1923
    private static function countIndent($text)
1924
    {
1925
        $result = 0;
1926
        $length = strlen($text);
1927
        for ($i = 0; $i < $length; $i++) {
1928
            if ($text[$i] === "\t") {
1929
                $result++;
1930
            } else {
1931
                break;
1932
            }
1933
        }
1934

1935
        return $result;
1936
    }
1937

1938
    /**
1939
     * Добавление корневого элемента.
1940
     *
1941
     * @param string $value Значение элемента.
1942
     * @return TreeNode Созданный элемент.
1943
     */
1944
    public function addRoot($value)
1945
    {
1946
        $result = new TreeNode($value);
1947
        $this->roots[] = $result;
1948

1949
        return $result;
1950
    }
1951

1952
    /**
1953
     * Разбор ответа сервера.
1954
     *
1955
     * @param array $lines Строки с ответом сервера.
1956
     * @throws IrbisException
1957
     */
1958
    public function parse(array $lines)
1959
    {
1960
        if (!count($lines)) {
1961
            return;
1962
        }
1963

1964
        $list = array();
1965
        $currentLevel = 0;
1966
        $line = $lines[0];
1967
        if (self::countIndent($line) !== 0) {
1968
            throw new IrbisException();
1969
        }
1970

1971
        $list[] = new TreeNode($line);
1972
        $lines = array_slice($lines, 1);
1973
        foreach ($lines as $line) {
1974
            if (is_null_or_empty($line)) {
1975
                continue;
1976
            }
1977

1978
            $level = self::countIndent($line);
1979
            if ($level > ($currentLevel + 1)) {
1980
                throw new IrbisException();
1981
            }
1982

1983
            $currentLevel = $level;
1984
            $line = substr($line, $currentLevel);
1985
            $node = new TreeNode($line);
1986
            $node->level = $currentLevel;
1987
            $list[] = $node;
1988
        }
1989

1990
        $maxLevel = 0;
1991
        foreach ($list as $item) {
1992
            if ($item->level > $maxLevel) {
1993
                $maxLevel = $item->level;
1994
            }
1995
        }
1996

1997
        for ($level = 0; $level < $maxLevel; $level++) {
1998
            self::arrange1($list, $level);
1999
        }
2000

2001
        foreach ($list as $item) {
2002
            if ($item->level === 0) {
2003
                $this->roots[] = $item;
2004
            }
2005
        }
2006
    }
2007
} // class TreeFile
2008

2009
/**
2010
 * @brief Информация о базе данных ИРБИС.
2011
 */
2012
final class DatabaseInfo
2013
{
2014
    /**
2015
     * @var string Имя базы данных.
2016
     */
2017
    public $name = '';
2018

2019
    /**
2020
     * @var string Описание базы данных.
2021
     */
2022
    public $description = '';
2023

2024
    /**
2025
     * @var int Максимальный MFN.
2026
     */
2027
    public $maxMfn = 0;
2028

2029
    /**
2030
     * @var array Логически удалённые записи.
2031
     */
2032
    public $logicallyDeletedRecords = array();
2033

2034
    /**
2035
     * @var array Физически удалённые записи.
2036
     */
2037
    public $physicallyDeletedRecords = array();
2038

2039
    /**
2040
     * @var array Неактуализированные записи.
2041
     */
2042
    public $nonActualizedRecords = array();
2043

2044
    /**
2045
     * @var array Заблокированные записи.
2046
     */
2047
    public $lockedRecords = array();
2048

2049
    /**
2050
     * @var bool Признак блокировки базы данных в целом.
2051
     */
2052
    public $databaseLocked = false;
2053

2054
    /**
2055
     * @var bool База только для чтения.
2056
     */
2057
    public $readOnly = false;
2058

2059
    static function parseLine($line)
2060
    {
2061
        $result = array();
2062
        $items = explode(SHORT_DELIMITER, $line);
2063
        foreach ($items as $item) {
2064
            $result[] = intval($item);
2065
        }
2066

2067
        return $result;
2068
    }
2069

2070
    /**
2071
     * Разбор ответа сервера (см. getDatabaseInfo).
2072
     *
2073
     * @param array $lines Ответ сервера.
2074
     * @return DatabaseInfo
2075
     */
2076
    public static function parseResponse(array $lines)
2077
    {
2078
        $result = new DatabaseInfo();
2079
        if (!empty($lines)) {
2080
            $result->logicallyDeletedRecords = self::parseLine($lines[0]);
2081
            $result->physicallyDeletedRecords = self::parseLine(safe_get($lines, 1));
2082
            $result->nonActualizedRecords = self::parseLine(safe_get($lines, 2));
2083
            $result->lockedRecords = self::parseLine(safe_get($lines, 3));
2084
            $result->maxMfn = intval(safe_get($lines, 4));
2085
            $result->databaseLocked = intval(safe_get($lines, 5)) !== 0;
2086
        }
2087

2088
        return $result;
2089
    }
2090

2091
    /**
2092
     * Получение списка баз данных из MNU-файла.
2093
     *
2094
     * @param MenuFile $menu Меню.
2095
     * @return array
2096
     */
2097
    public static function parseMenu(MenuFile $menu)
2098
    {
2099
        $result = array();
2100
        foreach ($menu->entries as $entry) {
2101
            $name = $entry->code;
2102
            if ($name === '*****') {
2103
                break;
2104
            }
2105

2106
            $description = $entry->comment;
2107
            $readOnly = false;
2108
            if ($name[0] === '-') {
2109
                $name = substr($name, 1);
2110
                $readOnly = true;
2111
            }
2112

2113
            $db = new DatabaseInfo();
2114
            $db->name = $name;
2115
            $db->description = $description;
2116
            $db->readOnly = $readOnly;
2117
            $result[] = $db;
2118
        }
2119

2120
        return $result;
2121
    }
2122

2123
    public function __toString()
2124
    {
2125
        return $this->name;
2126
    }
2127
} // class DatabaseInfo
2128

2129
/**
2130
 * @brief Информация о запущенном на ИРБИС-сервере процессе.
2131
 */
2132
final class ProcessInfo
2133
{
2134
    /**
2135
     * @var string Просто порядковый номер в списке.
2136
     */
2137
    public $number = '';
2138

2139
    /**
2140
     * @var string С каким клиентом взаимодействует.
2141
     */
2142
    public $ipAddress = '';
2143

2144
    /**
2145
     * @var string Логин оператора.
2146
     */
2147
    public $name = '';
2148

2149
    /**
2150
     * @var string Идентификатор клиента.
2151
     */
2152
    public $clientId = '';
2153

2154
    /**
2155
     * @var string Тип АРМ.
2156
     */
2157
    public $workstation = '';
2158

2159
    /**
2160
     * @var string Время запуска.
2161
     */
2162
    public $started = '';
2163

2164
    /**
2165
     * @var string Последняя выполненная
2166
     * (или выполняемая) команда.
2167
     */
2168
    public $lastCommand = '';
2169

2170
    /**
2171
     * @var string Порядковый номер последней команды.
2172
     */
2173
    public $commandNumber = '';
2174

2175
    /**
2176
     * @var string Индентификатор процесса.
2177
     */
2178
    public $processId = '';
2179

2180
    /**
2181
     * @var string Состояние.
2182
     */
2183
    public $state = '';
2184

2185
    public static function parse(array $lines)
2186
    {
2187
        $result = array();
2188
        if (empty($lines) || count($lines) < 2)
2189
            return $result;
2190

2191
        $processCount = (int)$lines[0];
2192
        $linesPerProcess = (int)$lines[1];
2193
        if (!$processCount || !$linesPerProcess) {
2194
            return $result;
2195
        }
2196

2197
        $lines = array_slice($lines, 2);
2198
        for ($i = 0; $i < $processCount; $i++) {
2199
            if (count($lines) < 10)
2200
                break;
2201

2202
            $process = new ProcessInfo();
2203
            $process->number = $lines[0];
2204
            $process->ipAddress = $lines[1];
2205
            $process->name = $lines[2];
2206
            $process->clientId = $lines[3];
2207
            $process->workstation = $lines[4];
2208
            $process->started = $lines[5];
2209
            $process->lastCommand = $lines[6];
2210
            $process->commandNumber = $lines[7];
2211
            $process->processId = $lines[8];
2212
            $process->state = $lines[9];
2213

2214
            $result[] = $process;
2215
            $lines = array_slice($lines, $linesPerProcess);
2216
        }
2217

2218
        return $result;
2219
    }
2220

2221
    public function __toString()
2222
    {
2223
        return "$this->number $this->ipAddress $this->name";
2224
    }
2225
} // class ProcessInfo
2226

2227
/**
2228
 * @brief Информация о версии ИРБИС-сервера.
2229
 */
2230
final class VersionInfo
2231
{
2232
    /**
2233
     * @var string На какое юридическое лицо приобретен сервер.
2234
     */
2235
    public $organization = '';
2236

2237
    /**
2238
     * @var string Собственно версия сервера. Например, 64.2008.1
2239
     */
2240
    public $version = '';
2241

2242
    /**
2243
     * @var int Максимальное количество подключений.
2244
     */
2245
    public $maxClients = 0;
2246

2247
    /**
2248
     * @var int Текущее количество подключений.
2249
     */
2250
    public $connectedClients = 0;
2251

2252
    /**
2253
     * Разбор ответа сервера.
2254
     *
2255
     * @param array $lines Строки с ответом сервера.
2256
     */
2257
    public function parse(array $lines)
2258
    {
2259
        if (count($lines) === 3) {
2260
            $this->version = $lines[0];
2261
            $this->connectedClients = (int)$lines[1];
2262
            $this->maxClients = (int)$lines[2];
2263
        } else {
2264
            $this->organization = $lines[0];
2265
            $this->version = safe_get($lines, 1);
2266
            $this->connectedClients = (int)safe_get($lines, 2);
2267
            $this->maxClients = (int)safe_get($lines, 3);
2268
        }
2269
    }
2270

2271
    public function __toString()
2272
    {
2273
        return $this->version;
2274
    }
2275
} // class VersionInfo
2276

2277
/**
2278
 * Информация о клиенте, подключенном к серверу ИРБИС
2279
 * (не обязательно о текущем).
2280
 */
2281
final class ClientInfo
2282
{
2283
    /**
2284
     * @var string Порядковый номер.
2285
     */
2286
    public $number = '';
2287

2288
    /**
2289
     * @var string Адрес клиента.
2290
     */
2291
    public $ipAddress = '';
2292

2293
    /**
2294
     * @var string Порт клиента.
2295
     */
2296
    public $port = '';
2297

2298
    /**
2299
     * @var string Логин.
2300
     */
2301
    public $name = '';
2302

2303
    /**
2304
     * @var string Идентификатор клиентской программы
2305
     * (просто уникальное число).
2306
     */
2307
    public $id = '';
2308

2309
    /**
2310
     * @var string Клиентский АРМ.
2311
     */
2312
    public $workstation = '';
2313

2314
    /**
2315
     * @var string Момент подключения к серверу.
2316
     */
2317
    public $registered = '';
2318

2319
    /**
2320
     * @var string Последнее подтверждение, посланное серверу.
2321
     */
2322
    public $acknowledged = '';
2323

2324
    /**
2325
     * @var string Последняя команда, посланная серверу.
2326
     */
2327
    public $lastCommand = '';
2328

2329
    /**
2330
     * @var string Номер последней команды.
2331
     */
2332
    public $commandNumber = '';
2333

2334
    /**
2335
     * Разбор ответа сервера.
2336
     *
2337
     * @param array $lines Строки ответа.
2338
     */
2339
    public function parse(array $lines)
2340
    {
2341
        if (empty($lines) || count($lines) < 10) {
2342
            return;
2343
        }
2344

2345
        $this->number = $lines[0];
2346
        $this->ipAddress = $lines[1];
2347
        $this->port = $lines[2];
2348
        $this->name = $lines[3];
2349
        $this->id = $lines[4];
2350
        $this->workstation = $lines[5];
2351
        $this->registered = $lines[6];
2352
        $this->acknowledged = $lines[7];
2353
        $this->lastCommand = $lines[8];
2354
        $this->commandNumber = $lines[9];
2355
    }
2356

2357
    public function __toString()
2358
    {
2359
        return $this->ipAddress;
2360
    }
2361
} // class ClientInfo
2362

2363
/**
2364
 * Информация о зарегистрированном пользователе системы
2365
 * (по данным client_m.mnu).
2366
 */
2367
final class UserInfo
2368
{
2369
    /**
2370
     * @var string Номер по порядку в списке.
2371
     */
2372
    public $number = '';
2373

2374
    /**
2375
     * @var string Логин.
2376
     */
2377
    public $name = '';
2378

2379
    /**
2380
     * @var string Пароль.
2381
     */
2382
    public $password = '';
2383

2384
    /**
2385
     * @var string Доступность АРМ Каталогизатор.
2386
     */
2387
    public $cataloger = '';
2388

2389
    /**
2390
     * @var string АРМ Читатель.
2391
     */
2392
    public $reader = '';
2393

2394
    /**
2395
     * @var string АРМ Книговыдача.
2396
     */
2397
    public $circulation = '';
2398

2399
    /**
2400
     * @var string АРМ Комплектатор.
2401
     */
2402
    public $acquisitions = '';
2403

2404
    /**
2405
     * @var string АРМ Книгообеспеченность.
2406
     */
2407
    public $provision = '';
2408

2409
    /**
2410
     * @var string АРМ Администратор.
2411
     */
2412
    public $administrator = '';
2413

2414
    public static function formatPair($prefix, $value, $default)
2415
    {
2416
        if (same_string($value, $default)) {
2417
            return '';
2418
        }
2419

2420
        return $prefix . '=' . $value . ';';
2421
    }
2422

2423
    /**
2424
     * Формирование строкового представления пользователя.
2425
     *
2426
     * @return string
2427
     */
2428
    public function encode()
2429
    {
2430
        return $this->name . "\r\n"
2431
            . $this->password . "\r\n"
2432
            . self::formatPair('C', $this->cataloger, 'irbisc.ini')
2433
            . self::formatPair('R', $this->reader, 'irbisr.ini')
2434
            . self::formatPair('B', $this->circulation, 'irbisb.ini')
2435
            . self::formatPair('M', $this->acquisitions, 'irbism.ini')
2436
            . self::formatPair('K', $this->provision, 'irbisk.ini')
2437
            . self::formatPair('A', $this->administrator, 'irbisa.ini');
2438
    }
2439

2440
    /**
2441
     * Разбор ответа сервера.
2442
     *
2443
     * @param array $lines Строки ответа сервера.
2444
     * @return array
2445
     */
2446
    public static function parse(array $lines)
2447
    {
2448
        $result = array();
2449
        if (empty($lines) || count($lines) < 2)
2450
            return $result;
2451

2452
        $userCount = intval($lines[0]);
2453
        $linesPerUser = intval($lines[1]);
2454
        if (!$userCount || !$linesPerUser)
2455
            return $result;
2456

2457
        $lines = array_slice($lines, 2);
2458
        for ($i = 0; $i < $userCount; $i++) {
2459
            if (empty($lines) || count($lines) < 9)
2460
                break;
2461

2462
            $user = new UserInfo();
2463
            $user->number = $lines[0];
2464
            $user->name = $lines[1];
2465
            $user->password = $lines[2];
2466
            $user->cataloger = $lines[3];
2467
            $user->reader = $lines[4];
2468
            $user->circulation = $lines[5];
2469
            $user->acquisitions = $lines[6];
2470
            $user->provision = $lines[7];
2471
            $user->administrator = $lines[8];
2472
            $result[] = $user;
2473

2474
            $lines = array_slice($lines, $linesPerUser + 1);
2475
        }
2476

2477
        return $result;
2478
    }
2479

2480
    public function __toString()
2481
    {
2482
        return $this->name;
2483
    }
2484
} // class UserInfo
2485

2486
/**
2487
 * Данные для метода printTable.
2488
 */
2489
final class TableDefinition
2490
{
2491
    /**
2492
     * @var string Имя базы данных.
2493
     */
2494
    public $database = '';
2495

2496
    /**
2497
     * @var string Имя таблицы.
2498
     */
2499
    public $table = '';
2500

2501
    /**
2502
     * @var array Заголовки таблицы.
2503
     */
2504
    public $headers = array();
2505

2506
    /**
2507
     * @var string Режим таблицы.
2508
     */
2509
    public $mode = '';
2510

2511
    /**
2512
     * @var string Поисковый запрос.
2513
     */
2514
    public $searchQuery = '';
2515

2516
    /**
2517
     * @var int Минимальный MFN.
2518
     */
2519
    public $minMfn = 0;
2520

2521
    /**
2522
     * @var int Максимальный MFN.
2523
     */
2524
    public $maxMfn = 0;
2525

2526
    /**
2527
     * @var string Запрос для последовательного поиска.
2528
     */
2529
    public $sequentialQuery = '';
2530

2531
    /**
2532
     * @var array Список MFN, по которым строится таблица.
2533
     */
2534
    public $mfnList = array();
2535

2536
    public function __toString()
2537
    {
2538
        return $this->table;
2539
    }
2540
} // class TableDefinition
2541

2542
/**
2543
 * Статистика работы ИРБИС-сервера.
2544
 */
2545
final class ServerStat
2546
{
2547
    /**
2548
     * @var array Подключенные клиенты.
2549
     */
2550
    public $runningClients = array();
2551

2552
    /**
2553
     * @var int Число клиентов, подключенных в текущий момент.
2554
     */
2555
    public $clientCount = 0;
2556

2557
    /**
2558
     * @var int Общее количество команд, исполненных сервером с момента запуска.
2559
     */
2560
    public $totalCommandCount = 0;
2561

2562
    /**
2563
     * Разбор ответа сервера.
2564
     *
2565
     * @param array $lines Строки ответа сервера.
2566
     */
2567
    public function parse(array $lines)
2568
    {
2569
        if (empty($lines) || count($lines) < 2)
2570
            return;
2571

2572
        $this->totalCommandCount = intval($lines[0]);
2573
        $this->clientCount = intval($lines[1]);
2574
        $linesPerClient = intval($lines[2]);
2575
        if (!$linesPerClient) {
2576
            return;
2577
        }
2578

2579
        $lines = array_slice($lines, 3);
2580

2581
        for ($i = 0; $i < $this->clientCount; $i++) {
2582
            $client = new ClientInfo();
2583
            $client->parse($lines);
2584
            if (!$client->name)
2585
                break;
2586
            $this->runningClients[] = $client;
2587
            $lines = array_slice($lines, $linesPerClient + 1);
2588
        }
2589
    }
2590

2591
    public function __toString()
2592
    {
2593
        $result = strval($this->totalCommandCount) . "\n"
2594
            . strval($this->clientCount) . "\n" . '8' . "\n";
2595
        foreach ($this->runningClients as $client) {
2596
            $result .= (strval($client) . "\n");
2597
        }
2598

2599
        return $result;
2600
    }
2601
} // class ServerStat
2602

2603
/**
2604
 * Параметры для запроса постингов с сервера.
2605
 */
2606
final class PostingParameters
2607
{
2608
    /**
2609
     * @var string База данных.
2610
     */
2611
    public $database = '';
2612

2613
    /**
2614
     * @var int Номер первого постинга.
2615
     */
2616
    public $firstPosting = 1;
2617

2618
    /**
2619
     * @var string Формат.
2620
     */
2621
    public $format = '';
2622

2623
    /**
2624
     * @var int Требуемое количество постингов.
2625
     */
2626
    public $numberOfPostings = 0;
2627

2628
    /**
2629
     * @var string Термин.
2630
     */
2631
    public $term = '';
2632

2633
    /**
2634
     * @var array Список термов.
2635
     */
2636
    public $listOfTerms = array();
2637
} // class PostingParameters
2638

2639
/**
2640
 * Параметры для запроса терминов с сервера.
2641
 */
2642
final class TermParameters
2643
{
2644
    /**
2645
     * @var string Имя базы данных.
2646
     */
2647
    public $database = '';
2648

2649
    /**
2650
     * @var int Количество считываемых терминов.
2651
     */
2652
    public $numberOfTerms = 0;
2653

2654
    /**
2655
     * @var bool Возвращать в обратном порядке?
2656
     */
2657
    public $reverseOrder = false;
2658

2659
    /**
2660
     * @var string Начальный термин.
2661
     */
2662
    public $startTerm = '';
2663

2664
    /**
2665
     * @var string Формат.
2666
     */
2667
    public $format = '';
2668
} // class TermParameters
2669

2670
/**
2671
 * Информация о термине поискового словаря.
2672
 */
2673
final class TermInfo
2674
{
2675
    /**
2676
     * @var int Количество ссылок.
2677
     */
2678
    public $count = 0;
2679

2680
    /**
2681
     * @var string Поисковый термин.
2682
     */
2683
    public $text = '';
2684

2685
    public static function parse(array $lines)
2686
    {
2687
        $result = array();
2688
        foreach ($lines as $line) {
2689
            if (!is_null_or_empty($line)) {
2690
                $parts = explode('#', $line, 2);
2691
                $term = new TermInfo();
2692
                $term->count = intval($parts[0]);
2693
                $term->text = safe_get($parts, 1);
2694
                $result[] = $term;
2695
            }
2696
        }
2697

2698
        return $result;
2699
    }
2700

2701
    public function __toString()
2702
    {
2703
        return $this->count . '#' . $this->text;
2704
    }
2705
} // class TermInfo
2706

2707
/**
2708
 * Постинг термина в поисковом индексе.
2709
 */
2710
final class TermPosting
2711
{
2712
    /**
2713
     * @var int MFN записи с искомым термином.
2714
     */
2715
    public $mfn = 0;
2716

2717
    /**
2718
     * @var int Метка поля с искомым термином.
2719
     */
2720
    public $tag = 0;
2721

2722
    /**
2723
     * @var int Повторение поля.
2724
     */
2725
    public $occurrence = 0;
2726

2727
    /**
2728
     * @var int Количество повторений.
2729
     */
2730
    public $count = 0;
2731

2732
    /**
2733
     * @var string Результат форматирования.
2734
     */
2735
    public $text = '';
2736

2737
    /**
2738
     * Разбор ответа сервера.
2739
     *
2740
     * @param array $lines Строки ответа.
2741
     * @return array Массив постингов.
2742
     */
2743
    public static function parse(array $lines)
2744
    {
2745
        $result = array();
2746
        foreach ($lines as $line) {
2747
            $parts = explode('#', $line, 5);
2748
            if (count($parts) < 4) {
2749
                break;
2750
            }
2751

2752
            $item = new TermPosting();
2753
            $item->mfn = intval($parts[0]);
2754
            $item->tag = intval(safe_get($parts, 1));
2755
            $item->occurrence = intval(safe_get($parts, 2));
2756
            $item->count = intval(safe_get($parts, 3));
2757
            $item->text = safe_get($parts, 4);
2758
            $result[] = $item;
2759
        }
2760

2761
        return $result;
2762
    }
2763

2764
    public function __toString()
2765
    {
2766
        return $this->mfn . '#' . $this->tag . '#'
2767
            . $this->occurrence . '#' . $this->count
2768
            . '#' . $this->text;
2769
    }
2770
} // class TermPosting
2771

2772
/**
2773
 * Параметры для поиска записей (метод searchEx).
2774
 */
2775
final class SearchParameters
2776
{
2777
    /**
2778
     * @var string Имя базы данных.
2779
     */
2780
    public $database = '';
2781

2782
    /**
2783
     * @var int Индекс первой требуемой записи.
2784
     */
2785
    public $firstRecord = 1;
2786

2787
    /**
2788
     * @var string Формат для расформатирования записей.
2789
     */
2790
    public $format = '';
2791

2792
    /**
2793
     * @var int Максимальный MFN.
2794
     */
2795
    public $maxMfn = 0;
2796

2797
    /**
2798
     * @var int Минимальный MFN.
2799
     */
2800
    public $minMfn = 0;
2801

2802
    /**
2803
     * @var int Общее число требуемых записей.
2804
     */
2805
    public $numberOfRecords = 0;
2806

2807
    /**
2808
     * @var string Выражение для поиска по словарю.
2809
     */
2810
    public $expression = '';
2811

2812
    /**
2813
     * @var string Выражение для последовательного поиска.
2814
     */
2815
    public $sequential = '';
2816

2817
    /**
2818
     * @var string Выражение для локальной фильтрации.
2819
     */
2820
    public $filter = '';
2821

2822
    /**
2823
     * @var bool Признак кодировки UTF-8.
2824
     */
2825
    public $isUtf = false;
2826

2827
    /**
2828
     * @var bool Признак вложенного вызова.
2829
     */
2830
    public $nested = false;
2831
} // class SearchParameters
2832

2833
/**
2834
 * Сценарий поиска.
2835
 */
2836
final class SearchScenario
2837
{
2838
    /**
2839
     * @var string Название поискового атрибута (автор, инвентарный номер и т. д.).
2840
     */
2841
    public $name = '';
2842

2843
    /**
2844
     * @var string Префикс соответствующих терминов в словаре (может быть пустым).
2845
     */
2846
    public $prefix = '';
2847

2848
    /**
2849
     * @var int Тип словаря для соответствующего поиска.
2850
     */
2851
    public $dictionaryType = 0;
2852

2853
    /**
2854
     * @var string Имя файла справочника.
2855
     */
2856
    public $menuName = '';
2857

2858
    /**
2859
     * @var string Имя формата (без расширения).
2860
     */
2861
    public $oldFormat = '';
2862

2863
    /**
2864
     * @var string Способ корректировки по словарю.
2865
     */
2866
    public $correction = '';
2867

2868
    /**
2869
     * @var string Исходное положение переключателя "Усечение".
2870
     */
2871
    public $truncation = '';
2872

2873
    /**
2874
     * @var string Текст подсказки/предупреждения.
2875
     */
2876
    public $hint = '';
2877

2878
    /**
2879
     * @var string Параметр пока не задействован.
2880
     */
2881
    public $modByDicAuto = '';
2882

2883
    /**
2884
     * @var string Применимые логические операторы.
2885
     */
2886
    public $logic = '';
2887

2888
    /**
2889
     * @var string Правила автоматического расширения поиска
2890
     * на основе авторитетного файла или тезауруса.
2891
     */
2892
    public $advance = '';
2893

2894
    /**
2895
     * @var string Имя формата показа документов.
2896
     */
2897
    public $format = '';
2898

2899
    static function get(IniSection $section, $name, $index)
2900
    {
2901
        $fullName = 'Item' . $name . $index;
2902
        return $section->getValue($fullName);
2903
    }
2904

2905
    /**
2906
     * Разбор INI-файла.
2907
     *
2908
     * @param IniFile $iniFile
2909
     * @return array
2910
     */
2911
    public static function parse(IniFile $iniFile)
2912
    {
2913
        $result = array();
2914
        $section = $iniFile->findSection('SEARCH');
2915
        if ($section) {
2916
            $count = intval($section->getValue('ItemNumb'));
2917
            for ($i = 0; $i < $count; $i++) {
2918
                $scenario = new SearchScenario();
2919
                $scenario->name = self::get($section, "Name", $i);
2920
                $scenario->prefix = self::get($section, "Pref", $i);
2921
                $scenario->dictionaryType = intval(self::get($section, "DictionType", $i));
2922
                $scenario->menuName = self::get($section, "Menu", $i);
2923
                $scenario->oldFormat = '';
2924
                $scenario->correction = self::get($section, "ModByDic", $i);
2925
                $scenario->truncation = self::get($section, "Tranc", $i);
2926
                $scenario->hint = self::get($section, "Hint", $i);
2927
                $scenario->modByDicAuto = self::get($section, "ModByDicAuto", $i);
2928
                $scenario->logic = self::get($section, "Logic", $i);
2929
                $scenario->advance = self::get($section, "Adv", $i);
2930
                $scenario->format = self::get($section, "Pft", $i);
2931
                $result[] = $scenario;
2932
            }
2933
        }
2934

2935
        return $result;
2936
    }
2937
} // class SearchScenario
2938

2939
/**
2940
 * PAR-файл -- содержит пути к файлам базы данных ИРБИС.
2941
 */
2942
final class ParFile
2943
{
2944

2945
    // Пример файла IBIS.PAR:
2946
    //
2947
    // 1=.\datai\ibis\
2948
    // 2=.\datai\ibis\
2949
    // 3=.\datai\ibis\
2950
    // 4=.\datai\ibis\
2951
    // 5=.\datai\ibis\
2952
    // 6=.\datai\ibis\
2953
    // 7=.\datai\ibis\
2954
    // 8=.\datai\ibis\
2955
    // 9=.\datai\ibis\
2956
    // 10=.\datai\ibis\
2957
    // 11=f:\webshare\
2958

2959
    /**
2960
     * @var string Путь к файлу XRF.
2961
     */
2962
    public $xrf = '';
2963

2964
    /**
2965
     * @var string Путь к файлу MST.
2966
     */
2967
    public $mst = '';
2968

2969
    /**
2970
     * @var string Путь к файлу CNT.
2971
     */
2972
    public $cnt = '';
2973

2974
    /**
2975
     * @var string Путь к файлу N01.
2976
     */
2977
    public $n01 = '';
2978

2979
    /**
2980
     * @var string В ИРБИС64 не используется.
2981
     */
2982
    public $n02 = '';
2983

2984
    /**
2985
     * @var string Путь к файлу L01.
2986
     */
2987
    public $l01 = '';
2988

2989
    /**
2990
     * @var string В ИРБИС64 не используется.
2991
     */
2992
    public $l02 = '';
2993

2994
    /**
2995
     * @var string Путь к файлу IFP.
2996
     */
2997
    public $ifp = '';
2998

2999
    /**
3000
     * @var string Путь к файлу ANY.
3001
     */
3002
    public $any = '';
3003

3004
    /**
3005
     * @var string Путь к PFT-файлам.
3006
     */
3007
    public $pft = '';
3008

3009
    /**
3010
     * @var string Расположение внешних объектов (поле 951).
3011
     * Параметр появился в версии 2012.
3012
     */
3013
    public $ext = '';
3014

3015
    /**
3016
     * ParFile constructor.
3017
     * @param string $mst Путь к MST-файлу.
3018
     */
3019
    public function __construct($mst = '')
3020
    {
3021
        $this->mst = $mst;
3022
        $this->xrf = $mst;
3023
        $this->cnt = $mst;
3024
        $this->l01 = $mst;
3025
        $this->l02 = $mst;
3026
        $this->n01 = $mst;
3027
        $this->n02 = $mst;
3028
        $this->ifp = $mst;
3029
        $this->any = $mst;
3030
        $this->pft = $mst;
3031
        $this->ext = $mst;
3032
    }
3033

3034
    /**
3035
     * Разбор ответа сервера.
3036
     *
3037
     * @param array $lines Ответ сервера.
3038
     * @throws IrbisException
3039
     */
3040
    public function parse(array $lines)
3041
    {
3042
        $map = array();
3043
        foreach ($lines as $line) {
3044
            if (is_null_or_empty($line)) {
3045
                continue;
3046
            }
3047

3048
            $parts = explode('=', $line, 2);
3049
            if (count($parts) != 2) {
3050
                throw new IrbisException();
3051
            }
3052

3053
            $key = trim($parts[0]);
3054
            $value = trim($parts[1]);
3055
            $map[$key] = $value;
3056
        }
3057

3058
        $this->xrf = $map['1'];
3059
        $this->mst = $map['2'];
3060
        $this->cnt = $map['3'];
3061
        $this->n01 = $map['4'];
3062
        $this->n02 = $map['5'];
3063
        $this->l01 = $map['6'];
3064
        $this->l02 = $map['7'];
3065
        $this->ifp = $map['8'];
3066
        $this->any = $map['9'];
3067
        $this->pft = $map['10'];
3068
        $this->ext = $map['11'];
3069
    } // function parse
3070

3071
    public function __toString()
3072
    {
3073
        return '1=' . $this->xrf . PHP_EOL
3074
            . '2=' . $this->mst . PHP_EOL
3075
            . '3=' . $this->cnt . PHP_EOL
3076
            . '4=' . $this->n01 . PHP_EOL
3077
            . '5=' . $this->n02 . PHP_EOL
3078
            . '6=' . $this->l01 . PHP_EOL
3079
            . '7=' . $this->l02 . PHP_EOL
3080
            . '8=' . $this->ifp . PHP_EOL
3081
            . '9=' . $this->any . PHP_EOL
3082
            . '10=' . $this->pft . PHP_EOL
3083
            . '11=' . $this->ext . PHP_EOL;
3084
    } // function __toString()
3085

3086
} // class ParFile
3087

3088
/**
3089
 * Строка OPT-файла.
3090
 */
3091
final class OptLine
3092
{
3093
    /**
3094
     * @var string Паттерн.
3095
     */
3096
    public $pattern = '';
3097

3098
    /**
3099
     * @var string Соответствующий рабочий лист.
3100
     */
3101
    public $worksheet = '';
3102

3103
    /**
3104
     * @param $text
3105
     * @throws IrbisException
3106
     */
3107
    public function parse($text)
3108
    {
3109
        $parts = preg_split("/\s+/", trim($text), 2, PREG_SPLIT_NO_EMPTY);
3110
        if (count($parts) != 2) {
3111
            throw new IrbisException();
3112
        }
3113

3114
        $this->pattern = $parts[0];
3115
        $this->worksheet = $parts[1];
3116
    } // function parse
3117

3118
    public function __toString()
3119
    {
3120
        return $this->pattern . ' ' . $this->worksheet;
3121
    } // function __toString
3122

3123
} // class OptLine
3124

3125
/**
3126
 * OPT-файл -- файл оптимизации рабочих листов и форматов показа.
3127
 */
3128
final class OptFile
3129
{
3130
    // Пример OPT-файла
3131
    //
3132
    // 920
3133
    // 5
3134
    // PAZK  PAZK42
3135
    // PVK   PVK42
3136
    // SPEC  SPEC42
3137
    // J     !RPJ51
3138
    // NJ    !NJ31
3139
    // NJP   !NJ31
3140
    // NJK   !NJ31
3141
    // AUNTD AUNTD42
3142
    // ASP   ASP42
3143
    // MUSP  MUSP
3144
    // SZPRF SZPRF
3145
    // BOUNI BOUNI
3146
    // IBIS  IBIS
3147
    // +++++ PAZK42
3148
    // *****
3149

3150
    /**
3151
     * @var int Длина рабочего листа.
3152
     */
3153
    public $worksheetLength = 5;
3154

3155
    /**
3156
     * @var int Метка поля рабочего листа.
3157
     */
3158
    public $worksheetTag = 920;
3159

3160
    /**
3161
     * @var array Строки с паттернами.
3162
     */
3163
    public $lines = array();
3164

3165
    /**
3166
     * Получение рабочего листа записи.
3167
     *
3168
     * @param MarcRecord $record Запись
3169
     * @return string Рабочий лист.
3170
     */
3171
    public function getWorksheet(MarcRecord $record)
3172
    {
3173
        return $record->fm($this->worksheetTag);
3174
    }
3175

3176
    /**
3177
     * Разбор ответа сервера.
3178
     *
3179
     * @param array $lines Строки OPT-файла.
3180
     * @throws IrbisException
3181
     */
3182
    public function parse(array $lines)
3183
    {
3184
        if (empty($lines) || count($lines) < 2)
3185
            throw new IrbisException();
3186

3187
        $this->worksheetTag = intval($lines[0]);
3188
        $this->worksheetLength = intval($lines[1]);
3189
        $lines = array_slice($lines, 2);
3190
        foreach ($lines as $line) {
3191
            if (is_null_or_empty($line)) {
3192
                continue;
3193
            }
3194

3195
            if ($line[0] === '*') {
3196
                break;
3197
            }
3198

3199
            $item = new OptLine();
3200
            $item->parse($line);
3201
            $this->lines[] = $item;
3202
        }
3203
    }
3204

3205
    public static function sameChar($pattern, $testable)
3206
    {
3207
        if ($pattern === '+') {
3208
            return true;
3209
        }
3210

3211
        return strtolower($pattern) === strtolower($testable);
3212
    }
3213

3214
    /**
3215
     * Сопоставление строки с OPT-шаблоном.
3216
     *
3217
     * @param string $pattern Шаблон.
3218
     * @param string $testable Проверяемая строка.
3219
     * @return bool Совпало?
3220
     */
3221
    public static function sameText($pattern, $testable)
3222
    {
3223
        if (!$pattern) {
3224
            return false;
3225
        }
3226

3227
        if (!$testable) {
3228
            return $pattern[0] === '+';
3229
        }
3230

3231
        $patternIndex = 0;
3232
        $testableIndex = 0;
3233
        while (true) {
3234
            $patternChar = $patternIndex < strlen($pattern)
3235
                ? $pattern[$patternIndex] : "\0";
3236
            $testableChar = $testableIndex < strlen($testable)
3237
                ? $testable[$testableIndex] : "\0";
3238
            $patternNext = $patternIndex++ < strlen($pattern);
3239
            $testableNext = $testableIndex++ < strlen($testable);
3240

3241
            if ($patternNext && !$testableNext) {
3242
                if ($patternChar === '+') {
3243
                    while ($patternIndex < strlen($pattern)) {
3244
                        $patternChar = $pattern[$patternIndex];
3245
                        $patternIndex++;
3246
                        if ($patternChar !== '+') {
3247
                            return false;
3248
                        }
3249
                    }
3250

3251
                    return true;
3252
                }
3253
            }
3254

3255
            if ($patternNext != $testableNext) {
3256
                return false;
3257
            }
3258

3259
            if (!$patternNext) {
3260
                return true;
3261
            }
3262

3263
            if (!self::sameChar($patternChar, $testableChar)) {
3264
                return false;
3265
            }
3266
        }
3267

3268
        return false; // for PhpStorm
3269
    }
3270

3271
    /**
3272
     * Подбор значения для указанного текста.
3273
     *
3274
     * @param string $text Проверяемый текст.
3275
     * @return string|null Найденное значение либо null.
3276
     */
3277
    public function resolveWorksheet($text)
3278
    {
3279
        foreach ($this->lines as $line) {
3280
            if (self::sameText($line->pattern, $text)) {
3281
                return $line->worksheet;
3282
            }
3283
        }
3284

3285
        return null;
3286
    }
3287

3288
    public function __toString()
3289
    {
3290
        $result = strval($this->worksheetTag) . PHP_EOL
3291
            . strval($this->worksheetLength) . PHP_EOL;
3292

3293
        foreach ($this->lines as $line) {
3294
            $result .= (strval($line) . PHP_EOL);
3295
        }
3296

3297
        $result .= '*****' . PHP_EOL;
3298

3299
        return $result;
3300
    }
3301

3302
} // class OptFile
3303

3304
final class GblParameter
3305
{
3306
    /**
3307
     * @var string Наименование параметра, которое появится
3308
     * в названии столбца, задающего параметр.
3309
     */
3310
    public $title = '';
3311

3312
    /**
3313
     * @var string Значение параметра или пусто, если пользователю
3314
     * предлагается задать его значение перед выполнением
3315
     * корректировки. В этой строке можно задать имя файла
3316
     * меню (с расширением MNU) или имя рабочего листа подполей
3317
     * (с расширением Wss), которые будут поданы для выбора
3318
     * значения параметра.
3319
     */
3320
    public $value = '';
3321

3322
} // class GblParameter
3323

3324
/**
3325
 * Оператор глобальной корректировки с параметрами.
3326
 */
3327
final class GblStatement
3328
{
3329
    /**
3330
     * @var string Команда, например, ADD или DEL.
3331
     */
3332
    public $command = '';
3333

3334
    /**
3335
     * @var string Первый параметр, как правило, спецификация поля/подполя.
3336
     */
3337
    public $parameter1 = '';
3338

3339
    /**
3340
     * @var string Второй параметр, как правило, спецификация повторения.
3341
     */
3342
    public $parameter2 = '';
3343

3344
    /**
3345
     * @var string Первый формат, например, выражение для замены.
3346
     */
3347
    public $format1 = '';
3348

3349
    /**
3350
     * @var string Второй формат, например, заменяющее выражение.
3351
     */
3352
    public $format2 = '';
3353

3354
    /**
3355
     * GblStatement constructor.
3356
     *
3357
     * @param string $command Команда.
3358
     * @param string $parameter1 Параметр 1.
3359
     * @param string $parameter2 Параметр 2.
3360
     * @param string $format1 Формат 1.
3361
     * @param string $format2 Формат 2.
3362
     */
3363
    public function __construct($command,
3364
                                $parameter1 = 'XXXXXXXXX',
3365
                                $parameter2 = 'XXXXXXXXX',
3366
                                $format1 = 'XXXXXXXXX',
3367
                                $format2 = 'XXXXXXXXX')
3368
    {
3369
        $this->command = $command;
3370
        $this->parameter1 = $parameter1;
3371
        $this->parameter2 = $parameter2;
3372
        $this->format1 = $format1;
3373
        $this->format2 = $format2;
3374
    }
3375

3376
    public function __toString()
3377
    {
3378
        return $this->command . IRBIS_DELIMITER
3379
            . $this->parameter1 . IRBIS_DELIMITER
3380
            . $this->parameter2 . IRBIS_DELIMITER
3381
            . $this->format1 . IRBIS_DELIMITER
3382
            . $this->format2 . IRBIS_DELIMITER;
3383
    }
3384

3385
} // class GblStatement
3386

3387
/**
3388
 * Настройки для глобальной корректировки.
3389
 */
3390
final class GblSettings
3391
{
3392
    /**
3393
     * @var bool Актуализировать записи?
3394
     */
3395
    public $actualize = true;
3396

3397
    /**
3398
     * @var bool Запускать autoin.gbl?
3399
     */
3400
    public $autoin = false;
3401

3402
    /**
3403
     * @var string Имя базы данных.
3404
     */
3405
    public $database = '';
3406

3407
    /**
3408
     * @var string Имя файла.
3409
     */
3410
    public $filename = '';
3411

3412
    /**
3413
     * @var bool Применять формальный контроль?
3414
     */
3415
    public $formalControl = false;
3416

3417
    /**
3418
     * @var int Нижняя граница MFN для поиска обрабатываемых записей.
3419
     */
3420
    public $lowerBound = 0;
3421

3422
    /**
3423
     * @var int Максимальный MFN.
3424
     */
3425
    public $maxMfn = 0;
3426

3427
    /**
3428
     * @var array Список MFN для обработки.
3429
     */
3430
    public $mfnList = array();
3431

3432
    /**
3433
     * @var int Минимальный MFN. 0 означает "все записи в базе".
3434
     */
3435
    public $minMfn = 0;
3436

3437
    /**
3438
     * @var array Параметры глобальной корректировки.
3439
     * Как правило, параметров нет.
3440
     */
3441
    public $parameters = array();
3442

3443
    /**
3444
     * @var string Поисковое выражение отбора записей по словарю.
3445
     */
3446
    public $searchExpression = '';
3447

3448
    /**
3449
     * @var string Поисковое выражение последовательного поиска.
3450
     */
3451
    public $sequentialExpression = '';
3452

3453
    /**
3454
     * @var array Массив операторов.
3455
     */
3456
    public $statements = array();
3457

3458
    /**
3459
     * @var int Верхняя граница MFN для поиска обрабатываемых записей.
3460
     */
3461
    public $upperBound = 0;
3462

3463
    /**
3464
     * Произвести подстановку параметров (если таковые наличествуют).
3465
     *
3466
     * @param $text string Текст, в котором должна быть произведена подстановка.
3467
     * @return string Текст после подстановок.
3468
     */
3469
    public function substituteParameters($text)
3470
    {
3471
        $length = count($this->parameters);
3472
       for ($i = 0; $i < $length; ++$i) {
3473
           $mark = '%' . strval($i + 1);
3474
           $text = str_replace($mark, $this->parameters[$i]->value, $text);
3475
       }
3476

3477
       return $text;
3478
    }
3479

3480
} // class GblSettings
3481

3482
/**
3483
 * Клиентский запрос.
3484
 */
3485
final class ClientQuery
3486
{
3487
    private $accumulator = '';
3488

3489
    public function __construct(Connection $connection, $command)
3490
    {
3491
        $this->addAnsi($command)->newLine();
3492
        $this->addAnsi($connection->workstation)->newLine();
3493
        $this->addAnsi($command)->newLine();
3494
        $this->add($connection->clientId)->newLine();
3495
        $this->add($connection->queryId)->newLine();
3496
        $this->addAnsi($connection->password)->newLine();
3497
        $this->addAnsi($connection->username)->newLine();
3498
        $this->newLine();
3499
        $this->newLine();
3500
        $this->newLine();
3501
    }
3502

3503
    /**
3504
     * Добавляем целое число
3505
     * (по факту выходит кодировка ANSI).
3506
     *
3507
     * @param int $value Число.
3508
     * @return $this
3509
     */
3510
    public function add($value)
3511
    {
3512
        $this->addAnsi(strval($value));
3513

3514
        return $this;
3515
    }
3516

3517
    /**
3518
     * Добавляем текст в кодировке ANSI.
3519
     *
3520
     * @param string $value Добавляемый текст.
3521
     * @return $this
3522
     */
3523
    public function addAnsi($value)
3524
    {
3525
        $converted = utfToAnsi($value);
3526
        $this->accumulator .= $converted;
3527

3528
        return $this;
3529
    }
3530

3531
    /**
3532
     * Добавляем формат. Кодировка UTF8.
3533
     *
3534
     * @param string $format Формат.
3535
     * @return bool|ClientQuery
3536
     */
3537
    public function addFormat($format)
3538
    {
3539
        if (!$format) {
3540
            $this->newLine();
3541
            return false;
3542
        }
3543

3544
        $prepared = prepare_format(ltrim($format));
3545

3546
        if ($format[0] === '@') {
3547
            $this->addAnsi($format);
3548
        } else if ($format[0] === '!') {
3549
            $this->addUtf($prepared);
3550
        } else {
3551
            $this->addUtf("!" . $prepared);
3552
        }
3553

3554
        return $this->newLine();
3555
    }
3556

3557
    /**
3558
     * Добавляем текст в кодировке UTF-8.
3559
     *
3560
     * @param string $value Добавляемый текст.
3561
     * @return $this
3562
     */
3563
    public function addUtf($value)
3564
    {
3565
        $this->accumulator .= $value;
3566

3567
        return $this;
3568
    }
3569

3570
    /**
3571
     * Добавляем перевод строки.
3572
     *
3573
     * @return $this
3574
     */
3575
    public function newLine()
3576
    {
3577
        $this->accumulator .= chr(10);
3578

3579
        return $this;
3580
    }
3581

3582
    public function __toString()
3583
    {
3584
        return strlen($this->accumulator) . chr(10) . $this->accumulator;
3585
    }
3586
} // class ClientQuery
3587

3588
/**
3589
 * Ответ сервера.
3590
 */
3591
final class ServerResponse
3592
{
3593
    /**
3594
     * @var string Код команды (дублирует запрос).
3595
     */
3596
    public $command = '';
3597

3598
    /**
3599
     * @var int Идентификатор клиента (дублирует запрос).
3600
     */
3601
    public $clientId = 0;
3602

3603
    /**
3604
     * @var int Номер команды (дублирует запрос).
3605
     */
3606
    public $queryId = 0;
3607

3608
    /**
3609
     * @var int Код возврата (бывает не у всех ответов).
3610
     */
3611
    public $returnCode = 0;
3612

3613
    /**
3614
     * @var int Размер ответа в байтах
3615
     * (в некоторых сценариях не возвращается).
3616
     */
3617
    public $answerSize = 0;
3618

3619
    /**
3620
     * @var string Версия сервера
3621
     * (в некоторых сценариях не возвращается).
3622
     */
3623
    public $serverVersion = '';
3624

3625
    private $connection;
3626
    private $answer;
3627
    private $offset;
3628
    private $answerLength;
3629

3630
    public function __construct(Connection $connection, $socket)
3631
    {
3632
        $this->connection = $connection;
3633
        $this->answer = '';
3634

3635
		if ($connection->webServer === false) :
3636
			while ($buf = socket_read($socket, 2048)) {
3637
				$this->answer .= $buf;
3638
			}
3639
		else:
3640
			
3641
			$this->answer =	str_replace(IRBIS_START_REQUEST . chr(10), null, $socket);
3642
			$this->answer =	str_replace(IRBIS_END_REQUEST, null, $this->answer);
3643
					
3644
		endif;
3645

3646
        if ($connection->debug) {
3647
            $this->debug();
3648
        }
3649

3650
        $this->offset = 0;
3651
        $this->answerLength = strlen($this->answer);
3652

3653
        $this->command = $this->readAnsi();
3654
        $this->clientId = $this->readInteger();
3655
        $this->queryId = $this->readInteger();
3656
        $this->answerSize = $this->readInteger();
3657
        $this->serverVersion = $this->readAnsi();
3658
        for ($i = 0; $i < 5; $i++) {
3659
            $this->readAnsi();
3660
        }
3661
    }
3662

3663
    /**
3664
     * Проверка кода возврата.
3665
     *
3666
     * @param array $goodCodes Разрешенные коды возврата.
3667
     * @return bool Результат проверки.
3668
     */
3669
    public function checkReturnCode(array $goodCodes = array())
3670
    {
3671
        if ($this->getReturnCode() < 0) {
3672
            if (!in_array($this->returnCode, $goodCodes)) {
3673
                $this->connection->lastError = $this->returnCode;
3674
                return false;
3675
            }
3676
        }
3677
        return true;
3678
    }
3679

3680
    /**
3681
     * Отладочная печать.
3682
     */
3683
    public function debug()
3684
    {
3685
        file_put_contents('php://stderr', print_r($this->answer, TRUE));
3686
    }
3687

3688
    /**
3689
     * Чтение строки без преобразования кодировок.
3690
     *
3691
     * @return string Прочитанная строка.
3692
     */
3693
    public function getLine()
3694
    {
3695
        $result = '';
3696

3697
        while ($this->offset < $this->answerLength) {
3698
            $symbol = $this->answer[$this->offset];
3699
            $this->offset++;
3700

3701
            if ($symbol === chr(13)) {
3702
                if ($this->answer[$this->offset] === chr(10)) {
3703
                    $this->offset++;
3704
                }
3705
                break;
3706
            }
3707

3708
            $result .= $symbol;
3709
        }
3710

3711
        return $result;
3712
    }
3713

3714
    /**
3715
     * Получение кода возврата.
3716
     * Вызывается один раз в свой час и только тогда.
3717
     * Отрицательное число свидетельствует о проблеме.
3718
     *
3719
     * @return int Код возврата.
3720
     */
3721
    public function getReturnCode()
3722
    {
3723
        $this->returnCode = $this->readInteger();
3724
        return $this->returnCode;
3725
    }
3726

3727
    /**
3728
     * Чтение строки в кодировке ANSI.
3729
     *
3730
     * @return string Прочитанная строка.
3731
     */
3732
    public function readAnsi()
3733
    {
3734
        $result = $this->getLine();
3735
        $result = ansiToUtf($result);
3736

3737
        return $result;
3738
    }
3739

3740
    /**
3741
     * Чтение целого числа.
3742
     *
3743
     * @return int Прочитанное число.
3744
     */
3745
    public function readInteger()
3746
    {
3747
        $line = $this->getLine();
3748

3749
        return intval($line);
3750
    }
3751

3752
    /**
3753
     * Чтение оставшихся строк в кодировке ANSI.
3754
     *
3755
     * @return array
3756
     */
3757
    public function readRemainingAnsiLines()
3758
    {
3759
        $result = array();
3760

3761
        while ($this->offset < $this->answerLength) {
3762
            $line = $this->readAnsi();
3763
            $result[] = $line;
3764
        }
3765

3766
        return $result;
3767
    }
3768

3769
    /**
3770
     * Чтение оставшегося текста в кодировке ANSI.
3771
     *
3772
     * @return bool|string
3773
     */
3774
    public function readRemainingAnsiText()
3775
    {
3776
        $result = substr($this->answer, $this->offset);
3777
        $this->offset = $this->answerLength;
3778
        $result = ansiToUtf($result);
3779

3780
        return $result;
3781
    }
3782

3783
    /**
3784
     * Чтение оставшихся строк в кодировке UTF-8.
3785
     *
3786
     * @return array
3787
     */
3788
    public function readRemainingUtfLines()
3789
    {
3790
        $result = array();
3791

3792
        while ($this->offset < $this->answerLength) {
3793
            $line = $this->readUtf();
3794
            $result[] = $line;
3795
        }
3796

3797
        return $result;
3798
    }
3799

3800
    /**
3801
     * Чтение оставшегося текста в кодировке UTF-8.
3802
     *
3803
     * @return bool|string
3804
     */
3805
    public function readRemainingUtfText()
3806
    {
3807
        $result = substr($this->answer, $this->offset);
3808
        $this->offset = $this->answerLength;
3809

3810
        return $result;
3811
    }
3812

3813
    /**
3814
     * Чтение строки в кодировке UTF-8.
3815
     *
3816
     * @return string
3817
     */
3818
    public function readUtf()
3819
    {
3820
        return $this->getLine();
3821
    }
3822

3823
    public function GetCurl()
3824
	{
3825
		return $this->accumulator;
3826
	}
3827
} // class ServerResponse
3828

3829
/**
3830
 * Подключение к ИРБИС-серверу.
3831
 */
3832
final class Connection
3833
{
3834
    /**
3835
     * @var string Адрес сервера (можно как my.domain.com,
3836
     * так и 192.168.1.1).
3837
     */
3838
    public $host = '127.0.0.1';
3839

3840
    /**
3841
     * @var int Порт сервера.
3842
     */
3843
    public $port = 6666;
3844

3845
    /**
3846
     * @var string Логин пользователя. Регистр символов не учитывается.
3847
     */
3848
    public $username = '';
3849

3850
    /**
3851
     * @var string Пароль пользователя. Регистр символов учитывается.
3852
     */
3853
    public $password = '';
3854

3855
    /**
3856
     * @var string Имя текущей базы данных.
3857
     */
3858
    public $database = 'IBIS';
3859

3860
    /**
3861
     * @var string Код АРМа.
3862
     */
3863
    public $workstation = CATALOGER;
3864

3865
    /**
3866
     * @var int Идентификатор клиента.
3867
     * Задаётся автоматически при подключении к серверу.
3868
     */
3869
    public $clientId = 0;
3870

3871
    /**
3872
     * @var int Последовательный номер запроса к серверу.
3873
     * Ведётся автоматически.
3874
     */
3875
    public $queryId = 0;
3876

3877
    /**
3878
     * @var string Версия сервера (доступна после подключения).
3879
     */
3880
    public $serverVersion = '';
3881

3882
    /**
3883
     * @var IniFile Серверный INI-файл (доступен после подключения).
3884
     */
3885
    public $iniFile = null;
3886

3887
    /**
3888
     * @var int Интервал подтверждения, минуты
3889
     * (доступен после подключения).
3890
     */
3891
    public $interval = 0;
3892

3893
    private $connected = false;
3894

3895
    /**
3896
     * @var bool Признак отладки.
3897
     */
3898
    public $debug = false;
3899

3900
    /**
3901
     * @var int Код последней ошибки.
3902
     */
3903
    public $lastError = 0;
3904

3905
    /**
3906
     * @var bool Признак использования cgi (WebToIrbisServer)
3907
     */
3908
    public $webServer = false;
3909
	
3910
	/**
3911
     * @var string Относительный путь к шлюзу
3912
	 * default '/cgi-bin/irbis64r_plus/WebToIrbisServer.exe'
3913
     */
3914
    public $webCgi = '/cgi-bin/irbis64r_plus/WebToIrbisServer.exe';
3915

3916
    //================================================================
3917

3918
    function __destruct()
3919
    {
3920
        $this->disconnect();
3921
    }
3922

3923
    function _checkConnection()
3924
    {
3925
        if (!$this->connected) {
3926
            $this->lastError = -100003;
3927
            return false;
3928
        }
3929

3930
        return true;
3931
    }
3932

3933
    //================================================================
3934

3935
    /**
3936
     * Актуализация всех неактуализированных записей
3937
     * в указанной базе данных.
3938
     *
3939
     * @param string $database Имя базы данных.
3940
     * @return bool Признак успешности операции.
3941
     */
3942
    public function actualizeDatabase($database)
3943
    {
3944
        return $this->actualizeRecord($database, 0);
3945
    } // function actualizeDatabase
3946

3947
    /**
3948
     * Актуализация записи с указанным MFN.
3949
     *
3950
     * @param string $database Имя базы данных.
3951
     * @param int $mfn MFN, подлежащий актуализации.
3952
     * @return bool Признак успешности операции.
3953
     */
3954
    public function actualizeRecord($database, $mfn)
3955
    {
3956
        if (!$this->_checkConnection())
3957
            return false;
3958

3959
        $query = new ClientQuery($this, 'F');
3960
        $query->addAnsi($database)->newLine();
3961
        $query->add($mfn)->newLine();
3962
        $response = $this->execute($query);
3963
        if (!$response || !$response->checkReturnCode())
3964
            return false;
3965

3966
        return true;
3967
    } // function actualizeRecord
3968

3969
    /**
3970
     * Подключение к серверу ИРБИС64.
3971
     *
3972
     * @return bool Признак успешности операции.
3973
     */
3974
    function connect()
3975
    {
3976
        if ($this->connected)
3977
            return true;
3978

3979
        AGAIN:
3980
        $this->clientId = rand(100000, 900000);
3981
        $this->queryId = 1;
3982
        $query = new ClientQuery($this, 'A');
3983
        $query->addAnsi($this->username)->newLine();
3984
        $query->addAnsi($this->password);
3985

3986
        $response = $this->execute($query);
3987
        if (!$response)
3988
            return false;
3989

3990
        $response->getReturnCode();
3991
        if ($response->returnCode == -3337) {
3992
            goto AGAIN;
3993
        }
3994

3995
        if ($response->returnCode < 0) {
3996
            $this->lastError = $response->returnCode;
3997
            return false;
3998
        }
3999

4000
        $this->connected = true;
4001
        $this->serverVersion = $response->serverVersion;
4002
        $this->interval = intval($response->readUtf());
4003
        $lines = $response->readRemainingAnsiLines();
4004
        $this->iniFile = new IniFile();
4005
        $this->iniFile->parse($lines);
4006

4007
        return true;
4008
    } // function connect
4009

4010
    /**
4011
     * Создание базы данных.
4012
     *
4013
     * @param string $database Имя создаваемой базы.
4014
     * @param string $description Описание в свободной форме.
4015
     * @param int $readerAccess Читатель будет иметь доступ?
4016
     * @return bool Признак успешности операции.
4017
     */
4018
    function createDatabase($database, $description, $readerAccess = 1)
4019
    {
4020
        if (!$this->_checkConnection()) {
4021
            return false;
4022
        }
4023

4024
        $query = new ClientQuery($this, 'T');
4025
        $query->addAnsi($database)->newLine();
4026
        $query->addAnsi($description)->newLine();
4027
        $query->add($readerAccess)->newLine();
4028
        $response = $this->execute($query);
4029

4030
        return $response && $response->checkReturnCode();
4031
    } // function createDatabase
4032

4033
    /**
4034
     * Создание словаря в указанной базе данных.
4035
     *
4036
     * @param string $database Имя базы данных.
4037
     * @return bool Признак успешности операции.
4038
     */
4039
    public function createDictionary($database)
4040
    {
4041
        if (!$this->_checkConnection()) {
4042
            return false;
4043
        }
4044

4045
        $query = new ClientQuery($this, 'Z');
4046
        $query->addAnsi($database)->newLine();
4047
        $response = $this->execute($query);
4048

4049
        return $response && $response->checkReturnCode();
4050
    } // function createDictionary
4051

4052
    /**
4053
     * Удаление указанной базы данных.
4054
     *
4055
     * @param string $database Имя удаляемой базы данных.
4056
     * @return bool Признак успешности операции.
4057
     */
4058
    public function deleteDatabase($database)
4059
    {
4060
        if (!$this->_checkConnection()) {
4061
            return false;
4062
        }
4063

4064
        $query = new ClientQuery($this, 'W');
4065
        $query->addAnsi($database)->newLine();
4066
        $response = $this->execute($query);
4067

4068
        return $response && $response->checkReturnCode();
4069
    } // function deleteDatabase
4070

4071
    /**
4072
     * Удаление на сервере указанного файла.
4073
     *
4074
     * @param string $fileName Спецификация файла.
4075
     */
4076
    public function deleteFile($fileName)
4077
    {
4078
        $this->formatRecord("&uf('+9K$fileName')", 1);
4079
    } // function deleteFile
4080

4081
    /**
4082
     * Удаление записи по её MFN.
4083
     *
4084
     * @param int $mfn MFN удаляемой записи.
4085
     * @return bool Признак успешности операции.
4086
     */
4087
    public function deleteRecord($mfn)
4088
    {
4089
        $record = $this->readRecord($mfn);
4090
        if (!$record) {
4091
            return false;
4092
        }
4093

4094
        if (!$record->isDeleted()) {
4095
            $record->status |= LOGICALLY_DELETED;
4096
            $this->writeRecord($record);
4097
        }
4098

4099
        return true;
4100
    } // function deleteRecord
4101

4102
    /**
4103
     * Отключение от сервера.
4104
     *
4105
     * @return bool Признак успешности операции.
4106
     */
4107
    public function disconnect()
4108
    {
4109
        if (!$this->connected) {
4110
            return true;
4111
        }
4112

4113
        $query = new ClientQuery($this, 'B');
4114
        $query->addAnsi($this->username);
4115
        if (!$this->execute($query)) {
4116
            return false;
4117
        }
4118

4119
        $this->connected = false;
4120

4121
        return true;
4122
    } // function disconnect
4123

4124
    /**
4125
     * Отправка клиентского запроса на сервер
4126
     * и получение ответа от него.
4127
     *
4128
     * @param ClientQuery $query Клиентский запрос.
4129
     * @return bool|ServerResponse Ответ сервера
4130
     * либо признак сбоя операции.
4131
     */
4132
    public function execute(ClientQuery $query)
4133
    {
4134
        $this->lastError = 0;
4135
		if ($this->webServer === false) {
4136

4137
            // подключение по протоколу ИРБИС-сервера
4138
            $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
4139
            if ($socket === false) {
4140
                $this->lastError = -100001;
4141
                return false;
4142
            }
4143

4144
            if (!socket_connect($socket, $this->host, $this->port)) {
4145
                socket_close($socket);
4146
                $this->lastError = -100002;
4147
                return false;
4148
            }
4149

4150
            $packet = strval($query);
4151
            socket_write($socket, $packet, strlen($packet));
4152
            if ($this->debug) {
4153
                file_put_contents('php://stderr', print_r($packet, TRUE));
4154
            }
4155
        }
4156
        else {
4157

4158
            // подключение через cgi-прокси
4159
            $curl = curl_init();
4160
            $url = 'http://' . $this->host . $this->webCgi;
4161
            curl_setopt_array($curl, array(
4162
                    CURLOPT_URL => $url,
4163
                    CURLOPT_USERAGENT => 'irbis\client',
4164
                    CURLOPT_RETURNTRANSFER => true,
4165
                    CURLOPT_HTTPHEADER => array('Accept: *.*', 'Content-Type: application/octet-stream'),
4166
                    CURLOPT_POST => true,
4167
                    CURLOPT_POSTFIELDS => strval($query->GetCurl()),
4168
                )
4169
            );
4170
            $socket = curl_exec($curl);
4171
            curl_close($curl);
4172
        }
4173

4174
        $response = new ServerResponse($this, $socket);
4175
        $this->queryId++;
4176

4177
        return $response;
4178
    } // function execute
4179

4180
    /**
4181
     * Выполнение произвольной команды.
4182
     *
4183
     * @param string $command Код команды.
4184
     * @param array $params Опциональные параметры в кодировке ANSI.
4185
     * @return bool|ServerResponse Ответ сервера
4186
     * либо признак сбоя операции.
4187
     */
4188
    public function executeAnyCommand($command, array $params = [])
4189
    {
4190
        if (!$this->_checkConnection()) {
4191
            return false;
4192
        }
4193

4194
        $query = new ClientQuery($this, $command);
4195
        foreach ($params as $param) {
4196
            $query->addAnsi($param)->newLine();
4197
        }
4198

4199
        return $this->execute($query);
4200
    } // function executeAnyCommand
4201

4202
    /**
4203
     * Форматирование записи с указанным MFN.
4204
     *
4205
     * @param string $format Текст формата.
4206
     * @param int $mfn MFN записи.
4207
     * @return bool|string Результат расформатирования
4208
     * либо признак сбоя операции.
4209
     */
4210
    public function formatRecord($format, $mfn)
4211
    {
4212
        if (!$this->_checkConnection()) {
4213
            return false;
4214
        }
4215

4216
        $query = new ClientQuery($this, 'G');
4217
        $query->addAnsi($this->database)->newLine();
4218
        $query->addFormat($format);
4219
        $query->add(1)->newLine();
4220
        $query->add($mfn)->newLine();
4221
        $response = $this->execute($query);
4222
        if (!$response || !$response->checkReturnCode()) {
4223
            return false;
4224
        }
4225

4226
        return $response->readRemainingUtfText();
4227
    } // function formatRecord
4228

4229
    /**
4230
     * Форматирование записи в клиентском представлении.
4231
     *
4232
     * @param string $format Текст формата.
4233
     * @param MarcRecord $record Запись.
4234
     * @return bool|string Результат расформатирования
4235
     * либо признак сбоя операции.
4236
     */
4237
    public function formatVirtualRecord($format, MarcRecord $record)
4238
    {
4239
        if (!$this->_checkConnection()) {
4240
            return false;
4241
        }
4242

4243
        if (!$record) {
4244
            return false;
4245
        }
4246

4247
        $query = new ClientQuery($this, 'G');
4248
        $database = $record->database ?: $this->database;
4249
        $query->addAnsi($database)->newLine();
4250
        $query->addFormat($format);
4251
        $query->add(-2)->newLine();
4252
        $query->addUtf($record->encode());
4253
        $response = $this->execute($query);
4254
        if (!$response || !$response->checkReturnCode()) {
4255
            return false;
4256
        }
4257

4258
        return $response->readRemainingUtfText();
4259
    } // function formatVirtualRecord
4260

4261
    /**
4262
     * Расформатирование нескольких записей.
4263
     *
4264
     * @param string $format Формат.
4265
     * @param array $mfnList Массив MFN.
4266
     * @return array|bool Результат расформатирования
4267
     * либо признак сбоя операции.
4268
     */
4269
    public function formatRecords($format, array $mfnList)
4270
    {
4271
        if (!$this->_checkConnection())
4272
            return false;
4273

4274
        if (!$mfnList)
4275
            return array();
4276

4277
        $query = new ClientQuery($this, 'G');
4278
        $query->addAnsi($this->database)->newLine();
4279
        if (!$query->addFormat($format))
4280
            return array();
4281

4282
        $query->add(count($mfnList))->newLine();
4283
        foreach ($mfnList as $mfn)
4284
            $query->add($mfn)->newLine();
4285

4286
        $response = $this->execute($query);
4287
        if (!$response || !$response->checkReturnCode())
4288
            return false;
4289

4290
        $lines = $response->readRemainingUtfLines();
4291
        $result = array();
4292
        foreach ($lines as $line) {
4293
            $parts = explode('#', $line, 2);
4294
            if (count($parts) == 2)
4295
                $result[] = irbis_to_dos($parts[1]);
4296
        }
4297

4298
        return $result;
4299
    } // function formatRecords
4300

4301
    /**
4302
     * Получение информации о базе данных.
4303
     *
4304
     * @param string $database Имя базы данных.
4305
     * @return bool|DatabaseInfo Информация о базе данных
4306
     * либо признак сбоя операции.
4307
     */
4308
    public function getDatabaseInfo($database = '')
4309
    {
4310
        if (!$this->_checkConnection())
4311
            return false;
4312

4313
        $database = $database ?: $this->database;
4314
        $query = new ClientQuery($this, '0');
4315
        $query->addAnsi($database);
4316
        $response = $this->execute($query);
4317
        if (!$response || !$response->checkReturnCode())
4318
            return false;
4319

4320
        $lines = $response->readRemainingAnsiLines();
4321

4322
        return DatabaseInfo::parseResponse($lines);
4323
    } // function getDatabaseInfo
4324

4325
    /**
4326
     * Получение максимального MFN для указанной базы данных.
4327
     *
4328
     * @param string $database Имя базы данных.
4329
     * @return int Максимальный MFN
4330
     * либо 0 в качестве признака сбоя операции.
4331
     */
4332
    public function getMaxMfn($database)
4333
    {
4334
        if (!$this->_checkConnection()) {
4335
            return 0;
4336
        }
4337

4338
        $query = new ClientQuery($this, 'O');
4339
        $query->addAnsi($database);
4340
        $response = $this->execute($query);
4341
        if (!$response || !$response->checkReturnCode()) {
4342
            return 0;
4343
        }
4344

4345
        return $response->returnCode;
4346
    } // function getMaxMfn
4347

4348
    /**
4349
     * Массив постингов для указанных записи и префикса.
4350
     * @param int $mfn MFN записи.
4351
     * @param string $prefix Префикс в виде "A=$".
4352
     * @return array Массив TermPosting
4353
     * (пустой в случае сбоя операции).
4354
     */
4355
    public function getRecordPostings($mfn, $prefix)
4356
    {
4357
        $result = array();
4358
        if (!$this->_checkConnection()) {
4359
            return $result;
4360
        }
4361

4362
        $query = new ClientQuery($this, 'V');
4363
        $query->addAnsi($this->database)->newLine();
4364
        $query->add($mfn)->newLine();
4365
        $query->addUtf($prefix)->newLine();
4366
        $response = $this->execute($query);
4367
        if (!$response || !$response->checkReturnCode()) {
4368
            return $result;
4369
        }
4370

4371
        $lines = $response->readRemainingUtfLines();
4372

4373
        return TermPosting::parse($lines);
4374
    } // function getRecordPostings
4375

4376
    /**
4377
     * Получение статистики с сервера.
4378
     *
4379
     * @return bool|ServerStat Статистика
4380
     * либо признак сбоя операции.
4381
     */
4382
    public function getServerStat()
4383
    {
4384
        if (!$this->_checkConnection()) {
4385
            return false;
4386
        }
4387

4388
        $query = new ClientQuery($this, '+1');
4389
        $response = $this->execute($query);
4390
        if (!$response || !$response->checkReturnCode()) {
4391
            return false;
4392
        }
4393

4394
        $result = new ServerStat();
4395
        $result->parse($response->readRemainingAnsiLines());
4396

4397
        return $result;
4398
    } // function getServerStat
4399

4400
    /**
4401
     * Получение версии сервера.
4402
     *
4403
     * @return bool|VersionInfo Версия сервера
4404
     * либо признак сбоя операции.
4405
     */
4406
    public function getServerVersion()
4407
    {
4408
        if (!$this->_checkConnection())
4409
            return false;
4410

4411
        $query = new ClientQuery($this, '1');
4412
        $response = $this->execute($query);
4413
        if (!$response || !$response->checkReturnCode())
4414
            return false;
4415

4416
        $result = new VersionInfo();
4417
        $result->parse($response->readRemainingAnsiLines());
4418

4419
        return $result;
4420
    } // function getServerVersion
4421

4422
    /**
4423
     * Получение списка пользователей с сервера.
4424
     *
4425
     * @return array|bool Список пользователей
4426
     * либо признак сбоя операции.
4427
     */
4428
    public function getUserList()
4429
    {
4430
        if (!$this->_checkConnection())
4431
            return false;
4432

4433
        $query = new ClientQuery($this, '+9');
4434
        $response = $this->execute($query);
4435
        if (!$response || !$response->checkReturnCode())
4436
            return false;
4437

4438
        return UserInfo::parse($response->readRemainingAnsiLines());
4439
    } // function getUserList
4440

4441
    /**
4442
     * Глобальная корректировка.
4443
     *
4444
     * @param GblSettings $settings Параметры корректировки.
4445
     * @return array|bool Массив результатов корректировки
4446
     * либо признак сбоя операции.
4447
     */
4448
    public function globalCorrection(GblSettings $settings)
4449
    {
4450
        if (!$this->_checkConnection())
4451
            return false;
4452

4453
        $query = new ClientQuery($this, '5');
4454
        $database = $settings->database ?: $this->database;
4455
        $query->addAnsi($database)->newLine();
4456
        $query->add(intval($settings->actualize))->newLine();
4457
        if (!is_null_or_empty($settings->filename)) {
4458
            $query->addAnsi('@' . $settings->filename)->newLine();
4459
        } else {
4460
            // "!" здесь означает, что передавать будем в UTF-8
4461
            // не знаю, что тут означает "0"
4462
            $encoded = '!0' . IRBIS_DELIMITER;
4463
            foreach ($settings->statements as $statement) {
4464
                $encoded .=  $settings->substituteParameters(strval($statement));
4465
            }
4466
            $encoded .= IRBIS_DELIMITER;
4467
            $query->addUtf($encoded)->newLine();
4468
        }
4469

4470
        // отбор записей на основе поиска
4471
        $query->addUtf($settings->searchExpression)->newLine(); // поиск по словарю
4472
        $query->add($settings->lowerBound)->newLine(); // нижняя граница MFN
4473
        $query->add($settings->upperBound)->newLine(); // верхняя граница MFN
4474
        $query->addUtf($settings->sequentialExpression)->newLine(); // последовательный
4475

4476
        // TODO поддержка режима "кроме отмеченных"
4477
        if (!$settings->mfnList) {
4478
            $count = $settings->maxMfn - $settings->minMfn + 1;
4479
            $query->add($count)->newLine();
4480
            for ($mfn = $settings->minMfn; $mfn < $settings->maxMfn; $mfn++) {
4481
                $query->add($mfn)->newLine();
4482
            }
4483
        } else {
4484
            $query->add(count($settings->mfnList))->newLine();
4485
            foreach ($settings->mfnList as $item) {
4486
                $query->add($item)->newLine();
4487
            }
4488
        }
4489

4490
        if (!$settings->formalControl)
4491
            $query->addAnsi('*')->newLine();
4492

4493
        if (!$settings->autoin)
4494
            $query->addAnsi('&')->newLine();
4495

4496
        $response = $this->execute($query);
4497
        if (!$response || !$response->checkReturnCode())
4498
            return false;
4499

4500
        return $response->readRemainingAnsiLines();
4501
    } // function globalCorrection
4502

4503
    /**
4504
     * @return bool Получение статуса,
4505
     * подключен ли клиент в настоящее время.
4506
     */
4507
    public function isConnected()
4508
    {
4509
        return $this->connected;
4510
    } // function isConnected
4511

4512
    /**
4513
     * Получение списка баз данных с сервера.
4514
     *
4515
     * @param string $specification Спецификация файла со списком баз.
4516
     * @return array|bool Список баз данных
4517
     * либо признак сбоя операции.
4518
     */
4519
    public function listDatabases($specification = '1..dbnam2.mnu')
4520
    {
4521
        if (!$this->_checkConnection())
4522
            return false;
4523

4524
        $menu = $this->readMenuFile($specification);
4525
        if (!$menu)
4526
            return false;
4527

4528
        return DatabaseInfo::parseMenu($menu);
4529
    } // function listDatabases
4530

4531
    /**
4532
     * Получение списка файлов.
4533
     *
4534
     * @param string $specification Спецификация.
4535
     * @return array|bool Список файлов
4536
     * либо признак сбоя операции.
4537
     */
4538
    public function listFiles($specification)
4539
    {
4540
        if (!$this->_checkConnection())
4541
            return false;
4542

4543
        $query = new ClientQuery($this, '!');
4544
        $query->addAnsi($specification)->newLine();
4545
        $response = $this->execute($query);
4546
        if (!$response)
4547
            return false;
4548

4549
        $lines = $response->readRemainingAnsiLines();
4550
        $result = array();
4551
        foreach ($lines as $line) {
4552
            $files = irbis_to_lines($line);
4553
            foreach ($files as $file) {
4554
                if (!is_null_or_empty($file)) {
4555
                    $result[] = $file;
4556
                }
4557
            }
4558
        }
4559

4560
        return $result;
4561
    } // function listFiles
4562

4563
    /**
4564
     * Получение списка серверных процессов.
4565
     *
4566
     * @return array|bool Список процессов
4567
     * либо признак сбоя операции.
4568
     */
4569
    public function listProcesses()
4570
    {
4571
        if (!$this->_checkConnection())
4572
            return false;
4573

4574
        $query = new ClientQuery($this, '+3');
4575
        $response = $this->execute($query);
4576
        if (!$response || !$response->checkReturnCode())
4577
            return false;
4578

4579
        $lines = $response->readRemainingAnsiLines();
4580

4581
        return ProcessInfo::parse($lines);
4582
    } // function listProcesses
4583

4584
    /**
4585
     * Получение списка терминов с указанным префиксом.
4586
     *
4587
     * @param string $prefix Префикс.
4588
     * @return array Термины (очищенные от префикса)
4589
     * (пустой массив при сбое операции).
4590
     */
4591
    public function listTerms($prefix)
4592
    {
4593
        $result = array();
4594

4595
        if (!$this->_checkConnection())
4596
            return $result;
4597

4598
        $prefixLength = strlen($prefix);
4599
        $startTerm = $prefix;
4600
        $lastTerm = $startTerm;
4601
        while (true) {
4602
            $terms = $this->readTerms($startTerm, 512);
4603
            if (!$terms)
4604
                break;
4605

4606
            foreach ($terms as $term) {
4607
                $text = $term->text;
4608
                if (strcmp(substr($text, 0, $prefixLength), $prefix)) {
4609
                    break 2;
4610
                }
4611
                if ($text !== $startTerm) {
4612
                    $lastTerm = $text;
4613
                    $text = substr($text, $prefixLength);
4614
                    $result[] = $text;
4615
                }
4616
            }
4617
            $startTerm = $lastTerm;
4618
        }
4619

4620
        return $result;
4621
    } // function listTerms
4622

4623
    /**
4624
     * Пустая операция (используется для периодического
4625
     * подтверждения подключения клиента).
4626
     *
4627
     * @return bool Всегда true при наличии подключения,
4628
     * т. к. код возврата не анализируется.
4629
     * Всегда false при отсутствии подключения.
4630
     */
4631
    public function noOp()
4632
    {
4633
        if (!$this->_checkConnection()) {
4634
            return false;
4635
        }
4636

4637
        $query = new ClientQuery($this, 'N');
4638
        if (!$this->execute($query))
4639
            return false;
4640

4641
        return true;
4642
    } // function noOp
4643

4644
    /**
4645
     * Разбор строки подключения.
4646
     *
4647
     * @param string $connectionString Строка подключения.
4648
     * @throws IrbisException Ошибка в структуре строки подключения.
4649
     */
4650
    public function parseConnectionString($connectionString)
4651
    {
4652
        $items = explode(';', $connectionString);
4653
        foreach ($items as $item) {
4654
            if (is_null_or_empty($item)) {
4655
                continue;
4656
            }
4657

4658
            $parts = explode('=', $item, 2);
4659
            if (count($parts) !== 2) {
4660
                continue;
4661
            }
4662

4663
            $name = strtolower(trim($parts[0]));
4664
            $value = trim($parts[1]);
4665

4666
            switch ($name) {
4667
                case 'host':
4668
                case 'server':
4669
                case 'address':
4670
                    $this->host = $value;
4671
                    break;
4672

4673
                case 'port':
4674
                    $this->port = intval($value);
4675
                    break;
4676

4677
                case 'user':
4678
                case 'username':
4679
                case 'name':
4680
                case 'login':
4681
                    $this->username = $value;
4682
                    break;
4683

4684
                case 'pwd':
4685
                case 'password':
4686
                    $this->password = $value;
4687
                    break;
4688

4689
                case 'db':
4690
                case 'database':
4691
                case 'catalog':
4692
                    $this->database = $value;
4693
                    break;
4694

4695
                case 'arm':
4696
                case 'workstation':
4697
                    $this->workstation = $value;
4698
                    break;
4699

4700
                case 'debug':
4701
                    $this->debug = $value;
4702
                    break;
4703

4704
                default:
4705
                    throw new IrbisException("Unknown key $name");
4706
            }
4707
        }
4708
    } // function parseConnectionString
4709

4710
    /**
4711
     * Расформатирование таблицы.
4712
     *
4713
     * @param TableDefinition $definition Определение таблицы.
4714
     * @return bool|string Результат расформатирования
4715
     * либо признак сбоя операции.
4716
     */
4717
    public function printTable(TableDefinition $definition)
4718
    {
4719
        if (!$this->_checkConnection())
4720
            return false;
4721

4722
        $database = $definition->database ?: $this->database;
4723
        $query = new ClientQuery($this, '7');
4724
        $query->addAnsi($database)->newLine();
4725
        $query->addAnsi($definition->table)->newLine();
4726
        $query->addAnsi('')->newLine(); // вместо заголовков
4727
        $query->addAnsi($definition->mode)->newLine();
4728
        $query->addAnsi($definition->searchQuery)->newLine();
4729
        $query->add($definition->minMfn)->newLine();
4730
        $query->add($definition->maxMfn)->newLine();
4731
        $query->addUtf($definition->sequentialQuery)->newLine();
4732
        $query->addAnsi(''); // вместо перечня MFN
4733
        $response = $this->execute($query);
4734
        if (!$response)
4735
            return false;
4736

4737
        return $response->readRemainingUtfText();
4738
    } // function printTable
4739

4740
    /**
4741
     * Получение INI-файла с сервера.
4742
     *
4743
     * @param string $specification Спецификация файла.
4744
     * @return IniFile|null INI-файл
4745
     * либо null в качестве признака сбоя операции.
4746
     */
4747
    public function readIniFile($specification)
4748
    {
4749
        $lines = $this->readTextLines($specification);
4750
        if (!$lines)
4751
            return null;
4752

4753
        $result = new IniFile();
4754
        $result->parse($lines);
4755

4756
        return $result;
4757
    } // function readIniFile
4758

4759
    /**
4760
     * Чтение MNU-файла с сервера.
4761
     *
4762
     * @param string $specification Спецификация файла.
4763
     * @return bool|MenuFile MNU-файл
4764
     * либо признак сбоя операции.
4765
     */
4766
    public function readMenuFile($specification)
4767
    {
4768
        $lines = $this->readTextLines($specification);
4769
        if (!$lines)
4770
            return false;
4771

4772
        $result = new MenuFile();
4773
        $result->parse($lines);
4774

4775
        return $result;
4776
    } // function readMenuFile
4777

4778
    /**
4779
     * Чтение OPT-файла с сервера.
4780
     *
4781
     * @param string $specification Спецификация файла.
4782
     * @return bool|OptFile OPT-файл
4783
     * либо признак сбоя операции.
4784
     * @throws IrbisException Ошибка в структуре OPT-файла.
4785
     */
4786
    public function readOptFile($specification)
4787
    {
4788
        $lines = $this->readTextLines($specification);
4789
        if (!$lines)
4790
            return false;
4791

4792
        $result = new OptFile();
4793
        $result->parse($lines);
4794

4795
        return $result;
4796
    } // function readOptFile
4797

4798
    /**
4799
     * Чтение PAR-файла с сервера.
4800
     *
4801
     * @param string $specification Спецификация файла.
4802
     * @return bool|ParFile PAR-файл
4803
     * либо признак сбоя операции.
4804
     * @throws IrbisException Ошибка в структуре PAR-файла.
4805
     */
4806
    public function readParFile($specification)
4807
    {
4808
        $lines = $this->readTextLines($specification);
4809
        if (!$lines)
4810
            return false;
4811

4812
        $result = new ParFile();
4813
        $result->parse($lines);
4814

4815
        return $result;
4816
    } // function readParFile
4817

4818
    /**
4819
     * Считывание постингов из поискового индекса.
4820
     *
4821
     * @param PostingParameters $parameters Параметры постингов.
4822
     * @return array|bool Массив постингов
4823
     * либо признак сбоя операции.
4824
     */
4825
    public function readPostings(PostingParameters $parameters)
4826
    {
4827
        if (!$this->_checkConnection())
4828
            return false;
4829

4830
        $database = $parameters->database ?: $this->database;
4831
        $query = new ClientQuery($this, 'I');
4832
        $query->addAnsi($database)->newLine();
4833
        $query->add($parameters->numberOfPostings)->newLine();
4834
        $query->add($parameters->firstPosting)->newLine();
4835
        $query->addFormat($parameters->format);
4836
        if (!$parameters->listOfTerms) {
4837
            $query->addUtf($parameters->term)->newLine();
4838
        } else {
4839
            foreach ($parameters->listOfTerms as $term) {
4840
                $query->addUtf($term)->newLine();
4841
            }
4842
        }
4843

4844
        $response = $this->execute($query);
4845
        if (!$response || !$response->checkReturnCode(codes_for_read_terms()))
4846
            return false;
4847

4848
        $lines = $response->readRemainingUtfLines();
4849

4850
        return TermPosting::parse($lines);
4851
    } // function readPostings
4852

4853
    /**
4854
     * Чтение указанной записи в "сыром" виде.
4855
     *
4856
     * @param string $mfn MFN записи
4857
     * @return bool|RawRecord Запись
4858
     * либо признак сбоя операции.
4859
     */
4860
    public function readRawRecord($mfn)
4861
    {
4862
        if (!$this->_checkConnection())
4863
            return false;
4864

4865
        $query = new ClientQuery($this, 'C');
4866
        $query->addAnsi($this->database)->newLine();
4867
        $query->add($mfn)->newLine();
4868
        $response = $this->execute($query);
4869
        if (!$response || !$response->checkReturnCode(codes_for_read_record()))
4870
            return false;
4871

4872
        $result = new RawRecord();
4873
        $result->decode($response->readRemainingUtfLines());
4874
        $result->database = $this->database;
4875

4876
        return $result;
4877
    } // function readRawRecord
4878

4879
    /**
4880
     * Чтение указанной записи.
4881
     *
4882
     * @param int $mfn MFN записи
4883
     * @return bool|MarcRecord Запись
4884
     * либо признак сбоя операции.
4885
     */
4886
    public function readRecord($mfn)
4887
    {
4888
        if (!$this->_checkConnection())
4889
            return false;
4890

4891
        $query = new ClientQuery($this, 'C');
4892
        $query->addAnsi($this->database)->newLine();
4893
        $query->add($mfn)->newLine();
4894

4895
        // явно запрашиваем последнюю версию записи,
4896
        // т. к. этого требуют сервера от Батрака
4897
        $query->add(0)->newLine();
4898

4899
        $response = $this->execute($query);
4900
        if (!$response || !$response->checkReturnCode(codes_for_read_record()))
4901
            return false;
4902

4903
        $result = new MarcRecord();
4904
        $result->decode($response->readRemainingUtfLines());
4905
        $result->database = $this->database;
4906

4907
        return $result;
4908
    } // function readRecord
4909

4910
    /**
4911
     * Чтение указанной версии записи.
4912
     *
4913
     * @param int $mfn MFN записи
4914
     * @param int $version Версия записи
4915
     * @return bool|MarcRecord Запись
4916
     * либо признак сбоя операции.
4917
     */
4918
    public function readRecordVersion($mfn, $version)
4919
    {
4920
        if (!$this->_checkConnection())
4921
            return false;
4922

4923
        $query = new ClientQuery($this, 'C');
4924
        $query->addAnsi($this->database)->newLine();
4925
        $query->add($mfn)->newLine();
4926
        $query->add($version);
4927

4928
        $response = $this->execute($query);
4929
        if (!$response || !$response->checkReturnCode(codes_for_read_record()))
4930
            return false;
4931

4932
        $result = new MarcRecord();
4933
        $result->decode($response->readRemainingUtfLines());
4934
        $result->database = $this->database;
4935

4936
        return $result;
4937
    } // function readRecordVersion
4938

4939
    /**
4940
     * Чтение с сервера нескольких записей.
4941
     *
4942
     * @param array $mfnList Массив MFN.
4943
     * @return array Массив записей
4944
     * (пустой массив как признак сбоя операции).
4945
     */
4946
    public function readRecords(array $mfnList)
4947
    {
4948
        if (!$this->_checkConnection())
4949
            return array();
4950

4951
        if (!$mfnList) {
4952
            return array();
4953
        }
4954

4955
        if (count($mfnList) == 1) {
4956
            $result = array();
4957
            $record = $this->readRecord($mfnList[0]);
4958
            if ($record) {
4959
                $result[] = $record;
4960
            }
4961
            return $result;
4962
        }
4963

4964
        $query = new ClientQuery($this, 'G');
4965
        $query->addAnsi($this->database)->newLine();
4966
        $query->addAnsi(ALL_FORMAT)->newLine();
4967
        $query->add(count($mfnList))->newLine();
4968
        foreach ($mfnList as $mfn) {
4969
            $query->add($mfn)->newLine();
4970
        }
4971
        $response = $this->execute($query);
4972
        if (!$response || !$response->checkReturnCode())
4973
            return array();
4974

4975
        $lines = $response->readRemainingUtfLines();
4976
        $result = array();
4977
        foreach ($lines as $line) {
4978
            $parts = explode('#', $line, 2);
4979
            if (count($parts) > 1) {
4980
                $parts = explode("\x1F", $parts[1]);
4981
                $parts = array_slice($parts, 1);
4982
                $record = new MarcRecord();
4983
                $record->decode($parts);
4984
                $record->database = $this->database;
4985
                $result[] = $record;
4986
            }
4987
        }
4988

4989
        return $result;
4990
    } // function readRecords
4991

4992
    /**
4993
     * Загрузка сценариев поиска с сервера.
4994
     *
4995
     * @param string $specification Спецификация.
4996
     * @return array|bool Массив сценариев
4997
     * либо признак сбоя операции.
4998
     */
4999
    public function readSearchScenario($specification)
5000
    {
5001
        if (!$this->_checkConnection())
5002
            return false;
5003

5004
        $iniFile = $this->readIniFile($specification);
5005
        if (!$iniFile)
5006
            return false;
5007

5008
        return SearchScenario::parse($iniFile);
5009
    } // function readSearchScenario
5010

5011
    /**
5012
     * Простое получение терминов поискового словаря.
5013
     *
5014
     * @param string $startTerm Начальный термин.
5015
     * @param int $numberOfTerms Необходимое количество терминов.
5016
     * @return array|bool Массив терминов
5017
     * либо призак сбоя операции.
5018
     */
5019
    public function readTerms($startTerm, $numberOfTerms = 100)
5020
    {
5021
        $parameters = new TermParameters();
5022
        $parameters->startTerm = $startTerm;
5023
        $parameters->numberOfTerms = $numberOfTerms;
5024

5025
        return $this->readTermsEx($parameters);
5026
    } // function readTerms
5027

5028
    /**
5029
     * Получение терминов поискового словаря.
5030
     *
5031
     * @param TermParameters $parameters Параметры терминов.
5032
     * @return array|bool Массив терминов
5033
     * либо признак сбоя операции.
5034
     */
5035
    public function readTermsEx(TermParameters $parameters)
5036
    {
5037
        if (!$this->_checkConnection())
5038
            return false;
5039

5040
        $command = $parameters->reverseOrder ? 'P' : 'H';
5041
        $database = $parameters->database ?: $this->database;
5042
        $query = new ClientQuery($this, $command);
5043
        $query->addAnsi($database)->newLine();
5044
        $query->addUtf($parameters->startTerm)->newLine();
5045
        $query->add($parameters->numberOfTerms)->newLine();
5046
        $query->addFormat($parameters->format);
5047
        $response = $this->execute($query);
5048
        if (!$response || !$response->checkReturnCode(codes_for_read_terms()))
5049
            return false;
5050

5051
        $lines = $response->readRemainingUtfLines();
5052

5053
        return TermInfo::parse($lines);
5054
    } // function readTermsEx
5055

5056
    /**
5057
     * Получение текстового файла с сервера.
5058
     *
5059
     * @param string $specification Спецификация файла.
5060
     * @return bool|string Текст файла
5061
     * либо признак сбоя операции.
5062
     */
5063
    public function readTextFile($specification)
5064
    {
5065
        if (!$this->_checkConnection())
5066
            return false;
5067

5068
        $query = new ClientQuery($this, 'L');
5069
        $query->addAnsi($specification)->newLine();
5070
        $response = $this->execute($query);
5071
        if (!$response)
5072
            return false;
5073

5074
        $result = $response->readAnsi();
5075
        $result = irbis_to_dos($result);
5076

5077
        return $result;
5078
    } // function readTextFile
5079

5080
    /**
5081
     * Получение текстового файла в виде массива строк.
5082
     *
5083
     * @param string $specification Спецификация файла.
5084
     * @return array Массив строк
5085
     * (пустой массив как признак сбоя операции).
5086
     */
5087
    public function readTextLines($specification)
5088
    {
5089
        if (!$this->_checkConnection())
5090
            return array();
5091

5092
        $query = new ClientQuery($this, 'L');
5093
        $query->addAnsi($specification)->newLine();
5094
        $response = $this->execute($query);
5095
        if (!$response)
5096
            return array();
5097

5098
        $result = $response->readAnsi();
5099
        $result = irbis_to_lines($result);
5100

5101
        return $result;
5102
    } // function readTextLines
5103

5104
    /**
5105
     * Чтение TRE-файла с сервера.
5106
     *
5107
     * @param string $specification Спецификация файла.
5108
     * @return bool|TreeFile TRE-файл
5109
     * либо признак сбоя операции.
5110
     * @throws IrbisException Ошибка в структуре TRE-файла.
5111
     */
5112
    public function readTreeFile($specification)
5113
    {
5114
        $lines = $this->readTextLines($specification);
5115
        if (!$lines)
5116
            return false;
5117

5118
        $result = new TreeFile();
5119
        $result->parse($lines);
5120

5121
        return $result;
5122
    } // function readTreeFile
5123

5124
    /**
5125
     * Пересоздание словаря для указанной базы данных.
5126
     *
5127
     * @param string $database База данных.
5128
     * @return bool Признак успешности операции.
5129
     */
5130
    public function reloadDictionary($database)
5131
    {
5132
        if (!$this->_checkConnection())
5133
            return false;
5134

5135
        $query = new ClientQuery($this, 'Y');
5136
        $query->addAnsi($database)->newLine();
5137
        if (!$this->execute($query))
5138
            return false;
5139

5140
        return true;
5141
    } // function reloadDictionary
5142

5143
    /**
5144
     * Пересоздание мастер-файла для указанной базы данных.
5145
     *
5146
     * @param string $database База данных.
5147
     * @return bool Признак успешности операции.
5148
     */
5149
    public function reloadMasterFile($database)
5150
    {
5151
        if (!$this->_checkConnection())
5152
            return false;
5153

5154
        $query = new ClientQuery($this, 'X');
5155
        $query->addAnsi($database)->newLine();
5156
        if (!$this->execute($query))
5157
            return false;
5158

5159
        return true;
5160
    } // function reloadMasterFile
5161

5162
    /**
5163
     * Получение INI-файла с сервера.
5164
     *
5165
     * @param string $specification Спецификация файла.
5166
     * @return IniFile Полученный INI-файл.
5167
     * @throws IrbisException Файл не найден.
5168
     */
5169
    public function requireIniFile($specification)
5170
    {
5171
        $lines = $this->readTextLines($specification);
5172
        if (!$lines)
5173
            throw new IrbisException("File not found: " . $specification);
5174

5175
        $result = new IniFile();
5176
        $result->parse($lines);
5177

5178
        return $result;
5179
    } // function requireIniFile
5180

5181
    /**
5182
     * Получение MNU-файла с сервера.
5183
     *
5184
     * @param string $specification Спецификация файла.
5185
     * @return MenuFile Полученный MNU-файл.
5186
     * @throws IrbisException Файл не найден.
5187
     */
5188
    public function requireMenuFile($specification)
5189
    {
5190
        $lines = $this->readTextLines($specification);
5191
        if (!$lines)
5192
            throw new IrbisException("File not found: " . $specification);
5193

5194
        $result = new MenuFile();
5195
        $result->parse($lines);
5196

5197
        return $result;
5198
    } // function requireMenuFile
5199

5200
    /**
5201
     * Получение OPT-файла с сервера.
5202
     *
5203
     * @param string $specification Спецификация файла.
5204
     * @return OptFile Полученный OPT-файл.
5205
     * @throws IrbisException Файл не найден.
5206
     */
5207
    public function requireOptFile($specification)
5208
    {
5209
        $lines = $this->readTextLines($specification);
5210
        if (!$lines)
5211
            throw new IrbisException("File not found: " . $specification);
5212

5213
        $result = new OptFile();
5214
        $result->parse($lines);
5215

5216
        return $result;
5217
    } // function requireOptFile
5218

5219
    /**
5220
     * Получение PAR-файла с сервера.
5221
     *
5222
     * @param string $specification Спецификация файла.
5223
     * @return ParFile Полученный PAR-файл.
5224
     * @throws IrbisException Файл не найден.
5225
     */
5226
    public function requireParFile($specification)
5227
    {
5228
        $lines = $this->readTextLines($specification);
5229
        if (!$lines)
5230
            throw new IrbisException("File not found: " . $specification);
5231

5232
        $result = new ParFile();
5233
        $result->parse($lines);
5234

5235
        return $result;
5236
    } // function requireParFile
5237

5238
    /**
5239
     * Получение текстового файла с сервера.
5240
     *
5241
     * @param string $specification Спецификация файла.
5242
     * @return string Текст полученного файла.
5243
     * @throws IrbisException Файл не найден.
5244
     */
5245
    public function requireTextFile($specification)
5246
    {
5247
        $result = $this->readTextFile($specification);
5248
        if (!$result || is_null_or_empty($result))
5249
            throw new IrbisException("File not found: " . $specification);
5250

5251
        return $result;
5252
    } // function requireTextFile
5253

5254
    /**
5255
     * Получение TRE-файла с сервера.
5256
     *
5257
     * @param string $specification Спецификация файла.
5258
     * @return TreeFile Полученный TRE-файл.
5259
     * @throws IrbisException Файл не найден.
5260
     */
5261
    public function requireTreeFile($specification)
5262
    {
5263
        $lines = $this->readTextLines($specification);
5264
        if (!$lines)
5265
            throw new IrbisException("File not found: " . $specification);
5266

5267
        $result = new TreeFile();
5268
        $result->parse($lines);
5269

5270
        return $result;
5271
    } // function requireTreeFile
5272

5273
    /**
5274
     * Перезапуск сервера (без утери подключенных клиентов).
5275
     *
5276
     * @return bool Признак успешности операции.
5277
     */
5278
    public function restartServer()
5279
    {
5280
        if (!$this->_checkConnection())
5281
            return false;
5282

5283
        $query = new ClientQuery($this, '+8');
5284
        if (!$this->execute($query))
5285
            return false;
5286

5287
        return true;
5288
    } // function restartServer
5289

5290
    /**
5291
     * Простой поиск записей (не более 32 тыс. записей).
5292
     *
5293
     * @param string $expression Выражение для поиска по словарю.
5294
     * @return array|bool Массив найденных MFN
5295
     * либо признак сбоя операции.
5296
     */
5297
    public function search($expression)
5298
    {
5299
        $parameters = new SearchParameters();
5300
        $parameters->expression = $expression;
5301
        $found = $this->searchEx($parameters);
5302

5303
        return FoundLine::toMfn($found);
5304
    } // function search
5305

5306
    /**
5307
     * Поиск всех записей (даже если их окажется больше 32 тыс.).
5308
     *
5309
     * @param string $expression Выражение для поиска по словарю.
5310
     * @return array Массив MFN найденных записей
5311
     * (возможно, пустой).
5312
     */
5313
    public function searchAll($expression)
5314
    {
5315
        $result = array();
5316
        if (!$this->_checkConnection())
5317
            return $result;
5318

5319
        $firstRecord = 1;
5320
        $totalCount = 0;
5321

5322
        while (true) {
5323
            $query = new ClientQuery($this, 'K');
5324
            $query->addAnsi($this->database)->newLine();
5325
            $query->addUtf((string)$expression)->newLine();
5326
            $query->add(0)->newLine();
5327
            $query->add($firstRecord)->newLine();
5328
            $response = $this->execute($query);
5329
            if (!$response || !$response->checkReturnCode())
5330
                return $result; // TODO реагировать правильно
5331

5332
            if ($firstRecord == 1) {
5333
                $totalCount = $response->readInteger();
5334
                if (!$totalCount) {
5335
                    break;
5336
                }
5337
            } else {
5338
                $response->readInteger(); // Eat the line
5339
            }
5340

5341
            $lines = $response->readRemainingUtfLines();
5342
            $found = FoundLine::parseMfn($lines);
5343
            if (!$found)
5344
                break;
5345

5346
            $result = $result + $found;
5347
            $firstRecord += count($found);
5348
            if ($firstRecord >= $totalCount)
5349
                break;
5350
        } // while
5351

5352
        return $result;
5353
    } // function searchAll
5354

5355
    /**
5356
     * Определение количества записей,
5357
     * соответствующих поисковому выражению.
5358
     *
5359
     * @param string $expression Поисковое выражение.
5360
     * @return int Количество соответствующих записей.
5361
     */
5362
    public function searchCount($expression)
5363
    {
5364
        if (!$this->_checkConnection())
5365
            return 0;
5366

5367
        $query = new ClientQuery($this, 'K');
5368
        $query->addAnsi($this->database)->newLine();
5369
        $query->addUtf((string)$expression)->newLine();
5370
        $query->add(0)->newLine();
5371
        $query->add(0);
5372
        $response = $this->execute($query);
5373
        if (!$response || !$response->checkReturnCode())
5374
            return false;
5375

5376
        return $response->readInteger(); // Число найденных записей
5377
    } // function searchCount
5378

5379
    /**
5380
     * Расширенный поиск записей.
5381
     *
5382
     * @param SearchParameters $parameters Параметры поиска.
5383
     * @return array|bool Массив найденных записей
5384
     * либо признак сбоя операции.
5385
     */
5386
    public function searchEx(SearchParameters $parameters)
5387
    {
5388
        if (!$this->_checkConnection())
5389
            return false;
5390

5391
        $database = $parameters->database ?: $this->database;
5392
        $query = new ClientQuery($this, 'K');
5393
        $query->addAnsi($database)->newLine();
5394
        $query->addUtf((string)($parameters->expression))->newLine();
5395
        $query->add($parameters->numberOfRecords)->newLine();
5396
        $query->add($parameters->firstRecord)->newLine();
5397
        $query->addFormat($parameters->format);
5398
        $query->add($parameters->minMfn)->newLine();
5399
        $query->add($parameters->maxMfn)->newLine();
5400
        $query->addAnsi($parameters->sequential)->newLine();
5401
        $response = $this->execute($query);
5402
        if (!$response || !$response->checkReturnCode())
5403
            return false;
5404

5405
        $response->readInteger(); // Число найденных записей.
5406
        $lines = $response->readRemainingUtfLines();
5407
        $result = FoundLine::parse($lines);
5408

5409
        return $result;
5410
    } // function searchEx
5411

5412
    /**
5413
     * Поиск записей с их одновременным считыванием.
5414
     *
5415
     * @param string $expression Поисковое выражение.
5416
     * @param int $limit Максимальное количество загружаемых записей.
5417
     * @return array Массив полученных записей
5418
     * (возможно, пустой).
5419
     */
5420
    public function searchRead($expression, $limit = 0)
5421
    {
5422
        $parameters = new SearchParameters();
5423
        $parameters->expression = $expression;
5424
        $parameters->format = ALL_FORMAT;
5425
        $parameters->numberOfRecords = $limit;
5426
        $found = $this->searchEx($parameters);
5427
        if (!$found)
5428
            return array();
5429

5430
        $result = array();
5431
        foreach ($found as $item) {
5432
            $lines = explode("\x1F", $item->description);
5433
            $lines = array_slice($lines, 1);
5434
            $record = new MarcRecord();
5435
            $record->decode($lines);
5436
            $record->database = $this->database;
5437
            $result[] = $record;
5438
        }
5439

5440
        return $result;
5441
    } // function searchRead
5442

5443
    /**
5444
     * Поиск и считывание одной записи, соответствующей выражению.
5445
     * Если таких записей больше одной, то будет считана любая из них.
5446
     * Если таких записей нет, будет возвращен null.
5447
     *
5448
     * @param string $expression Поисковое выражение.
5449
     * @return MarcRecord|null Полученная запись либо null,
5450
     * если запись не найдена.
5451
     */
5452
    public function searchSingleRecord($expression)
5453
    {
5454
        $found = $this->searchRead($expression, 1);
5455
        if (count($found))
5456
            return $found[0];
5457

5458
        return null;
5459
    } // function searchSingleRecord
5460

5461
    /**
5462
     * Бросает исключение, если произошла ошибка
5463
     * при выполнении последней операции.
5464
     * @throws IrbisException Обнаружена ошибка,
5465
     * выброшено исключение.
5466
     */
5467
    public function throwOnError()
5468
    {
5469
        if ($this->lastError < 0)
5470
            throw new IrbisException($this->lastError);
5471
    } // function throwOnError
5472

5473
    /**
5474
     * Выдача строки подключения для текущего соединения.
5475
     * Соединение не обязательно должно быть установлено.
5476
     *
5477
     * @return string Строка подключения для текушего соединения
5478
     * (не обязательно активного).
5479
     */
5480
    public function toConnectionString()
5481
    {
5482
        return 'host=' . $this->host
5483
            . ';port=' . $this->port
5484
            . ';username=' . $this->username
5485
            . ';password=' . $this->password
5486
            . ';database=' . $this->database
5487
            . ';arm=' . $this->workstation . ';';
5488
    } // function toConnectionString
5489

5490
    /**
5491
     * Опустошение указанной базы данных.
5492
     *
5493
     * @param string $database База данных.
5494
     * @return bool Признак успешности операции.
5495
     */
5496
    public function truncateDatabase($database)
5497
    {
5498
        if (!$this->_checkConnection()) {
5499
            return false;
5500
        }
5501

5502
        $query = new ClientQuery($this, 'S');
5503
        $query->addAnsi($database)->newLine();
5504
        if (!$this->execute($query))
5505
            return false;
5506

5507
        return true;
5508
    } // function truncateDatabase
5509

5510
    /**
5511
     * Восстановление записи по её MFN.
5512
     *
5513
     * @param int $mfn MFN восстанавливаемой записи.
5514
     * @return bool|MarcRecord Восстановленная запись
5515
     * либо признак сбоя операции.
5516
     */
5517
    public function undeleteRecord($mfn)
5518
    {
5519
        $record = $this->readRecord($mfn);
5520
        if (!$record)
5521
            return $record;
5522

5523
        if ($record->isDeleted()) {
5524
            $record->status &= ~LOGICALLY_DELETED;
5525
            if (!$this->writeRecord($record))
5526
                return false;
5527
        }
5528

5529
        return $record;
5530
    } // function undeleteRecord
5531

5532
    /**
5533
     * Разблокирование указанной базы данных.
5534
     *
5535
     * @param string $database База данных.
5536
     * @return bool Признак успешности операции.
5537
     */
5538
    public function unlockDatabase($database)
5539
    {
5540
        if (!$this->_checkConnection())
5541
            return false;
5542

5543
        $query = new ClientQuery($this, 'U');
5544
        $query->addAnsi($database)->newLine();
5545
        if (!$this->execute($query))
5546
            return false;
5547

5548
        return true;
5549
    } // function unlockDatabase
5550

5551
    /**
5552
     * Разблокирование записей.
5553
     *
5554
     * @param string $database База данных.
5555
     * @param array $mfnList Массив MFN.
5556
     * @return bool Признак успешности операции.
5557
     */
5558
    public function unlockRecords($database, array $mfnList)
5559
    {
5560
        if (!$this->_checkConnection())
5561
            return false;
5562

5563
        if (count($mfnList) == 0)
5564
            return true;
5565

5566
        $database = $database ?: $this->database;
5567
        $query = new ClientQuery($this, 'Q');
5568
        $query->addAnsi($database)->newLine();
5569
        foreach ($mfnList as $mfn)
5570
            $query->add($mfn)->newLine();
5571

5572
        if (!$this->execute($query))
5573
            return false;
5574

5575
        return true;
5576
    } // function unlockRecords
5577

5578
    /**
5579
     * Обновление строк серверного INI-файла
5580
     * для текущего пользователя.
5581
     *
5582
     * @param array $lines Изменённые строки.
5583
     * @return bool Признак успешности операции.
5584
     */
5585
    public function updateIniFile(array $lines)
5586
    {
5587
        if (!$this->_checkConnection())
5588
            return false;
5589

5590
        if (!$lines)
5591
            return true;
5592

5593
        $query = new ClientQuery($this, '8');
5594
        foreach ($lines as $line)
5595
            $query->addAnsi($line)->newLine();
5596

5597
        if (!$this->execute($query))
5598
            return false;
5599

5600
        return true;
5601
    } // function updateIniFile
5602

5603
    /**
5604
     * Обновление списка пользователей на сервере.
5605
     *
5606
     * @param array $users Список пользователей.
5607
     * @return bool Признак успешности операции.
5608
     */
5609
    public function updateUserList(array $users)
5610
    {
5611
        if (!$this->_checkConnection())
5612
            return false;
5613

5614
        $query = new ClientQuery($this, '+7');
5615
        foreach ($users as $user)
5616
            $query->addAnsi($user->encode())->newLine();
5617
        if (!$this->execute($query))
5618
            return false;
5619

5620
        return true;
5621
    } // function updateUserList
5622

5623
    /**
5624
     * Сохранение на сервере "сырой" записи.
5625
     *
5626
     * @param RawRecord $record Запись для сохранения.
5627
     * @return bool|int Новый максимальный MFN в базе данных
5628
     * либо признак сбоя операции.
5629
     */
5630
    public function writeRawRecord(RawRecord $record)
5631
    {
5632
        if (!$this->_checkConnection())
5633
            return false;
5634

5635
        $database = $record->database ?: $this->database;
5636
        $query = new ClientQuery($this, 'D');
5637
        $query->addAnsi($database)->newLine();
5638
        $query->add(0)->newLine();
5639
        $query->add(1)->newLine();
5640
        $query->addUtf($record->encode())->newLine();
5641
        $response = $this->execute($query);
5642
        if (!$response || !$response->checkReturnCode())
5643
            return false;
5644

5645
        return $response->returnCode;
5646
    } // function writeRawRecord
5647

5648
    /**
5649
     * Сохранение записи на сервере.
5650
     *
5651
     * @param MarcRecord $record Запись для сохранения (новая или ранее считанная).
5652
     * @param int $lockFlag Оставить запись заблокированной?
5653
     * @param int $actualize Актуализировать словарь?
5654
     * @param bool $dontParse Не разбирать результат.
5655
     * @return bool|int Новый максимальный MFN в базе данных
5656
     * либо признак сбоя операции.
5657
     */
5658
    public function writeRecord(MarcRecord $record, $lockFlag = 0, $actualize = 1,
5659
                                $dontParse = false)
5660
    {
5661
        if (!$this->_checkConnection())
5662
            return false;
5663

5664
        $database = $record->database ?: $this->database;
5665
        $query = new ClientQuery($this, 'D');
5666
        $query->addAnsi($database)->newLine();
5667
        $query->add($lockFlag)->newLine();
5668
        $query->add($actualize)->newLine();
5669
        $query->addUtf($record->encode())->newLine();
5670
        $response = $this->execute($query);
5671
        if (!$response || !$response->checkReturnCode())
5672
            return false;
5673

5674
        if (!$dontParse) {
5675
            $record->fields = array();
5676
            $temp = $response->readRemainingUtfLines();
5677
            if (count($temp) > 1) {
5678
                $lines = array($temp[0]);
5679
                $lines = array_merge($lines, explode(SHORT_DELIMITER, $temp[1]));
5680
                $record->decode($lines);
5681
                $record->database = $database;
5682
            }
5683
        }
5684

5685
        return $response->returnCode;
5686
    } // function writeRecord
5687

5688
    /**
5689
     * Сохранение нескольких записей на сервере (могут относиться к разным базам).
5690
     *
5691
     * @param array $records Записи.
5692
     * @param int $lockFlag
5693
     * @param int $actualize
5694
     * @param bool $dontParse
5695
     * @return bool Признак успешности операции.
5696
     */
5697
    public function writeRecords(array $records, $lockFlag = 0, $actualize = 1,
5698
                                 $dontParse = false)
5699
    {
5700
        if (!$this->_checkConnection())
5701
            return false;
5702

5703
        if (!$records)
5704
            return true;
5705

5706
        if (count($records) == 1) {
5707
            $this->writeRecord($records[0]);
5708

5709
            return true;
5710
        }
5711

5712
        $query = new ClientQuery($this, '6');
5713
        $query->add($lockFlag)->newLine();
5714
        $query->add($actualize)->newLine();
5715
        foreach ($records as $record) {
5716
            $database = $record->database ?: $this->database;
5717
            $query->addUtf($database . IRBIS_DELIMITER . $record->encode())->newLine();
5718
        }
5719

5720
        $response = $this->execute($query);
5721
        if (!$response)
5722
            return false;
5723

5724
        $response->getReturnCode();
5725

5726
        if (!$dontParse) {
5727
            $lines = $response->readRemainingUtfLines();
5728
            $length = count($records);
5729
            for ($i = 0; $i < $length; $i++) {
5730
                $text = $lines[$i];
5731
                if (is_null_or_empty($text)) {
5732
                    continue;
5733
                }
5734

5735
                $record = $records[$i];
5736
                $record->clear();
5737
                $record->database = $record->database ?: $this->database;
5738
                $recordLines = irbis_to_lines($text);
5739
                $record->parse($recordLines);
5740
            }
5741
        }
5742

5743
        return true;
5744
    } // function writeRecords
5745

5746
    /**
5747
     * Сохранение текстового файла на сервере.
5748
     *
5749
     * @param string $specification Спецификация файла
5750
     * (включая текст файла).
5751
     * @return bool Признак успешности операции.
5752
     */
5753
    public function writeTextFile($specification)
5754
    {
5755
        if (!$this->_checkConnection())
5756
            return false;
5757

5758
        $query = new ClientQuery($this, 'L');
5759
        $query->addAnsi($specification);
5760
        if (!$this->execute($query))
5761
            return false;
5762

5763
        return true;
5764
    } // function writeTextFile
5765

5766
} // class Connection
5767

5768
final class UI
5769
{
5770

5771
    /**
5772
     * @var Connection Активное подключение к серверу.
5773
     */
5774
    public $connection;
5775

5776
    /**
5777
     * Конструктор.
5778
     *
5779
     * @param Connection $connection Активное (!) подключение к серверу.
5780
     * @throws IrbisException
5781
     */
5782
    public function __construct(Connection $connection)
5783
    {
5784
        if (!$connection->isConnected())
5785
            throw new IrbisException();
5786

5787
        $this->connection = $connection;
5788
    }
5789

5790
    /**
5791
     * Вывод выпадающего списка баз данных.
5792
     *
5793
     * @param string $class
5794
     * @param string $selected
5795
     * @throws IrbisException
5796
     */
5797
    public function listDatabases($class = '', $selected = '')
5798
    {
5799
        $dbnnamecat = $this->connection->iniFile->getValue('Main', 'DBNNAMECAT', 'dbnam3.mnu');
5800
        $databases = $this->connection->listDatabases('1..' . $dbnnamecat);
5801
        if (!$databases)
5802
            throw new IrbisException();
5803

5804
        $classText = '';
5805
        if ($class) {
5806
            $classText = "class='{$class}'";
5807
        }
5808
        echo "<select name='catalogBox' $classText>" . PHP_EOL;
5809
        foreach ($databases as $database) {
5810
            $selectedText = '';
5811
            if (same_string($database->name, $selected)) {
5812
                $selectedText = 'selected';
5813
            }
5814
            echo "<option value='{$database->name}' $selectedText>{$database->description}</option>" . PHP_EOL;
5815
        }
5816
        echo "</select>" . PHP_EOL;
5817
    } // function listDatabases
5818

5819
    /**
5820
     * Получение сценариев поиска.
5821
     *
5822
     * @return array
5823
     * @throws IrbisException
5824
     */
5825
    public function getSearchScenario()
5826
    {
5827
        // TODO доделать
5828
        $ini = $this->connection->iniFile;
5829
        $fileName = $ini->getValue("MAIN", 'SearchIni'); // ???
5830
        $section = $ini->findSection("SEARCH");
5831
        if (!$section) {
5832
            throw new IrbisException();
5833
        }
5834
        $result = SearchScenario::parse($ini);
5835

5836
        return $result;
5837
    } // function getSearchScenario
5838

5839
    /**
5840
     * Вывод выпадающего списка сценариев поиска.
5841
     *
5842
     * @param $name
5843
     * @param $scenarios
5844
     * @param string $class
5845
     * @param int $selectedIndex
5846
     * @param string $selectedValue
5847
     */
5848
    public function listSearchScenario($name, $scenarios, $class = '', $selectedIndex = -1,
5849
                                       $selectedValue = '')
5850
    {
5851
        echo "<select name='$name'>" . PHP_EOL;
5852
        $classText = '';
5853
        if ($class) {
5854
            $classText = " class='$class'";
5855
        }
5856
        $index = 0;
5857
        foreach ($scenarios as $scenario) {
5858
            $selectedText = '';
5859
            if ($selectedValue) {
5860
                if (same_string($scenario->prefix, $selectedValue)) {
5861
                    $selectedText = 'selected';
5862
                }
5863
            } else if ($index == $selectedIndex) {
5864
                $selectedText = 'selected';
5865
            }
5866
            echo "<option value='{$scenario->prefix}' $selectedText $classText>{$scenario->name}</option>" . PHP_EOL;
5867
            $index++;
5868
        }
5869
        echo "</select>" . PHP_EOL;
5870
    } // function listSearchScenario
5871

5872
} // class IrbisUI
5873

5874
/**
5875
 * Запись в XRF-файле. Содержит информацию о смещении записи
5876
 * и ее статус.
5877
 */
5878
final class XrfRecord
5879
{
5880
    /**
5881
     * @var int Младшая часть смещения.
5882
     */
5883
    public $low;
5884

5885
    /**
5886
     * @var int Старшая часть смещения.
5887
     */
5888
    public $high;
5889

5890
    /**
5891
     * @var int Статус записи.
5892
     */
5893
    public $status;
5894

5895
    /**
5896
     * Запись (логически или физически) удалена?
5897
     * @return bool
5898
     */
5899
    public function isDeleted()
5900
    {
5901
        return ($this->status & 3) != 0;
5902
    }
5903

5904
    /**
5905
     * Смещение записи.
5906
     * @return int
5907
     */
5908
    public function offset()
5909
    {
5910
        return ($this->high << 32) + $this->low;
5911
    }
5912
} // class XrfRecord
5913

5914
/**
5915
 * XRF-файл.
5916
 */
5917
final class XrfFile
5918
{
5919
    // Файл.
5920
    private $file;
5921

5922
    /**
5923
     * XrfFile constructor.
5924
     * @param $filename
5925
     * @throws IrbisException
5926
     */
5927
    public function __construct($filename)
5928
    {
5929
        $this->file = fopen($filename, 'rb');
5930
        if (!$this->file) {
5931
            throw new IrbisException("Can't open " . $filename);
5932
        }
5933
    } // function __construct
5934

5935
    public function __destruct()
5936
    {
5937
        if ($this->file)
5938
            fclose($this->file);
5939
    } // function __destruct
5940

5941
    /**
5942
     * Считывание записи по MFN.
5943
     * @param int $mfn MFN записи.
5944
     * @return XrfRecord
5945
     */
5946
    public function read($mfn)
5947
    {
5948
        $offset = ($mfn - 1) * 12;
5949
        fseek($this->file, $offset, SEEK_SET);
5950
        $content = fread($this->file, 12);
5951
        $result = new XrfRecord();
5952
        $result->low = unpack("N", $content, 0);
5953
        $result->high = unpack("N", $content, 4);
5954
        $result->status = unpack("N", $content, 8);
5955
        return $result;
5956
    } // function read
5957

5958
} // class XrfFile
5959

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.