Completed
Push — master ( 4831e4...32ef47 )
by
unknown
09:35
created

FeedService   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 413
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 91.13%

Importance

Changes 9
Bugs 3 Features 2
Metric Value
wmc 40
c 9
b 3
f 2
lcom 1
cbo 11
dl 0
loc 413
ccs 185
cts 203
cp 0.9113
rs 8.2609

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 21 1
A findAll() 0 3 1
A findAllFromAllUsers() 0 3 1
B create() 0 55 7
A updateAll() 0 15 3
D update() 0 102 11
B importArticles() 0 61 7
A markDeleted() 0 5 1
A unmarkDeleted() 0 5 1
A purgeDeleted() 0 14 3
A deleteUser() 0 3 1
A patch() 0 19 3

How to fix   Complexity   

Complex Class

Complex classes like FeedService often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FeedService, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * ownCloud - News
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Alessandro Cosentino <[email protected]>
9
 * @author Bernhard Posselt <[email protected]>
10
 * @copyright Alessandro Cosentino 2012
11
 * @copyright Bernhard Posselt 2012, 2014
12
 */
13
14
namespace OCA\News\Service;
15
16
use HTMLPurifier;
17
18
use OCP\ILogger;
19
use OCP\IL10N;
20
use OCP\AppFramework\Db\DoesNotExistException;
21
use OCP\AppFramework\Utility\ITimeFactory;
22
23
use OCA\News\Db\Feed;
24
use OCA\News\Db\Item;
25
use OCA\News\Db\FeedMapper;
26
use OCA\News\Db\ItemMapper;
27
use OCA\News\Fetcher\Fetcher;
28
use OCA\News\Fetcher\FetcherException;
29
use OCA\News\Config\Config;
30
31
32
class FeedService extends Service {
33
34
    private $feedFetcher;
35
    private $itemMapper;
36
    private $feedMapper;
37
    private $logger;
38
    private $l10n;
39
    private $timeFactory;
40
    private $autoPurgeMinimumInterval;
41
    private $purifier;
42
    private $loggerParams;
43
44 29
    public function __construct(FeedMapper $feedMapper,
45
                                Fetcher $feedFetcher,
46
                                ItemMapper $itemMapper,
47
                                ILogger $logger,
48
                                IL10N $l10n,
49
                                ITimeFactory $timeFactory,
50
                                Config $config,
51
                                HTMLPurifier $purifier,
52
                                $LoggerParameters){
53 29
        parent::__construct($feedMapper);
54 29
        $this->feedFetcher = $feedFetcher;
55 29
        $this->itemMapper = $itemMapper;
56 29
        $this->logger = $logger;
57 29
        $this->l10n = $l10n;
58 29
        $this->timeFactory = $timeFactory;
59 29
        $this->autoPurgeMinimumInterval =
60 29
            $config->getAutoPurgeMinimumInterval();
61 29
        $this->purifier = $purifier;
62 29
        $this->feedMapper = $feedMapper;
63 29
        $this->loggerParams = $LoggerParameters;
64 29
    }
65
66
    /**
67
     * Finds all feeds of a user
68
     * @param string $userId the name of the user
69
     * @return Feed[]
70
     */
71 3
    public function findAll($userId){
72 3
        return $this->feedMapper->findAllFromUser($userId);
73
    }
74
75
76
    /**
77
     * Finds all feeds from all users
78
     * @return array of feeds
79
     */
80 1
    public function findAllFromAllUsers() {
81 1
        return $this->feedMapper->findAll();
82
    }
83
84
85
    /**
86
     * Creates a new feed
87
     * @param string $feedUrl the url to the feed
88
     * @param int $folderId the folder where it should be put into, 0 for root
89
     * folder
90
     * @param string $userId for which user the feed should be created
91
     * @param string $title if given, this is used for the opml feed title
92
     * @throws ServiceConflictException if the feed exists already
93
     * @throws ServiceNotFoundException if the url points to an invalid feed
94
     * @return Feed the newly created feed
95
     */
96 3
    public function create($feedUrl, $folderId, $userId, $title=null){
97
        // first try if the feed exists already
98
        try {
99 3
            list($feed, $items) = $this->feedFetcher->fetch($feedUrl);
100
101
            // try again if feed exists depending on the reported link
102
            try {
103 2
                $this->feedMapper->findByUrlHash($feed->getUrlHash(), $userId);
104
                throw new ServiceConflictException(
105
                    $this->l10n->t('Can not add feed: Exists already'));
106
107
            // If no matching feed was found everything was ok
108 2
            } catch(DoesNotExistException $ex){}
109
110
            // insert feed
111 2
            $itemCount = count($items);
112 2
            $feed->setFolderId($folderId);
113 2
            $feed->setUserId($userId);
114 2
            $feed->setArticlesPerUpdate($itemCount);
115
116 2
            if ($title !== null && $title !== '') {
117
                $feed->setTitle($title);
118
            }
119
120 2
            $feed = $this->feedMapper->insert($feed);
121
122
            // insert items in reverse order because the first one is usually
123
            // the newest item
124 2
            $unreadCount = 0;
125 2
            for($i=$itemCount-1; $i>=0; $i--){
126 2
                $item = $items[$i];
127 2
                $item->setFeedId($feed->getId());
128
129
                // check if item exists (guidhash is the same)
130
                // and ignore it if it does
131
                try {
132 2
                    $this->itemMapper->findByGuidHash(
133 2
                        $item->getGuidHash(), $item->getFeedId(), $userId);
134 1
                    continue;
135 2
                } catch(DoesNotExistException $ex){
136 2
                    $unreadCount += 1;
137 2
                    $item->setBody($this->purifier->purify($item->getBody()));
138 2
                    $this->itemMapper->insert($item);
139
                }
140 2
            }
141
142
            // set unread count
143 2
            $feed->setUnreadCount($unreadCount);
144
145 2
            return $feed;
146 1
        } catch(FetcherException $ex){
147 1
            $this->logger->debug($ex->getMessage(), $this->loggerParams);
148 1
            throw new ServiceNotFoundException($ex->getMessage());
149
        }
150
    }
151
152
153
    /**
154
     * Runs all the feed updates
155
     */
156
    public function updateAll(){
157
        // TODO: this method is not covered by any tests
158
        $feeds = $this->feedMapper->findAll();
159
        foreach($feeds as $feed){
160
            try {
161
                $this->update($feed->getId(), $feed->getUserId());
162
            } catch(\Exception $ex){
163
                // something is really wrong here, log it
164
                $this->logger->error(
165
                    'Unexpected error when updating feed ' . $ex->getMessage(),
166
                    $this->loggerParams
167
                );
168
            }
169
        }
170
    }
171
172
173
    /**
174
     * Updates a single feed
175
     * @param int $feedId the id of the feed that should be updated
176
     * @param string $userId the id of the user
177
     * @param bool $forceUpdate update even if the article exists already
178
     * @throws ServiceNotFoundException if the feed does not exist
179
     * @return Feed the updated feed entity
180
     */
181 12
    public function update($feedId, $userId, $forceUpdate=false){
182 12
        $existingFeed = $this->find($feedId, $userId);
183
184 10
        if($existingFeed->getPreventUpdate() === true) {
185 1
            return $existingFeed;
186
        }
187
188
        // for backwards compability it can be that the location is not set
189
        // yet, if so use the url
190 9
        $location = $existingFeed->getLocation();
191 9
        if (!$location) {
192 9
            $location = $existingFeed->getUrl();
193 9
        }
194
195
        try {
196 9
            list($fetchedFeed, $items) = $this->feedFetcher->fetch(
197 9
                $location,
198 9
                false,
199 9
                $existingFeed->getLastModified(),
200 9
                $existingFeed->getEtag(),
201 9
                $existingFeed->getFullTextEnabled()
202 9
            );
203
204
            // if there is no feed it means that no update took place
205 7
            if (!$fetchedFeed) {
206 1
                return $existingFeed;
207
            }
208
209
            // update number of articles on every feed update
210 6
            $itemCount = count($items);
211
212
            // this is needed to adjust to updates that add more items
213
            // than when the feed was created. You can't update the count
214
            // if it's lower because it may be due to the caching headers
215
            // that were sent as the request and it might cause unwanted
216
            // deletion and reappearing of feeds
217 6
            if ($itemCount > $existingFeed->getArticlesPerUpdate()) {
218 1
                $existingFeed->setArticlesPerUpdate($itemCount);
219 1
            }
220
221 6
            $existingFeed->setLastModified($fetchedFeed->getLastModified());
222 6
            $existingFeed->setEtag($fetchedFeed->getEtag());
223 6
            $existingFeed->setLocation($fetchedFeed->getLocation());
224
225
            // insert items in reverse order because the first one is
226
            // usually the newest item
227 6
            for($i=$itemCount-1; $i>=0; $i--){
228 6
                $item = $items[$i];
229 6
                $item->setFeedId($existingFeed->getId());
230
231
                try {
232 6
                    $dbItem = $this->itemMapper->findByGuidHash(
233 6
                        $item->getGuidHash(), $feedId, $userId
234 6
                    );
235
236
                    // in case of update
237 5
                    if ($forceUpdate ||
238 5
                        $item->getPubDate() > $dbItem->getPubDate()) {
239
240 4
                        $dbItem->setTitle($item->getTitle());
241 4
                        $dbItem->setUrl($item->getUrl());
242 4
                        $dbItem->setAuthor($item->getAuthor());
243 4
                        $dbItem->setSearchIndex($item->getSearchIndex());
244 4
                        $dbItem->setRtl($item->getRtl());
245 4
                        $dbItem->setLastModified($item->getLastModified());
246 4
                        $dbItem->setPubDate($item->getPubDate());
247 4
                        $dbItem->setEnclosureMime($item->getEnclosureMime());
248 4
                        $dbItem->setEnclosureLink($item->getEnclosureLink());
249 4
                        $dbItem->setBody(
250 4
                            $this->purifier->purify($item->getBody())
251 4
                        );
252
253
                        // update modes: 0 nothing, 1 set unread
254 4
                        if ($existingFeed->getUpdateMode() === 1) {
255 1
                            $dbItem->setUnread();
256 1
                        }
257
258 4
                        $this->itemMapper->update($dbItem);
259 4
                    }
260 6
                } catch(DoesNotExistException $ex){
261 1
                    $item->setBody(
262 1
                        $this->purifier->purify($item->getBody())
263 1
                    );
264 1
                    $this->itemMapper->insert($item);
265
                }
266 6
            }
267
268
            // mark feed as successfully updated
269 6
            $existingFeed->setUpdateErrorCount(0);
270 6
            $existingFeed->setLastUpdateError('');
271
272 8
        } catch(FetcherException $ex){
273 1
            $existingFeed->setUpdateErrorCount(
274 1
                $existingFeed->getUpdateErrorCount()+1
275 1
            );
276 1
            $existingFeed->setLastUpdateError($ex->getMessage());
277
        }
278
279 7
        $this->feedMapper->update($existingFeed);
280
281 7
        return $this->find($feedId, $userId);
282
    }
283
284
    /**
285
     * Import articles
286
     * @param array $json the array with json
287
     * @param string $userId the username
288
     * @return Feed if one had to be created for nonexistent feeds
289
     */
290 2
    public function importArticles($json, $userId) {
291 2
        $url = 'http://owncloud/nofeed';
292 2
        $urlHash = md5($url);
293
294
        // build assoc array for fast access
295 2
        $feeds = $this->findAll($userId);
296 2
        $feedsDict = [];
297 2
        foreach($feeds as $feed) {
298 2
            $feedsDict[$feed->getLink()] = $feed;
299 2
        }
300
301 2
        $createdFeed = false;
302
303
        // loop over all items and get the corresponding feed
304
        // if the feed does not exist, create a separate feed for them
305 2
        foreach ($json as $entry) {
306 2
            $item = Item::fromImport($entry);
307 2
            $item->setLastModified($this->timeFactory->getTime());
308 2
            $feedLink = $entry['feedLink'];  // this is not set on the item yet
309
310 2
            if(array_key_exists($feedLink, $feedsDict)) {
311 2
                $feed = $feedsDict[$feedLink];
312 2
                $item->setFeedId($feed->getId());
313 2
            } elseif(array_key_exists($url, $feedsDict)) {
314
                $feed = $feedsDict[$url];
315
                $item->setFeedId($feed->getId());
316
            } else {
317 1
                $createdFeed = true;
318 1
                $feed = new Feed();
319 1
                $feed->setUserId($userId);
320 1
                $feed->setLink($url);
321 1
                $feed->setUrl($url);
322 1
                $feed->setTitle($this->l10n->t('Articles without feed'));
323 1
                $feed->setAdded($this->timeFactory->getTime());
324 1
                $feed->setFolderId(0);
325 1
                $feed->setPreventUpdate(true);
326 1
                $feed = $this->feedMapper->insert($feed);
327
328 1
                $item->setFeedId($feed->getId());
329 1
                $feedsDict[$feed->getLink()] = $feed;
330
            }
331
332
            try {
333
                // if item exists, copy the status
334 2
                $existingItem = $this->itemMapper->findByGuidHash(
335 2
                    $item->getGuidHash(), $feed->getId(), $userId);
336 1
                $existingItem->setStatus($item->getStatus());
337 1
                $this->itemMapper->update($existingItem);
338 2
            } catch(DoesNotExistException $ex){
339 2
                $item->setBody($this->purifier->purify($item->getBody()));
340 2
                $item->generateSearchIndex();
341 2
                $this->itemMapper->insert($item);
342
            }
343 2
        }
344
345 2
        if($createdFeed) {
346 1
            return $this->feedMapper->findByUrlHash($urlHash, $userId);
347
        }
348
349 1
        return null;
350
    }
351
352
353
    /**
354
     * Use this to mark a feed as deleted. That way it can be un-deleted
355
     * @param int $feedId the id of the feed that should be deleted
356
     * @param string $userId the name of the user for security reasons
357
     * @throws ServiceNotFoundException when feed does not exist
358
     */
359 1
    public function markDeleted($feedId, $userId) {
360 1
        $feed = $this->find($feedId, $userId);
361 1
        $feed->setDeletedAt($this->timeFactory->getTime());
362 1
        $this->feedMapper->update($feed);
363 1
    }
364
365
366
    /**
367
     * Use this to undo a feed deletion
368
     * @param int $feedId the id of the feed that should be restored
369
     * @param string $userId the name of the user for security reasons
370
     * @throws ServiceNotFoundException when feed does not exist
371
     */
372 1
    public function unmarkDeleted($feedId, $userId) {
373 1
        $feed = $this->find($feedId, $userId);
374 1
        $feed->setDeletedAt(0);
375 1
        $this->feedMapper->update($feed);
376 1
    }
377
378
379
    /**
380
     * Deletes all deleted feeds
381
     * @param string $userId if given it purges only feeds of that user
382
     * @param boolean $useInterval defaults to true, if true it only purges
383
     * entries in a given interval to give the user a chance to undo the
384
     * deletion
385
     */
386 2
    public function purgeDeleted($userId=null, $useInterval=true) {
387 2
        $deleteOlderThan = null;
388
389 2
        if ($useInterval) {
390 1
            $now = $this->timeFactory->getTime();
391 1
            $deleteOlderThan = $now - $this->autoPurgeMinimumInterval;
392 1
        }
393
394 2
        $toDelete = $this->feedMapper->getToDelete($deleteOlderThan, $userId);
395
396 2
        foreach ($toDelete as $feed) {
397 2
            $this->feedMapper->delete($feed);
398 2
        }
399 2
    }
400
401
402
    /**
403
     * Deletes all feeds of a user, delete items first since the user_id
404
     * is not defined in there
405
     * @param string $userId the name of the user
406
     */
407 1
    public function deleteUser($userId) {
408 1
        $this->feedMapper->deleteUser($userId);
409 1
    }
410
411
    /**
412
     * @param $feedId
413
     * @param $userId
414
     * @param $diff an array containing the fields to update, e.g.:
415
     *  [
416
     *      'ordering' => 1,
417
     *      'fullTextEnabled' => true,
418
     *      'pinned' => true,
419
     *      'updateMode' => 0,
420
     *      'title' => 'title'
421
     * ]
422
     * @throws ServiceNotFoundException if feed does not exist
423
     */
424 6
    public function patch($feedId, $userId, $diff=[]) {
425 6
        $feed = $this->find($feedId, $userId);
426
427 5
        foreach ($diff as $attribute => $value) {
428 5
            $method = 'set' . ucfirst($attribute);
429 5
            $feed->$method($value);
430 5
        }
431
432
        // special feed updates
433 5
        if (array_key_exists('fullTextEnabled', $diff)) {
434
            // disable caching for the next update
435 1
            $feed->setEtag('');
436 1
            $feed->setLastModified(0);
437 1
            $this->feedMapper->update($feed);
438 1
            return $this->update($feedId, $userId, true);
439
        }
440
441 4
        return $this->feedMapper->update($feed);
442
    }
443
444
}
445