l10n_update.compare.inc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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 https://www.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 bool $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. foreach ($projects as $name => $data) {
  42. // For dev releases, remove the '-dev' part and trust the translation
  43. // server to fall back to the latest stable release for that branch.
  44. if (isset($data['info']['version']) && strpos($data['info']['version'], '-dev')) {
  45. if (preg_match("/^(\d+\.x-\d+\.).*$/", $data['info']['version'], $matches)) {
  46. // Example matches: 7.x-1.x-dev, 7.x-1.0-alpha1+5-dev => 7.x-1.x.
  47. $data['info']['version'] = $matches[1] . 'x';
  48. }
  49. elseif (preg_match("/^(\d+\.).*$/", $data['info']['version'], $matches)) {
  50. // Example match: 7.33-dev => 7.x (Drupal core).
  51. $data['info']['version'] = $matches[1] . 'x';
  52. }
  53. }
  54. $data += array(
  55. 'version' => isset($data['info']['version']) ? $data['info']['version'] : '',
  56. 'core' => isset($data['info']['core']) ? $data['info']['core'] : DRUPAL_CORE_COMPATIBILITY,
  57. 'l10n_path' => isset($data['info']['l10n path']) && $data['info']['l10n path'] ? $data['info']['l10n path'] : $default_server['pattern'],
  58. 'status' => 1,
  59. );
  60. $project = (object) $data;
  61. $projects[$name] = $project;
  62. // Create or update the project record.
  63. db_merge('l10n_update_project')
  64. ->key(array('name' => $project->name))
  65. ->fields(array(
  66. 'name' => $project->name,
  67. 'project_type' => $project->project_type,
  68. 'core' => $project->core,
  69. 'version' => $project->version,
  70. 'l10n_path' => $project->l10n_path,
  71. 'status' => $project->status,
  72. ))
  73. ->execute();
  74. // Invalidate the cache of translatable projects.
  75. l10n_update_clear_cache_projects();
  76. }
  77. }
  78. return $projects;
  79. }
  80. /**
  81. * Get update module's project list.
  82. *
  83. * @return array
  84. * List of projects to be updated.
  85. */
  86. function l10n_update_project_list() {
  87. $projects = array();
  88. $disabled = variable_get('l10n_update_check_disabled', 0);
  89. // Unlike update module, this one has no cache.
  90. _l10n_update_project_info_list($projects, system_rebuild_module_data(), 'module', $disabled);
  91. _l10n_update_project_info_list($projects, system_rebuild_theme_data(), 'theme', $disabled);
  92. // Allow other modules to alter projects before fetching and comparing.
  93. drupal_alter('l10n_update_projects', $projects);
  94. l10n_update_remove_disabled_projects($projects);
  95. return $projects;
  96. }
  97. /**
  98. * Removes disabled projects from the project list.
  99. *
  100. * @param array $projects
  101. * Array of projects keyed by the project machine name.
  102. */
  103. function l10n_update_remove_disabled_projects(&$projects) {
  104. $disabled_projects = variable_get('l10n_update_disabled_projects', array());
  105. foreach ($disabled_projects as $disabled_name) {
  106. // Remove projects with matching name either by full string of by wild card.
  107. if (strpos($disabled_name, '*') !== FALSE) {
  108. $pattern = str_replace('*', '.*?', $disabled_name);
  109. $matches = preg_grep('/^' . $pattern . '$/i', array_keys($projects));
  110. foreach ($matches as $match) {
  111. unset($projects[$match]);
  112. }
  113. }
  114. elseif (isset($projects[$disabled_name])) {
  115. unset($projects[$disabled_name]);
  116. }
  117. }
  118. }
  119. /**
  120. * Populate an array of project data.
  121. *
  122. * Based on _update_process_info_list()
  123. *
  124. * @param array $projects
  125. * The list of projects to populate.
  126. * @param array $list
  127. * System module list as returned by system_rebuild_module_data() or
  128. * system_rebuild_theme_data().
  129. * @param string $project_type
  130. * The project type to process: 'theme' or 'module'.
  131. * @param bool $disabled
  132. * TRUE to include disabled projects too.
  133. */
  134. function _l10n_update_project_info_list(array &$projects, array $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 figure out what project it belongs to.
  188. *
  189. * Uses a file object as returned by system_rebuild_module_data(). Based on
  190. * update_get_project_name().
  191. *
  192. * @param object $file
  193. * Project info file object.
  194. *
  195. * @return string
  196. * The project's machine name this file belongs to.
  197. *
  198. * @see system_get_files_database()
  199. * @see update_get_project_name()
  200. */
  201. function l10n_update_get_project_name($file) {
  202. $project_name = '';
  203. if (isset($file->info['project'])) {
  204. $project_name = $file->info['project'];
  205. }
  206. elseif (isset($file->info['package']) && (strpos($file->info['package'], 'Core') === 0)) {
  207. $project_name = 'drupal';
  208. }
  209. return $project_name;
  210. }
  211. /**
  212. * Retrieve data for default server.
  213. *
  214. * @return array
  215. * Array of server parameters:
  216. * - "server_pattern": URI containing po file pattern.
  217. */
  218. function l10n_update_default_translation_server() {
  219. $pattern = variable_get('l10n_update_default_update_url', L10N_UPDATE_DEFAULT_SERVER_PATTERN);
  220. return array(
  221. 'pattern' => $pattern,
  222. );
  223. }
  224. /**
  225. * Check for the latest release of project translations.
  226. *
  227. * @param array $projects
  228. * Array of project names to check. Defaults to all translatable projects.
  229. * @param string $langcodes
  230. * Array of language codes. Defaults to all translatable languages.
  231. *
  232. * @todo Return batch array or NULL
  233. */
  234. function l10n_update_check_projects($projects = array(), $langcodes = array()) {
  235. if (l10n_update_use_remote_source()) {
  236. // Retrieve the status of both remote and local translation sources by
  237. // using a batch process.
  238. l10n_update_check_projects_batch($projects, $langcodes);
  239. }
  240. else {
  241. // Retrieve and save the status of local translations only.
  242. l10n_update_check_projects_local($projects, $langcodes);
  243. variable_set('l10n_update_last_check', REQUEST_TIME);
  244. }
  245. }
  246. /**
  247. * Gets and stores the status and timestamp of remote po files.
  248. *
  249. * A batch process is used to check for po files at remote locations and (when
  250. * configured) to check for po files in the local file system. The most recent
  251. * translation source states are stored in the state variable
  252. * 'l10n_update_translation_status'.
  253. *
  254. * @param array $projects
  255. * Array of project names to check. Defaults to all translatable projects.
  256. * @param array $langcodes
  257. * Array of language codes. Defaults to all translatable languages.
  258. */
  259. function l10n_update_check_projects_batch($projects = array(), $langcodes = array()) {
  260. // Build and set the batch process.
  261. $batch = l10n_update_batch_status_build($projects, $langcodes);
  262. batch_set($batch);
  263. }
  264. /**
  265. * Builds a batch to get the status of remote and local translation files.
  266. *
  267. * The batch process fetches the state of both local and (if configured) remote
  268. * translation files. The data of the most recent translation is stored per
  269. * per project and per language. This data is stored in a state variable
  270. * 'l10n_update_translation_status'. The timestamp it was last updated is stored
  271. * in the state variable 'l10n_upate_last_checked'.
  272. *
  273. * @param array $projects
  274. * Array of project names for which to check the state of translation files.
  275. * Defaults to all translatable projects.
  276. * @param array $langcodes
  277. * Array of language codes. Defaults to all translatable languages.
  278. *
  279. * @return array
  280. * Batch definition array.
  281. */
  282. function l10n_update_batch_status_build($projects = array(), $langcodes = array()) {
  283. $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
  284. $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
  285. $options = _l10n_update_default_update_options();
  286. $operations = _l10n_update_batch_status_operations($projects, $langcodes, $options);
  287. $batch = array(
  288. 'operations' => $operations,
  289. 'title' => t('Checking translations'),
  290. 'progress_message' => '',
  291. 'finished' => 'l10n_update_batch_status_finished',
  292. 'error_message' => t('Error checking translation updates.'),
  293. 'file' => drupal_get_path('module', 'l10n_update') . '/l10n_update.batch.inc',
  294. );
  295. return $batch;
  296. }
  297. /**
  298. * Constructs batch operations checking remote translation status.
  299. *
  300. * @param array $projects
  301. * Array of project names to be processed.
  302. * @param array $langcodes
  303. * Array of language codes.
  304. * @param array $options
  305. * Batch processing options.
  306. *
  307. * @return array
  308. * Array of batch operations.
  309. */
  310. function _l10n_update_batch_status_operations(array $projects, array $langcodes, array $options = array()) {
  311. $operations = array();
  312. foreach ($projects as $project) {
  313. foreach ($langcodes as $langcode) {
  314. // Check status of local and remote translation sources.
  315. $operations[] = array(
  316. 'l10n_update_batch_status_check',
  317. array(
  318. $project,
  319. $langcode,
  320. $options,
  321. ),
  322. );
  323. }
  324. }
  325. return $operations;
  326. }
  327. /**
  328. * Check and store the status and timestamp of local po files.
  329. *
  330. * Only po files in the local file system are checked. Any remote translation
  331. * files will be ignored.
  332. *
  333. * Projects may contain a server_pattern option containing a pattern of the
  334. * path to the po source files. If no server_pattern is defined the default
  335. * translation directory is checked for the po file. When a server_pattern is
  336. * defined the specified location is checked. The server_pattern can be set in
  337. * the module's .info.yml file or by using
  338. * hook_l10n_update_projects_alter().
  339. *
  340. * @param array $projects
  341. * Array of project names for which to check the state of translation files.
  342. * Defaults to all translatable projects.
  343. * @param array $langcodes
  344. * Array of language codes. Defaults to all translatable languages.
  345. */
  346. function l10n_update_check_projects_local($projects = array(), $langcodes = array()) {
  347. $projects = l10n_update_get_projects($projects);
  348. $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
  349. // For each project and each language we check if a local po file is
  350. // available. When found the source object is updated with the appropriate
  351. // type and timestamp of the po file.
  352. foreach ($projects as $name => $project) {
  353. foreach ($langcodes as $langcode) {
  354. $source = l10n_update_source_build($project, $langcode);
  355. if ($file = l10n_update_source_check_file($source)) {
  356. l10n_update_status_save($name, $langcode, L10N_UPDATE_LOCAL, $file);
  357. }
  358. }
  359. }
  360. }