So, here's the skinny.
I have an HTML form (of which there will be many depending on the Letter template) that passes the form data into a Letter Template via PHP. THIS...I have successfully done.
However, what I need is to pass the data INTO the template, THEN turn that template INTO a PDF.
Any suggestions? AND......GO
I've recently use pdftk (server) to do so: https://www.pdflabs.com/tools/pdftk-server/
First, install it on a webserver or locally.
Then, here's a PHP class I adapted from the web (copy it and name it PdfFormToPdftk.php):
<?php
class PdfFormToPdftk {
/*
* Path to raw PDF form
* #var string
*/
private $pdfurl;
/*
* Path to PDFKTK
* #var string
*/
private $pdftkpath;
/*
* Path to temp files
* #var string
*/
private $tmppath;
/*
* Errors
* #var string
*/
private $errors;
/*
* Last command done
* #var string
*/
private $lastcmd;
/*
* Form data
* #var array
*/
private $data;
/*
* Path to filled PDF form
* #var string
*/
private $output;
/*
* Flag for flattening the file
* #var string
*/
private $flatten;
public function __construct($pdfurl, $data, $tmppath, $pdftkpath = '/usr/bin/pdftk') {
$this->pdfurl = $pdfurl;
$this->data = $data;
$this->tmppath = $tmppath;
$this->pdftkpath = $pdftkpath;
}
private function tempfile() {
return tempnam($this->tmppath, gethostname());
}
public function fields($pretty = false) {
$tmp = $this->tempfile();
exec("{$this->pdftkpath} {$this->pdfurl} dump_data_fields > {$tmp}");
$con = file_get_contents($tmp);
unlink($tmp);
return $pretty == true ? nl2br($con) : $con;
}
private function makeFdf() {
$fdf = '%FDF-1.2
1 0 obj<</FDF<< /Fields[';
foreach ($this->data as $key => $value) {
$fdf .= '<</T(' . $key . ')/V(' . $value . ')>>';
}
$fdf .= "] >> >>
endobj
trailer
<</Root 1 0 R>>
%%EOF";
$fdf_file = $this->tempfile();
file_put_contents($fdf_file, $fdf);
return $fdf_file;
}
public function flatten() {
$this->flatten = ' flatten';
return $this;
}
private function generate() {
$fdf = $this->makeFdf();
$this->output = $this->tempfile();
$cmd = "{$this->pdftkpath} {$this->pdfurl} fill_form {$fdf} output {$this->output}{$this->flatten} 2>&1";
$this->lastcmd = $cmd;
exec($cmd, $outputAndErrors, $returnValue);
$this->errors = $outputAndErrors;
unlink($fdf);
}
public function save($path = null) {
if (is_null($path)) {
return $this;
}
if (!$this->output) {
$this->generate();
}
$dest = pathinfo($path, PATHINFO_DIRNAME);
if (!file_exists($dest)) {
mkdir($dest, 0775, true);
}
if (!copy($this->output, $path)) {
echo "failed to copy $path...\n";
}
unlink($this->output);
$this->output = $path;
return $this;
}
public function download() {
if (!$this->output) {
$this->generate();
}
$filepath = $this->output;
if (file_exists($filepath)) {
header('Content-Description: File Transfer');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename=' . uniqid(gethostname()) . '.pdf');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
}
}
}
You could use it like this in some script:
<?php
require_once 'PdfFormToPdftk.php';
$datas = ['firstname' => 'Foo', 'lastname' => 'Bar'];
$output_file = 'yourfile.pdf';
$template_pdf_file = 'templatefile.pdf'; //where your virgin pdf template contains at least 'firstname' and 'lastname' as editable fields. You might want to use Adobe Acrobat Pro and save it as a Adobe Static PDF Form
$path_to_pdftk_server = '/opt/pdflabs/pdftk/bin/pdftk'; // type 'which pdftk' in your console to find yours
$pdf = new PdfFormToPdftk($template_pdf_file, $datas, '/', $path_to_pdftk_server);
$file = $pdf->save($output_file);
Related
I'm trying to serve large audio files from google cloud storage with seeking support.
I have difficulties understanding php fopen and google stream wrapper working together.
When fopen is called it immediately calls stream_open from google StreamWrapper class.
However im unable to pass options to it through fopen context. I would like to set bitwise option 0b10000 as its STREAM_MUST_SEEK option. $flags parameter is always 0.
https://www.php.net/manual/en/streamwrapper.stream-open
Documentation shows there are atleast two options you can set, but it doesnt tell where you can set them.
Without $flag set to 0b10000 im getting:
PHP Warning: stream_copy_to_stream(): Failed to seek to position 85721088 in the stream in /home/project/src/Classes/StreamResponse.php on line 296
If i set $flags to 0b10000 it works and supports seeking.
$opts = array(
'gs' => array('key' => 'value')
);
$context = stream_context_create($opts);
$out = fopen('php://output', 'wb');
$file = fopen($this->file->getPathname(), 'rb', false, $context);
stream_copy_to_stream($file, $out, $this->maxlen, $this->offset);
fclose($out);
fclose($file);
/**
* Callback handler for when a stream is opened. For reads, we need to
* download the file to see if it can be opened.
*
* #param string $path The path of the resource to open
* #param string $mode The fopen mode. Currently only supports ('r', 'rb', 'rt', 'w', 'wb', 'wt')
* #param int $flags Bitwise options STREAM_USE_PATH|STREAM_REPORT_ERRORS|STREAM_MUST_SEEK
* #param string $openedPath Will be set to the path on success if STREAM_USE_PATH option is set
* #return bool
*/
public function stream_open($path, $mode, $flags, &$openedPath)
{
$client = $this->openPath($path);
// strip off 'b' or 't' from the mode
$mode = rtrim($mode, 'bt');
$options = [];
if ($this->context) {
$contextOptions = stream_context_get_options($this->context);
if (array_key_exists($this->protocol, $contextOptions)) {
$options = $contextOptions[$this->protocol] ?: [];
}
}
if ($mode == 'w') {
$this->stream = new WriteStream(null, $options);
$this->stream->setUploader(
$this->bucket->getStreamableUploader(
$this->stream,
$options + ['name' => $this->file]
)
);
} elseif ($mode == 'r') {
try {
// Lazy read from the source
$options['restOptions']['stream'] = true;
$this->stream = new ReadStream(
$this->bucket->object($this->file)->downloadAsStream($options)
);
// Wrap the response in a caching stream to make it seekable
if (!$this->stream->isSeekable() && ($flags & STREAM_MUST_SEEK)) {
$this->stream = new CachingStream($this->stream);
}
} catch (ServiceException $ex) {
return $this->returnError($ex->getMessage(), $flags);
}
} else {
return $this->returnError('Unknown stream_open mode.', $flags);
}
if ($flags & STREAM_USE_PATH) {
$openedPath = $path;
}
return true;
}
For Windows Chrome (and probably many other browsers), this code works for serving an mp3 in an audio element:
/**
*
* #param string $filename
* #return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
*/
public function getMp3($filename) {
$fileContents = Storage::disk(\App\Helpers\CoachingCallsHelper::DISK)->get($filename);
$fileSize = Storage::disk(\App\Helpers\CoachingCallsHelper::DISK)->size($filename);
$shortlen = $fileSize - 1;
$headers = [
'Accept-Ranges' => 'bytes',
'Content-Range' => 'bytes 0-' . $shortlen . '/' . $fileSize,
'Content-Type' => "audio/mpeg"
];
Log::debug('$headers=' . json_encode($headers));
$response = response($fileContents, 200, $headers);
return $response;
}
But when I use an iPhone to browse to the same page, the mp3 file does not show the total duration, and when I play it, it says "Live broadcast".
I've tried to follow suggestions from various answers of this question (HTML5 <audio> Safari live broadcast vs not) and other articles I've read, but none seem to have an effect.
No matter how I change the headers, the mp3 seems to function as desired on Windows and does not work on iOS.
How can I debug what I'm doing wrong?
Here is HTML:
<audio controls preload="auto">
<source src="{{$coachingCall->getMp3Url()}}" type="audio/mpeg"/>
<p>Your browser doesnt support embedded HTML5 audio. Here is a link to the audio instead.</p>
</audio>
MP3 files don't have timestamps, and therefore no inherent length that can be known ahead of time. Chrome is just guessing, based on the bitrate at the beginning of the file and the byte size of the file. It doesn't really know.
Some players don't bother guessing.
Also, all browsers on iOS are Safari under the hood, thanks to some incredibly restrictive policies by Apple. Therefore, Chrome on iOS is really just a wrapper for a Safari web view.
Whoa, that was a very difficult problem to solve. (It took me days.)
And I learned that it wasn't just iOS that was having problems: Safari on Mac hadn't been working either.
Now I think everything works on every browser I've tested.
I'm really glad I found this example to follow.
Here is my answer:
/**
*
* #param string $disk
* #param string $filename
* #return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\StreamedResponse
*/
public static function getMediaFile($disk, $filename) {
$rangeHeader = request()->header('Range');
$fileContents = Storage::disk($disk)->get($filename);
$fullFilePath = Storage::disk($disk)->path($filename); //https://stackoverflow.com/a/49532280/470749
$headers = ['Content-Type' => Storage::disk($disk)->mimeType($fullFilePath)];
if ($rangeHeader) {
return self::getResponseStream($disk, $fullFilePath, $fileContents, $rangeHeader, $headers);
} else {
$httpStatusCode = 200;
return response($fileContents, $httpStatusCode, $headers);
}
}
/**
*
* #param string $disk
* #param string $fullFilePath
* #param string $fileContents
* #param string $rangeRequestHeader
* #param array $responseHeaders
* #return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public static function getResponseStream($disk, $fullFilePath, $fileContents, $rangeRequestHeader, $responseHeaders) {
$stream = Storage::disk($disk)->readStream($fullFilePath);
$fileSize = strlen($fileContents);
$fileSizeMinusOneByte = $fileSize - 1; //because it is 0-indexed. https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
list($param, $rangeHeader) = explode('=', $rangeRequestHeader);
if (strtolower(trim($param)) !== 'bytes') {
abort(400, "Invalid byte range request"); //Note, this is not how https://stackoverflow.com/a/29997555/470749 did it
}
list($from, $to) = explode('-', $rangeHeader);
if ($from === '') {
$end = $fileSizeMinusOneByte;
$start = $end - intval($from);
} elseif ($to === '') {
$start = intval($from);
$end = $fileSizeMinusOneByte;
} else {
$start = intval($from);
$end = intval($to);
}
$length = $end - $start + 1;
$httpStatusCode = 206;
$responseHeaders['Content-Range'] = sprintf('bytes %d-%d/%d', $start, $end, $fileSize);
$responseStream = response()->stream(function() use ($stream, $start, $length) {
fseek($stream, $start, SEEK_SET);
echo fread($stream, $length);
fclose($stream);
}, $httpStatusCode, $responseHeaders);
return $responseStream;
}
I can't comment since I just made my account, so... complementing RYAN's
Just found out that you can save some loading time removing the
$fileContents = Storage::disk($disk)->get($filename);
And replacing it with
$fileSize = Storage::disk($disk)->size($filename);
Passing the size directly to the getResponseStream function, instead of downloading the whole content into a variable and then measuring the length.
Thank you Ryan, saved me a lot of precious time with the stinky safari.
I am handling imap in php via socket. Everything works perfect. I build mail swift object, then convert it to string and send to imap with append command, it works, I get OK [APPENDUID 1 4497] (Success), but if I attach file greater than about 3kb - imap server responds me nothing, and the mail doesn't append! What is the problem? May be I should make append something like part by part? I mean cut mail body for several parts and make several fwrite()?
These are several parts of my code. But I think problem is not in the code, but in some common imap stuff:
/**
* #param string $command
* #param string $successPattern
* #param bool $withCounter
* #return false|string
*/
protected function sendCommand($command, $successPattern = '', $withCounter = true)
{
$counter = $withCounter ? "{$this->commandHash}{$this->commandCounter} " : "";
$successPattern = !$successPattern ? $counter . 'OK' : $successPattern;
fwrite($this->stream, "{$counter}{$command}\r\n");
$this->commandCounter++;
$previousLine = '';
$buf = '';
$time = time();
while ((time() - $time) < $this->timeOut) {
$newLine = fread($this->stream, 4096);
if(!strlen($newLine)) continue;
$buf .= $newLine;
file_put_contents("/LOG2.txt", $newLine, FILE_APPEND);
if (strripos($previousLine.$newLine, $successPattern) !== FALSE){
$this->responseContainer->setLastResponseText($buf);
return $buf;
}
if (strripos($previousLine.$newLine, $this->commandHash . ($this->commandCounter - 1) . ' NO') !== FALSE
|| strripos($previousLine.$newLine, $this->commandHash . ($this->commandCounter - 1) . ' BAD') !== FALSE){
$this->responseContainer->setLastErrorText($buf);
return false;
}
$previousLine = $newLine;
}
var_dump(" Time out");
$this->responseContainer->setLastErrorText("{$command} {$counter} Time out");
return false;
}
/**
* #param $mailString
* #param string $folder
* #return false|int
*/
public function append($mailString, $folder = "INBOX")
{
if($this->sendCommand("APPEND {$folder} {".strlen($mailString)."}", "go ahead")){
$response = $this->sendCommand($mailString, '', false);
return $this->imapParser->parseAppendResult($response);
}
return false;
}
$message = new \Swift_Message();
$message->setSubject($mail->subject);
$message->setFrom($mail->from);
$message->setTo($mail->to);
$message->addPart($mail->body, 'text/html');
foreach($mail->files as $file){
$attachment = \Swift_Attachment::fromPath($file->getPath());
$attachment->setFilename($file->name.".".$file->type);
$message->attach($attachment);
}
$appendUid = $this->imapService->getCommander()->append($message->toString());
Creating stream:
/**
* #param string $host
* #param integer $port
* #return bool
*/
protected function createStream($host, $port)
{
//#todo proxy authentication
if($this->stream = #stream_socket_client("ssl://{$host}:{$port}", $errno, $errstr, $this->timeOut, STREAM_CLIENT_CONNECT, $this->context)) {
stream_set_timeout($this->stream, $this->timeOut);
return $this->stream;
}
$this->responseContainer->setLastErrorText("Failed connection to imap. Without proxy. " . $errstr);
return false;
}
I don't know if is possible to add an image in a template with PHPWord using TemplateProcessor.
I have a document (form.docx). This document is a template. I read the fields (${name} for example) and replace it with a text.
$template = new TemplateProcessor('form.docx');
$template->setValue('name', $name);
But now, I want to put an image but I don't know how I do it. I try with this:
$img='img.jpg';
$template->setValue('image',$img);
Doesn't works. I try other form, creating a section and add this section to template but this fails.
$phpWord = new PhpWord();
$section = $phpWord->createSection();
$section->addImage('img.jpg', array('width'=>210, 'height'=>210, 'align'=>'center'));
$template = new TemplateProcessor('form.docx');
$template->setValue('image',$section).
Anyone know how to put an image in a .docx using a template?
Thanks.
This should do the trick:
// there has to be a text ${foto} in your templatefile.docx for this to work
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor('templatefile.docx');
$templateProcessor->setImageValue('foto', array('path' => 'dummy_foto.jpg', 'width' => 100, 'height' => 100, 'ratio' => true));
$templateProcessor->saveAs('result.docx');
// open the file
header("Content-disposition: attachment;filename=" .'result.docx' . " ; charset=iso-8859-1");
echo file_get_contents('result.docx');
The Template Processor can have a save_image function.
see How to add/set images on PHPOffice/PHPWord Template?
Example:
$dokument->save_image('image2',$imagefile,$dokument);
open TemplateProcessor.php from phpWord folder.
replace this sample code inside.
<?php
/**
* PHPWord
*
* Copyright (c) 2010 PHPWord
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* #category PHPWord
* #package PHPWord
* #copyright Copyright (c) 010 PHPWord
* #license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL
* #version Beta 0.6.2, 24.07.2010
*/
/**
* PHPWord_DocumentProperties
*
* #category PHPWord
* #package PHPWord
* #copyright Copyright (c) 2009 - 2010 PHPWord (http://www.codeplex.com/PHPWord)
*/
class PHPWord_Template {
private $_objZip;
private $_tempFileName;
private $_documentXML;
private $_header1XML;
private $_footer1XML;
private $_rels;
private $_types;
private $_countRels;
/**
* Create a new Template Object
* #param string $strFilename
*/
public function __construct($strFilename) {
$path = dirname($strFilename);
$this->_tempFileName = $path . DIRECTORY_SEPARATOR . time() . '.docx'; // $path doesn't include the trailing slash - Custom code by Matt Bowden (blenderstyle) 04/12/2011
copy($strFilename, $this->_tempFileName); // Copy the source File to the temp File
$this->_objZip = new ZipArchive();
$this->_objZip->open($this->_tempFileName);
$this->_documentXML = $this->_objZip->getFromName('word/document.xml');
$this->_header1XML = $this->_objZip->getFromName('word/header1.xml'); // Custom code by Matt Bowden (blenderstyle) 04/12/2011
$this->_footer1XML = $this->_objZip->getFromName('word/footer1.xml'); // Custom code by Matt Bowden (blenderstyle) 04/12/2011
$this->_rels = $this->_objZip->getFromName('word/_rels/document.xml.rels'); #erap 07/07/2015
$this->_types = $this->_objZip->getFromName('[Content_Types].xml'); #erap 07/07/2015
$this->_countRels = substr_count($this->_rels, 'Relationship') - 1; #erap 07/07/2015
}
/**
* Set a Template value
* #param mixed $search
* #param mixed $replace
*/
public function setValue($search, $replace) {
$search = '${' . $search . '}';
$replace = $this->limpiarString($replace);
$this->_documentXML = str_replace($search, $replace, $this->_documentXML);
$this->_header1XML = str_replace($search, $replace, $this->_header1XML); // Custom code by Matt Bowden (blenderstyle) 04/12/2011
$this->_footer1XML = str_replace($search, $replace, $this->_footer1XML); // Custom code by Matt Bowden (blenderstyle) 04/12/2011
}
/**
* Save Template
* #param string $strFilename
*/
public function save($strFilename) {
if (file_exists($strFilename))
unlink($strFilename);
$this->_objZip->addFromString('word/document.xml', $this->_documentXML);
$this->_objZip->addFromString('word/header1.xml', $this->_header1XML); // Custom code by Matt Bowden (blenderstyle) 04/12/2011
$this->_objZip->addFromString('word/footer1.xml', $this->_footer1XML); // Custom code by Matt Bowden (blenderstyle) 04/12/2011
$this->_objZip->addFromString('word/_rels/document.xml.rels', $this->_rels); #erap 07/07/2015
$this->_objZip->addFromString('[Content_Types].xml', $this->_types); #erap 07/07/2015
// Close zip file
if ($this->_objZip->close() === false)
throw new Exception('Could not close zip file.');
rename($this->_tempFileName, $strFilename);
}
public function replaceImage($path, $imageName) {
$this->_objZip->deleteName('word/media/' . $imageName);
$this->_objZip->addFile($path, 'word/media/' . $imageName);
}
public function replaceStrToImg( $strKey, $arrImgPath ){
//289x108
$strKey = '${'.$strKey.'}';
$relationTmpl = '<Relationship Id="RID" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/IMG"/>';
$imgTmpl = '<w:pict><v:shape type="#_x0000_t75" style="width:WIDpx;height:HEIpx"><v:imagedata r:id="RID" o:title=""/></v:shape></w:pict>';
$typeTmpl = ' <Override PartName="/word/media/IMG" ContentType="image/EXT"/>';
$toAdd = $toAddImg = $toAddType = '';
$aSearch = array('RID', 'IMG');
$aSearchType = array('IMG', 'EXT');
foreach($arrImgPath as $img){
$imgExt = array_pop( explode('.', $img['img']) );
if( in_array($imgExt, array('jpg', 'JPG') ) )
$imgExt = 'jpeg';
$imgName = 'img' . $this->_countRels . '.' . $imgExt;
$rid = 'rId' . $this->_countRels++;
$this->_objZip->addFile($img['img'], 'word/media/' . $imgName);
if( isset($img['size']) ){
$w = $img['size'][0];
$h = $img['size'][1];
}
else{
$w = 289;
$h = 108;
}
$toAddImg .= str_replace(array('RID', 'WID', 'HEI'), array($rid, $w, $h), $imgTmpl) ;
if( isset($img['dataImg']) )
$toAddImg .= '<w:br/><w:t>' . $this->limpiarString($img['dataImg']) . '</w:t><w:br/>';
$aReplace = array($imgName, $imgExt);
$toAddType .= str_replace($aSearchType, $aReplace, $typeTmpl) ;
$aReplace = array($rid, $imgName);
$toAdd .= str_replace($aSearch, $aReplace, $relationTmpl);
}
$this->_documentXML = str_replace('<w:t>' . $strKey . '</w:t>', $toAddImg, $this->_documentXML);
$this->_types = str_replace('</Types>', $toAddType, $this->_types) . '</Types>';
$this->_rels = str_replace('</Relationships>', $toAdd, $this->_rels) . '</Relationships>';
}
function limpiarString($str) {
return str_replace(
array('&', '<', '>', "\n"),
array('&', '<', '>', "\n" . '<w:br/>'),
$str
);
}
}
?>
You can get full modified code from here.
I've been trying to solve this but can't seem to find anything on it.
When you "Download ZIP" from github, it doesn't give you the "theme_name.zip" that you'd hope for, but rather "theme_name-master.zip", which when average users install this the messed up folder name throws off child themes.
How can this be remedied, so that the zip download does not change anything?
For simplicity, You could host a script somewhere that downloads the script, renames the folder without the -master and then rezip's the project, then send it to the user as a download.
So something like (Requires PHP5 >= 5.2.0, cURL, ZipArchive, safe_mode & open_basedir off):
Fork it from GitHub ;p
<?php
//Example Usage
new GitDL('https://github.com/lcherone/GitDL');
/**
* GitHub Project/Repository Downloader proxy.
* This class will handle downloading, removing master folder prefix,
* repacking and proxying back the project as a download.
*
* #author Lawrence Cherone
* #version 0.2
*/
class GitDL{
// project files working directory - automatically created
const PWD = "./project_files/";
/**
* Class construct.
*
* #param string $url
*/
function __construct($url=null){
// check construct argument
if(!$url) die('Class Error: Missing construct argument: $url');
// fix trailing slash if any
$url = rtrim($url, '/');
// assign class properties
$this->project = basename($url);
$this->project_url = $url.'/archive/master.zip';
$this->tmp_file = md5($url).'.zip';
// make project working folder
if(!file_exists(self::PWD)){
mkdir(self::PWD.md5($url), 0775, true);
}
// get project zip from GitHub
try{
$this->get_project();
}catch(Exception $e){
die($e->getMessage());
}
// extract project zip from git
$this->extract(self::PWD.$this->tmp_file, self::PWD.md5($url));
// remove the master part, by renaming
rename(self::PWD.md5($url).'/'.$this->project.'-master', self::PWD.md5($url).'/'.$this->project);
// rezip project files
$this->zipcreate(self::PWD.md5($url), self::PWD.'new_'.$this->tmp_file);
// send zip to user
header('Content-Description: File Transfer');
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="'.$this->project.'.zip"');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: '.sprintf("%u", filesize(self::PWD.'new_'.$this->tmp_file)));
readfile(self::PWD.'new_'.$this->tmp_file);
// cleanup
$this->destroy_dir(self::PWD.md5($url));
unlink(self::PWD.$this->tmp_file);
unlink(self::PWD.'new_'.$this->tmp_file);
}
/**
* cURL GitHub project downloader.
* No support for open base dir/safe mode as there is a GitHub redirect to there CDN
* a HEAD pre-check is done to check project exists,
* project zip is written directly to the file.
*/
function get_project(){
// check curl installed
if(!function_exists('curl_init')){
throw new Exception('cURL Error: You must have cURL installed to use this class.');
}
// check for unsupported settings
if (ini_get('open_basedir') != '' || ini_get('safe_mode') == 'On'){
throw new Exception('cURL Error: safe_mode or an open_basedir is enabled, class not supported.');
}
// HEAD request - To verify the project exists
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => $this->project_url,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FAILONERROR => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_BINARYTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_NOBODY => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_SSL_VERIFYPEER => false,
));
// lets grab it
if(curl_exec($ch) !== false){
$fp = fopen(self::PWD.$this->tmp_file, 'a+b');
if(flock($fp, LOCK_EX | LOCK_NB)){
// empty possible contents
ftruncate($fp, 0);
rewind($fp);
// HTTP GET request - write directly to the file
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => $this->project_url,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FAILONERROR => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_BINARYTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_FILE => $fp,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_SSL_VERIFYPEER => false,
));
// transfer failed
if(curl_exec($ch) === false){
ftruncate($fp, 0);
throw new Exception('cURL Error: transfer failed.');
}
fflush($fp);
flock($fp, LOCK_UN);
curl_close($ch);
}
fclose($fp);
}else{
curl_close($ch);
throw new Exception('Error: '.htmlentities($this->project).' project not found on GitHub');
}
}
/**
* Create zip from extracted/fixed project.
*
* #uses ZipArchive
* #uses RecursiveIteratorIterator
* #param string $source
* #param string $destination
* #return bool
*/
function zipcreate($source, $destination) {
if (!extension_loaded('zip') || !file_exists($source)) {
return false;
}
$zip = new ZipArchive();
if (!$zip->open($destination, ZIPARCHIVE::CREATE)) {
return false;
}
$source = str_replace('\\', '/', realpath($source));
if (is_dir($source) === true) {
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
$file = str_replace('\\', '/', realpath($file));
if (is_dir($file) === true) {
$zip->addEmptyDir(str_replace($source.'/', '', $file.'/'));
} else if (is_file($file) === true) {
$zip->addFromString(str_replace($source.'/', '', $file), file_get_contents($file));
}
}
}
return $zip->close();
}
/**
* Extract Zip file
*
* #uses ZipArchive
* #param string $source
* #param string $destination
* #return bool
*/
function extract($source, $destination){
$zip = new ZipArchive;
if($zip->open($source) === TRUE) {
$zip->extractTo($destination);
$zip->close();
return true;
} else {
return false;
}
}
/**
* Recursive directory remover/deleter
*
* #uses RecursiveIteratorIterator
* #param string $dir
* #return bool
*/
function destroy_dir($dir) {
foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) {
$path->isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname());
}
return rmdir($dir);
}
}
?>
That's a bit problematic and I don't think there's a real fix, only workarounds. I faced the same problem with plugins and have only a partial solution: it works only on plugin/theme updates, not when installing.
Never tried, but maybe it's possible to perform the folder rename on plugin (register_activation_hook) or theme (after_switch_theme) activation.
I've checked the core and the filter upgrader_source_selection works for themes and plugins. The repo slug is in GitHub's URL: http://github.com/user/repo-name.
public static $repo_slug = 'repo-name';
add_filter( 'upgrader_source_selection', array( $this, 'rename_github_zip' ), 1, 3 );
/**
* Removes the prefix "-master" when updating from GitHub zip files
*
* See: https://github.com/YahnisElsts/plugin-update-checker/issues/1
*
* #param string $source
* #param string $remote_source
* #param object $thiz
* #return string
*/
public function rename_github_zip( $source, $remote_source, $thiz )
{
if( strpos( $source, self::$repo_slug ) === false )
return $source;
$path_parts = pathinfo($source);
$newsource = trailingslashit($path_parts['dirname']). trailingslashit( self::$repo_slug );
rename($source, $newsource);
return $newsource;
}