To receive notifications about scheduled maintenance, please subscribe to the mailing-list gitlab-operations@sympa.ethz.ch. You can subscribe to the mailing-list at https://sympa.ethz.ch

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

10
11
12
13
14
use SMW\ApplicationFactory;
#use SMWQuery;
#use SMWQueryProcessor;
#use SMWQueryResult;

vermeul's avatar
vermeul committed
15
class YATA {
16
    public static $annotations = "";
17
    public static $page_annotations = "";
vermeul's avatar
vermeul committed
18
    //
vermeul's avatar
vermeul committed
19
    // Register any render callbacks with the parser
vermeul's avatar
vermeul committed
20
    //
vermeul's avatar
vermeul committed
21
22
23
    public static function onParserSetup( &$parser ) {

       // Create a function hook associating the "example" magic word with renderExample()
vermeul's avatar
...    
vermeul committed
24
25
26
27
28
29
       $parser->setFunctionHook('annot',     'YATA::renderStartAnnot');
       $parser->setFunctionHook('annotend',  'YATA::renderEndAnnot');
       $parser->setFunctionHook('annotlist', 'YATA::annotations_list');
       $parser->setFunctionHook('annotask',  'YATA::annotation_query');
       $parser->setFunctionHook('annotcat',  'YATA::annotation_categories');
       $parser->setFunctionHook('annotpage', 'YATA::annotation_page');
vermeul's avatar
vermeul committed
30
31
    }

vermeul's avatar
vermeul committed
32
    //
vermeul's avatar
vermeul committed
33
    // Render the output of {{#annot: comment | category | id}}.
vermeul's avatar
vermeul committed
34
    //
vermeul's avatar
vermeul committed
35
    public static function renderStartAnnot( $parser, $comment='', $category='', $id='' ) {
vermeul's avatar
...    
vermeul committed
36
37
38
39
40
41
        if ($comment === 'list') {
            return self::annotations_list($parser);    
        }
        else {
            return "<span id=$id title='$comment'>";
        }
vermeul's avatar
vermeul committed
42
43
    }

vermeul's avatar
vermeul committed
44
    //
vermeul's avatar
vermeul committed
45
    // Render the output of {{#annotend:}}.
vermeul's avatar
vermeul committed
46
    //
vermeul's avatar
vermeul committed
47
    public static function renderEndAnnot( $parser, $id='' ) {
vermeul's avatar
vermeul committed
48
49
50
       return "</span>";
    }
    
vermeul's avatar
vermeul committed
51
    //
vermeul's avatar
vermeul committed
52
    // Render the output of {{#annotask:}}
vermeul's avatar
vermeul committed
53
    //
vermeul's avatar
vermeul committed
54
    public static function annotation_query( $parser, $querystring) {
55
56
        $title = $parser->getTitle();
        $wikiPage = new WikiPage( $title );
vermeul's avatar
vermeul committed
57
58
59
60
61
62
        $queries_found = preg_match_all(
            '/\[\[(?P<query>.*?)\]\]/s',
            $querystring, 
            $query_params, 
            PREG_OFFSET_CAPTURE
        );
vermeul's avatar
vermeul committed
63
64
        $searches = array();
        $in_categories = array();
vermeul's avatar
vermeul committed
65
66
67
68

        $where = array();
        $dbr = wfGetDB( DB_REPLICA );
        $seen = array();
69
        $errors = array();
vermeul's avatar
vermeul committed
70
71
        foreach($query_params["query"] as $query) {
            list($field, $values) = preg_split('/\s*\:\s*/', $query[0]);
72
73
74
75
            # syntax-sugar
            if ($field === 'wikitext') {
                $field = 'wiki_text';
            }
vermeul's avatar
vermeul committed
76
77
78
79
            if( $field === 'category') {
                $categories = preg_split('/\s*\,\s*/', $values);
                $cat_ids = array();
                foreach( $categories as $category ) {
vermeul's avatar
vermeul committed
80
                    array_push($in_categories, $category);
vermeul's avatar
vermeul committed
81
82
83
84
85
86
87
88
89
                    $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 {
90
                        array_push($errors, "no such category found: $category");
vermeul's avatar
vermeul committed
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
                    }
                }
                $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;
            }
vermeul's avatar
...    
vermeul committed
106
107
108
109
110
111
112
113
114
115
116
            elseif( $field === 'page' ) {
                $keyvalues=array();
                self::extract_keyvalues($values, $keyvalues);
                $where[] = 
                    "EXISTS(
                        SELECT NULL
                        FROM yata_page_annotation pa
                    ";
                    //    WHERE pa.page_id = ".$wikiPage->getId()

            }
vermeul's avatar
vermeul committed
117
            elseif( in_array($field, array('wiki_text', 'comment')) ) {
vermeul's avatar
vermeul committed
118
                array_push($searches, $field . ': ' . $values);
vermeul's avatar
vermeul committed
119
120
121
122
123
124
125
126
                if ( preg_match( '/[\%_]/', $values ) ) {
                    $where[] = "$field LIKE (" . $dbr->addQuotes($values).")";
                }
                else {
                    $where["a.$field"] = $values;
                }
            }
        }
127

128
	if ($errors) {
129
130
131
132
133
            foreach( $errors as $error) {
                $errstr .= $error . "<br/>";
            }
            return $errstr;
        }
vermeul's avatar
vermeul committed
134
135
136
137
138
        

        # fetch the annotation details
        $annotations = $dbr->select(
            array(
vermeul's avatar
vermeul committed
139
140
141
142
143
144
                'a' => 'yata_annotation',
                'u' => 'user',
                'ac'=> 'yata_annotation_category',
                'c' => 'yata_category',
                'pc'=> 'yata_category',
                'p' => 'page',
vermeul's avatar
vermeul committed
145
146
            ),
            array(
vermeul's avatar
vermeul committed
147
148
149
150
151
152
153
154
155
156
                '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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
            ),
            $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') ),
            )
        );

172
        $table = "";
vermeul's avatar
vermeul committed
173
174
175
176
177
178
179
180
181
182
183
184
185
        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
186
        $table .= '([' . $title->getFullURL() . '?action=purge' . ' refresh page]):';
vermeul's avatar
vermeul committed
187
188
        if (count($annotations)>0) {
            $table .= '
vermeul's avatar
vermeul committed
189
<table class="wikitable sortable jquery-tablesorter">
vermeul's avatar
vermeul committed
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
    <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
226
        }
vermeul's avatar
vermeul committed
227

vermeul's avatar
vermeul committed
228
        return $table;
vermeul's avatar
vermeul committed
229
    }
vermeul's avatar
vermeul committed
230

vermeul's avatar
vermeul committed
231

vermeul's avatar
vermeul committed
232
    //
vermeul's avatar
vermeul committed
233
    // Render the output of {{#annotlist:}}.
vermeul's avatar
vermeul committed
234
    //
vermeul's avatar
vermeul committed
235
236
237
    public static function annotations_list( $parser ) {
        $title = $parser->getTitle();
        $wikiPage = new WikiPage( $title );
238
239
        #$dbr = wfGetDB( DB_REPLICA );
        $dbr = wfGetDB( DB_MASTER );
vermeul's avatar
vermeul committed
240

vermeul's avatar
vermeul committed
241
242
243
        $table = <<<"EOT"
{| class="wikitable sortable"
|-
vermeul's avatar
vermeul committed
244
! Category !! Hashtag !! Comment !! Link !! Annotated text !! Last edited by !! Modification date
vermeul's avatar
vermeul committed
245

vermeul's avatar
vermeul committed
246
EOT;
247
        # fetch the annotation details
vermeul's avatar
vermeul committed
248
249
250
251
252
        $annotations = $dbr->select(
            array(
                a =>'yata_annotation',
                ac=>'yata_annotation_category',
                c =>'yata_category',
253
254
                pc=>'yata_category',
                u =>'user',
vermeul's avatar
vermeul committed
255
256
            ),
            array(
257
                comment         => "a.comment", 
vermeul's avatar
vermeul committed
258
259
                wiki_text       => "a.wiki_text",
                start_char      => "a.start_char",
vermeul's avatar
vermeul committed
260
                bookmark        => "a.bookmark",
vermeul's avatar
vermeul committed
261
                category        => "c.name",
vermeul's avatar
vermeul committed
262
                hashtag         => "c.hashtag",
263
264
265
                parent_category => "pc.name",
                last_edited_by  => "u.user_name",
                last_modified   => "a.modified_date"
vermeul's avatar
vermeul committed
266
267
            ),
            array(
vermeul's avatar
...    
vermeul committed
268
                'a.page_id' => $wikiPage->getId(),
vermeul's avatar
vermeul committed
269
270
271
272
273
274
            ),
            __METHOD__, 
            array(
                'ORDER BY' => 'a.start_char ASC' 
            ), 
            array( 
275
276
277
278
                '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
279
280
            )
        );
vermeul's avatar
vermeul committed
281

282
        # compose the wiki table
vermeul's avatar
vermeul committed
283
284
285
        foreach($annotations as $annotation) {
            # if a category has no parent, just show the category name
            # in all other cases show parent_category:child_category
286
287
288
289
290
            $wikitext = preg_replace(
                '/{{\s*#annot(?:<end>end)?.*?}}/', 
                '',
                $annotation->wiki_text
            );
291
            $cat = $annotation->parent_category ? $annotation->parent_category . "/" . $annotation->category : $annotation->category;
vermeul's avatar
vermeul committed
292
293
            $table .= "|-\n";
            $table .= "| ".$cat
vermeul's avatar
vermeul committed
294
                   ." || ".$annotation->hashtag
vermeul's avatar
vermeul committed
295
                   ." || ".$annotation->comment 
vermeul's avatar
vermeul committed
296
                   ." || [[$title#".$annotation->bookmark . "]]"
297
298
299
                   ." || " . $wikitext
                   ."\n || " . $annotation->last_edited_by
                   ."\n || " . $annotation->last_modified
vermeul's avatar
vermeul committed
300
                   ."\n";
vermeul's avatar
vermeul committed
301
302
303
304
        }
        $table .= "|}";
        return $table;
        
vermeul's avatar
vermeul committed
305
306
    }

vermeul's avatar
vermeul committed
307
    //
vermeul's avatar
vermeul committed
308
    // Render the output of {{#annotcat: list}}.
vermeul's avatar
vermeul committed
309
    //
310
    public static function annotation_categories( $parser, $method, $start_with=null ) {
311
        if ($method  != "list") {
vermeul's avatar
vermeul committed
312
313
            return "";
        }
314
        $dbr = wfGetDB( DB_MASTER );
vermeul's avatar
vermeul committed
315
316
317
318
319
320
321
322
323
324
325
326

        $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') );


327
328
329
        $table = <<<"EOT"
{| class="wikitable sortable"
|-
vermeul's avatar
vermeul committed
330
! Category !! Hashtag !! Description !! id !! parent_id
331
332
333
334
335
336
337

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
338
            $table .= "| ".$category->name
vermeul's avatar
vermeul committed
339
                   ." || ".$category->hashtag
vermeul's avatar
vermeul committed
340
                   ." || ".$category->description
vermeul's avatar
vermeul committed
341
342
                   ." || ".$category->id
                   ." || ".$category->parent_id
vermeul's avatar
vermeul committed
343
                   ."\n";
vermeul's avatar
vermeul committed
344
345
346
347
348
            $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
349
                        ." || ".$child_category->hashtag
vermeul's avatar
vermeul committed
350
351
352
353
354
355
356
                        ." || ".$child_category->description
                        ." || ".$child_category->id
                        ." || ".$child_category->parent_id
                        ."\n";
                }
            }

357
358
359
360
361
362
        }
        $table .= "|}";
        return $table;

    }

vermeul's avatar
...    
vermeul committed
363
    //
364
    // Rendering of {{#annotpage: list}}
vermeul's avatar
...    
vermeul committed
365
    //
vermeul's avatar
vermeul committed
366
    public static function annotation_page( $parser, $method ) {
367
        if ($method != "list") {
vermeul's avatar
vermeul committed
368
369
            return "";
        }
vermeul's avatar
...    
vermeul committed
370
371
        $title = $parser->getTitle();
        $wikiPage = new WikiPage( $title );
vermeul's avatar
...    
vermeul committed
372
        $wikiPage->doPurge();
vermeul's avatar
...    
vermeul committed
373
374
375
376
377
378
379
380
381
382
383
        $dbr = wfGetDB( DB_MASTER );

        $annotations = self::get_page_annotations($dbr, $wikiPage );

        $table = <<<"EOT"
{| class="wikitable sortable"
|-
! Name !! Value !! Creation Date !! Created By

EOT;

vermeul's avatar
vermeul committed
384
        foreach($annotations as $annotation) {
vermeul's avatar
...    
vermeul committed
385
386
387
            # if a annotations has no parent, just show the annotations name
            # in all other cases show parent_annotations:child_annotations
            $table .= "|-\n";
vermeul's avatar
vermeul committed
388
389
            $table .= "| ".$annotation->name
                ." || ".$annotation->value
vermeul's avatar
...    
vermeul committed
390
                ." || ".wfTimestamp( TS_ISO_8601, $annotation->insert_date)
vermeul's avatar
vermeul committed
391
                ." || ".$annotation->user_name
vermeul's avatar
...    
vermeul committed
392
393
394
395
396
397
398
                ."\n";
        }
        $table .= "|}";
        return $table;

    }

vermeul's avatar
vermeul committed
399
400

    public static function onPageContentSave(WikiPage &$wikiPage, User &$user, Content &$content, $summary, $isMinor, $isWatch, $section, $flags, Status &$status ){
vermeul's avatar
vermeul committed
401
    /*
402
403
404
405
406
407
408
        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)
409
410
        2. parse the annotations (see parse_annotations)
        3. replace the wiki_text 
411
412
        
        The annotations are saved in the next hook: onPageContentSaveComplete (see below)
413

vermeul's avatar
vermeul committed
414
415
416
417
418
419
420
421
    */

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

            $data = $content->getNativeData();
vermeul's avatar
vermeul committed
422
423
424
            # 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);
425

vermeul's avatar
...    
vermeul committed
426
            # 1. add / delete categories 
vermeul's avatar
vermeul committed
427
428
429
430
431
432
433
            try {
                $data = self::manage_categories($dbw, $data);
            }
            catch (Exception $e) {
               $status->fatal($e->getMessage());
               return false;
            }
vermeul's avatar
vermeul committed
434

vermeul's avatar
...    
vermeul committed
435
            # 2. get all annotations that exist in the current wikitext
vermeul's avatar
vermeul committed
436
            try {
vermeul's avatar
...    
vermeul committed
437
                list($annotations, $page_annotations) = self::parse_annotations($dbw, $data, $wikiPage);
vermeul's avatar
vermeul committed
438
439
440
441
442
            }
            catch (Exception $e) {
               $status->fatal($e->getMessage());
               return false;
            }
443

444
            # keep the annotations for saving later (onPageContentSaveComplete)
445
            self::$annotations = $annotations;
446
            self::$page_annotations = $page_annotations;
447

vermeul's avatar
...    
vermeul committed
448
            # 3. replace the content and save it
449
450
            $content = new WikitextContent( $data );
        }
vermeul's avatar
vermeul committed
451
452
453
454

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

vermeul's avatar
merge    
vermeul committed
457
458
459
460
    /*
    This function is called after the save page request has been processed.
    @see https://www.mediawiki.org/wiki/Manual:Hooks/PageContentSaveComplete
    
461
462
463
    - 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.
464
465
466
467
    */
    public static function onPageContentSaveComplete($wikiPage, $user, $content, $summary,
    $isMinor, $isWatch, $section, $flags, $revision, $status, $baseRevId ) {
        $dbw = wfGetDB( DB_MASTER );
468
469
470
        $seen_bookmarks = array();
        $seen_annotation_ids = array();
       $i = 0; 
471
472
473
        foreach ( self::$annotations as $bookmark => $annotation ){
            $annotation_id = 0;
            $ex_annot = self::get_annotation($dbw, $wikiPage, $bookmark);
474
475

            # existing annotation found: update wiki_text, comment
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
            if ($ex_annot) {
                $annotation_id = $ex_annot->id;
                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
                        )
                    );
                }
            }
495
            # new annotation found: insert annotation
496
            else {
vermeul's avatar
vermeul committed
497
                # write annotation
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
                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;
                }
528
            }
529
530
531
532
533
            # 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
534

535
536
537
            # insert / update the annotation categories
            if ( $annotation['categories'] ) {
                $seen_acs = array();
538
                foreach ( $annotation['categories'] as $category ){
vermeul's avatar
vermeul committed
539
540
                    # assign all found categories to this annotation
                    if(! is_null($category)) {
541
542
                        self::insert_annotation_category($dbw, $wikiPage, $annotation_id, $category->id);
                        array_push($seen_acs, $category->id);
vermeul's avatar
vermeul committed
543
544
                    }
                }
545
546
                # delete all annotation categories which are no longer assigned
                # to this annotation
vermeul's avatar
merge    
vermeul committed
547
548
549
550
551
552
553
554
555
                $dbw->delete(
                    'yata_annotation_category',
                    array( 
                        page_id       => $wikiPage->getId(),
                        annotation_id => $annotation_id,
                        'category_id NOT IN(' .$dbw->makeList($seen_acs). ')'
                    )
                );
            }
556
557
558
559
560
561
562
563
564
565
            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
566
            }
vermeul's avatar
vermeul committed
567
        }
568
        
vermeul's avatar
vermeul committed
569

570
        # find all all annotations on that page...
vermeul's avatar
merge    
vermeul committed
571
572
573
        $where = [
            "page_id" => $wikiPage->getId()
        ];
574
575
576
        #...which are no longer present
        if ($seen_annotation_ids) {
            $where[] = 'id NOT IN(' .$dbw->makeList($seen_annotation_ids). ')';
577
578
579
580
581
        }

        $annotations_to_delete = $dbw->select(
            'yata_annotation',
            array( 'id' ),
vermeul's avatar
vermeul committed
582
            $where
583
584
        );

585

586
587
        # delete all annotations which are no longer present
        foreach( $annotations_to_delete as $annotation) {
588
589
590
591
592
593
            $dbw->delete(
                'yata_annotation',
                array(
                    'id' => $annotation->id 
                )
            );
vermeul's avatar
merge    
vermeul committed
594
595
596
597
598
599
600
601
602

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

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

603
604
605
606
            $dbw->delete(
                'yata_annotation_category',
                $delete_where
            );
vermeul's avatar
vermeul committed
607
        }
608

vermeul's avatar
...    
vermeul committed
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
        $ex_page_annotations = self::get_page_annotations($dbw, $wikiPage);
        $ex_annotations = array();
        if ($ex_page_annotations) {
            foreach ($ex_page_annotations as $ex_page_annotation) {
                $ex_annotations[$ex_page_annotation->name] = $ex_page_annotation->value;
            }
        }

        foreach( self::$page_annotations as $key => $value) {
            if (array_key_exists($key, $ex_annotations)){
                if ($ex_annotations[$key] != $value) {
                    self::update_page_annotation($dbw, $wikiPage, $key, $value);
                }
                else {
                    # value has not changed - do nothing
                }
                unset($ex_annotations[$key]);
            }
            else {
                self::insert_page_annotation($dbw, $wikiPage, $user, $key, $value);
            }
        }

        # remove the keys and values that are no longer in use.
        foreach( $ex_annotations as $key => $value) {
            self::delete_page_annotation($dbw, $wikiPage, $key, $value);
635
        }
vermeul's avatar
...    
vermeul committed
636

vermeul's avatar
vermeul committed
637
638
    }

vermeul's avatar
vermeul committed
639
    public static function add_category($dbw, $category_name, $hashtag, $description, $parent) {
vermeul's avatar
vermeul committed
640
        $category_name = trim($category_name);
641
642
643
        $parent_id = null;
        if (! is_null($parent)) {
            $parent_category = self::get_category($dbw, $parent);
vermeul's avatar
vermeul committed
644
            if(! $parent_category) {
vermeul's avatar
vermeul committed
645
                throw new Exception("Error adding new category: no such parent-category found: $parent");
vermeul's avatar
vermeul committed
646
            }
647
648
649
            $parent_id = $parent_category->id;
        }

vermeul's avatar
vermeul committed
650
651
652
653
654
655
656
657
658
659
660
661
662
        $exists = $dbw->selectRow(
            'yata_category',
            array('id'),
            array(
                name => $category_name,
                parent_id => $parent_id
            )
        );

        if ($exists) {
            return;
        }

663
        # make sure hashtags always start with a hash symbol: #
vermeul's avatar
vermeul committed
664
        if ($hashtag && substr($hashtag, 0,1) != '#') {
665
666
667
            $hashtag = '#'.$hashtag;
        }

668
669
670
        $dbw->insert(
            'yata_category',
            array(
vermeul's avatar
vermeul committed
671
                name        => $category_name,
vermeul's avatar
vermeul committed
672
                hashtag     => $hashtag,
673
674
675
676
677
678
679
                description => trim($description),
                parent_id   => $parent_id
            )
        );

    }

vermeul's avatar
vermeul committed
680
681
682
683
684
685
686
687
688
689
690
691
692
693
    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);
694
            if ($category_exists && $category_exists->id != $category->id) {
vermeul's avatar
vermeul committed
695
696
697
698
                throw new Exception("Error updating category - already exists: $new_category_name");
            }
            $set["name"] = $args["name"];
        }
vermeul's avatar
vermeul committed
699
        if (array_key_exists("hashtag", $args) ) {
vermeul's avatar
vermeul committed
700
            if ($args["hashtag"] && substr($args["hashtag"], 0,1) != '#') {
701
702
                $args["hashtag"] = '#'.$args["hashtag"];
            }
vermeul's avatar
vermeul committed
703
704
            $set["hashtag"] = $args["hashtag"];
        }
vermeul's avatar
vermeul committed
705
706
707
708
709
710
711
712
713
714
715
716
717
        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
718
            }
vermeul's avatar
vermeul committed
719
720
721
            else {
                # define category as a top category (remove parent)
                $set["parent_id"] = null;
vermeul's avatar
vermeul committed
722
            }
vermeul's avatar
vermeul committed
723
724
        }
        if ($set) {
vermeul's avatar
vermeul committed
725
726
727
728
729
730
731
732
            $dbw->update(
                'yata_category',
                $set,
                array(
                    id => $category->id
                )
            );
        }
vermeul's avatar
vermeul committed
733
734
735
736
        else {
            throw new Exception("Error updating category - nothing needs to be updated.");
        }
        
vermeul's avatar
vermeul committed
737
738
739
    }

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

vermeul's avatar
vermeul committed
741
        if (substr($category_name, 0, 1) == '#') {
vermeul's avatar
vermeul committed
742
743
744
745
746
747
748
749
750
            # we go a hashtag, which should be unique.
            $dbw->delete(
                'yata_category',
                array(
                    hashtag => $category_name,
                )
            );
            return;
        }
vermeul's avatar
vermeul committed
751
        $cat_entry = self::get_category($dbw, $category_name);
vermeul's avatar
vermeul committed
752
753
754
        if (!$cat_entry) {
            throw new Exception("Error deleting category - no such category: $category_name");
        }
755
756
757
758
759
760
761
762
        $dbw->delete(
            'yata_category',
            array(
                id => $cat_entry->id
            )
        );
    }

vermeul's avatar
vermeul committed
763
764
    private static function split_cat_argstring($arg_string) {
        $args = array();
vermeul's avatar
vermeul committed
765
766
        # name='xxx', description='blabla', hashtag='#blabla', parent='parent/category'
        # splits argumentlist above and returns a dictionary instead.
vermeul's avatar
vermeul committed
767
        $keyvals = preg_split('/\s*\,\s*(?=(name|hashtag|description|parent))/', $arg_string);
vermeul's avatar
vermeul committed
768
769
770
771
        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
772
773
774
            $value = preg_replace('/^\s*(\'|\")/', '', $value);
            $value = preg_replace('/(\'|\")\s*$/', '', $value);
            $value = preg_replace('/\s*$/', '', $value);
vermeul's avatar
vermeul committed
775
776
777
778
779
            $args[$key] = $value;
        }
        return $args;
    }

780
781
    private static function category_add_del_callback($matches) {
        $dbw = wfGetDB( DB_MASTER );
vermeul's avatar
vermeul committed
782
783
        # split the arguments (after #annotcat:)
        #
vermeul's avatar
vermeul committed
784
785
        # {{#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
786
        # {{#annotcat: del | parent_category_name/category_name}}
vermeul's avatar
vermeul committed
787
        # {{#annotcat: del | #shortcut }}
vermeul's avatar
vermeul committed
788
789
        
        list($method, $arg1, $arg2) = preg_split('/\s*\|\s*/', $matches["params"]);
790
        if ( $method === "add" ) {
vermeul's avatar
vermeul committed
791
            $args = self::split_cat_argstring($arg1);
vermeul's avatar
vermeul committed
792
            self::add_category($dbw, $args['name'], $args['hashtag'], $args['description'], $args['parent']);
vermeul's avatar
vermeul committed
793
794
        }
        elseif ( $method === "up" ) {
vermeul's avatar
vermeul committed
795
796
            $args = self::split_cat_argstring($arg2);
            self::up_category($dbw, $arg1, $args);
797
798
        }
        elseif ( $method === "del" ) {
vermeul's avatar
vermeul committed
799
800
            # $arg_string already contains the category name, we do not have to split the arguments
            self::del_category($dbw, $arg1);
801
        }
vermeul's avatar
vermeul committed
802
803
804
        else {
            return $matches[0];
        }
vermeul's avatar
vermeul committed
805
806
807

        # remove the {{#annotcat: add|up|del }} commands from the source code
        # by returning an empty string
808
809
810
811
        return "";
    }

    // manage any categories added
812
    public static function manage_categories( $dbw, $data) {
813
814
815
816
817
818
819
820
821
822
823
        # 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;
    }

824
825
826
827
828
829
830
831
832
833
834
835
    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;
    }

vermeul's avatar
vermeul committed
836
837
    public static function get_page_annotations($dbr, $wikiPage) {
        $order_by = array('ORDER BY' => 'name');
vermeul's avatar
...    
vermeul committed
838
839
840
        $where = array(
            'pa.page_id' => $wikiPage->getId()
        );
vermeul's avatar
...    
vermeul committed
841
842
843
844
845
846
847
848
849
850
851
852

        $annotations = $dbr->select(
            array('pa' => 'yata_page_annotation', 
                  'u'  => 'user'),
            array(
                'pa.id',
                'pa.page_id',
                'pa.name',
                'pa.value',
                'pa.insert_date',
                'u.user_name'
            ),
vermeul's avatar
...    
vermeul committed
853
            $where,
vermeul's avatar
...    
vermeul committed
854
855
856
857
858
859
860
861
862
            __METHOD__,
            $order_by,
            array(
                'u' => array( 'LEFT JOIN', array('u.user_id = pa.user_id') ) 
            )
        );
        return $annotations;
    }

vermeul's avatar
...    
vermeul committed
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
    public static function update_page_annotation($dbw, $wikiPage, $key, $value) {
        $dbw->update(
            'yata_page_annotation',
            array(
                "value"         => $value,
                "modified_date" => $dbw->timestamp(),
            ),
            array(
                "page_id"  => $wikiPage->getId(),
                "name"     => $key
            )
        );
    }

    public static function insert_page_annotation($dbw, $wikiPage, $user, $key, $value) {
        $dbw->insert(
            'yata_page_annotation',
            array(
                "page_id"       => $wikiPage->getId(),
                "name"          => $key,
                "value"         => $value,
                "user_id"       => $user->getId(),
                "insert_date"   => $dbw->timestamp(),
                "modified_date" => $dbw->timestamp(),
            )
        );
    }

    public static function delete_page_annotation($dbw, $wikiPage, $key, $value) {
        $dbw->delete(
            'yata_page_annotation',
            array(
                "page_id" => $wikiPage->getId(),
                "name"    => $key,
                "value"   => $value,
            )
        );
    }
901

vermeul's avatar
vermeul committed
902
903
904
905
906
907
    public static function search_categories($dbr, $where, $order_by) {
        if (! $order_by) {
            $order_by = array('ORDER BY' => 'name');
        }

        $categories = $dbr->select(
908
909
910
911
912
913
914
915
            array('c' => 'yata_category'),
            array( 
                'c.id', 
                'c.name', 
                'c.hashtag', 
                'c.description', 
                'c.parent_id'
            ),
vermeul's avatar
vermeul committed
916
917
918
919
920
921
922
            $where,
            __METHOD__,
            $order_by
        );
        return $categories;
    }

923
924
925
    public static function get_category($dbr, $category) {
        $category = trim($category);

vermeul's avatar
vermeul committed
926
927
928
929
930
931
932
933
934
        $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
935
        # we received a hashtag: use that.
vermeul's avatar
vermeul committed
936
        if (substr($category, 0,1) == '#') {
vermeul's avatar
vermeul committed
937
            $row = $dbr->selectRow(
vermeul's avatar
vermeul committed
938
939
                array('c' =>'yata_category'),
                $selection,
vermeul's avatar
vermeul committed
940
                array(
vermeul's avatar
vermeul committed
941
                    'c.hashtag' => $category,
vermeul's avatar
vermeul committed
942
943
944
945
946
947
948
949
950
951
952
953
                )
            );
            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
954
955
                array('c' =>'yata_category'),
                $selection,
vermeul's avatar
vermeul committed
956
957
958
                array(
                    'name'        => $parent_cat,
                    'parent_id'   => null
vermeul's avatar
vermeul committed
959
960
961
962
963
964
965
                )
            );
            return $row;
        }
        else {
            # search for category where parent matches
            # by self-joining table via parent_id
vermeul's avatar
vermeul committed
966
            $selection['parent_name'] = 'pc.name';
vermeul's avatar
vermeul committed
967
            $row = $dbr->selectRow(
vermeul's avatar
vermeul committed
968
969
                array('pc'=>'yata_category', 'c'=>'yata_category'),
                $selection,
vermeul's avatar
vermeul committed
970
                array(
vermeul's avatar
vermeul committed
971
                    'c.name' => $child_cat,
972
                    'pc.name' => $parent_cat,
vermeul's avatar
vermeul committed
973
974
975
                ),
                __METHOD__,
                array(),
vermeul's avatar
vermeul committed
976
                array( 'pc' => array( 'INNER JOIN', array ( 'c.parent_id = pc.id' ) ) )
vermeul's avatar
vermeul committed
977
978
979
980
981
            );
            return $row;
        } 
    }

982
983
984
985
    public static function get_category_and_parent_for_id($dbr, $id) {
        $row = $dbr->selectRow(
            array(c=>'yata_category', pc=>'yata_category'),
            array(
986
                'id'          => 'c.id',
vermeul's avatar
vermeul committed
987
988
989
990
991
                'name'        => 'c.name',
                'hashtag'     => 'c.hashtag',
                'description' => 'c.description',
                'parent_name' => 'pc.name',
                'parent_id'   => 'pc.id'
992
993
994
995
996
997
998
999
1000
1001
1002
            ),
            array(
                'c.id' => $id
            ),
            __METHOD__,
            array(),
            array('pc' => array('LEFT JOIN', array('c.parent_id = pc.id') ) )
        );
        return $row;
    }

vermeul's avatar
vermeul committed
1003
    public static function get_child_categories($dbr, $category, $level=0) {
vermeul's avatar
vermeul committed
1004
1005
        $rows = $dbr->select(
            array('yata_category'),
vermeul's avatar
vermeul committed
1006
            array('id', 'name', 'hashtag', 'description', 'parent_id', "$level as level"),
vermeul's avatar
vermeul committed
1007
1008
1009
            array(
                "parent_id" => $category->id
            )
vermeul's avatar
vermeul committed
1010
        );
vermeul's avatar
vermeul committed
1011
1012
1013
1014
1015

        $all_child_categories = array();

        foreach($rows as $row) {
            array_push($all_child_categories, $row);
vermeul's avatar
vermeul committed
1016
            $child_categories = self::get_child_categories($dbr, $row, $level+1);
vermeul's avatar
vermeul committed
1017
1018
1019
1020
1021
1022

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

vermeul's avatar
vermeul committed
1025
1026
1027
1028
1029
1030
1031
1032
1033
    public static function delete_annotations($dbw, $wikiPage) {
        $dbw->delete(
            'yata_annotation',
            array(
                page_id => $wikiPage->getId()
            )
        );
    }

1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
    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
1046
1047
1048
1049
1050
1051
1052
1053
1054
    public static function delete_annotation_categories($dbw, $wikiPage) {
        $dbw->delete(
            'yata_annotation_category',
            array(
                page_id => $wikiPage->getId()
            )
        );
    }

1055
1056
    public static function insert_annotation_category($dbw, $wikiPage, $annotation_id, $category_id) {
        $ex_ac = $dbw->selectRow(
vermeul's avatar
vermeul committed
1057
            'yata_annotation_category',
1058
            array('anz' => 'COUNT(*)'),
vermeul's avatar
vermeul committed
1059
            array(
1060
                page_id       => $wikiPage->getId(),
vermeul's avatar
vermeul committed
1061
1062
1063
1064
                annotation_id => $annotation_id,
                category_id   => $category_id
            )
        );
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
        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
1078
1079
    }

1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
    # 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
1093
1094
1095
        return $id;
    }

vermeul's avatar
...    
vermeul committed
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
    public static function extract_keyvalues($string, &$keyvalues=array()) {
        // Match key-value pairs in a given string.

        // Will match key and values that are:
        // within single quotes '     \'.*?(?<!\\\\)\'
        // within double quotes "     \".*?(?<!\\\\)\"
        // with no quotes at all (and no spaces): [^\s]+
        $re =
            '/(?P<key>\'.*?(?<!\\\\)\'|\".*?(?<!\\\\)\"|[^\s]+)'
            .'\s*?=\s*?'
            .'(?P<val>\'.*?(?<!\\\\)\'|\".*?(?<!\\\\)\"|[^\s]+)/';
        preg_match_all($re, $string, $matches, PREG_SET_ORDER, 0);
        foreach($matches as $match) {
            # trim keys and values: remove any spaces and quotes
            $key = trim($match["key"], " '\"");
            $val = trim($match["val"], " '\"");
            $keyvalues[$key] = $val;
        }
    }

1116
1117
1118
    # Main function to parse all annotations,
    # insert a random id (where it doesn't exist)
    public static function parse_annotations($dbw, &$data, $wikiPage){
1119
1120
1121
1122
        /*
        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
1123
        4. find out which annotations have been removed from the wikitext and delete them
1124
1125
1126
        5. assign every annotation to one or more categories
        6. assign every annotation to this page
        */
vermeul's avatar
vermeul committed
1127

1128
1129
1130
        # match either #annot: or #annotend:
        # fetch all options too
        $annotations_found = preg_match_all(
vermeul's avatar
...    
vermeul committed
1131
            '/(?P<annotation>{{\s*#annot((?P<end>end)|(?P<page>page))?\s*:\s*(?P<opts>.*?)\s*}})/s', 
vermeul's avatar
vermeul committed
1132
            $data, 
1133
            $reg_params, 
vermeul's avatar
vermeul committed
1134
1135
            PREG_OFFSET_CAPTURE
        );
1136
1137
1138
1139
1140
1141

        # 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();
1142
        $page_annotations = array();
1143
1144
1145
1146
1147
1148
1149

        # 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
1150
        
1151
1152
1153
1154
1155
1156
1157
        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]);

vermeul's avatar
...    
vermeul committed
1158
            # we encountered an annotation ending
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
            if ($reg_params["page"][$index][0] === "page") {
               //print($reg_params["page"][$index][0]);
                $opts = $reg_params["opts"][$index][0];

                // Match key-value pairs.
                // see https://regex101.com/r/wK0eD2/1 for detailed explanations
                // Will match key and values that are:
                // within single quotes '     \'.*?(?<!\\\\)\'
                // within double quotes "     \".*?(?<!\\\\)\"
                // with no quotes at all (and no spaces): [^\s]+
                $re =
                 '/(?P<key>\'.*?(?<!\\\\)\'|\".*?(?<!\\\\)\"|[^\s]+)'
                 .'\s*?=\s*?'
                 .'(?P<val>\'.*?(?<!\\\\)\'|\".*?(?<!\\\\)\"|[^\s]+)/';
                preg_match_all($re, $opts, $matches, PREG_SET_ORDER, 0);
                foreach($matches as $match) {
                    # trim keys and values: remove any spaces and quotes
                    $key = trim($match["key"], " '\"");
                    $val = trim($match["val"], " '\"");
                    $page_annotations[$key] = $val;
1179
                }
1180
                //var_dump($page_annotations);
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
            }

            # we encountered an annotation ending
            elseif ($reg_params["end"][$index][0] === "end") {
                $bookmark = $reg_params["opts"][$index][0];
                # we encountered an id: try to find its beginning
                if ($bookmark) {
                    if ( $annotations_with_id[$bookmark] ) {
                        # we fetch the new id we created.
                        $new_bookmark = $annotations_with_id[$bookmark];
                        $annotations[ $new_bookmark ]["end"] = 
                            $reg_params["end"][$index];

                        # extract the wiki text between the annotations
                        #$start_char = $annotations[$new_bookmark]["start"][1] 
                        #            + strlen($annotations[ $new_bookmark ]["start"][0]);
                        $start_char = $annotations[$new_bookmark]["start_char"];
                        $end_char   = $reg_params["annotation"][$index][1];
                        $annotations[$new_bookmark]["wiki_text"]  = 
                            substr($data, $start_char, $end_char-$start_char);

                        $new_data = $new_data . "{{#annotend:$new_bookmark}}";
                    }
                    else {
vermeul's avatar
vermeul committed
1205
                        throw new Exception("found #annotend with bookmark=$bookmark but no annotation starts with this bookmark");
1206
1207
1208
1209
                    }
                }
                # we found an ending, but have not enough starts: there seems to be a start-ending mismatch
                elseif ( count($starts) == 0 ) {
vermeul's avatar
vermeul committed
1210
                    throw new Exception("starts and ends do not match: I encountered an ending but no corresponding start.");
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
                }
                # we pop the last start from our stack to match it with its ending
                else {
                    $start = array_pop($starts);
                    $start_char = $start[1] + strlen($start[0]);
                    $end_char   = $reg_params["annotation"][$index][1];
        
                    $new_bookmark = $start["new_bookmark"];
                    $annotations[$new_bookmark] = array(
                        "is_new"     => true,
                        "categories" => $start["categories"],
                        "comment"    => $start["comment"],
                        "wiki_text"  => substr($data, $start_char, $end_char-$start_char)
                    );
                    $reg_params["annotation"][$index][2] = $new_bookmark;
                    $reg_params["annotation"][$index]["new_annot"] = "{{#annotend:$new_bookmark}}";
                    $new_data = $new_data . "{{#annotend:$new_bookmark}}";
                }
            }
vermeul's avatar
...    
vermeul committed
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301

            # we encountered an annotation START
            elseif ($reg_params["end"][$index][0] === "" && $reg_params["page"][$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
				$cat_strs = preg_split("/\s*\,\s*/", $category, -1, PREG_SPLIT_NO_EMPTY);
                $cats = array();
                foreach($cat_strs as $cat_str) {
                    # we got a category id, which consists only of numbers
                    if ( preg_match("/^\s*\d*\s*$/", $cat_str) ) {
                        $cat = self::get_category_and_parent_for_id($dbw, $cat_str);
                    }
                    else {
                        $cat = self::get_category($dbw, $cat_str);
                    }
                    if ($cat) {
                        array_push($cats, $cat);
                    }
                    else {
                        throw new Exception("no such category: $cat_str");
                    }
                }
                # we encountered an id: replace it with a unique one,
                # if it doesn't exist yet.
                $new_bookmark = 0;
                if ($bookmark) {
                    if ( self::check_annotation_bookmark_exist($dbw, $wikiPage, $bookmark))
                        {
                        # it is an existing annotation, we keep the id
                        $new_bookmark = $bookmark;
                    } 
                    else {
                        # we replace the manually entered id by a random one
                        $new_bookmark = self::create_bookmark();
                    }
                    $annotations_with_id[$bookmark] = $new_bookmark;
                    $annotations[$new_bookmark] = array();
                    $annotations[$new_bookmark]["start_char"] = $value[1] + strlen($value[0]);
                    $annotations[$new_bookmark]["is_new"] = true;
                    $annotations[$new_bookmark]["categories"] = $cats;
                    $annotations[$new_bookmark]["comment"] = $comment;
                }
                # we found no id, push it on the stack to find its 
                # ending counterpart later
                else {
                    $new_bookmark = self::create_bookmark();
                    $value["new_bookmark"] = $new_bookmark;
                    $value["categories"] = $cats;
                    $value["comment"] = $comment;
                    array_push($starts, $value);
                }

                # replace category names with their corresponding id's
                $new_category = join(',',
                    array_map(
                        function($row) { return $row->id; },
                        $cats
                    )
                );
                $new_data = $new_data . "{{#annot:$comment|$new_category|$new_bookmark}}";
            }

1302
1303
1304
1305
1306
1307
        }
        # add the end of the text
        $new_data = $new_data . substr($data, $start_loc, strlen($data)-$start_loc);

        # replace the original wikiText
        $data = $new_data;
vermeul's avatar
...    
vermeul committed
1308
        return array($annotations, $page_annotations);
vermeul's avatar
vermeul committed
1309
1310
1311
    }


1312
1313
1314
1315
    //
    // Schema updates, called on Vagrant machine with: mwscript update.php
    // otherwise: cd maintenance; php update.php
    // creates the necessary tables for YATA.
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
    public static function onLoadExtensionSchemaUpdates( DatabaseUpdater $updater = null ) {

        $map = [ 'mysql', 'postgres' ];
        $type = $updater->getDB()->getType();

        if ( !in_array( $type, $map ) ) {
            throw new Exception( "YATA extension does not currently support $type database." );
        }
        $sql = __DIR__ . '/sql/yata.' . $type . '.sql';
        $updater->addExtensionTable( 'yata', $sql );

        return true;
 }
vermeul's avatar
vermeul committed
1329

1330
1331
    // called right after «Save Changes»
    // when an error is present, returns to edit page and prints the error
vermeul's avatar
vermeul committed
1332
    // may be used to check whether the syntax of the annotations is correct
1333
1334
1335
    public static function onEditFilter($editor, $text, $section, &$error, $summary) {
    }

vermeul's avatar
...    
vermeul committed
1336
    // this is called after �Save changes�
1337
    // and after onEditFilter
vermeul's avatar
vermeul committed
1338
    public static function onArticlePrepareTextForEdit($wikiPage, $popts) {
1339
1340
1341
1342
    }

    // first hook called when editing a page
    // even before any checks whether a user is allowed to
1343
    public static function onAlternateEdit($editPage) {
1344
1345
    }

1346
1347
    // This hook is called after hitting the «edit» button of a page.
    // It transforms all categories from their internal ID into readable text.
1348
1349
    // This allows us to change the category names later without having
    // to change all source code where this category occurs.
1350
    public static function onEditFormInitialText($editPage) {
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
        $data = $editPage->textbox1;
        $data =  preg_replace_callback(
            '/(?:{{#annot:(?P<params>.*?)}})/s',
            'self::replace_id_with_category',
            $data
        );
        // assign the new content
        $editPage->textbox1 = $data;
    }

    // preload edit forms with some content
    public static function onEditFormPreloadText(&$text, &$title) {
    }

    // callback function for onEditFormInitialText hook
    // all category_id's are replaced with the category names themselves
1367
    // using the hashtag (if available) or the parent_category/child_category naming scheme (fallback).
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
    private static function replace_id_with_category($matches) {
        $dbw = wfGetDB( DB_MASTER );
        list($comment, $cat, $bookmark) = preg_split('/\s*\|\s*/', $matches["params"]);
        
        if (!$cat) {
            return $matches[0];
        }
        # TODO: check whether category is numeric
        $categories = preg_split('/\s*,\s*/', $cat);
        $cat_and_parents = array();
        foreach($categories as $category) {
            $cat_and_parent = self::get_category_and_parent_for_id($dbw, $category);
            if ($cat_and_parent) {
1381
1382
1383
1384
                if ($cat_and_parent->hashtag) {
		    array_push($cat_and_parents, $cat_and_parent->hashtag);
                }
                elseif ($cat_and_parent->parent_name) {
vermeul's avatar
vermeul committed
1385
1386
1387
1388
1389
                    array_push($cat_and_parents, $cat_and_parent->parent_name .'/'. $cat_and_parent->name);
                }
                else {
                    array_push($cat_and_parents, $cat_and_parent->name);
                }
1390
1391
1392
1393
1394
1395
1396
1397
1398
            }
        }
        $annot = "{{#annot: " . $comment." | ";
        $annot.= join(', ', $cat_and_parents);
        $annot.= " | $bookmark}}";

        return $annot;
    }

vermeul's avatar
vermeul committed
1399
}