Factory.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. * @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
  5. *
  6. * @author Bart Visscher <bartv@thisnet.nl>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author Lukas Reschke <lukas@statuscode.ch>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Robin Appelman <robin@icewind.nl>
  11. * @author Robin McCorkell <robin@mccorkell.me.uk>
  12. * @author Roeland Jago Douma <roeland@famdouma.nl>
  13. *
  14. * @license AGPL-3.0
  15. *
  16. * This code is free software: you can redistribute it and/or modify
  17. * it under the terms of the GNU Affero General Public License, version 3,
  18. * as published by the Free Software Foundation.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU Affero General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU Affero General Public License, version 3,
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>
  27. *
  28. */
  29. namespace OC\L10N;
  30. use OCP\IConfig;
  31. use OCP\IRequest;
  32. use OCP\IUserSession;
  33. use OCP\L10N\IFactory;
  34. /**
  35. * A factory that generates language instances
  36. */
  37. class Factory implements IFactory {
  38. /** @var string */
  39. protected $requestLanguage = '';
  40. /**
  41. * cached instances
  42. * @var array Structure: Lang => App => \OCP\IL10N
  43. */
  44. protected $instances = [];
  45. /**
  46. * @var array Structure: App => string[]
  47. */
  48. protected $availableLanguages = [];
  49. /**
  50. * @var array Structure: string => callable
  51. */
  52. protected $pluralFunctions = [];
  53. /** @var IConfig */
  54. protected $config;
  55. /** @var IRequest */
  56. protected $request;
  57. /** @var IUserSession */
  58. protected $userSession;
  59. /** @var string */
  60. protected $serverRoot;
  61. /**
  62. * @param IConfig $config
  63. * @param IRequest $request
  64. * @param IUserSession $userSession
  65. * @param string $serverRoot
  66. */
  67. public function __construct(IConfig $config,
  68. IRequest $request,
  69. IUserSession $userSession,
  70. $serverRoot) {
  71. $this->config = $config;
  72. $this->request = $request;
  73. $this->userSession = $userSession;
  74. $this->serverRoot = $serverRoot;
  75. }
  76. /**
  77. * Get a language instance
  78. *
  79. * @param string $app
  80. * @param string|null $lang
  81. * @return \OCP\IL10N
  82. */
  83. public function get($app, $lang = null) {
  84. $app = \OC_App::cleanAppId($app);
  85. if ($lang !== null) {
  86. $lang = str_replace(array('\0', '/', '\\', '..'), '', (string) $lang);
  87. }
  88. if ($lang === null || !$this->languageExists($app, $lang)) {
  89. $lang = $this->findLanguage($app);
  90. }
  91. if (!isset($this->instances[$lang][$app])) {
  92. $this->instances[$lang][$app] = new L10N(
  93. $this, $app, $lang,
  94. $this->getL10nFilesForApp($app, $lang)
  95. );
  96. }
  97. return $this->instances[$lang][$app];
  98. }
  99. /**
  100. * Find the best language
  101. *
  102. * @param string|null $app App id or null for core
  103. * @return string language If nothing works it returns 'en'
  104. */
  105. public function findLanguage($app = null) {
  106. if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
  107. return $this->requestLanguage;
  108. }
  109. /**
  110. * At this point ownCloud might not yet be installed and thus the lookup
  111. * in the preferences table might fail. For this reason we need to check
  112. * whether the instance has already been installed
  113. *
  114. * @link https://github.com/owncloud/core/issues/21955
  115. */
  116. if($this->config->getSystemValue('installed', false)) {
  117. $userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() : null;
  118. if(!is_null($userId)) {
  119. $userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
  120. } else {
  121. $userLang = null;
  122. }
  123. } else {
  124. $userId = null;
  125. $userLang = null;
  126. }
  127. if ($userLang) {
  128. $this->requestLanguage = $userLang;
  129. if ($this->languageExists($app, $userLang)) {
  130. return $userLang;
  131. }
  132. }
  133. try {
  134. // Try to get the language from the Request
  135. $lang = $this->getLanguageFromRequest($app);
  136. if ($userId !== null && $app === null && !$userLang) {
  137. $this->config->setUserValue($userId, 'core', 'lang', $lang);
  138. }
  139. return $lang;
  140. } catch (LanguageNotFoundException $e) {
  141. // Finding language from request failed fall back to default language
  142. $defaultLanguage = $this->config->getSystemValue('default_language', false);
  143. if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
  144. return $defaultLanguage;
  145. }
  146. }
  147. // We could not find any language so fall back to english
  148. return 'en';
  149. }
  150. /**
  151. * Find all available languages for an app
  152. *
  153. * @param string|null $app App id or null for core
  154. * @return array an array of available languages
  155. */
  156. public function findAvailableLanguages($app = null) {
  157. $key = $app;
  158. if ($key === null) {
  159. $key = 'null';
  160. }
  161. // also works with null as key
  162. if (!empty($this->availableLanguages[$key])) {
  163. return $this->availableLanguages[$key];
  164. }
  165. $available = ['en']; //english is always available
  166. $dir = $this->findL10nDir($app);
  167. if (is_dir($dir)) {
  168. $files = scandir($dir);
  169. if ($files !== false) {
  170. foreach ($files as $file) {
  171. if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
  172. $available[] = substr($file, 0, -5);
  173. }
  174. }
  175. }
  176. }
  177. // merge with translations from theme
  178. $theme = $this->config->getSystemValue('theme');
  179. if (!empty($theme)) {
  180. $themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
  181. if (is_dir($themeDir)) {
  182. $files = scandir($themeDir);
  183. if ($files !== false) {
  184. foreach ($files as $file) {
  185. if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
  186. $available[] = substr($file, 0, -5);
  187. }
  188. }
  189. }
  190. }
  191. }
  192. $this->availableLanguages[$key] = $available;
  193. return $available;
  194. }
  195. /**
  196. * @param string|null $app App id or null for core
  197. * @param string $lang
  198. * @return bool
  199. */
  200. public function languageExists($app, $lang) {
  201. if ($lang === 'en') {//english is always available
  202. return true;
  203. }
  204. $languages = $this->findAvailableLanguages($app);
  205. return array_search($lang, $languages) !== false;
  206. }
  207. /**
  208. * @param string|null $app
  209. * @return string
  210. * @throws LanguageNotFoundException
  211. */
  212. private function getLanguageFromRequest($app) {
  213. $header = $this->request->getHeader('ACCEPT_LANGUAGE');
  214. if ($header) {
  215. $available = $this->findAvailableLanguages($app);
  216. // E.g. make sure that 'de' is before 'de_DE'.
  217. sort($available);
  218. $preferences = preg_split('/,\s*/', strtolower($header));
  219. foreach ($preferences as $preference) {
  220. list($preferred_language) = explode(';', $preference);
  221. $preferred_language = str_replace('-', '_', $preferred_language);
  222. foreach ($available as $available_language) {
  223. if ($preferred_language === strtolower($available_language)) {
  224. return $available_language;
  225. }
  226. }
  227. // Fallback from de_De to de
  228. foreach ($available as $available_language) {
  229. if (substr($preferred_language, 0, 2) === $available_language) {
  230. return $available_language;
  231. }
  232. }
  233. }
  234. }
  235. throw new LanguageNotFoundException();
  236. }
  237. /**
  238. * @param string|null $app App id or null for core
  239. * @return string
  240. */
  241. public function setLanguageFromRequest($app = null) {
  242. try {
  243. $requestLanguage = $this->getLanguageFromRequest($app);
  244. } catch (LanguageNotFoundException $e) {
  245. $requestLanguage = 'en';
  246. }
  247. if ($app === null && !$this->requestLanguage) {
  248. $this->requestLanguage = $requestLanguage;
  249. }
  250. return $requestLanguage;
  251. }
  252. /**
  253. * Get a list of language files that should be loaded
  254. *
  255. * @param string $app
  256. * @param string $lang
  257. * @return string[]
  258. */
  259. // FIXME This method is only public, until OC_L10N does not need it anymore,
  260. // FIXME This is also the reason, why it is not in the public interface
  261. public function getL10nFilesForApp($app, $lang) {
  262. $languageFiles = [];
  263. $i18nDir = $this->findL10nDir($app);
  264. $transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
  265. if ((\OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
  266. || \OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
  267. || \OC_Helper::isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
  268. || \OC_Helper::isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
  269. )
  270. && file_exists($transFile)) {
  271. // load the translations file
  272. $languageFiles[] = $transFile;
  273. }
  274. // merge with translations from theme
  275. $theme = $this->config->getSystemValue('theme');
  276. if (!empty($theme)) {
  277. $transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
  278. if (file_exists($transFile)) {
  279. $languageFiles[] = $transFile;
  280. }
  281. }
  282. return $languageFiles;
  283. }
  284. /**
  285. * find the l10n directory
  286. *
  287. * @param string $app App id or empty string for core
  288. * @return string directory
  289. */
  290. protected function findL10nDir($app = null) {
  291. if (in_array($app, ['core', 'lib', 'settings'])) {
  292. if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
  293. return $this->serverRoot . '/' . $app . '/l10n/';
  294. }
  295. } else if ($app && \OC_App::getAppPath($app) !== false) {
  296. // Check if the app is in the app folder
  297. return \OC_App::getAppPath($app) . '/l10n/';
  298. }
  299. return $this->serverRoot . '/core/l10n/';
  300. }
  301. /**
  302. * Creates a function from the plural string
  303. *
  304. * Parts of the code is copied from Habari:
  305. * https://github.com/habari/system/blob/master/classes/locale.php
  306. * @param string $string
  307. * @return string
  308. */
  309. public function createPluralFunction($string) {
  310. if (isset($this->pluralFunctions[$string])) {
  311. return $this->pluralFunctions[$string];
  312. }
  313. if (preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
  314. // sanitize
  315. $nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
  316. $plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
  317. $body = str_replace(
  318. array( 'plural', 'n', '$n$plurals', ),
  319. array( '$plural', '$n', '$nplurals', ),
  320. 'nplurals='. $nplurals . '; plural=' . $plural
  321. );
  322. // add parents
  323. // important since PHP's ternary evaluates from left to right
  324. $body .= ';';
  325. $res = '';
  326. $p = 0;
  327. for($i = 0; $i < strlen($body); $i++) {
  328. $ch = $body[$i];
  329. switch ( $ch ) {
  330. case '?':
  331. $res .= ' ? (';
  332. $p++;
  333. break;
  334. case ':':
  335. $res .= ') : (';
  336. break;
  337. case ';':
  338. $res .= str_repeat( ')', $p ) . ';';
  339. $p = 0;
  340. break;
  341. default:
  342. $res .= $ch;
  343. }
  344. }
  345. $body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
  346. $function = create_function('$n', $body);
  347. $this->pluralFunctions[$string] = $function;
  348. return $function;
  349. } else {
  350. // default: one plural form for all cases but n==1 (english)
  351. $function = create_function(
  352. '$n',
  353. '$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
  354. );
  355. $this->pluralFunctions[$string] = $function;
  356. return $function;
  357. }
  358. }
  359. }