1<?php 2 3declare(strict_types=1); 4 5/** 6 * @copyright Copyright (c) 2016, ownCloud, Inc. 7 * 8 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 9 * @author Joas Schilling <coding@schilljs.com> 10 * @author Morris Jobke <hey@morrisjobke.de> 11 * 12 * @license AGPL-3.0 13 * 14 * This code is free software: you can redistribute it and/or modify 15 * it under the terms of the GNU Affero General Public License, version 3, 16 * as published by the Free Software Foundation. 17 * 18 * This program is distributed in the hope that it will be useful, 19 * but WITHOUT ANY WARRANTY; without even the implied warranty of 20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 * GNU Affero General Public License for more details. 22 * 23 * You should have received a copy of the GNU Affero General Public License, version 3, 24 * along with this program. If not, see <http://www.gnu.org/licenses/> 25 * 26 */ 27namespace OCA\UpdateNotification\Notification; 28 29use OC\BackgroundJob\TimedJob; 30use OC\Installer; 31use OC\Updater\VersionCheck; 32use OCP\App\IAppManager; 33use OCP\Http\Client\IClientService; 34use OCP\IConfig; 35use OCP\IGroup; 36use OCP\IGroupManager; 37use OCP\Notification\IManager; 38 39class BackgroundJob extends TimedJob { 40 protected $connectionNotifications = [3, 7, 14, 30]; 41 42 /** @var IConfig */ 43 protected $config; 44 45 /** @var IManager */ 46 protected $notificationManager; 47 48 /** @var IGroupManager */ 49 protected $groupManager; 50 51 /** @var IAppManager */ 52 protected $appManager; 53 54 /** @var IClientService */ 55 protected $client; 56 57 /** @var Installer */ 58 protected $installer; 59 60 /** @var string[] */ 61 protected $users; 62 63 public function __construct(IConfig $config, 64 IManager $notificationManager, 65 IGroupManager $groupManager, 66 IAppManager $appManager, 67 IClientService $client, 68 Installer $installer) { 69 // Run once a day 70 $this->setInterval(60 * 60 * 24); 71 72 $this->config = $config; 73 $this->notificationManager = $notificationManager; 74 $this->groupManager = $groupManager; 75 $this->appManager = $appManager; 76 $this->client = $client; 77 $this->installer = $installer; 78 } 79 80 protected function run($argument) { 81 if (\OC::$CLI && !$this->config->getSystemValueBool('debug', false)) { 82 try { 83 // Jitter the pinging of the updater server and the appstore a bit. 84 // Otherwise all Nextcloud installations are pinging the servers 85 // in one of 288 86 sleep(random_int(1, 180)); 87 } catch (\Exception $e) { 88 } 89 } 90 91 $this->checkCoreUpdate(); 92 $this->checkAppUpdates(); 93 } 94 95 /** 96 * Check for ownCloud update 97 */ 98 protected function checkCoreUpdate() { 99 if (\in_array($this->getChannel(), ['daily', 'git'], true)) { 100 // "These aren't the update channels you're looking for." - Ben Obi-Wan Kenobi 101 return; 102 } 103 104 $updater = $this->createVersionCheck(); 105 106 $status = $updater->check(); 107 if ($status === false) { 108 $errors = 1 + (int) $this->config->getAppValue('updatenotification', 'update_check_errors', 0); 109 $this->config->setAppValue('updatenotification', 'update_check_errors', $errors); 110 111 if (\in_array($errors, $this->connectionNotifications, true)) { 112 $this->sendErrorNotifications($errors); 113 } 114 } elseif (\is_array($status)) { 115 $this->config->setAppValue('updatenotification', 'update_check_errors', 0); 116 $this->clearErrorNotifications(); 117 118 if (isset($status['version'])) { 119 $this->createNotifications('core', $status['version'], $status['versionstring']); 120 } 121 } 122 } 123 124 /** 125 * Send a message to the admin when the update server could not be reached 126 * @param int $numDays 127 */ 128 protected function sendErrorNotifications($numDays) { 129 $this->clearErrorNotifications(); 130 131 $notification = $this->notificationManager->createNotification(); 132 try { 133 $notification->setApp('updatenotification') 134 ->setDateTime(new \DateTime()) 135 ->setObject('updatenotification', 'error') 136 ->setSubject('connection_error', ['days' => $numDays]); 137 138 foreach ($this->getUsersToNotify() as $uid) { 139 $notification->setUser($uid); 140 $this->notificationManager->notify($notification); 141 } 142 } catch (\InvalidArgumentException $e) { 143 return; 144 } 145 } 146 147 /** 148 * Remove error notifications again 149 */ 150 protected function clearErrorNotifications() { 151 $notification = $this->notificationManager->createNotification(); 152 try { 153 $notification->setApp('updatenotification') 154 ->setSubject('connection_error') 155 ->setObject('updatenotification', 'error'); 156 } catch (\InvalidArgumentException $e) { 157 return; 158 } 159 $this->notificationManager->markProcessed($notification); 160 } 161 162 /** 163 * Check all installed apps for updates 164 */ 165 protected function checkAppUpdates() { 166 $apps = $this->appManager->getInstalledApps(); 167 foreach ($apps as $app) { 168 $update = $this->isUpdateAvailable($app); 169 if ($update !== false) { 170 $this->createNotifications($app, $update); 171 } 172 } 173 } 174 175 /** 176 * Create notifications for this app version 177 * 178 * @param string $app 179 * @param string $version 180 * @param string $visibleVersion 181 */ 182 protected function createNotifications($app, $version, $visibleVersion = '') { 183 $lastNotification = $this->config->getAppValue('updatenotification', $app, false); 184 if ($lastNotification === $version) { 185 // We already notified about this update 186 return; 187 } 188 189 if ($lastNotification !== false) { 190 // Delete old updates 191 $this->deleteOutdatedNotifications($app, $lastNotification); 192 } 193 194 $notification = $this->notificationManager->createNotification(); 195 try { 196 $notification->setApp('updatenotification') 197 ->setDateTime(new \DateTime()) 198 ->setObject($app, $version); 199 200 if ($visibleVersion !== '') { 201 $notification->setSubject('update_available', ['version' => $visibleVersion]); 202 } else { 203 $notification->setSubject('update_available'); 204 } 205 206 foreach ($this->getUsersToNotify() as $uid) { 207 $notification->setUser($uid); 208 $this->notificationManager->notify($notification); 209 } 210 } catch (\InvalidArgumentException $e) { 211 return; 212 } 213 214 $this->config->setAppValue('updatenotification', $app, $version); 215 } 216 217 /** 218 * @return string[] 219 */ 220 protected function getUsersToNotify(): array { 221 if ($this->users !== null) { 222 return $this->users; 223 } 224 225 $notifyGroups = (array) json_decode($this->config->getAppValue('updatenotification', 'notify_groups', '["admin"]'), true); 226 $this->users = []; 227 foreach ($notifyGroups as $group) { 228 $groupToNotify = $this->groupManager->get($group); 229 if ($groupToNotify instanceof IGroup) { 230 foreach ($groupToNotify->getUsers() as $user) { 231 $this->users[$user->getUID()] = true; 232 } 233 } 234 } 235 236 $this->users = array_keys($this->users); 237 238 return $this->users; 239 } 240 241 /** 242 * Delete notifications for old updates 243 * 244 * @param string $app 245 * @param string $version 246 */ 247 protected function deleteOutdatedNotifications($app, $version) { 248 $notification = $this->notificationManager->createNotification(); 249 try { 250 $notification->setApp('updatenotification') 251 ->setObject($app, $version); 252 } catch (\InvalidArgumentException $e) { 253 return; 254 } 255 $this->notificationManager->markProcessed($notification); 256 } 257 258 /** 259 * @return VersionCheck 260 */ 261 protected function createVersionCheck(): VersionCheck { 262 return new VersionCheck( 263 $this->client, 264 $this->config 265 ); 266 } 267 268 /** 269 * @return string 270 */ 271 protected function getChannel(): string { 272 return \OC_Util::getChannel(); 273 } 274 275 /** 276 * @param string $app 277 * @return string|false 278 */ 279 protected function isUpdateAvailable($app) { 280 return $this->installer->isUpdateAvailable($app); 281 } 282} 283