Using the Duolingo API with PHP to Get Profile Data

Duolingo logo

For those who are interested in learning a new language, Duolingo is a great place to start. It’s free, and some recent studies suggest that it’s more effective than Rosetta Stone (which is not free).

I’ve been using Duolingo for the past few months and decided to do something similar with my Duolingo profile as I did with my Code School profile – put it on the sidebar of this blog.

However, I soon discovered that I could not do that, as Duolingo’s terms of service banned Web scraping. Fortunately, I discovered that an API is available!

Using this API, I was able to write a PHP script that I host which will pull a user’s data, then filter out only language information that is pertinent (to this particular task).

Source for duolingo.php:

<?php

    function file_get_contents_curl($url){

       $ch = curl_init();
       curl_setopt($ch, CURLOPT_URL, $url);
       curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
       curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
       curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
       $data = curl_exec($ch);
       curl_close($ch);
       flush();
       return $data;
    }

    /* gets username from querystring */
    $username = $_GET['nick'];

    /* builds profile URL on Duolingo */
    $profile_url =  'https://www.duolingo.com/api/1/users/show?username=' . $username;

    /* get JSON from Duolingo API */
    $json = file_get_contents_curl($profile_url);  

    /* decode JSON into associative array */
    $results = json_decode($json,true);

    /* get nested array "languages" */
    $languages = $results['languages'];
    //var_dump($languages);

    /* create inner div */
    $html = '<div id="duolingo_inner">';

    /* add anchor tag to user's profile */
    $html = $html . '<a href="https://www.duolingo.com/' . $username . '" target="_blank">';

    /* add Duolingo logo from Internet */
    $html = $html . '<img src="https://i.imgur.com/mF9aokL.png" alt="' . $username . '\'s Duolingo profile" />';

    /* loop through languages to get only active ones */
    foreach ($languages as $language){
    	$temp_html = "
<table>\n";
    	$include = 0;
    	//var_dump($language);
    	foreach($language as $attribute=>$value){
    		if ($attribute == 'points' && $value > 0) {
    			$include = 1;
    		}
    		if ($attribute == 'language_string') {
    			$temp_html = $temp_html . "<tr><th colspan='2'><h2>$value</h2></th></tr>\n";
    		}
    		if ($attribute == 'points') {
    			$temp_html = $temp_html . "<tr><td class='tdleft'>Points: </td><td class='tdright'>$value</td></tr>\n";
    		}
    		if ($attribute == 'level') {
    			$temp_html = $temp_html . "<tr><td class='tdleft'>Level: </td><td class='tdright'>$value</td></tr>\n";
    		}
    		if ($attribute == 'to_next_level') {
    			$temp_html = $temp_html . "<tr><td class='tdleft'>Points to Next Level: </td><td class='tdright'>$value</td></tr>\n";
    		}
    		//echo $attribute . ' ' . $value . "
\n";
    	}
    	if ($include == 1) {
    		$temp_html = $temp_html . "</table>\n";
    		$html = $html . $temp_html;
    	}
    }

    /* close anchor tag and inner div */
    $html = $html . "</a></div>";
    
    /* return the html */
    echo $html;

    /* uncommenting below will show full contents of JSON from languages section */
    //$languages_json = json_encode($languages);
    //echo $languages_json;
?>

If you want to put this on WordPress, create a Text widget and insert the following code. This assumes that you are hosting duolingo.php in the root directory. Also, change the username in the querystring as appropriate.

<style scoped="">
#duolingo_inner {
   border: 1px solid white;
   text-align: center;
   vertical-align: middle;
   display:block;
   margin-left: auto;
   margin-right: auto;
   margin-bottom: 10px;
}
.tdleft {
 text-align: left;
}
.tdright {
 text-align: right;
}
#duolingo_inner table {
   margin-left: auto;
   margin-right: auto;
}
#duolingo_inner img {
   display:block !important;
   margin-left: auto;
   margin-right: auto;
   width: 50%;
}
</style>
<div id="duolingo_outer">
</div>
<script>
(function($) {
$("#duolingo_outer").load("/duolingo.php?nick=deepinthecode");
})(jQuery);
</script>

And that’s it! You will likely need to change the CSS if you don’t like how it looks on your browser as my widget inherits from my WP theme.

Adding the Property Attribute to Style Sheet Declarations in WordPress for HTML5 Validation

HTML5 validator error

Of all the blogging platforms I’ve tried, WordPress is my favorite. If it is self-hosted, it is easy to customize.

One of the pitfalls of customization, however, is how easy it can be for a plugin of dubious quality to wreak havoc on your site. One of the problems I have had is that while WordPress does an excellent job at rendering valid HTML5 code, it is not difficult at all for a plugin to cause validation errors, even as a result of calling built-in WP functions.

In this case, a style sheet declaration was being triggered by a plugin using WP’s wp_register_style function. This function and the wp_enqueue_style function end up using the WP_Styles class, which is found in the /wp-includes/class.wp-styles.php file.

This class is called by WordPress and themes to insert link tags in the head section of your page. However, a plugin or theme can use the functions mentioned above to declare a CSS file anywhere on the page, inside or outside the head section.

This is not inherently problematic in that the page will be rendered normally. However, if the link tag is declared in the body of the HTML document rather than the head (as it happened in my case), the HTML5 code will not correctly validate if no property tag is present.

HTML5 validator error
No property attribute in a link tag not in the head? It won’t validate.

This line of code looks like it comes from a plugin, specifically the Comments Evolved (formerly known as “Google+ Comments for WordPress”) plugin. However, it is not – a little hacking on WordPress is in order here. Also, I’m going to report this as an issue so that the fix may be included in the next update.

If you open the file /wp-includes/class.wp-styles.php and add “property=’stylesheet'” to two lines (93 and 103 in my editor) as below, you shouldn’t see this problem again.

		$tag = apply_filters( 'style_loader_tag', "<link rel='$rel' property='stylesheet' id='$handle-css' $title href='$href' type='text/css' media='$media' />\n", $handle, $href );
		if ( 'rtl' === $this->text_direction && isset($obj->extra['rtl']) && $obj->extra['rtl'] ) {
			if ( is_bool( $obj->extra['rtl'] ) || 'replace' === $obj->extra['rtl'] ) {
				$suffix = isset( $obj->extra['suffix'] ) ? $obj->extra['suffix'] : '';
				$rtl_href = str_replace( "{$suffix}.css", "-rtl{$suffix}.css", $this->_css_href( $obj->src , $ver, "$handle-rtl" ));
			} else {
				$rtl_href = $this->_css_href( $obj->extra['rtl'], $ver, "$handle-rtl" );
			}

			/** This filter is documented in wp-includes/class.wp-styles.php */
			$rtl_tag = apply_filters( 'style_loader_tag', "<link rel='$rel' property='stylesheet' id='$handle-rtl-css' $title href='$rtl_href' type='text/css' media='$media' />\n", $handle, $rtl_href );

			if ( $obj->extra['rtl'] === 'replace' ) {
				$tag = $rtl_tag;
			} else {
				$tag .= $rtl_tag;
			}
		}

This will not fix any plugins or themes that use another mechanism to declare style sheets outside the head, but it has fixed several errors caused by plugins.