GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 42c65a...a98f71 )
by
unknown
07:18
created

QueryStatement::escapeContains()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 10
Ratio 100 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 10
loc 10
ccs 7
cts 7
cp 1
rs 9.4285
cc 1
eloc 7
nc 1
nop 1
crap 1
1
<?php
2
namespace Dkd\PhpCmis;
3
4
/**
5
 * This file is part of php-cmis-lib.
6
 *
7
 * (c) Dimitri Ebert <[email protected]>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
use Dkd\PhpCmis\Data\ObjectIdInterface;
14
use Dkd\PhpCmis\Data\ObjectTypeInterface;
15
use Dkd\PhpCmis\Definitions\PropertyDefinitionInterface;
16
use Dkd\PhpCmis\Definitions\TypeDefinitionInterface;
17
use Dkd\PhpCmis\Exception\CmisInvalidArgumentException;
18
use Dkd\PhpCmis\Exception\CmisObjectNotFoundException;
19
20
/**
21
 * Query Statement
22
 *
23
 * Prepares a query statement based on either a manually supplied
24
 * statement or one generated from supplied property list, type
25
 * list, clause and ordering.
26
 *
27
 * Used with a manual statement:
28
 *
29
 * $statement = new QueryStatement($session, 'SELECT ...');
30
 *
31
 * Used with property, type lists, clause and ordering:
32
 *
33
 * $statement = new QueryStatement(
34
 *     $session,
35
 *     NULL,
36
 *     array('prop1', 'prop2'),
37
 *     array('type1', 'type2'),
38
 *     'prop1 = type1.foobar',
39
 *     array('prop1 ASC')
40
 * );
41
 *
42
 * Note that this is an approximation of the OpenCMIS Java implementation:
43
 * Java allows multiple constructors but PHP does not; allowing additional
44
 * constructor arguments and making the manual statement optional makes it
45
 * possible to construct instances in nearly the same way as in Java. It's
46
 * close, but not exactly the same - however, when used through the public
47
 * APIs (Session->createQueryStatement) there is no difference in behavior.
48
 */
49
class QueryStatement implements QueryStatementInterface
50
{
51
    /**
52
     * @var SessionInterface
53
     */
54
    protected $session;
55
56
    /**
57
     * @var string
58
     */
59
    protected $statement;
60
61
    /**
62
     * @var array
63
     */
64
    protected $parametersMap = array();
65
66
    /**
67
     * Creates a prepared statement for querying the CMIS repository. Requires
68
     * at least the Session as parameter, then accepts either a manual statement
69
     * or a list of property IDs, type IDs, a where clause and orderings which
70
     * will then generate a prepared statement based on those values.
71
     *
72
     * See also main class desciption.
73
     *
74
     * @param SessionInterface $session The initialized Session for communicating
75
     * @param string $statement Optional, manually prepared statement. If provided,
76
     *      excludes the use of property list, type list, where clause and ordering.
77
     * @param array $selectPropertyIds An array PropertyDefinitionInterface
78
     *      or strings, can be mixed. When strings are provided those can be
79
     *      either the actual ID of the property or the query name thereof.
80
     * @param array $fromTypes An array of TypeDefinitionInterface or strings,
81
     *      can be mixed. When strings are provided those can be either the
82
     *      actual ID of the type, or it can be the query name thereof. If
83
     *      an array of arrays is provided, each array is expected to contain
84
     *      a TypeDefinition or string as first member and an alias as second.
85
     * @param string|null $whereClause If searching by custom clause, provide here.
86
     * @param array $orderByPropertyIds List of property IDs by which to sort.
87
     *      Each value can be either a PropertyDefinitionInterface instance,
88
     *      a string (in which case, ID or queryName) or an array of a string
89
     *      or PropertyDefinition as first member and ASC or DESC as second.
90
     *      E.g. valid strings: "cm:title ASC", "cm:title", "P:cm:title".
91
     *      Valid arrays: [PropertyDefinitionInterface, "ASC"], ["cm:title", "ASC"]
92
     * @throws CmisInvalidArgumentException
93
     */
94 120
    public function __construct(
95
        SessionInterface $session,
96
        $statement = null,
97
        array $selectPropertyIds = array(),
98
        array $fromTypes = array(),
99
        $whereClause = null,
100
        array $orderByPropertyIds = array()
101
    ) {
102 120
        $this->session = $session;
103 120
        $statementString = trim((string) $statement);
104
105 120
        if (empty($statementString)) {
106 26
            if (empty($selectPropertyIds)) {
107 3
                throw new CmisInvalidArgumentException(
108 3
                    'Statement was empty so property list must not be empty!',
109
                    1441286811
110 3
                );
111
            }
112 23
            if (empty($fromTypes)) {
113 1
                throw new CmisInvalidArgumentException(
114 1
                    'Statement was empty so types list must not be empty!',
115
                    1441286812
116 1
                );
117
            }
118 22
            $statementString = $this->generateStatementFromPropertiesAndTypesLists(
119 22
                $selectPropertyIds,
120 22
                $fromTypes,
121 22
                $whereClause,
122
                $orderByPropertyIds
123 22
            );
124 22
        } else {
125 94
            if (!empty($selectPropertyIds)) {
126 1
                throw new CmisInvalidArgumentException(
127 1
                    'Manual statement cannot be used when properties are used',
128
                    1441286813
129 1
                );
130
            }
131 93
            if (!empty($fromTypes)) {
132 1
                throw new CmisInvalidArgumentException(
133 1
                    'Manual statement cannot be used when types are used',
134
                    1441286814
135 1
                );
136
            }
137 92
            if (!empty($whereClause)) {
138 1
                throw new CmisInvalidArgumentException(
139 1
                    'Manual statement cannot be used when clause is used',
140
                    1441286815
141 1
                );
142
            }
143 91
            if (!empty($orderByPropertyIds)) {
144 1
                throw new CmisInvalidArgumentException(
145 1
                    'Manual statement cannot be used when orderings are used',
146
                    1441286816
147 1
                );
148
            }
149
        }
150
151 112
        $this->statement = $statementString;
152 112
    }
153
154
    /**
155
     * Generates a statement based on input criteria, with the necessary
156
     * JOINs in place for selecting attributes related to all provided types.
157
     *
158
     * @param array $selectPropertyIds An array PropertyDefinitionInterface
159
     *      or strings, can be mixed. When strings are provided those can be
160
     *      either the actual ID of the property or the query name thereof.
161
     * @param array $fromTypes An array of TypeDefinitionInterface or strings,
162
     *      can be mixed. When strings are provided those can be either the
163
     *      actual ID of the type, or it can be the query name thereof. If
164
     *      an array of arrays is provided, each array is expected to contain
165
     *      a TypeDefinition or string as first member and an alias as second.
166
     * @param string|null $whereClause If searching by custom clause, provide here.
167
     * @param array $orderByPropertyIds List of property IDs by which to sort.
168
     *      Each value can be either a PropertyDefinitionInterface instance,
169
     *      a string (in which case, ID or queryName) or an array of a string
170
     *      or PropertyDefinition as first member and ASC or DESC as second.
171
     *      E.g. valid strings: "cm:title ASC", "cm:title", "P:cm:title".
172
     *      Valid arrays: [PropertyDefinitionInterface, "ASC"], ["cm:title", "ASC"]
173
     * @return string
174
     */
175 22
    protected function generateStatementFromPropertiesAndTypesLists(
176
        array $selectPropertyIds,
177
        array $fromTypes,
178
        $whereClause,
179
        array $orderByPropertyIds
180
    ) {
181 22
        $statementString = 'SELECT ' . $this->generateStatementPropertyList($selectPropertyIds, false);
182
183 22
        $primaryTable = array_shift($fromTypes);
184 22
        list ($primaryTableQueryName, $primaryAlias) = $this->getQueryNameAndAliasForType($primaryTable, 'primary');
185
186 22
        $statementString .= ' FROM ' . $primaryTableQueryName . ' ' . $primaryAlias;
187
188 22
        while (count($fromTypes) > 0) {
189 7
            $secondaryTable = array_shift($fromTypes);
190
            /*
191
             * we build an automatic alias here, a simple one-byte ASCII value
192
             * generated based on remaining tables. If 26 tables remain, a "z"
193
             * is generated. If 1 table remains, an "a" is generated. The alias
194
             * is, unfortunately, required for the JOIN to work correctly. It
195
             * only gets used if the type string does not contain an alias.
196
             */
197 7
            $alias = chr(97 + count($fromTypes));
198 7
            list ($secondaryTableQueryName, $alias) = $this->getQueryNameAndAliasForType($secondaryTable, $alias);
199 7
            $statementString .= ' JOIN ' . $secondaryTableQueryName . ' AS ' . $alias .
200 7
                ' ON ' . $primaryAlias . '.cmis:objectId = ' . $alias . '.cmis:objectId';
201 7
        }
202
203 22
        if (trim((string) $whereClause)) {
204 5
            $statementString .= ' WHERE ' . trim($whereClause);
205 5
        }
206
207 22
        if (!empty($orderByPropertyIds)) {
208 7
            $statementString .= ' ORDER BY ' . $this->generateStatementPropertyList($orderByPropertyIds, true);
209 7
        }
210 22
        return trim($statementString);
211
    }
212
213
    /**
214
     * Translates a TypeDefinition or string into a query name for
215
     * that TypeDefinition. Returns the input string as fallback if
216
     * the type could not be resolved. Input may contain an alias,
217
     * if so, we split and preserve the alias but attempt to translate
218
     * the type ID part.
219
     *
220
     * @param mixed $typeDefinitionMixed Input describing the type
221
     * @param string $autoAlias If alias is not provided
222
     * @return array
223
     */
224 23
    protected function getQueryNameAndAliasForType($typeDefinitionMixed, $autoAlias)
225
    {
226 23
        $alias = null;
227 23
        if (is_array($typeDefinitionMixed)) {
228 4
            list ($typeDefinitionMixed, $alias) = $typeDefinitionMixed;
229 4
        }
230 23
        if ($typeDefinitionMixed instanceof TypeDefinitionInterface) {
231 4
            $queryName = $typeDefinitionMixed->getQueryName();
232 23
        } elseif (is_string($typeDefinitionMixed) && strpos($typeDefinitionMixed, ' ')) {
233 1
            list ($typeDefinitionMixed, $alias) = explode(' ', $typeDefinitionMixed, 2);
234 1
        }
235
        try {
236 23
            $queryName = $this->session->getTypeDefinition($typeDefinitionMixed)->getQueryName();
237 23
        } catch (CmisObjectNotFoundException $error) {
238 1
            $queryName = $typeDefinitionMixed;
239
        }
240 23
        return array($queryName, ($alias ? $alias : $autoAlias));
241
    }
242
243
    /**
244
     * Renders a statement-compatible string of property selections,
245
     * with ordering support if $withOrdering is true. Input properties
246
     * can be an array of strings, an array of PropertyDefinition, or
247
     * when $withOrdering is true, an array of arrays each containing
248
     * a string or PropertyDefinition plus ASC or DESC as second value.
249
     *
250
     * @param array $properties
251
     * @param boolean $withOrdering
252
     * @return string
253
     */
254 22
    protected function generateStatementPropertyList(array $properties, $withOrdering)
255
    {
256 22
        $statement = array();
257 22
        foreach ($properties as $property) {
258 22
            $ordering = ($withOrdering ? 'ASC' : '');
259 22
            if ($withOrdering) {
260 7
                if (is_array($property)) {
261 1
                    list ($property, $ordering) = $property;
262 7
                } elseif (is_string($property) && strpos($property, ' ')) {
263 4
                    list ($property, $ordering) = explode(' ', $property, 2);
264 4
                }
265 7
            }
266 22
            if ($property instanceof PropertyDefinitionInterface) {
267 5
                $propertyQueryName = $property->getQueryName();
268 5
            } else {
269 20
                $propertyQueryName = $property;
270
            }
271 22
            $statement[] = rtrim($propertyQueryName . ' ' . $ordering);
272 22
        }
273 22
        return implode(', ', $statement);
274
    }
275
276
    /**
277
     * Executes the query.
278
     *
279
     * @param boolean $searchAllVersions <code>true</code> if all document versions should be included in the search
280
     *      results, <code>false</code> if only the latest document versions should be included in the search results
281
     * @param OperationContextInterface|null $context the operation context to use
282
     * @return QueryResultInterface[]
283
     */
284 1
    public function query($searchAllVersions, OperationContextInterface $context = null)
285
    {
286 1
        return $this->session->query($this->toQueryString(), $searchAllVersions, $context);
287
    }
288
289
    /**
290
     * Sets the designated parameter to the given boolean.
291
     *
292
     * @param integer $parameterIndex the parameter index (one-based)
293
     * @param boolean $bool the boolean
294
     */
295 3
    public function setBoolean($parameterIndex, $bool)
296
    {
297 3
        $this->setParameter($parameterIndex, $bool === true ? 'TRUE' : 'FALSE');
298 2
    }
299
300
    /**
301
     * Sets the designated parameter to the given DateTime value.
302
     *
303
     * @param integer $parameterIndex the parameter index (one-based)
304
     * @param \DateTime $dateTime the DateTime value as DateTime object
305
     */
306 3
    public function setDateTime($parameterIndex, \DateTime $dateTime)
307
    {
308 3
        $this->setParameter($parameterIndex, $dateTime->format(Constants::QUERY_DATETIMEFORMAT));
309 2
    }
310
311
    /**
312
     * Sets the designated parameter to the given DateTime value with the prefix 'TIMESTAMP '.
313
     *
314
     * @param integer $parameterIndex the parameter index (one-based)
315
     * @param \DateTime $dateTime the DateTime value as DateTime object
316
     */
317 3
    public function setDateTimeTimestamp($parameterIndex, \DateTime $dateTime)
318
    {
319 3
        $this->setParameter($parameterIndex, 'TIMESTAMP ' . $dateTime->format(Constants::QUERY_DATETIMEFORMAT));
320 2
    }
321
322
    /**
323
     * Sets the designated parameter to the given object ID.
324
     *
325
     * @param integer $parameterIndex the parameter index (one-based)
326
     * @param ObjectIdInterface $id the object ID
327
     */
328 6
    public function setId($parameterIndex, ObjectIdInterface $id)
329
    {
330 6
        $this->setParameter($parameterIndex, $this->escape($id->getId()));
331 5
    }
332
333
    /**
334
     * Sets the designated parameter to the given number.
335
     *
336
     * @param integer $parameterIndex the parameter index (one-based)
337
     * @param integer $number the value to be set as number
338
     * @throws CmisInvalidArgumentException If number not of type integer
339
     */
340 5
    public function setNumber($parameterIndex, $number)
341
    {
342 5
        if (!is_int($number)) {
343 1
            throw new CmisInvalidArgumentException('Number must be of type integer!');
344
        }
345
346 4
        $this->setParameter($parameterIndex, $number);
347 3
    }
348
349
    /**
350
     * Sets the designated parameter to the query name of the given property.
351
     *
352
     * @param integer $parameterIndex the parameter index (one-based)
353
     * @param PropertyDefinitionInterface $propertyDefinition
354
     * @throws CmisInvalidArgumentException If property has no query name
355
     */
356 4
    public function setProperty($parameterIndex, PropertyDefinitionInterface $propertyDefinition)
357
    {
358 4
        $queryName = $propertyDefinition->getQueryName();
359 4
        if (empty($queryName)) {
360 1
            throw new CmisInvalidArgumentException('Property has no query name!');
361
        }
362
363 3
        $this->setParameter($parameterIndex, $this->escape($queryName));
364 2
    }
365
366
    /**
367
     * Sets the designated parameter to the given string.
368
     *
369
     * @param integer $parameterIndex the parameter index (one-based)
370
     * @param string $string the string
371
     * @throws CmisInvalidArgumentException If given value is not a string
372
     */
373 8
    public function setString($parameterIndex, $string)
374
    {
375 8
        if (!is_string($string)) {
376 1
            throw new CmisInvalidArgumentException('Parameter string must be of type string!');
377
        }
378
379 7
        $this->setParameter($parameterIndex, $this->escape($string));
380 6
    }
381
382
    /**
383
     * Sets the designated parameter to the given string in a CMIS contains statement.
384
     *
385
     * Note that the CMIS specification requires two levels of escaping. The first level escapes ', ", \ characters
386
     * to \', \" and \\. The characters *, ? and - are interpreted as text search operators and are not escaped
387
     * on first level.
388
     * If *, ?, - shall be used as literals, they must be passed escaped with \*, \? and \- to this method.
389
     *
390
     * For all statements in a CONTAINS() clause it is required to isolate those from a query statement.
391
     * Therefore a second level escaping is performed. On the second level grammar ", ', - and \ are escaped with a \.
392
     * See the spec for further details.
393
     *
394
     * Summary (input --> first level escaping --> second level escaping and output):
395
     * * --> * --> *
396
     * ? --> ? --> ?
397
     * - --> - --> -
398
     * \ --> \\ --> \\\\
399
     * (for any other character following other than * ? -)
400
     * \* --> \* --> \\*
401
     * \? --> \? --> \\?
402
     * \- --> \- --> \\-
403
     * ' --> \' --> \\\'
404
     * " --> \" --> \\\"
405
     *
406
     * @param integer $parameterIndex the parameter index (one-based)
407
     * @param string $string the CONTAINS string
408
     * @throws CmisInvalidArgumentException If given value is not a string
409
     */
410 10
    public function setStringContains($parameterIndex, $string)
411
    {
412 10
        if (!is_string($string)) {
413 1
            throw new CmisInvalidArgumentException('Parameter string must be of type string!');
414
        }
415
416 9
        $this->setParameter($parameterIndex, $this->escapeContains($string));
417 8
    }
418
419
    /**
420
     * Sets the designated parameter to the given string.
421
     * It does not escape backslashes ('\') in front of '%' and '_'.
422
     *
423
     * @param integer $parameterIndex the parameter index (one-based)
424
     * @param $string
425
     * @throws CmisInvalidArgumentException If given value is not a string
426
     */
427 10
    public function setStringLike($parameterIndex, $string)
428
    {
429 10
        if (!is_string($string)) {
430 1
            throw new CmisInvalidArgumentException('Parameter string must be of type string!');
431
        }
432
433 9
        $this->setParameter($parameterIndex, $this->escapeLike($string));
434 8
    }
435
436
    /**
437
     * Sets the designated parameter to the query name of the given type.
438
     *
439
     * @param integer $parameterIndex the parameter index (one-based)
440
     * @param ObjectTypeInterface $type the object type
441
     */
442 3
    public function setType($parameterIndex, ObjectTypeInterface $type)
443
    {
444 3
        $this->setParameter($parameterIndex, $this->escape($type->getQueryName()));
445 2
    }
446
447
    /**
448
     * Sets the designated parameter to the given value
449
     *
450
     * @param integer $parameterIndex
451
     * @param mixed $value
452
     * @throws CmisInvalidArgumentException If parameter index is not of type integer
453
     */
454 49
    protected function setParameter($parameterIndex, $value)
455
    {
456 49
        if (!is_int($parameterIndex)) {
457 10
            throw new CmisInvalidArgumentException('Parameter index must be of type integer!');
458
        }
459
460 39
        $this->parametersMap[$parameterIndex] = $value;
461 39
    }
462
463
    /**
464
     * Returns the query statement.
465
     *
466
     * @return string the query statement, not null
467
     */
468 4
    public function toQueryString()
469
    {
470 4
        $queryString = '';
471 4
        $inString = false;
472 4
        $parameterIndex = 0;
473 4
        $length = strlen($this->statement);
474
475 4
        for ($i=0; $i < $length; $i++) {
476 4
            $char = $this->statement{$i};
477 4
            if ($char === '\'') {
478 1
                if ($inString && $this->statement{max(0, $i-1)} === '\\') {
479 1
                    $inString = true;
480 1
                } else {
481 1
                    $inString = !$inString;
482
                }
483 1
                $queryString .= $char;
484 4
            } elseif ($char === '?' && !$inString) {
485 3
                $parameterIndex ++;
486 3
                $queryString .= $this->parametersMap[$parameterIndex];
487 3
            } else {
488 4
                $queryString .= $char;
489
            }
490 4
        }
491
492 4
        return $queryString;
493
    }
494
495
    /**
496
     * Escapes string for query
497
     *
498
     * @param $string
499
     * @return string
500
     */
501 29
    protected function escape($string)
502
    {
503 29
        return "'" . addcslashes($string, '\'\\') . "'";
504
    }
505
506
    /**
507
     * Escapes string, but not escapes backslashes ('\') in front of '%' and '_'.
508
     *
509
     * @param $string
510
     * @return string
511
     */
512 19 View Code Duplication
    protected function escapeLike($string)
513
    {
514 19
        $escapedString = addcslashes($string, '\'\\');
515
        $replace = array(
516 19
            '\\\\%' => '\\%',
517 19
            '\\\\_' => '\\_',
518 19
        );
519 19
        $escapedString = str_replace(array_keys($replace), array_values($replace), $escapedString);
520 19
        return "'" . $escapedString . "'";
521
    }
522
523
    /**
524
     * Escapes string, but not escapes backslashes ('\') in front of '*' and '?'.
525
     *
526
     * @param $string
527
     * @return string
528
     */
529 20 View Code Duplication
    protected function escapeContains($string)
530
    {
531 20
        $escapedString = addcslashes($string, '"\'\\');
532
        $replace = array(
533 20
            '\\\\*' => '\*',
534 20
            '\\\\?' => '\?',
535 20
        );
536 20
        $escapedString = str_replace(array_keys($replace), array_values($replace), $escapedString);
537 20
        return "'" . $escapedString . "'";
538
    }
539
}
540