001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.transaction.participant; 020 021import java.util.Arrays; 022import java.util.Set; 023import java.util.HashSet; 024import java.util.StringTokenizer; 025import java.io.Serializable; 026import java.util.regex.Pattern; 027 028import org.jpos.core.*; 029import org.jpos.iso.*; 030import org.jpos.rc.CMF; 031import org.jpos.rc.Result; 032import org.jpos.transaction.Context; 033import org.jpos.transaction.ContextConstants; 034import org.jpos.transaction.TransactionParticipant; 035import org.jpos.util.Caller; 036 037import static org.jpos.transaction.ContextConstants.*; 038 039public class CheckFields implements TransactionParticipant, Configurable { 040 private Configuration cfg; 041 private String request; 042 private Pattern PCODE_PATTERN = Pattern.compile("^[\\d|\\w]{6}$"); 043 private Pattern TID_PATTERN = Pattern.compile("^[\\w\\s]{1,16}"); 044 private Pattern MID_PATTERN = Pattern.compile("^[\\w\\s]{1,15}"); 045 private Pattern TRANSMISSION_TIMESTAMP_PATTERN = Pattern.compile("^\\d{10}"); 046 private Pattern LOCAL_TIMESTAMP_PATTERN = Pattern.compile("^\\d{14}"); 047 private Pattern CAPTUREDATE_PATTERN = Pattern.compile("^\\d{4}"); 048 private Pattern ORIGINAL_DATA_ELEMENTS_PATTERN = Pattern.compile("^\\d{30,41}$"); 049 private boolean ignoreCardValidation = false; 050 private boolean allowExtraFields = false; 051 private Pattern track1Pattern = null; 052 private Pattern track2Pattern = null; 053 054 public int prepare (long id, Serializable context) { 055 Context ctx = (Context) context; 056 Result rc = ctx.getResult(); 057 try { 058 ISOMsg m = ctx.get (request); 059 if (m == null) { 060 ctx.getResult().fail(CMF.INVALID_TRANSACTION, Caller.info(), "'%s' not available in Context", request); 061 return ABORTED | NO_JOIN | READONLY; 062 } 063 Set<String> validFields = new HashSet<>(); 064 assertFields (ctx, m, cfg.get ("mandatory", ""), true, validFields, rc); 065 assertFields (ctx, m, cfg.get ("optional", ""), false, validFields, rc); 066 if (!allowExtraFields) assertNoExtraFields (m, validFields, rc); 067 } catch (Throwable t) { 068 rc.fail(CMF.SYSTEM_ERROR, Caller.info(), t.getMessage()); 069 ctx.log(t); 070 } 071 return (rc.hasFailures() ? ABORTED : PREPARED) | NO_JOIN | READONLY; 072 } 073 074 public void setConfiguration (Configuration cfg) { 075 this.cfg = cfg; 076 request = cfg.get ("request", ContextConstants.REQUEST.toString()); 077 ignoreCardValidation = cfg.getBoolean("ignore-card-validation", false); 078 allowExtraFields = cfg.getBoolean("allow-extra-fields", false); 079 String t1 = cfg.get("track1-pattern", null); 080 if (t1 != null) { 081 track1Pattern = Pattern.compile(t1); 082 } 083 String t2 = cfg.get("track2-pattern", null); 084 if (t2 != null) { 085 track2Pattern = Pattern.compile(t2); 086 } 087 } 088 089 private void assertFields(Context ctx, ISOMsg m, String fields, boolean mandatory, Set<String> validFields, Result rc) { 090 StringTokenizer st = new StringTokenizer (fields, ", "); 091 while (st.hasMoreTokens()) { 092 String s = st.nextToken(); 093 ContextConstants k = null; 094 try { 095 k = ContextConstants.valueOf(s); 096 } catch (IllegalArgumentException ignored) { } 097 if (k != null) { 098 switch (k) { 099 case PCODE: 100 putPCode(ctx, m, mandatory, validFields, rc); 101 break; 102 case CARD: 103 putCard(ctx, m, mandatory, validFields, rc); 104 break; 105 case TID: 106 putTid(ctx, m, mandatory, validFields, rc); 107 break; 108 case MID: 109 putMid(ctx, m, mandatory, validFields, rc); 110 break; 111 case TRANSMISSION_TIMESTAMP: 112 putTimestamp(ctx, m, TRANSMISSION_TIMESTAMP.toString(), 7, TRANSMISSION_TIMESTAMP_PATTERN, mandatory, validFields, rc); 113 break; 114 case TRANSACTION_TIMESTAMP: 115 putTimestamp(ctx, m, TRANSACTION_TIMESTAMP.toString(), 12, LOCAL_TIMESTAMP_PATTERN, mandatory, validFields, rc); 116 break; 117 case POS_DATA_CODE: 118 putPDC(ctx, m, mandatory, validFields, rc); 119 break; 120 case CAPTURE_DATE: 121 putCaptureDate(ctx, m, mandatory, validFields, rc); 122 break; 123 case AMOUNT: 124 putAmount(ctx, m, mandatory, validFields, rc); 125 break; 126 case ORIGINAL_DATA_ELEMENTS: 127 putOriginalDataElements(ctx, m, mandatory, validFields, rc); 128 break; 129 default: 130 k = null; 131 } 132 } 133 if (k == null) { 134 if (mandatory && !m.hasField(s)) 135 rc.fail(CMF.MISSING_FIELD, Caller.info(), s); 136 else 137 validFields.add(s); 138 } 139 } 140 } 141 private void assertNoExtraFields (ISOMsg m, Set validFields, Result rc) { 142 StringBuilder sb = new StringBuilder(); 143 for (int i=1; i<=m.getMaxField(); i++) { // we start at 1, MTI is always valid 144 String s = Integer.toString (i); 145 if (m.hasField(i) && !validFields.contains (s)) { 146 if (sb.length() > 0) 147 sb.append (' '); 148 sb.append (s); 149 } 150 } 151 if (sb.length() > 0) 152 rc.fail(CMF.EXTRA_FIELD, Caller.info(), sb.toString()); 153 } 154 155 private void putCard (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 156 boolean hasCard = m.hasAny("2", "14", "35", "45"); 157 if (!mandatory && !hasCard) 158 return; // nothing to do, card is optional 159 160 try { 161 Card.Builder cb = Card.builder(); 162 if (track1Pattern != null) 163 cb.withTrack1Builder(Track1.builder().pattern(track1Pattern)); 164 if (track2Pattern != null) 165 cb.withTrack2Builder(Track2.builder().pattern(track2Pattern)); 166 cb.isomsg(m); 167 if (ignoreCardValidation) 168 cb.validator(null); 169 Card card = cb.build(); 170 ctx.put (ContextConstants.CARD.toString(), card); 171 if (card.hasTrack1()) 172 validFields.add("45"); 173 if (card.hasTrack2()) 174 validFields.add("35"); 175 if (card.getPan() != null && m.hasField(2)) 176 validFields.add("2"); 177 if (card.getExp() != null && m.hasField(14)) 178 validFields.add("14"); 179 } catch (InvalidCardException e) { 180 validFields.addAll(Arrays.asList("2", "14", "35", "45")); 181 if (hasCard) { 182 rc.fail(CMF.INVALID_CARD_NUMBER, Caller.info(), e.getMessage()); 183 } else if (mandatory) { 184 rc.fail(CMF.MISSING_FIELD, Caller.info(), e.getMessage()); 185 } 186 } 187 } 188 189 private void putPCode (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 190 if (m.hasField(3)) { 191 String s = m.getString(3); 192 193 validFields.add("3"); 194 if (PCODE_PATTERN.matcher(s).matches()) { 195 ctx.put(ContextConstants.PCODE.toString(), m.getString(3)); 196 } else 197 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid PCODE '%s'", s); 198 } else if (mandatory) { 199 rc.fail(CMF.MISSING_FIELD, Caller.info(), "PCODE"); 200 } 201 } 202 203 private void putTid (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 204 if (m.hasField(41)) { 205 String s = m.getString(41); 206 validFields.add("41"); 207 if (TID_PATTERN.matcher(s).matches()) { 208 ctx.put(ContextConstants.TID.toString(), m.getString(41)); 209 } else 210 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid TID '%s'", s); 211 } else if (mandatory) { 212 rc.fail(CMF.MISSING_FIELD, Caller.info(), "TID"); 213 } 214 } 215 216 private void putMid (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 217 if (m.hasField(42)) { 218 String s = m.getString(42); 219 validFields.add("42"); 220 if (MID_PATTERN.matcher(s).matches()) { 221 ctx.put(ContextConstants.MID.toString(), m.getString(42)); 222 } else 223 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid MID '%s'", s); 224 } else if (mandatory) { 225 rc.fail(CMF.MISSING_FIELD, Caller.info(), "MID"); 226 } 227 } 228 private void putTimestamp (Context ctx, ISOMsg m, String key, int fieldNumber, Pattern ptrn, boolean mandatory, Set<String> validFields, Result rc) { 229 if (m.hasField(fieldNumber)) { 230 String s = m.getString(fieldNumber); 231 validFields.add(Integer.toString(fieldNumber)); 232 if (ptrn.matcher(s).matches()) 233 ctx.put (key, ISODate.parseISODate(s)); 234 else 235 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", key, s); 236 } else if (mandatory) { 237 rc.fail(CMF.MISSING_FIELD, Caller.info(), TRANSMISSION_TIMESTAMP.toString()); 238 } 239 } 240 241 private void putCaptureDate (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 242 if (m.hasField(17)) { 243 String s = m.getString(17); 244 validFields.add("17"); 245 if (CAPTUREDATE_PATTERN.matcher(s).matches()) 246 ctx.put (CAPTURE_DATE.toString(), ISODate.parseISODate(s + "120000")); 247 else 248 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", CAPTURE_DATE, s); 249 } else if (mandatory) { 250 rc.fail(CMF.MISSING_FIELD, Caller.info(), CAPTURE_DATE.toString()); 251 } 252 } 253 254 private void putPDC (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 255 if (m.hasField(22)) { 256 byte[] b = m.getBytes(22); 257 validFields.add("22"); 258 if (b.length != 16) { 259 rc.fail( 260 CMF.INVALID_FIELD, 261 Caller.info(), "Invalid %s '%s'", 262 ContextConstants.POS_DATA_CODE.toString(), 263 ISOUtil.hexString(b) 264 ); 265 } 266 else { 267 ctx.put(ContextConstants.POS_DATA_CODE.toString(), PosDataCode.valueOf(m.getBytes(22))); 268 } 269 } else if (mandatory) { 270 rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.POS_DATA_CODE.toString()); 271 } 272 } 273 274 private void putAmount (Context ctx, ISOMsg m, boolean mandatory,Set<String> validFields, Result rc) { 275 Object o4 = m.getComponent(4); 276 Object o5 = m.getComponent(5); 277 ISOAmount a4 = null; 278 ISOAmount a5 = null; 279 if (o4 instanceof ISOAmount) { 280 a4 = (ISOAmount) o4; 281 validFields.add("4"); 282 } 283 if (o5 instanceof ISOAmount) { 284 a5 = (ISOAmount) o5; 285 validFields.add("5"); 286 } 287 if (a5 != null) { 288 ctx.put (AMOUNT.toString(), a5); 289 if (a4 != null) { 290 ctx.put (LOCAL_AMOUNT.toString(), a4); 291 } 292 } else if (a4 != null) { 293 ctx.put (AMOUNT.toString(), a4); 294 } 295 if (mandatory && (a4 == null && a5 == null)) 296 rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.AMOUNT.toString()); 297 } 298 299 private void putOriginalDataElements (Context ctx, ISOMsg m, boolean mandatory, Set<String> validFields, Result rc) { 300 String s = m.getString(56); 301 if (s != null) { 302 validFields.add("56"); 303 if (ORIGINAL_DATA_ELEMENTS_PATTERN.matcher(s).matches()) { 304 ctx.put (ORIGINAL_MTI.toString(), s.substring(0,4)); 305 ctx.put (ORIGINAL_STAN.toString(), s.substring(4,16)); 306 ctx.put (ORIGINAL_TIMESTAMP.toString(), ISODate.parseISODate (s.substring(16,30))); 307 } else { 308 rc.fail(CMF.INVALID_FIELD, Caller.info(), "Invalid %s '%s'", ORIGINAL_DATA_ELEMENTS, s); 309 } 310 } else if (mandatory) { 311 rc.fail(CMF.MISSING_FIELD, Caller.info(), ContextConstants.ORIGINAL_DATA_ELEMENTS.toString()); 312 } 313 } 314}