1<?php 2/** 3 * Redproject Syntax Plugin: Display Roadmap and other things 4 * 5 * @author Algorys 6 */ 7 8if (!defined('DOKU_INC')) die(); 9require 'vendor/php-redmine-api/lib/autoload.php'; 10 11 12class syntax_plugin_redproject extends DokuWiki_Syntax_Plugin { 13 const RI_IMPERSONATE = 4; 14 15 public function getType() { 16 return 'container'; 17 } 18 19 /** 20 * @return string Paragraph type 21 */ 22 public function getPType() { 23 return 'normal'; 24 } 25 // Keep syntax inside plugin 26 function getAllowedTypes() { 27 return array('container', 'baseonly', 'substition','protected','disabled','formatting','paragraphs'); 28 } 29 30 public function getSort() { 31 return 198; 32 } 33 34 function connectTo($mode) { 35 $this->Lexer->addSpecialPattern('<redproject[^>]*/>', $mode,'plugin_redproject'); 36 $this->Lexer->addEntryPattern('<redproject[^>]*>(?=.*</redproject>)', $mode,'plugin_redproject'); 37 } 38 function postConnect() { 39 $this->Lexer->addExitPattern('</redproject>', 'plugin_redproject'); 40 } 41 42 function getServerFromJson($server) { 43 $json_file = file_get_contents(__DIR__.'/server.json'); 44 $json_data = json_decode($json_file, true); 45 if(isset($json_data[$server])) { 46 return $json_data[$server]; 47 } else { 48 return null; 49 } 50 } 51 52 function getPercent($opIssue, $totalIssue) { 53 $p = $opIssue / $totalIssue; 54 $progress = $p * 100; 55 return round($progress, 1); 56 } 57 58 // Do the regexp 59 function handle($match, $state, $pos, Doku_Handler $handler) { 60 switch($state){ 61 case DOKU_LEXER_SPECIAL : 62 case DOKU_LEXER_ENTER : 63 $data = array( 64 'state'=>$state, 65 'proj'=> '', 66 ); 67 preg_match("/server *= *(['\"])(.*?)\\1/", $match, $server); 68 if (count($server) != 0) { 69 $server_data = $this->getServerFromJson($server[2]); 70 if( ! is_null($server_data)){ 71 $data['server_url'] = $server_data['url']; 72 $data['server_token'] = $server_data['api_token']; 73 } 74 } 75 if (!isset($data['server_token'])) { 76 $data['server_token'] = $this->getConf('redproject.API'); 77 } 78 if (!isset($data['server_url'])) { 79 $data['server_url'] = $this->getConf('redproject.url'); 80 } 81 82 // Looking for id 83 preg_match("/proj *= *(['\"])(.*?)\\1/", $match, $proj); 84 if( count($proj) != 0 ) { 85 $data['proj'] = $proj[2]; 86 } else { 87 return array( 88 'state'=>$state, 89 'error'=>true, 90 'text'=>'##ERROR <redproject>: project name required##' 91 ); 92 } 93 94 return $data; 95 case DOKU_LEXER_UNMATCHED : 96 return array('state'=>$state, 'text'=>$match); 97 default: 98 return array('state'=>$state, 'bytepos_end' => $pos + strlen($match)); 99 } 100 } 101 102 // Main render_link 103 function _render_project($renderer, $data) { 104 $client = new Redmine\Client($data['server_url'], $data['server_token']); 105 // Get Id user of the Wiki if Impersonate 106 $view = $this->getConf('redproject.view'); 107 if ($view == self::RI_IMPERSONATE) { 108 $redUser = $_SERVER['REMOTE_USER']; 109 // Attempt to collect information with this user 110 $client->setImpersonateUser($redUser); 111 } 112 // Get Project Info 113 $proj = $client->api('project')->show($data['proj']); 114 if($proj){ 115 $projId = $proj['project']['id']; 116 $projIdent = $proj['project']['identifier']; 117 $projName = $proj['project']['name']; 118 $projParent = $proj['project']['parent']; 119 if ( ! empty($projParent)) { 120 $nameParent = $projParent['name']; 121 $parentId = $client->api('project')->getIdByName($nameParent); 122 $parent = $client->api('project')->show($parentId); 123 $parentIdent = $parent['project']['identifier']; 124 } 125 $projHome = $proj['project']['homepage']; 126 $projDesc = $proj['project']['description']; 127 // RENDERER PROJECT INFO 128 // Title 129 $renderer->doc .= '<h2 class="title">'.$this->getLang('title').'</h2>'; 130 if($projHome) { 131 $renderer->doc .= '<div class="title">'; 132 $renderer->doc .= '<a href="'.$projHome.'"><div class="circle">HOME</div></a>'; 133 $renderer->doc .= '<div class="title-droite">'; 134 $renderer->doc .= '<span class="info-title">'.$projName.'</span>'; 135 $renderer->doc .= '<div class="see-it">'; 136 $renderer->doc .= '<a href="'.$data['server_url'].'/projects/'.$projIdent.'">See it in redmine</a>'; 137 $renderer->doc .= '</div>';// /.see-it 138 $renderer->doc .= '</div>'; // /.title-droite 139 $renderer->doc .= '</div>'; // /.title 140 } else { 141 $renderer->doc .= '<div class="title">'; 142 $renderer->doc .= '<a href="'.$projHome.'" title="Add Homepage"><div class="circle">+</div></a>'; 143 $renderer->doc .= '<div class="title-droite">'; 144 $renderer->doc .= '<span class="info-title">'.$projName.'</span>'; 145 $renderer->doc .= '<div class="see-it">'; 146 $renderer->doc .= '<a href="'.$data['server_url'].'/projects/'.$projIdent.'">See it in redmine</a>'; 147 $renderer->doc .= '</div>';// /.see-it 148 $renderer->doc .= '</div>'; // /.title-droite 149 $renderer->doc .= '</div>'; // /.title 150 151 } 152 // DESCRIPTION 153 if ($projDesc == ''){ 154 $renderer->doc .= '<div class="desc"><h4>'.$this->getLang('desctitle').'</h4> <p>'.$this->getLang('description').'</p></div>'; 155 } else { 156 $renderer->doc .= '<div class="desc"><h4>'.$this->getLang('desctitle').'</h4> <p class="desc"> ' . $projDesc . '</p></div>'; 157 } 158 // VERSIONS 159 $versions = $client->api('version')->all($data['proj']); 160 // Parsing Version 161 if($versions) { 162 $renderer->doc .= '<div class="version"><h3>'.$this->getLang('vertitle').'</h3>'; 163 $renderer->doc .= '<div class="panel-group" id="version-accordion-nb" role="tablist">'; 164 for($i = 0; $i < count($versions['versions']); $i++) { 165 // Begin Accordion 166 $renderer->doc .= '<div class="panel panel-primary descver">'; 167 $foundVersion = $versions['versions'][$i]; 168 $versionId = $foundVersion['id']; 169 $renderer->doc .= '<div class="version panel-heading">'; 170 $renderer->doc .= '<h4 class="panel-title">'; 171 $renderer->doc .= '<a class="version" data-toggle="collapse" data-parent="#version-accordion-nb" href="#collapse-version-nb-'.$versionId.'">'; 172 $renderer->doc .= '<span class="version">Version ' . $foundVersion['name'] . '</span> '; 173 $renderer->doc .= '</a>'; 174 $statusClass = (($foundVersion['status'] == 'open') ? 'statusop' : 'statuscl'); 175 $renderer->doc .= '<span class="'.$statusClass.'"> ' . $foundVersion['status'] . ' </span>'; 176 $renderer->doc .= '</h4>'; // /.panel-title 177 $renderer->doc .= '</div>'; // /.panel-heading 178 $renderer->doc .= '<div id="collapse-version-nb-'.$versionId.'" class="panel-collapse collapse">'; 179 // PANEL BODY 180 $renderer->doc .= '<div class="panel-body">'; 181 // Time Entries 182 $createdOn = DateTime::createFromFormat(DateTime::ISO8601, $foundVersion['created_on']); 183 $updatedOn = DateTime::createFromFormat(DateTime::ISO8601, $foundVersion['updated_on']); 184 $renderer->doc .= '<p><b>Description :</b> '.$foundVersion['description'].'</p>'; 185 $renderer->doc .= '<p><a href="'.$data['server_url'].'/versions/'.$versionId.'">See this version in redmine</a></p>'; 186 $renderer->doc .= '<p><b>'.$this->getLang('createdon').'</b>'.$createdOn->format(DateTime::RFC850).'</p>'; 187 $renderer->doc .= '<p><b>'.$this->getLang('updatedon').'</b>'.$updatedOn->format(DateTime::RFC850).'</p>'; 188 // Issues of Versions 189 $issueTotal = $client->api('issue')->all(array( 190 'project_id' => $projId, 191 'status_id' => '*', 192 'fixed_version_id' => $foundVersion['id'], 193 'limit' => 1 194 )); 195 // Total issues & open 196 $issueOpen = $client->api('issue')->all(array( 197 'project_id' => $projId, 198 'status_id' => 'open', 199 'fixed_version_id' => $foundVersion['id'], 200 'limit' => 1 201 )); 202 // Get percent version 203 $diffIssue = $issueTotal['total_count'] - $issueOpen['total_count']; 204 $progress = $this->getPercent($diffIssue,$issueTotal['total_count']); 205 // renderer Progressbar 206 $renderer->doc .= '<span class="col-md-3">'; 207 $renderer->doc .= '<a href="'.$data['server_url'].'/projects/'.$projIdent.'/issues">'.$issueTotal['total_count'].' issues ('.$diffIssue.' closed - '.$issueOpen['total_count'].' open)</a>'; 208 $renderer->doc .= '</span>'; // /.col-md-3 209 $renderer->doc .= '<span class="col-md-6">'; 210 $renderer->doc .= '<div class="progress">'; 211 $renderer->doc .= '<span class="progress-bar" role="progressbar" aria-valuenow="70" 212 aria-valuemin="0" aria-valuemax="100" style="width:'.$progress.'%">'; 213 $renderer->doc .= '<span class="doku">'.$progress.'% Complete</span>'; 214 $renderer->doc .= '</span></div>'; // ./progress 215 $renderer->doc .= '</span>'; // ./col-md-6 216 $renderer->doc .= '</div>'; // /.panel-body 217 $renderer->doc .= '</div>'; // /#collapse-version-nb-'.$versionId.' .panel-collapse 218 $renderer->doc .= '</div>'; // /.panel .panel-default 219 $renderer->doc .= '<br>'; 220 } 221 $renderer->doc .= '</div>'; // /.panel-group 222 } else { 223 $renderer->doc .= '<div class="version"><h3>'.$this->getLang('vertitle').'</h3>'; 224 $renderer->doc .= $nbVersion . ' versions'; 225 $renderer->doc .= 'div class="descver"><p>' . $this->getLang('noversion') . '</p></div>'; 226 } 227 $renderer->doc .= '</div>'; 228 229 // DETAILS 230 // Get Number of Version 231 for($v = 0; $v < count($versions['versions']); $v++) { 232 $nbVersion = $v + 1; 233 } 234 // Get number of Issues 235 $issueTotal = $client->api('issue')->all(array( 236 'project_id' => $projId, 237 'status_id' => '*', 238 'limit' => 1 239 )); 240 $issueOpen = $client->api('issue')->all(array( 241 'project_id' => $projId, 242 'status_id' => 'open', 243 'limit' => 1 244 )); 245 // Initialize Array 246 $usersByRole = array(); 247 $members = $client->api('membership')->all($projId); 248 // Found each Members 249 for($m = 0; $m < count($members['memberships']); $m++) { 250 // $z++; 251 $memberFound = $members['memberships'][$m]; 252 $currentUser = $memberFound['user']; 253 for($r = 0; $r <count($memberFound['roles']); $r++) { 254 $currentRole = $memberFound['roles'][$r]; 255 $roleId = $currentRole['id']; 256 // If doesn't exist in usersByRole, create it 257 if(!$usersByRole[$roleId]) { 258 $currentRole['members'] = array($currentUser); 259 $usersByRole[$roleId] = $currentRole; 260 } 261 // Else Push to array 262 else { 263 array_push($usersByRole[$roleId]['members'], $currentUser); 264 } 265 } 266 } 267 // Renderer Details 268 $renderer->doc .= '<div class="details">'; 269 $renderer->doc .= '<h3>'.$this->getlang('hdetail').'</h3>'; 270 // Stats 271 $renderer->doc .= '<div class="stats">'; 272 if($projParent == ''){ 273 $renderer->doc .= '<p>'.$this->getLang('mainproj').'</p>'; 274 } else { 275 $renderer->doc .= '<p>'.$this->getLang('subproject').' <a href="'.$data['server_url'].'/projects/'.$parentIdent.'">'.$nameParent.'</a></p>'; 276 } 277 $renderer->doc .= '<p>'.$this->getLang('tversion').'<span class="label label-info">'.$nbVersion.'</span>'.$this->getLang('vversion').'</p>'; 278 $renderer->doc .= '<p><span class="label label-success">'. $issueTotal['total_count'].'</span>'.$this->getLang('issues').'<span class="label label-warning">'.$issueOpen['total_count'].'</span>'.$this->getLang('open').'</p>'; 279 $renderer->doc .= '<p><span class="label label-info">'.$m.'</span>'.$this->getLang('membdetail').'</p>'; 280 $renderer->doc .= '</div>'; // /.stats 281 $renderer->doc .= '</div>'; // /.details 282 // MEMBERSHIPS & ROLES 283 $langMembers = $this->getLang('membres'); 284 $renderer->doc .= '<h3 class="member">'. $langMembers . '</h3>'; 285 // Display new array usersByRole 286 $renderer->doc .= '<div class="member">'; 287 foreach($usersByRole as $role => $currentRole) { 288 $renderer->doc .= '<p class="member">'.$currentRole['name'].' : '; 289 // Define a total to render commas 290 $total = count($currentRole['members']); 291 foreach($currentRole['members'] as $who => $currentUser) { 292 $userId = $currentUser['id']; 293 $mailCurrentUser = $client->api('user')->show($userId); 294 $mailUser = $mailCurrentUser['user']['mail']; 295 $renderer->doc .= ' <a href="mailto:'.$mailUser.'?Subject=Project '.$projName.'"target="_top"><span>'. $currentUser['name'] . '</span></a>' ; 296 if ($who < $total - 1) { 297 $renderer->doc .= ','; 298 } 299 } 300 $renderer->doc .= '</p>'; 301 } 302 $renderer->doc .= '</div>'; 303 } else { 304 $renderer->doc .= '<h2 class="title">'.$this->getLang('private').'</h2>'; 305 $renderer->doc .= '<div class="desc" style="float: none;"><h3>'.$this->getLang('info').'</h3>'.$this->getLang('norights').' </p></div>'; 306 307 } 308 } 309 // Dokuwiki Renderer 310 function render($mode, Doku_Renderer $renderer, $data) { 311 $renderer->info['cache'] = false; 312 if($mode != 'xhtml') return false; 313 314 if($data['error']) { 315 $renderer->doc .= $data['text']; 316 return true; 317 } 318 switch($data['state']) { 319 case DOKU_LEXER_SPECIAL : 320 $this->_render_project($renderer, $data); 321 break; 322 case DOKU_LEXER_ENTER : 323 $this->_render_project($renderer, $data); 324 break; 325 case DOKU_LEXER_EXIT: 326 case DOKU_LEXER_UNMATCHED : 327 $renderer->doc .= $renderer->_xmlEntities($data['text']); 328 break; 329 } 330 return true; 331 } 332} 333