Router.class.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. <?php
  2. /**
  3. * Request router
  4. *
  5. * @author Christopher Han <xiphux@gmail.com>
  6. * @copyright Copyright (c) 2010 Christopher Han
  7. * @package GitPHP
  8. * @subpackage Router
  9. */
  10. class GitPHP_Router
  11. {
  12. /**
  13. * Route map
  14. *
  15. * @var array
  16. */
  17. protected $routes = array();
  18. /**
  19. * Query parameter map
  20. *
  21. * @var array
  22. */
  23. protected $queryParameters = array();
  24. /**
  25. * Clean url flag
  26. *
  27. * @var boolean
  28. */
  29. protected $cleanurl = false;
  30. /**
  31. * Abbreviate hashes flag
  32. *
  33. * @var boolean
  34. */
  35. protected $abbreviate = false;
  36. /**
  37. * Base url
  38. *
  39. * @var string
  40. */
  41. protected $baseurl;
  42. /**
  43. * Full url
  44. *
  45. * @var string
  46. */
  47. protected $fullurl;
  48. /**
  49. * Constructor
  50. *
  51. * @param boolean $cleanurl true to generate clean urls
  52. * @param boolean $abbreviate true to abbreviate hashes
  53. */
  54. public function __construct($cleanurl = false, $abbreviate = false)
  55. {
  56. $this->cleanurl = $cleanurl;
  57. $this->abbreviate = $abbreviate;
  58. $this->baseurl = GitPHP_Util::BaseUrl();
  59. if (empty($this->baseurl))
  60. $this->baseurl = '/';
  61. $this->fullurl = GitPHP_Util::BaseUrl(true);
  62. $this->InitializeRoutes();
  63. $this->InitializeQueryParameters();
  64. }
  65. /**
  66. * Get clean url setting
  67. *
  68. * @return boolean
  69. */
  70. public function GetCleanUrl()
  71. {
  72. return $this->cleanurl;
  73. }
  74. /**
  75. * Set clean url setting
  76. *
  77. * @param boolean $cleanurl true to generate clean urls
  78. */
  79. public function SetCleanUrl($cleanurl)
  80. {
  81. $this->cleanurl = $cleanurl;
  82. }
  83. /**
  84. * Get abbreviate hash setting
  85. *
  86. * @return boolean
  87. */
  88. public function GetAbbreviate()
  89. {
  90. return $this->abbreviate;
  91. }
  92. /**
  93. * Set abbreviate hash setting
  94. *
  95. * @param boolean $abbreviate abbreviate
  96. */
  97. public function SetAbbreviate($abbreviate)
  98. {
  99. $this->abbreviate = $abbreviate;
  100. }
  101. /**
  102. * Get base url
  103. *
  104. * @param boolean $full true to return full base url (include protocol and hostname)
  105. * @return string base url
  106. */
  107. public function GetBaseUrl($full = false)
  108. {
  109. if ($full)
  110. return $this->fullurl;
  111. return $this->baseurl;
  112. }
  113. /**
  114. * Set base url
  115. *
  116. * @param string $baseurl base url
  117. */
  118. public function SetBaseUrl($baseurl)
  119. {
  120. $this->baseurl = $baseurl;
  121. $this->fullurl = $baseurl;
  122. }
  123. /**
  124. * Initialize route map
  125. */
  126. private function InitializeRoutes()
  127. {
  128. // project view
  129. $projectroute = new GitPHP_Route('projects/:project', array(
  130. 'project' => '[^\?]+'
  131. ));
  132. // project-specific action with hash and output method
  133. $this->routes[] = new GitPHP_Route(':action/:hash/:output', array(
  134. 'action' => 'blobs',
  135. 'hash' => '[0-9A-Fa-f]{4,40}|HEAD',
  136. 'output' => 'plain'
  137. ), array(), $projectroute);
  138. // project-specific action with hash
  139. $this->routes[] = new GitPHP_Route(':action/:hash', array(
  140. 'action' => 'commits|trees|blobs|search|snapshot|commitdiff|blobdiff|blame',
  141. 'hash' => '[0-9A-Fa-f]{4,40}|HEAD'
  142. ), array(), $projectroute);
  143. // project-specific action with hash or ref
  144. $this->routes[] = new GitPHP_Route(':action/:hash', array(
  145. 'action' => 'shortlog|log',
  146. 'hash' => '[^\?]+'
  147. ), array(), $projectroute);
  148. // map heads to shortlog
  149. $this->routes[] = new GitPHP_Route(':action/:hash', array(
  150. 'action' => 'heads',
  151. 'hash' => '[^\?]+'
  152. ), array(
  153. 'action' => 'shortlog'
  154. ), $projectroute);
  155. // project-specific graphs
  156. $this->routes[] = new GitPHP_Route(':action/:graphtype', array(
  157. 'action' => 'graphs',
  158. 'graphtype' => '[a-z]+'
  159. ), array(), $projectroute);
  160. // project-specific tag
  161. $this->routes[] = new GitPHP_Route(':action/:tag', array(
  162. 'action' => 'tags',
  163. 'tag' => '[^\?]+'
  164. ), array(), $projectroute);
  165. $formats = GitPHP_Archive::SupportedFormats();
  166. if (count($formats) > 0) {
  167. $formatconstraint = implode("|", array_keys($formats));
  168. // project specific snapshot format with hash
  169. $this->routes[] = new GitPHP_Route(':format/:hash', array(
  170. 'format' => $formatconstraint,
  171. 'hash' => '[0-9A-Fa-f]{4,40}|HEAD'
  172. ), array(
  173. 'action' => 'snapshot'
  174. ), $projectroute);
  175. // project specific snapshot format
  176. $this->routes[] = new GitPHP_Route(':format', array(
  177. 'format' => $formatconstraint
  178. ), array(
  179. 'action' => 'snapshot'
  180. ), $projectroute);
  181. }
  182. // project-specific action only
  183. $this->routes[] = new GitPHP_Route(':action', array(
  184. 'action' => 'tags|heads|shortlog|log|search|atom|rss|snapshot|commits|graphs|trees|blobs|history|commitdiff|blobdiff'
  185. ), array(), $projectroute);
  186. $this->routes[] = $projectroute;
  187. // non-project action
  188. $this->routes[] = new GitPHP_Route(':action', array(
  189. 'action' => 'opml|projectindex|login|logout'
  190. ));
  191. usort($this->routes, array('GitPHP_Route', 'CompareRoute'));
  192. }
  193. /**
  194. * Initialize query parameter map
  195. */
  196. private function InitializeQueryParameters()
  197. {
  198. $this->queryParameters = array(
  199. 'project' => 'p',
  200. 'action' => 'a',
  201. 'hash' => 'h',
  202. 'hashbase' => 'hb',
  203. 'hashparent' => 'hp',
  204. 'graphtype' => 'g',
  205. 'output' => 'o',
  206. 'format' => 'fmt',
  207. 'tag' => 't',
  208. 'page' => 'pg',
  209. 'search' => 's',
  210. 'searchtype' => 'st',
  211. 'diffmode' => 'd',
  212. 'file' => 'f',
  213. 'mark' => 'm',
  214. 'prefix' => 'prefix',
  215. 'sort' => 'sort',
  216. 'lang' => 'l',
  217. 'redirect' => 'redirect'
  218. );
  219. }
  220. /**
  221. * Convert a parameter array to a query variable array
  222. *
  223. * @param array $params parameter array
  224. * @return array query variable array
  225. */
  226. private function ParameterArrayToQueryVarArray($params)
  227. {
  228. $queryvars = array();
  229. if (count($params) < 1)
  230. return $queryvars;
  231. foreach ($params as $param => $val) {
  232. if (empty($val))
  233. continue;
  234. if (empty($this->queryParameters[$param]))
  235. continue;
  236. $queryvar = $this->queryParameters[$param];
  237. if (!empty($queryvar))
  238. $queryvars[$queryvar] = $val;
  239. }
  240. return $queryvars;
  241. }
  242. /**
  243. * Convert a query variable array to a parameter array
  244. *
  245. * @param array $queryvars query variable array
  246. * @return array parameter array
  247. */
  248. private function QueryVarArrayToParameterArray($queryvars)
  249. {
  250. $params = array();
  251. if (count($queryvars) < 1)
  252. return $params;
  253. foreach ($queryvars as $var => $val) {
  254. if (empty($val))
  255. continue;
  256. $param = array_search($var, $this->queryParameters);
  257. if (!empty($param))
  258. $params[$param] = $val;
  259. }
  260. return $params;
  261. }
  262. /**
  263. * Build route from url parameters
  264. *
  265. * @param array $urlparams url parameters
  266. */
  267. private function BuildRoute($urlparams)
  268. {
  269. foreach ($this->routes as $route) {
  270. if (!$route->Valid($urlparams))
  271. continue;
  272. $path = $route->Build($urlparams);
  273. $usedparams = $route->GetUsedParameters();
  274. return array($path, $usedparams);
  275. }
  276. return array(null, array());
  277. }
  278. /**
  279. * Find route matching query
  280. *
  281. * @param string $query query
  282. * @return array query parameters
  283. */
  284. private function FindRoute($query)
  285. {
  286. if (empty($query))
  287. return array();
  288. foreach ($this->routes as $route) {
  289. $params = $route->Match($query);
  290. if ($params === false)
  291. continue;
  292. return $params;
  293. }
  294. return array();
  295. }
  296. /**
  297. * Gets a controller for an action
  298. *
  299. * @return GitPHP_ControllerBase
  300. */
  301. public function GetController()
  302. {
  303. $params = $this->QueryVarArrayToParameterArray($_GET);
  304. if (!empty($_POST)) {
  305. $params = array_merge($params, $this->QueryVarArrayToParameterArray($_POST));
  306. }
  307. if (!empty($_GET['q'])) {
  308. $restparams = GitPHP_Router::ReadCleanUrl($_SERVER['REQUEST_URI']);
  309. if (count($restparams) > 0)
  310. $params = array_merge($params, $restparams);
  311. }
  312. $controller = null;
  313. $action = null;
  314. if (!empty($params['action']))
  315. $action = $params['action'];
  316. switch ($action) {
  317. case 'search':
  318. $controller = new GitPHP_Controller_Search();
  319. break;
  320. case 'commitdiff':
  321. case 'commitdiff_plain':
  322. $controller = new GitPHP_Controller_Commitdiff();
  323. if ($action === 'commitdiff_plain')
  324. $controller->SetParam('output', 'plain');
  325. break;
  326. case 'blobdiff':
  327. case 'blobdiff_plain':
  328. $controller = new GitPHP_Controller_Blobdiff();
  329. if ($action === 'blobdiff_plain')
  330. $controller->SetParam('output', 'plain');
  331. break;
  332. case 'history':
  333. $controller = new GitPHP_Controller_History();
  334. break;
  335. case 'shortlog':
  336. case 'log':
  337. $controller = new GitPHP_Controller_Log();
  338. if ($action === 'shortlog')
  339. $controller->SetParam('short', true);
  340. break;
  341. case 'snapshot':
  342. $controller = new GitPHP_Controller_Snapshot();
  343. break;
  344. case 'tree':
  345. case 'trees':
  346. $controller = new GitPHP_Controller_Tree();
  347. break;
  348. case 'tags':
  349. if (empty($params['tag'])) {
  350. $controller = new GitPHP_Controller_Tags();
  351. break;
  352. }
  353. case 'tag':
  354. $controller = new GitPHP_Controller_Tag();
  355. break;
  356. case 'heads':
  357. $controller = new GitPHP_Controller_Heads();
  358. break;
  359. case 'blame':
  360. $controller = new GitPHP_Controller_Blame();
  361. break;
  362. case 'blob':
  363. case 'blobs':
  364. case 'blob_plain':
  365. $controller = new GitPHP_Controller_Blob();
  366. if ($action === 'blob_plain')
  367. $controller->SetParam('output', 'plain');
  368. break;
  369. case 'atom':
  370. case 'rss':
  371. $controller = new GitPHP_Controller_Feed();
  372. if ($action == 'rss')
  373. $controller->SetParam('format', GitPHP_Controller_Feed::RssFormat);
  374. else if ($action == 'atom')
  375. $controller->SetParam('format', GitPHP_Controller_Feed::AtomFormat);
  376. break;
  377. case 'commit':
  378. case 'commits':
  379. $controller = new GitPHP_Controller_Commit();
  380. break;
  381. case 'summary':
  382. $controller = new GitPHP_Controller_Project();
  383. break;
  384. case 'project_index':
  385. case 'projectindex':
  386. $controller = new GitPHP_Controller_ProjectList();
  387. $controller->SetParam('txt', true);
  388. break;
  389. case 'opml':
  390. $controller = new GitPHP_Controller_ProjectList();
  391. $controller->SetParam('opml', true);
  392. break;
  393. case 'login':
  394. $controller = new GitPHP_Controller_Login();
  395. if (!empty($_POST['username']))
  396. $controller->SetParam('username', $_POST['username']);
  397. if (!empty($_POST['password']))
  398. $controller->SetParam('password', $_POST['password']);
  399. break;
  400. case 'logout':
  401. $controller = new GitPHP_Controller_Logout();
  402. break;
  403. case 'graph':
  404. case 'graphs':
  405. //$controller = new GitPHP_Controller_Graph();
  406. //break;
  407. case 'graphdata':
  408. //$controller = new GitPHP_Controller_GraphData();
  409. //break;
  410. default:
  411. if (!empty($params['project'])) {
  412. $controller = new GitPHP_Controller_Project();
  413. } else {
  414. $controller = new GitPHP_Controller_ProjectList();
  415. }
  416. }
  417. foreach ($params as $paramname => $paramval) {
  418. if ($paramname !== 'action')
  419. $controller->SetParam($paramname, $paramval);
  420. }
  421. $controller->SetRouter($this);
  422. return $controller;
  423. }
  424. /**
  425. * Get message controller
  426. *
  427. * @return GitPHP_ControllerBase
  428. */
  429. public function GetMessageController()
  430. {
  431. $params = $this->QueryVarArrayToParameterArray($_GET);
  432. if (!empty($_GET['q'])) {
  433. $restparams = GitPHP_Router::ReadCleanUrl($_SERVER['REQUEST_URI']);
  434. if (count($restparams) > 0)
  435. $params = array_merge($params, $restparams);
  436. }
  437. $controller = new GitPHP_Controller_Message();
  438. foreach ($params as $paramname => $paramval) {
  439. if ($paramname !== 'action')
  440. $controller->SetParam($paramname, $paramval);
  441. }
  442. $controller->SetRouter($this);
  443. return $controller;
  444. }
  445. /**
  446. * Read a rest-style clean url
  447. *
  448. * @param string $url url
  449. * @return array request parameters from url
  450. */
  451. private function ReadCleanUrl($url)
  452. {
  453. $querypos = strpos($url, '?');
  454. if ($querypos !== false)
  455. $url = substr($url, 0, $querypos);
  456. $url = rtrim($url, "/");
  457. $baseurl = GitPHP_Util::AddSlash(GitPHP_Util::BaseUrl(), false);
  458. if (empty($baseurl))
  459. $baseurl = '/';
  460. if (strncmp($baseurl, $url, strlen($baseurl)) === 0)
  461. $url = substr($url, strlen($baseurl));
  462. return $this->FindRoute($url);
  463. }
  464. /**
  465. * Generate a url
  466. *
  467. * @param array $params request parameters
  468. * @param boolean $full true to get full url (include protocol and hostname)
  469. */
  470. public function GetUrl($params = array(), $full = false)
  471. {
  472. if ($full)
  473. $baseurl = $this->fullurl;
  474. else
  475. $baseurl = $this->baseurl;
  476. if ($this->cleanurl) {
  477. if (substr_compare($baseurl, 'index.php', -9) === 0) {
  478. $baseurl = dirname($baseurl);
  479. }
  480. $baseurl = GitPHP_Util::AddSlash($baseurl, false);
  481. } else {
  482. if (substr_compare($baseurl, 'index.php', -9) !== 0) {
  483. $baseurl = GitPHP_Util::AddSlash($baseurl, false);
  484. }
  485. }
  486. if (count($params) < 1)
  487. return $baseurl;
  488. $abbreviate = $this->abbreviate;
  489. if ($abbreviate && !empty($params['project']) && ($params['project'] instanceof GitPHP_Project)) {
  490. if ($params['project']->GetCompat())
  491. $abbreviate = false;
  492. }
  493. foreach ($params as $paramname => $paramval) {
  494. switch ($paramname) {
  495. case 'hash':
  496. case 'hashbase':
  497. case 'hashparent':
  498. case 'mark':
  499. $params[$paramname] = GitPHP_Router::GetHash($paramval, $abbreviate);
  500. break;
  501. case 'tag':
  502. $params[$paramname] = GitPHP_Router::GetTag($paramval);
  503. break;
  504. case 'project':
  505. $params[$paramname] = GitPHP_Router::GetProject($paramval);
  506. break;
  507. }
  508. }
  509. if ($this->cleanurl) {
  510. if (!empty($params['action'])) {
  511. switch ($params['action']) {
  512. case 'blob':
  513. case 'commit':
  514. case 'tree':
  515. case 'graph':
  516. case 'tag':
  517. // these actions are plural in clean urls
  518. $params['action'] = $params['action'] . 's';
  519. break;
  520. }
  521. }
  522. list($queryurl, $exclude) = $this->BuildRoute($params);
  523. $baseurl .= $queryurl;
  524. foreach ($exclude as $excludeparam) {
  525. unset($params[$excludeparam]);
  526. }
  527. }
  528. $querystr = GitPHP_Router::GetQueryString($params);
  529. if (empty($querystr))
  530. return $baseurl;
  531. return $baseurl . '?' . $querystr;
  532. }
  533. /**
  534. * Gets query parameters for a url
  535. *
  536. * @param array $params query parameters
  537. * @return string query string
  538. */
  539. private function GetQueryString($params = array())
  540. {
  541. if (count($params) < 1)
  542. return null;
  543. $query = $this->ParameterArrayToQueryVarArray($params);
  544. if (count($query) < 1)
  545. return null;
  546. $querystr = null;
  547. foreach ($query as $var => $val) {
  548. if (empty($val))
  549. continue;
  550. if (!empty($querystr))
  551. $querystr .= '&';
  552. $querystr .= $var . '=' . rawurlencode($val);
  553. }
  554. return $querystr;
  555. }
  556. /**
  557. * Gets a hash for a string or hash-identified object
  558. *
  559. * @param string|GitPHP_GitObject $value string or hashed object
  560. * @param boolean $abbreviate true to abbreviate hash
  561. * @return string hash
  562. */
  563. private static function GetHash($value, $abbreviate = false)
  564. {
  565. if ($value instanceof GitPHP_GitObject)
  566. return $value->GetHash($abbreviate);
  567. else if (is_string($value))
  568. return $value;
  569. return null;
  570. }
  571. /**
  572. * Gets an identifier for a tag
  573. *
  574. * @param string|GitPHP_Tag $value string or tag
  575. * @return string hash
  576. */
  577. private static function GetTag($value)
  578. {
  579. if ($value instanceof GitPHP_Tag)
  580. return $value->GetName();
  581. else if (is_string($value))
  582. return $value;
  583. return null;
  584. }
  585. /**
  586. * Gets a project identifier for a project
  587. *
  588. * @param string|GitPHP_Project $value string or project
  589. * @return string identifier
  590. */
  591. private static function GetProject($value)
  592. {
  593. if ($value instanceof GitPHP_Project) {
  594. return $value->GetProject();
  595. } else if (is_string($value)) {
  596. return $value;
  597. }
  598. }
  599. }