l10n_update.compare.inc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <?php
  2. /**
  3. * @file
  4. * The API for comparing project translation status with available translation.
  5. */
  6. /**
  7. * Load common APIs.
  8. */
  9. // @todo Combine functions differently in files to avoid unnecessary includes.
  10. // Follow-up issue http://drupal.org/node/1834298
  11. require_once __DIR__ . '/l10n_update.translation.inc';
  12. /**
  13. * Clear the project data table.
  14. */
  15. function l10n_update_flush_projects() {
  16. db_truncate('l10n_update_project')->execute();
  17. drupal_static_reset('l10n_update_build_projects');
  18. }
  19. /**
  20. * Rebuild project list
  21. *
  22. * @param $refresh
  23. * TRUE: Refresh project list.
  24. *
  25. * @return array
  26. * Array of project objects to be considered for translation update.
  27. */
  28. function l10n_update_build_projects($refresh = FALSE) {
  29. $projects = &drupal_static(__FUNCTION__, array(), $refresh);
  30. if (empty($projects)) {
  31. module_load_include('inc', 'l10n_update');
  32. // Get the project list based on .info files.
  33. $projects = l10n_update_project_list();
  34. // Mark all previous projects as disabled and store new project data.
  35. db_update('l10n_update_project')
  36. ->fields(array(
  37. 'status' => 0,
  38. ))
  39. ->execute();
  40. $default_server = l10n_update_default_translation_server();
  41. if (module_exists('update')) {
  42. $projects_info = update_get_available(TRUE);
  43. }
  44. foreach ($projects as $name => $data) {
  45. // Force update fetch of project data in cases where Drupal's performance
  46. // optimized approach is missing out on some projects.
  47. // @see http://drupal.org/node/1671570#comment-6216090
  48. if (module_exists('update') && !isset($projects_info[$name])) {
  49. module_load_include('fetch.inc', 'update');
  50. _update_process_fetch_task($data);
  51. $available = _update_get_cached_available_releases();
  52. if (!empty($available[$name])) {
  53. $projects_info[$name] = $available[$name];
  54. }
  55. }
  56. if (isset($projects_info[$name]['releases']) && $projects_info[$name]['project_status'] != 'not-fetched') {
  57. // Find out if a dev version is installed.
  58. if (preg_match("/^[0-9]+\.x-([0-9]+)\..*-dev$/", $data['info']['version'], $matches)) {
  59. // Find a suitable release to use as alternative translation.
  60. foreach ($projects_info[$name]['releases'] as $project_release) {
  61. // The first release with the same major release number which is not
  62. // a dev release is the one. Releases are sorted the most recent first.
  63. if ($project_release['version_major'] == $matches[1] &&
  64. (!isset($project_release['version_extra']) || $project_release['version_extra'] != 'dev')) {
  65. $release = $project_release;
  66. break;
  67. }
  68. }
  69. }
  70. if (!empty($release['version'])) {
  71. $data['info']['version'] = $release['version'];
  72. }
  73. unset($release);
  74. }
  75. // Without Update module we do a best effort fallback. A development
  76. // release will fall back to the corresponding release version.
  77. elseif (!isset($projects_info) && isset($data['info']['version'])) {
  78. if (preg_match('/[^x](\+\d+)?-dev$/', $data['info']['version'])) {
  79. $data['info']['version'] = preg_replace('/(\+\d+)?-dev$/', '', $data['info']['version']);
  80. }
  81. }
  82. $data += array(
  83. 'version' => isset($data['info']['version']) ? $data['info']['version'] : '',
  84. 'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY,
  85. 'l10n_path' => isset($data['info']['l10n path']) && $data['info']['l10n path'] ? $data['info']['l10n path'] : $default_server['pattern'],
  86. 'status' => 1,
  87. );
  88. $project = (object) $data;
  89. $projects[$name] = $project;
  90. // Create or update the project record.
  91. db_merge('l10n_update_project')
  92. ->key(array('name' => $project->name))
  93. ->fields(array(
  94. 'name' => $project->name,
  95. 'project_type' => $project->project_type,
  96. 'core' => $project->core,
  97. 'version' => $project->version,
  98. 'l10n_path' => $project->l10n_path,
  99. 'status' => $project->status,
  100. ))
  101. ->execute();
  102. // Invalidate the cache of translatable projects.
  103. l10n_update_clear_cache_projects();
  104. }
  105. }
  106. return $projects;
  107. }
  108. /**
  109. * Get update module's project list
  110. *
  111. * @return array
  112. */
  113. function l10n_update_project_list() {
  114. $projects = array();
  115. $disabled = variable_get('l10n_update_check_disabled', 0);
  116. // Unlike update module, this one has no cache
  117. _l10n_update_project_info_list($projects, system_rebuild_module_data(), 'module', $disabled);
  118. _l10n_update_project_info_list($projects, system_rebuild_theme_data(), 'theme', $disabled);
  119. // Allow other modules to alter projects before fetching and comparing.
  120. drupal_alter('l10n_update_projects', $projects);
  121. return $projects;
  122. }
  123. /**
  124. * Populate an array of project data.
  125. *
  126. * Based on _update_process_info_list()
  127. *
  128. * @param $projects
  129. * @param $list
  130. * @param $project_type
  131. * @param $disabled
  132. * TRUE to include disabled projects too
  133. */
  134. function _l10n_update_project_info_list(&$projects, $list, $project_type, $disabled = FALSE) {
  135. foreach ($list as $file) {
  136. if (!$disabled && empty($file->status)) {
  137. // Skip disabled modules or themes.
  138. continue;
  139. }
  140. // Skip if the .info file is broken.
  141. if (empty($file->info)) {
  142. continue;
  143. }
  144. // If the .info doesn't define the 'project', try to figure it out.
  145. if (!isset($file->info['project'])) {
  146. $file->info['project'] = l10n_update_get_project_name($file);
  147. }
  148. // If the .info defines the 'interface translation project', this value will
  149. // override the 'project' value.
  150. if (isset($file->info['interface translation project'])) {
  151. $file->info['project'] = $file->info['interface translation project'];
  152. }
  153. // If we still don't know the 'project', give up.
  154. if (empty($file->info['project'])) {
  155. continue;
  156. }
  157. // If we don't already know it, grab the change time on the .info file
  158. // itself. Note: we need to use the ctime, not the mtime (modification
  159. // time) since many (all?) tar implementations will go out of their way to
  160. // set the mtime on the files it creates to the timestamps recorded in the
  161. // tarball. We want to see the last time the file was changed on disk,
  162. // which is left alone by tar and correctly set to the time the .info file
  163. // was unpacked.
  164. if (!isset($file->info['_info_file_ctime'])) {
  165. $info_filename = dirname($file->uri) . '/' . $file->name . '.info';
  166. $file->info['_info_file_ctime'] = filectime($info_filename);
  167. }
  168. $project_name = $file->info['project'];
  169. if (!isset($projects[$project_name])) {
  170. // Only process this if we haven't done this project, since a single
  171. // project can have multiple modules or themes.
  172. $projects[$project_name] = array(
  173. 'name' => $project_name,
  174. 'info' => $file->info,
  175. 'datestamp' => isset($file->info['datestamp']) ? $file->info['datestamp'] : 0,
  176. 'includes' => array($file->name => isset($file->info['name']) ? $file->info['name'] : $file->name),
  177. 'project_type' => $project_name == 'drupal' ? 'core' : $project_type,
  178. );
  179. }
  180. else {
  181. $projects[$project_name]['includes'][$file->name] = $file->info['name'];
  182. $projects[$project_name]['info']['_info_file_ctime'] = max($projects[$project_name]['info']['_info_file_ctime'], $file->info['_info_file_ctime']);
  183. }
  184. }
  185. }
  186. /**
  187. * Given a $file object (as returned by system_rebuild_module_data()), figure
  188. * out what project it belongs to.
  189. *
  190. * Based on update_get_project_name().
  191. *
  192. * @param $file
  193. * @return string
  194. * @see system_get_files_database()
  195. */
  196. function l10n_update_get_project_name($file) {
  197. $project_name = '';
  198. if (isset($file->info['project'])) {
  199. $project_name = $file->info['project'];
  200. }
  201. elseif (isset($file->info['package']) && (strpos($file->info['package'], 'Core') === 0)) {
  202. $project_name = 'drupal';
  203. }
  204. return $project_name;
  205. }
  206. /**
  207. * Retrieve data for default server.
  208. *
  209. * @return array
  210. * Array of server parameters:
  211. * - "server_pattern": URI containing po file pattern.
  212. */
  213. function l10n_update_default_translation_server() {
  214. $pattern = variable_get('l10n_update_default_update_url', L10N_UPDATE_DEFAULT_SERVER_PATTERN);
  215. return array(
  216. 'pattern' => $pattern,
  217. );
  218. }
  219. /**
  220. * Check for the latest release of project translations.
  221. *
  222. * @param array $projects
  223. * Array of project names to check. Defaults to all translatable projects.
  224. * @param string $langcodes
  225. * Array of language codes. Defaults to all translatable languages.
  226. *
  227. * @return array
  228. * Available sources indexed by project and language.
  229. */
  230. // @todo Return batch or NULL
  231. function l10n_update_check_projects($projects = array(), $langcodes = array()) {
  232. if (l10n_update_use_remote_source()) {
  233. // Retrieve the status of both remote and local translation sources by
  234. // using a batch process.
  235. l10n_update_check_projects_batch($projects, $langcodes);
  236. }
  237. else {
  238. // Retrieve and save the status of local translations only.
  239. l10n_update_check_projects_local($projects, $langcodes);
  240. variable_set('l10n_update_last_check', REQUEST_TIME);
  241. }
  242. }
  243. /**
  244. * Gets and stores the status and timestamp of remote po files.
  245. *
  246. * A batch process is used to check for po files at remote locations and (when
  247. * configured) to check for po files in the local file system. The most recent
  248. * translation source states are stored in the state variable
  249. * 'l10n_update_translation_status'.
  250. *
  251. * @param array $projects
  252. * Array of project names to check. Defaults to all translatable projects.
  253. * @param string $langcodes
  254. * Array of language codes. Defaults to all translatable languages.
  255. */
  256. function l10n_update_check_projects_batch($projects = array(), $langcodes = array()) {
  257. // Build and set the batch process.
  258. $batch = l10n_update_batch_status_build($projects, $langcodes);
  259. batch_set($batch);
  260. }
  261. /**
  262. * Builds a batch to get the status of remote and local translation files.
  263. *
  264. * The batch process fetches the state of both local and (if configured) remote
  265. * translation files. The data of the most recent translation is stored per
  266. * per project and per language. This data is stored in a state variable
  267. * 'l10n_update_translation_status'. The timestamp it was last updated is stored
  268. * in the state variable 'l10n_upate_last_checked'.
  269. *
  270. * @param array $projects
  271. * Array of project names for which to check the state of translation files.
  272. * Defaults to all translatable projects.
  273. * @param array $langcodes
  274. * Array of language codes. Defaults to all translatable languages.
  275. *
  276. * @return array
  277. * Batch definition array.
  278. */
  279. function l10n_update_batch_status_build($projects = array(), $langcodes = array()) {
  280. $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
  281. $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
  282. $options = _l10n_update_default_update_options();
  283. $operations = _l10n_update_batch_status_operations($projects, $langcodes, $options);
  284. $batch = array(
  285. 'operations' => $operations,
  286. 'title' => t('Checking translations'),
  287. 'progress_message' => '',
  288. 'finished' => 'l10n_update_batch_status_finished',
  289. 'error_message' => t('Error checking translation updates.'),
  290. 'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
  291. );
  292. return $batch;
  293. }
  294. /**
  295. * Helper function to construct batch operations checking remote translation
  296. * status.
  297. *
  298. * @param array $projects
  299. * Array of project names to be processed.
  300. * @param array $langcodes
  301. * Array of language codes.
  302. * @param array $options
  303. * Batch processing options.
  304. *
  305. * @return array
  306. * Array of batch operations.
  307. */
  308. function _l10n_update_batch_status_operations($projects, $langcodes, $options = array()) {
  309. $operations = array();
  310. foreach ($projects as $project) {
  311. foreach ($langcodes as $langcode) {
  312. // Check status of local and remote translation sources.
  313. $operations[] = array('l10n_update_batch_status_check', array($project, $langcode, $options));
  314. }
  315. }
  316. return $operations;
  317. }
  318. /**
  319. * Check and store the status and timestamp of local po files.
  320. *
  321. * Only po files in the local file system are checked. Any remote translation
  322. * files will be ignored.
  323. *
  324. * Projects may contain a server_pattern option containing a pattern of the
  325. * path to the po source files. If no server_pattern is defined the default
  326. * translation directory is checked for the po file. When a server_pattern is
  327. * defined the specified location is checked. The server_pattern can be set in
  328. * the module's .info.yml file or by using
  329. * hook_l10n_update_projects_alter().
  330. *
  331. * @param array $projects
  332. * Array of project names for which to check the state of translation files.
  333. * Defaults to all translatable projects.
  334. * @param array $langcodes
  335. * Array of language codes. Defaults to all translatable languages.
  336. */
  337. function l10n_update_check_projects_local($projects = array(), $langcodes = array()) {
  338. $projects = l10n_update_get_projects($projects);
  339. $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
  340. // For each project and each language we check if a local po file is
  341. // available. When found the source object is updated with the appropriate
  342. // type and timestamp of the po file.
  343. foreach ($projects as $name => $project) {
  344. foreach ($langcodes as $langcode) {
  345. $source = l10n_update_source_build($project, $langcode);
  346. if ($file = l10n_update_source_check_file($source)) {
  347. l10n_update_status_save($name, $langcode, L10N_UPDATE_LOCAL, $file);
  348. }
  349. }
  350. }
  351. }