diff --git a/migration/i1.0a-release/oracle/201303051733_IDEMPIERE-347.sql b/migration/i1.0a-release/oracle/201303051733_IDEMPIERE-347.sql new file mode 100644 index 0000000000..f5029c91ab --- /dev/null +++ b/migration/i1.0a-release/oracle/201303051733_IDEMPIERE-347.sql @@ -0,0 +1,8 @@ +-- Mar 5, 2013 5:32:43 PM COT +-- IDEMPIERE-347 passwords hash +UPDATE AD_User SET EMailVerifyDate=NULL, EMail='webservice @ gardenworld.com',Updated=TO_DATE('2013-03-05 17:32:43','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_User_ID=50001 +; + +SELECT register_migration_script('201303051733_IDEMPIERE-347.sql') FROM dual +; + diff --git a/migration/i1.0a-release/postgresql/201303051733_IDEMPIERE-347.sql b/migration/i1.0a-release/postgresql/201303051733_IDEMPIERE-347.sql new file mode 100644 index 0000000000..07608a4cce --- /dev/null +++ b/migration/i1.0a-release/postgresql/201303051733_IDEMPIERE-347.sql @@ -0,0 +1,8 @@ +-- Mar 5, 2013 5:32:43 PM COT +-- IDEMPIERE-347 passwords hash +UPDATE AD_User SET EMailVerifyDate=NULL, EMail='webservice @ gardenworld.com',Updated=TO_TIMESTAMP('2013-03-05 17:32:43','YYYY-MM-DD HH24:MI:SS'),UpdatedBy=100 WHERE AD_User_ID=50001 +; + +SELECT register_migration_script('201303051733_IDEMPIERE-347.sql') FROM dual +; + diff --git a/org.adempiere.base/src/org/compiere/model/MUser.java b/org.adempiere.base/src/org/compiere/model/MUser.java index ff5f85d53e..d69e0da2d3 100644 --- a/org.adempiere.base/src/org/compiere/model/MUser.java +++ b/org.adempiere.base/src/org/compiere/model/MUser.java @@ -902,7 +902,7 @@ public class MUser extends X_AD_User if (email_login && getPassword() != null && getPassword().length() > 0) { // email is mandatory for users with password if (getEMail() == null || getEMail().length() == 0) { - log.saveError("SaveError", Msg.getMsg(getCtx(), "FillMandatory") + Msg.getElement(getCtx(), COLUMNNAME_EMail)); + log.saveError("SaveError", Msg.getMsg(getCtx(), "FillMandatory") + Msg.getElement(getCtx(), COLUMNNAME_EMail) + " - " + toString()); return false; } // email with password must be unique on the same tenant diff --git a/org.adempiere.base/src/org/compiere/model/M_Element.java b/org.adempiere.base/src/org/compiere/model/M_Element.java index d60d53671d..c1f53048a7 100644 --- a/org.adempiere.base/src/org/compiere/model/M_Element.java +++ b/org.adempiere.base/src/org/compiere/model/M_Element.java @@ -287,12 +287,12 @@ public class M_Element extends X_AD_Element || is_ValueChanged(M_Element.COLUMNNAME_Name) ) { // Print Info - sql = new StringBuilder("UPDATE AD_PrintFormatItem pi SET PrintName=") + sql = new StringBuilder("UPDATE AD_PrintFormatItem SET PrintName=") .append(DB.TO_STRING(getPrintName())) .append(", Name=").append(DB.TO_STRING(getName())) .append(" WHERE IsCentrallyMaintained='Y'") .append(" AND EXISTS (SELECT * FROM AD_Column c ") - .append("WHERE c.AD_Column_ID=pi.AD_Column_ID AND c.AD_Element_ID=") + .append("WHERE c.AD_Column_ID=AD_PrintFormatItem.AD_Column_ID AND c.AD_Element_ID=") .append(get_ID()).append(")"); no = DB.executeUpdate(sql.toString(), get_TrxName()); if (log.isLoggable(Level.FINE)) log.fine("PrintFormatItem updated #" + no); diff --git a/org.adempiere.base/src/org/compiere/print/MPrintFormat.java b/org.adempiere.base/src/org/compiere/print/MPrintFormat.java index 9c9697aec0..8b30fe1e49 100644 --- a/org.adempiere.base/src/org/compiere/print/MPrintFormat.java +++ b/org.adempiere.base/src/org/compiere/print/MPrintFormat.java @@ -19,8 +19,10 @@ package org.compiere.print; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -503,8 +505,7 @@ public class MPrintFormat extends X_AD_PrintFormat // Get Info String sql = "SELECT TableName," // 1 - + " (SELECT COUNT(*) FROM AD_PrintFormat x WHERE x.AD_Table_ID=t.AD_Table_ID AND x.AD_Client_ID=c.AD_Client_ID) AS Count," - + " COALESCE (cpc.AD_PrintColor_ID, pc.AD_PrintColor_ID) AS AD_PrintColor_ID," // 3 + + " COALESCE (cpc.AD_PrintColor_ID, pc.AD_PrintColor_ID) AS AD_PrintColor_ID," // 2 + " COALESCE (cpf.AD_PrintFont_ID, pf.AD_PrintFont_ID) AS AD_PrintFont_ID," + " COALESCE (cpp.AD_PrintPaper_ID, pp.AD_PrintPaper_ID) AS AD_PrintPaper_ID " + "FROM AD_Table t, AD_Client c" @@ -528,21 +529,18 @@ public class MPrintFormat extends X_AD_PrintFormat // Name String TableName = rs.getString(1); String ColumnName = TableName + "_ID"; - String s = ColumnName; + String basename = ColumnName; if (!ColumnName.equals("T_Report_ID")) { - s = Msg.translate (ctx, ColumnName); - if (ColumnName.equals (s)) // not found - s = Msg.translate (ctx, TableName); + basename = Msg.translate (ctx, ColumnName); + if (ColumnName.equals (basename)) // not found + basename = Msg.translate (ctx, TableName); } - int count = rs.getInt(2); - if (count > 0) - s += "_" + (count+1); - pf.setName(s); + setUniqueName(AD_Client_ID, pf, basename); // - pf.setAD_PrintColor_ID(rs.getInt(3)); - pf.setAD_PrintFont_ID(rs.getInt(4)); - pf.setAD_PrintPaper_ID(rs.getInt(5)); + pf.setAD_PrintColor_ID(rs.getInt(2)); + pf.setAD_PrintFont_ID(rs.getInt(3)); + pf.setAD_PrintPaper_ID(rs.getInt(4)); // error = false; } @@ -561,8 +559,7 @@ public class MPrintFormat extends X_AD_PrintFormat return null; // Save & complete - if (!pf.save()) - return null; + pf.saveEx(); GridField[] gridFields = null; @@ -634,6 +631,12 @@ public class MPrintFormat extends X_AD_PrintFormat return pf; } + private static boolean exists(int clientID, String name) { + final String sql = "SELECT COUNT(*) FROM AD_PrintFormat WHERE AD_Client_ID=? AND Name=?"; + int cnt = DB.getSQLValue(null, sql, clientID, name); + return cnt > 0; + } + /************************************************************************** * Create MPrintFormat for Table * @param ctx context @@ -663,8 +666,7 @@ public class MPrintFormat extends X_AD_PrintFormat // Get Info String sql = "SELECT TableName," // 1 - + " (SELECT COUNT(*) FROM AD_PrintFormat x WHERE x.AD_Table_ID=t.AD_Table_ID AND x.AD_Client_ID=c.AD_Client_ID) AS Count," - + " COALESCE (cpc.AD_PrintColor_ID, pc.AD_PrintColor_ID) AS AD_PrintColor_ID," // 3 + + " COALESCE (cpc.AD_PrintColor_ID, pc.AD_PrintColor_ID) AS AD_PrintColor_ID," // 2 + " COALESCE (cpf.AD_PrintFont_ID, pf.AD_PrintFont_ID) AS AD_PrintFont_ID," + " COALESCE (cpp.AD_PrintPaper_ID, pp.AD_PrintPaper_ID) AS AD_PrintPaper_ID " + "FROM AD_Table t, AD_Client c" @@ -688,21 +690,18 @@ public class MPrintFormat extends X_AD_PrintFormat // Name String TableName = rs.getString(1); String ColumnName = TableName + "_ID"; - String s = ColumnName; + String basename = ColumnName; if (!ColumnName.equals("T_Report_ID")) { - s = Msg.translate (ctx, ColumnName); - if (ColumnName.equals (s)) // not found - s = Msg.translate (ctx, TableName); + basename = Msg.translate (ctx, ColumnName); + if (ColumnName.equals (basename)) // not found + basename = Msg.translate (ctx, TableName); } - int count = rs.getInt(2); - if (count > 0) - s += "_" + (count+1); - pf.setName(s); + setUniqueName(AD_Client_ID, pf, basename); // - pf.setAD_PrintColor_ID(rs.getInt(3)); - pf.setAD_PrintFont_ID(rs.getInt(4)); - pf.setAD_PrintPaper_ID(rs.getInt(5)); + pf.setAD_PrintColor_ID(rs.getInt(2)); + pf.setAD_PrintFont_ID(rs.getInt(3)); + pf.setAD_PrintPaper_ID(rs.getInt(4)); // error = false; } @@ -721,8 +720,7 @@ public class MPrintFormat extends X_AD_PrintFormat return null; // Save & complete - if (!pf.save()) - return null; + pf.saveEx(); // pf.dump(); pf.setItems (createItems(ctx, pf)); // @@ -746,7 +744,6 @@ public class MPrintFormat extends X_AD_PrintFormat // Get Info String sql = "SELECT t.TableName," - + " (SELECT COUNT(*) FROM AD_PrintFormat x WHERE x.AD_ReportView_ID=rv.AD_ReportView_ID AND x.AD_Client_ID=c.AD_Client_ID) AS Count," + " COALESCE (cpc.AD_PrintColor_ID, pc.AD_PrintColor_ID) AS AD_PrintColor_ID," + " COALESCE (cpf.AD_PrintFont_ID, pf.AD_PrintFont_ID) AS AD_PrintFont_ID," + " COALESCE (cpp.AD_PrintPaper_ID, pp.AD_PrintPaper_ID) AS AD_PrintPaper_ID," @@ -772,19 +769,16 @@ public class MPrintFormat extends X_AD_PrintFormat if (rs.next()) { // Name - String name = ReportName; - if (name == null || name.length() == 0) - name = rs.getString(1); // TableName - int count = rs.getInt(2); - if (count > 0) - name += "_" + count; - pf.setName(name); + String basename = ReportName; + if (basename == null || basename.length() == 0) + basename = rs.getString(1); // TableName + setUniqueName(AD_Client_ID, pf, basename); // - pf.setAD_PrintColor_ID(rs.getInt(3)); - pf.setAD_PrintFont_ID(rs.getInt(4)); - pf.setAD_PrintPaper_ID(rs.getInt(5)); + pf.setAD_PrintColor_ID(rs.getInt(2)); + pf.setAD_PrintFont_ID(rs.getInt(3)); + pf.setAD_PrintPaper_ID(rs.getInt(4)); // - pf.setAD_Table_ID (rs.getInt(6)); + pf.setAD_Table_ID (rs.getInt(5)); error = false; } else @@ -802,14 +796,27 @@ public class MPrintFormat extends X_AD_PrintFormat return null; // Save & complete - if (!pf.save()) - return null; + pf.saveEx(); // pf.dump(); pf.setItems (createItems(ctx, pf)); // return pf; } // createFromReportView + private static void setUniqueName(int AD_Client_ID, MPrintFormat pf, String basename) { + String name = basename; + pf.setName(name); + boolean sleep = false; + while (exists(AD_Client_ID, name)) { + if (sleep) + Env.sleep(1); // wait 1 sec to get next second in datetime + else + sleep = true; + name = basename + "_" + getDateTime(); + pf.setName(name); + } + } + /** * Create Items. @@ -1022,8 +1029,8 @@ public class MPrintFormat extends X_AD_PrintFormat // Set Name - Remove TEMPLATE - add copy to.setName(Util.replace(to.getName(), "TEMPLATE", String.valueOf(to_Client_ID))); to.setName(to.getName() - + " " + Msg.getMsg(ctx, "Copy") - + " " + to.hashCode()); // unique name + + " - " + Util.cleanAmp(Msg.getMsg(ctx, "Copy")) + + " " + getDateTime()); // unique name // to.saveEx(); @@ -1034,6 +1041,13 @@ public class MPrintFormat extends X_AD_PrintFormat /*************************************************************************/ + private static String getDateTime() { + Calendar cal = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + String dt = sdf.format(cal.getTime()); + return dt; + } + /** Cached Formats */ static private CCache s_formats = new CCache(Table_Name, 30); diff --git a/org.adempiere.base/src/org/compiere/print/MPrintFormatItem.java b/org.adempiere.base/src/org/compiere/print/MPrintFormatItem.java index 5e4897bf0b..edc3ba7793 100644 --- a/org.adempiere.base/src/org/compiere/print/MPrintFormatItem.java +++ b/org.adempiere.base/src/org/compiere/print/MPrintFormatItem.java @@ -665,21 +665,21 @@ public class MPrintFormatItem extends X_AD_PrintFormatItem // && MClient.get(getCtx()).isMultiLingualDocument() && getPrintName() != null && getPrintName().length() > 0) { - String sql = "UPDATE AD_PrintFormatItem_Trl trl " + String sql = "UPDATE AD_PrintFormatItem_Trl " + "SET PrintName = (SELECT e.PrintName " + "FROM AD_Element_Trl e, AD_Column c " - + "WHERE e.AD_Language=trl.AD_Language" + + "WHERE e.AD_Language=AD_PrintFormatItem_Trl.AD_Language" + " AND e.AD_Element_ID=c.AD_Element_ID" + " AND c.AD_Column_ID=" + getAD_Column_ID() + ") " + "WHERE AD_PrintFormatItem_ID = " + get_ID() + " AND EXISTS (SELECT * " + "FROM AD_Element_Trl e, AD_Column c " - + "WHERE e.AD_Language=trl.AD_Language" + + "WHERE e.AD_Language=AD_PrintFormatItem_Trl.AD_Language" + " AND e.AD_Element_ID=c.AD_Element_ID" + " AND c.AD_Column_ID=" + getAD_Column_ID() - + " AND trl.AD_PrintFormatItem_ID = " + get_ID() + ")" + + " AND AD_PrintFormatItem_Trl.AD_PrintFormatItem_ID = " + get_ID() + ")" + " AND EXISTS (SELECT * FROM AD_Client " - + "WHERE AD_Client_ID=trl.AD_Client_ID AND IsMultiLingualDocument='Y')"; + + "WHERE AD_Client_ID=AD_PrintFormatItem_Trl.AD_Client_ID AND IsMultiLingualDocument='Y')"; int no = DB.executeUpdate(sql, get_TrxName()); if (log.isLoggable(Level.FINE)) log.fine("translations updated #" + no); } diff --git a/org.adempiere.base/src/org/compiere/util/Login.java b/org.adempiere.base/src/org/compiere/util/Login.java index 9767e30989..c0c83fd5e0 100644 --- a/org.adempiere.base/src/org/compiere/util/Login.java +++ b/org.adempiere.base/src/org/compiere/util/Login.java @@ -170,7 +170,8 @@ public class Login * @param app_pwd pwd * @param force ignore pwd * @return Array of Role KeyNamePair or null if error - * The error (NoDatabase, UserPwdError, DBLogin) is saved in the log + * The error (NoDatabase, UserPwdError, DBLogin) is saved in the log + * @deprecated */ protected KeyNamePair[] getRoles (CConnection cc, String app_user, String app_pwd, boolean force) @@ -205,7 +206,8 @@ public class Login * Sets Context with login info * @param app_user Principal * @return role array or null if in error. - * The error (NoDatabase, UserPwdError, DBLogin) is saved in the log + * The error (NoDatabase, UserPwdError, DBLogin) is saved in the log + * @deprecated use public KeyNamePair[] getRoles(String app_user, KeyNamePair client) */ public KeyNamePair[] getRoles (Principal app_user) { @@ -224,7 +226,8 @@ public class Login * @param app_user user id * @param app_pwd password * @return role array or null if in error. - * The error (NoDatabase, UserPwdError, DBLogin) is saved in the log + * The error (NoDatabase, UserPwdError, DBLogin) is saved in the log + * @deprecated use public KeyNamePair[] getRoles(String app_user, KeyNamePair client) */ public KeyNamePair[] getRoles (String app_user, String app_pwd) { @@ -237,7 +240,8 @@ public class Login * @param app_pwd pwd * @param force ignore pwd * @return role array or null if in error. - * The error (NoDatabase, UserPwdError, DBLogin) is saved in the log + * The error (NoDatabase, UserPwdError, DBLogin) is saved in the log + * @deprecated use public KeyNamePair[] getRoles(String app_user, KeyNamePair client) */ private KeyNamePair[] getRoles (String app_user, String app_pwd, boolean force) { @@ -296,6 +300,8 @@ public class Login " AND c.IsActive='Y') AND " + " AD_User.IsActive='Y'"; + // deprecate this method - it cannot cope with same user found on multiple clients + // use public KeyNamePair[] getRoles(String app_user, KeyNamePair client) approach instead MUser user = MTable.get(m_ctx, MUser.Table_ID).createQuery( where, null).setParameters(app_user).firstOnly(); // throws error if username collision occurs // always do calculation to confuse timing based attacks @@ -397,7 +403,13 @@ public class Login do // read all roles { MUser user = new MUser(m_ctx, rs.getInt(1), null); - if (user.getPassword() != null && user.getPassword().equals(app_pwd)) { + boolean valid = false; + if (hash_password) { + valid = user.authenticateHash(app_pwd); + } else { + valid = user.getPassword() != null && user.getPassword().equals(app_pwd); + } + if (valid) { int AD_Role_ID = rs.getInt(2); if (AD_Role_ID == 0) Env.setContext(m_ctx, "#SysAdmin", "Y"); diff --git a/org.adempiere.server-feature/server.product.launch b/org.adempiere.server-feature/server.product.launch index e0776935f5..62ef88a07d 100644 --- a/org.adempiere.server-feature/server.product.launch +++ b/org.adempiere.server-feature/server.product.launch @@ -22,7 +22,7 @@ - + diff --git a/org.idempiere.fitnesse.fixture/src/org/idempiere/fitnesse/fixture/Login.java b/org.idempiere.fitnesse.fixture/src/org/idempiere/fitnesse/fixture/Login.java index 0d8561108f..b99424a0ca 100644 --- a/org.idempiere.fitnesse.fixture/src/org/idempiere/fitnesse/fixture/Login.java +++ b/org.idempiere.fitnesse.fixture/src/org/idempiere/fitnesse/fixture/Login.java @@ -27,6 +27,7 @@ package org.idempiere.fitnesse.fixture; import org.compiere.model.MSession; +import org.compiere.model.MUser; import org.compiere.util.Env; import org.compiere.util.KeyNamePair; @@ -140,7 +141,29 @@ public class Login extends TableFixture { return null; // already logged with same data org.compiere.util.Login login = new org.compiere.util.Login(m_ads!=null ? m_ads.getCtx() : null); - KeyNamePair[] roles = login.getRoles(m_user, m_password); + + KeyNamePair[] clients = login.getClients(m_user, m_password); + boolean okclient = false; + KeyNamePair selectedClient = null; + for (KeyNamePair client : clients) { + if (client.getKey() == m_client_id) { + okclient = true; + selectedClient = client; + break; + } + } + if (!okclient) + return "Error logging in - client not allowed for this user"; + + Env.setContext(m_ads.getCtx(), "#AD_Client_ID", (String) selectedClient.getID()); + MUser user = MUser.get (m_ads.getCtx(), m_user); + if (user != null) { + Env.setContext(m_ads.getCtx(), "#AD_User_ID", user.getAD_User_ID() ); + Env.setContext(m_ads.getCtx(), "#AD_User_Name", user.getName() ); + Env.setContext(m_ads.getCtx(), "#SalesRep_ID", user.getAD_User_ID() ); + } + + KeyNamePair[] roles = login.getRoles(m_user, selectedClient); if (roles != null) { boolean okrole = false; @@ -153,19 +176,6 @@ public class Login extends TableFixture { if (!okrole) return "Error logging in - role not allowed for this user"; - KeyNamePair[] clients = login.getClients( new KeyNamePair(m_role_id, "" ) ); - boolean okclient = false; - for (KeyNamePair client : clients) { - if (client.getKey() == m_client_id) { - okclient = true; - break; - } - } - if (!okclient) - return "Error logging in - client not allowed for this role"; - - m_ads.getCtx().setProperty("#AD_Client_ID", Integer.toString(m_client_id)); - KeyNamePair[] orgs = login.getOrgs( new KeyNamePair(m_role_id, "" )); if (orgs == null) diff --git a/org.idempiere.webservices/WEB-INF/src/org/idempiere/webservices/AbstractService.java b/org.idempiere.webservices/WEB-INF/src/org/idempiere/webservices/AbstractService.java index a285bdc36a..a1a3130c7a 100644 --- a/org.idempiere.webservices/WEB-INF/src/org/idempiere/webservices/AbstractService.java +++ b/org.idempiere.webservices/WEB-INF/src/org/idempiere/webservices/AbstractService.java @@ -33,6 +33,7 @@ import org.adempiere.base.ServiceQuery; import org.adempiere.base.equinox.EquinoxExtensionLocator; import org.adempiere.exceptions.AdempiereException; import org.compiere.model.Lookup; +import org.compiere.model.MUser; import org.compiere.model.MWebService; import org.compiere.model.MWebServiceType; import org.compiere.model.PO; @@ -89,7 +90,29 @@ public class AbstractService { return ret; Login login = new Login(m_cs.getCtx()); - KeyNamePair[] roles = login.getRoles(loginRequest.getUser(), loginRequest.getPass()); + KeyNamePair[] clients = login.getClients(loginRequest.getUser(), loginRequest.getPass()); + boolean okclient = false; + KeyNamePair selectedClient = null; + for (KeyNamePair client : clients) { + if (client.getKey() == loginRequest.getClientID()) { + okclient = true; + selectedClient = client; + break; + } + } + if (!okclient) + return "Error logging in - client not allowed for this user"; + + m_cs.getCtx().setProperty("#AD_Client_ID", "" + loginRequest.getClientID()); + Env.setContext(m_cs.getCtx(), "#AD_Client_ID", (String) selectedClient.getID()); + MUser user = MUser.get (m_cs.getCtx(), loginRequest.getUser()); + if (user != null) { + Env.setContext(m_cs.getCtx(), "#AD_User_ID", user.getAD_User_ID() ); + Env.setContext(m_cs.getCtx(), "#AD_User_Name", user.getName() ); + Env.setContext(m_cs.getCtx(), "#SalesRep_ID", user.getAD_User_ID() ); + } + + KeyNamePair[] roles = login.getRoles(loginRequest.getUser(), selectedClient); if (roles != null) { boolean okrole = false; for (KeyNamePair role : roles) { @@ -101,19 +124,6 @@ public class AbstractService { if (!okrole) return "Error logging in - role not allowed for this user"; - KeyNamePair[] clients = login.getClients(new KeyNamePair(loginRequest.getRoleID(), "")); - boolean okclient = false; - for (KeyNamePair client : clients) { - if (client.getKey() == loginRequest.getClientID()) { - okclient = true; - break; - } - } - if (!okclient) - return "Error logging in - client not allowed for this role"; - - m_cs.getCtx().setProperty("#AD_Client_ID", "" + loginRequest.getClientID()); - KeyNamePair[] orgs = login.getOrgs(new KeyNamePair(loginRequest.getRoleID(), "")); if (orgs == null) @@ -152,8 +162,6 @@ public class AbstractService { if (!m_cs.login(AD_User_ID, loginRequest.getRoleID(), loginRequest.getClientID(), loginRequest.getOrgID(), loginRequest.getWarehouseID(), loginRequest.getLang())) return "Error logging in"; - - } else { return "Error logging in - no roles or user/pwd invalid for user " + loginRequest.getUser(); } diff --git a/selenese/.classpath b/selenese/.classpath index ed71c23e5c..70a367469e 100644 --- a/selenese/.classpath +++ b/selenese/.classpath @@ -2,6 +2,6 @@ - + diff --git a/ztl/.classpath b/ztl/.classpath index 8bd45de0c6..9778165d9f 100644 --- a/ztl/.classpath +++ b/ztl/.classpath @@ -3,6 +3,6 @@ - +