Passed
Push — master ( ed742f...014eda )
by Mark
03:52 queued 01:43
created

action.php (1 issue)

Labels
Severity
1
<?php
2
/**
3
 * DokuSIOC - SIOC plugin for DokuWiki
4
 *
5
 * version 0.1.2
6
 *
7
 * DokuSIOC integrates the SIOC ontology within DokuWiki and provides an
8
 * alternate RDF/XML views of the wiki documents.
9
 *
10
 * For DokuWiki we can't use the Triplify script because DokuWiki has not a RDBS
11
 * backend. But the wiki API provides enough methods to get the data out, so
12
 * DokuSIOC as a plugin uses the export hook to provide accessible data as
13
 * RDF/XML, using the SIOC ontology as vocabulary.
14
 *
15
 * METADATA
16
 *
17
 * @author    Michael Haschke @ eye48.com
18
 * @copyright 2009 Michael Haschke
19
 * @license   http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU General Public License 2.0 (GPLv2)
20
 * @version   0.1.2
21
 *
22
 * WEBSITES
23
 *
24
 * @link      http://eye48.com/go/dokusioc Plugin Website and Overview
25
 * @link      http://github.com/haschek/DokuWiki-Plugin-DokuSIOC/issues Issue tracker
26
 *
27
 * LICENCE
28
 *
29
 * This program is free software: you can redistribute it and/or modify it under
30
 * the terms of the GNU General Public License as published by the Free Software
31
 * Foundation, version 2 of the License.
32
 *
33
 * This program is distributed in the hope that it will be useful, but WITHOUT
34
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
35
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
36
 *
37
 * @link      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU General Public License 2.0 (GPLv2)
38
 *
39
 * CHANGELOG
40
 *
41
 * 0.1.2
42
 * - fix: meta link to post type is standard use now (issue 9)
43
 * - mod: titles for SIOC documents (issue 10)
44
 * - mod: use sioc:UserAccount instead of deprecated sioc:User (issue 2)
45
 * 0.1.1 (bugfix release)
46
 * - fix header output for content negotiation
47
 * - fix URIs for profile and SIOC ressource
48
 * - better dc:title for revisions
49
 * - add complete URI to rdf:about for foaf:Document (Profile) to make it explicit
50
 * - add rel="canonical" for URIs with type parameter, to prevent double content
51
 * 0.1
52
 * - exchange licence b/c CC-BY-SA was incompatible with GPL
53
 * - restructuring code base
54
 * - fix: wrong meta link for revisions
55
 * - add: possibility to send noindex by x-robots-tag via HTTP header
56
 * - add: soft check for requested application type
57
 * - mod: use search method to get container content on next sub level
58
 * - mod: better dc:title for foaf:document,
59
 * - mod: better distinction between user/container/post resources
60
 * - mod: normalize URIs
61
 * - fix: URIs for SIOC documents
62
 * - mod: use dcterms:created and sioc:has_creator only for first revision of wiki page b/c of inadequate meta data
63
 * - add: backlinks from wiki via dcterms:isReferencedBy
64
 * - add: contributors by sioc:has_modifier (only for last revision b/c of wrong meta data for older revisions)
65
 * - rem: foaf:person link in sioct:WikiArticle b/c it routes to same data like sioc:has_creater/modifier
66
 * - rem: Talis SIOC widget for comments b/c incompatibility with DokuWiki JS
67
 * poc
68
 * - proof of concept release under CC-BY-SA
69
 **/
70
71
class action_plugin_dokusioc extends DokuWiki_Action_Plugin {
72
73
    private $agentlink = 'http://eye48.com/go/dokusioc?v=0.1.2';
74
75
    /* -- Methods to manage plugin ------------------------------------------ */
76
77
    /**
78
     * Register its handlers with the DokuWiki's event controller
79
     */
80
    public function register(Doku_Event_Handler $controller): void {
81
        //print_r(headers_list()); die();
82
        // test the requested action
83
        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'checkAction', $controller);
84
    }
85
86
    /* -- Event handlers ---------------------------------------------------- */
87
88
    public function checkAction($action, $controller) {
89
        global $INFO;
90
        //print_r($INFO); die();
91
        //print_r(headers_list()); die();
92
93
        if($action->data === 'export_siocxml') {
94
            // give back rdf
95
            $this->exportSioc();
96
        } elseif(($action->data === 'show' || $action->data === 'index') && $INFO['perm']
97
            && !defined('DOKU_MEDIADETAIL')
98
            && ($INFO['exists'] || getDwUserInfo($INFO['id'], $this)) && !isHiddenPage($INFO['id'])) {
99
            if($this->isRdfXmlRequest()) {
100
                // forward to rdfxml document if requested
101
                // print_r(headers_list()); die();
102
                $location = $this->createRdfLink();
103
                if(function_exists('header_remove')) {
104
                    header_remove();
105
                }
106
                header('Location: ' . $location['href'], true, 303);
107
                exit();
108
            } else {
109
                // add meta link to html head
110
                $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'createRdfLink');
111
            }
112
        }
113
        /*
114
        else
115
        {
116
            print_r(array($action->data, $INFO['perm'], defined('DOKU_MEDIADETAIL'), $INFO['exists'],
117
                    getDwUserInfo($INFO['id'],$this), isHiddenPage($INFO['id'])));
118
            die();
119
        }
120
        */
121
    }
122
123
    public function exportSioc() {
124
        global $ID, $INFO, $conf, $REV, $auth;
125
126
        // Test for hidden pages
127
128
        if(isHiddenPage($ID)) {
129
            $this->exit("HTTP/1.0 404 Not Found");
130
        }
131
132
        // Get type of SIOC content
133
134
        $sioc_type = $this->getContenttype();
135
136
        // Test for valid types
137
138
        if(!(($sioc_type == 'post' && $INFO['exists']) || $sioc_type == 'user' || $sioc_type == 'container')) {
139
            $this->exit("HTTP/1.0 404 Not Found");
140
        }
141
142
        // Test for permission
143
144
        if(!$INFO['perm']) {
145
            // not enough rights to see the wiki page
146
            $this->exit("HTTP/1.0 401 Unauthorized");
147
        }
148
149
        // Forward to URI with explicit type attribut
150
        if(!isset($_GET['type'])) {
151
            header('Location:' . $_SERVER['REQUEST_URI'] . '&type=' . $sioc_type, true, 302);
152
        }
153
154
        // Include SIOC libs
155
156
        require_once(__DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'sioc_inc.php');
157
        require_once(__DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'sioc_dokuwiki.php');
158
159
        // Create exporter
160
161
        $rdf              = new SIOCExporter();
162
        $rdf->profile_url = normalizeUri(
163
            getAbsUrl(
164
                exportlink(
0 ignored issues
show
The function exportlink was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

164
                /** @scrutinizer ignore-call */ 
165
                exportlink(
Loading history...
165
                    $ID, 'siocxml',
166
                    array('type' => $sioc_type), false, '&'
167
                )
168
            )
169
        );
170
        $rdf->setURLParameters('type', 'id', 'page', false);
171
172
        // Create SIOC-RDF content
173
174
        switch($sioc_type) {
175
            case 'container':
176
                $rdf = $this->exportContainercontent($rdf);
177
                break;
178
179
            case 'user':
180
                $rdf = $this->exportUsercontent($rdf);
181
                break;
182
183
            case 'post':
184
            default:
185
                $rdf = $this->exportPostcontent($rdf);
186
                break;
187
        }
188
189
        // export
190
        if($this->getConf('noindx')) {
191
            header("X-Robots-Tag: noindex", true);
192
        }
193
        $rdf->export();
194
195
        //print_r(headers_list()); die();
196
        die();
197
    }
198
199
    private function exit($headermsg) {
200
        header($headermsg);
201
        die();
202
    }
203
204
    /* -- public class methods ---------------------------------------------- */
205
206
    private function getContenttype() {
207
        global $ID, $conf;
208
209
        // check for type if unknown
210
        if(!($_GET['type'] ?? "")) {
211
            $userinfo = getDwUserInfo($ID, $this);
212
213
            if($userinfo) {
214
                $type = 'user';
215
            } elseif(isset($_GET['do']) && $_GET['do'] == 'index') {
216
                $type = 'container';
217
            } else {
218
                $type = 'post';
219
            }
220
221
        } else {
222
            $type = $_GET['type'];
223
        }
224
225
        return $type;
226
227
    }
228
229
    private function exportContainercontent($exporter) {
230
        global $ID, $INFO, $conf;
231
232
        if($ID == $conf['start']) {
233
            $title = $conf['title'];
234
        } elseif(isset($INFO['meta']['title'])) {
235
            $title = $INFO['meta']['title'];
236
        } else {
237
            $title = $ID;
238
        }
239
240
        $exporter->setParameters(
241
            'Container: ' . $title,
242
            getAbsUrl(),
243
            getAbsUrl() . 'doku.php?',
244
            'utf-8',
245
            $this->agentlink
246
        );
247
248
        // create container object
249
        $wikicontainer = new SIOCDokuWikiContainer(
250
            $ID,
251
            normalizeUri($exporter->siocURL('container', $ID))
252
        );
253
254
        /* container is type=wiki */
255
        if($ID == $conf['start']) {
256
            $wikicontainer->isWiki();
257
        }
258
        /* sioc:name              */
259
        if($INFO['exists']) {
260
            $wikicontainer->addTitle($INFO['meta']['title']);
261
        }
262
        /* has_parent             */
263
        if($INFO['namespace']) {
264
            $wikicontainer->addParent($INFO['namespace']);
265
        }
266
267
        // search next level entries (posts, sub containers) in container
268
        require_once(DOKU_INC . 'inc/search.php');
269
        $dir        = utf8_encodeFN(str_replace(':', '/', $ID));
270
        $entries    = array();
271
        $posts      = array();
272
        $containers = array();
273
        search($entries, $conf['datadir'], 'search_index', array('ns' => $ID), $dir);
274
        foreach($entries as $entry) {
275
            if($entry['type'] === 'f') {
276
                // wikisite
277
                $posts[] = $entry;
278
            } elseif($entry['type'] === 'd') {
279
                // sub container
280
                $containers[] = $entry;
281
            }
282
        }
283
284
        // without sub content it can't be a container (so it does not exist as a container)
285
        if(count($posts) + count($containers) == 0) {
286
            $this->exit("HTTP/1.0 404 Not Found");
287
        }
288
289
        if(count($posts) > 0) {
290
            $wikicontainer->addArticles($posts);
291
        }
292
        if(count($containers) > 0) {
293
            $wikicontainer->addContainers($containers);
294
        }
295
296
        //print_r($containers);die();
297
298
        // add container to exporter
299
        $exporter->addObject($wikicontainer);
300
301
        return $exporter;
302
    }
303
304
    /* -- private helpers --------------------------------------------------- */
305
306
    private function exportUsercontent($exporter) {
307
        global $ID;
308
309
        // get user info
310
        $userinfo = getDwUserInfo($ID, $this);
311
312
        // no userinfo means there is no user space or user does not exists
313
        if($userinfo === false) {
314
            $this->exit("HTTP/1.0 404 Not Found");
315
        }
316
317
        $exporter->setParameters(
318
            'Account: ' . $userinfo['name'],
319
            getAbsUrl(),
320
            getAbsUrl() . 'doku.php?',
321
            'utf-8',
322
            $this->agentlink
323
        );
324
        // create user object
325
        //print_r($userinfo); die();
326
        // $id, $url, $userid, $name, $email
327
        $wikiuser = new SIOCDokuWikiUser(
328
            $ID,
329
            normalizeUri($exporter->siocURL('user', $ID)),
330
            $userinfo['user'],
331
            $userinfo['name'],
332
            $userinfo['mail']
333
        );
334
        /* TODO: avatar (using Gravatar) */
335
        /* TODO: creator_of */
336
        // add user to exporter
337
        $exporter->addObject($wikiuser);
338
339
        //print_r(headers_list());die();
340
        return $exporter;
341
    }
342
343
    private function exportPostcontent($exporter) {
344
        global $ID, $INFO, $REV, $conf;
345
346
        $exporter->setParameters(
347
            'Article: ' . $INFO['meta']['title'] . ($REV ? ' (rev ' . $REV . ')' : ''),
348
            $this->getDokuUrl(),
349
            $this->getDokuUrl() . 'doku.php?',
350
            'utf-8',
351
            $this->agentlink
352
        );
353
354
        // create user object
355
        // $id, $uri, $name, $email, $homepage='', $foaf_uri='', $role=false, $nick='', $sioc_url='', $foaf_url=''
356
        $dwuserpage_id = cleanID($this->getConf('userns')) . ($conf['useslash'] ? '/' : ':') . $INFO['editor'];
357
        /*
358
        if ($INFO['editor'] && $this->getConf('userns'))
359
            $pageuser = new SIOCUser($INFO['editor'],
360
                                        normalizeUri(getAbsUrl(exportlink($dwuserpage_id, 'siocxml',
361
                                            array('type'=>'user'), false, '&'))), // user page
362
                                        $INFO['meta']['contributor'][$INFO['editor']],
363
                                        getDwUserInfo($dwuserpage_id,$this,'mail'),
364
                                        '', // no homepage is saved for dokuwiki user
365
                                        '#'.$INFO['editor'], // local uri
366
                                        false, // no roles right now
367
                                        '', // no nick name is saved for dokuwiki user
368
                                        normalizeUri($exporter->siocURL('user', $dwuserpage_id))
369
                                    );
370
        */
371
372
        // create wiki page object
373
        $wikipage = new SIOCDokuWikiArticle(
374
            $ID, // id
375
            normalizeUri(
376
                $exporter->siocURL(
377
                    'post', $ID . ($REV ? $exporter->_urlseparator . 'rev'
378
                              . $exporter->_urlequal . $REV : '')
379
                )
380
            ), // url
381
            $INFO['meta']['title'] . ($REV ? ' (rev ' . $REV . ')' : ''), // subject
382
            rawWiki($ID, $REV) // body (content)
383
        );
384
        /* encoded content   */
385
        $wikipage->addContentEncoded(p_cached_output(wikiFN($ID, $REV), 'xhtml'));
386
        /* created           */
387
        if(isset($INFO['meta']['date']['created'])) {
388
            $wikipage->addCreated(date('c', $INFO['meta']['date']['created']));
389
        }
390
        /* or modified       */
391
        if(isset($INFO['meta']['date']['modified'])) {
392
            $wikipage->addModified(date('c', $INFO['meta']['date']['modified']));
393
        }
394
        /* creator/modifier  */
395
        if($INFO['editor'] && $this->getConf('userns')) {
396
            $wikipage->addCreator(array('foaf:maker' => '#' . $INFO['editor'], 'sioc:modifier' => $dwuserpage_id));
397
        }
398
        /* is creator        */
399
        if(isset($INFO['meta']['date']['created'])) {
400
            $wikipage->isCreator();
401
        }
402
        /* intern wiki links */
403
        $wikipage->addLinks($INFO['meta']['relation']['references']);
404
405
        // contributors - only for last revision b/c of wrong meta data for older revisions
406
        if(!$REV && $this->getConf('userns') && isset($INFO['meta']['contributor'])) {
407
            $cont_temp = array();
408
            $cont_ns   = $this->getConf('userns') . ($conf['useslash'] ? '/' : ':');
409
            foreach($INFO['meta']['contributor'] as $cont_id => $cont_name) {
410
                $cont_temp[$cont_ns . $cont_id] = $cont_name;
411
            }
412
            $wikipage->addContributors($cont_temp);
413
        }
414
415
        // backlinks - only for last revision
416
        if(!$REV) {
417
            require_once(DOKU_INC . 'inc/fulltext.php');
418
            $backlinks = ft_backlinks($ID);
419
            if(count($backlinks) > 0) {
420
                $wikipage->addBacklinks($backlinks);
421
            }
422
        }
423
424
        // TODO: addLinksExtern
425
426
        /* previous and next revision */
427
        $changelog = new PageChangeLog($ID);
428
        $pagerevs  = $changelog->getRevisions(0, $conf['recent'] + 1);
429
        $prevrev   = false;
430
        $nextrev   = false;
431
        if(!$REV) {
432
            // latest revision, previous rev is on top in array
433
            $prevrev = 0;
434
        } else {
435
            // other revision
436
            $currentrev = array_search($REV, $pagerevs);
437
            if($currentrev !== false) {
438
                $prevrev = $currentrev + 1;
439
                $nextrev = $currentrev - 1;
440
            }
441
        }
442
        if($prevrev !== false && $prevrev > -1 && page_exists($ID, $pagerevs[$prevrev])) {
443
            /* previous revision*/
444
            $wikipage->addVersionPrevious($pagerevs[$prevrev]);
445
        }
446
        if($nextrev !== false && $nextrev > -1 && page_exists($ID, $pagerevs[$nextrev])) {
447
            /* next revision*/
448
            $wikipage->addVersionNext($pagerevs[$nextrev]);
449
        }
450
451
        /* latest revision   */
452
        if($REV) {
453
            $wikipage->addVersionLatest();
454
        }
455
        // TODO: topics
456
        /* has_container     */
457
        if($INFO['namespace']) {
458
            $wikipage->addContainer($INFO['namespace']);
459
        }
460
        /* has_space         */
461
        if($this->getConf('owners')) {
462
            $wikipage->addSite($this->getConf('owners'));
463
        }
464
        // TODO: dc:contributor / has_modifier
465
        // TODO: attachment (e.g. pictures in that dwns)
466
467
        // add wiki page to exporter
468
        $exporter->addObject($wikipage);
469
        //if ($INFO['editor'] && $this->getConf('userns')) $exporter->addObject($pageuser);
470
471
        return $exporter;
472
473
    }
474
475
    private function getDokuUrl($url = null) {
476
        return getAbsUrl($url);
477
    }
478
479
    public function isRdfXmlRequest(): bool {
480
        // get accepted types
481
        $http_accept = trim($_SERVER['HTTP_ACCEPT']);
482
483
        // save accepted types in array
484
        $accepted = explode(',', $http_accept);
485
486
        /*
487
        $debuginfo = implode(' // ', array(date('c',$_SERVER['REQUEST_TIME']), $_SERVER['HTTP_REFERER'],
488
        $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_HOST'], $_SERVER['HTTP_USER_AGENT'], $_SERVER['HTTP_ACCEPT']));
489
        global $conf; //print_r($conf); die();
490
        //die($debuginfo);
491
        $debuglog = @fopen($conf['tmpdir'].DIRECTORY_SEPARATOR.'requests.log', 'ab');
492
        @fwrite($debuglog, $debuginfo."\n");
493
        @fclose($debuglog);
494
        @chmod($conf['tmpdir'].DIRECTORY_SEPARATOR.'requests.log', 0777);
495
        */
496
497
        // soft check, route to RDF when client requests it (don't check quality of request)
498
499
        if($this->getConf('softck') && strpos($_SERVER['HTTP_ACCEPT'], 'application/rdf+xml') !== false) {
500
            return true;
501
        }
502
503
        if(count($accepted) > 0) {
504
            // hard check, only serve RDF if it is requested first or equal to first type
505
506
            // extract accepting ratio
507
            $test_accept = array();
508
            foreach($accepted as $format) {
509
                $formatspec = explode(';', $format);
510
                $k          = trim($formatspec[0]);
511
                if(count($formatspec) === 2) {
512
                    $test_accept[$k] = trim($formatspec[1]);
513
                } else {
514
                    $test_accept[$k] = 'q=1.0';
515
                }
516
            }
517
518
            // sort by ratio
519
            arsort($test_accept);
520
            $accepted_order = array_keys($test_accept);
521
522
            if($accepted_order[0] === 'application/rdf+xml' ||
523
                (array_key_exists('application/rdf+xml', $test_accept)
524
                    && $test_accept['application/rdf+xml'] === 'q=1.0')
525
            ) {
526
                return true;
527
            }
528
        }
529
530
        // print_r($accepted_order);print_r($test_accept);die();
531
532
        return false;
533
534
    }
535
536
    /**
537
     */
538
    public function createRdfLink($event = null, $param = null) {
539
        global $ID, $INFO, $conf;
540
541
        // Test for hidden pages
542
543
        if(isHiddenPage($ID)) {
544
            return false;
545
        }
546
547
        // Get type of SIOC content
548
549
        $sioc_type = $this->getContenttype();
550
551
        // Test for valid types
552
553
        if(!(($sioc_type === 'post' && $INFO['exists']) || $sioc_type === 'user' || $sioc_type === 'container')) {
554
            return false;
555
        }
556
557
        // Test for permission
558
559
        if(!$INFO['perm']) {
560
            // not enough rights to see the wiki page
561
            return false;
562
        }
563
564
        $userinfo = getDwUserInfo($ID, $this);
565
566
        // Create attributes for meta link
567
568
        $metalink['type'] = 'application/rdf+xml';
569
        $metalink['rel']  = 'meta';
570
571
        switch($sioc_type) {
572
            case 'container':
573
                $title     = htmlentities(
574
                    "Container '" . ($INFO['meta']['title'] ?? $ID) . "' (SIOC document as RDF/XML)"
575
                );
576
                $queryAttr = array('type' => 'container');
577
                break;
578
579
            case 'user':
580
                $title     = htmlentities("User account '" . $userinfo['name'] . "' (SIOC document as RDF/XML)");
581
                $queryAttr = array('type' => 'user');
582
                break;
583
584
            case 'post':
585
            default:
586
                $title     = htmlentities("Article '" . $INFO['meta']['title'] . "' (SIOC document as RDF/XML)");
587
                $queryAttr = array('type' => 'post');
588
                if(isset($_GET['rev']) && $_GET['rev'] === (int) $_GET['rev']) {
589
                    $queryAttr['rev'] = $_GET['rev'];
590
                }
591
                break;
592
        }
593
594
        $metalink['title'] = $title;
595
        $metalink['href']  = normalizeUri(getAbsUrl(exportlink($ID, 'siocxml', $queryAttr, false, '&')));
596
597
        if($event !== null) {
598
            $event->data['link'][] = $metalink;
599
600
            // set canocial link for type URIs to prevent indexing double content
601
            if($_GET['type'] ?? "") {
602
                $event->data['link'][] = array('rel' => 'canonical', 'href' => getAbsUrl(wl($ID)));
603
            }
604
        }
605
606
        return $metalink;
607
    }
608
}
609
610
if(!function_exists('getAbsUrl')) {
611
    function getAbsUrl($url = null) {
612
        if($url === null) {
613
            $url = DOKU_BASE;
614
        }
615
        return str_replace(DOKU_BASE, DOKU_URL, $url);
616
    }
617
}
618
619
if(!function_exists('getDwUserEmail')) {
620
    function getDwUserEmail($user) {
621
        global $auth;
622
        if($info = $auth->getUserData($user)) {
623
            return $info['mail'];
624
        } else {
625
            return false;
626
        }
627
    }
628
}
629
630
if(!function_exists('getDwUserInfo')) {
631
    function getDwUserInfo($id, $pobj, $key = null) {
632
        global $auth, $conf;
633
634
        if(!$pobj->getConf('userns')) {
635
            return false;
636
        }
637
638
        // get user id
639
        $userid = str_replace(cleanID($pobj->getConf('userns')) . ($conf['useslash'] ? '/' : ':'), '', $id);
640
641
        if($info = $auth->getUserData($userid)) {
642
            if($key) {
643
                return $info['key'];
644
            } else {
645
                return $info;
646
            }
647
        } else {
648
            return false;
649
        }
650
    }
651
}
652
653
// sort query attributes by name
654
if(!function_exists('normalizeUri')) {
655
    function normalizeUri($uri) {
656
        // part URI
657
        $parts = explode('?', $uri);
658
659
        // part query
660
        if(isset($parts[1])) {
661
            $query = $parts[1];
662
663
            // test separator
664
            $sep = '&';
665
            if(strpos($query, '&amp;') !== false) {
666
                $sep = '&amp;';
667
            }
668
            $attr = explode($sep, $query);
669
670
            sort($attr);
671
672
            $parts[1] = implode($sep, $attr);
673
        }
674
675
        return implode('?', $parts);
676
    }
677
}
678
679