SIP2Response   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 196
Duplicated Lines 0 %

Test Coverage

Coverage 97.3%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 15
eloc 148
c 3
b 0
f 0
dl 0
loc 196
ccs 36
cts 37
cp 0.973
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 14 4
A checkCRC() 0 14 3
B parseVariableData() 0 45 8
1
<?php
2
3
namespace lordelph\SIP2\Response;
4
5
use lordelph\SIP2\Exception\LogicException;
6
use lordelph\SIP2\Exception\RuntimeException;
7
use lordelph\SIP2\SIP2Client;
8
use lordelph\SIP2\SIP2Message;
9
10
/**
11
 * Class SIP2Response provides a base class for responses and a factory method for constructing them
12
 *
13
 * Derived classes declare the variable data they expect to receive, and provide a parser for the 'fixed'
14
 * fields
15
 *
16
 * @licence    https://opensource.org/licenses/MIT
17
 * @copyright  John Wohlers <[email protected]>
18
 * @copyright  Paul Dixon <[email protected]>
19
 */
20
abstract class SIP2Response extends SIP2Message
21
{
22
    const AA_PATRON_IDENTIFIER = 'AA';
23
    const AB_ITEM_IDENTIFIER = 'AB';
24
    const AE_PERSONAL_NAME = 'AE';
25
    const AF_SCREEN_MESSAGE = 'AF';
26
    const AG_PRINT_LINE = 'AG';
27
    const AH_DUE_DATE = 'AH';
28
    const AJ_TITLE_IDENTIFIER = 'AJ';
29
    const AM_LIBRARY_NAME='AM';
30
    const AN_TERMINAL_LOCATION='AN';
31
    const AO_INSTITUTION_ID = 'AO';
32
    const AP_CURRENT_LOCATION = 'AP';
33
    const AQ_PERMANENT_LOCATION='AQ';
34
    const AS_HOLD_ITEMS = 'AS';
35
    const AT_OVERDUE_ITEMS = 'AT';
36
    const AU_CHARGED_ITEMS = 'AU';
37
    const AV_FINE_ITEMS = 'AV';
38
    const AY_SEQUENCE_NUMBER = 'AY';
39
    const BD_HOME_ADDRESS = 'BD';
40
    const BE_EMAIL_ADDRESS = 'BE';
41
    const BF_HOME_PHONE_NUMBER = 'BF';
42
    const BG_OWNER = 'BG';
43
    const BH_CURRENCY_TYPE = 'BH';
44
    const BK_TRANSACTION_ID= 'BK';
45
    const BL_VALID_PATRON = 'BL';
46
    const BM_RENEWED_ITEMS = 'BM';
47
    const BN_UNRENEWED_ITEMS = 'BN';
48
    const BR_QUEUE_POSITION = 'BR';
49
    const BS_PICKUP_LOCATION = 'BS';
50
    const BT_FEE_TYPE = 'BT';
51
    const BU_RECALL_ITEMS = 'BU';
52
    const BV_FEE_AMOUNT = 'BV';
53
    const BW_EXPIRATION_DATE = 'BW';
54
    const BX_SUPPORTED_MESSAGES='BX';
55
    const BZ_HOLD_ITEMS_LIMIT = 'BZ';
56
    const CA_OVERDUE_ITEMS_LIMIT = 'CA';
57
    const CB_CHARGED_ITEMS_LIMIT = 'CB';
58
    const CC_FEE_LIMIT = 'CC';
59
    const CD_UNAVAILABLE_HOLD_ITEMS = 'CD';
60
    const CF_HOLD_QUEUE_LENGTH = 'CF';
61
    const CH_ITEM_PROPERTIES= 'CH';
62
    const CI_SECURITY_INHIBIT = 'CI';
63
    const CJ_RECALL_DATE = 'CJ';
64
    const CK_MEDIA_TYPE= 'CK';
65
    const CL_SORT_BIN='CL';
66
    const CM_HOLD_PICKUP_DATE = 'CM';
67
    const CQ_VALID_PATRON_PASSWORD = 'CQ';
68
69
    /** @var array maps SIP2 numeric response codes onto response classes */
70
    private static $mapResponseToClass = [
71
        '10' => CheckInResponse::class,
72
        '12' => CheckOutResponse::class,
73
        '16' => HoldResponse::class,
74
        '18' => ItemInformationResponse::class,
75
        '20' => ItemStatusUpdateResponse::class,
76
        '24' => PatronStatusResponse::class,
77
        '26' => PatronEnableResponse::class,
78
        '30' => RenewResponse::class,
79
        '36' => EndSessionResponse::class,
80
        '38' => FeePaidResponse::class,
81
        '64' => PatronInformationResponse::class,
82
        '66' => RenewAllResponse::class,
83
        '94' => LoginResponse::class,
84
        '98' => ACSStatusResponse::class,
85
    ];
86
87
    /** @var array maps SIP2 variable code names to a definition */
88
    private static $mapCodeToVarDef = [
89
        self::AA_PATRON_IDENTIFIER => ['name' => 'PatronIdentifier', 'default'=>''],
90
        self::AB_ITEM_IDENTIFIER => ['name' => 'ItemIdentifier', 'default'=>''],
91
        self::AE_PERSONAL_NAME => ['name' => 'PersonalName', 'default'=>''],
92
        self::AF_SCREEN_MESSAGE => ['name' => 'ScreenMessage', 'type' => 'array', 'default'=>[]],
93
        self::AG_PRINT_LINE => ['name' => 'PrintLine', 'type' => 'array', 'default'=>[]],
94
        self::AH_DUE_DATE => ['name' => 'DueDate', 'default'=>''],
95
        self::AJ_TITLE_IDENTIFIER => ['name' => 'TitleIdentifier', 'default'=>''],
96
        self::AM_LIBRARY_NAME => ['name' => 'LibraryName', 'default'=>''],
97
        self::AN_TERMINAL_LOCATION => ['name' => 'TerminalLocation', 'default'=>''],
98
        self::AO_INSTITUTION_ID => ['name' => 'InstitutionId', 'default'=>''],
99
        self::AP_CURRENT_LOCATION => ['name' => 'CurrentLocation', 'default'=>''],
100
        self::AQ_PERMANENT_LOCATION => ['name' => 'PermanentLocation', 'default'=>''],
101
        self::AS_HOLD_ITEMS => ['name' => 'HoldItems', 'type' => 'array', 'default'=>[]],
102
        self::AT_OVERDUE_ITEMS => ['name' => 'OverdueItems', 'type' => 'array', 'default'=>[]],
103
        self::AU_CHARGED_ITEMS => ['name' => 'ChargedItems', 'type' => 'array', 'default'=>[]],
104
        self::AV_FINE_ITEMS => ['name' => 'FineItems', 'type' => 'array', 'default'=>[]],
105
        self::AY_SEQUENCE_NUMBER => ['name' => 'SequenceNumber', 'default'=>''],
106
        self::BD_HOME_ADDRESS => ['name' => 'HomeAddress', 'default'=>''],
107
        self::BE_EMAIL_ADDRESS => ['name' => 'EmailAddress', 'default'=>''],
108
        self::BF_HOME_PHONE_NUMBER => ['name' => 'HomePhoneNumber', 'default'=>''],
109
        self::BG_OWNER => ['name' => 'Owner', 'default'=>''],
110
        self::BH_CURRENCY_TYPE => ['name' => 'CurrencyType', 'default'=>''],
111
        self::BK_TRANSACTION_ID => ['name' => 'TransactionId', 'default'=>''],
112
        self::BL_VALID_PATRON => ['name' => 'ValidPatron', 'default'=>''],
113
        self::BM_RENEWED_ITEMS => ['name' => 'RenewedItems', 'type' => 'array', 'default'=>[]],
114
        self::BN_UNRENEWED_ITEMS => ['name' => 'UnrenewedItems', 'type' => 'array', 'default'=>[]],
115
        self::BR_QUEUE_POSITION => ['name' => 'QueuePosition', 'default'=>''],
116
        self::BS_PICKUP_LOCATION => ['name' => 'PickupLocation', 'default'=>''],
117
        self::BT_FEE_TYPE => ['name' => 'FeeType', 'default'=>''],
118
        self::BU_RECALL_ITEMS => ['name' => 'RecallItems', 'type' => 'array', 'default'=>[]],
119
        self::BV_FEE_AMOUNT => ['name' => 'FeeAmount', 'default'=>''],
120
        self::BW_EXPIRATION_DATE => ['name' => 'ExpirationDate', 'default'=>''],
121
        self::BX_SUPPORTED_MESSAGES => ['name' => 'SupportedMessages', 'default'=>''],
122
        self::BZ_HOLD_ITEMS_LIMIT => ['name' => 'HoldItemsLimit', 'default'=>''],
123
        self::CA_OVERDUE_ITEMS_LIMIT => ['name' => 'OverdueItemsLimit', 'default'=>''],
124
        self::CB_CHARGED_ITEMS_LIMIT => ['name' => 'ChargedItemsLimit', 'default'=>''],
125
        self::CC_FEE_LIMIT => ['name' => 'FeeLimit', 'default'=>''],
126
        self::CD_UNAVAILABLE_HOLD_ITEMS => ['name' => 'UnavailableHoldItems', 'type' => 'array', 'default'=>[]],
127
        self::CF_HOLD_QUEUE_LENGTH => ['name' => 'HoldQueueLength', 'default'=>''],
128
        self::CH_ITEM_PROPERTIES => ['name' => 'ItemProperties', 'default'=>''],
129
        self::CI_SECURITY_INHIBIT => ['name' => 'SecurityInhibit', 'default'=>''],
130
        self::CJ_RECALL_DATE => ['name' => 'RecallDate', 'default'=>''],
131
        self::CK_MEDIA_TYPE => ['name' => 'MediaType', 'default'=>''],
132
        self::CL_SORT_BIN => ['name' => 'SortBin', 'default'=>''],
133
        self::CM_HOLD_PICKUP_DATE => ['name' => 'HoldPickupDate', 'default'=>''],
134
        self::CQ_VALID_PATRON_PASSWORD => ['name' => 'ValidPatronPassword', 'default'=>'']
135
    ];
136
137
    protected $allowedVariables = [];
138 21
139
    public static function parse($raw): SIP2Response
140 21
    {
141
        if (empty($raw) || !self::checkCRC($raw)) {
142
            throw new LogicException("Empty string or bad CRC not expected here");//@codeCoverageIgnore
143
        }
144 21
145 21
        $type = substr($raw, 0, 2);
146 1
        if (!isset(self::$mapResponseToClass[$type])) {
147
            throw new RuntimeException("Unexpected SIP2 response $type");
148
        }
149
150 20
        //good to go
151 20
        $className = self::$mapResponseToClass[$type];
152
        return new $className($raw);
153
    }
154 22
155
    public static function checkCRC($raw)
156 22
    {
157 21
        if (!SIP2Client::isCRCCheckEnabled()) {
158 21
            //CRC checks are disabled
159 21
            return true;
160
        }
161
        if (preg_match('/^(.*AZ)(.{4})$/', trim($raw), $match)) {
162
            $plaintext=$match[1];
163 1
            $checksum=$match[2];
164
            return self::crc($plaintext) == $checksum;
165
        }
166 17
167
        //no checksum added to message
168
        return true;
169 17
    }
170 17
171
    protected function parseVariableData($response, $start)
172
    {
173 17
        //init allowed variables
174 17
        foreach ($this->allowedVariables as $code) {
175
            if (!isset(self::$mapCodeToVarDef[$code])) {
176 17
                throw new LogicException("Unexpected $code in allowed variables"); //@codeCoverageIgnore
177
            }
178
            $name = self::$mapCodeToVarDef[$code]['name'];
179
            if (!$this->hasVariable($name)) {
180 17
                //add a definition for this variable
181
                $this->var[$name] = self::$mapCodeToVarDef[$code];
182 17
            }
183 17
        }
184
185
        $items = explode("|", substr($response, $start, -7));
186 17
187 17
        foreach ($items as $item) {
188
            $value = substr($item, 2);
189
190
            //we ignore anything with no value
191 17
            $clean = trim($value, "\x00..\x1F");
192
            if ($clean==='') {
193
                continue;
194 17
            }
195
196
            $field = substr($item, 0, 2);
197 2
198 1
            //expected?
199 1
            if (!in_array($field, $this->allowedVariables)) {
200 1
                //we tolerate unexpected values and treat them as array types
201
                //named after the code if we don't have a definition for it
202 1
                if (!isset(self::$mapCodeToVarDef[$field])) {
203
                    self::$mapCodeToVarDef[$field]=[
204 2
                        'name' => $field,
205
                        'type' => 'array'
206 2
                    ];
207
                    $name=$field;
208
                } else {
209 17
                    $name = self::$mapCodeToVarDef[$field]['name'];
210 17
                }
211
                $this->var[$name] = self::$mapCodeToVarDef[$field];
212 17
            }
213
214
            $name = self::$mapCodeToVarDef[$field]['name'];
215
            $this->addVariable($name, $clean);
216
        }
217
    }
218
}
219