PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / DataAccess / LogQueryBuilder / JoinTables.php
matomo / app / core / DataAccess / LogQueryBuilder Last commit date
JoinGenerator.php 1 year ago JoinTables.php 1 year ago
JoinTables.php
273 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\DataAccess\LogQueryBuilder;
10
11 use Exception;
12 use Piwik\DataAccess\LogAggregator;
13 use Piwik\Plugin\LogTablesProvider;
14 class JoinTables extends \ArrayObject
15 {
16 /**
17 * @var LogTablesProvider
18 */
19 private $logTableProvider;
20 // NOTE: joins can be specified explicitly as arrays w/ 'joinOn' keys or implicitly as table names. when
21 // table names are used, the joins dependencies are assumed based on how we want to order those joins.
22 // the below table list the possible dependencies of each table, and is specifically designed to enforce
23 // the following order:
24 // log_link_visit_action, log_action, log_visit, log_conversion, log_conversion_item
25 // which means if an array is supplied where log_visit comes before log_link_visitAction, it will
26 // be moved to after it.
27 private $implicitTableDependencies = ['log_link_visit_action' => [], 'log_action' => ['log_link_visit_action', 'log_conversion', 'log_conversion_item', 'log_visit'], 'log_visit' => ['log_link_visit_action', 'log_action'], 'log_conversion' => ['log_link_visit_action', 'log_action', 'log_visit'], 'log_conversion_item' => ['log_link_visit_action', 'log_action', 'log_visit', 'log_conversion']];
28 /**
29 * Tables constructor.
30 * @param LogTablesProvider $logTablesProvider
31 * @param array $tables
32 */
33 public function __construct(LogTablesProvider $logTablesProvider, $tables)
34 {
35 $this->logTableProvider = $logTablesProvider;
36 foreach ($tables as $table) {
37 $this->checkTableCanBeUsedForSegmentation($table);
38 }
39 $this->exchangeArray(array_values($tables));
40 }
41 public function getTables()
42 {
43 return $this->getArrayCopy();
44 }
45 public function addTableToJoin($tableName)
46 {
47 $this->checkTableCanBeUsedForSegmentation($tableName);
48 $this->append($tableName);
49 }
50 public function hasJoinedTable($tableName)
51 {
52 $tables = in_array($tableName, $this->getTables());
53 if ($tables) {
54 return \true;
55 }
56 foreach ($this as $table) {
57 if (is_array($table)) {
58 if (!isset($table['tableAlias']) && $table['table'] === $table) {
59 return \true;
60 } elseif (isset($table['tableAlias']) && $table['tableAlias'] === $table) {
61 return \true;
62 }
63 }
64 }
65 return \false;
66 }
67 public function hasJoinedTableManually($tableToFind, $joinToFind)
68 {
69 foreach ($this as $table) {
70 if (is_array($table) && !empty($table['table']) && $table['table'] === $tableToFind && (!isset($table['tableAlias']) || $table['tableAlias'] === $tableToFind) && (!isset($table['join']) || strtolower($table['join']) === 'left join') && isset($table['joinOn']) && $table['joinOn'] === $joinToFind) {
71 return \true;
72 }
73 }
74 return \false;
75 }
76 public function getLogTable($tableName)
77 {
78 return $this->logTableProvider->getLogTable($tableName);
79 }
80 public function findIndexOfManuallyAddedTable($tableNameToFind)
81 {
82 foreach ($this as $index => $table) {
83 if (is_array($table) && !empty($table['table']) && $table['table'] === $tableNameToFind && (!isset($table['join']) || strtolower($table['join']) === 'left join') && (!isset($table['tableAlias']) || $table['tableAlias'] === $tableNameToFind)) {
84 return $index;
85 }
86 }
87 }
88 public function hasAddedTableManually($tableToFind)
89 {
90 $table = $this->findIndexOfManuallyAddedTable($tableToFind);
91 return isset($table);
92 }
93 public function sort()
94 {
95 // we do not use $this->uasort as we do not want to maintain keys
96 $tables = $this->getTables();
97 // the first entry is always the FROM table
98 $firstTable = array_shift($tables);
99 $sorted = [$firstTable];
100 // With the ability to specify which index to use, the $firstTable var may be an array
101 if (is_array($firstTable) && !empty($firstTable['table'])) {
102 $firstTable = $firstTable['table'];
103 }
104 if (strpos($firstTable, LogAggregator::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX) === 0) {
105 // the first table might be a temporary segment table in which case we need to keep the next one as well
106 $sorted[] = array_shift($tables);
107 }
108 $dependencies = $this->parseDependencies($tables);
109 $this->visitTableListDfs($tables, $dependencies, function ($tableInfo) use(&$sorted) {
110 $sorted[] = $tableInfo;
111 });
112 $this->exchangeArray($sorted);
113 }
114 public function isTableJoinableOnVisit($tableToCheck)
115 {
116 $table = $this->getLogTable($tableToCheck);
117 if (empty($table)) {
118 return \false;
119 }
120 if ($table->getColumnToJoinOnIdVisit()) {
121 return \true;
122 }
123 if ($table->getLinkTableToBeAbleToJoinOnVisit()) {
124 return \true;
125 }
126 $otherWays = $table->getWaysToJoinToOtherLogTables();
127 if (empty($otherWays)) {
128 return \false;
129 }
130 foreach ($otherWays as $logTable => $column) {
131 if ($logTable == 'log_visit' || $this->isTableJoinableOnVisit($logTable)) {
132 return \true;
133 }
134 }
135 return \false;
136 }
137 public function isTableJoinableOnAction($tableToCheck)
138 {
139 $table = $this->getLogTable($tableToCheck);
140 if (empty($table)) {
141 return \false;
142 }
143 if ($table->getColumnToJoinOnIdAction()) {
144 return \true;
145 }
146 $otherWays = $table->getWaysToJoinToOtherLogTables();
147 if (empty($otherWays)) {
148 return \false;
149 }
150 foreach ($otherWays as $logTable => $column) {
151 if ($logTable == 'log_action' || $this->isTableJoinableOnAction($logTable)) {
152 return \true;
153 }
154 }
155 return \false;
156 }
157 public function addTableDependency($table, $dependentTable)
158 {
159 if (!empty($this->implicitTableDependencies[$table])) {
160 return;
161 }
162 $this->implicitTableDependencies[$table] = [$dependentTable];
163 }
164 private function checkTableCanBeUsedForSegmentation($tableName)
165 {
166 if (!is_array($tableName) && !$this->getLogTable($tableName)) {
167 throw new Exception("Table '{$tableName}' can't be used for segmentation");
168 }
169 }
170 private function parseDependencies(array $tables)
171 {
172 $dependencies = [];
173 foreach ($tables as $key => &$fromInfo) {
174 if (is_string($fromInfo)) {
175 $dependencies[$key] = $this->assumeImplicitJoinDependencies($tables, $fromInfo);
176 continue;
177 }
178 if (empty($fromInfo['joinOn'])) {
179 continue;
180 }
181 $table = isset($fromInfo['tableAlias']) ? $fromInfo['tableAlias'] : $fromInfo['table'];
182 $tablesInExpr = $this->parseSqlTables($fromInfo['joinOn'], $table);
183 $dependencies[$key] = $tablesInExpr;
184 }
185 return $dependencies;
186 }
187 private function assumeImplicitJoinDependencies($allTablesToQuery, $table)
188 {
189 $implicitTableDependencies = $this->implicitTableDependencies;
190 $result = [];
191 if (isset($implicitTableDependencies[$table])) {
192 $result = $implicitTableDependencies[$table];
193 // only include dependencies that are in the list of requested tables (ie, if we want to
194 // query from log_conversion joining on log_link_visit_action, we don't want to add log_visit
195 // to the sql statement)
196 $result = array_filter($result, function ($table) use($allTablesToQuery) {
197 return $this->isInTableArray($allTablesToQuery, $table);
198 });
199 }
200 return $result;
201 }
202 private function isInTableArray($tables, $table)
203 {
204 foreach ($tables as $entry) {
205 if (is_string($entry) && $entry == $table) {
206 return \true;
207 }
208 if (is_array($entry) && $entry['table'] == $table) {
209 return \true;
210 }
211 }
212 return \false;
213 }
214 private function parseSqlTables($joinOn, $self)
215 {
216 preg_match_all('/\\b([a-zA-Z0-9_`]+)\\.[a-zA-Z0-9_`]+\\b/', $joinOn, $matches);
217 $tables = [];
218 foreach ($matches[1] as $table) {
219 if ($table === $self) {
220 continue;
221 }
222 $tables[] = $table;
223 }
224 return $tables;
225 }
226 private function visitTableListDfs($tables, $dependencies, $visitor)
227 {
228 $visited = [];
229 foreach ($tables as $index => $tableInfo) {
230 if (empty($visited[$index])) {
231 $this->visitTableListDfsSingle($tables, $dependencies, $visitor, $index, $visited);
232 }
233 }
234 }
235 private function visitTableListDfsSingle($tables, $dependencies, $visitor, $tableToVisitIndex, &$visited)
236 {
237 $visited[$tableToVisitIndex] = \true;
238 $tableToVisit = $tables[$tableToVisitIndex];
239 if (!empty($dependencies[$tableToVisitIndex])) {
240 foreach ($dependencies[$tableToVisitIndex] as $dependencyTableName) {
241 $dependentTableToVisit = $this->findTableIndex($tables, $dependencyTableName);
242 if ($dependentTableToVisit === null) {
243 // sanity check, in case the dependent table is not in the list of tables to query
244 continue;
245 }
246 if (!empty($visited[$dependentTableToVisit])) {
247 // skip if already visited
248 continue;
249 }
250 // visit dependent table...
251 $this->visitTableListDfsSingle($tables, $dependencies, $visitor, $dependentTableToVisit, $visited);
252 }
253 }
254 // ...then visit current table
255 $visitor($tableToVisit);
256 }
257 private function findTableIndex($tables, $tableToSearchFor)
258 {
259 foreach ($tables as $key => $info) {
260 $tableName = null;
261 if (is_string($info)) {
262 $tableName = $info;
263 } elseif (is_array($info)) {
264 $tableName = isset($info['tableAlias']) ? $info['tableAlias'] : $info['table'];
265 }
266 if ($tableName == $tableToSearchFor) {
267 return $key;
268 }
269 }
270 return null;
271 }
272 }
273