PageRenderTime 61ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/class-taxonomy.php

https://github.com/humanmade/babble
PHP | 949 lines | 541 code | 144 blank | 264 comment | 117 complexity | cd5c2eb44328c4191f3db0312413116e MD5 | raw file
  1. <?php
  2. /**
  3. * Manages the translations for taxonomies.
  4. *
  5. * @package Babble
  6. * @since Alpha 1.2
  7. */
  8. class Babble_Taxonomies extends Babble_Plugin {
  9. /**
  10. * A simple flag to stop infinite recursion in various places.
  11. *
  12. * @var boolean
  13. **/
  14. protected $no_recursion;
  15. /**
  16. * The current version for purposes of rewrite rules, any
  17. * DB updates, cache busting, etc
  18. *
  19. * @var int
  20. **/
  21. protected $version = 1;
  22. /**
  23. * The shadow taxonomies created to handle the translated terms.
  24. *
  25. * @var array
  26. **/
  27. protected $taxonomies;
  28. /**
  29. * The languages represented by each of the shadow taxonomies.
  30. *
  31. * @var array
  32. **/
  33. protected $lang_map;
  34. /**
  35. * Setup any add_action or add_filter calls. Initiate properties.
  36. *
  37. * @return void
  38. **/
  39. public function __construct() {
  40. $this->setup( 'babble-taxonomy', 'plugin' );
  41. $this->add_action( 'bbl_created_new_shadow_post', 'created_new_shadow_post', null, 2 );
  42. $this->add_action( 'bbl_registered_shadow_post_types', 'registered_shadow_post_types' );
  43. $this->add_action( 'init', 'init_early', 0 );
  44. $this->add_action( 'parse_request' );
  45. $this->add_action( 'registered_taxonomy', null, null, 3 );
  46. $this->add_action( 'save_post', null, null, 2 );
  47. $this->add_action( 'set_object_terms', null, null, 5 );
  48. $this->add_filter( 'get_terms' );
  49. $this->add_filter( 'term_link', null, null, 3 );
  50. $this->add_filter( 'bbl_translated_taxonomy', null, null, 2 );
  51. $this->add_filter( 'admin_body_class' );
  52. }
  53. // WP HOOKS
  54. // ========
  55. /**
  56. * Hooks the WP init action early
  57. *
  58. * @return void
  59. **/
  60. public function init_early() {
  61. // This translation will connect each term with it's translated equivalents
  62. register_taxonomy( 'term_translation', 'term', array(
  63. 'rewrite' => false,
  64. 'public' => true, # ?
  65. 'show_ui' => true, # ?
  66. 'show_in_nav_menus' => false,
  67. 'label' => __( 'Term Translation ID', 'babble' ),
  68. ) );
  69. }
  70. /**
  71. * Hooks the WP registered_taxonomy action
  72. *
  73. * @param string $taxonomy The name of the newly registered taxonomy
  74. * @param string|array $args The object_type(s)
  75. * @param array $args The args passed to register the taxonomy
  76. * @return void
  77. **/
  78. public function registered_taxonomy( $taxonomy, $object_type, $args ) {
  79. if ( in_array( $taxonomy, $this->ignored_taxonomies() ) ) {
  80. return;
  81. }
  82. if ( $this->no_recursion ) {
  83. return;
  84. }
  85. $this->no_recursion = true;
  86. if ( ! is_array( $object_type ) ) {
  87. $object_type = array_unique( (array) $object_type );
  88. }
  89. // Use the Babble term counting function, unless the taxonomy registrant
  90. // has defined their own – in which case we'll just have to hope against
  91. // hope that it's Babble aware :S
  92. // FIXME: Setting this in the following fashion seems hacky… I feel uncomfortable.
  93. if ( empty( $GLOBALS[ 'wp_taxonomies' ][ $taxonomy ]->update_count_callback ) ) {
  94. $GLOBALS[ 'wp_taxonomies' ][ $taxonomy ]->update_count_callback = array( & $this, 'update_post_term_count' );
  95. }
  96. // Untranslated taxonomies do not have shadow equivalents in each language,
  97. // but do apply to the bast post_type and all it's shadow post_types.
  98. if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
  99. // Apply this taxonomy to all the shadow post types
  100. // of all of the base post_types it applies to.
  101. foreach ( $object_type as $ot ) {
  102. if ( ! ( $base_post_type = bbl_get_base_post_type( $ot ) ) ) {
  103. continue;
  104. }
  105. $shadow_post_types = bbl_get_shadow_post_types( $base_post_type );
  106. foreach ( $shadow_post_types as $shadow_post_type ) {
  107. register_taxonomy_for_object_type( $taxonomy, $shadow_post_type );
  108. }
  109. }
  110. $this->no_recursion = false;
  111. return;
  112. }
  113. // @FIXME: Not sure this is the best way to specify languages
  114. $langs = bbl_get_active_langs();
  115. // Lose the default language as any existing taxonomies are in that language
  116. unset( $langs[ bbl_get_default_lang_url_prefix() ] );
  117. // @FIXME: Is it reckless to convert ALL object instances in $args to an array?
  118. foreach ( $args as $key => & $arg ) {
  119. if ( is_object( $arg ) )
  120. $arg = get_object_vars( $arg );
  121. // Don't set any args reserved for built-in post_types
  122. if ( '_' == substr( $key, 0, 1 ) )
  123. unset( $args[ $key ] );
  124. }
  125. #$args[ 'rewrite' ] = false;
  126. unset( $args[ 'name' ] );
  127. unset( $args[ 'object_type' ] );
  128. $slug = ( $args[ 'rewrite' ][ 'slug' ] ) ? $args[ 'rewrite' ][ 'slug' ] : $taxonomy;
  129. foreach ( $langs as $lang ) {
  130. $new_args = $args;
  131. $new_object_type = array();
  132. // N.B. Here we assume that the taxonomy is on a post type
  133. foreach( $object_type as $ot )
  134. $new_object_type[] = bbl_get_post_type_in_lang( $ot, $lang->code );
  135. if ( false !== $args[ 'rewrite' ] ) {
  136. if ( ! is_array( $new_args[ 'rewrite' ] ) )
  137. $new_args[ 'rewrite' ] = array();
  138. // Do I not need to add this query_var into the query_vars filter? It seems not.
  139. $new_args[ 'query_var' ] = $new_args[ 'rewrite' ][ 'slug' ] = $this->get_slug_in_lang( $slug, $lang->code );
  140. }
  141. // @FIXME: Note currently we are in danger of a taxonomy name being longer than 32 chars
  142. // Perhaps we need to create some kind of map like (taxonomy) + (lang) => (shadow translated taxonomy)
  143. $new_taxonomy = strtolower( "{$taxonomy}_{$lang->code}" );
  144. $this->taxonomies[ $new_taxonomy ] = $taxonomy;
  145. if ( ! isset( $this->lang_map[ $lang->code ] ) || ! is_array( $this->lang_map[ $lang->code ] ) )
  146. $this->lang_map[ $lang->code ] = array();
  147. $this->lang_map[ $lang->code ][ $taxonomy ] = $new_taxonomy;
  148. register_taxonomy( $new_taxonomy, $new_object_type, $new_args );
  149. }
  150. // bbl_stop_logging();
  151. $this->no_recursion = false;
  152. }
  153. public function ignored_taxonomies() {
  154. return array( 'post_translation', 'term_translation' );
  155. }
  156. public function is_taxonomy_translated( $taxonomy ) {
  157. if( in_array( $taxonomy, $this->ignored_taxonomies() ) ) {
  158. return false;
  159. }
  160. // @FIXME: Remove this when menu's are translatable
  161. if( 'nav_menu' == $taxonomy ) {
  162. return false;
  163. }
  164. return apply_filters( 'bbl_translated_taxonomy', true, $taxonomy );
  165. }
  166. /**
  167. * Hooks the WP bbl_registered_shadow_post_types action to check that we've applied
  168. * all untranslated taxonomies to the shadow post types created for this base
  169. * post type.
  170. *
  171. * @param string $post_type The post type for which the shadow post types have been registered.
  172. * @return void
  173. **/
  174. public function registered_shadow_post_types( $post_type ) {
  175. $taxonomies = get_object_taxonomies( $post_type );
  176. $object_type = (array) $post_type;
  177. foreach ( $taxonomies as $taxonomy ) {
  178. // Untranslated taxonomies do not have shadow equivalents in each language,
  179. // but do apply to the bast post_type and all it's shadow post_types.
  180. if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
  181. // Apply this taxonomy to all the shadow post types
  182. // of all of the base post_types it applies to.
  183. foreach ( $object_type as $ot ) {
  184. if ( ! ( $base_post_type = bbl_get_base_post_type( $ot ) ) ) {
  185. continue;
  186. }
  187. $shadow_post_types = bbl_get_shadow_post_types( $base_post_type );
  188. foreach ( $shadow_post_types as $shadow_post_type ) {
  189. register_taxonomy_for_object_type( $taxonomy, $shadow_post_type );
  190. }
  191. }
  192. }
  193. }
  194. }
  195. /**
  196. * Hooks the Babble action bbl_created_new_shadow_post, which is fired
  197. * when a new translation post is created, to sync any existing untranslated
  198. * taxonomy terms.
  199. *
  200. * @param int $new_post_id The ID of the new post (to sync to)
  201. * @param int $origin_post_id The ID of the originating post (to sync from)
  202. * @return void
  203. **/
  204. public function created_new_shadow_post( $new_post_id, $origin_post_id ) {
  205. $new_post = get_post( $new_post_id );
  206. if ( ! ( $origin_post = get_post( $origin_post_id ) ) ) {
  207. return;
  208. }
  209. if ( $this->no_recursion ) {
  210. return;
  211. }
  212. $this->no_recursion = true;
  213. $taxonomies = get_object_taxonomies( $origin_post->post_type );
  214. foreach ( $taxonomies as $taxonomy ) {
  215. if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
  216. $term_ids = wp_get_object_terms( $origin_post->ID, $taxonomy, array( 'fields' => 'ids' ) );
  217. $term_ids = array_map( 'absint', $term_ids );
  218. wp_set_object_terms( $new_post->ID, $term_ids, $taxonomy );
  219. }
  220. }
  221. $this->no_recursion = false;
  222. }
  223. /**
  224. * Hooks the WP save_post action to resync data
  225. * when requested.
  226. *
  227. * @param int $post_id The ID of the WP post
  228. * @param object $post The WP Post object
  229. * @return void
  230. **/
  231. public function save_post( $post_id, $post ) {
  232. $this->maybe_resync_terms( $post_id, $post );
  233. }
  234. /**
  235. * Hooks the WordPress term_link filter to provide functions to provide
  236. * appropriate links for the shadow taxonomies.
  237. *
  238. * @see get_term_link from whence much of this was copied
  239. *
  240. * @param string $termlink The currently generated term URL
  241. * @param object $term The WordPress term object we're generating a link for
  242. * @param string $taxonomy The
  243. * @return string The term link
  244. **/
  245. public function term_link( $termlink, $term, $taxonomy ) {
  246. $taxonomy = strtolower( $taxonomy );
  247. // No need to worry about the built in taxonomies
  248. if ( 'post_tag' == $taxonomy || 'category' == $taxonomy || ! isset( $this->taxonomies[ $taxonomy ] ) ) {
  249. return $termlink;
  250. }
  251. // Deal with our shadow taxonomies
  252. if ( ! ( $base_taxonomy = $this->get_base_taxonomy( $taxonomy ) ) ) {
  253. return $termlink;
  254. }
  255. // START copying from get_term_link, replacing $taxonomy with $base_taxonomy
  256. global $wp_rewrite;
  257. if ( !is_object($term) ) {
  258. if ( is_int($term) ) {
  259. $term = &get_term($term, $base_taxonomy);
  260. } else {
  261. $term = &get_term_by('slug', $term, $base_taxonomy);
  262. }
  263. }
  264. if ( !is_object($term) ) {
  265. $term = new WP_Error('invalid_term', __('Empty Term', 'babble'));
  266. }
  267. if ( is_wp_error( $term ) ) {
  268. return $term;
  269. }
  270. $termlink = $wp_rewrite->get_extra_permastruct($base_taxonomy);
  271. $slug = $term->slug;
  272. $t = get_taxonomy($base_taxonomy);
  273. if ( empty($termlink) ) {
  274. if ( 'category' == $base_taxonomy ) {
  275. $termlink = '?cat=' . $term->term_id;
  276. } elseif ( $t->query_var ) {
  277. $termlink = "?$t->query_var=$slug";
  278. } else {
  279. $termlink = "?taxonomy=$base_taxonomy&term=$slug";
  280. }
  281. $termlink = home_url($termlink);
  282. } else {
  283. if ( $t->rewrite['hierarchical'] ) {
  284. $hierarchical_slugs = array();
  285. $ancestors = get_ancestors($term->term_id, $base_taxonomy);
  286. foreach ( (array)$ancestors as $ancestor ) {
  287. $ancestor_term = get_term($ancestor, $base_taxonomy);
  288. $hierarchical_slugs[] = $ancestor_term->slug;
  289. }
  290. $hierarchical_slugs = array_reverse($hierarchical_slugs);
  291. $hierarchical_slugs[] = $slug;
  292. $termlink = str_replace("%$base_taxonomy%", implode('/', $hierarchical_slugs), $termlink);
  293. } else {
  294. $termlink = str_replace("%$base_taxonomy%", $slug, $termlink);
  295. }
  296. $termlink = home_url( user_trailingslashit($termlink, 'category') );
  297. }
  298. // STOP copying from get_term_link
  299. return $termlink;
  300. }
  301. /**
  302. * Hooks the WP get_terms filter to ensure the terms all have transids.
  303. *
  304. * @param array $terms The terms which have been got
  305. * @return array The terms which were got
  306. **/
  307. public function get_terms( $terms ) {
  308. foreach ( $terms as $term ) {
  309. if ( empty( $term ) ) {
  310. continue;
  311. }
  312. if ( isset( $this->taxonomies ) ) {
  313. continue;
  314. }
  315. if ( isset( $this->taxonomies[ $term->taxonomy ] ) ) {
  316. if ( ! $this->get_transid( $term->term_id ) ) {
  317. throw new exception( "ERROR: Translated term ID $term->term_id does not have a transid" );
  318. } else {
  319. continue;
  320. }
  321. }
  322. if ( ! $this->get_transid( $term->term_id ) ) {
  323. $this->set_transid( $term->term_id );
  324. }
  325. }
  326. return $terms;
  327. }
  328. /**
  329. * Hooks the WP parse_request action
  330. *
  331. * FIXME: Should I be extending and replacing the WP class?
  332. *
  333. * @param object $wp WP object, passed by reference (so no need to return)
  334. * @return void
  335. **/
  336. public function parse_request( $wp ) {
  337. if ( is_admin() ) {
  338. return;
  339. }
  340. // Sequester the original query, in case we need it to get the default content later
  341. if ( ! isset( $wp->query_vars[ 'bbl_tax_original_query' ] ) ) {
  342. $wp->query_vars[ 'bbl_tax_original_query' ] = $wp->query_vars;
  343. }
  344. $taxonomy = false;
  345. $terms = false;
  346. $taxonomies = get_taxonomies( null, 'objects' );
  347. $lang_taxonomies = array();
  348. foreach ( $taxonomies as $taxonomy => $tax_obj ) {
  349. $tax = $this->get_taxonomy_in_lang( $taxonomy, bbl_get_current_lang_code() );
  350. $lang_taxonomies[ $tax_obj->rewrite[ 'slug' ] ] = $tax;
  351. }
  352. if ( isset( $wp->query_vars[ 'tag' ] ) ) {
  353. $taxonomy = $this->get_taxonomy_in_lang( 'post_tag', $wp->query_vars[ 'lang' ] );
  354. $terms = $wp->query_vars[ 'tag' ];
  355. unset( $wp->query_vars[ 'tag' ] );
  356. } else if ( isset( $wp->query_vars[ 'category_name' ] ) ) {
  357. $taxonomy = $this->get_taxonomy_in_lang( 'category', $wp->query_vars[ 'lang' ] );
  358. $terms = $wp->query_vars[ 'category_name' ];
  359. unset( $wp->query_vars[ 'category_name' ] );
  360. } else {
  361. $taxonomies = array();
  362. foreach ( $lang_taxonomies as $slug => $tax ) {
  363. if ( isset( $wp->query_vars[ $slug ] ) ) {
  364. $taxonomies[] = $tax;
  365. break;
  366. }
  367. }
  368. if ( $taxonomies ) {
  369. $post_types = array();
  370. foreach ( $taxonomies as $taxonomy ) {
  371. $taxonomy = get_taxonomy( $taxonomy );
  372. $post_types = array_merge( $post_types, $taxonomy->object_type );
  373. // Filter out the post_types not in this language
  374. foreach ( $post_types as & $post_type ) {
  375. $post_type = bbl_get_post_type_in_lang( $post_type );
  376. }
  377. $post_types = array_unique( $post_types );
  378. }
  379. $wp->query_vars[ 'post_type' ] = $post_types;
  380. }
  381. }
  382. if ( $taxonomy && $terms ) {
  383. if ( ! isset( $wp->query_vars[ 'tax_query' ] ) || ! is_array( $wp->query_vars[ 'tax_query' ] ) ) {
  384. $wp->query_vars[ 'tax_query' ] = array();
  385. }
  386. $wp->query_vars[ 'tax_query' ][] = array(
  387. 'taxonomy' => $taxonomy,
  388. 'field' => 'slug',
  389. 'terms' => $terms,
  390. );
  391. }
  392. }
  393. /**
  394. * Hooks the WP set_object_terms action to sync any untranslated
  395. * taxonomies across to the translations.
  396. *
  397. * @param int $object_id The object to relate to
  398. * @param array $terms The slugs or ids of the terms
  399. * @param array $tt_ids The term_taxonomy_ids
  400. * @param string $taxonomy The name of the taxonomy for which terms are being set
  401. * @param bool $append If false will delete difference of terms
  402. * @return void
  403. **/
  404. public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy, $append ) {
  405. if ( $this->no_recursion ) {
  406. return;
  407. }
  408. $this->no_recursion = true;
  409. // DO NOT SYNC THE TRANSID TAXONOMIES!!
  410. if ( in_array( $taxonomy, $this->ignored_taxonomies() ) ) {
  411. $this->no_recursion = false;
  412. return;
  413. }
  414. if ( $this->is_taxonomy_translated( $taxonomy ) ) {
  415. // Here we assume that this taxonomy is on a post type
  416. $translations = bbl_get_post_translations( $object_id );
  417. foreach ( $translations as $lang_code => & $translation ) {
  418. if ( bbl_get_post_lang_code( $object_id ) == $lang_code ) {
  419. continue;
  420. }
  421. $translated_taxonomy = bbl_get_taxonomy_in_lang( $taxonomy, $lang_code );
  422. $translated_terms = array();
  423. foreach ( $terms as $term ) {
  424. if ( is_int( $term ) ) {
  425. $_term = get_term( $term, $taxonomy );
  426. } else {
  427. $_term = get_term_by( 'name', $term, $taxonomy );
  428. }
  429. if ( is_wp_error( $_term ) or empty( $_term ) ) {
  430. continue;
  431. }
  432. $translated_term = $this->get_term_in_lang( $_term->term_id, $taxonomy, $lang_code, false );
  433. $translated_terms[] = (int) $translated_term->term_id;
  434. }
  435. $result = wp_set_object_terms( $translation->ID, $translated_terms, $translated_taxonomy, $append );
  436. }
  437. } else {
  438. // Here we assume that this taxonomy is on a post type
  439. $translations = bbl_get_post_translations( $object_id );
  440. foreach ( $translations as $lang_code => & $translation ) {
  441. if ( bbl_get_post_lang_code( $object_id ) == $lang_code ) {
  442. continue;
  443. }
  444. wp_set_object_terms( $translation->ID, $terms, $taxonomy, $append );
  445. }
  446. }
  447. $this->no_recursion = false;
  448. }
  449. // CALLBACKS
  450. // =========
  451. // PUBLIC METHODS
  452. // ==============
  453. public function admin_body_class( $class ) {
  454. $taxonomy = get_current_screen() ? get_current_screen()->taxonomy : null;
  455. if ( $taxonomy ) {
  456. $class .= ' bbl-taxonomy-' . $taxonomy;
  457. }
  458. return $class;
  459. }
  460. public function bbl_translated_taxonomy( $translated, $taxonomy ) {
  461. if ( 'term_translation' == $taxonomy ) {
  462. return false;
  463. }
  464. if ( 'nav_menu' == $taxonomy ) {
  465. return false;
  466. }
  467. if ( 'link_category' == $taxonomy ) {
  468. return false;
  469. }
  470. if ( 'post_format' == $taxonomy ) {
  471. return false;
  472. }
  473. return $translated;
  474. }
  475. /**
  476. * Provided with a taxonomy name, e.g. `post_tag`, and a language
  477. * code, will return the shadow taxonomy in that language.
  478. *
  479. * @param string $taxonomy The origin taxonomy
  480. * @param string $lang_code The target language code
  481. * @return string The taxonomy name in that language
  482. **/
  483. public function translated_taxonomy( $origin_taxonomy, $lang_code ) {
  484. return strtolower( "{$origin_taxonomy}_{$lang_code}" );
  485. }
  486. /**
  487. * Get the terms which are the translations for the provided
  488. * term ID. N.B. The returned array of term objects (and false
  489. * values) will include the term for the term ID passed.
  490. *
  491. * @FIXME: We should cache the translation groups, as we do for posts
  492. *
  493. * @param int|object $term Either a WP Term object, or a term_id
  494. * @return array Either an array keyed by the site languages, each key containing false (if no translation) or a WP Term object
  495. **/
  496. public function get_term_translations( $term, $taxonomy ) {
  497. $term = get_term( $term, $taxonomy );
  498. $langs = bbl_get_active_langs();
  499. $translations = array();
  500. foreach ( $langs as $lang ) {
  501. $translations[ $lang->code ] = false;
  502. }
  503. $transid = $this->get_transid( $term->term_id );
  504. // I thought the fracking bug where the get_objects_in_term function returned integers
  505. // as strings was fixed. Seems not. See #17646 for details. Argh.
  506. $term_ids = array_map( 'absint', get_objects_in_term( $transid, 'term_translation' ) );
  507. // We're dealing with terms across multiple taxonomies
  508. $base_taxonomy = isset( $this->taxonomies[ $taxonomy ] ) ? $this->taxonomies[ $taxonomy ] : $taxonomy ;
  509. $taxonomies = array();
  510. $taxonomies[] = $base_taxonomy;
  511. foreach ( $this->lang_map as $lang_taxes ) {
  512. if ( $lang_taxes[ $base_taxonomy ] ) {
  513. $taxonomies[] = $lang_taxes[ $base_taxonomy ];
  514. }
  515. }
  516. // Get all the translations in one cached DB query
  517. $existing_terms = get_terms( $taxonomies, array( 'include' => $term_ids, 'hide_empty' => false ) );
  518. // Finally, we're ready to return the terms in this
  519. // translation group.
  520. $terms = array();
  521. foreach ( $existing_terms as $t ) {
  522. $terms[ $this->get_taxonomy_lang_code( $t->taxonomy ) ] = $t;
  523. }
  524. return $terms;
  525. }
  526. /**
  527. * Returns the term in a particular language, or the fallback content
  528. * if there's no term available.
  529. *
  530. * @param int|object $term Either a WP Term object, or a term_id
  531. * @param string $lang_code The language code for the required language
  532. * @param boolean $fallback If true: if a term is not available, fallback to the default language content (defaults to true)
  533. * @return object|boolean The WP Term object, or if $fallback was false and no post then returns false
  534. **/
  535. public function get_term_in_lang( $term, $taxonomy, $lang_code, $fallback = true ) {
  536. $translations = $this->get_term_translations( $term, $taxonomy );
  537. if ( isset( $translations[ $lang_code ] ) ) {
  538. return $translations[ $lang_code ];
  539. }
  540. if ( ! $fallback ) {
  541. return false;
  542. }
  543. return $translations[ bbl_get_default_lang_code() ];
  544. }
  545. /**
  546. * Return the admin URL to create a new translation for a term in a
  547. * particular language.
  548. *
  549. * @param int|object $default_term The term in the default language to create a new translation for, either WP Post object or post ID
  550. * @param string $lang The language code
  551. * @return string The admin URL to create the new translation
  552. * @access public
  553. **/
  554. public function get_new_term_translation_url( $default_term, $lang_code, $taxonomy = null ) {
  555. if ( ! is_int( $default_term ) && is_null( $taxonomy ) ) {
  556. throw new exception( 'get_new_term_translation_url: Cannot get term from term_id without taxonomy' );
  557. }
  558. if ( ! is_null( $taxonomy ) ) {
  559. $default_term = get_term( $default_term, $taxonomy );
  560. }
  561. if ( is_wp_error( $default_term ) ) {
  562. throw new exception( 'get_new_term_translation_url: Error getting term from term_id and taxonomy: ' . print_r( $default_term, true ) );
  563. }
  564. $url = admin_url( 'post-new.php' );
  565. $args = array(
  566. 'bbl_origin_term' => $default_term->term_id,
  567. 'bbl_origin_taxonomy' => $default_term->taxonomy,
  568. 'lang' => $lang_code,
  569. 'post_type' => 'bbl_job',
  570. );
  571. $url = add_query_arg( $args, $url );
  572. return $url;
  573. }
  574. /**
  575. * Returns the language code associated with a particular taxonomy.
  576. *
  577. * @param string $taxonomy The taxonomy to get the language for
  578. * @return string The lang code
  579. **/
  580. public function get_taxonomy_lang_code( $taxonomy ) {
  581. if ( ! isset( $this->taxonomies[ $taxonomy ] ) ) {
  582. return bbl_get_default_lang_code();
  583. }
  584. foreach ( $this->lang_map as $lang => $data ) {
  585. foreach ( $data as $trans_tax ) {
  586. if ( $taxonomy == $trans_tax ) {
  587. return $lang;
  588. }
  589. }
  590. }
  591. return false;
  592. }
  593. /**
  594. * Return the base taxonomy (in the default language) for a
  595. * provided taxonomy.
  596. *
  597. * @param string $taxonomy The name of a taxonomy
  598. * @return string The name of the base taxonomy
  599. **/
  600. public function get_base_taxonomy( $taxonomy ) {
  601. if ( ! isset( $this->taxonomies[ $taxonomy ] ) ) {
  602. return $taxonomy;
  603. }
  604. return $this->taxonomies[ $taxonomy ];
  605. }
  606. /**
  607. * Returns the equivalent taxonomy in the specified language.
  608. *
  609. * @param string $taxonomy A taxonomy to return in a given language
  610. * @param string $lang_code The language code for the required language (optional, defaults to current)
  611. * @return boolean|string The taxonomy name, or false if no taxonomy was specified
  612. **/
  613. public function get_taxonomy_in_lang( $taxonomy, $lang_code = null ) {
  614. // Some taxonomies are untranslated…
  615. if ( ! $this->is_taxonomy_translated( $taxonomy ) ) {
  616. return $taxonomy;
  617. }
  618. if ( ! $taxonomy ) {
  619. return false; // @FIXME: Should I actually be throwing an error here?
  620. }
  621. if ( is_null( $lang_code ) ) {
  622. $lang_code = bbl_get_current_lang_code();
  623. }
  624. $base_taxonomy = $this->get_base_taxonomy( $taxonomy );
  625. if ( bbl_get_default_lang_code() == $lang_code ) {
  626. return $base_taxonomy;
  627. }
  628. return $this->lang_map[ $lang_code ][ $base_taxonomy ];
  629. }
  630. /**
  631. * Returns a slug translated into a particular language.
  632. *
  633. * @TODO: This is more or less the same method as Babble_Post_Public::get_taxonomy_lang_code, do I need to DRY that up?
  634. *
  635. * @param string $slug The slug to translate
  636. * @param string $lang_code The language code for the required language (optional, defaults to current)
  637. * @return string A translated slug
  638. **/
  639. public function get_slug_in_lang( $slug, $lang_code = null ) {
  640. if ( is_null( $lang_code ) ) {
  641. $lang_code = bbl_get_current_lang_code();
  642. }
  643. $_slug = mb_strtolower( apply_filters( 'bbl_translate_taxonomy_slug', $slug, $lang_code ) );
  644. // @FIXME: For some languages the translation might be the same as the original
  645. if ( $_slug && $_slug != $slug ) {
  646. return $_slug;
  647. }
  648. // Do we need to check that the slug is unique at this point?
  649. return mb_strtolower( "{$_slug}_{$lang_code}" );
  650. }
  651. public function initialise_translation( $origin_term, $taxonomy, $lang_code ) {
  652. $new_taxonomy = $this->get_slug_in_lang( $taxonomy, $lang_code );
  653. $transid = $this->get_transid( $origin_term->term_id );
  654. // Insert translation:
  655. $this->no_recursion = true;
  656. $new_term_id = wp_insert_term( $origin_term->name . ' - ' . $lang_code, $new_taxonomy );
  657. $this->no_recursion = false;
  658. $new_term = get_term( $new_term_id['term_id'], $new_taxonomy );
  659. // Assign transid to translation:
  660. $this->set_transid( $new_term_id['term_id'], $transid );
  661. return $new_term;
  662. }
  663. // PRIVATE/PROTECTED METHODS
  664. // =========================
  665. /**
  666. * Will update term count based on object types of the current
  667. * taxonomy. Will only count the post(s) in the default language.
  668. *
  669. * Private function for the default callback for post_tag and category
  670. * taxonomies.
  671. *
  672. * @param array $terms List of Term taxonomy IDs
  673. * @param object $taxonomy Current taxonomy object of terms
  674. */
  675. function update_post_term_count( $terms, $taxonomy ) {
  676. global $wpdb;
  677. $object_types = (array) $taxonomy->object_type;
  678. foreach ( $object_types as &$object_type ) {
  679. list( $object_type ) = explode( ':', $object_type );
  680. // Babble specific code, to only count in primary language
  681. $object_type = bbl_get_post_type_in_lang( $object_type, bbl_get_default_lang_code() );
  682. }
  683. $object_types = array_unique( $object_types );
  684. if ( false !== ( $check_attachments = array_search( 'attachment', $object_types ) ) ) {
  685. unset( $object_types[ $check_attachments ] );
  686. $check_attachments = true;
  687. }
  688. if ( $object_types ) {
  689. $object_types = esc_sql( array_filter( $object_types, 'post_type_exists' ) );
  690. }
  691. foreach ( (array) $terms as $term ) {
  692. $count = 0;
  693. // Attachments can be 'inherit' status, we need to base count off the parent's status if so
  694. if ( $check_attachments ) {
  695. $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts p1 WHERE p1.ID = $wpdb->term_relationships.object_id AND ( post_status = 'publish' OR ( post_status = 'inherit' AND post_parent > 0 AND ( SELECT post_status FROM $wpdb->posts WHERE ID = p1.post_parent ) = 'publish' ) ) AND post_type = 'attachment' AND term_taxonomy_id = %d", $term ) );
  696. }
  697. if ( $object_types ) {
  698. $count += (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->term_relationships, $wpdb->posts WHERE $wpdb->posts.ID = $wpdb->term_relationships.object_id AND post_status = 'publish' AND post_type IN ('" . implode("', '", $object_types ) . "') AND term_taxonomy_id = %d", $term ) );
  699. }
  700. do_action( 'edit_term_taxonomy', $term, $taxonomy );
  701. $wpdb->update( $wpdb->term_taxonomy, compact( 'count' ), array( 'term_taxonomy_id' => $term ) );
  702. do_action( 'edited_term_taxonomy', $term, $taxonomy );
  703. }
  704. }
  705. /**
  706. * Return the translation group ID (a term ID) that the given term ID
  707. * belongs to.
  708. *
  709. * @param int $target_term_id The term ID to find the translation group for
  710. * @return int The transID the target term belongs to
  711. **/
  712. public function get_transid( $target_term_id ) {
  713. if ( $transid = wp_cache_get( $target_term_id, 'bbl_term_transids' ) ) {
  714. return $transid;
  715. }
  716. if ( ! $target_term_id ) {
  717. throw new exception( "Please specify a target term_id" );
  718. }
  719. $transids = wp_get_object_terms( $target_term_id, 'term_translation', array( 'fields' => 'ids' ) );
  720. // "There can be only one" (so we'll just drop the others)
  721. if ( isset( $transids[ 0 ] ) ) {
  722. $transid = $transids[ 0 ];
  723. } else {
  724. $transid = $this->set_transid( $target_term_id );
  725. }
  726. wp_cache_add( $target_term_id, $transid, 'bbl_term_transids' );
  727. return $transid;
  728. }
  729. /**
  730. * Set the translation group ID (a term ID) that the given term ID
  731. * belongs to.
  732. *
  733. * @param int $target_term_id The term ID to set the translation group for
  734. * @param int $translation_group_id The ID of the translation group to add this
  735. * @return int The transID the target term belongs to
  736. **/
  737. public function set_transid( $target_term_id, $transid = null ) {
  738. if ( ! $target_term_id ) {
  739. throw new exception( "Please specify a target term_id" );
  740. }
  741. if ( ! $transid ) {
  742. $transid_name = 'term_transid_' . uniqid();
  743. $result = wp_insert_term( $transid_name, 'term_translation', array() );
  744. if ( is_wp_error( $result ) ) {
  745. error_log( "Problem creating a new Term TransID: " . print_r( $result, true ) );
  746. } else {
  747. $transid = $result[ 'term_id' ];
  748. }
  749. }
  750. $result = wp_set_object_terms( $target_term_id, absint( $transid ), 'term_translation' );
  751. if ( is_wp_error( $result ) ) {
  752. error_log( "Problem associating TransID with new posts: " . print_r( $result, true ) );
  753. }
  754. wp_cache_delete( $target_term_id, 'bbl_term_transids' );
  755. return $transid;
  756. }
  757. /**
  758. * Checks for the relevant POSTed field, then
  759. * resyncs the terms.
  760. *
  761. * @param int $post_id The ID of the WP post
  762. * @param object $post The WP Post object
  763. * @return void
  764. **/
  765. protected function maybe_resync_terms( $post_id, $post ) {
  766. // Check that the fields were included on the screen, we
  767. // can do this by checking for the presence of the nonce.
  768. $nonce = isset( $_POST[ '_bbl_metabox_resync' ] ) ? $_POST[ '_bbl_metabox_resync' ] : false;
  769. if ( ! in_array( $post->post_status, array( 'draft', 'publish' ) ) ) {
  770. return;
  771. }
  772. if ( ! $nonce ) {
  773. return;
  774. }
  775. $posted_id = isset( $_POST[ 'post_ID' ] ) ? $_POST[ 'post_ID' ] : 0;
  776. if ( $posted_id != $post_id ) {
  777. return;
  778. }
  779. // While we're at it, let's check the nonce
  780. check_admin_referer( "bbl_resync_translation-$post_id", '_bbl_metabox_resync' );
  781. if ( $this->no_recursion ) {
  782. return;
  783. }
  784. $this->no_recursion = true;
  785. $taxonomies = get_object_taxonomies( $post->post_type );
  786. $origin_post = bbl_get_post_in_lang( $post_id, bbl_get_default_lang_code() );
  787. // First dissociate all the terms from synced taxonomies from this post
  788. wp_delete_object_term_relationships( $post_id, $taxonomies );
  789. // Now associate terms from synced taxonomies in from the origin post
  790. foreach ( $taxonomies as $taxonomy ) {
  791. $origin_taxonomy = $taxonomy;
  792. if ( $this->is_taxonomy_translated( $taxonomy ) ) {
  793. $origin_taxonomy = bbl_get_taxonomy_in_lang( $taxonomy, bbl_get_default_lang_code() );
  794. }
  795. $term_ids = wp_get_object_terms( $origin_post->ID, $origin_taxonomy, array( 'fields' => 'ids' ) );
  796. $term_ids = array_map( 'absint', $term_ids );
  797. $result = wp_set_object_terms( $post_id, $term_ids, $taxonomy );
  798. if ( is_wp_error( $result, true ) ) {
  799. throw new exception( "Problem syncing terms: " . print_r( $terms, true ), " Error: " . print_r( $result, true ) );
  800. }
  801. }
  802. }
  803. }
  804. global $bbl_taxonomies;
  805. $bbl_taxonomies = new Babble_Taxonomies();