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