aboutsummaryrefslogtreecommitdiffhomepage
path: root/tests/bookmark
diff options
context:
space:
mode:
Diffstat (limited to 'tests/bookmark')
-rw-r--r--tests/bookmark/LinkDBTest.php622
-rw-r--r--tests/bookmark/LinkFilterTest.php507
-rw-r--r--tests/bookmark/LinkUtilsTest.php506
3 files changed, 1635 insertions, 0 deletions
diff --git a/tests/bookmark/LinkDBTest.php b/tests/bookmark/LinkDBTest.php
new file mode 100644
index 00000000..2990a6b5
--- /dev/null
+++ b/tests/bookmark/LinkDBTest.php
@@ -0,0 +1,622 @@
1<?php
2/**
3 * Link datastore tests
4 */
5
6namespace Shaarli\Bookmark;
7
8use DateTime;
9use ReferenceLinkDB;
10use ReflectionClass;
11use Shaarli;
12
13require_once 'application/feed/Cache.php';
14require_once 'application/Utils.php';
15require_once 'tests/utils/ReferenceLinkDB.php';
16
17
18/**
19 * Unitary tests for LinkDB
20 */
21class LinkDBTest extends \PHPUnit\Framework\TestCase
22{
23 // datastore to test write operations
24 protected static $testDatastore = 'sandbox/datastore.php';
25
26 /**
27 * @var ReferenceLinkDB instance.
28 */
29 protected static $refDB = null;
30
31 /**
32 * @var LinkDB public LinkDB instance.
33 */
34 protected static $publicLinkDB = null;
35
36 /**
37 * @var LinkDB private LinkDB instance.
38 */
39 protected static $privateLinkDB = null;
40
41 /**
42 * Instantiates public and private LinkDBs with test data
43 *
44 * The reference datastore contains public and private links that
45 * will be used to test LinkDB's methods:
46 * - access filtering (public/private),
47 * - link searches:
48 * - by day,
49 * - by tag,
50 * - by text,
51 * - etc.
52 */
53 public static function setUpBeforeClass()
54 {
55 self::$refDB = new ReferenceLinkDB();
56 self::$refDB->write(self::$testDatastore);
57
58 self::$publicLinkDB = new LinkDB(self::$testDatastore, false, false);
59 self::$privateLinkDB = new LinkDB(self::$testDatastore, true, false);
60 }
61
62 /**
63 * Resets test data for each test
64 */
65 protected function setUp()
66 {
67 if (file_exists(self::$testDatastore)) {
68 unlink(self::$testDatastore);
69 }
70 }
71
72 /**
73 * Allows to test LinkDB's private methods
74 *
75 * @see
76 * https://sebastian-bergmann.de/archives/881-Testing-Your-Privates.html
77 * http://stackoverflow.com/a/2798203
78 */
79 protected static function getMethod($name)
80 {
81 $class = new ReflectionClass('Shaarli\Bookmark\LinkDB');
82 $method = $class->getMethod($name);
83 $method->setAccessible(true);
84 return $method;
85 }
86
87 /**
88 * Instantiate LinkDB objects - logged in user
89 */
90 public function testConstructLoggedIn()
91 {
92 new LinkDB(self::$testDatastore, true, false);
93 $this->assertFileExists(self::$testDatastore);
94 }
95
96 /**
97 * Instantiate LinkDB objects - logged out or public instance
98 */
99 public function testConstructLoggedOut()
100 {
101 new LinkDB(self::$testDatastore, false, false);
102 $this->assertFileExists(self::$testDatastore);
103 }
104
105 /**
106 * Attempt to instantiate a LinkDB whereas the datastore is not writable
107 *
108 * @expectedException Shaarli\Exceptions\IOException
109 * @expectedExceptionMessageRegExp /Error accessing "null"/
110 */
111 public function testConstructDatastoreNotWriteable()
112 {
113 new LinkDB('null/store.db', false, false);
114 }
115
116 /**
117 * The DB doesn't exist, ensure it is created with dummy content
118 */
119 public function testCheckDBNew()
120 {
121 $linkDB = new LinkDB(self::$testDatastore, false, false);
122 unlink(self::$testDatastore);
123 $this->assertFileNotExists(self::$testDatastore);
124
125 $checkDB = self::getMethod('check');
126 $checkDB->invokeArgs($linkDB, array());
127 $this->assertFileExists(self::$testDatastore);
128
129 // ensure the correct data has been written
130 $this->assertGreaterThan(0, filesize(self::$testDatastore));
131 }
132
133 /**
134 * The DB exists, don't do anything
135 */
136 public function testCheckDBLoad()
137 {
138 $linkDB = new LinkDB(self::$testDatastore, false, false);
139 $datastoreSize = filesize(self::$testDatastore);
140 $this->assertGreaterThan(0, $datastoreSize);
141
142 $checkDB = self::getMethod('check');
143 $checkDB->invokeArgs($linkDB, array());
144
145 // ensure the datastore is left unmodified
146 $this->assertEquals(
147 $datastoreSize,
148 filesize(self::$testDatastore)
149 );
150 }
151
152 /**
153 * Load an empty DB
154 */
155 public function testReadEmptyDB()
156 {
157 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
158 $emptyDB = new LinkDB(self::$testDatastore, false, false);
159 $this->assertEquals(0, sizeof($emptyDB));
160 $this->assertEquals(0, count($emptyDB));
161 }
162
163 /**
164 * Load public links from the DB
165 */
166 public function testReadPublicDB()
167 {
168 $this->assertEquals(
169 self::$refDB->countPublicLinks(),
170 sizeof(self::$publicLinkDB)
171 );
172 }
173
174 /**
175 * Load public and private links from the DB
176 */
177 public function testReadPrivateDB()
178 {
179 $this->assertEquals(
180 self::$refDB->countLinks(),
181 sizeof(self::$privateLinkDB)
182 );
183 }
184
185 /**
186 * Save the links to the DB
187 */
188 public function testSave()
189 {
190 $testDB = new LinkDB(self::$testDatastore, true, false);
191 $dbSize = sizeof($testDB);
192
193 $link = array(
194 'id' => 42,
195 'title' => 'an additional link',
196 'url' => 'http://dum.my',
197 'description' => 'One more',
198 'private' => 0,
199 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150518_190000'),
200 'tags' => 'unit test'
201 );
202 $testDB[$link['id']] = $link;
203 $testDB->save('tests');
204
205 $testDB = new LinkDB(self::$testDatastore, true, false);
206 $this->assertEquals($dbSize + 1, sizeof($testDB));
207 }
208
209 /**
210 * Count existing links
211 */
212 public function testCount()
213 {
214 $this->assertEquals(
215 self::$refDB->countPublicLinks(),
216 self::$publicLinkDB->count()
217 );
218 $this->assertEquals(
219 self::$refDB->countLinks(),
220 self::$privateLinkDB->count()
221 );
222 }
223
224 /**
225 * Count existing links - public links hidden
226 */
227 public function testCountHiddenPublic()
228 {
229 $linkDB = new LinkDB(self::$testDatastore, false, true);
230
231 $this->assertEquals(
232 0,
233 $linkDB->count()
234 );
235 $this->assertEquals(
236 0,
237 $linkDB->count()
238 );
239 }
240
241 /**
242 * List the days for which links have been posted
243 */
244 public function testDays()
245 {
246 $this->assertEquals(
247 array('20100309', '20100310', '20121206', '20121207', '20130614', '20150310'),
248 self::$publicLinkDB->days()
249 );
250
251 $this->assertEquals(
252 array('20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'),
253 self::$privateLinkDB->days()
254 );
255 }
256
257 /**
258 * The URL corresponds to an existing entry in the DB
259 */
260 public function testGetKnownLinkFromURL()
261 {
262 $link = self::$publicLinkDB->getLinkFromUrl('http://mediagoblin.org/');
263
264 $this->assertNotEquals(false, $link);
265 $this->assertContains(
266 'A free software media publishing platform',
267 $link['description']
268 );
269 }
270
271 /**
272 * The URL is not in the DB
273 */
274 public function testGetUnknownLinkFromURL()
275 {
276 $this->assertEquals(
277 false,
278 self::$publicLinkDB->getLinkFromUrl('http://dev.null')
279 );
280 }
281
282 /**
283 * Lists all tags
284 */
285 public function testAllTags()
286 {
287 $this->assertEquals(
288 array(
289 'web' => 3,
290 'cartoon' => 2,
291 'gnu' => 2,
292 'dev' => 1,
293 'samba' => 1,
294 'media' => 1,
295 'software' => 1,
296 'stallman' => 1,
297 'free' => 1,
298 '-exclude' => 1,
299 'hashtag' => 2,
300 // The DB contains a link with `sTuff` and another one with `stuff` tag.
301 // They need to be grouped with the first case found - order by date DESC: `sTuff`.
302 'sTuff' => 2,
303 'ut' => 1,
304 ),
305 self::$publicLinkDB->linksCountPerTag()
306 );
307
308 $this->assertEquals(
309 array(
310 'web' => 4,
311 'cartoon' => 3,
312 'gnu' => 2,
313 'dev' => 2,
314 'samba' => 1,
315 'media' => 1,
316 'software' => 1,
317 'stallman' => 1,
318 'free' => 1,
319 'html' => 1,
320 'w3c' => 1,
321 'css' => 1,
322 'Mercurial' => 1,
323 'sTuff' => 2,
324 '-exclude' => 1,
325 '.hidden' => 1,
326 'hashtag' => 2,
327 'tag1' => 1,
328 'tag2' => 1,
329 'tag3' => 1,
330 'tag4' => 1,
331 'ut' => 1,
332 ),
333 self::$privateLinkDB->linksCountPerTag()
334 );
335 $this->assertEquals(
336 array(
337 'web' => 4,
338 'cartoon' => 2,
339 'gnu' => 1,
340 'dev' => 1,
341 'samba' => 1,
342 'media' => 1,
343 'html' => 1,
344 'w3c' => 1,
345 'css' => 1,
346 'Mercurial' => 1,
347 '.hidden' => 1,
348 'hashtag' => 1,
349 ),
350 self::$privateLinkDB->linksCountPerTag(['web'])
351 );
352 $this->assertEquals(
353 array(
354 'web' => 1,
355 'html' => 1,
356 'w3c' => 1,
357 'css' => 1,
358 'Mercurial' => 1,
359 ),
360 self::$privateLinkDB->linksCountPerTag(['web'], 'private')
361 );
362 }
363
364 /**
365 * Test filter with string.
366 */
367 public function testFilterString()
368 {
369 $tags = 'dev cartoon';
370 $request = array('searchtags' => $tags);
371 $this->assertEquals(
372 2,
373 count(self::$privateLinkDB->filterSearch($request, true, false))
374 );
375 }
376
377 /**
378 * Test filter with string.
379 */
380 public function testFilterArray()
381 {
382 $tags = array('dev', 'cartoon');
383 $request = array('searchtags' => $tags);
384 $this->assertEquals(
385 2,
386 count(self::$privateLinkDB->filterSearch($request, true, false))
387 );
388 }
389
390 /**
391 * Test hidden tags feature:
392 * tags starting with a dot '.' are only visible when logged in.
393 */
394 public function testHiddenTags()
395 {
396 $tags = '.hidden';
397 $request = array('searchtags' => $tags);
398 $this->assertEquals(
399 1,
400 count(self::$privateLinkDB->filterSearch($request, true, false))
401 );
402
403 $this->assertEquals(
404 0,
405 count(self::$publicLinkDB->filterSearch($request, true, false))
406 );
407 }
408
409 /**
410 * Test filterHash() with a valid smallhash.
411 */
412 public function testFilterHashValid()
413 {
414 $request = smallHash('20150310_114651');
415 $this->assertEquals(
416 1,
417 count(self::$publicLinkDB->filterHash($request))
418 );
419 $request = smallHash('20150310_114633' . 8);
420 $this->assertEquals(
421 1,
422 count(self::$publicLinkDB->filterHash($request))
423 );
424 }
425
426 /**
427 * Test filterHash() with an invalid smallhash.
428 *
429 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException
430 */
431 public function testFilterHashInValid1()
432 {
433 $request = 'blabla';
434 self::$publicLinkDB->filterHash($request);
435 }
436
437 /**
438 * Test filterHash() with an empty smallhash.
439 *
440 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException
441 */
442 public function testFilterHashInValid()
443 {
444 self::$publicLinkDB->filterHash('');
445 }
446
447 /**
448 * Test reorder with asc/desc parameter.
449 */
450 public function testReorderLinksDesc()
451 {
452 self::$privateLinkDB->reorder('ASC');
453 $stickyIds = [11, 10];
454 $standardIds = [42, 4, 9, 1, 0, 7, 6, 8, 41];
455 $linkIds = array_merge($stickyIds, $standardIds);
456 $cpt = 0;
457 foreach (self::$privateLinkDB as $key => $value) {
458 $this->assertEquals($linkIds[$cpt++], $key);
459 }
460 self::$privateLinkDB->reorder('DESC');
461 $linkIds = array_merge(array_reverse($stickyIds), array_reverse($standardIds));
462 $cpt = 0;
463 foreach (self::$privateLinkDB as $key => $value) {
464 $this->assertEquals($linkIds[$cpt++], $key);
465 }
466 }
467
468 /**
469 * Test rename tag with a valid value present in multiple links
470 */
471 public function testRenameTagMultiple()
472 {
473 self::$refDB->write(self::$testDatastore);
474 $linkDB = new LinkDB(self::$testDatastore, true, false);
475
476 $res = $linkDB->renameTag('cartoon', 'Taz');
477 $this->assertEquals(3, count($res));
478 $this->assertContains(' Taz ', $linkDB[4]['tags']);
479 $this->assertContains(' Taz ', $linkDB[1]['tags']);
480 $this->assertContains(' Taz ', $linkDB[0]['tags']);
481 }
482
483 /**
484 * Test rename tag with a valid value
485 */
486 public function testRenameTagCaseSensitive()
487 {
488 self::$refDB->write(self::$testDatastore);
489 $linkDB = new LinkDB(self::$testDatastore, true, false);
490
491 $res = $linkDB->renameTag('sTuff', 'Taz');
492 $this->assertEquals(1, count($res));
493 $this->assertEquals('Taz', $linkDB[41]['tags']);
494 }
495
496 /**
497 * Test rename tag with invalid values
498 */
499 public function testRenameTagInvalid()
500 {
501 $linkDB = new LinkDB(self::$testDatastore, false, false);
502
503 $this->assertFalse($linkDB->renameTag('', 'test'));
504 $this->assertFalse($linkDB->renameTag('', ''));
505 // tag non existent
506 $this->assertEquals([], $linkDB->renameTag('test', ''));
507 $this->assertEquals([], $linkDB->renameTag('test', 'retest'));
508 }
509
510 /**
511 * Test delete tag with a valid value
512 */
513 public function testDeleteTag()
514 {
515 self::$refDB->write(self::$testDatastore);
516 $linkDB = new LinkDB(self::$testDatastore, true, false);
517
518 $res = $linkDB->renameTag('cartoon', null);
519 $this->assertEquals(3, count($res));
520 $this->assertNotContains('cartoon', $linkDB[4]['tags']);
521 }
522
523 /**
524 * Test linksCountPerTag all tags without filter.
525 * Equal occurrences should be sorted alphabetically.
526 */
527 public function testCountLinkPerTagAllNoFilter()
528 {
529 $expected = [
530 'web' => 4,
531 'cartoon' => 3,
532 'dev' => 2,
533 'gnu' => 2,
534 'hashtag' => 2,
535 'sTuff' => 2,
536 '-exclude' => 1,
537 '.hidden' => 1,
538 'Mercurial' => 1,
539 'css' => 1,
540 'free' => 1,
541 'html' => 1,
542 'media' => 1,
543 'samba' => 1,
544 'software' => 1,
545 'stallman' => 1,
546 'tag1' => 1,
547 'tag2' => 1,
548 'tag3' => 1,
549 'tag4' => 1,
550 'ut' => 1,
551 'w3c' => 1,
552 ];
553 $tags = self::$privateLinkDB->linksCountPerTag();
554
555 $this->assertEquals($expected, $tags, var_export($tags, true));
556 }
557
558 /**
559 * Test linksCountPerTag all tags with filter.
560 * Equal occurrences should be sorted alphabetically.
561 */
562 public function testCountLinkPerTagAllWithFilter()
563 {
564 $expected = [
565 'gnu' => 2,
566 'hashtag' => 2,
567 '-exclude' => 1,
568 '.hidden' => 1,
569 'free' => 1,
570 'media' => 1,
571 'software' => 1,
572 'stallman' => 1,
573 'stuff' => 1,
574 'web' => 1,
575 ];
576 $tags = self::$privateLinkDB->linksCountPerTag(['gnu']);
577
578 $this->assertEquals($expected, $tags, var_export($tags, true));
579 }
580
581 /**
582 * Test linksCountPerTag public tags with filter.
583 * Equal occurrences should be sorted alphabetically.
584 */
585 public function testCountLinkPerTagPublicWithFilter()
586 {
587 $expected = [
588 'gnu' => 2,
589 'hashtag' => 2,
590 '-exclude' => 1,
591 '.hidden' => 1,
592 'free' => 1,
593 'media' => 1,
594 'software' => 1,
595 'stallman' => 1,
596 'stuff' => 1,
597 'web' => 1,
598 ];
599 $tags = self::$privateLinkDB->linksCountPerTag(['gnu'], 'public');
600
601 $this->assertEquals($expected, $tags, var_export($tags, true));
602 }
603
604 /**
605 * Test linksCountPerTag public tags with filter.
606 * Equal occurrences should be sorted alphabetically.
607 */
608 public function testCountLinkPerTagPrivateWithFilter()
609 {
610 $expected = [
611 'cartoon' => 1,
612 'dev' => 1,
613 'tag1' => 1,
614 'tag2' => 1,
615 'tag3' => 1,
616 'tag4' => 1,
617 ];
618 $tags = self::$privateLinkDB->linksCountPerTag(['dev'], 'private');
619
620 $this->assertEquals($expected, $tags, var_export($tags, true));
621 }
622}
diff --git a/tests/bookmark/LinkFilterTest.php b/tests/bookmark/LinkFilterTest.php
new file mode 100644
index 00000000..808f8122
--- /dev/null
+++ b/tests/bookmark/LinkFilterTest.php
@@ -0,0 +1,507 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Exception;
6use ReferenceLinkDB;
7
8/**
9 * Class LinkFilterTest.
10 */
11class LinkFilterTest extends \PHPUnit\Framework\TestCase
12{
13 /**
14 * @var string Test datastore path.
15 */
16 protected static $testDatastore = 'sandbox/datastore.php';
17 /**
18 * @var LinkFilter instance.
19 */
20 protected static $linkFilter;
21
22 /**
23 * @var ReferenceLinkDB instance
24 */
25 protected static $refDB;
26
27 /**
28 * @var LinkDB instance
29 */
30 protected static $linkDB;
31
32 /**
33 * Instantiate linkFilter with ReferenceLinkDB data.
34 */
35 public static function setUpBeforeClass()
36 {
37 self::$refDB = new ReferenceLinkDB();
38 self::$refDB->write(self::$testDatastore);
39 self::$linkDB = new LinkDB(self::$testDatastore, true, false);
40 self::$linkFilter = new LinkFilter(self::$linkDB);
41 }
42
43 /**
44 * Blank filter.
45 */
46 public function testFilter()
47 {
48 $this->assertEquals(
49 self::$refDB->countLinks(),
50 count(self::$linkFilter->filter('', ''))
51 );
52
53 $this->assertEquals(
54 self::$refDB->countLinks(),
55 count(self::$linkFilter->filter('', '', 'all'))
56 );
57
58 $this->assertEquals(
59 self::$refDB->countLinks(),
60 count(self::$linkFilter->filter('', '', 'randomstr'))
61 );
62
63 // Private only.
64 $this->assertEquals(
65 self::$refDB->countPrivateLinks(),
66 count(self::$linkFilter->filter('', '', false, 'private'))
67 );
68
69 // Public only.
70 $this->assertEquals(
71 self::$refDB->countPublicLinks(),
72 count(self::$linkFilter->filter('', '', false, 'public'))
73 );
74
75 $this->assertEquals(
76 ReferenceLinkDB::$NB_LINKS_TOTAL,
77 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, ''))
78 );
79
80 $this->assertEquals(
81 self::$refDB->countUntaggedLinks(),
82 count(
83 self::$linkFilter->filter(
84 LinkFilter::$FILTER_TAG,
85 /*$request=*/
86 '',
87 /*$casesensitive=*/
88 false,
89 /*$visibility=*/
90 'all',
91 /*$untaggedonly=*/
92 true
93 )
94 )
95 );
96
97 $this->assertEquals(
98 ReferenceLinkDB::$NB_LINKS_TOTAL,
99 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, ''))
100 );
101 }
102
103 /**
104 * Filter links using a tag
105 */
106 public function testFilterOneTag()
107 {
108 $this->assertEquals(
109 4,
110 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false))
111 );
112
113 $this->assertEquals(
114 4,
115 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'all'))
116 );
117
118 $this->assertEquals(
119 4,
120 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'default-blabla'))
121 );
122
123 // Private only.
124 $this->assertEquals(
125 1,
126 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'private'))
127 );
128
129 // Public only.
130 $this->assertEquals(
131 3,
132 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'public'))
133 );
134 }
135
136 /**
137 * Filter links using a tag - case-sensitive
138 */
139 public function testFilterCaseSensitiveTag()
140 {
141 $this->assertEquals(
142 0,
143 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'mercurial', true))
144 );
145
146 $this->assertEquals(
147 1,
148 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'Mercurial', true))
149 );
150 }
151
152 /**
153 * Filter links using a tag combination
154 */
155 public function testFilterMultipleTags()
156 {
157 $this->assertEquals(
158 2,
159 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'dev cartoon', false))
160 );
161 }
162
163 /**
164 * Filter links using a non-existent tag
165 */
166 public function testFilterUnknownTag()
167 {
168 $this->assertEquals(
169 0,
170 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'null', false))
171 );
172 }
173
174 /**
175 * Return links for a given day
176 */
177 public function testFilterDay()
178 {
179 $this->assertEquals(
180 4,
181 count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206'))
182 );
183 }
184
185 /**
186 * 404 - day not found
187 */
188 public function testFilterUnknownDay()
189 {
190 $this->assertEquals(
191 0,
192 count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '19700101'))
193 );
194 }
195
196 /**
197 * Use an invalid date format
198 * @expectedException Exception
199 * @expectedExceptionMessageRegExp /Invalid date format/
200 */
201 public function testFilterInvalidDayWithChars()
202 {
203 self::$linkFilter->filter(LinkFilter::$FILTER_DAY, 'Rainy day, dream away');
204 }
205
206 /**
207 * Use an invalid date format
208 * @expectedException Exception
209 * @expectedExceptionMessageRegExp /Invalid date format/
210 */
211 public function testFilterInvalidDayDigits()
212 {
213 self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20');
214 }
215
216 /**
217 * Retrieve a link entry with its hash
218 */
219 public function testFilterSmallHash()
220 {
221 $links = self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'IuWvgA');
222
223 $this->assertEquals(
224 1,
225 count($links)
226 );
227
228 $this->assertEquals(
229 'MediaGoblin',
230 $links[7]['title']
231 );
232 }
233
234 /**
235 * No link for this hash
236 *
237 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException
238 */
239 public function testFilterUnknownSmallHash()
240 {
241 self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah');
242 }
243
244 /**
245 * Full-text search - no result found.
246 */
247 public function testFilterFullTextNoResult()
248 {
249 $this->assertEquals(
250 0,
251 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'azertyuiop'))
252 );
253 }
254
255 /**
256 * Full-text search - result from a link's URL
257 */
258 public function testFilterFullTextURL()
259 {
260 $this->assertEquals(
261 2,
262 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
263 );
264
265 $this->assertEquals(
266 2,
267 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars org'))
268 );
269 }
270
271 /**
272 * Full-text search - result from a link's title only
273 */
274 public function testFilterFullTextTitle()
275 {
276 // use miscellaneous cases
277 $this->assertEquals(
278 2,
279 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'userfriendly -'))
280 );
281 $this->assertEquals(
282 2,
283 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'UserFriendly -'))
284 );
285 $this->assertEquals(
286 2,
287 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
288 );
289
290 // use miscellaneous case and offset
291 $this->assertEquals(
292 2,
293 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'RFrIendL'))
294 );
295 }
296
297 /**
298 * Full-text search - result from the link's description only
299 */
300 public function testFilterFullTextDescription()
301 {
302 $this->assertEquals(
303 1,
304 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'publishing media'))
305 );
306
307 $this->assertEquals(
308 1,
309 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'mercurial w3c'))
310 );
311
312 $this->assertEquals(
313 3,
314 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '"free software"'))
315 );
316 }
317
318 /**
319 * Full-text search - result from the link's tags only
320 */
321 public function testFilterFullTextTags()
322 {
323 $this->assertEquals(
324 6,
325 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web'))
326 );
327
328 $this->assertEquals(
329 6,
330 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', 'all'))
331 );
332
333 $this->assertEquals(
334 6,
335 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', 'bla'))
336 );
337
338 // Private only.
339 $this->assertEquals(
340 1,
341 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, 'private'))
342 );
343
344 // Public only.
345 $this->assertEquals(
346 5,
347 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, 'public'))
348 );
349 }
350
351 /**
352 * Full-text search - result set from mixed sources
353 */
354 public function testFilterFullTextMixed()
355 {
356 $this->assertEquals(
357 3,
358 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software'))
359 );
360 }
361
362 /**
363 * Full-text search - test exclusion with '-'.
364 */
365 public function testExcludeSearch()
366 {
367 $this->assertEquals(
368 1,
369 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free -gnu'))
370 );
371
372 $this->assertEquals(
373 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
374 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution'))
375 );
376 }
377
378 /**
379 * Full-text search - test AND, exact terms and exclusion combined, across fields.
380 */
381 public function testMultiSearch()
382 {
383 $this->assertEquals(
384 2,
385 count(self::$linkFilter->filter(
386 LinkFilter::$FILTER_TEXT,
387 '"Free Software " stallman "read this" @website stuff'
388 ))
389 );
390
391 $this->assertEquals(
392 1,
393 count(self::$linkFilter->filter(
394 LinkFilter::$FILTER_TEXT,
395 '"free software " stallman "read this" -beard @website stuff'
396 ))
397 );
398 }
399
400 /**
401 * Full-text search - make sure that exact search won't work across fields.
402 */
403 public function testSearchExactTermMultiFieldsKo()
404 {
405 $this->assertEquals(
406 0,
407 count(self::$linkFilter->filter(
408 LinkFilter::$FILTER_TEXT,
409 '"designer naming"'
410 ))
411 );
412
413 $this->assertEquals(
414 0,
415 count(self::$linkFilter->filter(
416 LinkFilter::$FILTER_TEXT,
417 '"designernaming"'
418 ))
419 );
420 }
421
422 /**
423 * Tag search with exclusion.
424 */
425 public function testTagFilterWithExclusion()
426 {
427 $this->assertEquals(
428 1,
429 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'gnu -free'))
430 );
431
432 $this->assertEquals(
433 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
434 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free'))
435 );
436 }
437
438 /**
439 * Test crossed search (terms + tags).
440 */
441 public function testFilterCrossedSearch()
442 {
443 $terms = '"Free Software " stallman "read this" @website stuff';
444 $tags = 'free';
445 $this->assertEquals(
446 1,
447 count(self::$linkFilter->filter(
448 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
449 array($tags, $terms)
450 ))
451 );
452 $this->assertEquals(
453 2,
454 count(self::$linkFilter->filter(
455 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
456 array('', $terms)
457 ))
458 );
459 $this->assertEquals(
460 1,
461 count(self::$linkFilter->filter(
462 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
463 array(false, 'PSR-2')
464 ))
465 );
466 $this->assertEquals(
467 1,
468 count(self::$linkFilter->filter(
469 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
470 array($tags, '')
471 ))
472 );
473 $this->assertEquals(
474 ReferenceLinkDB::$NB_LINKS_TOTAL,
475 count(self::$linkFilter->filter(
476 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT,
477 ''
478 ))
479 );
480 }
481
482 /**
483 * Filter links by #hashtag.
484 */
485 public function testFilterByHashtag()
486 {
487 $hashtag = 'hashtag';
488 $this->assertEquals(
489 3,
490 count(self::$linkFilter->filter(
491 LinkFilter::$FILTER_TAG,
492 $hashtag
493 ))
494 );
495
496 $hashtag = 'private';
497 $this->assertEquals(
498 1,
499 count(self::$linkFilter->filter(
500 LinkFilter::$FILTER_TAG,
501 $hashtag,
502 false,
503 'private'
504 ))
505 );
506 }
507}
diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php
new file mode 100644
index 00000000..78cb8f2a
--- /dev/null
+++ b/tests/bookmark/LinkUtilsTest.php
@@ -0,0 +1,506 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use PHPUnit\Framework\TestCase;
6use ReferenceLinkDB;
7use Shaarli\Config\ConfigManager;
8
9require_once 'tests/utils/CurlUtils.php';
10
11/**
12 * Class LinkUtilsTest.
13 */
14class LinkUtilsTest extends TestCase
15{
16 /**
17 * Test html_extract_title() when the title is found.
18 */
19 public function testHtmlExtractExistentTitle()
20 {
21 $title = 'Read me please.';
22 $html = '<html><meta>stuff</meta><title>' . $title . '</title></html>';
23 $this->assertEquals($title, html_extract_title($html));
24 $html = '<html><title>' . $title . '</title>blabla<title>another</title></html>';
25 $this->assertEquals($title, html_extract_title($html));
26 }
27
28 /**
29 * Test html_extract_title() when the title is not found.
30 */
31 public function testHtmlExtractNonExistentTitle()
32 {
33 $html = '<html><meta>stuff</meta></html>';
34 $this->assertFalse(html_extract_title($html));
35 }
36
37 /**
38 * Test headers_extract_charset() when the charset is found.
39 */
40 public function testHeadersExtractExistentCharset()
41 {
42 $charset = 'x-MacCroatian';
43 $headers = 'text/html; charset=' . $charset;
44 $this->assertEquals(strtolower($charset), header_extract_charset($headers));
45 }
46
47 /**
48 * Test headers_extract_charset() when the charset is not found.
49 */
50 public function testHeadersExtractNonExistentCharset()
51 {
52 $headers = '';
53 $this->assertFalse(header_extract_charset($headers));
54
55 $headers = 'text/html';
56 $this->assertFalse(header_extract_charset($headers));
57 }
58
59 /**
60 * Test html_extract_charset() when the charset is found.
61 */
62 public function testHtmlExtractExistentCharset()
63 {
64 $charset = 'x-MacCroatian';
65 $html = '<html><meta>stuff2</meta><meta charset="' . $charset . '"/></html>';
66 $this->assertEquals(strtolower($charset), html_extract_charset($html));
67 }
68
69 /**
70 * Test html_extract_charset() when the charset is not found.
71 */
72 public function testHtmlExtractNonExistentCharset()
73 {
74 $html = '<html><meta>stuff</meta></html>';
75 $this->assertFalse(html_extract_charset($html));
76 $html = '<html><meta>stuff</meta><meta charset=""/></html>';
77 $this->assertFalse(html_extract_charset($html));
78 }
79
80 /**
81 * Test html_extract_tag() when the tag <meta name= is found.
82 */
83 public function testHtmlExtractExistentNameTag()
84 {
85 $description = 'Bob and Alice share cookies.';
86 $html = '<html><meta>stuff2</meta><meta name="description" content="' . $description . '"/></html>';
87 $this->assertEquals($description, html_extract_tag('description', $html));
88 }
89
90 /**
91 * Test html_extract_tag() when the tag <meta name= is not found.
92 */
93 public function testHtmlExtractNonExistentNameTag()
94 {
95 $html = '<html><meta>stuff2</meta><meta name="image" content="img"/></html>';
96 $this->assertFalse(html_extract_tag('description', $html));
97 }
98
99 /**
100 * Test html_extract_tag() when the tag <meta property="og: is found.
101 */
102 public function testHtmlExtractExistentOgTag()
103 {
104 $description = 'Bob and Alice share cookies.';
105 $html = '<html><meta>stuff2</meta><meta property="og:description" content="' . $description . '"/></html>';
106 $this->assertEquals($description, html_extract_tag('description', $html));
107 }
108
109 /**
110 * Test html_extract_tag() when the tag <meta property="og: is not found.
111 */
112 public function testHtmlExtractNonExistentOgTag()
113 {
114 $html = '<html><meta>stuff2</meta><meta name="image" content="img"/></html>';
115 $this->assertFalse(html_extract_tag('description', $html));
116 }
117
118 /**
119 * Test the download callback with valid value
120 */
121 public function testCurlDownloadCallbackOk()
122 {
123 $callback = get_curl_download_callback(
124 $charset,
125 $title,
126 $desc,
127 $keywords,
128 false,
129 'ut_curl_getinfo_ok'
130 );
131 $data = [
132 'HTTP/1.1 200 OK',
133 'Server: GitHub.com',
134 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
135 'Content-Type: text/html; charset=utf-8',
136 'Status: 200 OK',
137 'end' => 'th=device-width">'
138 . '<title>Refactoring · GitHub</title>'
139 . '<link rel="search" type="application/opensea',
140 '<title>ignored</title>'
141 . '<meta name="description" content="desc" />'
142 . '<meta name="keywords" content="key1,key2" />',
143 ];
144 foreach ($data as $key => $line) {
145 $ignore = null;
146 $expected = $key !== 'end' ? strlen($line) : false;
147 $this->assertEquals($expected, $callback($ignore, $line));
148 if ($expected === false) {
149 break;
150 }
151 }
152 $this->assertEquals('utf-8', $charset);
153 $this->assertEquals('Refactoring · GitHub', $title);
154 $this->assertEmpty($desc);
155 $this->assertEmpty($keywords);
156 }
157
158 /**
159 * Test the download callback with valid values and no charset
160 */
161 public function testCurlDownloadCallbackOkNoCharset()
162 {
163 $callback = get_curl_download_callback(
164 $charset,
165 $title,
166 $desc,
167 $keywords,
168 false,
169 'ut_curl_getinfo_no_charset'
170 );
171 $data = [
172 'HTTP/1.1 200 OK',
173 'end' => 'th=device-width">'
174 . '<title>Refactoring · GitHub</title>'
175 . '<link rel="search" type="application/opensea',
176 '<title>ignored</title>'
177 . '<meta name="description" content="desc" />'
178 . '<meta name="keywords" content="key1,key2" />',
179 ];
180 foreach ($data as $key => $line) {
181 $ignore = null;
182 $this->assertEquals(strlen($line), $callback($ignore, $line));
183 }
184 $this->assertEmpty($charset);
185 $this->assertEquals('Refactoring · GitHub', $title);
186 $this->assertEmpty($desc);
187 $this->assertEmpty($keywords);
188 }
189
190 /**
191 * Test the download callback with valid values and no charset
192 */
193 public function testCurlDownloadCallbackOkHtmlCharset()
194 {
195 $callback = get_curl_download_callback(
196 $charset,
197 $title,
198 $desc,
199 $keywords,
200 false,
201 'ut_curl_getinfo_no_charset'
202 );
203 $data = [
204 'HTTP/1.1 200 OK',
205 '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
206 'end' => 'th=device-width">'
207 . '<title>Refactoring · GitHub</title>'
208 . '<link rel="search" type="application/opensea',
209 '<title>ignored</title>'
210 . '<meta name="description" content="desc" />'
211 . '<meta name="keywords" content="key1,key2" />',
212 ];
213 foreach ($data as $key => $line) {
214 $ignore = null;
215 $expected = $key !== 'end' ? strlen($line) : false;
216 $this->assertEquals($expected, $callback($ignore, $line));
217 if ($expected === false) {
218 break;
219 }
220 }
221 $this->assertEquals('utf-8', $charset);
222 $this->assertEquals('Refactoring · GitHub', $title);
223 $this->assertEmpty($desc);
224 $this->assertEmpty($keywords);
225 }
226
227 /**
228 * Test the download callback with valid values and no title
229 */
230 public function testCurlDownloadCallbackOkNoTitle()
231 {
232 $callback = get_curl_download_callback(
233 $charset,
234 $title,
235 $desc,
236 $keywords,
237 false,
238 'ut_curl_getinfo_ok'
239 );
240 $data = [
241 'HTTP/1.1 200 OK',
242 'end' => 'th=device-width">Refactoring · GitHub<link rel="search" type="application/opensea',
243 'ignored',
244 ];
245 foreach ($data as $key => $line) {
246 $ignore = null;
247 $this->assertEquals(strlen($line), $callback($ignore, $line));
248 }
249 $this->assertEquals('utf-8', $charset);
250 $this->assertEmpty($title);
251 $this->assertEmpty($desc);
252 $this->assertEmpty($keywords);
253 }
254
255 /**
256 * Test the download callback with an invalid content type.
257 */
258 public function testCurlDownloadCallbackInvalidContentType()
259 {
260 $callback = get_curl_download_callback(
261 $charset,
262 $title,
263 $desc,
264 $keywords,
265 false,
266 'ut_curl_getinfo_ct_ko'
267 );
268 $ignore = null;
269 $this->assertFalse($callback($ignore, ''));
270 $this->assertEmpty($charset);
271 $this->assertEmpty($title);
272 }
273
274 /**
275 * Test the download callback with an invalid response code.
276 */
277 public function testCurlDownloadCallbackInvalidResponseCode()
278 {
279 $callback = $callback = get_curl_download_callback(
280 $charset,
281 $title,
282 $desc,
283 $keywords,
284 false,
285 'ut_curl_getinfo_rc_ko'
286 );
287 $ignore = null;
288 $this->assertFalse($callback($ignore, ''));
289 $this->assertEmpty($charset);
290 $this->assertEmpty($title);
291 }
292
293 /**
294 * Test the download callback with an invalid content type and response code.
295 */
296 public function testCurlDownloadCallbackInvalidContentTypeAndResponseCode()
297 {
298 $callback = $callback = get_curl_download_callback(
299 $charset,
300 $title,
301 $desc,
302 $keywords,
303 false,
304 'ut_curl_getinfo_rs_ct_ko'
305 );
306 $ignore = null;
307 $this->assertFalse($callback($ignore, ''));
308 $this->assertEmpty($charset);
309 $this->assertEmpty($title);
310 }
311
312 /**
313 * Test the download callback with valid value, and retrieve_description option enabled.
314 */
315 public function testCurlDownloadCallbackOkWithDesc()
316 {
317 $callback = get_curl_download_callback(
318 $charset,
319 $title,
320 $desc,
321 $keywords,
322 true,
323 'ut_curl_getinfo_ok'
324 );
325 $data = [
326 'HTTP/1.1 200 OK',
327 'Server: GitHub.com',
328 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
329 'Content-Type: text/html; charset=utf-8',
330 'Status: 200 OK',
331 'th=device-width">'
332 . '<title>Refactoring · GitHub</title>'
333 . '<link rel="search" type="application/opensea',
334 'end' => '<title>ignored</title>'
335 . '<meta name="description" content="link desc" />'
336 . '<meta name="keywords" content="key1,key2" />',
337 ];
338 foreach ($data as $key => $line) {
339 $ignore = null;
340 $expected = $key !== 'end' ? strlen($line) : false;
341 $this->assertEquals($expected, $callback($ignore, $line));
342 if ($expected === false) {
343 break;
344 }
345 }
346 $this->assertEquals('utf-8', $charset);
347 $this->assertEquals('Refactoring · GitHub', $title);
348 $this->assertEquals('link desc', $desc);
349 $this->assertEquals('key1 key2', $keywords);
350 }
351
352 /**
353 * Test the download callback with valid value, and retrieve_description option enabled,
354 * but no desc or keyword defined in the page.
355 */
356 public function testCurlDownloadCallbackOkWithDescNotFound()
357 {
358 $callback = get_curl_download_callback(
359 $charset,
360 $title,
361 $desc,
362 $keywords,
363 true,
364 'ut_curl_getinfo_ok'
365 );
366 $data = [
367 'HTTP/1.1 200 OK',
368 'Server: GitHub.com',
369 'Date: Sat, 28 Oct 2017 12:01:33 GMT',
370 'Content-Type: text/html; charset=utf-8',
371 'Status: 200 OK',
372 'th=device-width">'
373 . '<title>Refactoring · GitHub</title>'
374 . '<link rel="search" type="application/opensea',
375 'end' => '<title>ignored</title>',
376 ];
377 foreach ($data as $key => $line) {
378 $ignore = null;
379 $expected = $key !== 'end' ? strlen($line) : false;
380 $this->assertEquals($expected, $callback($ignore, $line));
381 if ($expected === false) {
382 break;
383 }
384 }
385 $this->assertEquals('utf-8', $charset);
386 $this->assertEquals('Refactoring · GitHub', $title);
387 $this->assertEmpty($desc);
388 $this->assertEmpty($keywords);
389 }
390
391 /**
392 * Test count_private.
393 */
394 public function testCountPrivateLinks()
395 {
396 $refDB = new ReferenceLinkDB();
397 $this->assertEquals($refDB->countPrivateLinks(), count_private($refDB->getLinks()));
398 }
399
400 /**
401 * Test text2clickable.
402 */
403 public function testText2clickable()
404 {
405 $text = 'stuff http://hello.there/is=someone#here otherstuff';
406 $expectedText = 'stuff <a href="http://hello.there/is=someone#here">'
407 . 'http://hello.there/is=someone#here</a> otherstuff';
408 $processedText = text2clickable($text);
409 $this->assertEquals($expectedText, $processedText);
410
411 $text = 'stuff http://hello.there/is=someone#here(please) otherstuff';
412 $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)">'
413 . 'http://hello.there/is=someone#here(please)</a> otherstuff';
414 $processedText = text2clickable($text);
415 $this->assertEquals($expectedText, $processedText);
416
417 $text = 'stuff http://hello.there/is=someone#here(please)&no otherstuff';
418 $text = 'stuff http://hello.there/is=someone#here(please)&no otherstuff';
419 $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)&no">'
420 . 'http://hello.there/is=someone#here(please)&no</a> otherstuff';
421 $processedText = text2clickable($text);
422 $this->assertEquals($expectedText, $processedText);
423 }
424
425 /**
426 * Test testSpace2nbsp.
427 */
428 public function testSpace2nbsp()
429 {
430 $text = ' Are you thrilled by flags ?' . PHP_EOL . ' Really?';
431 $expectedText = '&nbsp; Are you &nbsp; thrilled &nbsp;by flags &nbsp; ?' . PHP_EOL . '&nbsp;Really?';
432 $processedText = space2nbsp($text);
433 $this->assertEquals($expectedText, $processedText);
434 }
435
436 /**
437 * Test hashtags auto-link.
438 */
439 public function testHashtagAutolink()
440 {
441 $index = 'http://domain.tld/';
442 $rawDescription = '#hashtag\n
443 # nothashtag\n
444 test#nothashtag #hashtag \#nothashtag\n
445 test #hashtag #hashtag test #hashtag.test\n
446 #hashtag #hashtag-nothashtag #hashtag_hashtag\n
447 What is #ашок anyway?\n
448 カタカナ #カタカナ」カタカナ\n';
449 $autolinkedDescription = hashtag_autolink($rawDescription, $index);
450
451 $this->assertContains($this->getHashtagLink('hashtag', $index), $autolinkedDescription);
452 $this->assertNotContains(' #hashtag', $autolinkedDescription);
453 $this->assertNotContains('>#nothashtag', $autolinkedDescription);
454 $this->assertContains($this->getHashtagLink('ашок', $index), $autolinkedDescription);
455 $this->assertContains($this->getHashtagLink('カタカナ', $index), $autolinkedDescription);
456 $this->assertContains($this->getHashtagLink('hashtag_hashtag', $index), $autolinkedDescription);
457 $this->assertNotContains($this->getHashtagLink('hashtag-nothashtag', $index), $autolinkedDescription);
458 }
459
460 /**
461 * Test hashtags auto-link without index URL.
462 */
463 public function testHashtagAutolinkNoIndex()
464 {
465 $rawDescription = 'blabla #hashtag x#nothashtag';
466 $autolinkedDescription = hashtag_autolink($rawDescription);
467
468 $this->assertContains($this->getHashtagLink('hashtag'), $autolinkedDescription);
469 $this->assertNotContains(' #hashtag', $autolinkedDescription);
470 $this->assertNotContains('>#nothashtag', $autolinkedDescription);
471 }
472
473 /**
474 * Test is_note with note URLs.
475 */
476 public function testIsNote()
477 {
478 $this->assertTrue(is_note('?'));
479 $this->assertTrue(is_note('?abcDEf'));
480 $this->assertTrue(is_note('?_abcDEf#123'));
481 }
482
483 /**
484 * Test is_note with non note URLs.
485 */
486 public function testIsNotNote()
487 {
488 $this->assertFalse(is_note(''));
489 $this->assertFalse(is_note('nope'));
490 $this->assertFalse(is_note('https://github.com/shaarli/Shaarli/?hi'));
491 }
492
493 /**
494 * Util function to build an hashtag link.
495 *
496 * @param string $hashtag Hashtag name.
497 * @param string $index Index URL.
498 *
499 * @return string HTML hashtag link.
500 */
501 private function getHashtagLink($hashtag, $index = '')
502 {
503 $hashtagLink = '<a href="' . $index . '?addtag=$1" title="Hashtag $1">#$1</a>';
504 return str_replace('$1', $hashtag, $hashtagLink);
505 }
506}