PluginProbe
Polylang / 2.3.10
Polylang v2.3.10
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / include / model.php

model.php in Polylang 2.3.10, at include/model.php

626 lines 21.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Setups the language and translations model based on WordPress taxonomies
5 *
6 * @since 1.2
7 */
8 class PLL_Model {
9 public $cache; // Our internal non persistent cache object
10 public $options;
11 public $post, $term; // Translated objects models
12
13 /**
14 * Constructor
15 * setups translated objects sub models
16 * setups filters and actions
17 *
18 * @since 1.2
19 *
20 * @param array $options Polylang options
21 */
22 public function __construct( &$options ) {
23 $this->options = &$options;
24
25 $this->cache = new PLL_Cache();
26 $this->post = new PLL_Translated_Post( $this ); // translated post sub model
27 $this->term = new PLL_Translated_Term( $this ); // translated term sub model
28
29 // We need to clean languages cache when editing a language and when modifying the permalink structure
30 add_action( 'edited_term_taxonomy', array( $this, 'clean_languages_cache' ), 10, 2 );
31 add_action( 'update_option_permalink_structure', array( $this, 'clean_languages_cache' ) );
32 add_action( 'update_option_siteurl', array( $this, 'clean_languages_cache' ) );
33 add_action( 'update_option_home', array( $this, 'clean_languages_cache' ) );
34
35 add_filter( 'get_terms_args', array( $this, 'get_terms_args' ) );
36
37 // Just in case someone would like to display the language description ;- )
38 add_filter( 'language_description', '__return_empty_string' );
39 }
40
41 /**
42 * Returns the list of available languages
43 * caches the list in a db transient ( except flags ), unless PLL_CACHE_LANGUAGES is set to false
44 * caches the list ( with flags ) in the private property $languages
45 *
46 * List of parameters accepted in $args:
47 *
48 * hide_empty => hides languages with no posts if set to true ( defaults to false )
49 * fields => return only that field if set ( see PLL_Language for a list of fields )
50 *
51 * @since 0.1
52 *
53 * @param array $args
54 * @return array|string|int list of PLL_Language objects or PLL_Language object properties
55 */
56 public function get_languages_list( $args = array() ) {
57 if ( false === $languages = $this->cache->get( 'languages' ) ) {
58
59 // Create the languages from taxonomies
60 if ( ( defined( 'PLL_CACHE_LANGUAGES' ) && ! PLL_CACHE_LANGUAGES ) || false === ( $languages = get_transient( 'pll_languages_list' ) ) ) {
61 $languages = get_terms( 'language', array( 'hide_empty' => false, 'orderby' => 'term_group' ) );
62 $languages = empty( $languages ) || is_wp_error( $languages ) ? array() : $languages;
63
64 $term_languages = get_terms( 'term_language', array( 'hide_empty' => false ) );
65 $term_languages = empty( $term_languages ) || is_wp_error( $term_languages ) ?
66 array() : array_combine( wp_list_pluck( $term_languages, 'slug' ), $term_languages );
67
68 if ( ! empty( $languages ) && ! empty( $term_languages ) ) {
69 // Don't use array_map + create_function to instantiate an autoloaded class as it breaks badly in old versions of PHP
70 foreach ( $languages as $k => $v ) {
71 $languages[ $k ] = new PLL_Language( $v, $term_languages[ 'pll_' . $v->slug ] );
72 }
73
74 // We will need the languages list to allow its access in the filter below
75 $this->cache->set( 'languages', $languages );
76
77 /**
78 * Filter the list of languages *before* it is stored in the persistent cache
79 * /!\ this filter is fired *before* the $polylang object is available
80 *
81 * @since 1.7.5
82 *
83 * @param array $languages the list of language objects
84 * @param object $model PLL_Model object
85 */
86 $languages = apply_filters( 'pll_languages_list', $languages, $this );
87
88 // Don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache
89 // Thanks to captin411 for catching this!
90 // See https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255;
91 set_transient( 'pll_languages_list', array_map( 'get_object_vars', $languages ) );
92 }
93 else {
94 $languages = array(); // In case something went wrong
95 }
96 }
97
98 // Create the languages directly from arrays stored in transients
99 else {
100 foreach ( $languages as $k => $v ) {
101 $languages[ $k ] = new PLL_Language( $v );
102 }
103 }
104
105 // Custom flags
106 if ( ! PLL_ADMIN ) {
107 foreach ( $languages as $language ) {
108 $language->set_custom_flag();
109 }
110 }
111
112 /**
113 * Filter the list of languages *after* it is stored in the persistent cache
114 * /!\ this filter is fired *before* the $polylang object is available
115 *
116 * @since 1.8
117 *
118 * @param array $languages the list of language objects
119 */
120 $languages = apply_filters( 'pll_after_languages_cache', $languages );
121 $this->cache->set( 'languages', $languages );
122 }
123
124 $args = wp_parse_args( $args, array( 'hide_empty' => false ) );
125
126 // Remove empty languages if requested
127 if ( $args['hide_empty'] ) {
128 $languages = wp_list_filter( $languages, array( 'count' => 0 ), 'NOT' );
129 }
130
131 return empty( $args['fields'] ) ? $languages : wp_list_pluck( $languages, $args['fields'] );
132 }
133
134 /**
135 * Cleans language cache
136 * can be called directly with no parameter
137 * called by the 'edited_term_taxonomy' filter with 2 parameters when count needs to be updated
138 *
139 * @since 1.2
140 *
141 * @param int $term not used
142 * @param string $taxonomy taxonomy name
143 */
144 public function clean_languages_cache( $term = 0, $taxonomy = null ) {
145 if ( empty( $taxonomy ) || 'language' == $taxonomy ) {
146 delete_transient( 'pll_languages_list' );
147 $this->cache->clean();
148 }
149 }
150
151 /**
152 * Don't query term metas when only our taxonomies are queried
153 *
154 * @since 2.3
155 *
156 * @param array $args WP_Term_Query arguments
157 * @return array
158 */
159 public function get_terms_args( $args ) {
160 if ( isset( $args['taxonomy'] ) && ! array_diff( (array) $args['taxonomy'], array( 'language', 'term_language', 'post_translations', 'term_translations' ) ) ) {
161 $args['update_term_meta_cache'] = false;
162 }
163 return $args;
164 }
165
166 /**
167 * Returns the language by its term_id, tl_term_id, slug or locale
168 *
169 * @since 0.1
170 *
171 * @param int|string $value term_id, tl_term_id, slug or locale of the queried language
172 * @return object|bool PLL_Language object, false if no language found
173 */
174 public function get_language( $value ) {
175 if ( is_object( $value ) ) {
176 return $value instanceof PLL_Language ? $value : $this->get_language( $value->term_id ); // will force cast to PLL_Language
177 }
178
179 if ( false === $return = $this->cache->get( 'language:' . $value ) ) {
180 foreach ( $this->get_languages_list() as $lang ) {
181 $this->cache->set( 'language:' . $lang->term_id, $lang );
182 $this->cache->set( 'language:' . $lang->tl_term_id, $lang );
183 $this->cache->set( 'language:' . $lang->slug, $lang );
184 $this->cache->set( 'language:' . $lang->locale, $lang );
185 }
186 $return = $this->cache->get( 'language:' . $value );
187 }
188
189 return $return;
190 }
191
192 /**
193 * Adds terms clauses to get_terms to filter them by languages - used in both frontend and admin
194 *
195 * @since 1.2
196 *
197 * @param array $clauses the list of sql clauses in terms query
198 * @param object $lang PLL_Language object
199 * @return array modified list of clauses
200 */
201 public function terms_clauses( $clauses, $lang ) {
202 if ( ! empty( $lang ) && false === strpos( $clauses['join'], 'pll_tr' ) ) {
203 $clauses['join'] .= $this->term->join_clause();
204 $clauses['where'] .= $this->term->where_clause( $lang );
205 }
206 return $clauses;
207 }
208
209 /**
210 * Returns post types that need to be translated
211 * the post types list is cached for better better performance
212 * wait for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php
213 *
214 * @since 1.2
215 *
216 * @param bool $filter true if we should return only valid registered post types
217 * @return array post type names for which Polylang manages languages and translations
218 */
219 public function get_translated_post_types( $filter = true ) {
220 if ( false === $post_types = $this->cache->get( 'post_types' ) ) {
221 $post_types = array( 'post' => 'post', 'page' => 'page' );
222
223 if ( ! empty( $this->options['media_support'] ) ) {
224 $post_types['attachment'] = 'attachment';
225 }
226
227 if ( ! empty( $this->options['post_types'] ) && is_array( $this->options['post_types'] ) ) {
228 $post_types = array_merge( $post_types, array_combine( $this->options['post_types'], $this->options['post_types'] ) );
229 }
230
231 /**
232 * Filter the list of post types available for translation.
233 * The default are post types which have the parameter ‘public’ set to true.
234 * The filter must be added soon in the WordPress loading process:
235 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
236 *
237 * @since 0.8
238 *
239 * @param array $post_types list of post type names
240 * @param bool $is_settings true when displaying the list of custom post types in Polylang settings
241 */
242 $post_types = apply_filters( 'pll_get_post_types', $post_types, false );
243
244 if ( did_action( 'after_setup_theme' ) ) {
245 $this->cache->set( 'post_types', $post_types );
246 }
247 }
248
249 return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types;
250 }
251
252 /**
253 * Returns true if Polylang manages languages and translations for this post type
254 *
255 * @since 1.2
256 *
257 * @param string|array $post_type post type name or array of post type names
258 * @return bool
259 */
260 public function is_translated_post_type( $post_type ) {
261 $post_types = $this->get_translated_post_types( false );
262 return ( is_array( $post_type ) && array_intersect( $post_type, $post_types ) || in_array( $post_type, $post_types ) || 'any' === $post_type && ! empty( $post_types ) );
263 }
264
265 /**
266 * Return taxonomies that need to be translated
267 *
268 * @since 1.2
269 *
270 * @param bool $filter true if we should return only valid registered taxonomies
271 * @return array array of registered taxonomy names for which Polylang manages languages and translations
272 */
273 public function get_translated_taxonomies( $filter = true ) {
274 if ( false === $taxonomies = $this->cache->get( 'taxonomies' ) ) {
275 $taxonomies = array( 'category' => 'category', 'post_tag' => 'post_tag' );
276
277 if ( ! empty( $this->options['taxonomies'] ) && is_array( $this->options['taxonomies'] ) ) {
278 $taxonomies = array_merge( $taxonomies, array_combine( $this->options['taxonomies'], $this->options['taxonomies'] ) );
279 }
280
281 /**
282 * Filter the list of taxonomies available for translation.
283 * The default are taxonomies which have the parameter ‘public’ set to true.
284 * The filter must be added soon in the WordPress loading process:
285 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
286 *
287 * @since 0.8
288 *
289 * @param array $taxonomies list of taxonomy names
290 * @param bool $is_settings true when displaying the list of custom taxonomies in Polylang settings
291 */
292 $taxonomies = apply_filters( 'pll_get_taxonomies', $taxonomies, false );
293 if ( did_action( 'after_setup_theme' ) ) {
294 $this->cache->set( 'taxonomies', $taxonomies );
295 }
296 }
297
298 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
299 }
300
301 /**
302 * Returns true if Polylang manages languages and translations for this taxonomy
303 *
304 * @since 1.2
305 *
306 * @param string|array $tax taxonomy name or array of taxonomy names
307 * @return bool
308 */
309 public function is_translated_taxonomy( $tax ) {
310 $taxonomies = $this->get_translated_taxonomies( false );
311 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
312 }
313
314 /**
315 * Return taxonomies that need to be filtered ( post_format like )
316 *
317 * @since 1.7
318 *
319 * @param bool $filter true if we should return only valid registered taxonomies
320 * @return array array of registered taxonomy names
321 */
322 public function get_filtered_taxonomies( $filter = true ) {
323 if ( did_action( 'after_setup_theme' ) ) {
324 static $taxonomies = null;
325 }
326
327 if ( empty( $taxonomies ) ) {
328 $taxonomies = array( 'post_format' => 'post_format' );
329
330 /**
331 * Filter the list of taxonomies not translatable but filtered by language.
332 * Includes only the post format by default
333 * The filter must be added soon in the WordPress loading process:
334 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
335 *
336 * @since 1.7
337 *
338 * @param array $taxonomies list of taxonomy names
339 * @param bool $is_settings true when displaying the list of custom taxonomies in Polylang settings
340 */
341 $taxonomies = apply_filters( 'pll_filtered_taxonomies', $taxonomies, false );
342 }
343
344 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
345 }
346
347 /**
348 * Returns true if Polylang filters this taxonomy per language
349 *
350 * @since 1.7
351 *
352 * @param string|array $tax taxonomy name or array of taxonomy names
353 * @return bool
354 */
355 public function is_filtered_taxonomy( $tax ) {
356 $taxonomies = $this->get_filtered_taxonomies( false );
357 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
358 }
359
360 /**
361 * Returns the query vars of all filtered taxonomies
362 *
363 * @since 1.7
364 *
365 * @return array
366 */
367 public function get_filtered_taxonomies_query_vars() {
368 $query_vars = array();
369 foreach ( $this->get_filtered_taxonomies() as $filtered_tax ) {
370 $tax = get_taxonomy( $filtered_tax );
371 $query_vars[] = $tax->query_var;
372 }
373 return $query_vars;
374 }
375
376 /**
377 * Create a default category for a language
378 *
379 * @since 1.2
380 *
381 * @param object|string|int $lang language
382 */
383 public function create_default_category( $lang ) {
384 $lang = $this->get_language( $lang );
385
386 // create a new category
387 // FIXME this is translated in admin language when we would like it in $lang
388 $cat_name = __( 'Uncategorized' );
389 $cat_slug = sanitize_title( $cat_name . '-' . $lang->slug );
390 $cat = wp_insert_term( $cat_name, 'category', array( 'slug' => $cat_slug ) );
391
392 // check that the category was not previously created ( in case the language was deleted and recreated )
393 $cat = isset( $cat->error_data['term_exists'] ) ? $cat->error_data['term_exists'] : $cat['term_id'];
394
395 // set language
396 $this->term->set_language( (int) $cat, $lang );
397
398 // this is a translation of the default category
399 $default = (int) get_option( 'default_category' );
400 $translations = $this->term->get_translations( $default );
401 if ( empty( $translations ) ) {
402 if ( $lg = $this->term->get_language( $default ) ) {
403 $translations[ $lg->slug ] = $default;
404 }
405 else {
406 $translations = array();
407 }
408 }
409
410 $this->term->save_translations( (int) $cat, $translations );
411 }
412
413 /**
414 * It is possible to have several terms with the same name in the same taxonomy ( one per language )
415 * but the native term_exists will return true even if only one exists
416 * so here the function adds the language parameter
417 *
418 * @since 1.4
419 *
420 * @param string $term_name the term name
421 * @param string $taxonomy taxonomy name
422 * @param int $parent parent term id
423 * @param string|object $language the language slug or object
424 * @return null|int the term_id of the found term
425 */
426 public function term_exists( $term_name, $taxonomy, $parent, $language ) {
427 global $wpdb;
428
429 $term_name = trim( wp_unslash( $term_name ) );
430
431 $select = "SELECT t.term_id FROM $wpdb->terms AS t";
432 $join = " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
433 $join .= $this->term->join_clause();
434 $where = $wpdb->prepare( ' WHERE tt.taxonomy = %s AND t.name = %s', $taxonomy, $term_name );
435 $where .= $this->term->where_clause( $this->get_language( $language ) );
436
437 if ( $parent > 0 ) {
438 $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent );
439 }
440
441 return $wpdb->get_var( $select . $join . $where );
442 }
443
444 /**
445 * Gets the number of posts per language in a date, author or post type archive
446 *
447 * @since 1.2
448 *
449 * @param object $lang
450 * @param array $q WP_Query arguments ( accepted: post_type, m, year, monthnum, day, author, author_name, post_format )
451 * @return int
452 */
453 public function count_posts( $lang, $q = array() ) {
454 global $wpdb;
455
456 $q = wp_parse_args( $q, array( 'post_type' => 'post' ) );
457
458 if ( ! is_array( $q['post_type'] ) ) {
459 $q['post_type'] = array( $q['post_type'] );
460 }
461
462 foreach ( $q['post_type'] as $key => $type ) {
463 if ( ! post_type_exists( $type ) ) {
464 unset( $q['post_type'][ $key ] );
465 }
466 }
467
468 if ( empty( $q['post_type'] ) ) {
469 $q['post_type'] = array( 'post' ); // we *need* a post type
470 }
471
472 $cache_key = md5( serialize( $q ) );
473 $counts = wp_cache_get( $cache_key, 'pll_count_posts' );
474
475 if ( false === $counts ) {
476 $select = "SELECT pll_tr.term_taxonomy_id, COUNT( * ) AS num_posts FROM {$wpdb->posts}";
477 $join = $this->post->join_clause();
478 $where = " WHERE post_status = 'publish'";
479 $where .= sprintf( " AND {$wpdb->posts}.post_type IN ( '%s' )", join( "', '", esc_sql( $q['post_type'] ) ) );
480 $where .= $this->post->where_clause( $this->get_languages_list() );
481 $groupby = ' GROUP BY pll_tr.term_taxonomy_id';
482
483 if ( ! empty( $q['m'] ) ) {
484 $q['m'] = '' . preg_replace( '|[^0-9]|', '', $q['m'] );
485 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 0, 4 ) );
486 if ( strlen( $q['m'] ) > 5 ) {
487 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 4, 2 ) );
488 }
489 if ( strlen( $q['m'] ) > 7 ) {
490 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 6, 2 ) );
491 }
492 }
493
494 if ( ! empty( $q['year'] ) ) {
495 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", $q['year'] );
496 }
497
498 if ( ! empty( $q['monthnum'] ) ) {
499 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", $q['monthnum'] );
500 }
501
502 if ( ! empty( $q['day'] ) ) {
503 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", $q['day'] );
504 }
505
506 if ( ! empty( $q['author_name'] ) ) {
507 $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) );
508 if ( $author ) {
509 $q['author'] = $author->ID;
510 }
511 }
512
513 if ( ! empty( $q['author'] ) ) {
514 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_author = %d", $q['author'] );
515 }
516
517 // filtered taxonomies ( post_format )
518 foreach ( $this->get_filtered_taxonomies_query_vars() as $tax_qv ) {
519
520 if ( ! empty( $q[ $tax_qv ] ) ) {
521 $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = {$wpdb->posts}.ID";
522 $join .= " INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id";
523 $join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id";
524 $where .= $wpdb->prepare( ' AND t.slug = %s', $q[ $tax_qv ] );
525 }
526 }
527
528 $res = $wpdb->get_results( $select . $join . $where . $groupby, ARRAY_A );
529 foreach ( (array) $res as $row ) {
530 $counts[ $row['term_taxonomy_id'] ] = $row['num_posts'];
531 }
532
533 wp_cache_set( $cache_key, $counts, 'pll_count_posts' );
534 }
535
536 return empty( $counts[ $lang->term_taxonomy_id ] ) ? 0 : $counts[ $lang->term_taxonomy_id ];
537 }
538
539 /**
540 * Setup the links model based on options
541 *
542 * @since 1.2
543 *
544 * @return object implementing "links_model interface"
545 */
546 public function get_links_model() {
547 $c = array( 'Directory', 'Directory', 'Subdomain', 'Domain' );
548 $class = get_option( 'permalink_structure' ) ? 'PLL_Links_' . $c[ $this->options['force_lang'] ] : 'PLL_Links_Default';
549
550 /**
551 * Filter the links model class to use
552 * /!\ this filter is fired *before* the $polylang object is available
553 *
554 * @since 2.1.1
555 *
556 * @param string $class A class name: PLL_Links_Default, PLL_Links_Directory, PLL_Links_Subdomain, PLL_Links_Domain
557 */
558 $class = apply_filters( 'pll_links_model', $class );
559
560 return new $class( $this );
561 }
562
563 /**
564 * Some backward compatibility with Polylang < 1.8
565 * allows for example to call $polylang->model->get_post_languages( $post_id ) instead of $polylang->model->post->get_language( $post_id )
566 * this works but should be slower than the direct call, thus an error is triggered in debug mode
567 *
568 * @since 1.8
569 *
570 * @param string $func Function name
571 * @param array $args Function arguments
572 */
573 public function __call( $func, $args ) {
574 $f = $func;
575
576 switch ( $func ) {
577 case 'get_object_term':
578 $o = ( false === strpos( $args[1], 'term' ) ) ? 'post' : 'term';
579 break;
580
581 case 'save_translations':
582 case 'delete_translation':
583 case 'get_translations':
584 case 'get_translation':
585 case 'join_clause':
586 $o = ( 'post' == $args[0] || $this->is_translated_post_type( $args[0] ) ) ? 'post' : ( 'term' == $args[0] || $this->is_translated_taxonomy( $args[0] ) ? 'term' : false );
587 unset( $args[0] );
588 break;
589
590 case 'set_post_language':
591 case 'get_post_language':
592 case 'set_term_language':
593 case 'get_term_language':
594 case 'delete_term_language':
595 case 'get_post':
596 case 'get_term':
597 $str = explode( '_', $func );
598 $f = empty( $str[2] ) ? $str[0] : $str[0] . '_' . $str[2];
599 $o = $str[1];
600 break;
601
602 case 'where_clause':
603 case 'get_objects_in_language':
604 $o = $args[1];
605 unset( $args[1] );
606 break;
607 }
608
609 if ( ! empty( $o ) && is_object( $this->$o ) && method_exists( $this->$o, $f ) ) {
610 if ( WP_DEBUG ) {
611 $debug = debug_backtrace();
612 $i = 1 + empty( $debug[1]['line'] ); // the file and line are in $debug[2] if the function was called using call_user_func
613
614 trigger_error( sprintf(
615 '%1$s was called incorrectly in %4$s on line %5$s: the call to $polylang->model->%1$s() has been deprecated in Polylang 1.8, use PLL()->model->%2$s->%3$s() instead.' . "\nError handler",
616 $func, $o, $f, $debug[ $i ]['file'], $debug[ $i ]['line']
617 ) );
618 }
619 return call_user_func_array( array( $this->$o, $f ), $args );
620 }
621
622 $debug = debug_backtrace();
623 trigger_error( sprintf( 'Call to undefined function PLL()->model->%1$s() in %2$s on line %3$s' . "\nError handler", $func, $debug[0]['file'], $debug[0]['line'] ), E_USER_ERROR );
624 }
625 }
626