l10n_update.module 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. <?php
  2. /**
  3. * @file
  4. * Download translations from remote localization server.
  5. *
  6. * @todo Fetch information from info files.
  7. */
  8. /**
  9. * Update mode: Remote server.
  10. */
  11. define('L10N_UPDATE_CHECK_REMOTE', 1);
  12. /**
  13. * Update mode: Local server.
  14. */
  15. define('L10N_UPDATE_CHECK_LOCAL', 2);
  16. /**
  17. * Update mode: both.
  18. */
  19. define('L10N_UPDATE_CHECK_ALL', L10N_UPDATE_CHECK_REMOTE | L10N_UPDATE_CHECK_LOCAL);
  20. /**
  21. * Translation import mode keeping translations which are edited after enabling
  22. * Locale Update module an only override default (un-edited) translations.
  23. */
  24. define('LOCALE_UPDATE_OVERRIDE_DEFAULT', 2);
  25. /**
  26. * The maximum number of projects which are checked for available translations each cron run.
  27. */
  28. define('L10N_UPDATE_CRON_PROJECTS', 10);
  29. /**
  30. * The maximum number of projects which are updated each cron run.
  31. */
  32. define('L10N_UPDATE_CRON_UPDATES', 2);
  33. /**
  34. * Implements hook_help().
  35. */
  36. function l10n_update_help($path, $arg) {
  37. switch ($path) {
  38. case 'admin/config/regional/translate/update':
  39. $output = '<p>' . t('List of latest imported translations and available updates for each enabled project and language.') . '</p>';
  40. $output .= '<p>' . t('If there are available updates you can click on Update for them to be downloaded and imported now or you can edit the configuration for them to be updated automatically on the <a href="@update-settings">Update settings page</a>', array('@update-settings' => url('admin/config/regional/language/update'))) . '</p>';
  41. return $output;
  42. break;
  43. case 'admin/config/regional/language/update':
  44. $output = '<p>' . t('These are the settings for the translation update system. To update your translations now, check out the <a href="@update-admin">Translation update administration page</a>.', array('@update-admin' => url('admin/config/regional/translate/update'))) . '</p>';
  45. return $output;
  46. break;
  47. }
  48. }
  49. /**
  50. * Implements hook_menu().
  51. */
  52. function l10n_update_menu() {
  53. $items['admin/config/regional/translate/update'] = array(
  54. 'title' => 'Update',
  55. 'description' => 'Available updates',
  56. 'page callback' => 'l10n_update_admin_overview',
  57. 'access arguments' => array('translate interface'),
  58. 'file' => 'l10n_update.admin.inc',
  59. 'weight' => 20,
  60. 'type' => MENU_LOCAL_TASK,
  61. );
  62. $items['admin/config/regional/language/update'] = array(
  63. 'title' => 'Translation updates',
  64. 'description' => 'Automatic update configuration',
  65. 'page callback' => 'drupal_get_form',
  66. 'page arguments' => array('l10n_update_admin_settings_form'),
  67. 'access arguments' => array('translate interface'),
  68. 'file' => 'l10n_update.admin.inc',
  69. 'weight' => 20,
  70. 'type' => MENU_LOCAL_TASK,
  71. );
  72. return $items;
  73. }
  74. /**
  75. * Implements hook_menu_alter().
  76. */
  77. function l10n_update_menu_alter(&$menu) {
  78. // Redirect l10n_client AJAX callback path for strings.
  79. if (module_exists('l10n_client')) {
  80. $menu['l10n_client/save']['page callback'] = 'l10n_update_client_save_string';
  81. }
  82. }
  83. /**
  84. * Implements hook_cron().
  85. *
  86. * Check one project/language at a time, download and import if update available
  87. */
  88. function l10n_update_cron() {
  89. $last = variable_get('l10n_update_last_check', 0);
  90. $frequency = variable_get('l10n_update_check_frequency', 0) * 24 *3600;
  91. if ($frequency && $last < REQUEST_TIME - $frequency) {
  92. module_load_include('check.inc', 'l10n_update');
  93. list($checked, $updated) = l10n_update_check_translations(L10N_UPDATE_CRON_PROJECTS, REQUEST_TIME - $frequency, L10N_UPDATE_CRON_UPDATES);
  94. if (count($checked) || count($updated)) {
  95. watchdog('l10n_update', 'Automatically checked @checked translations, updated @updated.', array('@checked' => count($checked), '@updated' => count($updated)));
  96. }
  97. variable_set('l10n_update_last_check', REQUEST_TIME);
  98. }
  99. }
  100. /**
  101. * Implements hook_form_alter().
  102. */
  103. function l10n_update_form_alter(&$form, $form_state, $form_id) {
  104. switch ($form_id) {
  105. case 'locale_translate_edit_form':
  106. case 'i18n_string_locale_translate_edit_form':
  107. $form['#submit'][] = 'l10n_update_locale_translate_edit_form_submit';
  108. break;
  109. case 'locale_languages_predefined_form':
  110. case 'locale_languages_custom_form':
  111. $form['#submit'][] = 'l10n_update_languages_changed_submit';
  112. break;
  113. case 'locale_languages_delete_form':
  114. // A language is being deleted.
  115. $form['#submit'][] = 'l10n_update_languages_delete_submit';
  116. break;
  117. }
  118. }
  119. /**
  120. * Implements hook_modules_enabled().
  121. *
  122. * Refresh project translation status and get translations if required.
  123. */
  124. function l10n_update_modules_enabled($modules) {
  125. module_load_include('project.inc', 'l10n_update');
  126. l10n_update_project_refresh($modules);
  127. }
  128. /**
  129. * Implements hook_modules_uninstalled().
  130. *
  131. * Remove data of uninstalled modules from {l10n_update_file} table and
  132. * rebuild the projects cache.
  133. */
  134. function l10n_update_modules_uninstalled($modules) {
  135. db_delete('l10n_update_file')
  136. ->condition('project', $modules)
  137. ->execute();
  138. // Rebuild {l10n_update_project} table.
  139. // Just like the system table, the project table holds both enabled and
  140. // disabled projects. Full control over its content is not possible.
  141. // To minimize polution we flush it here. The cost of rebuilding is small
  142. // compared to the {l10n_update_file} table.
  143. db_delete('l10n_update_project')->execute();
  144. module_load_include('project.inc', 'l10n_update');
  145. l10n_update_build_projects();
  146. }
  147. /**
  148. * Aditional submit handler for language forms
  149. *
  150. * We need to refresh status when a new language is enabled / disabled
  151. */
  152. function l10n_update_languages_changed_submit($form, $form_state) {
  153. module_load_include('check.inc', 'l10n_update');
  154. $langcode = $form_state['values']['langcode'];
  155. l10n_update_language_refresh(array($langcode));
  156. }
  157. /**
  158. * Additional submit handler for language deletion form.
  159. *
  160. * When a language is deleted, the file history of this language is cleared.
  161. */
  162. function l10n_update_languages_delete_submit($form, $form_state) {
  163. $langcode = $form_state['values']['langcode'];
  164. module_load_include('inc', 'l10n_update');
  165. l10n_update_delete_file_history($langcode);
  166. }
  167. /**
  168. * Additional submit handler for locale and i18n_string translation edit form.
  169. *
  170. * Mark locally edited translations as customized.
  171. *
  172. * @see l10n_update_form_alter()
  173. */
  174. function l10n_update_locale_translate_edit_form_submit($form, &$form_state) {
  175. module_load_include('inc', 'l10n_update');
  176. $lid = $form_state['values']['lid'];
  177. foreach ($form_state['values']['translations'] as $langcode => $value) {
  178. if (!empty($value) && $value != $form_state['complete form']['translations'][$langcode]['#default_value']) {
  179. // An update has been made, mark the string as customized.
  180. db_update('locales_target')
  181. ->fields(array('l10n_status' => L10N_UPDATE_STRING_CUSTOM))
  182. ->condition('lid', $lid)
  183. ->condition('language', $langcode)
  184. ->execute();
  185. }
  186. }
  187. }
  188. /**
  189. * Menu callback. Saves a string translation coming as POST data.
  190. */
  191. function l10n_update_client_save_string() {
  192. global $user, $language;
  193. if (l10n_client_access()) {
  194. if (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['textgroup']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
  195. // Ensure we have this source string before we attempt to save it.
  196. // @todo: add actual context support.
  197. $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $_POST['source'], ':context' => '', ':textgroup' => $_POST['textgroup']))->fetchField();
  198. if (!empty($lid)) {
  199. module_load_include('inc', 'l10n_update');
  200. $report = array('skips' => 0, 'additions' => 0, 'updates' => 0, 'deletes' => 0);
  201. // @todo: add actual context support.
  202. _l10n_update_locale_import_one_string_db($report, $language->language, '', $_POST['source'], $_POST['target'], $_POST['textgroup'], NULL, LOCALE_IMPORT_OVERWRITE, L10N_UPDATE_STRING_CUSTOM);
  203. cache_clear_all('locale:', 'cache', TRUE);
  204. _locale_invalidate_js($language->language);
  205. if (!empty($report['skips'])) {
  206. $message = theme('l10n_client_message', array('message' => t('Not saved locally due to invalid HTML content.')));
  207. }
  208. elseif (!empty($report['additions']) || !empty($report['updates'])) {
  209. $message = theme('l10n_client_message', array('message' => t('Translation saved locally.'), 'level' => WATCHDOG_INFO));
  210. }
  211. elseif (!empty($report['deletes'])) {
  212. $message = theme('l10n_client_message', array('message' => t('Translation successfuly removed locally.'), 'level' => WATCHDOG_INFO));
  213. }
  214. else {
  215. $message = theme('l10n_client_message', array('message' => t('Unknown error while saving translation locally.')));
  216. }
  217. // Submit to remote server if enabled.
  218. if (variable_get('l10n_client_use_server', FALSE) && user_access('submit translations to localization server') && ($_POST['textgroup'] == 'default')) {
  219. if (!empty($user->data['l10n_client_key'])) {
  220. $remote_result = l10n_client_submit_translation($language->language, $_POST['source'], $_POST['target'], $user->data['l10n_client_key'], l10n_client_user_token($user));
  221. $message .= theme('l10n_client_message', array('message' => $remote_result[1], 'level' => $remote_result[0] ? WATCHDOG_INFO : WATCHDOG_ERROR));
  222. }
  223. else {
  224. $server_url = variable_get('l10n_client_server', 'http://localize.drupal.org');
  225. $user_edit_url = url('user/' . $user->uid . '/edit', array('absolute' => TRUE));
  226. $message .= theme('l10n_client_message', array('message' => t('You could share your work with !l10n_server if you set your API key at !user_link.', array('!l10n_server' => l($server_url, $server_url), '!user_link' => l($user_edit_url, 'user/' . $user->uid . '/edit'))), 'level' => WATCHDOG_WARNING));
  227. }
  228. }
  229. }
  230. else {
  231. $message = theme('l10n_client_message', array('message' => t('Not saved due to source string missing.')));
  232. }
  233. }
  234. else {
  235. $message = theme('l10n_client_message', array('message' => t('Not saved due to missing form values.')));
  236. }
  237. }
  238. else {
  239. $message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.')));
  240. }
  241. drupal_json_output($message);
  242. exit;
  243. }
  244. /**
  245. * Get stored list of projects
  246. *
  247. * @param boolean $refresh
  248. * TRUE = refresh the project data.
  249. * @param boolean $disabled
  250. * TRUE = get enabled AND disabled projects.
  251. * FALSE = get enabled projects only.
  252. *
  253. * @return array
  254. * Array of project objects keyed by project name.
  255. */
  256. function l10n_update_get_projects($refresh = FALSE, $disabled = FALSE) {
  257. static $projects, $enabled;
  258. if (!isset($projects) || $refresh) {
  259. if (variable_get('l10n_update_rebuild_projects', 0)) {
  260. module_load_include('project.inc', 'l10n_update');
  261. variable_del('l10n_update_rebuild_projects');
  262. l10n_update_build_projects();
  263. }
  264. $projects = $enabled = array();
  265. $result = db_query('SELECT * FROM {l10n_update_project}');
  266. foreach ($result as $project) {
  267. $projects[$project->name] = $project;
  268. if ($project->status) {
  269. $enabled[$project->name] = $project;
  270. }
  271. }
  272. }
  273. return $disabled ? $projects : $enabled;
  274. }
  275. /**
  276. * Get server information, that can come from different sources.
  277. *
  278. * - From server list provided by modules. They can provide full server information or just the url
  279. * - From server_url in a project, we'll fetch latest data from the server itself
  280. *
  281. * @param string $name
  282. * Server name e.g. localize.drupal.org
  283. * @param string $url
  284. * Server url
  285. * @param boolean $refresh
  286. * TRUE = refresh the server data.
  287. *
  288. * @return array
  289. * Array of server data.
  290. */
  291. function l10n_update_server($name = NULL, $url = NULL, $refresh = FALSE) {
  292. static $info, $server_list;
  293. // Retrieve server list from modules
  294. if (!isset($server_list) || $refresh) {
  295. $server_list = module_invoke_all('l10n_servers');
  296. }
  297. // We need at least the server url to fetch all the information
  298. if (!$url && $name && isset($server_list[$name])) {
  299. $url = $server_list[$name]['server_url'];
  300. }
  301. // If we still don't have an url, cannot find this server, return false
  302. if (!$url) {
  303. return FALSE;
  304. }
  305. // Cache server information based on the url, refresh if asked
  306. $cid = 'l10n_update_server:' . $url;
  307. if ($refresh) {
  308. unset($info);
  309. cache_clear_all($cid, 'cache_l10n_update');
  310. }
  311. if (!isset($info[$url])) {
  312. if ($cache = cache_get($cid, 'cache_l10n_update')) {
  313. $info[$url] = $cache->data;
  314. }
  315. else {
  316. module_load_include('parser.inc', 'l10n_update');
  317. if ($name && !empty($server_list[$name])) {
  318. // The name is in our list, it can be full data or just an url
  319. $server = $server_list[$name];
  320. }
  321. else {
  322. // This may be a new server provided by a module / package
  323. $server = array('name' => $name, 'server_url' => $url);
  324. // If searching by name, store the name => url mapping
  325. if ($name) {
  326. $server_list[$name] = $server;
  327. }
  328. }
  329. // Now fetch server meta information form the server itself
  330. if ($server = l10n_update_get_server($server)) {
  331. cache_set($cid, $server, 'cache_l10n_update');
  332. $info[$url] = $server;
  333. }
  334. else {
  335. // If no server information, this will be FALSE. We won't search a server twice
  336. $info[$url] = FALSE;
  337. }
  338. }
  339. }
  340. return $info[$url];
  341. }
  342. /**
  343. * Implements hook_l10n_servers().
  344. *
  345. * @return array
  346. * Array of server data:
  347. * 'name' => server name
  348. * 'server_url' => server url
  349. * 'update_url' => update url
  350. */
  351. function l10n_update_l10n_servers() {
  352. module_load_include('inc', 'l10n_update');
  353. $server = l10n_update_default_server();
  354. return array($server['name'] => $server);
  355. }
  356. /**
  357. * Get update history.
  358. *
  359. * @param boolean $refresh
  360. * TRUE = refresh the history data.
  361. * @return
  362. * An array of translation files indexed by project and language.
  363. */
  364. function l10n_update_get_history($refresh = NULL) {
  365. static $status;
  366. if ($refresh || !isset($status)) {
  367. // Now add downloads history to projects
  368. $result = db_query("SELECT * FROM {l10n_update_file}");
  369. foreach ($result as $update) {
  370. $status[$update->project][$update->language] = $update;
  371. }
  372. }
  373. return $status;
  374. }
  375. /**
  376. * Get language list.
  377. *
  378. * @return array
  379. * Array of installed language names. English is the source language and
  380. * is therefore not included.
  381. */
  382. function l10n_update_language_list() {
  383. $languages = locale_language_list('name');
  384. // Skip English language
  385. if (isset($languages['en'])) {
  386. unset($languages['en']);
  387. }
  388. return $languages;
  389. }
  390. /**
  391. * Implements hook_theme().
  392. */
  393. function l10n_update_theme() {
  394. return array(
  395. 'l10n_update_project_status' => array(
  396. 'variables' => array('projects' => NULL, 'languages' => NULL, 'history' => NULL, 'available' => NULL, 'updates' => NULL),
  397. 'file' => 'l10n_update.admin.inc',
  398. ),
  399. 'l10n_update_single_project_wrapper' => array(
  400. 'project' => array('project' => NULL, 'project_status' => NULL, 'languages' => NULL, 'history' => NULL, 'updates' => NULL),
  401. 'file' => 'l10n_update.admin.inc',
  402. ),
  403. 'l10n_update_single_project_status' => array(
  404. 'variables' => array('project' => NULL, 'server' => NULL, 'status' => NULL),
  405. 'file' => 'l10n_update.admin.inc',
  406. ),
  407. 'l10n_update_current_release' => array(
  408. 'variables' => array('language' => NULL, 'release' => NULL, 'status' => NULL),
  409. 'file' => 'l10n_update.admin.inc',
  410. ),
  411. 'l10n_update_available_release' => array(
  412. 'variables' => array('release' => NULL),
  413. 'file' => 'l10n_update.admin.inc',
  414. ),
  415. 'l10n_update_version_status' => array(
  416. 'variables' => array('status' => NULL, 'type' => NULL),
  417. 'file' => 'l10n_update.admin.inc',
  418. ),
  419. );
  420. }
  421. /**
  422. * Build the warning message for when there is no data about available updates.
  423. *
  424. * @return sting
  425. * Message text with links.
  426. */
  427. function _l10n_update_no_data() {
  428. $destination = drupal_get_destination();
  429. return t('No information is available about potential new and updated translations for currently installed modules and themes. To check for updates, you may need to <a href="@run_cron">run cron</a> or you can <a href="@check_manually">check manually</a>. Please note that checking for available updates can take a long time, so please be patient.', array(
  430. '@run_cron' => url('admin/reports/status/run-cron', array('query' => $destination)),
  431. '@check_manually' => url('admin/config/regional/translate/update', array('query' => $destination)),
  432. ));
  433. }
  434. /**
  435. * Get available updates.
  436. *
  437. * @param boolean $refresh
  438. * TRUE = refresh the history data.
  439. *
  440. * @return array
  441. * Array of all projects for which updates are available. For each project
  442. * an array of update objects, one per language.
  443. */
  444. function l10n_update_available_updates($refresh = NULL) {
  445. module_load_include('check.inc', 'l10n_update');
  446. if ($available = l10n_update_available_releases($refresh)) {
  447. $history = l10n_update_get_history();
  448. return l10n_update_build_updates($history, $available);
  449. }
  450. }
  451. /**
  452. * Implements hook_flush_caches().
  453. *
  454. * Called from update.php (among others) to flush the caches.
  455. */
  456. function l10n_update_flush_caches() {
  457. if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') {
  458. cache_clear_all('*', 'cache_l10n_update', TRUE);
  459. variable_set('l10n_update_rebuild_projects', 1);
  460. }
  461. return array();
  462. }
  463. /**
  464. * Creates a .htaccess file in the translations directory if it is missing.
  465. */
  466. function l10n_update_ensure_htaccess() {
  467. $directory = variable_get('l10n_update_download_store', '');
  468. if ($directory) {
  469. file_create_htaccess($directory, FALSE);
  470. }
  471. }