1<?php
2
3/**
4 * DokuWiki Plugin struct (Helper Component)
5 *
6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
7 * @author  Andreas Gohr, Michael Große <dokuwiki@cosmocode.de>
8 */
9
10use dokuwiki\Extension\Plugin;
11use dokuwiki\plugin\struct\meta\AccessDataValidator;
12use dokuwiki\plugin\struct\meta\AccessTable;
13use dokuwiki\plugin\struct\meta\Assignments;
14use dokuwiki\plugin\struct\meta\Schema;
15use dokuwiki\plugin\struct\meta\StructException;
16
17/**
18 * The public interface for the struct plugin
19 *
20 * 3rd party developers should always interact with struct data through this
21 * helper plugin only. If additional interface functionality is needed,
22 * it should be added here.
23 *
24 * All functions will throw StructExceptions when something goes wrong.
25 *
26 * Remember to check permissions yourself!
27 */
28class helper_plugin_struct extends Plugin
29{
30    /**
31     * Class names of renderers which should NOT render struct data.
32     * All descendants are also blacklisted.
33     */
34    public const BLACKLIST_RENDERER = [
35        'Doku_Renderer_metadata',
36        '\renderer_plugin_qc'
37    ];
38
39    /**
40     * Get the structured data of a given page
41     *
42     * @param string $page The page to get data for
43     * @param string|null $schema The schema to use null for all
44     * @param int $time A timestamp if you want historic data
45     * @return array ('schema' => ( 'fieldlabel' => 'value', ...))
46     * @throws StructException
47     */
48    public function getData($page, $schema = null, $time = 0)
49    {
50        $page = cleanID($page);
51        if (!$time) {
52            $time = time();
53        }
54
55        if (is_null($schema)) {
56            $assignments = Assignments::getInstance();
57            $schemas = $assignments->getPageAssignments($page, false);
58        } else {
59            $schemas = [$schema];
60        }
61
62        $result = [];
63        foreach ($schemas as $schema) {
64            $schemaData = AccessTable::getPageAccess($schema, $page, $time);
65            $result[$schema] = $schemaData->getDataArray();
66        }
67
68        return $result;
69    }
70
71    /**
72     * Saves data for a given page (creates a new revision)
73     *
74     * If this call succeeds you can assume your data has either been saved or it was
75     * not necessary to save it because the data already existed in the wanted form or
76     * the given schemas are no longer assigned to that page.
77     *
78     * Important: You have to check write permissions for the given page before calling
79     * this function yourself!
80     *
81     * this duplicates a bit of code from entry.php - we could also fake post data and let
82     * entry handle it, but that would be rather unclean and might be problematic when multiple
83     * calls are done within the same request.
84     *
85     * @param string $page
86     * @param array $data ('schema' => ( 'fieldlabel' => 'value', ...))
87     * @param string $summary
88     * @param string $summary
89     * @throws StructException
90     * @todo should this try to lock the page?
91     *
92     *
93     */
94    public function saveData($page, $data, $summary = '', $minor = false)
95    {
96        $page = cleanID($page);
97        $summary = trim($summary);
98        if (!$summary) $summary = $this->getLang('summary');
99
100        if (!page_exists($page)) throw new StructException("Page does not exist. You can not attach struct data");
101
102        // validate and see if anything changes
103        $valid = AccessDataValidator::validateDataForPage($data, $page, $errors);
104        if ($valid === false) {
105            throw new StructException("Validation failed:\n%s", implode("\n", $errors));
106        }
107        if (!$valid) return; // empty array when no changes were detected
108
109        $newrevision = self::createPageRevision($page, $summary, $minor);
110
111        // save the provided data
112        $assignments = Assignments::getInstance();
113        foreach ($valid as $v) {
114            $v->saveData($newrevision);
115            // make sure this schema is assigned
116            $assignments->assignPageSchema($page, $v->getAccessTable()->getSchema()->getTable());
117        }
118    }
119
120    /**
121     * Save lookup data row
122     *
123     * @param AccessTable $access the table into which to save the data
124     * @param array $data data to be saved in the form of [columnName => 'data']
125     */
126    public function saveLookupData(AccessTable $access, $data)
127    {
128        if (!$access->getSchema()->isEditable()) {
129            throw new StructException('lookup save error: no permission for schema');
130        }
131        $validator = $access->getValidator($data);
132        if (!$validator->validate()) {
133            throw new StructException("Validation failed:\n%s", implode("\n", $validator->getErrors()));
134        }
135        if (!$validator->saveData()) {
136            throw new StructException('No data saved');
137        }
138    }
139
140    /**
141     * Creates a new page revision with the same page content as before
142     *
143     * @param string $page
144     * @param string $summary
145     * @param bool $minor
146     * @return int the new revision
147     */
148    public static function createPageRevision($page, $summary = '', $minor = false)
149    {
150        $summary = trim($summary);
151        // force a new page revision @see action_plugin_struct_entry::handle_pagesave_before()
152        $GLOBALS['struct_plugin_force_page_save'] = true;
153        saveWikiText($page, rawWiki($page), $summary, $minor);
154        unset($GLOBALS['struct_plugin_force_page_save']);
155        $file = wikiFN($page);
156        clearstatcache(false, $file);
157        return filemtime($file);
158    }
159
160    /**
161     * Get info about existing schemas
162     *
163     * @param string|null $schema the schema to query, null for all
164     * @return Schema[]
165     * @throws StructException
166     */
167    public static function getSchema($schema = null)
168    {
169        if (is_null($schema)) {
170            $schemas = Schema::getAll();
171        } else {
172            $schemas = [$schema];
173        }
174
175        $result = [];
176        foreach ($schemas as $table) {
177            $result[$table] = new Schema($table);
178        }
179        return $result;
180    }
181
182    /**
183     * Returns all pages known to the struct plugin
184     *
185     * That means all pages that have or had once struct data saved
186     *
187     * @param string|null $schema limit the result to a given schema
188     * @return array (page => (schema => true), ...)
189     * @throws StructException
190     */
191    public function getPages($schema = null)
192    {
193        $assignments = Assignments::getInstance();
194        return $assignments->getPages($schema);
195    }
196
197    /**
198     * Returns decoded JSON value or throws exception
199     * when passed parameter is not JSON
200     *
201     * @param string $value
202     * @return mixed
203     * @throws StructException
204     * @throws JsonException
205     */
206    public static function decodeJson($value)
207    {
208        if (empty($value)) return $value;
209        if ($value[0] !== '[') throw new StructException('Lookup expects JSON');
210        return json_decode($value, null, 512, JSON_THROW_ON_ERROR);
211    }
212}
213