ExceptionUtil::checkVariableIntegrityViolation()   F
last analyzed

Complexity

Conditions 16
Paths 284

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 32
rs 3.6833
c 0
b 0
f 0
cc 16
nc 284
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Jabe\Impl\Util;
4
5
use Doctrine\DBAL\Exception\{
6
    ConstraintViolationException,
7
    DeadlockException,
8
    ForeignKeyConstraintViolationException,
9
    ServerException
10
};
11
use Doctrine\ORM\ORMException;
12
use Jabe\ProcessEngineException;
13
use Jabe\Impl\Context\Context;
14
use Jabe\Impl\Persistence\Entity\ByteArrayEntity;
15
use Jabe\Repository\ResourceTypeInterface;
16
17
class ExceptionUtil
18
{
19
    public const PERSISTENCE_EXCEPTION_MESSAGE = "An exception occurred in the " .
20
        "persistence layer. Please check the server logs for a detailed message and the entire " .
21
        "exception stack trace.";
22
23
    public static function getExceptionStacktrace($obj): ?string
24
    {
25
        if ($obj instanceof \Throwable) {
26
            return $obj->getTraceAsString();
27
        } elseif ($obj instanceof ByteArrayEntity) {
28
            return StringUtil::fromBytes($obj->getBytes());
0 ignored issues
show
Bug introduced by
It seems like $obj->getBytes() can also be of type null; however, parameter $bytes of Jabe\Impl\Util\StringUtil::fromBytes() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

28
            return StringUtil::fromBytes(/** @scrutinizer ignore-type */ $obj->getBytes());
Loading history...
29
        }
30
        return null;
31
    }
32
33
34
    public static function createJobExceptionByteArray(?string $byteArray, ResourceTypeInterface $type): ?ByteArrayEntity
35
    {
36
        return self::createExceptionByteArray("job.exceptionByteArray", $byteArray, $type);
37
    }
38
39
    /**
40
     * create ByteArrayEntity with specified name and payload and make sure it's
41
     * persisted
42
     *
43
     * used in Jobs and ExternalTasks
44
     *
45
     * @param name type\source of the exception
46
     * @param byteArray payload of the exception
47
     * @param type resource type of the exception
48
     * @return persisted entity
49
     */
50
    public static function createExceptionByteArray(string $name, ?string $byteArray, ResourceTypeInterface $type): ?ByteArrayEntity
51
    {
52
        $result = null;
53
54
        if ($byteArray != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $byteArray of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
55
            $result = new ByteArrayEntity($name, $byteArray, $type);
56
            Context::getCommandContext()
57
            ->getByteArrayManager()
58
            ->insertByteArray($result);
59
        }
60
61
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type Jabe\Impl\Persistence\Entity\ByteArrayEntity which is incompatible with the documented return type Jabe\Impl\Util\persisted.
Loading history...
62
    }
63
64
    protected static function getPersistenceCauseException(/*ServerException|ORMException*/$persistenceException): \Throwable
65
    {
66
        if (method_exists($persistenceException, "getCause")) {
67
            $cause = $persistenceException->getCause();
68
            if ($cause !== null) {
69
                return $cause;
70
            }
71
        } elseif (method_exists($persistenceException, "getPrevious")) {
72
            $cause = $persistenceException->getPrevious();
73
            if ($cause !== null) {
74
                return $cause;
75
            }
76
        }
77
        return $persistenceException;
78
    }
79
80
    public static function unwrapException(/*ProcessEngineException|ServerException|ORMException*/$exception): ?\Exception
81
    {
82
        if ($exception instanceof ProcessEngineException) {
83
            $cause = null;
84
            if (method_exists($exception, "getCause")) {
85
                $cause = $exception->getCause();
86
            } elseif (method_exists($exception, "getPrevious")) {
87
                $cause = $exception->getPrevious();
88
            }
89
90
            if ($cause instanceof ProcessEngineException) {
91
                $processEngineExceptionCause = null;
92
                if (method_exists($cause, "getCause")) {
93
                    $processEngineExceptionCause = $cause->getCause();
94
                } elseif (method_exists($cause, "getPrevious")) {
95
                    $processEngineExceptionCause = $cause->getPrevious();
96
                }
97
98
                if ($processEngineExceptionCause instanceof ServerException || $processEngineExceptionCause instanceof ORMException) {
99
                    return self::unwrapException($processEngineExceptionCause);
100
                } else {
101
                    return null;
102
                }
103
            } elseif ($cause instanceof ServerException || $cause instanceof ORMException) {
104
                return self::getPersistenceCauseException($cause);
105
            } else {
106
                return null;
107
            }
108
        } elseif ($exception instanceof ServerException || $exception instanceof ORMException) {
109
            return self::getPersistenceCauseException($exception);
110
        } else {
111
            return null;
112
        }
113
    }
114
115
    public static function checkValueTooLongException($exception): bool
116
    {
117
        if ($exception instanceof ProcessEngineException) {
118
            $sqlException = self::unwrapException($exception);
119
            if ($sqlException === null) {
120
                return false;
121
            } else {
122
                return self::checkValueTooLongException($sqlException);
123
            }
124
        } elseif ($exception instanceof ServerException || $exception instanceof ORMException) {
125
            $message = $exception->getMessage();
126
            return strpos($message, "too long") !== false ||
127
                strpos($message, "too large") !== false ||
128
                strpos($message, "TOO LARGE") !== false ||
129
                strpos($message, "ORA-01461") !== false ||
130
                strpos($message, "ORA-01401") !== false ||
131
                strpos($message, "data would be truncated") !== false ||
132
                strpos($message, "SQLCODE=-302, SQLSTATE=22001");
133
        } else {
134
            return false;
135
        }
136
    }
137
138
    public static function checkConstraintViolationException($exception): bool
139
    {
140
        if ($exception instanceof ProcessEngineException) {
141
            $sqlException = self::unwrapException($exception);
142
            if ($sqlException === null) {
143
                return false;
144
            } else {
145
                return self::checkConstraintViolationException($sqlException);
146
            }
147
        }
148
149
        $message = $exception->getMessage();
150
        return $exception instanceof ConstraintViolationException ||
151
            strpos($message, "constraint") !== false ||
152
            strpos($message, "violat") !== false ||
153
            strpos(strtolower($message), "duplicate") !== false ||
154
            strpos($message, "ORA-00001") !== false ||
155
            strpos($message, "SQLCODE=-803, SQLSTATE=23505");
156
    }
157
158
    public static function checkForeignKeyConstraintViolation(/*ProcessEngineException|ServerException|ORMException*/$exception, bool $skipPostgres): bool
159
    {
160
        if ($exception instanceof ProcessEngineException) {
161
            $sqlException = self::unwrapException($exception);
162
            if ($sqlException === null) {
163
                return false;
164
            } else {
165
                return self::checkForeignKeyConstraintViolation($sqlException);
0 ignored issues
show
Bug introduced by
The call to Jabe\Impl\Util\Exception...eyConstraintViolation() has too few arguments starting with skipPostgres. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

165
                return self::/** @scrutinizer ignore-call */ checkForeignKeyConstraintViolation($sqlException);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
166
            }
167
        }
168
169
        if ($exception instanceof ForeignKeyConstraintViolationException) {
170
            return true;
171
        }
172
173
        $message = strtolower($exception->getMessage());
174
        $sqlState = null;
175
        if (method_exists($exception, 'getSQLState')) {
176
            $sqlState = $exception->getSQLState();
177
        } else {
178
            //SQLSTATE
179
            preg_match_all('/SQLSTATE\[(\d*)\]/', $message, $matches);
180
            if (!empty($matches[0])) {
181
                $sqlState = $matches[0];
182
            }
183
        }
184
        $errorCode = $exception->getCode();
185
186
        if ($sqlState == '23503' && $errorCode == 0) {
187
            return !$skipPostgres;
188
        } else {
189
            // SqlServer
190
            return strpos($message, "foreign key constraint") !== false ||
191
                $sqlState == "23000" && $errorCode == 547 ||
192
                // MySql & MariaDB & PostgreSQL
193
                $sqlState == "23000" && $errorCode == 1452 ||
194
                // Oracle & H2
195
                strpos($message, "integrity constraint") !== false ||
196
                // Oracle
197
                $sqlState == "23000" && $errorCode == 2291 ||
198
                // H2
199
                //$sqlState == "23506" && $errorCode == 23506 ||
200
                // DB2
201
                strpos($message, "sqlstate=23503") !== false && strpos($message, "sqlcode=-530") !== false ||
202
                // DB2 zOS
203
                $sqlState == "23503" && $errorCode == -530;
204
        }
205
    }
206
207
    public static function checkVariableIntegrityViolation($exception): bool
208
    {
209
        if ($exception instanceof ProcessEngineException) {
210
            $sqlException = self::unwrapException($exception);
211
            if ($sqlException === null) {
212
                return false;
213
            } else {
214
                return self::checkVariableIntegrityViolation($sqlException);
215
            }
216
        }
217
218
        $message = strtolower($exception->getMessage());
219
        $sqlState = null;
220
        if (method_exists($exception, 'getSQLState')) {
221
            $sqlState = $exception->getSQLState();
222
        } else {
223
            //SQLSTATE
224
            preg_match_all('/SQLSTATE\[(\d*)\]/', $message, $matches);
225
            if (!empty($matches[0])) {
226
                $sqlState = $matches[0];
227
            }
228
        }
229
        $errorCode = $exception->getCode();
230
231
        // MySQL & MariaDB
232
        return (strpos($message, "act_uniq_variable") !== false && $sqlState == "23000" && $errorCode == 1062)
233
            // PostgreSQL
234
            || (strpos($message, "act_uniq_variable") !== false && $sqlState == "23505" && $errorCode == 0)
235
            // SqlServer
236
            || (strpos($message, "act_uniq_variable") !== false && $sqlState == "23000" && $errorCode == 2601)
237
            // Oracle
238
            || (strpos($message, "act_uniq_variable") !== false && $sqlState == "23000" && $errorCode == 1);
239
    }
240
241
    public static function checkDeadlockException($sqlException): bool
242
    {
243
        $sqlState = null;
244
        if (method_exists($sqlException, 'getSQLState')) {
245
            $sqlState = $sqlException->getSQLState();
246
        } else {
247
            //SQLSTATE
248
            $message = strtolower($sqlException->getMessage());
249
            preg_match_all('/sqlstate\[(\d*)\]/', $message, $matches);
250
            if (!empty($matches[0])) {
251
                $sqlState = $matches[0];
252
            }
253
        }
254
        $errorCode = $sqlException->getCode();
255
256
        return DeadlockCodes::mariadbMysql()->equals($errorCode, $sqlState) ||
257
            DeadlockCodes::mssql()->equals($errorCode, $sqlState) ||
258
            DeadlockCodes::db2()->equals($errorCode, $sqlState) ||
259
            DeadlockCodes::oracle()->equals($errorCode, $sqlState) ||
260
            DeadlockCodes::postgres()->equals($errorCode, $sqlState);
261
    }
262
263
    public static function findBatchExecutorException($exception)
0 ignored issues
show
Unused Code introduced by
The parameter $exception is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

263
    public static function findBatchExecutorException(/** @scrutinizer ignore-unused */ $exception)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
264
    {
265
        return null;
266
    }
267
268
    /**
269
     * Pass logic, which directly calls MyBatis API. In case a MyBatis exception is thrown, it is
270
     * wrapped into a ProcessEngineException and never propagated directly to an Engine API
271
     * call. In some cases, the top-level exception and its message are shown as a response body in
272
     * the REST API. Wrapping all MyBatis API calls in our codebase makes sure that the top-level
273
     * exception is always a ProcessEngineException with a generic message. Like this, SQL
274
     * details are never disclosed to potential attackers.
275
     *
276
     * @param supplier which calls MyBatis API
277
     * @param <T> is the type of the return value
278
     * @return the value returned by the supplier
279
     * @throws ProcessEngineException which wraps the actual exception
280
     */
281
    public static function doWithExceptionWrapper(callable $supplier)
282
    {
283
        try {
284
            return $supplier();
285
        } catch (\Exception $ex) {
286
            throw self::wrapPersistenceException($ex);
287
        }
288
    }
289
290
    public static function wrapPersistenceException(\Exception $ex): ProcessEngineException
291
    {
292
        return new ProcessEngineException(self::PERSISTENCE_EXCEPTION_MESSAGE, $ex);
293
    }
294
}
295