YATA.class.php 44.6 KB
Newer Older
vermeul's avatar
vermeul committed
1
2
3
4
5
6
7
8
9
10
<?php
/**
 * YATA extension
 * @author Swen Vermeul
 * @file
 * @version 0.1
 * @license GNU General Public Licence 2.0
 */

class YATA {
11
    public static $annotations = "";
vermeul's avatar
vermeul committed
12
    //
vermeul's avatar
vermeul committed
13
    // Register any render callbacks with the parser
vermeul's avatar
vermeul committed
14
    //
vermeul's avatar
vermeul committed
15
16
17
18
19
    public static function onParserSetup( &$parser ) {

       // Create a function hook associating the "example" magic word with renderExample()
       $parser->setFunctionHook( 'annot', 'YATA::renderStartAnnot' );
       $parser->setFunctionHook( 'annotend', 'YATA::renderEndAnnot' );
vermeul's avatar
vermeul committed
20
       $parser->setFunctionHook( 'annotlist', 'YATA::annotations_list' );
21
       $parser->setFunctionHook( 'annotask', 'YATA::annotation_query' );
vermeul's avatar
vermeul committed
22
       $parser->setFunctionHook( 'annotcat', 'YATA::annotation_categories' );
vermeul's avatar
vermeul committed
23
24
    }

vermeul's avatar
vermeul committed
25
    //
vermeul's avatar
vermeul committed
26
    // Render the output of {{#annot: comment | category | id}}.
vermeul's avatar
vermeul committed
27
    //
vermeul's avatar
vermeul committed
28
    public static function renderStartAnnot( $parser, $comment='', $category='', $id='' ) {
29
        return "<span id=$id title='$comment'>";
vermeul's avatar
vermeul committed
30
31
    }

vermeul's avatar
vermeul committed
32
    //
vermeul's avatar
vermeul committed
33
    // Render the output of {{#annotend:}}.
vermeul's avatar
vermeul committed
34
    //
vermeul's avatar
vermeul committed
35
    public static function renderEndAnnot( $parser, $id='' ) {
vermeul's avatar
vermeul committed
36
37
38
       return "</span>";
    }
    
vermeul's avatar
vermeul committed
39
    //
vermeul's avatar
vermeul committed
40
    // Render the output of {{#annotask:}}
vermeul's avatar
vermeul committed
41
    //
vermeul's avatar
vermeul committed
42
    public static function annotation_query( $parser, $querystring) {
43
44
        $title = $parser->getTitle();
        $wikiPage = new WikiPage( $title );
vermeul's avatar
vermeul committed
45
46
47
48
49
50
        $queries_found = preg_match_all(
            '/\[\[(?P<query>.*?)\]\]/s',
            $querystring, 
            $query_params, 
            PREG_OFFSET_CAPTURE
        );
vermeul's avatar
vermeul committed
51
52
        $searches = array();
        $in_categories = array();
vermeul's avatar
vermeul committed
53
54
55
56

        $where = array();
        $dbr = wfGetDB( DB_REPLICA );
        $seen = array();
57
        $errors = array();
vermeul's avatar
vermeul committed
58
59
        foreach($query_params["query"] as $query) {
            list($field, $values) = preg_split('/\s*\:\s*/', $query[0]);
60
61
62
63
            # syntax-sugar
            if ($field === 'wikitext') {
                $field = 'wiki_text';
            }
vermeul's avatar
vermeul committed
64
65
66
67
            if( $field === 'category') {
                $categories = preg_split('/\s*\,\s*/', $values);
                $cat_ids = array();
                foreach( $categories as $category ) {
vermeul's avatar
vermeul committed
68
                    array_push($in_categories, $category);
vermeul's avatar
vermeul committed
69
70
71
72
73
74
75
76
77
                    $cat = self::get_category($dbr, $category);
                    if ($cat) {
                        $cat_ids[] = $cat->id;
                        $child_categories = self::get_child_categories($dbr, $cat);
                        foreach($child_categories as $child_category) {
                            $cat_ids[] = $child_category->id;
                        }
                    }
                    else {
78
                        array_push($errors, "no such category found: $category");
vermeul's avatar
vermeul committed
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
                    }
                }
                $where[] = 
                    "EXISTS( 
                        SELECT NULL 
                        FROM yata_annotation_category ac 
                        WHERE ac.annotation_id = a.id
                        AND ac.category_id IN ("
                    . join(',', $cat_ids)
                    . ")
                    )";
            }
            elseif( $field === 'user' ) {
                $where["u.username"] = $values;
            }
            elseif( in_array($field, array('wiki_text', 'comment')) ) {
vermeul's avatar
vermeul committed
95
                array_push($searches, $field . ': ' . $values);
vermeul's avatar
vermeul committed
96
97
98
99
100
101
102
103
                if ( preg_match( '/[\%_]/', $values ) ) {
                    $where[] = "$field LIKE (" . $dbr->addQuotes($values).")";
                }
                else {
                    $where["a.$field"] = $values;
                }
            }
        }
104

105
	if ($errors) {
106
107
108
109
110
            foreach( $errors as $error) {
                $errstr .= $error . "<br/>";
            }
            return $errstr;
        }
vermeul's avatar
vermeul committed
111
112
113
114
115
        

        # fetch the annotation details
        $annotations = $dbr->select(
            array(
vermeul's avatar
vermeul committed
116
117
118
119
120
121
                'a' => 'yata_annotation',
                'u' => 'user',
                'ac'=> 'yata_annotation_category',
                'c' => 'yata_category',
                'pc'=> 'yata_category',
                'p' => 'page',
vermeul's avatar
vermeul committed
122
123
            ),
            array(
vermeul's avatar
vermeul committed
124
125
126
127
128
129
130
131
132
133
                'category'        => "c.name",
                'parent_category' => "pc.name",
                'hashtag'         => "c.hashtag",
                'title'           => "p.page_title",
                'comment'         => "a.comment", 
                'wiki_text'       => "a.wiki_text",
                'start_char'      => "a.start_char",
                'bookmark'        => "a.bookmark",
                'last_edited_by'  => "u.user_name",
                'last_modified'   => "a.modified_date"
vermeul's avatar
vermeul committed
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
            ),
            $where,
            __METHOD__, 
            array(
                'ORDER BY' => 'a.start_char ASC' 
            ), 
            array( 
                'u'  => array( 'LEFT JOIN', array( 'u.user_id = a.user_id') ),
                'ac' => array( 'LEFT JOIN', array( 'ac.annotation_id = a.id' ) ),
                'c'  => array( 'LEFT JOIN', array( 'ac.category_id = c.id' ) ),
                'pc' => array( 'LEFT JOIN', array( 'pc.id = c.parent_id') ),
                'p'  => array( 'INNER JOIN', array( 'p.page_id = a.page_id') ),
            )
        );

vermeul's avatar
vermeul committed
149
150
151
152
153
154
155
156
157
158
159
160
161
        if ($searches) {
            $table .= "<b>Search for term:</b>";
            foreach($searches as $search_string) {
                $table .= "<pre>".$search_string."</pre>";
            }
        }
        if ($in_categories) {
            $table .= "<b>Search in Categories:</b>";
            foreach($in_categories as $category) {
                $table .= "<pre>".$category."</pre>";
            }
        }
        $table .= "<b>Results</b> ";
vermeul's avatar
vermeul committed
162
        $table .= '([' . $title->getFullURL() . '?action=purge' . ' refresh page]):';
vermeul's avatar
vermeul committed
163
164
        if (count($annotations)>0) {
            $table .= '
vermeul's avatar
vermeul committed
165
<table class="wikitable sortable jquery-tablesorter">
vermeul's avatar
vermeul committed
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
    <tr>
        <th class="headerSort headerSortUp" tabindex="0" role="columnheader button" title="Sort descending"> Category </th>
        <th class="headerSort" tabindex="0" role="columnheader button" title="Sort ascending"> Comment </th>
        <th class="headerSort" tabindex="0" role="columnheader button" title="Sort ascending"> Link </th>
        <th class="headerSort" tabindex="0" role="columnheader button" title="Sort ascending"> Annotated text </th>
        <th class="headerSort" tabindex="0" role="columnheader button" title="Sort ascending"> Last edited by </th>
        <th class="headerSort" tabindex="0" role="columnheader button" title="Sort ascending"> Modification date </th>
    </tr>';

            # compose the wiki table
            foreach($annotations as $annotation) {
                # if a category has no parent, just show the category name
                # in all other cases show parent_category:child_category
                $cat = $annotation->parent_category ? $annotation->parent_category . "/" . $annotation->category : $annotation->category;


                $wikitext = $annotation->wiki_text;
                $wikitext = htmlentities($wikitext, ENT_QUOTES);

                $a = array('=', '{', '|', '}');
                $b = array('&#61;', '&#123;', '&#124;', '&#125;');
                $wikitext = str_replace($a, $b, $wikitext);
                
                $table .= "<tr><td>".$cat
                       ."</td><td>".$annotation->comment 
                       ."</td><td>[[".$annotation->title."#".$annotation->bookmark . "]]"
                       ."</td><td><pre>" . $wikitext . "</pre>"
                       #." || "
                       ."</td><td>" . $annotation->last_edited_by
                       ."</td><td>" . wfTimestamp( TS_ISO_8601, $annotation->last_modified)
                       ."</td></tr>\n";
            }
            $table .= "</table>";
        }
        else {
            $table .= "<b>No results found.</b>";
vermeul's avatar
vermeul committed
202
        }
vermeul's avatar
vermeul committed
203

vermeul's avatar
vermeul committed
204
        return $table;
vermeul's avatar
vermeul committed
205
    }
vermeul's avatar
vermeul committed
206

vermeul's avatar
vermeul committed
207

vermeul's avatar
vermeul committed
208
    //
vermeul's avatar
vermeul committed
209
    // Render the output of {{#annotlist:}}.
vermeul's avatar
vermeul committed
210
    //
vermeul's avatar
vermeul committed
211
212
213
    public static function annotations_list( $parser ) {
        $title = $parser->getTitle();
        $wikiPage = new WikiPage( $title );
214
215
        #$dbr = wfGetDB( DB_REPLICA );
        $dbr = wfGetDB( DB_MASTER );
vermeul's avatar
vermeul committed
216

vermeul's avatar
vermeul committed
217
218
219
        $table = <<<"EOT"
{| class="wikitable sortable"
|-
vermeul's avatar
vermeul committed
220
! Category !! Hashtag !! Comment !! Link !! Annotated text !! Last edited by !! Modification date
vermeul's avatar
vermeul committed
221

vermeul's avatar
vermeul committed
222
EOT;
223
        # fetch the annotation details
vermeul's avatar
vermeul committed
224
225
226
227
228
        $annotations = $dbr->select(
            array(
                a =>'yata_annotation',
                ac=>'yata_annotation_category',
                c =>'yata_category',
229
230
                pc=>'yata_category',
                u =>'user',
vermeul's avatar
vermeul committed
231
232
            ),
            array(
233
                comment         => "a.comment", 
vermeul's avatar
vermeul committed
234
235
                wiki_text       => "a.wiki_text",
                start_char      => "a.start_char",
vermeul's avatar
vermeul committed
236
                bookmark        => "a.bookmark",
vermeul's avatar
vermeul committed
237
                category        => "c.name",
vermeul's avatar
vermeul committed
238
                hashtag         => "c.hashtag",
239
240
241
                parent_category => "pc.name",
                last_edited_by  => "u.user_name",
                last_modified   => "a.modified_date"
vermeul's avatar
vermeul committed
242
243
            ),
            array(
244
                'a.page_id' => $wikiPage->getId()
vermeul's avatar
vermeul committed
245
246
247
248
249
250
            ),
            __METHOD__, 
            array(
                'ORDER BY' => 'a.start_char ASC' 
            ), 
            array( 
251
252
253
254
                'u'  => array( 'LEFT JOIN', array( 'u.user_id = a.user_id') ),
                'ac' => array( 'LEFT JOIN', array( 'ac.annotation_id = a.id' ) ),
                'c'  => array( 'LEFT JOIN', array( 'ac.category_id = c.id' ) ),
                'pc' => array( 'LEFT JOIN', array( 'pc.id = c.parent_id') ),
vermeul's avatar
vermeul committed
255
256
            )
        );
vermeul's avatar
vermeul committed
257

258
        # compose the wiki table
vermeul's avatar
vermeul committed
259
260
261
        foreach($annotations as $annotation) {
            # if a category has no parent, just show the category name
            # in all other cases show parent_category:child_category
262
263
264
265
266
            $wikitext = preg_replace(
                '/{{\s*#annot(?:<end>end)?.*?}}/', 
                '',
                $annotation->wiki_text
            );
267
            $cat = $annotation->parent_category ? $annotation->parent_category . "/" . $annotation->category : $annotation->category;
vermeul's avatar
vermeul committed
268
269
            $table .= "|-\n";
            $table .= "| ".$cat
vermeul's avatar
vermeul committed
270
                   ." || ".$annotation->hashtag
vermeul's avatar
vermeul committed
271
                   ." || ".$annotation->comment 
vermeul's avatar
vermeul committed
272
                   ." || [[$title#".$annotation->bookmark . "]]"
273
274
275
                   ." || " . $wikitext
                   ."\n || " . $annotation->last_edited_by
                   ."\n || " . $annotation->last_modified
vermeul's avatar
vermeul committed
276
                   ."\n";
vermeul's avatar
vermeul committed
277
278
279
280
        }
        $table .= "|}";
        return $table;
        
vermeul's avatar
vermeul committed
281
282
    }

vermeul's avatar
vermeul committed
283
    //
vermeul's avatar
vermeul committed
284
    // Render the output of {{#annotcat: list}}.
vermeul's avatar
vermeul committed
285
    //
286
    public static function annotation_categories( $parser, $method, $start_with=null ) {
vermeul's avatar
vermeul committed
287
288
289
        if (! $method  == "list") {
            return "";
        }
290
        $dbr = wfGetDB( DB_MASTER );
vermeul's avatar
vermeul committed
291
292
293
294
295
296
297
298
299
300
301
302

        $where = array('parent_id'=>null);  # start with all top catgegories
        if ( $start_with ) {
            $start_with_cat = self::get_category($dbr, $start_with);
            if ( $start_with_cat ) {
                $where = array(id=>$start_with_cat->id);
            }
        }
                        
        $categories = self::search_categories($dbr, $where, array('ORDER BY' => 'name') );


303
304
305
        $table = <<<"EOT"
{| class="wikitable sortable"
|-
vermeul's avatar
vermeul committed
306
! Category !! Hashtag !! Description !! id !! parent_id
307
308
309
310
311
312
313

EOT;

        foreach($categories as $category) {
            # if a category has no parent, just show the category name
            # in all other cases show parent_category:child_category
            $table .= "|-\n";
vermeul's avatar
vermeul committed
314
            $table .= "| ".$category->name
vermeul's avatar
vermeul committed
315
                   ." || ".$category->hashtag
vermeul's avatar
vermeul committed
316
                   ." || ".$category->description
vermeul's avatar
vermeul committed
317
318
                   ." || ".$category->id
                   ." || ".$category->parent_id
vermeul's avatar
vermeul committed
319
                   ."\n";
vermeul's avatar
vermeul committed
320
321
322
323
324
            $child_categories = self::get_child_categories($dbr, $category, 1);
            if ($child_categories) {
                foreach ($child_categories as $child_category) {
                    $table .= "|-\n";
                    $table .= "|" . str_repeat('&nbsp;&nbsp;&nbsp;', $child_category->level) . $child_category->name
vermeul's avatar
vermeul committed
325
                        ." || ".$child_category->hashtag
vermeul's avatar
vermeul committed
326
327
328
329
330
331
332
                        ." || ".$child_category->description
                        ." || ".$child_category->id
                        ." || ".$child_category->parent_id
                        ."\n";
                }
            }

333
334
335
336
337
338
        }
        $table .= "|}";
        return $table;

    }

vermeul's avatar
vermeul committed
339
340

    public static function onPageContentSave(WikiPage &$wikiPage, User &$user, Content &$content, $summary, $isMinor, $isWatch, $section, $flags, Status &$status ){
vermeul's avatar
vermeul committed
341
    /*
342
343
344
345
346
347
348
        onPageContentSave: Hook on when page is saved
        we first replace syntax sugar where necessary, e.g.
        - change {{#annotend}} to {{#annotend:}}
        - change {{annotlist}} or {{#annotlist}} to {{#annotlist:}}

        next steps:
        1. add/delete/update categories (see manage_categories)
349
350
        2. parse the annotations (see parse_annotations)
        3. replace the wiki_text 
351
352
        
        The annotations are saved in the next hook: onPageContentSaveComplete (see below)
353

vermeul's avatar
vermeul committed
354
355
356
357
358
359
360
361
    */

        # get database handler
        $dbw = wfGetDB( DB_MASTER );
        $dbr = wfGetDB( DB_REPLICA );
        if ( $content instanceof TextContent ) {

            $data = $content->getNativeData();
vermeul's avatar
vermeul committed
362
363
364
            # replace all {{#annotend}} or {{annotend}} with {{#annotend:}}, where necessary
            # replace all {{#annotlist}} or {{annotlist}} with {{#annotlist:}}, where necessary
            $data = preg_replace( '/{{#*(annotend|annotlist)}}/U','{{#$1:}}', $data);
365
366

            # add / delete categories 
vermeul's avatar
vermeul committed
367
368
369
370
371
372
373
            try {
                $data = self::manage_categories($dbw, $data);
            }
            catch (Exception $e) {
               $status->fatal($e->getMessage());
               return false;
            }
vermeul's avatar
vermeul committed
374

375
            # get all annotations that exist in the current wikitext
vermeul's avatar
vermeul committed
376
377
378
379
380
381
382
            try {
                $annotations = self::parse_annotations($dbw, $data, $wikiPage);
            }
            catch (Exception $e) {
               $status->fatal($e->getMessage());
               return false;
            }
383

384
            # keep the annotations for saving later (onPageContentSaveComplete)
385
            self::$annotations = $annotations;
386
387

            # replace the content and save it
388
389
            $content = new WikitextContent( $data );
        }
vermeul's avatar
vermeul committed
390
391
392
393

        if ( !$status->isOK() ) {
            return false;
        }
394
    }
vermeul's avatar
vermeul committed
395

vermeul's avatar
merge    
vermeul committed
396
397
398
399
    /*
    This function is called after the save page request has been processed.
    @see https://www.mediawiki.org/wiki/Manual:Hooks/PageContentSaveComplete
    
400
401
402
    - save all new or update chaned annotations
    - Annotations which are no longer present should be deleted.
    - all entries in annotation_category which are no longer used should be deleted.
403
404
405
406
    */
    public static function onPageContentSaveComplete($wikiPage, $user, $content, $summary,
    $isMinor, $isWatch, $section, $flags, $revision, $status, $baseRevId ) {
        $dbw = wfGetDB( DB_MASTER );
407
408
409
        $seen_bookmarks = array();
        $seen_annotation_ids = array();
       $i = 0; 
410
411
412
        foreach ( self::$annotations as $bookmark => $annotation ){
            $annotation_id = 0;
            $ex_annot = self::get_annotation($dbw, $wikiPage, $bookmark);
413
414

            # existing annotation found: update wiki_text, comment
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
            if ($ex_annot) {
                $annotation_id = $ex_annot->id;
                $ex_annot = self::get_annotation($dbw, $wikiPage, $bookmark);
                if ($ex_annot->wiki_text != $annotation["wiki_text"] 
                     or $ex_annot->comment != $annotation["comment"] ) {
                    $dbw->update(
                        'yata_annotation',
                        array(
                            wiki_text => $annotation["wiki_text"],
                            comment   => $annotation["comment"],
                            user_id   => $user->getId(),
                            modified_date => $dbw->timestamp(),
                        ),
                        array(
                            page_id  => $wikiPage->getId(),
                            bookmark => $bookmark
                        )
                    );
                }
            }
435
            # new annotation found: insert annotation
436
            else {
vermeul's avatar
vermeul committed
437
                # write annotation
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
                try {
                    $data = array(
                            page_id    => $wikiPage->getId(),
                            comment    => $annotation['comment'],
                            wiki_text  => $annotation['wiki_text'],
                            bookmark   => $bookmark,
                            user_id    => $user->getId(),
                            insert_date => $dbw->timestamp(),
                            modified_date => $dbw->timestamp(),
                    );
            
                    $dbw->insert(
                        'yata_annotation',
                        array(
                            page_id    => $wikiPage->getId(),
                            comment    => $annotation['comment'],
                            wiki_text  => $annotation['wiki_text'],
                            bookmark   => $bookmark,
                            user_id    => $user->getId(),
                            insert_date => $dbw->timestamp(),
                            modified_date => $dbw->timestamp(),
                        )
                    );
                    $ex_annot = self::get_annotation($dbw, $wikiPage, $bookmark);
                    $annotation_id = $ex_annot->id;
                }
                catch (Exception $e) {
                    print($e->getMessage());
                    die;
                }
468
            }
469
470
471
472
473
            # keep track of all the annotations we have seen on this page,
            # to delete later all the annotations which have been removed from this page.
            array_push($seen_bookmarks, $bookmark);
            array_push($seen_annotation_ids, $annotation_id);

vermeul's avatar
vermeul committed
474

475
476
477
            # insert / update the annotation categories
            if ( $annotation['categories'] ) {
                $seen_acs = array();
478
                foreach ( $annotation['categories'] as $category ){
vermeul's avatar
vermeul committed
479
480
                    # assign all found categories to this annotation
                    if(! is_null($category)) {
481
482
                        self::insert_annotation_category($dbw, $wikiPage, $annotation_id, $category->id);
                        array_push($seen_acs, $category->id);
vermeul's avatar
vermeul committed
483
484
                    }
                }
485
486
                # delete all annotation categories which are no longer assigned
                # to this annotation
vermeul's avatar
merge    
vermeul committed
487
488
489
490
491
492
493
494
495
                $dbw->delete(
                    'yata_annotation_category',
                    array( 
                        page_id       => $wikiPage->getId(),
                        annotation_id => $annotation_id,
                        'category_id NOT IN(' .$dbw->makeList($seen_acs). ')'
                    )
                );
            }
496
497
498
499
500
501
502
503
504
505
            else {
                # delete all existing annotation categories,
                # since no category is assigned anymore
                $dbw->delete(
                    'yata_annotation_category',
                    array(
                        page_id       => $wikiPage->getId(),
                        annotation_id => $annotation_id,
                    )
                );
vermeul's avatar
vermeul committed
506
            }
vermeul's avatar
vermeul committed
507
        }
508
        
vermeul's avatar
vermeul committed
509

510
        # find all all annotations on that page...
vermeul's avatar
merge    
vermeul committed
511
512
513
        $where = [
            "page_id" => $wikiPage->getId()
        ];
514
515
516
        #...which are no longer present
        if ($seen_annotation_ids) {
            $where[] = 'id NOT IN(' .$dbw->makeList($seen_annotation_ids). ')';
517
518
519
520
521
        }

        $annotations_to_delete = $dbw->select(
            'yata_annotation',
            array( 'id' ),
vermeul's avatar
vermeul committed
522
            $where
523
524
        );

525

526
527
        # delete all annotations which are no longer present
        foreach( $annotations_to_delete as $annotation) {
528
529
530
531
532
533
            $dbw->delete(
                'yata_annotation',
                array(
                    'id' => $annotation->id 
                )
            );
vermeul's avatar
merge    
vermeul committed
534
535
536
537
538
539
540
541
542

            # delete all categories assigned to annotations
            # which do not exist anymore

            $delete_where = [
                page_id => $wikiPage->getId(),
                annotation_id => $annotation->id
            ];

543
544
545
546
            $dbw->delete(
                'yata_annotation_category',
                $delete_where
            );
vermeul's avatar
vermeul committed
547
548
549
        }
    }

vermeul's avatar
vermeul committed
550
    public static function add_category($dbw, $category_name, $hashtag, $description, $parent) {
vermeul's avatar
vermeul committed
551
        $category_name = trim($category_name);
552
553
554
        $parent_id = null;
        if (! is_null($parent)) {
            $parent_category = self::get_category($dbw, $parent);
vermeul's avatar
vermeul committed
555
            if(! $parent_category) {
vermeul's avatar
vermeul committed
556
                throw new Exception("Error adding new category: no such parent-category found: $parent");
vermeul's avatar
vermeul committed
557
            }
558
559
560
            $parent_id = $parent_category->id;
        }

vermeul's avatar
vermeul committed
561
562
563
564
565
566
567
568
569
570
571
572
573
        $exists = $dbw->selectRow(
            'yata_category',
            array('id'),
            array(
                name => $category_name,
                parent_id => $parent_id
            )
        );

        if ($exists) {
            return;
        }

574
        # make sure hashtags always start with a hash symbol: #
vermeul's avatar
vermeul committed
575
        if ($hashtag && substr($hashtag, 0,1) != '#') {
576
577
578
            $hashtag = '#'.$hashtag;
        }

579
580
581
        $dbw->insert(
            'yata_category',
            array(
vermeul's avatar
vermeul committed
582
                name        => $category_name,
vermeul's avatar
vermeul committed
583
                hashtag     => $hashtag,
584
585
586
587
588
589
590
                description => trim($description),
                parent_id   => $parent_id
            )
        );

    }

vermeul's avatar
vermeul committed
591
592
593
594
595
596
597
598
599
600
601
602
603
604
    public static function up_category($dbw, $ex_category_name, $args) {
        $category = self::get_category($dbw, $ex_category_name);
        if (!$category) {
            throw new Exception("Error updating category - no such category: $ex_category_name");
        }

        $set = array();
        if ($args["name"]) {
            # make sure this category does not exists yet
            $new_category_name = $args["name"];
            if ($category->parent_name) {
                $new_category_name = $category->parent_name . "/".$new_category;
            }
            $category_exists = self::get_category($dbw, $new_category_name);
605
            if ($category_exists && $category_exists->id != $category->id) {
vermeul's avatar
vermeul committed
606
607
608
609
                throw new Exception("Error updating category - already exists: $new_category_name");
            }
            $set["name"] = $args["name"];
        }
vermeul's avatar
vermeul committed
610
        if (array_key_exists("hashtag", $args) ) {
vermeul's avatar
vermeul committed
611
            if ($args["hashtag"] && substr($args["hashtag"], 0,1) != '#') {
612
613
                $args["hashtag"] = '#'.$args["hashtag"];
            }
vermeul's avatar
vermeul committed
614
615
            $set["hashtag"] = $args["hashtag"];
        }
vermeul's avatar
vermeul committed
616
617
618
619
620
621
622
623
624
625
626
627
628
        if (array_key_exists("description", $args) ) {
            $set["description"] = $args["description"];
        }
        if (array_key_exists("parent", $args) ) {
            # redefine the parent category
            if ($args["parent"]) {
                $parent_category = self::get_category($dbw, $args["parent"]);
                if (! $parent_category) {
                    throw new Exception("Error updating category - no such parent category: ". $args["parent"]);
                }
                else {
                    $set["parent_id"] = $parent_category->id;
                }
vermeul's avatar
vermeul committed
629
            }
vermeul's avatar
vermeul committed
630
631
632
            else {
                # define category as a top category (remove parent)
                $set["parent_id"] = null;
vermeul's avatar
vermeul committed
633
            }
vermeul's avatar
vermeul committed
634
635
        }
        if ($set) {
vermeul's avatar
vermeul committed
636
637
638
639
640
641
642
643
            $dbw->update(
                'yata_category',
                $set,
                array(
                    id => $category->id
                )
            );
        }
vermeul's avatar
vermeul committed
644
645
646
647
        else {
            throw new Exception("Error updating category - nothing needs to be updated.");
        }
        
vermeul's avatar
vermeul committed
648
649
650
    }

    public static function del_category($dbw, $category_name) {
651

vermeul's avatar
vermeul committed
652
        if (substr($category_name, 0, 1) == '#') {
vermeul's avatar
vermeul committed
653
654
655
656
657
658
659
660
661
            # we go a hashtag, which should be unique.
            $dbw->delete(
                'yata_category',
                array(
                    hashtag => $category_name,
                )
            );
            return;
        }
vermeul's avatar
vermeul committed
662
        $cat_entry = self::get_category($dbw, $category_name);
vermeul's avatar
vermeul committed
663
664
665
        if (!$cat_entry) {
            throw new Exception("Error deleting category - no such category: $category_name");
        }
666
667
668
669
670
671
672
673
        $dbw->delete(
            'yata_category',
            array(
                id => $cat_entry->id
            )
        );
    }

vermeul's avatar
vermeul committed
674
675
    private static function split_cat_argstring($arg_string) {
        $args = array();
vermeul's avatar
vermeul committed
676
677
        # name='xxx', description='blabla', hashtag='#blabla', parent='parent/category'
        # splits argumentlist above and returns a dictionary instead.
vermeul's avatar
vermeul committed
678
        $keyvals = preg_split('/\s*\,\s*(?=(name|hashtag|description|parent))/', $arg_string);
vermeul's avatar
vermeul committed
679
680
681
682
        foreach($keyvals as $keyval) {
            list($key, $value) = preg_split('/\s*\=\s*/', $keyval);
            # remove ay leading and trailing  " or '
            # in the value
vermeul's avatar
vermeul committed
683
684
685
            $value = preg_replace('/^\s*(\'|\")/', '', $value);
            $value = preg_replace('/(\'|\")\s*$/', '', $value);
            $value = preg_replace('/\s*$/', '', $value);
vermeul's avatar
vermeul committed
686
687
688
689
690
            $args[$key] = $value;
        }
        return $args;
    }

691
692
    private static function category_add_del_callback($matches) {
        $dbw = wfGetDB( DB_MASTER );
vermeul's avatar
vermeul committed
693
694
        # split the arguments (after #annotcat:)
        #
vermeul's avatar
vermeul committed
695
696
        # {{#annotcat: add | name=new top category, hashtag=#shortcut, description=some meaningful comments, parent=grandparent_category/parent_category }}
        # {{#annotcat: up  | name=new top category, hashtag=#shortcut, description=some meaningful comments, parent=grandparent_category/parent_category }}
vermeul's avatar
vermeul committed
697
        # {{#annotcat: del | parent_category_name/category_name}}
vermeul's avatar
vermeul committed
698
        # {{#annotcat: del | #shortcut }}
vermeul's avatar
vermeul committed
699
700
        
        list($method, $arg1, $arg2) = preg_split('/\s*\|\s*/', $matches["params"]);
701
        if ( $method === "add" ) {
vermeul's avatar
vermeul committed
702
            $args = self::split_cat_argstring($arg1);
vermeul's avatar
vermeul committed
703
            self::add_category($dbw, $args['name'], $args['hashtag'], $args['description'], $args['parent']);
vermeul's avatar
vermeul committed
704
705
        }
        elseif ( $method === "up" ) {
vermeul's avatar
vermeul committed
706
707
            $args = self::split_cat_argstring($arg2);
            self::up_category($dbw, $arg1, $args);
708
709
        }
        elseif ( $method === "del" ) {
vermeul's avatar
vermeul committed
710
711
            # $arg_string already contains the category name, we do not have to split the arguments
            self::del_category($dbw, $arg1);
712
        }
vermeul's avatar
vermeul committed
713
714
715
        else {
            return $matches[0];
        }
vermeul's avatar
vermeul committed
716
717
718

        # remove the {{#annotcat: add|up|del }} commands from the source code
        # by returning an empty string
719
720
721
722
        return "";
    }

    // manage any categories added
723
    public static function manage_categories( $dbw, $data) {
724
725
726
727
728
729
730
731
732
733
734
        # look for all tags {{#annotcat:(.*)}}
        # and pass the matches to the callback function category_add_del_callback
        # which will return an empty string in order to remove this tag before saving the page.
        $data = preg_replace_callback(
            '/({{#annotcat:\s*(?P<params>.*?)}})/s',
            'self::category_add_del_callback',
            $data
        );
        return $data;
    }

735
736
737
738
739
740
741
742
743
744
745
746
    public static function get_annotation($dbr, $wikiPage, $bookmark) {
        $row = $dbr->selectRow(
            'yata_annotation',
            array('id', 'wiki_text', 'comment'),
            array(
                page_id  => $wikiPage->getId(),
                bookmark => $bookmark
            )
        );
        return $row;
    }

747

vermeul's avatar
vermeul committed
748
749
750
751
752
753
    public static function search_categories($dbr, $where, $order_by) {
        if (! $order_by) {
            $order_by = array('ORDER BY' => 'name');
        }

        $categories = $dbr->select(
754
755
756
757
758
759
760
761
            array('c' => 'yata_category'),
            array( 
                'c.id', 
                'c.name', 
                'c.hashtag', 
                'c.description', 
                'c.parent_id'
            ),
vermeul's avatar
vermeul committed
762
763
764
765
766
767
768
            $where,
            __METHOD__,
            $order_by
        );
        return $categories;
    }

769
770
771
    public static function get_category($dbr, $category) {
        $category = trim($category);

vermeul's avatar
vermeul committed
772
773
774
775
776
777
778
779
780
        $selection = array( 
            'id'          => 'c.id', 
            'name'        => 'c.name', 
            'hashtag'     => 'c.hashtag', 
            'description' => 'c.description',
            'parent_name' => "''",
            'parent_id'   => 'c.parent_id' 
        );

vermeul's avatar
vermeul committed
781
        # we received a hashtag: use that.
vermeul's avatar
vermeul committed
782
        if (substr($category, 0,1) == '#') {
vermeul's avatar
vermeul committed
783
            $row = $dbr->selectRow(
vermeul's avatar
vermeul committed
784
785
                array('c' =>'yata_category'),
                $selection,
vermeul's avatar
vermeul committed
786
                array(
vermeul's avatar
vermeul committed
787
                    'c.hashtag' => $category,
vermeul's avatar
vermeul committed
788
789
790
791
792
793
794
795
796
797
798
799
                )
            );
            return $row;
        }

        # get parent and child category: parent_cat/child_cat
        list($parent_cat, $child_cat) = preg_split('/\s*\/\s*/', $category);

        # we might only have a parent (top) category
        if (is_null($child_cat)) {
            # search for a top category (no parent)
            $row = $dbr->selectRow(
vermeul's avatar
vermeul committed
800
801
                array('c' =>'yata_category'),
                $selection,
vermeul's avatar
vermeul committed
802
803
804
                array(
                    'name'        => $parent_cat,
                    'parent_id'   => null
vermeul's avatar
vermeul committed
805
806
807
808
809
810
811
                )
            );
            return $row;
        }
        else {
            # search for category where parent matches
            # by self-joining table via parent_id
vermeul's avatar
vermeul committed
812
            $selection['parent_name'] = 'pc.name';
vermeul's avatar
vermeul committed
813
            $row = $dbr->selectRow(
vermeul's avatar
vermeul committed
814
815
                array('pc'=>'yata_category', 'c'=>'yata_category'),
                $selection,
vermeul's avatar
vermeul committed
816
                array(
vermeul's avatar
vermeul committed
817
                    'c.name' => $child_cat,
818
                    'pc.name' => $parent_cat,
vermeul's avatar
vermeul committed
819
820
821
                ),
                __METHOD__,
                array(),
vermeul's avatar
vermeul committed
822
                array( 'pc' => array( 'INNER JOIN', array ( 'c.parent_id = pc.id' ) ) )
vermeul's avatar
vermeul committed
823
824
825
826
827
            );
            return $row;
        } 
    }

828
829
830
831
    public static function get_category_and_parent_for_id($dbr, $id) {
        $row = $dbr->selectRow(
            array(c=>'yata_category', pc=>'yata_category'),
            array(
832
                'id'          => 'c.id',
vermeul's avatar
vermeul committed
833
834
835
836
837
                'name'        => 'c.name',
                'hashtag'     => 'c.hashtag',
                'description' => 'c.description',
                'parent_name' => 'pc.name',
                'parent_id'   => 'pc.id'
838
839
840
841
842
843
844
845
846
847
848
            ),
            array(
                'c.id' => $id
            ),
            __METHOD__,
            array(),
            array('pc' => array('LEFT JOIN', array('c.parent_id = pc.id') ) )
        );
        return $row;
    }

vermeul's avatar
vermeul committed
849
    public static function get_child_categories($dbr, $category, $level=0) {
vermeul's avatar
vermeul committed
850
851
        $rows = $dbr->select(
            array('yata_category'),
vermeul's avatar
vermeul committed
852
            array('id', 'name', 'hashtag', 'description', 'parent_id', "$level as level"),
vermeul's avatar
vermeul committed
853
854
855
            array(
                "parent_id" => $category->id
            )
vermeul's avatar
vermeul committed
856
        );
vermeul's avatar
vermeul committed
857
858
859
860
861

        $all_child_categories = array();

        foreach($rows as $row) {
            array_push($all_child_categories, $row);
vermeul's avatar
vermeul committed
862
            $child_categories = self::get_child_categories($dbr, $row, $level+1);
vermeul's avatar
vermeul committed
863
864
865
866
867
868

            foreach($child_categories as $child_category) {
                array_push($all_child_categories, $child_category);
            }
        }
        return $all_child_categories;
vermeul's avatar
vermeul committed
869
870
    }

vermeul's avatar
vermeul committed
871
872
873
874
875
876
877
878
879
    public static function delete_annotations($dbw, $wikiPage) {
        $dbw->delete(
            'yata_annotation',
            array(
                page_id => $wikiPage->getId()
            )
        );
    }

880
881
882
883
884
885
886
887
888
889
890
891
    public static function check_annotation_bookmark_exist($dbr, $wikiPage, $bookmark) {
        $row = $dbr->selectRow(
            array('yata_annotation'),
            array('id'),
            array(
                bookmark => $bookmark,
                page_id  => $wikiPage->getId()
            )
        );
        return $row;
    }

vermeul's avatar
vermeul committed
892
893
894
895
896
897
898
899
900
    public static function delete_annotation_categories($dbw, $wikiPage) {
        $dbw->delete(
            'yata_annotation_category',
            array(
                page_id => $wikiPage->getId()
            )
        );
    }

901
902
    public static function insert_annotation_category($dbw, $wikiPage, $annotation_id, $category_id) {
        $ex_ac = $dbw->selectRow(
vermeul's avatar
vermeul committed
903
            'yata_annotation_category',
904
            array('anz' => 'COUNT(*)'),
vermeul's avatar
vermeul committed
905
            array(
906
                page_id       => $wikiPage->getId(),
vermeul's avatar
vermeul committed
907
908
909
910
                annotation_id => $annotation_id,
                category_id   => $category_id
            )
        );
911
912
913
914
915
916
917
918
919
920
921
922
923
        if ($ex_ac->anz) {
            return;
        }
        else {
            $dbw->insert(
                'yata_annotation_category',
                array(
                    page_id       => $wikiPage->getId(),
                    annotation_id => $annotation_id,
                    category_id   => $category_id
                )
            );
        }
vermeul's avatar
vermeul committed
924
925
    }

926
927
928
929
930
931
932
933
934
935
936
937
938
    # create a unique identifier / bookmark for all
    # annotations which don't have one.
    # It should be long enough to be 99.999% unique on a page
    public static function create_bookmark() {
        $id = substr(
                str_shuffle(
                    str_repeat(
                        '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
                        mt_rand(1,10)
                    )
                
                ),
        1,4);
vermeul's avatar
vermeul committed
939
940
941
        return $id;
    }

942
943
944
    # Main function to parse all annotations,
    # insert a random id (where it doesn't exist)
    public static function parse_annotations($dbw, &$data, $wikiPage){
945
946
947
948
        /*
        1. get all existing annotations which exist on that page.
        2. find out which annotations have been updated (using the unique identifier)
        3. find out which annotations have been added -> assign a unique identifier
vermeul's avatar
merge    
vermeul committed
949
        4. find out which annotations have been removed from the wikitext and delete them
950
951
952
        5. assign every annotation to one or more categories
        6. assign every annotation to this page
        */
vermeul's avatar
vermeul committed
953

954
955
956
957
        # match either #annot: or #annotend:
        # fetch all options too
        $annotations_found = preg_match_all(
            '/(?P<annotation>{{\s*#annot(?P<end>end)?\s*:\s*(?P<opts>.*?)\s*}})/s', 
vermeul's avatar
vermeul committed
958
            $data, 
959
            $reg_params, 
vermeul's avatar
vermeul committed
960
961
            PREG_OFFSET_CAPTURE
        );
962
963
964
965
966
967
968
969
970
971
972
973
974

        # we can have annotations with an id or without.
        # The id was either entered by hand (for creating overlapping annotations)
        # or it was automatically generated.
        $annotations_with_id = array();
        $annotations = array();

        # as we crawl through the found annotations and give them new ids,
        # we also recreate the wikitext containing all the new ids.
        $new_data = "";  
        $starts = array();
        $start_loc = 0;
        $end_loc   = 0;
vermeul's avatar
vermeul committed
975
        
976
977
978
979
980
981
982
        foreach($reg_params["annotation"] as $index => $value) {
            # $value[1] contains the location where the current tag has been found
            $end_loc = $value[1];
            # rewrite the source code to include new ids in the code
            $new_data = $new_data . substr($data, $start_loc, $end_loc-$start_loc);
            $start_loc = $value[1] + strlen($value[0]);

983
            # we encountered an annotation START
984
985
986
987
988
989
990
991
992
993
994
995
996
            if ($reg_params["end"][$index][0] === "") {
                # split comment|$category|$bookmark by vertical bar
                list($comment, $category, $bookmark) = preg_split(
                    '/\s*\|\s*/', 
                    $reg_params["opts"][$index][0]
                ); 
                # more than one category can be split using a comma: 
                #   category1, category2
                # categories are by default hierarchically organized and must appear
                # in following formats:
                #   parent_category/child_category
                #   /top_category
                #   top_category
997
998
999
				$cat_strs = preg_split("/\s*\,\s*/", $category, -1, PREG_SPLIT_NO_EMPTY);
                $cats = array();
                foreach($cat_strs as $cat_str) {
vermeul's avatar
vermeul committed
1000
                    # we got a category id, which consists only of numbers