l10n_update.module 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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. $menu['l10n_client/save']['page callback'] = 'l10n_update_client_save_string';
  80. }
  81. /**
  82. * Implements hook_cron().
  83. *
  84. * Check one project/language at a time, download and import if update available
  85. */
  86. function l10n_update_cron() {
  87. if ($frequency = variable_get('l10n_update_check_frequency', 0)) {
  88. module_load_include('check.inc', 'l10n_update');
  89. list($checked, $updated) = l10n_update_check_translations(L10N_UPDATE_CRON_PROJECTS, REQUEST_TIME - $frequency * 24 * 3600, L10N_UPDATE_CRON_UPDATES);
  90. watchdog('l10n_update', 'Automatically checked @checked translations, updated @updated.', array('@checked' => count($checked), '@updated' => count($updated)));
  91. }
  92. }
  93. /**
  94. * Implements hook_form_alter().
  95. */
  96. function l10n_update_form_alter(&$form, $form_state, $form_id) {
  97. switch ($form_id) {
  98. case 'locale_translate_edit_form':
  99. // Replace the submit callback by our own customized version
  100. $form['#submit'] = array('l10n_update_locale_translate_edit_form_submit');
  101. break;
  102. case 'locale_languages_predefined_form':
  103. case 'locale_languages_custom_form':
  104. $form['#submit'][] = 'l10n_update_languages_changed_submit';
  105. break;
  106. case 'locale_languages_delete_form':
  107. // A language is being deleted.
  108. $form['#submit'][] = 'l10n_update_languages_delete_submit';
  109. break;
  110. }
  111. }
  112. /**
  113. * Implements hook_modules_enabled().
  114. *
  115. * Refresh project translation status and get translations if required.
  116. */
  117. function l10n_update_modules_enabled($modules) {
  118. module_load_include('project.inc', 'l10n_update');
  119. l10n_update_project_refresh($modules);
  120. }
  121. /**
  122. * Implements hook_modules_uninstalled().
  123. *
  124. * Remove data of uninstalled modules from {l10n_update_file} table and
  125. * rebuild the projects cache.
  126. */
  127. function l10n_update_modules_uninstalled($modules) {
  128. db_delete('l10n_update_file')
  129. ->condition('project', $modules)
  130. ->execute();
  131. // Rebuild {l10n_update_project} table.
  132. // Just like the system table, the project table holds both enabled and
  133. // disabled projects. Full control over its content is not possible.
  134. // To minimize polution we flush it here. The cost of rebuilding is small
  135. // compared to the {l10n_update_file} table.
  136. db_delete('l10n_update_project')->execute();
  137. module_load_include('project.inc', 'l10n_update');
  138. l10n_update_build_projects();
  139. }
  140. /**
  141. * Aditional submit handler for language forms
  142. *
  143. * We need to refresh status when a new language is enabled / disabled
  144. */
  145. function l10n_update_languages_changed_submit($form, $form_state) {
  146. module_load_include('check.inc', 'l10n_update');
  147. $langcode = $form_state['values']['langcode'];
  148. l10n_update_language_refresh(array($langcode));
  149. }
  150. /**
  151. * Additional submit handler for language deletion form.
  152. *
  153. * When a language is deleted, the file history of this language is cleared.
  154. */
  155. function l10n_update_languages_delete_submit($form, $form_state) {
  156. $langcode = $form_state['values']['langcode'];
  157. module_load_include('inc', 'l10n_update');
  158. l10n_update_delete_file_history($langcode);
  159. }
  160. /**
  161. * Replacement submit handler for translation edit form.
  162. *
  163. * Process string editing form submissions marking translations as customized.
  164. * Saves all translations of one string submitted from a form.
  165. *
  166. * @see l10n_update_form_alter()
  167. * @todo Just mark as customized when string changed.
  168. */
  169. function l10n_update_locale_translate_edit_form_submit($form, &$form_state) {
  170. module_load_include('inc', 'l10n_update');
  171. $lid = $form_state['values']['lid'];
  172. foreach ($form_state['values']['translations'] as $key => $value) {
  173. $translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField();
  174. if (!empty($value)) {
  175. // Only update or insert if we have a value to use.
  176. if (!empty($translation)) {
  177. db_update('locales_target')
  178. ->fields(array(
  179. 'translation' => $value,
  180. 'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
  181. ))
  182. ->condition('lid', $lid)
  183. ->condition('language', $key)
  184. ->execute();
  185. }
  186. else {
  187. db_insert('locales_target')
  188. ->fields(array(
  189. 'lid' => $lid,
  190. 'translation' => $value,
  191. 'language' => $key,
  192. 'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
  193. ))
  194. ->execute();
  195. }
  196. }
  197. elseif (!empty($translation)) {
  198. // Empty translation entered: remove existing entry from database.
  199. db_delete('locales_target')
  200. ->condition('lid', $lid)
  201. ->condition('language', $key)
  202. ->execute();
  203. }
  204. // Force JavaScript translation file recreation for this language.
  205. _locale_invalidate_js($key);
  206. }
  207. drupal_set_message(t('The string has been saved.'));
  208. // Clear locale cache.
  209. _locale_invalidate_js();
  210. cache_clear_all('locale:', 'cache', TRUE);
  211. $form_state['redirect'] = 'admin/config/regional/translate/translate';
  212. return;
  213. }
  214. /**
  215. * Menu callback. Saves a string translation coming as POST data.
  216. */
  217. function l10n_update_client_save_string() {
  218. global $user, $language;
  219. if (l10n_client_access()) {
  220. if (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['textgroup']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
  221. // Ensure we have this source string before we attempt to save it.
  222. // @todo: add actual context support.
  223. $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();
  224. if (!empty($lid)) {
  225. module_load_include('inc', 'l10n_update');
  226. $report = array('skips' => 0, 'additions' => 0, 'updates' => 0, 'deletes' => 0);
  227. // @todo: add actual context support.
  228. _l10n_update_locale_import_one_string_db($report, $language->language, '', $_POST['source'], $_POST['target'], $_POST['textgroup'], NULL, LOCALE_IMPORT_OVERWRITE, L10N_UPDATE_STRING_CUSTOM);
  229. cache_clear_all('locale:', 'cache', TRUE);
  230. _locale_invalidate_js($language->language);
  231. if (!empty($report['skips'])) {
  232. $message = theme('l10n_client_message', array('message' => t('Not saved locally due to invalid HTML content.')));
  233. }
  234. elseif (!empty($report['additions']) || !empty($report['updates'])) {
  235. $message = theme('l10n_client_message', array('message' => t('Translation saved locally.'), 'level' => WATCHDOG_INFO));
  236. }
  237. elseif (!empty($report['deletes'])) {
  238. $message = theme('l10n_client_message', array('message' => t('Translation successfuly removed locally.'), 'level' => WATCHDOG_INFO));
  239. }
  240. else {
  241. $message = theme('l10n_client_message', array('message' => t('Unknown error while saving translation locally.')));
  242. }
  243. // Submit to remote server if enabled.
  244. if (variable_get('l10n_client_use_server', FALSE) && user_access('submit translations to localization server') && ($_POST['textgroup'] == 'default')) {
  245. if (!empty($user->data['l10n_client_key'])) {
  246. $remote_result = l10n_client_submit_translation($language->language, $_POST['source'], $_POST['target'], $user->data['l10n_client_key'], l10n_client_user_token($user));
  247. $message .= theme('l10n_client_message', array('message' => $remote_result[1], 'level' => $remote_result[0] ? WATCHDOG_INFO : WATCHDOG_ERROR));
  248. }
  249. else {
  250. $server_url = variable_get('l10n_client_server', 'http://localize.drupal.org');
  251. $user_edit_url = url('user/' . $user->uid . '/edit', array('absolute' => TRUE));
  252. $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));
  253. }
  254. }
  255. }
  256. else {
  257. $message = theme('l10n_client_message', array('message' => t('Not saved due to source string missing.')));
  258. }
  259. }
  260. else {
  261. $message = theme('l10n_client_message', array('message' => t('Not saved due to missing form values.')));
  262. }
  263. }
  264. else {
  265. $message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.')));
  266. }
  267. drupal_json_output($message);
  268. exit;
  269. }
  270. /**
  271. * Get stored list of projects
  272. *
  273. * @param boolean $refresh
  274. * TRUE = refresh the project data.
  275. * @param boolean $disabled
  276. * TRUE = get enabled AND disabled projects.
  277. * FALSE = get enabled projects only.
  278. *
  279. * @return array
  280. * Array of project objects keyed by project name.
  281. */
  282. function l10n_update_get_projects($refresh = FALSE, $disabled = FALSE) {
  283. static $projects, $enabled;
  284. if (!isset($projects) || $refresh) {
  285. if (variable_get('l10n_update_rebuild_projects', 0)) {
  286. module_load_include('project.inc', 'l10n_update');
  287. variable_del('l10n_update_rebuild_projects');
  288. l10n_update_build_projects();
  289. }
  290. $projects = $enabled = array();
  291. $result = db_query('SELECT * FROM {l10n_update_project}');
  292. foreach ($result as $project) {
  293. $projects[$project->name] = $project;
  294. if ($project->status) {
  295. $enabled[$project->name] = $project;
  296. }
  297. }
  298. }
  299. return $disabled ? $projects : $enabled;
  300. }
  301. /**
  302. * Get server information, that can come from different sources.
  303. *
  304. * - From server list provided by modules. They can provide full server information or just the url
  305. * - From server_url in a project, we'll fetch latest data from the server itself
  306. *
  307. * @param string $name
  308. * Server name e.g. localize.drupal.org
  309. * @param string $url
  310. * Server url
  311. * @param boolean $refresh
  312. * TRUE = refresh the server data.
  313. *
  314. * @return array
  315. * Array of server data.
  316. */
  317. function l10n_update_server($name = NULL, $url = NULL, $refresh = FALSE) {
  318. static $info, $server_list;
  319. // Retrieve server list from modules
  320. if (!isset($server_list) || $refresh) {
  321. $server_list = module_invoke_all('l10n_servers');
  322. }
  323. // We need at least the server url to fetch all the information
  324. if (!$url && $name && isset($server_list[$name])) {
  325. $url = $server_list[$name]['server_url'];
  326. }
  327. // If we still don't have an url, cannot find this server, return false
  328. if (!$url) {
  329. return FALSE;
  330. }
  331. // Cache server information based on the url, refresh if asked
  332. $cid = 'l10n_update_server:' . $url;
  333. if ($refresh) {
  334. unset($info);
  335. cache_clear_all($cid, 'cache_l10n_update');
  336. }
  337. if (!isset($info[$url])) {
  338. if ($cache = cache_get($cid, 'cache_l10n_update')) {
  339. $info[$url] = $cache->data;
  340. }
  341. else {
  342. module_load_include('parser.inc', 'l10n_update');
  343. if ($name && !empty($server_list[$name])) {
  344. // The name is in our list, it can be full data or just an url
  345. $server = $server_list[$name];
  346. }
  347. else {
  348. // This may be a new server provided by a module / package
  349. $server = array('name' => $name, 'server_url' => $url);
  350. // If searching by name, store the name => url mapping
  351. if ($name) {
  352. $server_list[$name] = $server;
  353. }
  354. }
  355. // Now fetch server meta information form the server itself
  356. if ($server = l10n_update_get_server($server)) {
  357. cache_set($cid, $server, 'cache_l10n_update');
  358. $info[$url] = $server;
  359. }
  360. else {
  361. // If no server information, this will be FALSE. We won't search a server twice
  362. $info[$url] = FALSE;
  363. }
  364. }
  365. }
  366. return $info[$url];
  367. }
  368. /**
  369. * Implements hook_l10n_servers().
  370. *
  371. * @return array
  372. * Array of server data:
  373. * 'name' => server name
  374. * 'server_url' => server url
  375. * 'update_url' => update url
  376. */
  377. function l10n_update_l10n_servers() {
  378. module_load_include('inc', 'l10n_update');
  379. $server = l10n_update_default_server();
  380. return array($server['name'] => $server);
  381. }
  382. /**
  383. * Get update history.
  384. *
  385. * @param boolean $refresh
  386. * TRUE = refresh the history data.
  387. * @return
  388. * An array of translation files indexed by project and language.
  389. */
  390. function l10n_update_get_history($refresh = NULL) {
  391. static $status;
  392. if ($refresh || !isset($status)) {
  393. // Now add downloads history to projects
  394. $result = db_query("SELECT * FROM {l10n_update_file}");
  395. foreach ($result as $update) {
  396. $status[$update->project][$update->language] = $update;
  397. }
  398. }
  399. return $status;
  400. }
  401. /**
  402. * Get language list.
  403. *
  404. * @return array
  405. * Array of installed language names. English is the source language and
  406. * is therefore not included.
  407. */
  408. function l10n_update_language_list() {
  409. $languages = locale_language_list('name');
  410. // Skip English language
  411. if (isset($languages['en'])) {
  412. unset($languages['en']);
  413. }
  414. return $languages;
  415. }
  416. /**
  417. * Implements hook_theme().
  418. */
  419. function l10n_update_theme() {
  420. return array(
  421. 'l10n_update_project_status' => array(
  422. 'variables' => array('projects' => NULL, 'languages' => NULL, 'history' => NULL, 'available' => NULL, 'updates' => NULL),
  423. 'file' => 'l10n_update.admin.inc',
  424. ),
  425. 'l10n_update_single_project_wrapper' => array(
  426. 'project' => array('project' => NULL, 'project_status' => NULL, 'languages' => NULL, 'history' => NULL, 'updates' => NULL),
  427. 'file' => 'l10n_update.admin.inc',
  428. ),
  429. 'l10n_update_single_project_status' => array(
  430. 'variables' => array('project' => NULL, 'server' => NULL, 'status' => NULL),
  431. 'file' => 'l10n_update.admin.inc',
  432. ),
  433. 'l10n_update_current_release' => array(
  434. 'variables' => array('language' => NULL, 'release' => NULL, 'status' => NULL),
  435. 'file' => 'l10n_update.admin.inc',
  436. ),
  437. 'l10n_update_available_release' => array(
  438. 'variables' => array('release' => NULL),
  439. 'file' => 'l10n_update.admin.inc',
  440. ),
  441. 'l10n_update_version_status' => array(
  442. 'variables' => array('status' => NULL, 'type' => NULL),
  443. 'file' => 'l10n_update.admin.inc',
  444. ),
  445. );
  446. }
  447. /**
  448. * Build the warning message for when there is no data about available updates.
  449. *
  450. * @return sting
  451. * Message text with links.
  452. */
  453. function _l10n_update_no_data() {
  454. $destination = drupal_get_destination();
  455. 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(
  456. '@run_cron' => url('admin/reports/status/run-cron', array('query' => $destination)),
  457. '@check_manually' => url('admin/config/regional/translate/update', array('query' => $destination)),
  458. ));
  459. }
  460. /**
  461. * Get available updates.
  462. *
  463. * @param boolean $refresh
  464. * TRUE = refresh the history data.
  465. *
  466. * @return array
  467. * Array of all projects for which updates are available. For each project
  468. * an array of update objects, one per language.
  469. */
  470. function l10n_update_available_updates($refresh = NULL) {
  471. module_load_include('check.inc', 'l10n_update');
  472. if ($available = l10n_update_available_releases($refresh)) {
  473. $history = l10n_update_get_history();
  474. return l10n_update_build_updates($history, $available);
  475. }
  476. }
  477. /**
  478. * Implements hook_flush_caches().
  479. *
  480. * Called from update.php (among others) to flush the caches.
  481. */
  482. function l10n_update_flush_caches() {
  483. if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') {
  484. cache_clear_all('*', 'cache_l10n_update', TRUE);
  485. variable_set('l10n_update_rebuild_projects', 1);
  486. }
  487. return array();
  488. }