PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.3.1
5.13.0 5.12.1 5.12.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 / ArchiveProcessor / Loader.php
matomo / app / core / ArchiveProcessor Last commit date
ArchivingStatus.php 2 years ago Loader.php 1 year ago LoaderLock.php 1 year ago Parameters.php 1 year ago PluginsArchiver.php 1 year ago PluginsArchiverException.php 2 years ago Record.php 1 year ago RecordBuilder.php 1 year ago Rules.php 1 year ago
Loader.php
490 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\ArchiveProcessor;
10
11 use Piwik\Archive\ArchiveInvalidator;
12 use Piwik\ArchiveProcessor;
13 use Piwik\Cache;
14 use Piwik\Common;
15 use Piwik\Config;
16 use Piwik\Container\StaticContainer;
17 use Piwik\Context;
18 use Piwik\DataAccess\ArchiveSelector;
19 use Piwik\DataAccess\ArchiveWriter;
20 use Piwik\DataAccess\Model;
21 use Piwik\DataAccess\RawLogDao;
22 use Piwik\Date;
23 use Piwik\Period;
24 use Piwik\Piwik;
25 use Piwik\SettingsServer;
26 use Piwik\Site;
27 use Piwik\Log\LoggerInterface;
28 use Piwik\CronArchive\SegmentArchiving;
29 /**
30 * This class uses PluginsArchiver class to trigger data aggregation and create archives.
31 */
32 class Loader
33 {
34 private static $archivingDepth = 0;
35 /**
36 * @var Parameters
37 */
38 protected $params;
39 /**
40 * @var ArchiveInvalidator
41 */
42 private $invalidator;
43 /**
44 * @var \Matomo\Cache\Cache
45 */
46 private $cache;
47 /**
48 * @var LoggerInterface
49 */
50 private $logger;
51 /**
52 * @var RawLogDao
53 */
54 private $rawLogDao;
55 /**
56 * @var Model
57 */
58 private $dataAccessModel;
59 /**
60 * @var bool
61 */
62 private $invalidateBeforeArchiving;
63 public function __construct(\Piwik\ArchiveProcessor\Parameters $params, $invalidateBeforeArchiving = \false)
64 {
65 $this->params = $params;
66 $this->invalidateBeforeArchiving = $invalidateBeforeArchiving;
67 $this->invalidator = StaticContainer::get(ArchiveInvalidator::class);
68 $this->cache = Cache::getTransientCache();
69 $this->logger = StaticContainer::get(LoggerInterface::class);
70 $this->rawLogDao = new RawLogDao();
71 $this->dataAccessModel = new Model();
72 }
73 /**
74 * @return bool
75 */
76 protected function isThereSomeVisits($visits)
77 {
78 return $visits > 0;
79 }
80 /**
81 * @return bool
82 */
83 protected function mustProcessVisitCount($visits)
84 {
85 return $visits === \false;
86 }
87 public function prepareArchive($pluginName)
88 {
89 return Context::changeIdSite($this->params->getSite()->getId(), function () use($pluginName) {
90 try {
91 ++self::$archivingDepth;
92 return $this->prepareArchiveImpl($pluginName);
93 } finally {
94 --self::$archivingDepth;
95 }
96 });
97 }
98 /**
99 * @throws \Exception
100 */
101 private function prepareArchiveImpl($pluginName)
102 {
103 $this->params->setRequestedPlugin($pluginName);
104 if (SettingsServer::isArchivePhpTriggered()) {
105 $requestedReport = Common::getRequestVar('requestedReport', '', 'string');
106 if (!empty($requestedReport)) {
107 $this->params->setArchiveOnlyReport($requestedReport);
108 }
109 }
110 // invalidate existing archives before we start archiving in case data was tracked in the past. if the archive is
111 // made invalid, we will correctly re-archive below.
112 if ($this->invalidateBeforeArchiving && \Piwik\ArchiveProcessor\Rules::isBrowserTriggerEnabled()) {
113 $this->invalidatedReportsIfNeeded();
114 }
115 // load existing data from archive
116 $data = $this->loadArchiveData();
117 if (sizeof($data) == 2) {
118 return $data;
119 }
120 list($idArchives, $visits, $visitsConverted, $foundRecords) = $data;
121 // only lock meet those conditions
122 if (ArchiveProcessor::$isRootArchivingRequest && !SettingsServer::isArchivePhpTriggered()) {
123 $lockId = $this->makeArchivingLockId();
124 //ini lock
125 $lock = new \Piwik\ArchiveProcessor\LoaderLock($lockId);
126 //set mysql lock the entire process if another process is running
127 $lock->setLock();
128 try {
129 $data = $this->loadArchiveData();
130 if (sizeof($data) == 2) {
131 return $data;
132 }
133 list($idArchives, $visits, $visitsConverted, $foundRecords) = $data;
134 return $this->insertArchiveData($visits, $visitsConverted, $idArchives, $foundRecords);
135 } finally {
136 $lock->unlock();
137 }
138 } else {
139 return $this->insertArchiveData($visits, $visitsConverted, $idArchives, $foundRecords);
140 }
141 }
142 /**
143 * @param $visits
144 * @param $visitsConverted
145 * @return array|false[]
146 */
147 protected function insertArchiveData($visits, $visitsConverted, $existingArchives, $foundRecords)
148 {
149 if (SettingsServer::isArchivePhpTriggered()) {
150 $this->logger->info("initiating archiving via core:archive for " . $this->params);
151 }
152 if (!empty($foundRecords)) {
153 $this->params->setFoundRequestedReports($foundRecords);
154 }
155 list($visits, $visitsConverted) = $this->prepareCoreMetricsArchive($visits, $visitsConverted);
156 list($idArchive, $visits) = $this->prepareAllPluginsArchive($visits, $visitsConverted);
157 if ($this->isThereSomeVisits($visits) || \Piwik\ArchiveProcessor\PluginsArchiver::doesAnyPluginArchiveWithoutVisits()) {
158 $idArchivesToQuery = [$idArchive];
159 if (!empty($foundRecords)) {
160 $idArchivesToQuery = array_merge($idArchivesToQuery, $existingArchives ?: []);
161 }
162 return [$idArchivesToQuery, $visits];
163 }
164 return [\false, \false];
165 }
166 /**
167 * @return string
168 * @throws \Exception
169 */
170 private function makeArchivingLockId()
171 {
172 $doneFlag = \Piwik\ArchiveProcessor\Rules::getDoneStringFlagFor([$this->params->getSite()->getId()], $this->params->getSegment(), $this->params->getPeriod()->getLabel(), $this->params->getRequestedPlugin());
173 return $this->params->getPeriod()->getDateStart()->toString() . $this->params->getPeriod()->getDateEnd()->toString() . '.' . $doneFlag;
174 }
175 /**
176 * @return array|false[]
177 */
178 protected function loadArchiveData()
179 {
180 // this hack was used to check the main function goes to return or continue
181 // NOTE: $idArchives will contain the latest DONE_OK/DONE_INVALIDATED archive as well as any partial archives
182 // with a ts_archived >= the DONE_OK/DONE_INVALIDATED date.
183 $archiveInfo = $this->loadExistingArchiveIdFromDb();
184 $idArchives = $archiveInfo['idArchives'];
185 $visits = $archiveInfo['visits'];
186 $visitsConverted = $archiveInfo['visitsConverted'];
187 $tsArchived = $archiveInfo['tsArchived'];
188 $doneFlagValue = $archiveInfo['doneFlagValue'];
189 $existingArchives = $archiveInfo['existingRecords'];
190 $requestedRecords = $this->params->getArchiveOnlyReportAsArray();
191 $isMissingRequestedRecords = !empty($requestedRecords) && is_array($existingArchives) && count($requestedRecords) != count($existingArchives);
192 if (!empty($idArchives) && !\Piwik\ArchiveProcessor\Rules::isActuallyForceArchivingSinglePlugin() && !$this->shouldForceInvalidatedArchive($doneFlagValue, $tsArchived) && !$isMissingRequestedRecords) {
193 // we have a usable idarchive (it's not invalidated and it's new enough), and we are not archiving
194 // a single report
195 return [$idArchives, $visits];
196 }
197 // NOTE: this optimization helps when archiving large periods. eg, if archiving a year w/ a segment where
198 // there are not visits in the entire year, we don't have to go through and do anything. but, w/o this
199 // code, we will end up launching archiving for each month, week and day, even though we don't have to.
200 //
201 // we don't create an archive in this case, because the archive may be in progress in some way, so a 0
202 // visits archive can be inaccurate in the long run.
203 if ($this->canSkipThisArchive()) {
204 if (!empty($idArchives)) {
205 return [$idArchives, $visits];
206 } else {
207 return [\false, 0];
208 }
209 }
210 return [$idArchives, $visits, $visitsConverted, $existingArchives];
211 }
212 /**
213 * Prepares the core metrics if needed.
214 *
215 * @param $visits
216 * @return array
217 */
218 protected function prepareCoreMetricsArchive($visits, $visitsConverted)
219 {
220 $createSeparateArchiveForCoreMetrics = $this->mustProcessVisitCount($visits) && !$this->doesRequestedPluginIncludeVisitsSummary();
221 if ($createSeparateArchiveForCoreMetrics) {
222 $requestedPlugin = $this->params->getRequestedPlugin();
223 $requestedReport = $this->params->getArchiveOnlyReport();
224 $isPartialArchive = $this->params->isPartialArchive();
225 $this->params->setRequestedPlugin('VisitsSummary');
226 $this->params->setArchiveOnlyReport(null);
227 $this->params->setIsPartialArchive(\false);
228 $metrics = Context::executeWithQueryParameters(['requestedReport' => ''], function () {
229 $pluginsArchiver = new \Piwik\ArchiveProcessor\PluginsArchiver($this->params);
230 $metrics = $pluginsArchiver->callAggregateCoreMetrics();
231 $pluginsArchiver->finalizeArchive();
232 return $metrics;
233 });
234 $this->params->setRequestedPlugin($requestedPlugin);
235 $this->params->setArchiveOnlyReport($requestedReport);
236 $this->params->setIsPartialArchive($isPartialArchive);
237 $visits = $metrics['nb_visits'];
238 $visitsConverted = $metrics['nb_visits_converted'];
239 }
240 return array($visits, $visitsConverted);
241 }
242 protected function prepareAllPluginsArchive($visits, $visitsConverted)
243 {
244 $pluginsArchiver = new \Piwik\ArchiveProcessor\PluginsArchiver($this->params);
245 if ($this->mustProcessVisitCount($visits) || $this->doesRequestedPluginIncludeVisitsSummary()) {
246 $metrics = $pluginsArchiver->callAggregateCoreMetrics();
247 $visits = $metrics['nb_visits'];
248 $visitsConverted = $metrics['nb_visits_converted'];
249 }
250 $forceArchivingWithoutVisits = !$this->isThereSomeVisits($visits) && $this->shouldArchiveForSiteEvenWhenNoVisits();
251 $pluginsArchiver->callAggregateAllPlugins($visits, $visitsConverted, $forceArchivingWithoutVisits);
252 $idArchive = $pluginsArchiver->finalizeArchive();
253 return array($idArchive, $visits);
254 }
255 protected function doesRequestedPluginIncludeVisitsSummary()
256 {
257 $processAllReportsIncludingVisitsSummary = \Piwik\ArchiveProcessor\Rules::shouldProcessReportsAllPlugins(array($this->params->getSite()->getId()), $this->params->getSegment(), $this->params->getPeriod()->getLabel());
258 $doesRequestedPluginIncludeVisitsSummary = $processAllReportsIncludingVisitsSummary || $this->params->getRequestedPlugin() == 'VisitsSummary';
259 return $doesRequestedPluginIncludeVisitsSummary;
260 }
261 protected function isArchivingForcedToTrigger()
262 {
263 $period = $this->params->getPeriod()->getLabel();
264 $debugSetting = 'always_archive_data_period';
265 // default
266 if ($period == 'day') {
267 $debugSetting = 'always_archive_data_day';
268 } elseif ($period == 'range') {
269 $debugSetting = 'always_archive_data_range';
270 }
271 return (bool) Config::getInstance()->Debug[$debugSetting];
272 }
273 /**
274 * Returns the idArchive if the archive is available in the database for the requested plugin.
275 * Returns false if the archive needs to be processed.
276 *
277 * (public for tests)
278 *
279 * @return array
280 */
281 public function loadExistingArchiveIdFromDb()
282 {
283 if ($this->isArchivingForcedToTrigger()) {
284 $this->logger->debug("Archiving forced to trigger for {$this->params}.");
285 // return no usable archive found, and no existing archive. this will skip invalidation, which should
286 // be fine since we just force archiving.
287 return ['idArchives' => \false, 'visits' => \false, 'visitsConverted' => \false, 'archiveExists' => \false, 'tsArchived' => \false, 'doneFlagValue' => \false, 'existingRecords' => null];
288 }
289 $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed();
290 $result = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC);
291 return $result;
292 }
293 /**
294 * Returns the minimum archive processed datetime to look at. Only public for tests.
295 *
296 * @return int|bool Datetime timestamp, or false if must look at any archive available
297 */
298 protected function getMinTimeArchiveProcessed()
299 {
300 // for range periods we can archive in a browser request request, make sure to check for the ttl no matter what
301 $isRangeArchiveAndArchivingEnabled = $this->params->getPeriod()->getLabel() == 'range' && \Piwik\ArchiveProcessor\Rules::isArchivingEnabledFor([$this->params->getSite()->getId()], $this->params->getSegment(), $this->params->getPeriod()->getLabel());
302 if (!$isRangeArchiveAndArchivingEnabled) {
303 $endDateTimestamp = self::determineIfArchivePermanent($this->params->getDateEnd());
304 if ($endDateTimestamp) {
305 // past archive
306 return $endDateTimestamp;
307 }
308 }
309 $dateStart = $this->params->getDateStart();
310 $period = $this->params->getPeriod();
311 $segment = $this->params->getSegment();
312 $site = $this->params->getSite();
313 // in-progress archive
314 return \Piwik\ArchiveProcessor\Rules::getMinTimeProcessedForInProgressArchive($dateStart, $period, $segment, $site);
315 }
316 protected static function determineIfArchivePermanent(Date $dateEnd)
317 {
318 $now = time();
319 $endTimestampUTC = strtotime($dateEnd->getDateEndUTC());
320 if ($endTimestampUTC <= $now) {
321 // - if the period we are looking for is finished, we look for a ts_archived that
322 // is greater than the last day of the archive
323 return $endTimestampUTC;
324 }
325 return \false;
326 }
327 private function shouldArchiveForSiteEvenWhenNoVisits()
328 {
329 $idSitesToArchive = $this->getIdSitesToArchiveWhenNoVisits();
330 return in_array($this->params->getSite()->getId(), $idSitesToArchive);
331 }
332 private function getIdSitesToArchiveWhenNoVisits()
333 {
334 $cache = Cache::getTransientCache();
335 $cacheKey = 'Archiving.getIdSitesToArchiveWhenNoVisits';
336 if (!$cache->contains($cacheKey)) {
337 $idSites = array();
338 // leaving undocumented unless decided otherwise
339 Piwik::postEvent('Archiving.getIdSitesToArchiveWhenNoVisits', array(&$idSites));
340 $cache->save($cacheKey, $idSites);
341 }
342 return $cache->fetch($cacheKey);
343 }
344 // public for tests
345 public function getReportsToInvalidate()
346 {
347 $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
348 foreach ($sitesPerDays as $dateStr => $siteIds) {
349 if (empty($siteIds) || !in_array($this->params->getSite()->getId(), $siteIds)) {
350 unset($sitesPerDays[$dateStr]);
351 }
352 $date = Date::factory($dateStr);
353 if ($date->isEarlier($this->params->getPeriod()->getDateStart()) || $date->isLater($this->params->getPeriod()->getDateEnd())) {
354 // date in list is not the current date, so ignore it
355 unset($sitesPerDays[$dateStr]);
356 }
357 }
358 return $sitesPerDays;
359 }
360 private function invalidatedReportsIfNeeded()
361 {
362 $sitesPerDays = $this->getReportsToInvalidate();
363 if (empty($sitesPerDays)) {
364 return;
365 }
366 foreach ($sitesPerDays as $date => $siteIds) {
367 try {
368 $this->invalidator->markArchivesAsInvalidated([$this->params->getSite()->getId()], array(Date::factory($date)), \false, $this->params->getSegment());
369 } catch (\Exception $e) {
370 Site::clearCache();
371 throw $e;
372 }
373 }
374 Site::clearCache();
375 }
376 public function canSkipThisArchive()
377 {
378 $params = $this->params;
379 $idSite = $params->getSite()->getId();
380 $isWebsiteUsingTracker = $this->isWebsiteUsingTheTracker($idSite);
381 $isArchivingForcedWhenNoVisits = $this->shouldArchiveForSiteEvenWhenNoVisits();
382 $hasSiteVisitsBetweenTimeframe = $this->hasSiteVisitsBetweenTimeframe($idSite, $params->getPeriod());
383 $hasChildArchivesInPeriod = $this->dataAccessModel->hasChildArchivesInPeriod($idSite, $params->getPeriod());
384 if ($this->canSkipArchiveForSegment()) {
385 return \true;
386 }
387 return $isWebsiteUsingTracker && !$isArchivingForcedWhenNoVisits && !$hasSiteVisitsBetweenTimeframe && !$hasChildArchivesInPeriod;
388 }
389 public function canSkipArchiveForSegment()
390 {
391 $params = $this->params;
392 if ($params->getSegment()->isEmpty()) {
393 return \false;
394 }
395 if (!empty($params->getRequestedPlugin()) && \Piwik\ArchiveProcessor\Rules::isSegmentPluginArchivingDisabled($params->getRequestedPlugin(), $params->getSite()->getId())) {
396 return \true;
397 }
398 // For better understanding of the next check please have a look at Rules::shouldProcessReportsAllPlugins implementation
399 // and what conditions it returns false on. For our use here, we need to ensure that:
400 // - we are not running CLI archiving
401 // - we are not dealing with a range period
402 // - we don't have an empty segment
403 // - we don't have a segment that should be preprocessed
404 // - we are not forcing a single plugin archiving
405 if (!\Piwik\ArchiveProcessor\Rules::shouldProcessReportsAllPlugins($params->getIdSites(), $params->getSegment(), $params->getPeriod()->getLabel())) {
406 return \false;
407 }
408 /** @var SegmentArchiving */
409 $segmentArchiving = StaticContainer::get(SegmentArchiving::class);
410 $segmentInfo = $segmentArchiving->findSegmentForHash($params->getSegment()->getHash(), $params->getSite()->getId());
411 if (!$segmentInfo) {
412 return \false;
413 }
414 $segmentArchiveStartDate = $segmentArchiving->getReArchiveSegmentStartDate($segmentInfo);
415 if ($segmentArchiveStartDate !== null && $segmentArchiveStartDate->isLater($params->getPeriod()->getDateEnd()->getEndOfDay())) {
416 $doneFlag = \Piwik\ArchiveProcessor\Rules::getDoneStringFlagFor([$params->getSite()->getId()], $params->getSegment(), $params->getPeriod()->getLabel(), $params->getRequestedPlugin());
417 // if there is no invalidation where the report is null, we can skip
418 // if we have invalidations for the period and name, but only for a specific reports, we can skip
419 // if the report is not null we only want to rearchive if we have invalidation for that report
420 // if we don't find invalidation for that report, we can skip
421 return !$this->dataAccessModel->hasInvalidationForPeriodAndName($params->getSite()->getId(), $params->getPeriod(), $doneFlag, $params->getArchiveOnlyReport());
422 }
423 return \false;
424 }
425 private function isWebsiteUsingTheTracker($idSite)
426 {
427 $idSitesNotUsingTracker = self::getSitesNotUsingTracker();
428 $isUsingTracker = !in_array($idSite, $idSitesNotUsingTracker);
429 return $isUsingTracker;
430 }
431 public static function getSitesNotUsingTracker()
432 {
433 $cache = Cache::getTransientCache();
434 $cacheKey = 'Archiving.isWebsiteUsingTheTracker';
435 $idSitesNotUsingTracker = $cache->fetch($cacheKey);
436 if ($idSitesNotUsingTracker === \false || !isset($idSitesNotUsingTracker)) {
437 // we want to trigger event only once
438 $idSitesNotUsingTracker = array();
439 /**
440 * This event is triggered when detecting whether there are sites that do not use the tracker.
441 *
442 * By default we only archive a site when there was actually any visit since the last archiving.
443 * However, some plugins do import data from another source instead of using the tracker and therefore
444 * will never have any visits for this site. To make sure we still archive data for such a site when
445 * archiving for this site is requested, you can listen to this event and add the idSite to the list of
446 * sites that do not use the tracker.
447 *
448 * @param bool $idSitesNotUsingTracker The list of idSites that rather import data instead of using the tracker
449 */
450 Piwik::postEvent('CronArchive.getIdSitesNotUsingTracker', array(&$idSitesNotUsingTracker));
451 $cache->save($cacheKey, $idSitesNotUsingTracker);
452 }
453 return $idSitesNotUsingTracker;
454 }
455 private function hasSiteVisitsBetweenTimeframe($idSite, Period $period)
456 {
457 $timezone = Site::getTimezoneFor($idSite);
458 list($date1, $date2) = $period->getBoundsInTimezone($timezone);
459 return $this->rawLogDao->hasSiteVisitsBetweenTimeframe($date1->getDatetime(), $date2->getDatetime(), $idSite);
460 }
461 public static function getArchivingDepth()
462 {
463 return self::$archivingDepth;
464 }
465 private function shouldForceInvalidatedArchive($value, $tsArchived)
466 {
467 $params = $this->params;
468 // the archive is invalidated and we are in a browser request that is allowed archive it
469 if ($value == ArchiveWriter::DONE_INVALIDATED && \Piwik\ArchiveProcessor\Rules::isArchivingEnabledFor([$params->getSite()->getId()], $params->getSegment(), $params->getPeriod()->getLabel())) {
470 // if coming from core:archive, force rearchiving, since if we don't the entry will be removed from archive_invalidations
471 // w/o being rearchived
472 if (SettingsServer::isArchivePhpTriggered()) {
473 return \true;
474 }
475 // if coming from a browser request, and period does not contain today, force rearchiving
476 $timezone = $params->getSite()->getTimezone();
477 if (!$params->getPeriod()->isDateInPeriod(Date::factoryInTimezone('today', $timezone))) {
478 return \true;
479 }
480 // if coming from a browser request, and period does contain today, check the ttl for the period (done just below this)
481 $minDatetimeArchiveProcessedUTC = \Piwik\ArchiveProcessor\Rules::getMinTimeProcessedForInProgressArchive($params->getDateStart(), $params->getPeriod(), $params->getSegment(), $params->getSite());
482 $minDatetimeArchiveProcessedUTC = Date::factory($minDatetimeArchiveProcessedUTC);
483 if ($minDatetimeArchiveProcessedUTC && Date::factory($tsArchived)->isEarlier($minDatetimeArchiveProcessedUTC)) {
484 return \true;
485 }
486 }
487 return \false;
488 }
489 }
490