|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* @copyright Copyright (c) 2016 Joas Schilling <[email protected]> |
|
4
|
|
|
* |
|
5
|
|
|
* @license GNU AGPL version 3 or any later version |
|
6
|
|
|
* |
|
7
|
|
|
* This program is free software: you can redistribute it and/or modify |
|
8
|
|
|
* it under the terms of the GNU Affero General Public License as |
|
9
|
|
|
* published by the Free Software Foundation, either version 3 of the |
|
10
|
|
|
* License, or (at your option) any later version. |
|
11
|
|
|
* |
|
12
|
|
|
* This program is distributed in the hope that it will be useful, |
|
13
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
14
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
15
|
|
|
* GNU Affero General Public License for more details. |
|
16
|
|
|
* |
|
17
|
|
|
* You should have received a copy of the GNU Affero General Public License |
|
18
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
19
|
|
|
* |
|
20
|
|
|
*/ |
|
21
|
|
|
|
|
22
|
|
|
namespace OCA\NextcloudAnnouncements\Cron; |
|
23
|
|
|
|
|
24
|
|
|
|
|
25
|
|
|
use OC\BackgroundJob\TimedJob; |
|
26
|
|
|
use OCA\NextcloudAnnouncements\Notification\Notifier; |
|
27
|
|
|
use OCP\AppFramework\Http; |
|
28
|
|
|
use OCP\Http\Client\IClientService; |
|
29
|
|
|
use OCP\IConfig; |
|
30
|
|
|
use OCP\IGroup; |
|
31
|
|
|
use OCP\IGroupManager; |
|
32
|
|
|
use OCP\IUser; |
|
33
|
|
|
use OCP\Notification\IManager as INotificationManager; |
|
34
|
|
|
use phpseclib\File\X509; |
|
35
|
|
|
|
|
36
|
|
|
class Crawler extends TimedJob { |
|
37
|
|
|
|
|
38
|
|
|
const FEED_URL = 'https://pushfeed.nextcloud.com/feed'; |
|
39
|
|
|
|
|
40
|
|
|
/** @var string */ |
|
41
|
|
|
protected $appName; |
|
42
|
|
|
/** @var IConfig */ |
|
43
|
|
|
protected $config; |
|
44
|
|
|
/** @var IGroupManager */ |
|
45
|
|
|
protected $groupManager; |
|
46
|
|
|
/** @var INotificationManager */ |
|
47
|
|
|
protected $notificationManager; |
|
48
|
|
|
/** @var IClientService */ |
|
49
|
|
|
protected $clientService; |
|
50
|
|
|
|
|
51
|
|
|
/** @var string[] */ |
|
52
|
|
|
protected $notifyUsers = []; |
|
53
|
|
|
|
|
54
|
|
|
/** |
|
55
|
|
|
* @param string $appName |
|
56
|
|
|
* @param IConfig $config |
|
57
|
|
|
* @param IGroupManager $groupManager |
|
58
|
|
|
* @param INotificationManager $notificationManager |
|
59
|
|
|
* @param IClientService $clientService |
|
60
|
|
|
*/ |
|
61
|
|
|
public function __construct($appName, IConfig $config, IGroupManager $groupManager, INotificationManager $notificationManager, IClientService $clientService) { |
|
62
|
|
|
$this->appName = $appName; |
|
63
|
|
|
$this->config = $config; |
|
64
|
|
|
$this->groupManager = $groupManager; |
|
65
|
|
|
$this->notificationManager = $notificationManager; |
|
66
|
|
|
$this->clientService = $clientService; |
|
67
|
|
|
|
|
68
|
|
|
// Run once per day |
|
69
|
|
|
$this->setInterval(24 * 60 * 60); |
|
70
|
|
|
} |
|
71
|
|
|
|
|
72
|
|
|
|
|
73
|
|
|
protected function run($argument) { |
|
74
|
|
|
try { |
|
75
|
|
|
$feedBody = $this->loadFeed(); |
|
76
|
|
|
$rss = simplexml_load_string($feedBody); |
|
77
|
|
|
if ($rss === false) { |
|
78
|
|
|
throw new \Exception('Invalid XML feed'); |
|
79
|
|
|
} |
|
80
|
|
|
} catch (\Exception $e) { |
|
81
|
|
|
// Something is wrong 🙊 |
|
82
|
|
|
return; |
|
83
|
|
|
} |
|
84
|
|
|
|
|
85
|
|
|
$lastPubDate = $this->config->getAppValue($this->appName, 'pub_date', 'now'); |
|
86
|
|
|
if ($lastPubDate === 'now') { |
|
87
|
|
|
// First call, don't spam the user with old stuff... |
|
88
|
|
|
$this->config->setAppValue($this->appName, 'pub_date', $rss->channel->pubDate); |
|
89
|
|
|
return; |
|
90
|
|
|
} else if ($rss->channel->pubDate === $lastPubDate) { |
|
91
|
|
|
// Nothing new here... |
|
92
|
|
|
return; |
|
93
|
|
|
} |
|
94
|
|
|
|
|
95
|
|
|
$lastPubDateTime = new \DateTime($lastPubDate); |
|
96
|
|
|
|
|
97
|
|
|
foreach ($rss->channel->item as $item) { |
|
98
|
|
|
$id = md5((string) $item->guid); |
|
99
|
|
|
if ($this->config->getAppValue($this->appName, $id, '') === 'published') { |
|
100
|
|
|
continue; |
|
101
|
|
|
} |
|
102
|
|
|
$pubDate = new \DateTime((string) $item->pubDate); |
|
103
|
|
|
|
|
104
|
|
|
if ($pubDate <= $lastPubDateTime) { |
|
105
|
|
|
continue; |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
$notification = $this->notificationManager->createNotification(); |
|
109
|
|
|
$notification->setApp($this->appName) |
|
110
|
|
|
->setDateTime($pubDate) |
|
111
|
|
|
->setObject($this->appName, $id) |
|
112
|
|
|
->setSubject(Notifier::SUBJECT, [(string) $item->title]) |
|
113
|
|
|
->setLink((string) $item->link); |
|
114
|
|
|
|
|
115
|
|
|
foreach ($this->getUsersToNotify() as $uid) { |
|
116
|
|
|
$notification->setUser($uid); |
|
117
|
|
|
$this->notificationManager->notify($notification); |
|
118
|
|
|
} |
|
119
|
|
|
|
|
120
|
|
|
$this->config->getAppValue($this->appName, $id, 'published'); |
|
121
|
|
|
} |
|
122
|
|
|
|
|
123
|
|
|
$this->config->setAppValue($this->appName, 'pub_date', $rss->channel->pubDate); |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
/** |
|
127
|
|
|
* @return string |
|
128
|
|
|
* @throws \Exception |
|
129
|
|
|
*/ |
|
130
|
|
|
protected function loadFeed() { |
|
131
|
|
|
$signature = $this->readFile('.signature'); |
|
132
|
|
|
|
|
133
|
|
|
if (!$signature) { |
|
134
|
|
|
throw new \Exception('Invalid signature fetched from the server'); |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
$certificate = new X509(); |
|
138
|
|
|
$certificate->loadCA(file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crt')); |
|
139
|
|
|
$loadedCertificate = $certificate->loadX509(file_get_contents(__DIR__ . '/../../appinfo/certificate.crt')); |
|
140
|
|
|
|
|
141
|
|
|
// Verify if the certificate has been revoked |
|
142
|
|
|
$crl = new X509(); |
|
143
|
|
|
$crl->loadCA(file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crt')); |
|
144
|
|
|
$crl->loadCRL(file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crl')); |
|
145
|
|
|
if ($crl->validateSignature() !== true) { |
|
146
|
|
|
throw new \Exception('Could not validate CRL signature'); |
|
147
|
|
|
} |
|
148
|
|
|
$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString(); |
|
149
|
|
|
$revoked = $crl->getRevoked($csn); |
|
150
|
|
|
if ($revoked !== false) { |
|
151
|
|
|
throw new \Exception('Certificate has been revoked'); |
|
152
|
|
|
} |
|
153
|
|
|
|
|
154
|
|
|
// Verify if the certificate has been issued by the Nextcloud Code Authority CA |
|
155
|
|
|
if($certificate->validateSignature() !== true) { |
|
156
|
|
|
throw new \Exception('App with id nextcloud_announcements has a certificate not issued by a trusted Code Signing Authority'); |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
// Verify if the certificate is issued for the requested app id |
|
160
|
|
|
$certInfo = openssl_x509_parse(file_get_contents(__DIR__ . '/../../appinfo/certificate.crt')); |
|
161
|
|
|
if(!isset($certInfo['subject']['CN'])) { |
|
162
|
|
|
throw new \Exception('App with id nextcloud_announcements has a cert with no CN'); |
|
163
|
|
|
} |
|
164
|
|
|
if($certInfo['subject']['CN'] !== 'nextcloud_announcements') { |
|
165
|
|
|
throw new \Exception(sprintf('App with id nextcloud_announcements has a cert issued to %s', $certInfo['subject']['CN'])); |
|
166
|
|
|
} |
|
167
|
|
|
|
|
168
|
|
|
$feedBody = $this->readFile('.rss'); |
|
169
|
|
|
|
|
170
|
|
|
// Check if the signature actually matches the downloaded content |
|
171
|
|
|
$certificate = openssl_get_publickey(file_get_contents(__DIR__ . '/../../appinfo/certificate.crt')); |
|
172
|
|
|
$verified = (bool)openssl_verify($feedBody, base64_decode($signature), $certificate, OPENSSL_ALGO_SHA512); |
|
173
|
|
|
openssl_free_key($certificate); |
|
174
|
|
|
|
|
175
|
|
|
if (!$verified) { |
|
176
|
|
|
// Signature does not match |
|
177
|
|
|
throw new \Exception('Feed has an invalid signature'); |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
return $feedBody; |
|
181
|
|
|
} |
|
182
|
|
|
|
|
183
|
|
|
/** |
|
184
|
|
|
* @param string $file |
|
185
|
|
|
* @return string |
|
186
|
|
|
* @throws \Exception |
|
187
|
|
|
*/ |
|
188
|
|
|
protected function readFile($file) { |
|
189
|
|
|
$client = $this->clientService->newClient(); |
|
190
|
|
|
$response = $client->get(self::FEED_URL . $file); |
|
191
|
|
|
if ($response->getStatusCode() !== Http::STATUS_OK) { |
|
192
|
|
|
throw new \Exception('Could not load file'); |
|
193
|
|
|
} |
|
194
|
|
|
return $response->getBody(); |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
/** |
|
198
|
|
|
* Get the list of users to notify |
|
199
|
|
|
* @return string[] |
|
200
|
|
|
*/ |
|
201
|
|
|
protected function getUsersToNotify() { |
|
202
|
|
|
if (!empty($this->notifyUsers)) { |
|
203
|
|
|
return array_keys($this->notifyUsers); |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
$groups = $this->config->getAppValue($this->appName, 'notification_groups', '["admin"]'); |
|
207
|
|
|
$groups = json_decode($groups, true); |
|
208
|
|
|
|
|
209
|
|
|
if ($groups === null) { |
|
210
|
|
|
return []; |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
|
|
foreach ($groups as $gid) { |
|
214
|
|
|
$group = $this->groupManager->get($gid); |
|
215
|
|
|
if (!($group instanceof IGroup)) { |
|
|
|
|
|
|
216
|
|
|
continue; |
|
217
|
|
|
} |
|
218
|
|
|
|
|
219
|
|
|
/** @var IUser[] $users */ |
|
220
|
|
|
$users = $group->getUsers(); |
|
221
|
|
|
foreach ($users as $user) { |
|
222
|
|
|
$uid = $user->getUID(); |
|
223
|
|
|
if (isset($this->notifyUsers[$uid])) { |
|
224
|
|
|
continue; |
|
225
|
|
|
} |
|
226
|
|
|
|
|
227
|
|
|
$this->notifyUsers[$uid] = true; |
|
228
|
|
|
} |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
|
|
return array_keys($this->notifyUsers); |
|
232
|
|
|
} |
|
233
|
|
|
} |
|
234
|
|
|
|
This error could be the result of:
1. Missing dependencies
PHP Analyzer uses your
composer.jsonfile (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects thecomposer.jsonto be in the root folder of your repository.Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the
requireorrequire-devsection?2. Missing use statement
PHP does not complain about undefined classes in
ìnstanceofchecks. For example, the following PHP code will work perfectly fine:If you have not tested against this specific condition, such errors might go unnoticed.