Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Baking badges library.
  19   *
  20   * @package    core
  21   * @subpackage badges
  22   * @copyright  2012 onwards Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Information on PNG file chunks can be found at http://www.w3.org/TR/PNG/#11Chunks
  31   * Some other info on PNG that I used http://garethrees.org/2007/11/14/pngcrush/
  32   *
  33   * Example of use:
  34   * $png = new PNG_MetaDataHandler('file.png');
  35   *
  36   * if ($png->check_chunks("tEXt", "openbadge")) {
  37   *     $newcontents = $png->add_chunks("tEXt", "openbadge", 'http://some.public.url/to.your.assertion.file');
  38   * }
  39   *
  40   * file_put_contents('file.png', $newcontents);
  41   */
  42  
  43  class PNG_MetaDataHandler
  44  {
  45      /** @var string File content as a string */
  46      private $_contents;
  47      /** @var int Length of the image file */
  48      private $_size;
  49      /** @var array Variable for storing parsed chunks */
  50      private $_chunks;
  51  
  52      /**
  53       * Prepares file for handling metadata.
  54       * Verifies that this file is a valid PNG file.
  55       * Unpacks file chunks and reads them into an array.
  56       *
  57       * @param string $contents File content as a string
  58       */
  59      public function __construct($contents) {
  60          $this->_contents = $contents;
  61          $png_signature = pack("C8", 137, 80, 78, 71, 13, 10, 26, 10);
  62  
  63          // Read 8 bytes of PNG header and verify.
  64          $header = substr($this->_contents, 0, 8);
  65  
  66          if ($header != $png_signature) {
  67              debugging('This is not a valid PNG image');
  68          }
  69  
  70          $this->_size = strlen($this->_contents);
  71  
  72          $this->_chunks = array();
  73  
  74          // Skip 8 bytes of IHDR image header.
  75          $position = 8;
  76          do {
  77              $chunk = @unpack('Nsize/a4type', substr($this->_contents, $position, 8));
  78              $this->_chunks[$chunk['type']][] = substr($this->_contents, $position + 8, $chunk['size']);
  79  
  80              // Skip 12 bytes chunk overhead.
  81              $position += $chunk['size'] + 12;
  82          } while ($position < $this->_size);
  83      }
  84  
  85      /**
  86       * Checks if a key already exists in the chunk of said type.
  87       * We need to avoid writing same keyword into file chunks.
  88       *
  89       * @param string $type Chunk type, like iTXt, tEXt, etc.
  90       * @param string $check Keyword that needs to be checked.
  91       *
  92       * @return boolean (true|false) True if file is safe to write this keyword, false otherwise.
  93       */
  94      public function check_chunks($type, $check) {
  95          if (array_key_exists($type, $this->_chunks)) {
  96              foreach (array_keys($this->_chunks[$type]) as $typekey) {
  97                  list($key, $data) = explode("\0", $this->_chunks[$type][$typekey]);
  98  
  99                  if (strcmp($key, $check) == 0) {
 100                      debugging('Key "' . $check . '" already exists in "' . $type . '" chunk.');
 101                      return false;
 102                  }
 103              }
 104          }
 105          return true;
 106      }
 107  
 108      /**
 109       * Adds a chunk with keyword and data to the file content.
 110       * Chunk is added to the end of the file, before IEND image trailer.
 111       *
 112       * @param string $type Chunk type, like iTXt, tEXt, etc.
 113       * @param string $key Keyword that needs to be added.
 114       * @param string $value Currently an assertion URL that is added to an image metadata.
 115       *
 116       * @return string $result File content with a new chunk as a string. Can be used in file_put_contents() to write to a file.
 117       * @throws \moodle_exception when unsupported chunk type is defined.
 118       */
 119      public function add_chunks($type, $key, $value) {
 120          if (strlen($key) > 79) {
 121              debugging('Key is too big');
 122          }
 123  
 124          $dataparts = [];
 125          if ($type === 'iTXt') {
 126              // International textual data (iTXt).
 127              // Keyword:             1-79 bytes (character string).
 128              $dataparts[] = $key;
 129              // Null separator:      1 byte.
 130              $dataparts[] = "\x00";
 131              // Compression flag:    1 byte
 132              // A value of 0 means no compression.
 133              $dataparts[] = "\x00";
 134              // Compression method:  1 byte
 135              // If compression is disabled, the method should also be 0.
 136              $dataparts[] = "\x00";
 137              // Language tag:        0 or more bytes (character string)
 138              // When there is no language specified leave empty.
 139  
 140              // Null separator:      1 byte.
 141              $dataparts[] = "\x00";
 142              // Translated keyword:  0 or more bytes
 143              // When there is no translation specified, leave empty.
 144  
 145              // Null separator:      1 byte.
 146              $dataparts[] = "\x00";
 147              // Text:                0 or more bytes.
 148              $dataparts[] = $value;
 149          } else if ($type === 'tEXt') {
 150              // Textual data (tEXt).
 151              // Keyword:             1-79 bytes (character string).
 152              $dataparts[] = $key;
 153              // Null separator:      1 byte.
 154              $dataparts[] = "\0";
 155              // Text:                n bytes (character string).
 156              $dataparts[] = $value;
 157          } else {
 158              throw new \moodle_exception('Unsupported chunk type: ' . $type);
 159          }
 160  
 161          $data = implode($dataparts);
 162  
 163          $crc = pack("N", crc32($type . $data));
 164          $len = pack("N", strlen($data));
 165  
 166          // Chunk format: length + type + data + CRC.
 167          // CRC is a CRC-32 computed over the chunk type and chunk data.
 168          $newchunk = $len . $type . $data . $crc;
 169          $this->_chunks[$type] = $data;
 170  
 171          $result = substr($this->_contents, 0, $this->_size - 12)
 172                  . $newchunk
 173                  . substr($this->_contents, $this->_size - 12, 12);
 174  
 175          return $result;
 176      }
 177  }