Die letzten Beiträge

Eigene Bildgrößen in der WordPress-Mediathek auswählbar machen

erstellt am:09. Oktober 2012

Dass man bei WordPress relativ leicht eigene Bildgrößen einstellen kann und in seinem Template einbinden kann ist meiner Meinung nach recht bekannt. Vor kurzem brauchte ich jedoch die Möglichkeit, dass der Anwender eben diese eigens angelegten Bildgrößen auch selber auswählen kann und sich nicht nur zwischen den vordefinierten Bildgrößen entscheiden muss:
WordPress-Mediathek Standard-Bildgrößen
Dazu bietet WordPress einen schönen Filter, welcher leicht zu implementieren ist. Vorerst jedoch muss man die Voraussetzung schaffen, um eigene Bildgrößen anlegen zu können. Der nachfolgende Code erklärt, wie man sowohl eigene Bildgrößen definiert und wie man diese in der Mediathek beim Bild einfügen - Dialog auswählbar macht. Er ist in die functions.php-Datei des Themes einzubauen:

/**
 * Enable support for Post Thumbnails 
 * (Support für eigene Bildgrößen einschalten)
 */
add_theme_support( 'post-thumbnails' );
 
/**
 * Add custom image sizes 
 * (Eigene Bildgrößen hinzufügen)
 */
add_action( 'after_setup_theme', 'af_add_custom_image_sizes' );
function af_add_custom_image_sizes() {
              //params: 'size-name', width, height, crop
    add_image_size( 'home-featured-image', 306, 152, false );
    add_image_size( 'imageslider-preview', 180, 240, false);
    add_image_size( 'imageslider-view', 99999, 600, false);
}
/**
 * Make custom image sizes available via media manager 
 * (Eigene Bildgrößen in der Mediathek verfügbar machen)
 */
add_filter( 'image_size_names_choose', 'custom_image_sizes_choose' );
function custom_image_sizes_choose( $sizes ) {
	$custom_sizes = array(
			'home-featured-image' => 'Startseitenbilder',
			'imageslider-preview' => 'Slider-Vorschau',
			'imageslider-view' => 'Slider-Bild'
	);
	return array_merge( $sizes, $custom_sizes );
}

Wichtig ist, dass man die Bezeichnung des ersten Parameters bei add_image_size gleich setzt zu den Keys im Array $custom_sizes. Am besten speichert man sich diese in einer eigenen Variable.
Im Template kann man dann über die Bezeichnungen die entsprechende Bildgröße einbinden:

/**
 * into the loop
 * (innerhalb des Loops)
 */
if(has_post_thumbnail()){
	the_post_thumbnail('home-featured-image');
}
/**
 * somewhere else i.e. outside the loop
 * woanders, z.B. außerhalb des Loops
 */
if(has_post_thumbnail($post->ID)){
	echo get_the_post_thumbnail($post->ID, 'home-featured-image');
}

Auch kann man die eigene Bildgröße in allen Bildfunktionen verwenden, die $size als Parameter empfangen können, beispielsweise wp_get_attachment_image(), wp_get_attachment_image_src() oder wp_get_attachment_link(). Die letzten 3 Funktionen benötigen jedoch eine Attachment-ID als Pflichtparameter (siehe Dokuverlinkungen).

Das Ergebnis in meinem Beispiel kann nun folgendermaßen aussehen:

Sieht man hinter den eigenen Bildgrößen keine Dateimaße und sind die Radio-Buttons ausgegraut und nicht anklickbar, so existiert diese Datei nicht in diesem Format. Gründe dafür können sein, dass sie zu klein ist für die eingestellte Bildgröße und somit nicht erstellt werden konnte oder dass die Datei hochgeladen wurde, bevor man die eigenen Bilddateigrößen festgelegt hat. Im Normalfall sollte jedoch folgende Ansicht danach vorhanden sein:
WordPress eigene Bilddateigrößen in der Mediathek auswählen

Viel Erfolg beim Umsetzen! Feedback und Verbesserungsvorschläge immer erwünscht! :)


Eigenen JavaScript-Code in das WordPress-Backend einbinden

erstellt am:08. Oktober 2012

Beim letzten Projekt benötigte ich JavaScript-Code, um das Backend ein wenig zu modifizieren. Hier will ich kurz anhand eines Beispiels festhalten, wie ich das realisiert habe.
Konkret ging es darum, dass ich im Backend ein Feld hatte, welches den Farbcode eines Elements im DOM bestimmen sollte. Der User sollte dazu aus einer vorgegebenen Liste mit Farbwerten einen auswählen und das DOM-Element sollte mittels Inline-CSS die entsprechende Hintergrundfarbe annehmen:

Normale Select-Option-HTML-Liste

Diese Liste wurde via Backend durch das (absolut empfehlenswerte!) Plugin Types eingepflegt und die Option-Values enthielten den CSS-kompatiblen Farbcode:

Da die Farbwerte jedoch teils recht nah beieinanderlagen, wollte ich die Select-Liste mit der entsprechenden Hintergrundfarbe unterlegen, so dass der User im Backend sieht, welchen Farbton er auswählt.
Das Plugin generiert folgenden HTML-Code für die Select-Liste:

<select id="wpcf-select-pagecolor-1178946369" name="wpcf[pagecolor]" class="wpcf-form-select form-select select">
	<option value="white" class="wpcf-form-option form-option option">Weiß</option>
	<option value="#cc3f14" class="wpcf-form-option form-option option">Rotorange</option>
	<option value="red" class="wpcf-form-option form-option option">Rot</option>
	<option value="#ffd900" class="wpcf-form-option form-option option">Dunkelgelb</option>
	<option value="#6f0" class="wpcf-form-option form-option option">Leuchtend Hellgrün</option>
	<option value="#24ff21" class="wpcf-form-option form-option option">Hellgrün</option>
	<option value="#0580ff" class="wpcf-form-option form-option option">Hellblau</option>
	<option value="#b2ffff" class="wpcf-form-option form-option option">Helltürkis</option>
	<option value="#ba87ff" class="wpcf-form-option form-option option">Hellviolett</option>
	<option value="#f200c2" class="wpcf-form-option form-option option">Pink</option>
</select>

 

Das Ergebnis sollte so aussehen:

Eingefärbte HTML-Select-Option-Liste

Dazu wollte ich mittels jQuery die value's der option-Tags auslesen, also die Werte, die ich via Types CSS-kompatibel im Backend vorher eingepflegt hatte.

Folgender Code half hier aus:

<!--?php 
function custom_admin_js() {
	$script = '<script type="text/javascript"-->// &lt; ![CDATA['; 	$script .= ' 	jQuery(document).ready( function(){ 		var setBackground = function(){ 			jQuery("select[name=\"wpcf[pagecolor]\"]").css("background-color",  				jQuery("select[name=\"wpcf[pagecolor]\"]").find("option:selected").val() 			)}; 		setBackground(); 		jQuery("select[name=\"wpcf[pagecolor]\"]").find("option").each(function(){ 			jQuery(this).css("background-color", jQuery(this).val()); 		}); 		jQuery("select[name=\"wpcf[pagecolor]\"]").on("change",setBackground); 	}); 	'; 	$script .= ' // ]]&gt;';
	echo $script;
}
add_action('admin_head', 'custom_admin_js');
?&gt;

Wichtig hier ist der add_action - Aufruf, der als 2. Parameter den Namen der Funktion bekommt, die obenstehend deklariert ist und den JavaScript-Code enthält. Dort kann natürlich auch jeder andere Code stehen.

In meinem Fall benötigte ich den obenstehenden jQuery-Code, welcher hier nochmal genauer erläutert wird:

jQuery(document).ready( function(){
	//save background-color setting function for select-element into variable
	var setBackground = function(){
		jQuery("select[name=\"wpcf[pagecolor]\"]").css("background-color", 
			jQuery("select[name=\"wpcf[pagecolor]\"]").find("option:selected").val()
		)};
 
	//call the function to set the background color of the select-element
	setBackground();
 
	//set background-color for all option-tags
	jQuery("select[name=\"wpcf[pagecolor]\"]").find("option").each(function(){
		jQuery(this).css("background-color", jQuery(this).val());
	});
 
	//bind change-event to select-element
	jQuery("select[name=\"wpcf[pagecolor]\"]").on("change",setBackground);
});

Als jQuery-Selector konnte ich auf keine ID zugreifen, da die ID vom Types-Plugin generiert wurde und in jeder Installation anders wäre, daher nutzte ich folgenden Selektor:

select[name="wpcf[pagecolor]"]

Dieser war für meine Zwecke ausreichend, da er eindeutig war. In der Variable setBackground speichere ich mir eine Funktion, die das in der Select-Liste selektierte Option-Tag ausliest und den value-Wert als Hintergrundfarbe des Select-Elements setzt. Danach rufe ich die Funktion auch gleich auf, um den Wert zu setzen. Als rein anonyme Funktion kann ich sie jedoch nicht nutzen, da ich auch bei Farbwechsel den neu ausgewählten Wert setzen wollte. Dazu dient die letzte Zeile, in welcher der Event-Handler "onchange" die Funktion "setBackground()" bei Wechsel des Option-Elements aufruft. Wichtig hierbei ist, dass die Funktion ohne () übergeben wird, da hier nur die Referenz auf die Funktion notwendig ist. Würde man die Klammern mit übergeben, würde die Funktion direkt an dieser Stelle im Code ausgeführt werden und nicht erst bei Aufruf des Change-Events.

Das wars dann auch schon!
Anregungen, Kommentare und Verbesserungsvorschläge sind erwünscht! :-)


Joomla Layout und View-Layout einer Komponente deaktivieren

erstellt am:21. Februar 2012

Soeben hatte ich das Problem, dass ich die Ausgabe in einer selbst entwickelten Komponente bei Joomla deaktivieren wollte, da ich als Ausgabeformat JSON haben wollte oder gegebenenfalls auch XML oder vielleicht sogar nur Text, plain ASCII. Ich habe schon lange nichts mehr mit Joomla gemacht, musste mich also erstmal wieder an das CMS gewöhnen und hab dabei erfreut festgestellt, dass Joomla ja nach MVC-Architektur aufgebaut ist! Da ich in den letzten größeren Projekten hauptsächlich mit Zend gearbeitet hatte, kam mir das nur entgegen. Aber zum eigentlichen Problem:
Nach Recherche fand ich in der Joomla-Doku über Generating JSON output folgende Zeilen, die helfen sollen:

< ?php
//Daten, die später als JSON zurück- bzw. ausgegeben werden sollen.
$data = array('some data');
 
// Lade das Document-Objekt.
$document =& JFactory::getDocument();
 
// Setze MIME Typ für JSON Ausgabe.
$document->setMimeEncoding('application/json');
 
// Dem Header "sagen", dass es sich um eine Datei handelt, die heruntergeladen werden soll. Dateiendung *.json
JResponse::setHeader('Content-Disposition','attachment;filename="'.$view->getName().'.json"');
 
// Ausgabe der Daten JSON-encoded durch die native PHP-Funktion json_encode.
echo json_encode($data);
?>

Da ich die Daten nicht als Datei benötigt habe, sondern die Ausgabe im Browser sehen wollte, hab ich die Zeile, wo der Header für eine Datei gesetzt wird, auskommentiert. Was ich bei vielen Dokus vermisse, ist der Ort, wo dieser Code eingebaut werden soll. Neulinge wissen meist nicht, wohin der Code gehört. Weiterhin kommt dazu, dass es stets mehrere Lösungen gibt, saubere und unsaubere ;) . Für mich klang alles nach View, da es ja um die Ausgabe im Browser ging, daher hab ich den Code in die view.html.php der View der Komponente gepackt. Die Datei liegt unter

com_KOMPONENTENNAME/views/KOMPONENTENNAME/view.html.php

Sie repräsentiert die View der Komponente und lädt das Default-Template unter

com_KOMPONENTENNAME/views/KOMPONENTENNAME/tmpl/default.php

aber das nur am Rande erwähnt. Die Ausgabe jedoch entsprach nicht meinem Wunsch: Es kam der Quelltext vollständig zurück mit html, head, title, meta, body und sonstigen HTML-Tags. Nach weiterer Suche traf ich auf einen Beitrag von 2009 zum Verbergen des Administrator- oder Frontend - Layout - Templates:

JRequest::setVar('tmpl', 'component');

Laut Angabe soll diese Zeile im Controller eingebaut werden. Sie bewirkt, dass das Layout nur den Inhalt der View anzeigt, ohne von einem Template umrahmt bzw. gewrappt zu werden. Ich habe die Zeile direkt hinter die MIME-Anweisung in die View gepackt. Neuladen hat gezeigt, dass jetzt die Menüstruktur der gesamten Seite nicht mehr da war, sondern nur noch der Inhalt der View! ... und leider noch die html, head, title, meta- und body-Tags. Zu XML- oder JSON-Output hatte ich nun nichts brauchbares mehr gefunden außer viele Fragen, wie das denn nun geht, also hab ich mein Suchquery geändert und nach Verwendung mit AJAX gesucht, da man dabei meist auch nur einen bestimmten Datensatz abfragen will ohne das Layout oder anderen Quelltext. Ebenso kann die Antwort dort ja auch JSON sein und mit AJAX weiterverarbeitet werden. Und siehe da, ich wurde fündig bei stackoverflow (wo auch sonst) in einer Antwort mit 0 "likes":

//after $this->display($tpl);
global $mainframe;
$mainframe->close();

Dies war die Lösung. Nun nochmal im Gesamtzusammenhang der Code, der mir JSON als Ausgabe in einer View in der Joomla-Komponente ermöglicht hat:

< ?php
//-- No direct access
defined('_JEXEC') || die('=;)');
jimport('joomla.application.component.view');
class KOMPONENTENNAMEViewKOMPONENTENNAME extends JView
{
    public function display($tpl = null)
    {
		// Get the document object.
		$document =& JFactory::getDocument(); 
		// Set the MIME type for JSON output.
		$document->setMimeEncoding('application/json');
		//Deaktiviert das Template und nutzt nur das Template 
		//der Komponente
		JRequest::setVar('tmpl', 'component');   	
 
		/* Datenverarbeitung aller Daten, beispielsweise ein  
		* Datensatz aus der Datenbank, welcher als Array 
		* zurückgegeben wird. Beispielcode:
		* $model = &$this->getModel();
		* $data = $model->getData();
		* echo json_encode( $data ); 
	    */
 
        parent::display($tpl);
		//Deaktivierung der gesamten Layout-Komponente, 
		//die für html, head, meta und body-Tags zuständig ist
		global $mainframe;
		$mainframe->close();
    }//function
}//class
?>

Der auskommentierte Teil in der Mitte steht für den Part, in welchem die Daten geladen werden, welche später JSON-encoded ausgegeben werden sollen.

Ich hoffe, der Code hilft noch vielen weiteren, die wie ich ewig danach gesucht haben!
Ich bin kein Joomla-Experte und freue mich daher sehr über Verbesserungsvorschläge und Anmerkungen! Die Lösung funktioniert zwar, aber ist sie auch sauber?


ACHTUNG! Warnung vor zustimmen.de – Subdomains mit facebook, schuelervz oder DarkOrbit – Optik

erstellt am:19. Februar 2012

Update 20.02.2012: Die Seiten sind mittlerweile alle von zustimmen.de gesperrt worden :) !

Gerade nach dem Lesen von Mimikama.at's Beitrag über die Warnung zur 50€ Rewe-Gutschein-Aktion (Facebook Fake) bin ich auf die Domain zustimmen.de gestolpert. Es scheint sich dort laut Impressum um eine Seite zu handeln, welche gratis Webspace anbietet unter verschiedenen Subdomains. Dort sind gerade mindestens 3 neue Seiten entstanden, die optisch 1:1-Klone sind. Betroffen davon sind Facebook, SchuelerVZ und das Browsergame "Dark Orbit" mit den Subdomains http://www.xdxaxrxkxoxrxbxixt.zustimmen.de, http://www.xfxaxcxexbxoxoxk.zustimmen.de und http://www.xsxcxhxuxexlxexrxvxz.zustimmen.de. Aufgefallen sind mir die Domains, wegen den kryptisch wirkenden Namen, da stets ein x zwischen jedem "normalen" Buchstaben steht. Die Seiten laden die Optik immer von der zu imitierenden Seite, so dass selbst bei Änderungen auch die Fake-Seiten sich aktualisieren.
FAKE: Screenshot der Facebook - Login-Seite - www_xfxaxcxexbxoxoxk_zustimmen_de
FAKE: Screenshot der SchuelerVZ - Loginseite - www_xsxcxhxuxexlxexrxvxz_zustimmen_de
FAKE: Screenshot der DarkOrbit - Loginseite www_xdxaxrxkxoxrxbxixt_zustimmen_de

Alle Seiten kann man an wesentlichen Punkten sofort als Fälschung erkennen:
Facebook Fälschung erkennen im Vergleich zur Original-Seite

  1. Die URL, also die Webadresse, stimmt nicht überein.
  2. Facebook sollte auch mittels "HTTPS" erreichbar sein. Dann taucht der Firmenname auch direkt neben dem kleinen Facebook-Icon auf. In der Fälschung ist das nicht der Fall.
  3. Hat man die Browsererweiterung von Web of Trust installiert, sieht man sofort, ob die Seite bereits von anderen bewertet ist oder noch nicht. Ist sie bereits als schlechte Webseite bewertet, wäre der Kreis rot. Wer mehr zu der Browsererweiterung lesen will, kann sich wiederum bei Mimikama.at im Download-Bereich belesen

Ich kann an dieser Stelle nur nochmal darauf hinweisen, dass man, bevor man sich egal wo einloggt, die Browserzeile überprüft! Sonst gibt man seine Login-Daten sofort in fremde Hände.
Wer darauf bereits reingefallen ist, sollte sofort sein Passwort ändern und am besten auch seine Emailadresse, mit der man sich dort registriert hat, sofern möglich. Ebenso sollte man sein Passwort für seinen Emailaccount ändern, wenn das Passwort das gleiche ist wie an eingegebener Stelle. Bisher ist dort zwar noch nichts programmiert, was das Auslesen oder Abfangen des Passworts ermöglicht, aber das kann sich schnell ändern!

Ich habe die Seiten bei zustimmen.de alle gemeldet und bin jetzt gespannt, wie schnell die Betreiber die Seiten wieder löschen. Beim Meldeformular von zustimmen.de ist es wichtig, dass man den Link zur Seite mit angibt, sonst kann man keinen Missbrauch melden. Die dort ebenfalls hinterlegten Whois-Informationen, also die Informationen, die die Person angegeben hat, die die Fake-Seiten dort registriert hat sind ebenfalls offensichtlich gefälscht:
xdxarkorbit.zustimmen.de-fake-whois und wurden alle von der gleichen Person registriert.


FAKE: GRATIS 50 € Amazon Geschenkkarte nur für unsere Facebook Fans!!

erstellt am:07. Februar 2012

Letzte Änderung: 9. Februar 2012

Lieber Besucher! Wenn du diese Information hilfreich fandest, freue ich mich über Feedback und natürlich erst recht darüber, wenn du dieses Wissen weiterverbreitest! Vielen Dank!

Mal wieder eines der Gewinnspiele, die exakt nach dem gleichen Muster ablaufen. Gleich zum Start: ES GIBT NIX ZU GEWINNEN! Das ist ein Fake wie auch die anderen Gewinnspiele dieser Sorte. Glaubst du nicht? Dann lade die Seite mal neu, nachdem die Anzahl der verfügbaren Gutscheine runtergetickt ist ...
Hier mal ein Video dazu:

Diese Gewinnspiele sehen immer gleich aus.
50euro Amazon Gutschein nur für facebook Fans Fake Gewinnspiel Screenshot

Auffällig ist die URL: http://amazon.de.gratisaktion.geschenkkarte2012.fan.facebook-service.de/, denn sie wirkt auf den ersten Blick tatsächlich wie eine Domain von amazon.de, da sie ja genau damit startet. Ist sie aber nicht, denn entscheidend ist der hintere Teil der Domain: http://amazon.de.gratisaktion.geschenkkarte2012.fan.facebook-service.de/, denn das ist die eigentliche Webadresse. Die Punkte in der URL dienen der Verwirrung der User.

Mich beruhigt ein wenig, dass die Anzahl der Likes nicht mehr so hoch ist, wie sonst und hoffe, dass sich daraus schließen lässt, dass diese Masche langsam bekannt wird und nicht jeder "liked" und draufklickt. Wenn du regelmäßig über solche Verarschungsgewinnspiele informiert werden willst, dann "like" doch einfach https://www.facebook.com/fakepostings. Dort findest du meist schon alle bekannten Fake-Gewinnspiele, wie das derzeitige von Amazon oder das Fake-Gewinnspiel von Vodafone, worüber schon vor einem Monat berichtet wurde.

Überrascht hat mich bei diesem Gewinnspiel, dass eine deutsche Domain "facebook-service.de" verwendet wurde. Die Domain selbst verweigert "Interessenten" dank 403-Forbidden-HTTP-Code den Zugriff (Stand: 07.02.2012, 17:20 Uhr). Bei der denic.de lassen sich so aber bequem die Inhaberdaten erfragen. Ein gewisser "Maxermann, Mill" ist dort eingetragen. Der Name klingt schon sehr ausgedacht (ich hoffe, ich beleidige damit niemanden! ;) ), die angegebene Adresse in Hamburg scheint jedoch gültig zu sein. Der Hoster selbst ist ein kleiner Hoster, den ich per Mail bereits angeschrieben habe. Mal sehen, was kommt.

Es sei einfach nochmal gesagt:

Zuerst denken, dann klicken!

Das Gewinnspiel würde bei 5.000 Gutscheinen á 50€ mal eben satte 250.000€ kosten.

Überraschung Nummer 2: WÄHREND ich hier grad noch drüber schreibe, hat sich der Inhalt der Webseite kurzzeitig geändert! Plötzlich war dort eine Aktion gegen die Facebook-Timeline.

########### UPDATE 07.02.2012, 18:16 Uhr ###########

Es tut sich was:
50€ Amazon Gutschein nur für Facebook Fans Fake Gewinnspiel Betrug gemeldet
Die Webseite wurde erfolgreich als Betrug gemeldet! Solltet ihr also beim Klick auf die oben genannte und mit Absicht nicht verlinkte URL nicht das oben abgebildete Bild sehen, verwendet ihr die falschen Sicherheitseinstellungen oder den falschen oder einen veralteten Browser! ;)

########### UPDATE 07.02.2012, 19:31 Uhr ###########

Wenn man mal direkt nach der Domain googelt, zeigt die Google-Vorschau an, dass über diese Domain auch schon andere "Gewinnspiele" gelaufen sind:
Gratis HTC Sensation XL mit AudioBeats nur für Facebook Fans Fake Gewinnspiel

Weiterhin kam eine Rückmeldung vom Hoster, dass sie sich darum kümmern werden.

########### UPDATE 08.02.2012, 12:54 Uhr ###########

Nun wurde das Bild ausgetauscht. Die Seite gibt an, ein Gewinnspiel für cards4u zu machen mit genau dem gleichen Inhalt wie beim Amazon-"Gewinnspiel".
Cards4u Facebook Fake Gewinnspiel Geschenkkarte 50€
Die angegebene Firma scheint nicht zu existieren, das Logo ist nirgends bekannt. Die Bilder werden immer bei img-teufel.de gehostet. Will man dort unter "Abuse" die Bilder melden, kommt leider stets die Meldung, dass das Bild unbekannt sei.
Mich verwundert ein wenig, dass die Domain immernoch erreichbar ist, insbesondere, da es eine deutsche Domain ist.

########### UPDATE 08.02.2012, 15:21 Uhr ###########

Ich hab mir die Seite nochmal etwas genauer angeschaut. Im Quelltext findet man z.B. die Zeile, die den Facebook-Icon einbindet.

<link href="http://www.hiphopacademy-hamburg.de/images/stories/logos/facebook-icon.jpg" rel="shortcut icon">
</link>

Die Seite hiphopacademy-hamburg.de weiß davon sicherlich nichts, bekommt aber eine Email von mir. Weiter unten im Quelltext wird noch ein Counter versteckt eingebettet, der zählt, wieviele Seitenaufrufe die Seite bereits hat:
Quelltext mit verstecktem Counter von ipcounter.de
Wie häufig diese Seite aufgerufen wurde, lässt sich also unter http://www.ipcounter.de/stats/68196667 ansehen.
Laut Regeln und AGB von ipcounter.de gilt aber: "Der Counter muss sichtbar auf der Seite eingebunden werden!". Dies ist nicht der Fall. Ich habe die Betreiber darüber informiert.

########### UPDATE 09.02.2012, 12:00 Uhr ###########

Der Hoster hat die Seite jetzt auch endlich entfernt, schien nicht ganz so einfach zu sein, wie ich mir das vorgestellt hatte ;) . Aber endlich ist die Fake-Seite gelöscht!


« Vorherige Einträge   
all-inkl.com webhosting