1<?php
2
3use dokuwiki\plugin\sqlite\Tools;
4
5class action_plugin_structpublish_migration extends DokuWiki_Action_Plugin
6{
7    const MIN_DB_STRUCT = 19;
8
9    /** @var string  */
10    protected $table = 'data_structpublish';
11
12    /**
13     * @inheritDoc
14     */
15    public function register(Doku_Event_Handler $controller)
16    {
17        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handleMigrations');
18    }
19
20    /**
21     * Call our custom migrations. We do not use our own database,
22     * so we cannot use the mechanism in sqlite init()
23     * which processes updateXXXX.sql files
24     *
25     * @param Doku_Event $event
26     * @return bool
27     * @throws Exception
28     */
29    public function handleMigrations(Doku_Event $event)
30    {
31        /** @var \helper_plugin_struct_db $helper */
32        $helper = plugin_load('helper', 'struct_db');
33
34        // abort if struct is not installed
35        if (!$helper) {
36            throw new Exception('Plugin struct is required!');
37        }
38
39        $sqlite = $helper->getDB();
40
41        list($dbVersionStruct, $dbVersionStructpublish) = $this->getDbVersions($sqlite);
42
43        // check if struct has required version
44        if ($dbVersionStruct < self::MIN_DB_STRUCT) {
45            throw new Exception(
46                'Plugin struct is outdated. Minimum required database version is ' . self::MIN_DB_STRUCT
47            );
48        }
49
50        // check whether we are already up-to-date
51        $latestVersion = $this->getLatestVersion();
52        if (isset($dbVersionStructpublish) && (int) $dbVersionStructpublish >= $latestVersion) {
53            return true;
54        }
55
56        // check whether we have any pending migrations
57        $pending = range(($dbVersionStructpublish ?: 0) + 1, $latestVersion);
58        if (empty($pending)) {
59            return true;
60        }
61
62        // execute the migrations
63        $ok = true;
64
65        foreach ($pending as $version) {
66            $call = 'migration' . $version;
67            $ok = $ok && $this->$call($sqlite);
68        }
69
70        // update migration status in struct database
71        if ($ok) {
72            $sql = "REPLACE INTO opts (val,opt) VALUES ($version,'dbversion_structpublish')";
73            $ok = $ok && $sqlite->query($sql);
74        }
75
76        return $ok;
77    }
78
79    /**
80     * Read the current versions for struct and struct publish from the database
81     *
82     * @param \dokuwiki\plugin\sqlite\SQLiteDB $sqlite
83     * @return array [structversion, structpublishversion]
84     */
85    protected function getDbVersions($sqlite)
86    {
87        $dbVersionStruct = null;
88        $dbVersionStructpublish = null;
89
90        $sql = 'SELECT opt, val FROM opts WHERE opt=? OR opt=?';
91        $vals = $sqlite->queryAll($sql, ['dbversion', 'dbversion_structpublish']);
92
93        foreach ($vals as $val) {
94            if ($val['opt'] === 'dbversion') {
95                $dbVersionStruct = $val['val'];
96            }
97            if ($val['opt'] === 'dbversion_structpublish') {
98                $dbVersionStructpublish = $val['val'];
99            }
100        }
101        return [$dbVersionStruct, $dbVersionStructpublish];
102    }
103
104    /**
105     * @return int
106     */
107    protected function getLatestVersion()
108    {
109        return (int) trim(file_get_contents(DOKU_PLUGIN . 'structpublish/db/latest.version', false));
110    }
111
112    /**
113     * Database setup
114     *
115     * @param \dokuwiki\plugin\sqlite\SQLiteDB $sqlite
116     * @return bool
117     */
118    protected function migration1($sqlite)
119    {
120        $file = DOKU_PLUGIN . 'structpublish/db/json/structpublish0001.struct.json';
121        $schemaJson = file_get_contents($file);
122        $importer = new \dokuwiki\plugin\struct\meta\SchemaImporter('structpublish', $schemaJson);
123        $ok = (bool) $importer->build();
124
125        if ($ok) {
126            $sql = io_readFile(DOKU_PLUGIN . 'structpublish/db/update0001.sql', false);
127            $sqlArr = Tools::SQLstring2array($sql);
128            foreach ($sqlArr as $sql) {
129                $ok = $ok && $sqlite->query($sql);
130            }
131        }
132
133        return $ok;
134    }
135
136    /**
137     * Reset 'latest' flag to 0 for all rows except actually latest ones
138     * for each pid / status combination.
139     *
140     * @param \dokuwiki\plugin\sqlite\SQLiteDB $sqlite
141     * @return bool
142     */
143    protected function migration2($sqlite)
144    {
145        $sql = "SELECT rid, pid, latest, col1, max(col4) FROM $this->table GROUP BY pid, col1";
146        $latest = $sqlite->queryAll($sql);
147        $rids = array_column($latest, 'rid');
148
149        $sql = "UPDATE $this->table SET latest = 0 WHERE rid NOT IN (" . implode(', ', $rids) . ')';
150
151        return (bool) $sqlite->query($sql);
152    }
153
154    /**
155     * Set 'latest' flag to 0 for all rows except actually latest ones
156     * for each page,
157     *
158     * @param \dokuwiki\plugin\sqlite\SQLiteDB $sqlite
159     * @return bool
160     */
161    protected function migration3($sqlite)
162    {
163        $sql = "WITH cte AS (
164            SELECT rid, pid, col1 AS status, col4 as rev,
165                   rank() OVER ( PARTITION BY pid
166                       ORDER BY col4 DESC, col1 = 'draft', col1 = 'approved', col1 = 'published'
167                   ) AS r
168            FROM data_structpublish
169        )
170        SELECT rid, pid, status, rev
171        FROM cte
172        WHERE r  = 1
173        ORDER BY pid ASC;";
174
175        $latest = $sqlite->queryAll($sql);
176        $rids = array_column($latest, 'rid');
177
178        $sql = "UPDATE $this->table SET latest = 0 WHERE rid NOT IN (" . implode(', ', $rids) . ')';
179
180        return (bool) $sqlite->query($sql);
181    }
182}
183