_listings instanceof Listings) { $this->setListings( new Listings()); } return $this->_listings; } /** * * set listings service * * @param \Ppb\Service\Listings $listings * * @return $this */ public function setListings(Listings $listings) { $this->_listings = $listings; return $this; } /** * * get users service * * @return \Ppb\Service\Users */ public function getUsers() { if (!$this->_users instanceof Users) { $this->setUsers( new Users()); } return $this->_users; } /** * * set users service * * @param \Ppb\Service\Users $users * * @return $this */ public function setUsers(Users $users) { $this->_users = $users; return $this; } /** * * get sales service * * @return \Ppb\Service\Sales */ public function getSales() { if (!$this->_sales instanceof Sales) { $this->setSales( new Sales()); } return $this->_sales; } /** * * set sales service * * @param \Ppb\Service\Sales $sales * * @return $this */ public function setSales(Sales $sales) { $this->_sales = $sales; return $this; } /** * * get sales listings service * * @return \Ppb\Service\Table\SalesListings */ public function getSalesListings() { if (!$this->_salesListings instanceof SalesListingsService) { $this->setSalesListings( new SalesListingsService()); } return $this->_salesListings; } /** * * set sales listings service * * @param \Ppb\Service\Table\SalesListings $salesListings * * @return $this */ public function setSalesListings(SalesListingsService $salesListings) { $this->_salesListings = $salesListings; return $this; } /** * * close expired listings and assign winners * limit to 50 per transaction * forUpdate means that the rows will be locked until the transaction is complete * * @return $this */ public function closeExpiredListings() { $listingsService = $this->getListings(); $usersService = $this->getUsers(); srand(); $rand = mt_rand(1000, 9999); usleep($rand); $select = $listingsService->getTable()->select() ->forUpdate() ->where('closed = ?', 0) ->where('deleted = ?', 0) ->where('draft = ?', 0) ->where('start_time < ?', new Expr('now()')) ->where('end_time < ?', new Expr('now()')) ->where('end_time is not null') ->limit(self::SELECT_TRANSACTION_LIMIT); $expiredListings = $listingsService->fetchAll($select) ->setAutomatic(true) ->close(); $noSale = array(); $noSaleReserve = array(); /** @var \Ppb\Db\Table\Row\Listing $listing */ foreach ($expiredListings as $listing) { if ($listing->getClosedFlag() === true) { if ($listing->getData('quantity') > 0) { $saleId = $listing->assignWinner(); if ($saleId === false) { if ($listing->countDependentRowset('\Ppb\Db\Table\Bids')) { $noSaleReserve[$listing['user_id']][] = $listing; } else { $noSale[$listing['user_id']][] = $listing; } } } } } // send email notifications to listings owners $mail = new \Listings\Model\Mail\OwnerNotification(); foreach ($noSale as $userId => $listings) { $user = $usersService->findBy('id', $userId); $mail->setUser($user) ->setListings($listings) ->noSale() ->send(); } foreach ($noSaleReserve as $userId => $listings) { $user = $usersService->findBy('id', $userId); $mail->setUser($user) ->setListings($listings) ->noSaleReserve() ->send(); } $expiredListings->relist(); return $this; } /** * * start scheduled listings * * @return $this */ public function startScheduledListings() { $listingsService = $this->getListings(); $adapter = $listingsService->getTable()->getAdapter(); $where = array( $adapter->quoteInto('closed = ?', 1), $adapter->quoteInto('deleted = ?', 0), $adapter->quoteInto('draft = ?', 0), $adapter->quoteInto('start_time < ?', new Expr('now()')), 'end_time > ' . new Expr('now()') . ' OR end_time is null', ); $listingsService->getTable()->update(array('closed' => 0, 'updated_at' => new Expr('now()')), $where); return $this; } /** * * method that purges cache routes and metadata * normally is run from the cron daily * * @return $this */ public function purgeCacheData() { /** @var \Cube\Cache $cache */ $cache = Front::getInstance()->getBootstrap()->getResource('cache'); $cache->getAdapter()->purge(CacheAdapter::ROUTES); $cache->getAdapter()->purge(CacheAdapter::METADATA); return $this; } public function purgeCacheQueries() { /** @var \Cube\Cache $cache */ $cache = Front::getInstance()->getBootstrap()->getResource('cache'); $expires = $cache->getAdapter()->getExpires(); $cache->getAdapter() ->setExpires(AbstractTable::QUERIES_CACHE_EXPIRES) ->purge(CacheAdapter::QUERIES); $cache->getAdapter() ->setExpires($expires); return $this; } /** * * delete expired rows from the sales listings table for which the * corresponding sale is marked as pending * * @return $this */ public function deletePendingSalesListings() { $settings = $this->getSettings(); $salesListingsService = $this->getSalesListings(); $select = $salesListingsService->getTable()->getAdapter() ->select() ->from(array('sl' => 'sales_listings'), 'sl.id') ->joinLeft(array('s' => 'sales'), 's.id = sl.sale_id', '') ->where('sl.created_at < ?', new Expr('(now() - interval ' . intval($settings['pending_sales_listings_expire_hours']) . ' minute)')) ->where('s.pending = ?', 1); $rows = $salesListingsService->fetchAll($select); $ids = array(); foreach ($rows as $row) { $ids[] = $row['id']; } $salesListingsService->delete($ids); return $this; } /** * * suspend users for which the debit balance date has been exceeded * * @return $this */ public function suspendUsersDebitExceededDate() { $usersService = $this->getUsers(); $settings = $this->getSettings(); if ($settings['user_account_type'] == 'personal' || $settings['payment_mode'] == 'account') { $select = $usersService->getTable()->select() ->where('active = ?', 1) ->where('role NOT IN (?)', array_keys(Users::getAdminRoles())) ->where('balance > max_debit') ->where('debit_exceeded_date is not null') ->where('debit_exceeded_date < ?', new Expr('(now() - interval ' . intval($settings['suspension_days']) . ' day)')) ->limit(self::SELECT_TRANSACTION_LIMIT); if ($settings['user_account_type'] == 'personal') { $select->where('account_mode = ?', 'account'); } $users = $usersService->fetchAll($select); $mail = new \Members\Model\Mail\User(); /** @var \Ppb\Db\Table\Row\User $user */ foreach ($users as $user) { $user->updateActive(0); $mail->accountBalanceExceeded($user)->send(); } } return $this; } public function sendNewsletters() { $newslettersRecipientsService = new NewslettersRecipients(); $newslettersService = new Newsletters(); $recipients = $newslettersRecipientsService->fetchAll( $newslettersRecipientsService->getTable()->getAdapter()->select() ->from(array('nr' => 'newsletters_recipients')) ->joinLeft(array('n' => 'newsletters'), 'n.id = nr.newsletter_id', array('title', 'content')) // ->joinLeft(array('u' => 'users'), 'u.id = nr.user_id', array('username', 'email')) ->limit(200)); $mail = new \Members\Model\Mail\User(); $ids = array(); $newslettersIds = array(); /** @var \Cube\Db\Table\Row $data */ foreach ($recipients as $data) { $ids[] = $data['id']; $newslettersIds[] = $data['newsletter_id']; $mail->newsletter($data['title'], $data['content'], $data['email'])->send(); } if (count($ids)) { $newslettersRecipientsService->delete($ids); $adapter = $newslettersService->getTable()->getAdapter(); $newslettersService->getTable()->update(array( 'updated_at' => new Expr('now()') ), $adapter->quoteInto('id IN (?)', array_unique($newslettersIds))); } } /** * * remove marked deleted listings from the database * * @return $this */ public function removeMarkedDeletedListings() { $listingsService = $this->getListings(); $select = $listingsService->getTable()->select() ->where('deleted = ?', 1) ->limit(self::SELECT_TRANSACTION_LIMIT); $listingsService->fetchAll($select)->setAdmin(true)->delete(); return $this; } /** * * mark as deleted closed listings * * @return $this */ public function markDeletedClosedListings() { $listingsService = $this->getListings(); $settings = $this->getSettings(); $adapter = $listingsService->getTable()->getAdapter(); $where = array( 'end_time is not null', $adapter->quoteInto('deleted = ?', 0), $adapter->quoteInto('closed = ?', 1), $adapter->quoteInto('end_time < ?', new Expr('(now() - interval ' . intval($settings['closed_listings_deletion_days']) . ' day)')) ); $listingsService->getTable()->update(array( 'deleted' => 1, ), $where); return $this; } /** * * notify users on subscriptions that are about to expire * only notify users for which the "re-bill if in account mode" setting doesnt apply * * @param array $subscription * * @return $this */ public function notifyUsersOnSubscriptionsAboutToExpire(array $subscription) { $usersService = $this->getUsers(); $settings = $this->getSettings(); $select = $usersService->getTable()->select() ->where("{$subscription['active']} = ?", 1) ->where("{$subscription['expirationDate']} < ?", new Expr('(now() + interval ' . self::DEFAULT_EXPIRATION_DAYS . ' day)')) ->where("{$subscription['expirationDate']} > ?", 0) ->where("{$subscription['emailFlag']} = ?", 0) ->limit(self::SELECT_TRANSACTION_LIMIT); $runQuery = true; if ($settings['rebill_expired_subscriptions']) { if ($settings['user_account_type'] == 'personal') { $select->where('account_mode = ?', 'live'); } else if ($settings['user_account_type'] == 'global' && $settings['payment_mode'] == 'account') { $runQuery = false; } } if ($runQuery) { $users = $usersService->fetchAll($select); /** @var \Ppb\Db\Table\Row\User $user */ $mail = new \Members\Model\Mail\User(); foreach ($users as $user) { $mail->subscriptionExpirationNotification($subscription, $user, self::DEFAULT_EXPIRATION_DAYS)->send(); $user->save(array( "{$subscription['emailFlag']}" => 1, )); } } return $this; } /** * * process expired user subscriptions * notify users of the expired subscription * re-bill if in account mode, and activate automatically * * @param array $subscription subscription type * * @return $this */ public function processExpiredSubscriptions(array $subscription) { $usersService = $this->getUsers(); $accountingService = new Accounting(); $settings = $this->getSettings(); $select = $usersService->getTable()->select() ->where("{$subscription['active']} = ?", 1) ->where("{$subscription['expirationDate']} < ?", new Expr('now()')) ->where("{$subscription['expirationDate']} > ?", 0) ->limit(self::SELECT_TRANSACTION_LIMIT); $users = $usersService->fetchAll($select); $mail = new \Members\Model\Mail\User(); /** @var \Ppb\Db\Table\Row\User $user */ foreach ($users as $user) { $user->save(array( "{$subscription['active']}" => 0 )); if ($settings['rebill_expired_subscriptions'] && $user->userPaymentMode() == 'account') { // bill subscription in account mode /** @var \Ppb\Service\Fees $feesService */ $feesService = new $subscription['feesService']($user); $totalAmount = $feesService->getTotalAmount(); $user->save(array( 'balance' => ($user['balance'] + $totalAmount) )); $accountingService->save(array( 'name' => array( 'string' => 'Automatic Renewal - %s', 'args' => array($subscription['name']), ), 'amount' => $totalAmount, 'user_id' => $user['id'], 'currency' => $settings['currency'], )); // activate subscription $user->$subscription['updateMethod'](1); $mail->subscriptionRenewed($subscription, $user)->send(); } else { $mail->subscriptionExpired($subscription, $user)->send(); } } return $this; } /** * * delete sales for which the payment due time limit has expired * (ref: force payment module) * * return $this */ public function deleteExpiredSalesForcePayment() { $salesService = $this->getSales(); $select = $salesService->getTable()->select() ->where('expires_at is not null') ->where('expires_at < ?', new Expr('now()')) ->limit(self::SELECT_TRANSACTION_LIMIT); $sales = $salesService->fetchAll($select); /** @var \Ppb\Db\Table\Row\Sale $sale */ foreach ($sales as $sale) { $sale->revert(); } return $this; } /** * * count listings that were not counted since their creation / last update * * return $this */ public function countListings() { $listingsService = $this->getListings(); $select = $listingsService->getTable()->select() ->where('counted_at is null OR counted_at < IF(updated_at is null, created_at, updated_at)') ->limit(100); $listings = $listingsService->fetchAll($select); /** @var \Ppb\Db\Table\Row\Listing $listing */ foreach ($listings as $listing) { $listing->processCategoryCounter(); } return $this; } /** * * remove search phrases that haven't been searched for within the last 6 months * * @return $this */ public function purgeAutocompleteTags() { $autocompleteTagsService = new AutocompleteTags(); $adapter = $autocompleteTagsService->getTable()->getAdapter(); $where = array( $adapter->quoteInto('created_at < ?', new Expr('now() - interval 6 month')), $adapter->quoteInto('updated_at < ?', new Expr('now() - interval 6 month')), ); $autocompleteTagsService->getTable()->delete($where); return $this; } /** * * remove expired rows from the users statistics table * * @return $this */ public function purgeUsersStatistics() { $usersStatisticsService = new UsersStatistics(); $adapter = $usersStatisticsService->getTable()->getAdapter(); $where = array( $adapter->quoteInto('updated_at < ?', new Expr('now() - interval ' . self::ONLINE_USERS_STATS_PURGE . ' minute')), ); $usersStatisticsService->getTable()->delete($where); return $this; } /** * * remove expired rows from the recently viewed listings table * * @return $this */ public function purgeRecentlyViewedListings() { $settings = $this->getSettings(); $recentlyViewedListingsService = new RecentlyViewedListings(); $adapter = $recentlyViewedListingsService->getTable()->getAdapter(); $where = array( $adapter->quoteInto('IF(updated_at is null, created_at, updated_at) < ?', new Expr('now() - interval ' . intval($settings['enable_recently_viewed_listings_expiration']) . ' hour')), ); $recentlyViewedListingsService->getTable()->delete($where); return $this; } /** * * method that deletes all unused uploaded files that are older than UNUSED_FILES_REMOVAL_TIME_LIMIT * this is to be called by a separate cron call to avoid server load * currently it processes 200 images per call * * @return $this */ public function purgeUnusedUploadedFiles() { $counter = null; $uploadsFolder = \Ppb\Utility::getPath('uploads'); // we have sorted all files by modified date $files = glob($uploadsFolder . '/*.*'); usort($files, function ($a, $b) { return filemtime($a) > filemtime($b); }); $files = array_filter($files); $files = array_slice($files, 0, 200); $prefix = $this->getListings()->getTable()->getPrefix(); $adapter = $this->getListings()->getTable()->getAdapter(); foreach ($files as $filePath) { $file = str_replace($uploadsFolder . DIRECTORY_SEPARATOR, '', $filePath); $stat = stat($filePath); if (($stat['mtime'] + self::UNUSED_FILES_REMOVAL_TIME_LIMIT) < time()) { if ($this->_isUsedFile($file, $adapter, $prefix) !== true) { @unlink($filePath); } else { @touch($filePath); } } } clearstatcache(); return $this; } /** * * gets live currency exchange rates and updates the currencies table * always gets based on the site's default currency * * @return $this */ public function updateCurrencyExchangeRates() { $settings = $this->getSettings(); $feed = 'http://www.floatrates.com/daily/' . strtolower($settings['currency']) . '.xml'; $xml = simplexml_load_file($feed); $object = new Config\Xml($xml); $currencies = $object->getData('item'); $data = array(); foreach ($currencies as $currency) { $isoCode = $currency['targetCurrency']; $conversionRate = number_format($currency['exchangeRate'], 6, '.', ''); if ($isoCode && $conversionRate > 0) { $data[] = array( 'data' => array( 'conversion_rate' => $conversionRate, ), 'where' => array( "iso_code = '" . $isoCode . "'", ) ); } } $currenciesService = new Table\Currencies(); foreach ($data as $row) { $currenciesService->update($row['data'], $row['where']); } // reset default currency exchange rate to 1 $currenciesService->update(array("conversion_rate" => 1), "iso_code = '" . $settings['currency'] . "'"); return $this; } /** * * check if a file from the /uploads/ folder is used by the software * the following tables will be checked: * * listings_media [value] * categories [logo_path] * advertising [content] * settings [value : site_logo_path] * users [store_settings : PREG : s:15:"store_logo_path";s:(d+):"FILENAME"] * * @param string $file * @param \Cube\Db\Adapter\AbstractAdapter $adapter * @param string $prefix * * @return bool */ protected function _isUsedFile($file, AbstractAdapter $adapter, $prefix) { $statement = $adapter->query("SELECT `value` as `file_path` FROM `" . $prefix . "listings_media` WHERE `value` = '" . $file . "' UNION SELECT `logo_path` as `file_path` FROM `" . $prefix . "categories` WHERE `logo_path` = '" . $file . "' UNION SELECT `content` as `file_path` FROM `" . $prefix . "advertising` WHERE `content` = '" . $file . "' AND `type` = 'image' UNION SELECT `value` as `file_path` FROM `" . $prefix . "settings` WHERE `value` = '" . $file . "' AND `name` = 'site_logo_path' UNION SELECT `store_settings` as `file_path` FROM `" . $prefix . "users` WHERE `store_settings` LIKE '%" . $file . "%'"); return ($statement->rowCount() > 0) ? true : false; } /** * * the method that will run all/specified cron jobs * * @param string|null $command * * @return $this */ public function run($command = null) { $settings = $this->getSettings(); $usersService = $this->getUsers(); switch ($command) { case 'purge-unused-uploaded-files': $this->purgeUnusedUploadedFiles(); break; case 'purge-cache-data': $this->purgeCacheData(); break; case 'update-currency-exchange-rates': $this->updateCurrencyExchangeRates(); break; default: $this->closeExpiredListings(); $this->startScheduledListings(); $this->suspendUsersDebitExceededDate(); if ($settings['pending_sales_listings_expire_hours']) { $this->deletePendingSalesListings(); } if ($settings['marked_deleted_listings_removal']) { $this->removeMarkedDeletedListings(); } if ($settings['closed_listings_deletion_days'] > 0) { $this->markDeletedClosedListings(); } $subscriptionTypes = $usersService->getSubscriptionTypes(); foreach ($subscriptionTypes as $subscription) { $this->notifyUsersOnSubscriptionsAboutToExpire($subscription); $this->processExpiredSubscriptions($subscription); } $this->sendNewsletters(); $this->deleteExpiredSalesForcePayment(); $this->countListings(); $this->purgeAutocompleteTags(); $this->purgeUsersStatistics(); $this->purgeRecentlyViewedListings(); $this->purgeCacheQueries(); break; } return $this; } }