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 switch ($options->getCmd()) { 99 case self::ANALYTICS: 100 $output = $options->getOpt('output', ''); 101 //if ($output == '-') $output = 'php://stdout'; 102 $this->updateAnalyticsData($namespaces, $output, $cache, $depth); 103 break; 104 case self::SYNC: 105 $this->syncPages(); 106 break; 107 default: 108 throw new \RuntimeException("Command unknown (" . $options->getCmd() . ")"); 109 } 110 111 112 } 113 114 /** 115 * @param array $namespaces 116 * @param $output 117 * @param bool $cache 118 * @param int $depth recursion depth. 0 for unlimited 119 */ 120 private function updateAnalyticsData($namespaces = array(), $output = null, $cache = false, $depth = 0) 121 { 122 123 $fileHandle = null; 124 if (!empty($output)) { 125 $fileHandle = @fopen($output, 'w'); 126 if (!$fileHandle) $this->fatal("Failed to open $output"); 127 } 128 129 $pages = $this->findPages($namespaces, $depth); 130 131 132 if (!empty($fileHandle)) { 133 $header = array( 134 'id', 135 'backlinks', 136 'broken_links', 137 'changes', 138 'chars', 139 'external_links', 140 'external_medias', 141 'h1', 142 'h2', 143 'h3', 144 'h4', 145 'h5', 146 'internal_links', 147 'internal_medias', 148 'words', 149 'score' 150 ); 151 fwrite($fileHandle, implode(",", $header) . PHP_EOL); 152 } 153 $pageCounter = 0; 154 while ($page = array_shift($pages)) { 155 $id = $page['id']; 156 157 $pageCounter++; 158 echo "Processing the page {$id} ($pageCounter)\n"; 159 160 $data = Analytics::processAndGetDataAsArray($id, $cache); 161 if (!empty($fileHandle)) { 162 $statistics = $data[Analytics::STATISTICS]; 163 $row = array( 164 'id' => $id, 165 'backlinks' => $statistics[Analytics::INTERNAL_BACKLINKS_COUNT], 166 'broken_links' => $statistics[Analytics::INTERNAL_LINKS_BROKEN_COUNT], 167 'changes' => $statistics[Analytics::EDITS_COUNT], 168 'chars' => $statistics[Analytics::CHARS_COUNT], 169 'external_links' => $statistics[Analytics::EXTERNAL_LINKS_COUNT], 170 'external_medias' => $statistics[Analytics::EXTERNAL_MEDIAS], 171 'h1' => $statistics[Analytics::HEADERS_COUNT]['h1'], 172 'h2' => $statistics[Analytics::HEADERS_COUNT]['h2'], 173 'h3' => $statistics[Analytics::HEADERS_COUNT]['h3'], 174 'h4' => $statistics[Analytics::HEADERS_COUNT]['h4'], 175 'h5' => $statistics[Analytics::HEADERS_COUNT]['h5'], 176 'internal_links' => $statistics[Analytics::INTERNAL_LINKS_COUNT], 177 'internal_medias' => $statistics[Analytics::INTERNAL_MEDIAS_COUNT], 178 'words' => $statistics[Analytics::WORDS_COUNT], 179 'low' => $data[Analytics::QUALITY]['low'] 180 ); 181 fwrite($fileHandle, implode(",", $row) . PHP_EOL); 182 } 183 } 184 if (!empty($fileHandle)) { 185 fclose($fileHandle); 186 } 187 188 } 189 190 /** 191 * Find the pages in the tree 192 * @param $namespaces 193 * @param $depth 194 * @return array 195 */ 196 private function findPages($namespaces = array(), $depth = 0) 197 { 198 // Run as admin to overcome the fact that 199 // anonymous user cannot set all links and backlinnks 200 201 202 global $conf; 203 $datadir = $conf['datadir']; 204 205 /** 206 * Run as admin to overcome the fact that 207 * anonymous user cannot see all links and backlinnks 208 */ 209 global $USERINFO; 210 $USERINFO['grps'] = array('admin'); 211 global $INPUT; 212 $INPUT->server->set('REMOTE_USER', "cli"); 213 214 $pages = array(); 215 foreach ($namespaces as $ns) { 216 217 search( 218 $pages, 219 $datadir, 220 'search_universal', 221 array( 222 'depth' => $depth, 223 'listfiles' => true, 224 'listdirs' => false, 225 'pagesonly' => true, 226 'skipacl' => true, 227 'firsthead' => false, 228 'meta' => false, 229 ), 230 str_replace(':', '/', $ns) 231 ); 232 233 // add the ns start page 234 if ($ns && page_exists($ns)) { 235 $pages[] = array( 236 'id' => $ns, 237 'ns' => getNS($ns), 238 'title' => p_get_first_heading($ns, false), 239 'size' => filesize(wikiFN($ns)), 240 'mtime' => filemtime(wikiFN($ns)), 241 'perm' => 16, 242 'type' => 'f', 243 'level' => 0, 244 'open' => 1, 245 ); 246 } 247 248 } 249 return $pages; 250 } 251 252 private function syncPages() 253 { 254 $sqlite = Sqlite::getSqlite(); 255 $res = $sqlite->query("select ID from pages"); 256 if (!$res) { 257 throw new \RuntimeException("An exception has occurred with the alias selection query"); 258 } 259 $res2arr = $sqlite->res2arr($res); 260 $sqlite->res_close($res); 261 foreach ($res2arr as $row) { 262 $id = $row['ID']; 263 if (!page_exists($id)) { 264 echo 'Page does not exist on the file system. Deleted from the database (' . $id . ")\n"; 265 Page::createFromId($id)->deleteInDb(); 266 } 267 } 268 269 270 } 271} 272