1<?php 2/** 3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4 * 5 * This source code is licensed under the GPL license found in the 6 * COPYING file in the root directory of this source tree. 7 * 8 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9 * @author ComboStrap <support@combostrap.com> 10 * 11 */ 12if (!defined('DOKU_INC')) die(); 13 14use ComboStrap\Analytics; 15use ComboStrap\Page; 16use ComboStrap\PluginUtility; 17use ComboStrap\Sqlite; 18use splitbrain\phpcli\Options; 19 20require_once(__DIR__ . '/class/Analytics.php'); 21 22/** 23 * The memory of the server 128 is not enough 24 */ 25ini_set('memory_limit', '256M'); 26 27/** 28 * Class cli_plugin_combo 29 * 30 * This is a cli: 31 * https://www.dokuwiki.org/devel:cli_plugins#example 32 * 33 * Usage: 34 * 35 * ``` 36 * docker exec -ti $(CONTAINER) /bin/bash 37 * ./bin/plugin.php combo -c 38 * ``` 39 * or via the IDE 40 * 41 * 42 * Example: 43 * https://www.dokuwiki.org/tips:grapher 44 * 45 */ 46class cli_plugin_combo extends DokuWiki_CLI_Plugin 47{ 48 const ANALYTICS = "analytics"; 49 const SYNC = "sync"; 50 51 /** 52 * register options and arguments 53 * @param Options $options 54 */ 55 protected function setup(Options $options) 56 { 57 $options->setHelp( 58 "Manage the analytics database\n\n" . 59 "analytics\n" . 60 "sync" 61 ); 62 $options->registerOption('version', 'print version', 'v'); 63 $options->registerCommand(self::ANALYTICS, "Update the analytics data"); 64 $options->registerOption( 65 'namespaces', 66 "If no namespace is given, the root namespace is assumed.", 67 'n', 68 true 69 ); 70 $options->registerOption( 71 'output', 72 "Optional, where to store the analytical data as csv eg. a filename.", 73 'o', 'file'); 74 $options->registerOption( 75 'cache', 76 "Optional, returns from the cache if set", 77 'c', false); 78 $options->registerOption( 79 'dry', 80 "Optional, dry-run", 81 'd', false); 82 $options->registerCommand(self::SYNC, "Sync the database"); 83 84 } 85 86 /** 87 * The main entry 88 * @param Options $options 89 */ 90 protected function main(Options $options) 91 { 92 93 $namespaces = array_map('cleanID', $options->getArgs()); 94 if (!count($namespaces)) $namespaces = array(''); //import from top 95 96 $cache = $options->getOpt('cache', false); 97 $depth = $options->getOpt('depth', 0); 98 $cmd = $options->getCmd(); 99 if ($cmd == "") { 100 $cmd = self::ANALYTICS; 101 } 102 switch ($cmd) { 103 case self::ANALYTICS: 104 $output = $options->getOpt('output', ''); 105 //if ($output == '-') $output = 'php://stdout'; 106 $this->updateAnalyticsData($namespaces, $output, $cache, $depth); 107 break; 108 case self::SYNC: 109 $this->syncPages(); 110 break; 111 default: 112 throw new \RuntimeException("Combo: Command unknown (" . $cmd . ")"); 113 } 114 115 116 } 117 118 /** 119 * @param array $namespaces 120 * @param $output 121 * @param bool $cache 122 * @param int $depth recursion depth. 0 for unlimited 123 */ 124 private function updateAnalyticsData($namespaces = array(), $output = null, $cache = false, $depth = 0) 125 { 126 127 $fileHandle = null; 128 if (!empty($output)) { 129 $fileHandle = @fopen($output, 'w'); 130 if (!$fileHandle) $this->fatal("Failed to open $output"); 131 } 132 133 $pages = $this->findPages($namespaces, $depth); 134 135 136 if (!empty($fileHandle)) { 137 $header = array( 138 'id', 139 'backlinks', 140 'broken_links', 141 'changes', 142 'chars', 143 'external_links', 144 'external_medias', 145 'h1', 146 'h2', 147 'h3', 148 'h4', 149 'h5', 150 'internal_links', 151 'internal_medias', 152 'words', 153 'score' 154 ); 155 fwrite($fileHandle, implode(",", $header) . PHP_EOL); 156 } 157 $pageCounter = 0; 158 while ($page = array_shift($pages)) { 159 $id = $page['id']; 160 161 $pageCounter++; 162 echo "Processing the page {$id} ($pageCounter)\n"; 163 164 $data = Analytics::processAndGetDataAsArray($id, $cache); 165 if (!empty($fileHandle)) { 166 $statistics = $data[Analytics::STATISTICS]; 167 $row = array( 168 'id' => $id, 169 'backlinks' => $statistics[Analytics::INTERNAL_BACKLINKS_COUNT], 170 'broken_links' => $statistics[Analytics::INTERNAL_LINKS_BROKEN_COUNT], 171 'changes' => $statistics[Analytics::EDITS_COUNT], 172 'chars' => $statistics[Analytics::CHARS_COUNT], 173 'external_links' => $statistics[Analytics::EXTERNAL_LINKS_COUNT], 174 'external_medias' => $statistics[Analytics::EXTERNAL_MEDIAS], 175 Analytics::H1 => $statistics[Analytics::HEADERS_COUNT][Analytics::H1], 176 'h2' => $statistics[Analytics::HEADERS_COUNT]['h2'], 177 'h3' => $statistics[Analytics::HEADERS_COUNT]['h3'], 178 'h4' => $statistics[Analytics::HEADERS_COUNT]['h4'], 179 'h5' => $statistics[Analytics::HEADERS_COUNT]['h5'], 180 'internal_links' => $statistics[Analytics::INTERNAL_LINKS_COUNT], 181 'internal_medias' => $statistics[Analytics::INTERNAL_MEDIAS_COUNT], 182 'words' => $statistics[Analytics::WORDS_COUNT], 183 'low' => $data[Analytics::QUALITY]['low'] 184 ); 185 fwrite($fileHandle, implode(",", $row) . PHP_EOL); 186 } 187 } 188 if (!empty($fileHandle)) { 189 fclose($fileHandle); 190 } 191 192 } 193 194 /** 195 * Find the pages in the tree 196 * @param $namespaces 197 * @param $depth 198 * @return array 199 */ 200 private function findPages($namespaces = array(), $depth = 0) 201 { 202 // Run as admin to overcome the fact that 203 // anonymous user cannot set all links and backlinnks 204 205 206 global $conf; 207 $datadir = $conf['datadir']; 208 209 /** 210 * Run as admin to overcome the fact that 211 * anonymous user cannot see all links and backlinnks 212 */ 213 global $USERINFO; 214 $USERINFO['grps'] = array('admin'); 215 global $INPUT; 216 $INPUT->server->set('REMOTE_USER', "cli"); 217 218 $pages = array(); 219 foreach ($namespaces as $ns) { 220 221 search( 222 $pages, 223 $datadir, 224 'search_universal', 225 array( 226 'depth' => $depth, 227 'listfiles' => true, 228 'listdirs' => false, 229 'pagesonly' => true, 230 'skipacl' => true, 231 'firsthead' => false, 232 'meta' => false, 233 ), 234 str_replace(':', '/', $ns) 235 ); 236 237 // add the ns start page 238 if ($ns && page_exists($ns)) { 239 $pages[] = array( 240 'id' => $ns, 241 'ns' => getNS($ns), 242 'title' => p_get_first_heading($ns, false), 243 'size' => filesize(wikiFN($ns)), 244 'mtime' => filemtime(wikiFN($ns)), 245 'perm' => 16, 246 'type' => 'f', 247 'level' => 0, 248 'open' => 1, 249 ); 250 } 251 252 } 253 return $pages; 254 } 255 256 private function syncPages() 257 { 258 $sqlite = Sqlite::getSqlite(); 259 $res = $sqlite->query("select ID from pages"); 260 if (!$res) { 261 throw new \RuntimeException("An exception has occurred with the alias selection query"); 262 } 263 $res2arr = $sqlite->res2arr($res); 264 $sqlite->res_close($res); 265 foreach ($res2arr as $row) { 266 $id = $row['ID']; 267 if (!page_exists($id)) { 268 echo 'Page does not exist on the file system. Deleted from the database (' . $id . ")\n"; 269 Page::createPagePathFromPath($id)->deleteInDb(); 270 } 271 } 272 273 274 } 275} 276