Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
21.15% covered (danger)
21.15%
11 / 52
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
DbStorage
21.15% covered (danger)
21.15%
11 / 52
28.57% covered (danger)
28.57%
2 / 7
384.33
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 preprocessNestedSqlArray
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 preprocessSqlArray
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFieldRegexFragment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateOptions
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
240
 validate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 calcUpdates
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
1<?php
2
3namespace Flow\Data\Storage;
4
5use Flow\Data\ObjectManager;
6use Flow\Data\ObjectStorage;
7use Flow\DbFactory;
8use Flow\Exception\DataModelException;
9use Flow\Model\UUID;
10
11/**
12 * Base class for all ObjectStorage implementers
13 * which use a database as the backing store.
14 *
15 * Includes some utility methods for database management and
16 * SQL security.
17 */
18abstract class DbStorage implements ObjectStorage {
19    /**
20     * @var DbFactory
21     */
22    protected $dbFactory;
23
24    /**
25     * The revision columns allowed to be updated
26     *
27     * @var string[]|true Allow of selective columns to allow, or true to allow
28     *   everything
29     */
30    protected $allowedUpdateColumns = true;
31
32    /**
33     * This is to prevent 'Update not allowed on xxx' error during moderation when
34     * * old cache is not purged and still holds obsolete deleted column
35     * * old cache is not purged and doesn't have the newly added column
36     *
37     * @var string[] Array of columns to ignore
38     */
39    protected $obsoleteUpdateColumns = [];
40
41    /**
42     * @param DbFactory $dbFactory
43     */
44    public function __construct( DbFactory $dbFactory ) {
45        $this->dbFactory = $dbFactory;
46    }
47
48    /**
49     * Runs preprocessSqlArray on each element of an array.
50     *
51     * @param array $outer The array to check
52     * @return array Preprocessed SQL array.
53     * @throws DataModelException
54     */
55    protected function preprocessNestedSqlArray( array $outer ) {
56        foreach ( $outer as $i => $data ) {
57            if ( !is_array( $data ) ) {
58                throw new DataModelException( "Unexpected non-array in nested SQL array" );
59            }
60            $outer[$i] = $this->preprocessSqlArray( $data );
61        }
62        return $outer;
63    }
64
65    /**
66     * Finds UUID objects and returns their database representation.
67     *
68     * @param array $data Query conditions for IDatabase::select
69     * @return array query conditions
70     */
71    protected function preprocessSqlArray( array $data ) {
72        $data = UUID::convertUUIDs( $data, 'binary' );
73
74        return $data;
75    }
76
77    /**
78     * Returns a regular expression fragment suitable for matching a valid
79     * SQL field name, and hopefully no injection attacks
80     * @return string Regular expression fragment
81     */
82    protected function getFieldRegexFragment() {
83        return '\s*[A-Za-z0-9\._]+\s*';
84    }
85
86    /**
87     * Internal security function to check an options array for
88     * SQL injection and other funkiness
89     * @todo Currently only supports LIMIT, OFFSET and ORDER BY
90     * @param array $options An options array passed to a query.
91     * @return bool
92     */
93    protected function validateOptions( $options ) {
94        static $validUnaryOptions = [
95            'UNIQUE',
96            'EXPLAIN',
97        ];
98
99        $fieldRegex = $this->getFieldRegexFragment();
100
101        foreach ( $options as $key => $value ) {
102            if ( is_numeric( $key ) && in_array( strtoupper( $value ), $validUnaryOptions ) ) {
103                continue;
104            } elseif ( is_numeric( $key ) ) {
105                wfDebug( __METHOD__ . ": Unrecognised unary operator $value\n" );
106                return false;
107            }
108
109            if ( $key === 'LIMIT' ) {
110                // LIMIT is one or two integers, separated by a comma.
111                if ( !preg_match( '/^\d+(,\d+)?$/', $value ) ) {
112                    wfDebug( __METHOD__ . ": Invalid LIMIT $value\n" );
113                    return false;
114                }
115            } elseif ( $key === 'ORDER BY' ) {
116                // ORDER BY is a list of field names with ASC / DESC afterwards
117                if ( is_string( $value ) ) {
118                    $value = explode( ',', $value );
119                }
120                $orderByRegex = "/^\s*$fieldRegex\s*(ASC|DESC)?\s*$/i";
121
122                foreach ( $value as $orderByField ) {
123                    if ( !preg_match( $orderByRegex, $orderByField ) ) {
124                        wfDebug( __METHOD__ . ": invalid ORDER BY field $orderByField\n" );
125                        return false;
126                    }
127                }
128            } elseif ( $key === 'OFFSET' ) {
129                // OFFSET is just an integer
130                if ( !is_numeric( $value ) ) {
131                    wfDebug( __METHOD__ . ": non-numeric offset $value\n" );
132                    return false;
133                }
134            } elseif ( $key === 'GROUP BY' ) {
135                if ( !preg_match( "/^{$fieldRegex}(,{$fieldRegex})+$/", $value ) ) {
136                    wfDebug( __METHOD__ . ": invalid GROUP BY field\n" );
137                }
138            } else {
139                wfDebug( __METHOD__ . ": Unknown option $key\n" );
140                return false;
141            }
142        }
143
144        // Everything passes
145        return true;
146    }
147
148    /**
149     * @inheritDoc
150     */
151    public function validate( array $row ) {
152        return true;
153    }
154
155    /**
156     * Calculates the DB updates to be performed to update data from $old to
157     * $new.
158     *
159     * @param array $old
160     * @param array $new
161     * @return array
162     * @throws DataModelException
163     */
164    public function calcUpdates( array $old, array $new ) {
165        $changeSet = ObjectManager::calcUpdatesWithoutValidation( $old, $new );
166
167        foreach ( $this->obsoleteUpdateColumns as $val ) {
168            // Need to use array_key_exists to check null value
169            if ( array_key_exists( $val, $changeSet ) ) {
170                unset( $changeSet[$val] );
171            }
172        }
173
174        if ( is_array( $this->allowedUpdateColumns ) ) {
175            $extra = array_diff( array_keys( $changeSet ), $this->allowedUpdateColumns );
176            if ( $extra ) {
177                throw new DataModelException( 'Update not allowed on: ' . implode( ', ', $extra ), 'process-data' );
178            }
179        }
180
181        return $changeSet;
182    }
183}