PluginProbe
Polylang / 2.0
Polylang v2.0
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.0, at include/model.php

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