token = $token; $this->gdata_version = '3.0'; $this->base_url = 'https://docs.google.com/feeds/default/private/full/'; $this->metadata_url = 'https://docs.google.com/feeds/metadata/default'; $this->chunk_size = 524288; // 512 KiB $this->max_resume_attempts = 5; $this->request_timeout = 5; $this->ssl_verify = true; $this->timer = array( 'start' => 0, 'stop' => 0, 'delta' => 0, 'cycle' => 0 ); $this->time_limit = @ini_get( 'max_execution_time' ); if ( ! $this->time_limit && '0' !== $this->time_limit ) $this->time_limit = 30; // default php max exec time } /** * Sets an option. * * @access public * @param string $option The option to set. * @param mixed $value The value to set the option to. */ public function set_option( $option, $value ) { switch ( $option ) { case 'chunk_size': if ( floatval($value) >= 0.5 ) { $this->chunk_size = floatval($value) * 1024 * 1024; // Transform from MiB to bytes return true; } break; case 'ssl_verify': $this->ssl_verify = ( bool ) $value; return true; case 'request_timeout': if ( intval( $value ) > 0 ) { $this->request_timeout = intval( $value ); return true; } break; case 'max_resume_attempts': $this->max_resume_attempts = intval($value); return true; } return false; } /** * Gets an option. * * @access public * @param string $option The option to get. */ public function get_option( $option ) { switch ( $option ) { case 'chunk_size': return $this->chunk_size; case 'ssl_verify': return $this->ssl_verify; case 'request_timeout': return $this->request_timeout; case 'max_resume_attempts': return $this->max_resume_attempts; } return false; } /** * This function makes all the requests to the API. * * @uses wp_remote_request * @access private * @param string $url The URL where the request is sent. * @param string $method The HTTP request method, defaults to 'GET'. * @param array $headers Headers to be sent. * @param string $body The body of the request. * @return mixed Returns an array containing the response on success or an instance of WP_Error on failure. */ private function request( $url, $method = 'GET', $headers = array(), $body = NULL ) { $args = array( 'method' => $method, 'timeout' => $this->request_timeout, 'httpversion' => '1.1', 'redirection' => 0, 'sslverify' => $this->ssl_verify, 'headers' => array( 'Authorization' => 'Bearer ' . $this->token, 'GData-Version' => $this->gdata_version ) ); if ( ! empty( $headers ) ) $args['headers'] = array_merge( $args['headers'], $headers ); if ( ! empty( $body ) ) $args['body'] = $body; return wp_remote_request( $url, $args ); } /** * Returns the feed from a URL. * * @access public * @param string $url The feed URL. * @return mixed Returns the feed as an instance of SimpleXMLElement on success or an instance of WP_Error on failure. */ public function get_feed( $url ) { if ( ! isset( $this->cache[$url] ) ) { $result = $this->cache_feed( $url ); if ( is_wp_error( $result ) ) return $result; } return $this->cache[$url]; } /** * Requests a feed and adds it to cache. * * @access private * @param string $url The feed URL. * @return mixed Returns TRUE on success or an instance of WP_Error on failure. */ private function cache_feed( $url ) { $result = $this->request( $url ); if ( is_wp_error( $result ) ) return $result; if ( $result['response']['code'] == '200' ) { $feed = @simplexml_load_string( $result['body'] ); if ( $feed === false ) return new WP_Error( 'invalid_data', "Could not create SimpleXMLElement from '" . $result['body'] . "'." ); $this->cache[$url] = $feed; return true; } return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to get '" . $url . "'. Response body: " . $result['body'] ); } /** * Deletes a resource from Google Docs. * * @access public * @param string $id Gdata Id of the resource to be deleted. * @return mixed Returns TRUE on success, an instance of WP_Error on failure. */ public function delete_resource( $id ) { $headers = array( 'If-Match' => '*' ); $result = $this->request( $this->base_url . $id . '?delete=true', 'DELETE', $headers ); if ( is_wp_error( $result ) ) return $result; if ( $result['response']['code'] == '200' ) return true; return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to delete resource '" . $id . "'. The resource might not have been deleted." ); } /** * Get the resumable-create-media link needed to upload files. * * @access private * @param string $parent The Id of the folder where the upload is to be made. Default is empty string. * @return mixed Returns a link on success, instance of WP_Error on failure. */ private function get_resumable_create_media_link( $parent = '' ) { $url = $this->base_url; if ( $parent ) $url .= $parent; $feed = $this->get_feed( $url ); if ( is_wp_error( $feed ) ) return $feed; foreach ( $feed->link as $link ) if ( $link['rel'] == 'http://schemas.google.com/g/2005#resumable-create-media' ) return ( string ) $link['href']; return new WP_Error( 'not_found', "The 'resumable_create_media_link' was not found in feed." ); } /** * Get used quota in bytes. * * @access public * @return mixed Returns the number of bytes used in Google Docs on success or an instance of WP_Error on failure. */ public function get_quota_used() { $feed = $this->get_feed( $this->metadata_url ); if ( is_wp_error( $feed ) ) return $feed; return ( string ) $feed->children( "http://schemas.google.com/g/2005" )->quotaBytesUsed; } /** * Get total quota in bytes. * * @access public * @return string|WP_Error Returns the total quota in bytes in Google Docs on success or an instance of WP_Error on failure. */ public function get_quota_total() { $feed = $this->get_feed( $this->metadata_url ); if ( is_wp_error( $feed ) ) return $feed; return ( string ) $feed->children( "http://schemas.google.com/g/2005" )->quotaBytesTotal; } /** * Function to prepare a file to be uploaded to Google Docs. * * The function requests a URI for uploading and prepends a new element in the resume_list array. * * @uses wp_check_filetype * @access public * * @param string $file Path to the file that is to be uploaded. * @param string $title Title to be given to the file. * @param string $parent ID of the folder in which to upload the file. * @param string $type MIME type of the file to be uploaded. The function tries to identify the type if it is omitted. * @return mixed Returns the URI where to upload on success, an instance of WP_Error on failure. */ public function prepare_upload( $file, $title, $parent = '', $type = '' ) { if ( ! @is_readable( $file ) ) return new WP_Error( 'not_file', "The path '" . $file . "' does not point to a readable file." ); // If a mime type wasn't passed try to guess it from the extension based on the WordPress allowed mime types if ( empty( $type ) ) { $check = wp_check_filetype( $file ); $this->upload_file_type = $type = $check['type']; } $size = filesize( $file ); $body = '' . $title . ''; $headers = array( 'Content-Type' => 'application/atom+xml', 'X-Upload-Content-Type' => $type, 'X-Upload-Content-Length' => (string) $size ); $url = $this->get_resumable_create_media_link( $parent ); if ( is_wp_error( $url ) ) return $url; $url .= '?convert=false'; // needed to upload a file $result = $this->request( $url, 'POST', $headers, $body ); if ( is_wp_error( $result ) ) return $result; if ( $result['response']['code'] != '200' ) return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to get '" . $url . "'." ); $this->file = array( 'path' => $file, 'size' => $size, 'location' => $result['headers']['location'], 'pointer' => 0 ); // Open file for reading. if ( !$this->file['handle'] = fopen( $file, "rb" ) ) return new WP_Error( 'open_error', "Could not open file '" . $file . "' for reading." ); // Start timer $this->timer['start'] = microtime( true ); return $result['headers']['location']; } /** * Resume an upload. * * @access public * @param string $file Path to the file which needs to be uploaded * @param string $location URI where to upload the file * @return mixed Returns the next location URI on success, an instance of WP_Error on failure. */ public function resume_upload( $file, $location ) { if ( ! @is_readable( $file ) ) return new WP_Error( 'not_file', "The path '" . $this->resume_list[$id]['path'] . "' does not point to a readable file. Upload has been canceled." ); $size = filesize( $file ); $headers = array( 'Content-Range' => 'bytes */' . $size ); $result = $this->request( $location, 'PUT', $headers ); if( is_wp_error( $result ) ) return $result; if ( '308' != $result['response']['code'] ) { if ( '201' == $result['response']['code'] ) { $feed = @simplexml_load_string( $result['body'] ); if ( $feed === false ) return new WP_Error( 'invalid_data', "Could not create SimpleXMLElement from '" . $result['body'] . "'." ); $this->file['id'] = substr( ( string ) $feed->children( "http://schemas.google.com/g/2005" )->resourceId, 5 ); return true; } return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to resume the upload of file '" . $file . "'." ); } if( isset( $result['headers']['location'] ) ) $location = $result['headers']['location']; $pointer = $this->pointer( $result['headers']['range'] ); $this->file = array( 'path' => $file, 'size' => $size, 'location' => $location, 'pointer' => $pointer ); // Open file for reading. if ( !$this->file['handle'] = fopen( $file, "rb" ) ) return new WP_Error( 'open_error', "Could not open file '" . $file . "' for reading." ); // Start timer $this->timer['start'] = microtime( true ); return $location; } /** * Work out where the file pointer should be from the range header. * * @access private * @param string $range The range HTTP response header. * @return integer Returns the number of bytes that have been uploaded. */ private function pointer( $range ) { return intval(substr( $range, strpos( $range, '-' ) + 1 )) + 1; } /** * Uploads a chunk of the file being uploaded. * * @access public * @return mixed Returns TRUE if the chunk was uploaded successfully; * returns Google Docs resource ID if the file upload finished; * returns an instance of WP_Error on failure. */ public function upload_chunk() { if ( !isset( $this->file['handle'] ) ) return new WP_Error( "no_upload", "There is no file being uploaded." ); $cycle_start = microtime( true ); fseek( $this->file['handle'], $this->file['pointer'] ); $chunk = @fread( $this->file['handle'], $this->chunk_size ); if ( false === $chunk ) { $is_file = (is_file($this->file['path'])) ? 1 : 0; $is_readable = (is_readable($this->file['path'])) ? 1 : 0; return new WP_Error( 'read_error', "Failed to read from file (path: ".$this->file['path'].", size: ".$this->file['size'].", pointer: ".$this->file['pointer'].", is_file: $is_file, is_readable: $is_readable)"); } $chunk_size = strlen( $chunk ); $bytes = 'bytes ' . (string)$this->file['pointer'] . '-' . (string)($this->file['pointer'] + $chunk_size - 1) . '/' . (string)$this->file['size']; $headers = array( 'Content-Range' => $bytes ); $result = $this->request( $this->file['location'], 'PUT', $headers, $chunk ); if ( !is_wp_error( $result ) ) if ( '308' == $result['response']['code'] ) { if ( isset( $result['headers']['range'] ) ) $this->file['pointer'] = $this->pointer( $result['headers']['range'] ); else $this->file['pointer'] += $chunk_size; if ( isset( $result['headers']['location'] ) ) $this->file['location'] = $result['headers']['location']; if ( $this->timer['cycle'] ) $this->timer['cycle'] = ( microtime( true ) - $cycle_start + $this->timer['cycle'] ) / 2; else $this->timer['cycle'] = microtime(true) - $cycle_start; return $this->file['location']; } elseif ( '201' == $result['response']['code'] ) { fclose( $this->file['handle'] ); // Stop timer $this->timer['stop'] = microtime(true); $this->timer['delta'] = $this->timer['stop'] - $this->timer['start']; if ( $this->timer['cycle'] ) $this->timer['cycle'] = ( microtime( true ) - $cycle_start + $this->timer['cycle'] ) / 2; else $this->timer['cycle'] = microtime(true) - $cycle_start; $this->file['pointer'] = $this->file['size']; $feed = @simplexml_load_string( $result['body'] ); if ( $feed === false ) return new WP_Error( 'invalid_data', "Could not create SimpleXMLElement from '" . $result['body'] . "'." ); $this->file['id'] = substr( ( string ) $feed->children( "http://schemas.google.com/g/2005" )->resourceId, 5 ); return true; } // If we got to this point it means the upload wasn't successful. fclose( $this->file['handle'] ); if ( is_wp_error( $result ) ) return $result; return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to upload a file chunk." ); } /** * Get the resource ID of the most recent uploaded file. * * @access public * @return string The ID of the uploaded file or an empty string. */ public function get_file_id() { if ( isset( $this->file['id'] ) ) return $this->file['id']; return ''; } /** * Get the upload speed recorded on the last upload performed. * * @access public * @return integer Returns the upload speed in bytes/second or 0. */ public function get_upload_speed() { if ( $this->timer['cycle'] > 0 ) if ( $this->file['size'] < $this->chunk_size ) return $this->file['size'] / $this->timer['cycle']; else return $this->chunk_size / $this->timer['cycle']; return 0; } /** * Get the percentage of the file uploaded. * * @return float Returns a percentage on success, 0 on failure. */ public function get_upload_percentage() { if ( isset( $this->file['path'] ) ) return $this->file['pointer'] * 100 / $this->file['size']; return 0; } /** * Returns the time taken for an upload to complete. * * @access public * @return float Returns the number of seconds the last upload took to complete, 0 if there has been no completed upload. */ public function time_taken() { return $this->timer['delta']; } public function get_content_link( $id, $title ) { $feed = $this->get_feed($this->base_url . $id); if ( is_wp_error( $feed ) ) return $feed; if ( $feed->title != $title ) return new WP_Error( 'bad_response', "Unexpected response"); $att = $feed->content->attributes(); return $att['src']; } public function download_data( $link, $saveas ) { $result = $this->request( $link ); if ( is_wp_error( $result ) ) return $result; if ( $result['response']['code'] != '200' ) return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to get '" . $url . "'." ); file_put_contents($saveas, $result['body']); } }