PluginProbe
Ebook Store / 6.25
Ebook Store v6.25
6.25 6.24 6.23 6.22 6.21 6.20 trunk
ebook-store / EbookStoreEbook.class.php

EbookStoreEbook.class.php in Ebook Store 6.25, at EbookStoreEbook.class.php

611 lines 21.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ebook Store - the ebook value object used for download links and for the
4 * per-order watermarked/encrypted PDF.
5 *
6 * @package EbookStore
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 class EbookStoreEbook {
14 /**
15 * Whether l() writes to the PHP error log.
16 *
17 * @var bool
18 */
19 public $log = true;
20
21 /**
22 * Post ID of the ebook.
23 *
24 * @var int|string
25 */
26 public $ebook_id = 0;
27
28 /**
29 * Raw get_post_meta() dump for the ebook, keyed by meta key.
30 *
31 * @var array
32 */
33 public $meta = array();
34
35 /**
36 * The primary attachment record ('file', 'url', 'type').
37 *
38 * @var array
39 */
40 public $file = array();
41
42 /**
43 * The 'ebook' meta record (price and other per-book settings).
44 *
45 * @var array
46 */
47 public $ebook = array();
48
49 /**
50 * Price taken from the 'ebook' meta record.
51 *
52 * @var string
53 */
54 public $price = '';
55
56 /**
57 * Size of the primary attachment in bytes.
58 *
59 * @var int
60 */
61 public $filesize = 0;
62
63 /**
64 * Human readable size of the primary attachment.
65 *
66 * Note: this property deliberately shares its name with the human_filesize()
67 * method. PHP keeps the two in separate symbol tables so both work, and
68 * renaming either one would break callers outside this class.
69 *
70 * @var string
71 */
72 public $human_filesize = '';
73
74 /**
75 * URL of the primary attachment.
76 *
77 * @var string
78 */
79 public $url = '';
80
81 /**
82 * MIME type of the primary attachment.
83 *
84 * @var string
85 */
86 public $type = '';
87
88 /**
89 * Ebook post title.
90 *
91 * @var string
92 */
93 public $title = '';
94
95 /**
96 * Formats this ebook is available in, e.g. array('pdf','epub').
97 *
98 * @var array
99 */
100 public $formats = array();
101
102 /**
103 * Absolute paths and sizes per format, keyed 'pdf' and 'pdf_size'.
104 *
105 * @var array
106 */
107 public $files = array();
108
109 /**
110 * Per-format download URL overrides. Assigned from outside the class by
111 * ebook_store.php and functions.php when an order already has a stored link.
112 *
113 * @var array
114 */
115 public $setLink = array();
116
117 /**
118 * WooCommerce order key, assigned from outside the class.
119 *
120 * @var string
121 */
122 public $wc_order = '';
123
124 /**
125 * Order post ID, assigned from outside the class.
126 *
127 * @var int|string
128 */
129 public $order_id = 0;
130
131 /**
132 * Plugin-order download key, assigned from outside the class. Declared so
133 * PHP 8.2+ does not report a dynamic-property deprecation.
134 *
135 * @var string
136 */
137 public $ebook_key = '';
138
139 /**
140 * Saved-checkout-form nonce, assigned from outside the class.
141 *
142 * @var string
143 */
144 public $md5_nonce = '';
145
146 function __construct($ebook_id) {
147 $this->ebook_id = $ebook_id;
148 $this->meta = get_post_meta($ebook_id);
149 $this->meta = is_array($this->meta) ? $this->meta : array();
150 $this->file = get_post_meta($ebook_id, 'ebook_wp_custom_attachment', true);
151 $this->file = is_array($this->file) ? $this->file : array();
152 $this->ebook = get_post_meta($ebook_id, 'ebook', true);
153 $this->ebook = is_array($this->ebook) ? $this->ebook : array();
154 $this->price = isset($this->ebook['ebook_price']) ? $this->ebook['ebook_price'] : '';
155 $this->filesize = isset($this->file['file']) ? (int) @filesize($this->file['file']) : 0;
156 $this->human_filesize = $this->human_filesize($this->filesize);
157 $this->url = isset($this->file['url']) ? $this->file['url'] : '';
158 $this->type = isset($this->file['type']) ? $this->file['type'] : '';
159 $this->title = get_the_title($ebook_id);
160 //$this->l($this->title);
161 if (is_array(@$this->meta['ebook_wp_custom_attachment'])) {
162 if (count($this->meta['ebook_wp_custom_attachment']) > 0) {
163 $this->formats[] = 'pdf';
164 $file = unserialize($this->meta['ebook_wp_custom_attachment'][0]);
165 $file = $file['file'];
166 $this->files['pdf'] = $file;
167 $this->files['pdf_size'] = (int) @filesize($file);
168 }
169
170 }
171 $extra_formats = array('mobi','epub','txt','zip','mp3','mp4');
172 foreach ($extra_formats as $format) {
173 if (@is_array($this->meta['ebook_wp_custom_attachment_' . $format])) {
174 if (@count(@$this->meta['ebook_wp_custom_attachment_' . $format]) > 0) {
175 $this->formats[] = $format;
176 $file = unserialize($this->meta['ebook_wp_custom_attachment_' . $format][0]);
177 $file = $file['file'];
178 $this->files[$format] = $file;
179 $this->files[$format . '_size'] = (int) @filesize($file);
180 }
181 }
182 }
183 // $this->l($this->files,true);
184 }
185 function l($data, $v = false) {
186 if ($this->log) {
187 error_log(print_r(debug_backtrace(),true));
188 if (is_array($data)) {
189 error_log(print_r($data,true));
190 } else {
191 error_log($data);
192 }
193 }
194 if ($v) {
195 echo "<pre>" . var_export($data, true) . "</pre>";
196 }
197 }
198 function human_filesize($bytes, $decimals = 2) {
199 $sz = 'BKMGTP';
200 $bytes = is_numeric($bytes) ? $bytes + 0 : 0;
201 // Clamp rather than suppress: an empty or huge value used to index past
202 // the end of $sz, which the old @ hid instead of fixing.
203 $factor = (int) floor((strlen((string) $bytes) - 1) / 3);
204 $factor = max(0, min($factor, strlen($sz) - 1));
205 return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $sz[$factor];
206 }
207 function format_links() {
208 if (is_array($this->formats) == false) {
209 return '';
210 }
211 return implode(', ', array_map(array($this, 'link'), $this->formats));
212 }
213 function link($format) {
214 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
215 $locale = ebook_store_get_locale_strings();
216 $formats_locale = ebook_store_get_format_labels();
217
218 $order_id = isset($this->order_id) ? absint($this->order_id) : 0;
219 $wc_order = isset($this->wc_order) ? (string) $this->wc_order : '';
220 $ebook_key = isset($this->ebook_key) ? (string) $this->ebook_key : '';
221
222 if ($wc_order !== '' && $order_id > 0) {
223 $link = add_query_arg(array('action' => 'wc_order_download','ebook_id' => $this->ebook_id, 'format' => $format, 'wc_order' => $wc_order, 'order_id' => $order_id),get_permalink($this->ebook_id));
224 } elseif ($ebook_key !== '') {
225 // A plugin (non-WooCommerce) order has no WooCommerce order key, so a
226 // wc_order_download link can never pass the order-key check. Send those
227 // buyers through the ebook_key download path instead, like the PDF link.
228 $key_args = array('action' => 'download', 'format' => $format, 'ebook_key' => $ebook_key);
229 if (isset($this->md5_nonce) && $this->md5_nonce != '') {
230 $key_args['md5_nonce'] = (string) $this->md5_nonce;
231 }
232 if ($order_id > 0) {
233 $key_args['order_id'] = $order_id;
234 }
235 $link = add_query_arg($key_args,get_permalink($this->ebook_id));
236 } else {
237 $link = add_query_arg(array('action' => 'wc_order_download','ebook_id' => $this->ebook_id, 'format' => $format, 'wc_order' => $wc_order, 'order_id' => $order_id),get_permalink($this->ebook_id));
238 }
239 if (isset($this->setLink[$format]) && $this->setLink[$format] != '') {
240 $link = $this->setLink[$format];
241 }
242 $label = isset($formats_locale[$format]) ? $formats_locale[$format] : $format;
243 $size = isset($this->files[$format . '_size']) ? $this->files[$format . '_size'] : 0;
244 return '<a href="' . esc_url($link) . '" target="_blank">' . esc_html($label) . '</a> <small><em>(' . esc_html($this->human_filesize($size)) . ')</em></small>';
245 }
246 function encrypted($order_id = null, $format = 'pdf') {
247 //error_reporting(E_ALL);
248 //ini_set('display_errors','1');
249 global $ebook_email_delivery, $ebook_qr_text, $ebook_png_path, $ebook_pngname, $attachment, $pdfHeaderText;
250 if (class_exists('FPDF') == false) {
251 require_once('fpdi/qrcode.class.php');
252 }
253
254 require_once('fpdi/qrcode.class.php');
255
256 if ($format == 'pdf' && get_option('encrypt_pdf')) {
257 $meta = get_post_meta($order_id);
258 $meta = is_array($meta) ? $meta : array();
259
260 $meta_value = function ($key) use ($meta) {
261 return isset($meta[$key][0]) ? $meta[$key][0] : '';
262 };
263
264 $r['payer_email'] = $meta_value('_billing_email');
265 $r['payment_date'] = $meta_value('_paid_date');
266 $r['txn_id'] = $meta_value('_transaction_id');
267 $r['first_name'] = $meta_value('_billing_first_name');
268 $r['last_name'] = $meta_value('_billing_last_name');
269 $r['residence_country'] = $meta_value('_billing_country');
270
271 if ($r['payer_email'] == '') {
272 $r['payer_email'] = $meta_value('payer_email');
273 $r['payment_date'] = $meta_value('payment_date');
274 $r['txn_id'] = $meta_value('txn_id');
275 $r['first_name'] = $meta_value('first_name');
276 $r['last_name'] = $meta_value('last_name');
277 $r['residence_country'] = $meta_value('residence_country');
278 }
279
280 // Under WooCommerce HPOS none of the billing details live in wp_postmeta,
281 // so both lookups above come back empty; ask the order object instead.
282 if ($r['payer_email'] == '' && (int) $order_id > 0 && function_exists('wc_get_order')) {
283 $wc_order = wc_get_order((int) $order_id);
284 if ($wc_order && method_exists($wc_order, 'get_billing_email')) {
285 $r['payer_email'] = (string) $wc_order->get_billing_email();
286 $r['first_name'] = (string) $wc_order->get_billing_first_name();
287 $r['last_name'] = (string) $wc_order->get_billing_last_name();
288 $r['residence_country'] = (string) $wc_order->get_billing_country();
289 $r['txn_id'] = (string) $wc_order->get_transaction_id();
290 $paid_date = $wc_order->get_date_paid();
291 $r['payment_date'] = $paid_date ? $paid_date->date('Y-m-d H:i:s') : '';
292 }
293 }
294
295 // The QR points at the order-verification page on this site (a link
296 // only — no buyer PII in the code itself), matching the other
297 // encryption path and the promise made in the settings screen.
298 $qr_key = $meta_value('ebook_key');
299 $ebook_qr_text = function_exists('ebook_store_get_order_verification_url')
300 ? ebook_store_get_order_verification_url($qr_key)
301 : home_url('/');
302 // PDF text is not HTML — esc_attr() would leak entities into the file.
303 $pdfHeaderText = function_exists( 'ebook_store_sanitize_pdf_text' )
304 ? ebook_store_sanitize_pdf_text( (string) get_option('buyer_info_text') )
305 : (string) get_option('buyer_info_text');
306
307 foreach ($r as $k => $v) {
308 if ( is_scalar( $v ) ) {
309 $clean = function_exists( 'ebook_store_sanitize_pdf_text' ) ? ebook_store_sanitize_pdf_text( (string) $v ) : (string) $v;
310 $pdfHeaderText = str_replace("%%$k%%", $clean, $pdfHeaderText);
311 }
312 }
313
314
315 $file = $this->files['pdf'];
316
317 $ebook_pdfname = basename($file);
318
319 $cache_dir = ebook_store_get_cache_dir($order_id);
320 if ($cache_dir === '') {
321 // Without a writable protected directory we must not fall back to
322 // the plugin folder, so serve the unwatermarked source instead.
323 if (function_exists('ebook_store_log')) {
324 ebook_store_log('pdf', 'Cache directory unavailable, serving the source file.', array('order_id' => $order_id));
325 }
326 return $this->files[$format];
327 }
328
329 $ebook_pdf_path = $cache_dir . '/' . $ebook_pdfname;
330
331 // Versions before 6.00 wrote this file inside the plugin folder, where
332 // it was served over HTTP. Move rather than regenerate so that live
333 // orders keep resolving to the copy the buyer was already given.
334 $legacy_dir = plugin_dir_path( __FILE__ ) . '/cache/' . md5($order_id . substr(NONCE_KEY, 0, 8));
335 $legacy_pdf_path = $legacy_dir . '/' . $ebook_pdfname;
336 if (!file_exists($ebook_pdf_path) && file_exists($legacy_pdf_path)) {
337 if (@rename($legacy_pdf_path, $ebook_pdf_path)) {
338 @rmdir($legacy_dir);
339 } else {
340 // The move failed; drop the web-readable copy anyway and let the
341 // generator below rebuild it in the protected location.
342 @unlink($legacy_pdf_path);
343 }
344 }
345
346 // The QR image is only read back while the PDF is being built, so a
347 // cache hit can skip generating it entirely.
348 if (file_exists($ebook_pdf_path)) {
349 return $ebook_pdf_path;
350 }
351
352 $password = $r['payer_email'];
353 if (function_exists('ebook_store_get_password_for_order')) {
354 // The shared resolver honours the "No password" and random-password
355 // settings and stores the secret once per order, so the PDF opens
356 // with the password the buyer was actually shown.
357 $password_context = array(
358 'payer_email' => $r['payer_email'],
359 'billing_email' => $meta_value('_billing_email'),
360 'fallback_email' => $r['payer_email'],
361 );
362 if (get_option('ebook_store_random_password') == 1) {
363 // Only used when no password is stored yet; wp_generate_password() is a CSPRNG.
364 $password_context['password'] = wp_generate_password( 10, false, false );
365 }
366 $password = (string) ebook_store_get_password_for_order((int) $order_id, $password_context);
367 } else {
368 if (get_option('ebook_store_random_password') == 1) {
369 // The open password must not be derivable from the cache path, so
370 // it comes from a CSPRNG and is stored once per order instead of
371 // being recomputed from the same hash that names the directory.
372 $stored_password = ($order_id > 0) ? get_post_meta($order_id, 'password', true) : '';
373 $stored_password = is_string($stored_password) ? $stored_password : '';
374 if ($stored_password === '') {
375 $stored_password = wp_generate_password( 10, false, false );
376 if ($order_id > 0) {
377 update_post_meta($order_id, 'password', $stored_password);
378 }
379 }
380 $password = $stored_password;
381 }
382 if (get_option('ebook_store_blank_password') == 1) {
383 $password = '';
384 }
385 }
386 $r['password'] = $password;
387
388 $owner_password = (string) get_option('ebook_store_owner_password');
389
390 if (function_exists('ebook_store_raise_pdf_memory')) {
391 ebook_store_raise_pdf_memory();
392 }
393 $pdf = new QRPDF();
394 $pdf->watermarkText = $pdfHeaderText;
395 $pdf->qrText = $ebook_qr_text;
396 $pdf->stampCount = 0;
397 try {
398 $pagecount = $pdf->setSourceFile($this->files[$format]);
399 for ($loop = 1; $loop <= $pagecount; $loop++) {
400 $tplidx = $pdf->importPage($loop);
401 $size = $pdf->getTemplateSize($tplidx);
402 $pdf->AddPage($size['orientation'], array($size['width'], $size['height']));
403 $pdf->useTemplate($tplidx);
404 $pdf->stampCurrentPage();
405 }
406
407 $pdf->applyProtection($password, $owner_password);
408 $pdf->Output($ebook_pdf_path, 'F');
409 } catch (\Throwable $e) {
410 // A half-written file must not be served as a cache hit next time.
411 if (file_exists($ebook_pdf_path)) {
412 @unlink($ebook_pdf_path);
413 }
414 if (function_exists('ebook_store_log')) {
415 ebook_store_log('pdf', 'PDF protection failed: ' . $e->getMessage(), array('order_id' => $order_id, 'ebook_id' => $this->ebook_id, 'file' => $this->files[$format]));
416 }
417 wp_die( esc_html( sprintf(
418 /* translators: %s: PDF import error. */
419 __( 'This PDF could not be processed for protection. Please re-save it as a standard PDF or turn encryption off. Details: %s', 'ebook-store' ),
420 $e->getMessage()
421 ) ) );
422 }
423 return $ebook_pdf_path;
424
425 // update_post_meta(@$order_id,'encrypted_pdf',wp_slash($ebook_pdf_path));
426 //wp_die(print_r($meta,true));
427 } elseif ( $format === 'epub' && get_option( 'ebook_store_epub_watermark' ) && isset( $this->files['epub'] ) ) {
428 // EPUB "protection" is social-DRM watermarking (it cannot be encrypted
429 // and stay reader-compatible). Mirror the PDF per-order cache pattern.
430 $file = $this->files['epub'];
431 $cache_dir = ebook_store_get_cache_dir( $order_id );
432 if ( $cache_dir === '' ) {
433 if ( function_exists( 'ebook_store_log' ) ) {
434 ebook_store_log( 'epub', 'cache dir unavailable, serving source', array( 'order_id' => $order_id ) );
435 }
436 return $this->files[$format];
437 }
438 $dest = $cache_dir . '/' . basename( $file );
439 if ( file_exists( $dest ) ) {
440 return $dest; // idempotent cache hit
441 }
442 if ( ! function_exists( 'ebook_store_watermark_epub' ) ) {
443 return $this->files[$format];
444 }
445 $watermark = ebook_store_resolve_buyer_watermark( (int) $order_id, (string) get_option( 'ebook_store_epub_watermark_text' ) );
446 $qr_text = '';
447 if ( get_option( 'ebook_store_epub_qr' ) && function_exists( 'ebook_store_get_order_verification_url' ) ) {
448 $qr_key = (string) get_post_meta( $order_id, 'ebook_key', true );
449 $qr_text = ebook_store_get_order_verification_url( $qr_key );
450 }
451 $out = ebook_store_watermark_epub(
452 $file,
453 $dest,
454 $watermark,
455 array(
456 'scope' => get_option( 'ebook_store_epub_watermark_scope', 'first' ),
457 'position' => get_option( 'ebook_store_epub_watermark_position', 'bottom' ),
458 'style' => get_option( 'ebook_store_epub_watermark_style', 'subtle' ),
459 'stamp_metadata' => get_option( 'ebook_store_epub_stamp_metadata' ),
460 'qr' => get_option( 'ebook_store_epub_qr' ),
461 'qr_scope' => get_option( 'ebook_store_epub_qr_scope', 'first' ),
462 'qr_position' => get_option( 'ebook_store_epub_qr_position', 'bottom' ),
463 'qr_text' => $qr_text,
464 'order_id' => (int) $order_id,
465 )
466 );
467 if ( $out === $dest && file_exists( $dest ) ) {
468 if ( $order_id > 0 ) {
469 update_post_meta( $order_id, 'encrypted_epub', wp_slash( $dest ) );
470 }
471 return $dest;
472 }
473 return $this->files[$format]; // fail-open: engine returned the source
474 } else {
475 return $this->files[$format];
476 }
477 }
478 function password_desc() {
479 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
480 $locale = ebook_store_get_locale_strings();
481 $formats_locale = ebook_store_get_format_labels();
482 if (get_option('encrypt_pdf') != 1) {
483 return '';
484 }
485 $context = $this->password_order_context();
486 $password = '';
487 if (function_exists('ebook_store_get_password_for_order')) {
488 $password = ebook_store_get_password_for_order($context['order_id'], array(
489 'payer_email' => $context['payer_email'],
490 'billing_email' => $context['billing_email'],
491 'fallback_email' => $context['fallback_email'],
492 ));
493 }
494 if ($password === '' && intval(get_option('ebook_store_blank_password')) === 1) {
495 return '';
496 }
497 if ($password === '' && $context['fallback_email'] !== '' && intval(get_option('ebook_store_random_password')) !== 1) {
498 $password = $context['fallback_email'];
499 }
500 if ($password === '' && intval(get_option('ebook_store_random_password')) !== 1) {
501 return $locale['password_desc'] . ': ' . __('your email address', 'ebook-store');
502 }
503 if ($password === '') {
504 return '';
505 }
506 return $locale['password_desc'] . ': ' . $password;
507 }
508 function password() {
509 if (get_option('encrypt_pdf') != 1) {
510 return '';
511 }
512 $context = $this->password_order_context();
513 $password = '';
514 if (function_exists('ebook_store_get_password_for_order')) {
515 $password = ebook_store_get_password_for_order($context['order_id'], array(
516 'payer_email' => $context['payer_email'],
517 'billing_email' => $context['billing_email'],
518 'fallback_email' => $context['fallback_email'],
519 ));
520 }
521 if ($password === '' && intval(get_option('ebook_store_blank_password')) !== 1) {
522 $password = $context['fallback_email'];
523 }
524 if ($password !== '' || intval(get_option('ebook_store_blank_password')) === 1) {
525 return $password;
526 }
527 $meta = get_post_meta($this->order_id);
528 $meta = is_array($meta) ? $meta : array();
529 if (get_option('ebook_store_random_password') == 1) {
530 // Legacy derivation, kept only for orders whose PDF was encrypted with
531 // it before 6.00. New orders never reach here: the branches above
532 // return the secret stored under the 'password' meta key.
533 return substr(md5($this->order_id . substr(NONCE_KEY, 0, 8)), 0, 8);
534 } else if (get_option('ebook_store_blank_password') == 1) {
535 return '';
536 }
537 return isset($meta['_billing_email'][0]) ? $meta['_billing_email'][0] : '';
538 }
539 protected function password_order_context() {
540 $context = array(
541 'order_id' => intval($this->order_id),
542 'payer_email' => '',
543 'billing_email' => '',
544 'fallback_email' => '',
545 );
546 if ($context['order_id'] === 0 && !empty($this->wc_order) && function_exists('wc_get_order_id_by_order_key')) {
547 $context['order_id'] = wc_get_order_id_by_order_key($this->wc_order);
548 }
549 if ($context['order_id'] > 0) {
550 $context['payer_email'] = get_post_meta($context['order_id'],'payer_email',true);
551 $context['billing_email'] = get_post_meta($context['order_id'],'_billing_email',true);
552 }
553 if ($context['billing_email'] === '' && $context['order_id'] > 0 && function_exists('wc_get_order')) {
554 $order_obj = wc_get_order($context['order_id']);
555 if ($order_obj && method_exists($order_obj, 'get_billing_email')) {
556 $context['billing_email'] = $order_obj->get_billing_email();
557 }
558 }
559 if ($context['fallback_email'] === '') {
560 $context['fallback_email'] = $context['billing_email'] ?: $context['payer_email'];
561 }
562 return $context;
563 }
564 }
565 /**
566 * Absolute path to the protected per-order cache directory, without a trailing
567 * slash.
568 *
569 * Lives under wp-content/uploads, never inside the plugin folder: the generated
570 * PDF is watermarked with the buyer's email, transaction ID and IP address and
571 * must never be reachable over HTTP.
572 *
573 * @param int|string $order_id Order post ID.
574 * @return string Absolute path, or '' when the directory cannot be created.
575 */
576 function ebook_store_get_cache_dir( $order_id ) {
577 $uploads = wp_upload_dir();
578 if ( ! empty( $uploads['error'] ) ) {
579 return '';
580 }
581
582 $base = untrailingslashit( $uploads['basedir'] ) . '/ebook-store-cache';
583
584 // The per-order hash is unchanged from earlier versions so that files moved
585 // out of the plugin folder are still found at the same relative location.
586 $dir = $base . '/' . md5( $order_id . substr( NONCE_KEY, 0, 8 ) );
587
588 if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
589 return '';
590 }
591
592 // Belt and braces: block direct HTTP access on both Apache and any server
593 // that respects a directory index.
594 if ( ! file_exists( $base . '/.htaccess' ) ) {
595 @file_put_contents( $base . '/.htaccess', "Require all denied\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n" );
596 }
597 if ( ! file_exists( $base . '/index.php' ) ) {
598 @file_put_contents( $base . '/index.php', "<?php\n// Silence is golden.\n" );
599 }
600
601 return $dir;
602 }
603 function ebook_store_human_filesize($bytes, $decimals = 2) {
604 $sz = 'BKMGTP';
605 $bytes = is_numeric($bytes) ? $bytes + 0 : 0;
606 // Clamp rather than suppress, see EbookStoreEbook::human_filesize().
607 $factor = (int) floor((strlen((string) $bytes) - 1) / 3);
608 $factor = max(0, min($factor, strlen($sz) - 1));
609 return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $sz[$factor];
610 }
611