List all of your Basecamp Classic files & attachments

Less of a blog, and more of a utility post - if you have PHP and want to find out what's eating up your Basecamp file-space allocation, this little script might help you.

Insert your API token in the right spot, insert your email in the user agent, and execute from the command line.



function fetch( $uri )
{
    /* Adjust these three variables */

    //  1 - your Basecamp URL
    $basecamp_url   = 'https://yourbasecamp.basecamphq.com/';

    // 2 - get this from your "Account" section
    $username       = 'YOURAPITOKEN';

    // 3 - enter your main account email, or face potential throttling
    $youremail      = 'youremail@youremail.com';


    $password       = 'notused';
    $session        = curl_init();

    curl_setopt($session, CURLOPT_URL, $basecamp_url . $uri );
    curl_setopt($session, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
    curl_setopt($session, CURLOPT_HTTPGET, 1);
    curl_setopt($session, CURLOPT_HEADER, false);
    curl_setopt($session, CURLOPT_HTTPHEADER, array('Accept: application/xml', 'Content-Type: application/xml'));
    curl_setopt($session, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($session, CURLOPT_USERPWD, $username . ":" . $password);
    curl_setopt($session, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($session, CURLOPT_USERAGENT, "Circlical Account Maintenance ($youremail)" );

    $response = curl_exec($session);
    curl_close($session);

    return $response;
}

function fffsize( $size )
{
    $base   = @log($size) / log(1024);
    $suffix = array("", "k", "M", "G", "T")[@floor($base)];
    return @round( pow(1024, $base - @floor($base)), 2 ) . $suffix;
}

function info( $str )
{
    echo "\033[01;36m" . $str  . " \033[0m\n";
}

$attachment_list    = [];
$projects           = simplexml_load_string( fetch( 'projects.xml' ) );

foreach( $projects as $p )
{

    $project_name   = $p->name;
    $project_id     = $p->id;
    $ncount         = 0;

    info( "Scanning project $project_name..." );

    do{
        $attachments = simplexml_load_string( fetch( "projects/{$project_id}/attachments.xml?n={$ncount}" ) );
        if( count( $attachments->attachment ) )
        {
            foreach( $attachments as $a )
            {
                $attachment_list[] = array(
                    'project'   => (string)$project_name,
                    'file'      => (string)$a->name,
                    'size'      => (string)$a->{'byte-size'},
                );

                $ncount++;
            }
        }
    }
    while( count( $attachments->attachment ) >= 100 );
}

// now sort the darned attachments
usort( $attachment_list, function( $a, $b ){
    return $b['size'] - $a['size'];
});

$mask = "| %-20.20s | %-30.30s | %15.15s |\n";


echo printf( $mask, "Project", "File", "Size" );

foreach( $attachment_list as $a )
{
    echo printf( $mask, $a['project'], $a['file'], fffsize( $a['size'] ) );
}



Pretty print your ZF2 JsonModel the easy way

Looking for a quick cut and paste, I found crazy composer modules and overcomplicated blog articles.  Thought I'd help Occam out a bit with this very simple blog entry!  BTW I lied, we're going to skip JsonModel.

 

Sorry for the crappy indentation, trying to make it fit in Squarespace.  JSON_PRETTY_PRINT came along with PHP 5.4

 


        public function prettyJsonAction()
        {
            $contacts = array( 'foo' => 'bar' );
            $response = $this->getResponse();

            $response
                ->getHeaders()
                ->addHeaderLine( 'Content-Type', 'application/json' );

            $response
                ->setContent( 
                    json_encode( $contacts, JSON_PRETTY_PRINT )
                );

            return $response;
        }
  
  

Getting Zend Framework 2 and Google's API to play nice

Need to use Google's API from your ZF2 app?  Tired of parsing through StackOverflow and Google's multiple document versions? You're in the right place.  In my particular case, I needed to connect to Google's calendar service.  Examples below.


Setup - ZF2

We're going to make use of the google/apiclient composer package for all of this.  Edit your composer.json and add "google/apiclient": "1.0.*@dev"

You're also going to need an oAuth callback route.  When your app authenticates with Google, it sends a passback token that the Google client uses to generate an auth token.  For now, create a route in a module of your choice, that leads to a controller that contains 'callbackAction'.  I called mine GoogleController, you'll see its code below.


Setup - Google

Most of the confusion that was evidenced, exists at this step.  There's a new UI rolled out, the docs don't match, and the names of the keys referenced in the docs are a function of these visuals.  Head over to https://console.developers.google.com/, and access the Project you would like to connect (create one if required).

Under APIS & AUTH on the left hand side, select APIs.  Here, "turn on" all of the "features" that your program needs.

Next, under Credentials, click on the Create a new Client ID button.  This will show you a modal window, where you have several options.  Select Web Application, and fill out the text boxes at the bottom (with your App's URLs).  Pay special attention to the second, that's the redirect URI whose callback route you configured earlier (callbackAction).  Click Create Client ID, and you're done.

Screenshot of the Create Client ID panel

Screenshot of the Create Client ID panel

Next, under Public API access at the bottom left, click on Create new Key.  This opens another small modal, where you will click on Server key.  A last panel appears which asks you which IP addresses you want to whitelist.  Type them in, and close the modal.  This created a table in the main view with API KEY.  This is commonly called Developer Key in the apps.

Gather all the details...

You can pretty much fill in this worksheet at this stage, we'll reference it in the code that follows:

  • DEVELOPER_KEY: "Key for server applications" > "API KEY"
  • CLIENT_ID: "Client ID for web application" > "CLIENT ID"
  • CLIENT_SECRET: "Client ID for web application" > "CLIENT SECRET"
  • CALLBACK: The callback URI you defined


Into Zend Framework...

The Google API that you installed with composer (you ran your php composer.phar selfupdate and php composer.phar update right?) really is a throwback to old university assignments.  The profs would come up with the most horrendous combinations of words and underscores.  We're going to hide that ugliness away in your main Module.  Create a factory in your getServiceConfig() that looks like:


'GoogleClient' => function( $sm ){
    $client = new \Google_Client();
    $client->setAccessType('online');
    $client->setApplicationName('doesnotmatter');
    $client->setClientId( CLIENT_ID );
    $client->setClientSecret( CLIENT_SECRET );
    $client->setRedirectUri( CALLBACK );
    $client->setDeveloperKey( DEVELOPER_KEY );
    return $client;
},

This is the client we'll use to establish "scope", which is critical to your usage of the API.  In my case, I wanted to schedule a Calendar event via AJAX.  I added a second factory for GoogleCalendar, like so:


'GoogleCalendar' => function( $sm ){
    $client = $sm->get('GoogleClient');
    $client->setScopes( array(
        'https://www.googleapis.com/auth/calendar'
    ));

    if( isset( $_SESSION['google_access_token'] ) )
        $client->setAccessToken( $_SESSION['google_access_token'] );

    if( !$client->getAccessToken() )
        return new \Application\Model\Google\AuthRequest( $client->createAuthUrl() );

    $calendar = new \Google_Service_Calendar( $client );
    return $calendar;
}

The clever you notices an interjected AuthRequest class to signal that the app (at that point in time) had no rights to adjust calendars.  It's a vehicle class, that exists since factories must return objects, it also cleans up the Controller code.  Here's my AuthRequest class, you can do this any way you need:



namespace Application\Model\Google;

class AuthRequest
{

    public $auth_url;


    public function __construct( $url )
    {
        $this->auth_url = $url;
    }

    /**
     * @param mixed $auth_url
     */
    public function setAuthUrl($auth_url)
    {
        $this->auth_url = $auth_url;
    }

    /**
     * @return mixed
     */
    public function getAuthUrl()
    {
        return $this->auth_url;
    }

}

ZF2 Application Flow

  • A modal window appears in my front end, which sends an AJAX request to a controller
  • The controller munges the data received, and tries to schedule the calendar events
  • It's possible that an oAuth dialog needs to be shown, handle it.
  • Conversely, possible that oAuth is already taken care of, schedule the events

Controller Action


function scheduleEvent
{
    // app-specific data processing

    try
    {
    $calendar = $this->getServiceLocator()->get('GoogleCalendar');

     if( $calendar instanceof AuthRequest )
         return new JsonModel( array( 'oauth' => true, 'url' => $calendar->getAuthUrl() ) );

     /** @var \Google_Service_Calendar $calendar */
     $event = new \Google_Service_Calendar_Event();
     $event->setSummary( "This is a test" );
     $dts = new \Google_Service_Calendar_EventDateTime();
     $dts->setDateTime( date( "Y-m-d\\TH:i:s.000-04:00" ) );
     $event->setStart( $dts );

     $dte = new \Google_Service_Calendar_EventDateTime();
     $dte->setDateTime( date( "Y-m-d\\TH:i:s.000-04:00", time() + 1800 ) );
     $event->setEnd( $dte );

     $att = array();
     foreach( $emails as $e )
     {
         $attendee = new \Google_Service_Calendar_EventAttendee();
         $attendee->setEmail( $e );
         $att[] = $attendee;
     }

     $event->setAttendees( $att );
     $x = $calendar->events->insert( "primary", $event, array( 'sendNotifications' => true ) );
            return new JsonModel( array( 'success' => true ) );
    }
    catch( \Exception $x )
    {
        return new JsonModel( array( 'success' => false, 'message' => $x->getMessage() ) );
    }
}


On the front end, the AJAX call looked like this.  In a nutshell, if it sees oauth in the json return, it triggers the Google callback


$.getJSON( '/sales/prospect/case-study', d, function( j ){
    if(j.oauth)
    {
        var popup = window.open(j.url, '_blank', 'width=500px,height=650px' );
        noty({
            layout: 'bottom',
            type: 'information',
            text: "You need to give Central access to your Google Calendar, please authorize Central and try again.",
            timeout: 10000
        });
    }
    else
    {
        if(j.success)
        {
            $("#act_case").modal('hide');
            showSuccess( "Case study successfully scheduled, nice work." );
        }
        else
        {
            showError(j.message);
        }
    }
}).always( function(){
    btn.trigger('ladda-stop');
});

What this does, is open up a Google authorization dialog.  The end user then selects their Google account, authorizes your app, and is driven to your callback URI.  Here's the code to handle the callback.  The official Google PHP docs are missing the parameter to 'authenticate'


public function callbackAction()
{
    $client = $this->getServiceLocator()->get('GoogleClient');
    if( $code = $this->params()->fromQuery('code') )
    {
        $client->authenticate( $code );
        $_SESSION['google_access_token'] = $client->getAccessToken();
    }

    $vm = new ViewModel();
    $vm->setTemplate('application/index/blank_raw' );
    $vm->setVariable( 'output', "" );
    $vm->setTerminal( true );

    return $vm;
}

Hope this helps! Drop me a note if I missed a critical detail that could have saved you time!  Me, I wish I'd found this article yesterday! ;)








Zend Framework 2 - Subqueries (Subselect) and Table Gateway

This one can be a bit of a pain, and I had a few folks ask me how to do this, thought I'd share here and pass a link around.  The question takes many shapes!

How can I perform a subquery in ZF2?

How do I properly escape a subquery in my Table Gateway?

I'm trying to echo my query to debug it and am getting warning messages from ZF2?

It's not terribly difficult, but the docs are a bit piecemeal on the net.  I've tried to find links on StackOverflow to help as well, but haven't found any good ones.  Here goes - hope it helps!

 

The function in your AbstractTableGateway will look like such, pay attention to fetchAllWithVotes:


namespace Application\Model\Table;

use Zend\Db\Adapter\Adapter;
use Zend\Db\Adapter\AdapterAwareInterface;
use Zend\Db\Sql\Expression;
use Zend\Db\Sql\Select;
use Zend\Db\TableGateway\AbstractTableGateway;

class SongsTable extends AbstractTableGateway implements AdapterAwareInterface
{
    protected $table = 'songs';

    public function setDbAdapter( Adapter $adapter)
    {
        $this->adapter = $adapter;
    }

    public function fetchAllWithVotes( $user_id )
    {
        $adapter   = $this->getAdapter();
        $resultset = $this->select( function( Select $select ) use ( $user_id, $adapter ) {

            $sub1   = new Select( 'votes' );
            $sub1->columns( array( 'total' => new Expression('COUNT(*)') ) )
                    ->where( array( 'id' => new Expression( 'song_id' ) ) );

            $sub2   = new Select( 'votes' );
            $sub2->columns( array( 'total' => new Expression('COUNT(*)') ) )
                    ->where( array( 'id' => new Expression( 'song_id' ) ) )
                        ->where( array( 'user_id' => $user_id ) );

            $select->columns(
                array(
                    'id',
                    'title',
                    'ytlink',
                    'total_votes' => new \Zend\Db\Sql\Expression( '?', array( $sub1 ) ),
                    'your_votes' => new \Zend\Db\Sql\Expression( '?', array( $sub2 ) ),
                )
            );

            var_dump( $select->getSqlString( $adapter->getPlatform() ) );

        });

        return $resultset->toArray();
    }
}

Pretty self-explanatory - all the guts are there!  Hope this helps :)